//////////////////////////////////////////////////////////////////////////////
/// @file main.cpp
/// @author Kai R. ()
/// @brief Example program for minimum quantity lubrication
/// 
/// @date 2023-02-15
/// @version 1.0.0
/// 
/// @copyright Copyright (c) 2023
/// 
//////////////////////////////////////////////////////////////////////////////

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

// Comment out the following #define if you are using an I2C(HW) Display.
// #define U8G2_SPI_DISPLAY

//
// Global constant definitions
//
constexpr uint8_t PIN_IN1 {2};   // Encoder pin (counter clockwise)
constexpr uint8_t PIN_IN2 {3};   // Encoder pin (clockwise)
constexpr uint8_t PIN_BTN {4};   // Encoder button

constexpr uint8_t MENUTXT_X {20};   // Column = X coordinate Text
constexpr uint8_t MENUTXT_Y {10};   // Row = Y coordinate Text
constexpr uint8_t MENUTXT_Y_OFFSET {17};

constexpr uint8_t MENUCSR_X {5};           // Column = X coordinate cursor
constexpr uint8_t MENUCSR_Y {MENUTXT_Y};   // Row = Y coordinate cursor
constexpr uint8_t MAX_MENU_ITEMS {4};      // Number of menu items
constexpr uint8_t DRAWCOLOR_INVERS {2};    // Inverted text display

constexpr uint8_t DATA_BUFFER_SIZE {4};   // Textbuffersize for data values (Hertz / Dutycycle)

constexpr uint8_t HERTZ {1};             // Default frequency in Hertz
constexpr uint8_t MAX_HERTZ {15};        // Maximum frequency
constexpr uint8_t MIN_HERTZ {1};         // Minimum frequency
constexpr uint8_t DUTYCYCLE {50};        // Default duty cycle
constexpr uint8_t MIN_DUTYCYCLE {0};     // Minimum duty cycle
constexpr uint8_t MAX_DUTYCYCLE {100};   // Maximum duty cycle

// Indicator character forces whether values have already been stored in the EEProm.
constexpr uint8_t INDICATOR_CHAR {'@'};
constexpr int EE_BASE_ADDRESS {0};   // Start address of the data storage area

struct Point {
  const uint8_t x;
  const uint8_t y;
};

struct PwmData {
  const uint8_t ichar;   // Indicator character
  uint8_t hertz;
  const uint8_t minHertz;
  const uint8_t maxHertz;
  uint8_t dutyCycle;
  const uint8_t minDutyCycle;
  const uint8_t maxDutyCycle;
};

struct MenuEntryType {
  Point coord;
  const char *text;
  const char *textAction;
};

//
// Data structure for menu display and data input control
//
struct MenuInput {
  // Methods
  bool askEncoder(uint8_t &, uint8_t, uint8_t);
  void resetOnValueChange();
  void displayMenue();

  // Data
  uint8_t curIndex {0};
  const uint8_t maxIndex {MAX_MENU_ITEMS - 1};

  bool isMenuActive {true};
  bool save {false};
  bool pwmActive {false};

  PwmData pwm {INDICATOR_CHAR, HERTZ, MIN_HERTZ, MAX_HERTZ, DUTYCYCLE, MIN_DUTYCYCLE, MAX_DUTYCYCLE};
#ifndef U8G2_SPI_DISPLAY
  U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2 {U8G2_R0, U8X8_PIN_NONE};   // Display object
#else
  U8G2_SSD1306_128X64_NONAME_1_4W_HW_SPI u8g2 {U8G2_R0, /* cs=*/10, /* dc=*/7, /* reset=*/8};
#endif
  Btn::ButtonSL btn {PIN_BTN};

private:
  const Point curCoords[MAX_MENU_ITEMS] {
      {MENUCSR_X, MENUCSR_Y                       },
      {MENUCSR_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 1},
      {MENUCSR_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 2},
      {MENUCSR_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 3}
  };

  const Point dataCoords[2] {
      {MENUTXT_X + 80, MENUCSR_Y                       },
      {MENUTXT_X + 80, MENUCSR_Y + MENUTXT_Y_OFFSET * 1}
  };
  // clang-format off
  MenuEntryType const menuEntryList[MAX_MENU_ITEMS] {  // Coordinates and text for the menu display
    {{MENUTXT_X, MENUCSR_Y},  "Frequenz :", ""},
    {{MENUTXT_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 1}, "Tastverh.:", ""},
    {{MENUTXT_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 2}, "Speichern", "Gespeichert"},
    {{MENUTXT_X, MENUCSR_Y + MENUTXT_Y_OFFSET * 3}, "Start", "Stopp"}
  };

  // clang-format on

  RotaryEncoder encoder {PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::FOUR3};
  char cursor[2] {">"};
  char textBuffer[DATA_BUFFER_SIZE];

} mInput;

//
// (Struct) Method for querying the rotary encoder
//
bool MenuInput::askEncoder(uint8_t &value, uint8_t minValue, uint8_t maxValue) {
  bool flag {true};
  encoder.tick();
  switch (encoder.getDirection()) {
    case RotaryEncoder::Direction::NOROTATION: flag = false; break;
    case RotaryEncoder::Direction::CLOCKWISE: value = (++value > maxValue) ? minValue : value; break;
    case RotaryEncoder::Direction::COUNTERCLOCKWISE:
      --value;
      value = (value < minValue || value > maxValue) ? maxValue : value;
      break;
  }
  return flag;
}

