#include <LiquidCrystal_I2C.h>
#include "GenericQueue.h"
#include "ADebouncer.h"


LiquidCrystal_I2C myLCD(0x27, 20, 4);

#define cookingLEDPin 12
#define readyLEDPin 11
#define newLEDPin 10

#define debouncePeriod 10
#define blinkPeriod 500

#define numberOfInputs 4
const int inputPin[numberOfInputs] = {9, 8, 7, 6};

ADebouncer debouncer[numberOfInputs];

class Timer {
  private:
    uint32_t _duration;
    uint32_t _startTime;
    uint32_t _elapsed;
    bool _state;
    bool _enable;
  public:
    bool &state;
    uint32_t &elapsed;
    uint32_t &duration;

    Timer(): state(_state), elapsed(_elapsed), duration(_duration) {}
    void timeDelay(uint32_t duration) {
      _duration = duration;
    }
    bool timerOn(bool enable) {
      if (enable) {
        uint32_t currTime = millis();
        if (!_enable) {
          _startTime = currTime;
        }
        if (!_state) {
          _elapsed = currTime - _startTime;
          if (_elapsed >= _duration) {
            _elapsed = _duration;
            state = true;
          }
        }
      } else {
        _state = false;
      }
      _enable = enable;
      return state;
    }
};

class Menu {
  public:
    String name;
    uint32_t duration;

  public:
    Menu() {
      name = "";
      duration = 1000UL;
    }

    Menu(String const& name, uint32_t duration) {
      this->name = name;
      this->duration = duration;
    }
};

Menu myMenu[] = { Menu("Potato", 5000UL)
                  , Menu("Sandwich", 4000UL)
                  , Menu("Burger", 3000UL)
                  , Menu("Chicken", 2000UL)
                };

const int maxQueue = 36;
GenericQueue<Menu> order(maxQueue);
const int initialAddQ = 4;

const String menuList[4] = {"OInQ.: ", "NextQ: ", "CurrQ: ", "Cooki: "};
String lcdBuffer[4];

Timer cookingTimer;
Timer blinkTimer;
Timer intervalLCD;

float remainingTime;
bool blinkState;

void Cooking();
void RaiseOrder();
void QueueChanged(Menu m, QueueEventArgs e);

void setup() {
  Serial.begin(115200);
  myLCD.init();
  myLCD.backlight();

  for (int index = 0 ; index < sizeof(inputPin) / sizeof(int) ; index++) {
    pinMode(inputPin[index], INPUT_PULLUP);
    debouncer[index].mode(INSTANT, debouncePeriod, HIGH);
  }

  pinMode(cookingLEDPin, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);

  order.onStateChanged(QueueChanged);
  blinkTimer.timeDelay(blinkPeriod);
  intervalLCD.timeDelay(100);

  ReadQueue();
  Cooking();
  LCDDisplay();
  delay(1000);
}

void loop() {
  blinkTimer.timerOn(!blinkTimer.state);
  if (blinkTimer.state) blinkState = !blinkState;
  RaiseOrder(); // Raise an Order
  ReadQueue();  // Update LCD Buffer
  Cooking();
  if (intervalLCD.timerOn(!intervalLCD.state)) {
    LCDDisplay();
  }
  digitalWrite(LED_BUILTIN, blinkState);
}

void RaiseOrder() {
  for (int index = 0 ; index < sizeof(inputPin) / sizeof(int) ; index++) {
    debouncer[index].debounce(!digitalRead(inputPin[index]));
    if (debouncer[index].rising()) {
      order.enqueue(myMenu[index]);
    }
  }
}

void Cooking() {
  int count = order.count;
  if (!order.isEmpty() ) {
    digitalWrite(cookingLEDPin, blinkState);
    Menu currOrder = order.peek();
    cookingTimer.timeDelay(currOrder.duration);
    cookingTimer.timerOn(true);
    if (cookingTimer.state) {
      cookingTimer.timerOn(false);
      order.dequeue();
    }
    remainingTime = (cookingTimer.duration - cookingTimer.elapsed) * 0.001;
  }
  else {
    remainingTime = 0.0;
    digitalWrite(cookingLEDPin, 0);
  }
}


void ReadQueue() {
  Menu currOrder;
  Menu nexOrder;

  int count = order.count;

  // Get Current Queue
  if (count > 0) {
    currOrder = order.peek();
  }

  // Get Next Queue
  if (count > 1) {
    nexOrder = order[1];
  }

  // Set LCD Information to Buffers : 13 characters per Line (7 charactors Reserved for Title)
  lcdBuffer[0] = PadMid(String(count), "(MQ" + String(order.count) + ")", 13, ' ' );
  lcdBuffer[1] = PadMid(nexOrder.name, (order.count > 1 ? " " + String(nexOrder.duration * 0.001, 1) + "s" : "-"), 13, ' ');
  lcdBuffer[2] = PadMid(currOrder.name, (order.count > 0 ? " " + String(currOrder.duration * 0.001, 1) + "s" : "-"), 13, ' ');
  lcdBuffer[3] = PadMid(order.count > 0 ? (blinkState == 1 ? currOrder.name + " " : "") : " ", order.count > 0 ? String(remainingTime, 1) + "s" : "-", 13, ' ');
}

void QueueChanged(QueueEventArgs e, Menu m) {
  switch (e.state) {
    case ENQUEUE:
      // newTimer.TimerOff(true);
      Serial.println("enqueue: " + String(m.name) + " New Order!");
      break;
    case DEQUEUE:
      // readyTimer.TimerOff(true);
      Serial.println("dequeue: " + String(m.name) + " Ready to serve!");
      break;
  };
}

void LCDDisplay() {
  String strLCD = "";
  for (int index = 0 ; index < 4 ; index++) {
    myLCD.setCursor(0, index);
    strLCD = menuList[index];
    strLCD = strLCD + lcdBuffer[index];
    myLCD.print(strLCD);
  }
}

// return string by padding charecter in the middle of 2 strings
String PadMid(String leftString, String rightString, int length, char paddingChar) {
  int lenPad = length - leftString.length() - rightString.length();
  for (int index = 0; index < lenPad; index++) {
    leftString += paddingChar;
  }
  return (leftString + rightString);
}