#include <RotaryEncoder.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include "PwmTimer1.hpp"
#include "Button_SL.hpp"

constexpr byte MAX_HERTZ{15};
constexpr byte MIN_HERTZ{1};
constexpr byte DEFAULT_DUTY{50};
constexpr byte pin_in1{2};
constexpr byte pin_in2{3};
constexpr byte pin_btn{4};
constexpr byte pin_led{13};

#ifndef LCD20x4
constexpr byte I2C_ADDR{0x27};
constexpr byte LCD_COLUMNS{16};
constexpr byte LCD_LINES{2};
#else
constexpr byte I2C_ADDR{0x3F};
constexpr byte LCD_COLUMNS{20};
constexpr byte LCD_LINES{4};
#endif

enum class ProgState : byte { adjust = 0, start, enable, running, idle };
enum class InputState : byte { hertz = 0, duty, save };

struct EncoderData {
  byte hertz;
  byte dutyc;
};

constexpr byte INDICATOR_CHAR{'@'};
constexpr int EE_BASE_ADDRESS{0};

struct EEPROMData {
  const byte ichar{INDICATOR_CHAR};
  byte hertz;
  byte dutyc;
};

using namespace Btn;
ButtonSL btn{pin_btn};

// RotaryEncoder encoder(pin_in1, pin_in2, RotaryEncoder::LatchMode::FOUR0);
// RotaryEncoder encoder(pin_in1, pin_in2, RotaryEncoder::LatchMode::TWO03);
RotaryEncoder encoder(pin_in1, pin_in2, RotaryEncoder::LatchMode::FOUR3);
LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);
EncoderData encData{MIN_HERTZ, DEFAULT_DUTY};

void readEEPROM(int, EncoderData &);
void writeEEPROM(int, EncoderData &);
bool askEncoder(EncoderData &, InputState);
void outputLcd(char *, EncoderData &, InputState);

void setup() {
  Serial.begin(115200);
  Serial.println("Start");

  btn.begin();
  btn.releaseOn();
  btn.setDebounceTime_ms(150);
  lcd.init();
  lcd.backlight();
  readEEPROM(EE_BASE_ADDRESS, encData);
  Pwm.begin(encData.hertz, encData.dutyc);
}

void loop() {
  static bool lastBtnAction{false};
  static char buffer[LCD_COLUMNS + 1];   // Achtung: buffer an Textlänge + 1 anpassen. Sonst gibt es komische Effekte.
  static ProgState state{ProgState::adjust};
  static InputState inState{InputState::hertz};

  if (askEncoder(encData, inState)) { state = ProgState::adjust; }
  switch (state) {
    case ProgState::adjust:
      Pwm.stop();
      buffer[0] = ' ';
      outputLcd(buffer, encData, inState);
      state = ProgState::idle;
      break;
    case ProgState::start:
      buffer[0] = 'R';
      lcd.setCursor(0, 1);
      lcd.print(buffer[0]);
      state = ProgState::enable;
      [[fallthrough]]   // https://en.cppreference.com/w/cpp/language/attributes/fallthrough
    case ProgState::enable: 
      Pwm.setHz(encData.hertz);
      Pwm.setDutyCycle(encData.dutyc);
      Pwm.start();
      state = ProgState::running;
      break;
    default: break;
  }

  // Tasterabfrage
  switch (btn.tick()) {
    case ButtonState::longPressed:
      if (inState == InputState::save) {
        writeEEPROM(EE_BASE_ADDRESS, encData);
        inState = InputState::hertz;
        outputLcd(buffer, encData, inState);
      } else {
        lastBtnAction = !lastBtnAction;
        state = (lastBtnAction) ? ProgState::start : ProgState::adjust;   // Will switch PWM on/off
      }
      break;
    case ButtonState::shortPressed:
      switch (inState) {
        case InputState::hertz: inState = InputState::duty; break;
        case InputState::duty: inState = (state != ProgState::running) ? InputState::save : InputState::hertz; break;
        case InputState::save: inState = InputState::hertz; break;
        default: break;
      }
      outputLcd(buffer, encData, inState);
      break;
    default: break;
  }
}

bool askEncoder(EncoderData &data, InputState inState) {
  encoder.tick();
  RotaryEncoder::Direction direction = encoder.getDirection();
  // Wenn der Encoder gar nicht bewegt wurde, Funktion sofort wieder verlassen.
  if (direction == RotaryEncoder::Direction::NOROTATION) {
    return false;
  }

  // Ansonsten den jeweils ausgewählten Wert einstellen.
  switch (inState) {
    case InputState::hertz:
      if (direction == RotaryEncoder::Direction::CLOCKWISE) {
        data.hertz = (++data.hertz > MAX_HERTZ) ? MIN_HERTZ : data.hertz; 
      } else if (direction == RotaryEncoder::Direction::COUNTERCLOCKWISE) {
        data.hertz = (--data.hertz < MIN_HERTZ) ? MAX_HERTZ : data.hertz; 
      }
      break;
    case InputState::duty:
      if (direction == RotaryEncoder::Direction::CLOCKWISE) {
        data.dutyc = (++data.dutyc > 100) ? 0 : data.dutyc;   // 100% or less
      } else if (direction == RotaryEncoder::Direction::COUNTERCLOCKWISE) {
        data.dutyc = (--data.dutyc > 100) ? 100 : data.dutyc;   // Unsigned! -1 becomes 255
      }
      break;
    default: break;
  }
  return true;
}

void outputLcd(char *text, EncoderData &ed, InputState is) {
  bool isRunning = (text[0] == 'R') ? true : false;
  sprintf(text, "%3d Hz  Tv: %3d%%", ed.hertz, ed.dutyc);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(text);
  memset(text, 32, LCD_COLUMNS); // 32 = Ascii code for the space character
  text[LCD_COLUMNS + 1] = '\0';
  switch (is) {
    case InputState::hertz: text[2] = '^'; break;
    case InputState::duty: text[14] = '^'; break;
    case InputState::save: text[0] = 'S'; break;
    default: break;
  }
  if (isRunning) { text[0] = 'R'; }
  lcd.setCursor(0, 1);
  lcd.print(text);
}

void readEEPROM(int eeAddr, EncoderData &ed) {
  EEPROMData readData;
  if (EEPROM.read(eeAddr) != readData.ichar) {   // Executed if no data has ever been saved.
    readData.hertz = MIN_HERTZ;
    readData.dutyc = DEFAULT_DUTY;
    EEPROM.put(eeAddr, readData);
  } else {
    EEPROM.get(eeAddr, readData);   // If data has been saved, read it out.
  }
  ed.hertz = readData.hertz;
  ed.dutyc = readData.dutyc;
}

void writeEEPROM(int eeAddr, EncoderData &ed) {
  EEPROMData readData;
  readData.hertz = ed.hertz;
  readData.dutyc = ed.dutyc;
  EEPROM.put(eeAddr, readData);
}
$abcdeabcde151015202530fghijfghij