//
// When the frequency/duty cycle value changes, the states for "stored" and "stop" also change.
// Pwm must be switched off
//
void MenuInput::resetOnValueChange() {
  save = false;
  pwmActive = false;
  Pwm.stop();
}

//
// (Struct) Method for Text output on the display
//
void MenuInput::displayMenue() {
  u8g2.firstPage();
  do {
    for (uint8_t i = 0; i <= maxIndex; ++i) {
      u8g2.setCursor(menuEntryList[i].coord.x, menuEntryList[i].coord.y);
      // Toggle text to match the action
      if ((save && i == 2) || (pwmActive && i == 3)) {
        u8g2.print(menuEntryList[i].textAction);
      } else {
        u8g2.print(menuEntryList[i].text);
      }
    }
    u8g2.setCursor(dataCoords[0].x, dataCoords[0].y);
    sprintf(textBuffer, "%3d", pwm.hertz);
    u8g2.print(textBuffer);
    u8g2.setCursor(dataCoords[1].x, dataCoords[1].y);
    sprintf(textBuffer, "%3d", pwm.dutyCycle);
    u8g2.print(textBuffer);
    u8g2.setCursor(curCoords[curIndex].x, curCoords[curIndex].y);
    u8g2.print(cursor);
    if (!isMenuActive) {
      uint8_t charHeight = u8g2.getMaxCharHeight();
      u8g2.setDrawColor(DRAWCOLOR_INVERS);
      u8g2.drawBox(2, curCoords[curIndex].y - (charHeight - 2), 126, charHeight);
    }
  } while (u8g2.nextPage());
}

//
// Function forward declarations
//
bool menuControl(MenuInput &);
void readEEPROM(int, PwmData &);
void writeEEPROM(int, PwmData &);

//
// Data input control
// Returns true if the screen content needs to be refreshed
//
bool menuControl(MenuInput &mI) {
  bool screenContentChanged = true;
  switch (mI.curIndex) {
    case 0:   // Menu item frequency (Hertz)
      if (mI.askEncoder(mI.pwm.hertz, mI.pwm.minHertz, mI.pwm.maxHertz)) {
        mI.resetOnValueChange();
      } else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
        mI.isMenuActive = true;
      } else {
        screenContentChanged = false;
      }
      break;
    case 1:   // Menu item duty cycle
      if (mI.askEncoder(mI.pwm.dutyCycle, mI.pwm.minDutyCycle, mI.pwm.maxDutyCycle)) {
        mI.resetOnValueChange();
      } else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
        mI.isMenuActive = true;
      } else {
        screenContentChanged = false;
      }
      break;
    case 2:   // Menu item save/saved
      if (mI.askEncoder(mI.curIndex, 0, mI.maxIndex)) {
        mI.isMenuActive = true;
      } else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
        writeEEPROM(EE_BASE_ADDRESS, mI.pwm);
        mI.save = true;
        mI.isMenuActive = true;
      } else {
        screenContentChanged = false;
      }
      break;
    case 3:   // Menu item start/stop
      if (mI.askEncoder(mI.curIndex, 0, mI.maxIndex)) {
        mI.isMenuActive = true;
      } else if (mI.btn.tick() != Btn::ButtonState::notPressed) {
        mI.pwmActive = !mI.pwmActive;
        if (mI.pwmActive) {
          Pwm.setHz(mI.pwm.hertz);
          Pwm.setDutyCycle(mI.pwm.dutyCycle);
          Pwm.start();
        } else {
          Pwm.stop();
        }
        mI.isMenuActive = true;
      } else {
        screenContentChanged = false;
      }
      break;
  }
  return screenContentChanged;
}

//
// Reading pwm data from EEProm
//
void readEEPROM(int eeAddr, PwmData &pd) {
  if (EEPROM.read(eeAddr) != pd.ichar) {   // Executed if no data has ever been saved.
    EEPROM.put(eeAddr, pd);
  } else {
    EEPROM.get(eeAddr, pd);   // If data has been saved, read it out.
  }
}

//
// Write pwm data to EEProm
//
void writeEEPROM(int eeAddr, PwmData &pd) { EEPROM.put(eeAddr, pd); }

//
// Setup
//
void setup(void) {
  Serial.begin(115200);

  mInput.btn.begin();
  mInput.btn.setDebounceTime_ms(100);
  mInput.u8g2.begin();
  mInput.u8g2.setFont(u8g2_font_6x13B_mr);

  readEEPROM(EE_BASE_ADDRESS, mInput.pwm);
  Pwm.begin(mInput.pwm.hertz, mInput.pwm.dutyCycle);
}

//
// Main Program
//
void loop(void) {
  static bool refreshDisplay {true};
  
  if (refreshDisplay) {
    mInput.displayMenue();
    refreshDisplay = false;
  }

  // One button press toggles between menu item selection and data entry
  if (mInput.isMenuActive && mInput.btn.tick() != Btn::ButtonState::notPressed) {
    mInput.isMenuActive = !mInput.isMenuActive;
    refreshDisplay = true;
  }
  switch (mInput.isMenuActive) {
    case true:   // Menu control
      if (mInput.askEncoder(mInput.curIndex, 0, mInput.maxIndex)) { refreshDisplay = true; }
      break;
    case false:   // Data input
      if (menuControl(mInput)) { refreshDisplay = true; }
      break;
  }
}
$abcdeabcde151015202530fghijfghij