text: #pragma once
  
  #include "ESPRotary.h"
  #include <math.h>
  #include "wled.h"
  
  class RgbRotaryEncoderUsermod : public Usermod
  {
    private:
      bool enabled = false;
      bool initDone = false;
      bool isDirty = false;
      BusDigital *ledBus;
      /*
      * Green - eb - Q4 - 32
      * Red   - ea - Q1 - 15
      * Black - sw - Q2 - 12
      */
      ESPRotary *rotaryEncoder;
      int8_t ledIo = 3; // GPIO to control the LEDs
      int8_t eaIo = 15; // "ea" from RGB Encoder Board
      int8_t ebIo = 32; // "eb" from RGB Encoder Board
      byte stepsPerClick = 4; // How many "steps" your rotary encoder does per click. This varies per rotary encoder
      /* This could vary per rotary encoder: Usually rotary encoders have 20 "clicks".
        If yours has less/more, adjust this to: 100% = 20 LEDs * incrementPerClick */
      byte incrementPerClick = 5;
      byte ledMode = 3;
      byte ledBrightness = 64;
  
      // This is all needed to calculate the brightness, rotary position, etc.
      const byte minPos = 5; // minPos is not zero, because if we want to turn the LEDs off, we use the built-in button ;)
      const byte maxPos = 100; // maxPos=100, like 100%
      const byte numLeds = 20;
      byte lastKnownPos = 0;
  
      byte currentColors[3];
      byte lastKnownBri = 0;
  
  
      void initRotaryEncoder()
      {
        PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } };
        if (!pinManager.allocateMultiplePins(pins, 2, UM_RGBRotaryEncoder)) {
          eaIo = -1;
          ebIo = -1;
          cleanup();
          return;
        }
  
        // I don't know why, but setting the upper bound here does not work. It results into 1717922932 O_o
        rotaryEncoder = new ESPRotary(eaIo, ebIo, stepsPerClick, incrementPerClick, maxPos, currentPos, incrementPerClick);
        rotaryEncoder->setUpperBound(maxPos); // I have to again set it here and then it works / is actually 100...
  
        rotaryEncoder->setChangedHandler(RgbRotaryEncoderUsermod::cbRotate);
      }
  
      void initLedBus()
      {
        byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255};
        BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);
  
        ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
        if (!ledBus->isOk()) {
          cleanup();
          return;
        }
  
        ledBus->setBrightness(ledBrightness);
      }
  
      void updateLeds()
      {
        switch (ledMode) {
          case 2:
            {
              currentColors[0] = 255; currentColors[1] = 0; currentColors[2] = 0;
              for (int i = 0; i < currentPos / incrementPerClick - 1; i++) {
                ledBus->setPixelColor(i, 0);
              }
              ledBus->setPixelColor(currentPos / incrementPerClick - 1, colorFromRgbw(currentColors));
              for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
                ledBus->setPixelColor(i, 0);
              }
            }
            break;
  
          default:
          case 1:
          case 3:
            // WLED orange (of course), which we will use in mode 1
            currentColors[0] = 255; currentColors[1] = 160; currentColors[2] = 0;
            for (int i = 0; i < currentPos / incrementPerClick; i++) {
              if (ledMode == 3) {
                hsv2rgb((i) / float(numLeds), 1, .25);
              }
              ledBus->setPixelColor(i, colorFromRgbw(currentColors));
            }
            for (int i = currentPos / incrementPerClick; i < numLeds; i++) {
              ledBus->setPixelColor(i, 0);
            }
            break;
        }
  
        isDirty = true;
      }
  
      void cleanup()
      {
        // Only deallocate pins if we allocated them ;)
        if (eaIo != -1) {
          pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder);
          eaIo = -1;
        }
        if (ebIo != -1) {
          pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder);
          ebIo = -1;
        }
  
        delete rotaryEncoder;
        delete ledBus;
  
        enabled = false;
      }
  
      int getPositionForBrightness()
      {
        return int(((float)bri / (float)255) * 100);
      }
  
      float fract(float x) { return x - int(x); }
  
      float mix(float a, float b, float t) { return a + (b - a) * t; }
  
      void hsv2rgb(float h, float s, float v) {
        currentColors[0] = int((v * mix(1.0, constrain(abs(fract(h + 1.0) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);
        currentColors[1] = int((v * mix(1.0, constrain(abs(fract(h + 0.6666666) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);
        currentColors[2] = int((v * mix(1.0, constrain(abs(fract(h + 0.3333333) * 6.0 - 3.0) - 1.0, 0.0, 1.0), s)) * 255);
      }
  
    public:
      static byte currentPos;
  
      // strings to reduce flash memory usage (used more than twice)
      static const char _name[];
      static const char _enabled[];
      static const char _ledIo[];
      static const char _eaIo[];
      static const char _ebIo[];
      static const char _ledMode[];
      static const char _ledBrightness[];
      static const char _stepsPerClick[];
      static const char _incrementPerClick[];
  
  
      static void cbRotate(ESPRotary& r) {
        currentPos = r.getPosition();
      }
  
      /**
       * Enable/Disable the usermod
       */
      // inline void enable(bool enable) { enabled = enable; }
      /**
       * Get usermod enabled/disabled state
       */
      // inline bool isEnabled() { return enabled; }
  
      /*
        * setup() is called once at boot. WiFi is not yet connected at this point.
        * You can use it to initialize variables, sensors or similar.
        */
      void setup()
      {
        if (enabled) {
          currentPos = getPositionForBrightness();
          lastKnownBri = bri;
  
          initRotaryEncoder();
          initLedBus();
  
          // No updating of LEDs here, as that's sometimes not working; loop() will take care of that
  
          initDone = true;
        }
      }
  
      /*
        * loop() is called continuously. Here you can check for events, read sensors, etc.
        * 
        * Tips:
        * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection.
        *    Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker.
        * 
        * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds.
        *    Instead, use a timer check as shown here.
        */
      void loop()
      {
        if (!enabled || strip.isUpdating()) return;
  
        rotaryEncoder->loop();
  
        // If the rotary was changed
        if(lastKnownPos != currentPos) {
          lastKnownPos = currentPos;
  
          bri = min(int(round((2.55 * currentPos))), 255);
          lastKnownBri = bri;
  
          updateLeds();
          colorUpdated(NOTIFIER_CALL_MODE_DIRECT_CHANGE);
        }
  
        // If the brightness is changed not with the rotary, update the rotary
        if (bri != lastKnownBri) {
          currentPos = lastKnownPos = getPositionForBrightness();
          lastKnownBri = bri;
          rotaryEncoder->resetPosition(currentPos);
          updateLeds();
        }
  
        // Update LEDs here in loop to also validate that we can update/show
        if (isDirty && ledBus->canShow()) {
          isDirty = false;
          ledBus->show();
        }
      }
  
      void addToConfig(JsonObject &root)
      {
        JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname
  
        top[FPSTR(_enabled)] = enabled;
        top[FPSTR(_ledIo)] = ledIo;
        top[FPSTR(_eaIo)] = eaIo;
        top[FPSTR(_ebIo)] = ebIo;
        top[FPSTR(_ledMode)] = ledMode;
        top[FPSTR(_ledBrightness)] = ledBrightness;
        top[FPSTR(_stepsPerClick)] = stepsPerClick;
        top[FPSTR(_incrementPerClick)] = incrementPerClick;
      }
  
      /**
       * readFromConfig() is called before setup() to populate properties from values stored in cfg.json
       *
       * The function should return true if configuration was successfully loaded or false if there was no configuration.
       */
      bool readFromConfig(JsonObject &root)
      {
        JsonObject top = root[FPSTR(_name)];
        if (top.isNull()) {
          DEBUG_PRINTF("[%s] No config found. (Using defaults.)\n", _name);
          return false;
        }
  
        bool oldEnabled = enabled;
        int8_t oldLedIo = ledIo;
        int8_t oldEaIo =  eaIo;
        int8_t oldEbIo =  ebIo;
        byte oldLedMode = ledMode;
        byte oldStepsPerClick = stepsPerClick;
        byte oldIncrementPerClick = incrementPerClick;
        byte oldLedBrightness = ledBrightness;
  
        getJsonValue(top[FPSTR(_enabled)], enabled);
        getJsonValue(top[FPSTR(_ledIo)], ledIo);
        getJsonValue(top[FPSTR(_eaIo)], eaIo);
        getJsonValue(top[FPSTR(_ebIo)], ebIo);
        getJsonValue(top[FPSTR(_stepsPerClick)], stepsPerClick);
        getJsonValue(top[FPSTR(_incrementPerClick)], incrementPerClick);
        ledMode = top[FPSTR(_ledMode)] > 0 && top[FPSTR(_ledMode)] < 4 ? top[FPSTR(_ledMode)] : ledMode;
        ledBrightness = top[FPSTR(_ledBrightness)] > 0 && top[FPSTR(_ledBrightness)] <= 255 ? top[FPSTR(_ledBrightness)] : ledBrightness;
  
        if (!initDone) {
          // First run: reading from cfg.json
          // Nothing to do here, will be all done in setup() 
        }
        // Mod was disabled, so run setup()
        else if (enabled && enabled != oldEnabled) {
          DEBUG_PRINTF("[%s] Usermod has been re-enabled\n", _name);
          setup();
        }
        // Config has been changed, so adopt to changes
        else {
          if (!enabled) {
            DEBUG_PRINTF("[%s] Usermod has been disabled\n", _name);
            cleanup();
          }
          else {
            DEBUG_PRINTF("[%s] Usermod is enabled\n", _name);
            if (ledIo != oldLedIo) {
              delete ledBus;
              initLedBus();
            }
  
            if (ledBrightness != oldLedBrightness) {
              ledBus->setBrightness(ledBrightness);
              isDirty = true;
            }
  
            if (ledMode != oldLedMode) {
              updateLeds();
            }
  
            if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) {
              pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder);
              pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder);
              
              delete rotaryEncoder;
              initRotaryEncoder();
            }
          }
  
          DEBUG_PRINTF("[%s] Config (re)loaded\n", _name);
        }
        
        return true;
      }
  
      /*
        * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!).
        * This could be used in the future for the system to determine whether your usermod is installed.
        */
      uint16_t getId()
      {
        return 0x4711;
      }
  
      //More methods can be added in the future, this example will then be extended.
      //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class!
  };
  
  byte RgbRotaryEncoderUsermod::currentPos = 5;
  // strings to reduce flash memory usage (used more than twice)
  const char RgbRotaryEncoderUsermod::_name[]              PROGMEM = "RGB-Rotary-Encoder";
  const char RgbRotaryEncoderUsermod::_enabled[]           PROGMEM = "Enabled";
  const char RgbRotaryEncoderUsermod::_ledIo[]             PROGMEM = "LED-pin";
  const char RgbRotaryEncoderUsermod::_eaIo[]              PROGMEM = "ea-pin";
  const char RgbRotaryEncoderUsermod::_ebIo[]              PROGMEM = "eb-pin";
  const char RgbRotaryEncoderUsermod::_ledMode[]           PROGMEM = "LED-Mode";
  const char RgbRotaryEncoderUsermod::_ledBrightness[]     PROGMEM = "LED-Brightness";
  const char RgbRotaryEncoderUsermod::_stepsPerClick[]     PROGMEM = "Steps-per-Click";
  const char RgbRotaryEncoderUsermod::_incrementPerClick[] PROGMEM = "Increment-per-Click";