#include <U8g2lib.h>
#include <RotaryEncoder.h>
#include "Yabl.hpp"
#include "KitchenTimer.hpp"
#include "AlarmTone.hpp"

//#define DISPLAY_Y32     // Remove the comment if the display has only 32 instead of 64 pixel lines 
#define MINUTES_DEFAULT   // Remove the comment if you want the time setting to start with the minutes. 

//
// gobal constants
//
constexpr unsigned int SECOND{1000};   // 1000ms = 1 Second (If the clock runs too slowly, the value can be reduced a little.)
constexpr byte BUFFERLENGTH{6};        // 5 characters + end-of-string character '\0'.

constexpr byte DISPLAY_MAX_X{128};
#ifndef DISPLAY_Y32
constexpr byte DISPLAY_MAX_Y{64};
#else
constexpr byte DISPLAY_MAX_Y{32};
#endif

// Font u8g2_font_freedoomr25_mn   // 19 Width 26 Height
// Font u8g2_font_logisoso42_tn    // 24 Width 51 Hight
constexpr byte FONT_WIDTH{24};
constexpr byte FONT_HIGHT{51};

// The following display values are calculated from the upper four values. No change necessary.
constexpr byte DISPLAY_X{(DISPLAY_MAX_X - FONT_WIDTH * (BUFFERLENGTH - 1)) / 2};   // Column = X Coordinate
constexpr byte DISPLAY_Y{(DISPLAY_MAX_Y + FONT_WIDTH) / 2};                        // Row = Y coordinate
constexpr byte MINUTES_LINE_X{DISPLAY_X};   // LINE = Coordinates for the line under minute and second digits
constexpr byte SECONDS_LINE_X{DISPLAY_X + FONT_WIDTH * 3};
constexpr byte LINE_Y{DISPLAY_Y + 2};        // Line below the numbers
constexpr byte LINE_WIDTH{FONT_WIDTH * 2};   // Line length = font width * 2

constexpr byte PIN_IN1{2};
constexpr byte PIN_IN2{3};
constexpr byte PIN_BTN{4};
constexpr byte PIN_ALARM{13};

constexpr unsigned int NOTE_F6{1397};
constexpr unsigned int NOTE_A6{1760};

//
// Global objects / variables
//
struct InputState {
  enum class state : byte { seconds = 0, minutes };
  RotaryEncoder encoder{PIN_IN1, PIN_IN2, RotaryEncoder::LatchMode::FOUR3};
#ifndef MINUTES_DEFAULT
  const state defaultState{state::seconds};
  state lastState{state::minutes};
#else
  const state defaultState{state::minutes};
  state lastState{state::seconds};
#endif
  state currentState{defaultState};
} input;

#ifndef DISPLAY_Y32
U8G2_SSD1306_128X64_NONAME_2_HW_I2C u8g2(U8G2_R0);
#else
U8G2_SSD1306_128X32_UNIVISION_2_HW_I2C u8g2(U8G2_R0);
#endif
KitchenTimer ktTimer;
AlarmTone alarm{PIN_ALARM};
using namespace Yabl;
Button btn{PIN_BTN};

//
// Forward declaration function(s).
//
KitchenTimerState runTimer(KitchenTimer &);
bool askEncoder(RotaryEncoder &, KitchenTimer &);
bool processInput(KitchenTimer &, InputState &);
void displayTime(KitchenTimer &, bool);
void setDisplayForInput(KitchenTimer &kT, InputState &iS);
void askRtButton(Button &, KitchenTimer &, InputState &);

//////////////////////////////////////////////////////////////////////////////
/// @brief Initialization part of the main program
///
//////////////////////////////////////////////////////////////////////////////
void setup(void) {
  Serial.begin(115200);
  u8g2.begin();
  // u8g2.setFont(u8g2_font_logisoso28_tr);
  // u8g2.setFont(u8g2_font_7_Seg_33x19_mn);
  // u8g2.setFont(u8g2_font_freedoomr25_mn);   // 19 Width 26 Hight
  u8g2.setFont(u8g2_font_logisoso42_tn);   // 24 Width 51 Hight

  btn.begin();
  btn.setAutoRelease_ms(1000);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief main program
///
//////////////////////////////////////////////////////////////////////////////
void loop() {
  KitchenTimerState ktState{ktTimer.getState()};
  switch (ktState) {
    case KitchenTimerState::active: runTimer(ktTimer); break;
    case KitchenTimerState::off: processInput(ktTimer, input); break;
    case KitchenTimerState::alarm:
      alarm.playAlarm();
      if (btn() != ButtonState::released) {   // Switch alarm off with encoder button
        setDisplayForInput(ktTimer, input);
      }
      if (askEncoder(input.encoder, ktTimer)) {   // Switch alarm off with encoder rotation
        ktTimer.setSeconds(0);                    // Reset count from rotation
        setDisplayForInput(ktTimer, input);
      }
      break;
  }
  // If the alarm is active, only the encoder query in the switch instruction may be active.
  if (ktState != KitchenTimerState::alarm) { askRtButton(btn, ktTimer, input); }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief The set time is continuously counted down
///        by 1 per second until the value is 0.
///
/// @param kT Reference on kitchen timer object
/// @return KitchenTimerState
//////////////////////////////////////////////////////////////////////////////
KitchenTimerState runTimer(KitchenTimer &kT) {
  if (kT(SECOND)) {
    --kT;
    switch (kT.timeIsUp()) {
      case false: kT.start(); break;
      case true: kT.setState(KitchenTimerState::alarm); break;
    }
    displayTime(kT, false);
  }
  return kT.getState();
}

//////////////////////////////////////////////////////////////////////////////
/// @brief The encoder signals are evaluated
///
/// @param enc Reference on encoder object
/// @param kT  Reference on kitchen timer object
/// @return true  if the an encoder signal was evaluated
/// @return false if no encoder signal was evaluated
//////////////////////////////////////////////////////////////////////////////
bool askEncoder(RotaryEncoder &enc, KitchenTimer &kT) {
  byte flag{true};
  enc.tick();
  switch (enc.getDirection()) {
    case RotaryEncoder::Direction::NOROTATION: flag = false; break;
    case RotaryEncoder::Direction::CLOCKWISE: ++kT; break;
    case RotaryEncoder::Direction::COUNTERCLOCKWISE: --kT; break;
  }
  return flag;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Control the inputs and set the input states
///
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
/// @return true when the encoder has been actuated
/// @return false if no encoder operation has occurred
//////////////////////////////////////////////////////////////////////////////
bool processInput(KitchenTimer &kT, InputState &iS) {
  bool encoderActuated{false};
  if (iS.lastState != iS.currentState) {
    switch (iS.currentState) {
      case InputState::state::seconds: kT.setUnitSeconds(); break;
      case InputState::state::minutes: kT.setUnitMinutes(); break;
    }
    iS.lastState = iS.currentState;
    displayTime(kT, true);
  }
  if (askEncoder(iS.encoder, ktTimer)) {
    switch (ktTimer.getState()) {
      case KitchenTimerState::alarm: ktTimer.setState(KitchenTimerState::off); break;
      default: displayTime(kT, true); break;
    }
    encoderActuated = true;
  }
  return encoderActuated;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Set the correct input status for the display indication
///
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
//////////////////////////////////////////////////////////////////////////////
void setDisplayForInput(KitchenTimer &kT, InputState &iS) {
  ktTimer.setState(KitchenTimerState::off);
  iS.lastState =
      (iS.defaultState == InputState::state::seconds) ? InputState::state::minutes : InputState::state::seconds;
  iS.currentState = iS.defaultState;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Write the two time units into a string and output the string
///        on the display.
///
/// @param kT Reference on kitchen timer object
/// @param showLine If true, a line will be displayed under the digits active
///                 for the input. If false, then no line is displayed.
//////////////////////////////////////////////////////////////////////////////
void displayTime(KitchenTimer &kT, bool showLine) {
  char charBuffer[BUFFERLENGTH];
  sprintf(charBuffer, "%02d:%02d", kT.getMinutes(), kT.getSeconds());
  u8g2.firstPage();
  do {
    u8g2.drawStr(DISPLAY_X, DISPLAY_Y, charBuffer);   // Output string on the display.
    if (showLine) {
      if (kT.getActiveUnit() == ActiveUnit::seconds) {
        u8g2.drawHLine(SECONDS_LINE_X, LINE_Y, LINE_WIDTH);
      } else {
        u8g2.drawHLine(MINUTES_LINE_X, LINE_Y, LINE_WIDTH);
      }
    }
  } while (u8g2.nextPage());
}


//////////////////////////////////////////////////////////////////////////////
/// @brief Query of the encoder's button function
///
/// @param b Reference on button object
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
//////////////////////////////////////////////////////////////////////////////
void askRtButton(Button &b, KitchenTimer &kT, InputState &iS) {
  switch (b()) {
    // If the button is pressed for a long time, it switches between timer active and timer off.
    case ButtonState::released: break;
    case ButtonState::pressedLong:
      if (!kT.timeIsUp()) {   // Switch on timer only if a time iS set.
        tone(PIN_ALARM, NOTE_A6, 30);
        switch (kT.getState()) {
          case KitchenTimerState::active: setDisplayForInput(ktTimer, input); break;
          case KitchenTimerState::off:
            kT.setState(KitchenTimerState::active);
            kT.setUnitSeconds();
            displayTime(kT, false);   // Delete underline
            kT.start();               // Start the countdown
            break;
          case KitchenTimerState::alarm: break;
        }
      }
      break;
    case ButtonState::pressed:
      if (kT.getState() == KitchenTimerState::active) { break; }
      switch (iS.currentState) {
        case InputState::state::seconds:
          tone(PIN_ALARM, NOTE_F6, 30);
          iS.currentState = InputState::state::minutes;
          break;
        case InputState::state::minutes:
          tone(PIN_ALARM, NOTE_F6, 30);
          iS.currentState = InputState::state::seconds;
          break;
        default: break;
      }   // inner switch
      break;
    default: break;
  }
}


/*
//////////////////////////////////////////////////////////////////////////////
/// @brief Query of the encoder's button function
///
/// @param b Reference on button object
/// @param kT Reference on kitchen timer object
/// @param iS Reference on input state structure
//////////////////////////////////////////////////////////////////////////////
void askRtButton(Button &b, KitchenTimer &kT, InputState &iS) {
  uint32_t duration;
  ButtonState isReleased = b();
  if (b.hasNewState() && isReleased) {
     duration = b.getDuration_ms();
     b.resetDuration();
    Serial.println(duration);
  } else {
    return;
  }

  switch (duration) {
    case 0 ... 699:
      if (kT.getState() == KitchenTimerState::active) { break; }
      switch (iS.currentState) {
        case InputState::state::seconds:
          tone(PIN_ALARM, NOTE_F6, 30);
          iS.currentState = InputState::state::minutes;
          break;
        case InputState::state::minutes:
          tone(PIN_ALARM, NOTE_F6, 30);
          iS.currentState = InputState::state::seconds;
          break;
        default: break;
      }   // inner switch
      break;
    default:   // If the button is pressed for a long time, it switches between timer active and timer off.
      if (!kT.timeIsUp()) {   // Switch on timer only if a time iS set.
        tone(PIN_ALARM, NOTE_A6, 30);
        switch (kT.getState()) {
          case KitchenTimerState::active: setDisplayForInput(ktTimer, input); break;
          case KitchenTimerState::off:
            kT.setState(KitchenTimerState::active);
            kT.setUnitSeconds();
            displayTime(kT, false);   // Delete underline
            kT.start();               // Start the countdown
            break;
          case KitchenTimerState::alarm: break;
        }
      }
      break;
  }
}
*/
$abcdeabcde151015202530fghijfghij