// Include libraries and user-defined header files
#include <LiquidCrystal_I2C.h> // Library for I2C LCD
#include "GenericQueue.h" // User-defined generic queue implementation
#include "ADebouncer.h" // User-defined debouncing library

// Initialize the I2C LCD object with the specified address and dimensions
LiquidCrystal_I2C myLCD(0x27, 20, 4);

// Define pin numbers for LEDs
#define COOKING_LED_PIN 12
#define READY_LED_PIN 11
#define NEW_LED_PIN 10

// Define debounce period and blink period
#define DEBOUNCE_PERIOD 10
#define BLINK_PERIOD 500

// Define the number of input pins and their corresponding pin numbers
#define NUMBER_OF_INPUTS 4
const int inputPin[NUMBER_OF_INPUTS] = {9, 8, 7, 6};

// Create debouncer objects for each input pin
ADebouncer debouncer[NUMBER_OF_INPUTS];

class Timer {
  private:
    uint32_t _duration; // Stores the duration of the timer
    uint32_t _startTime; // Stores the start time of the timer
    uint32_t _elapsed; // Stores the elapsed time of the timer
    bool _state; // Represents the state of the timer (e.g., running or expired)
    bool _enable; // Indicates whether the timer is enabled

  public:
    bool &state; // Reference to the state of the timer
    uint32_t &elapsed; // Reference to the elapsed time of the timer
    uint32_t &duration; // Reference to the duration of the timer

    // Constructor initializes the references to the state, elapsed, and duration
    Timer(): state(_state), elapsed(_elapsed), duration(_duration) {}

    // Set the duration of the timer
    void timeDelay(uint32_t duration) {
      _duration = duration;
    }

    // Activate or deactivate the timer based on the 'enable' parameter
    // Returns the state of the timer
    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 representing a menu with a name and duration
class Menu {
  public:
    String name;        // Name of the menu
    uint32_t duration;  // Duration of the menu in milliseconds

  public:
    // Default constructor
    Menu() {
      name = "";          // Initialize name to an empty string
      duration = 1000UL;  // Initialize duration to 1000 milliseconds
    }

    // Parameterized constructor
    Menu(String const& name, uint32_t duration) {
      this->name = name;          // Set the name to the provided value
      this->duration = duration; // Set the duration to the provided value
    }
};

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

// Define the maximum queue size
const int maxQueue = 36;

// Create a generic queue of Menu objects with the specified maximum size
GenericQueue<Menu> order(maxQueue);

// Define the initial size of the queue
const int initialAddQ = 4;

// Define an array of menu names
const String menuList[4] = {"OInQ.: ", "NextQ: ", "CurrQ: ", "Cooki: "};

// Initialize an array to hold LCD display content
String lcdBuffer[4];

// Define timers for cooking, blinking, and LCD update intervals
Timer cookingTimer;
Timer blinkTimer;
Timer intervalLCD;

// Variable to hold remaining time for cooking
float remainingTime;

// Variable to store the state of blinking
bool blinkState;

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

// Setup function
void setup() {
  Serial.begin(115200); // Start serial communication
  myLCD.init();         // Initialize the LCD
  myLCD.backlight();    // Turn on the backlight

  // Set input pins to INPUT_PULLUP and initialize debouncers
  for (int index = 0; index < sizeof(inputPin) / sizeof(int); index++) {
    pinMode(inputPin[index], INPUT_PULLUP);
    debouncer[index].mode(INSTANT, DEBOUNCE_PERIOD, HIGH);
  }

  pinMode(COOKING_LED_PIN, OUTPUT); // Set COOKING_LED_PIN as output
  pinMode(LED_BUILTIN, OUTPUT);     // Set LED_BUILTIN as output

  order.onStateChanged(QueueChanged); // Set the event handler for queue state changes
  blinkTimer.timeDelay(BLINK_PERIOD); // Set the time delay for blinking
  intervalLCD.timeDelay(100);         // Set the time delay for LCD update

  ReadQueue();  // Read the queue
  Cooking();    // Start the cooking process
  LCDDisplay(); // Update the LCD display
  delay(1000);  // Delay for 1 second
}

void loop() {
  // Control blinking based on timer
  blinkTimer.timerOn(!blinkTimer.state);
  if (blinkTimer.state) blinkState = !blinkState;

  // Perform order processing
  RaiseOrder();

  // Update LCD Buffer by reading the queue
  ReadQueue();

  // Perform cooking-related tasks
  Cooking();

  // Update LCD display at regular intervals
  if (intervalLCD.timerOn(!intervalLCD.state)) {
    LCDDisplay();
  }

  // Control the built-in LED based on blink state
  digitalWrite(LED_BUILTIN, blinkState);
}

// Process incoming orders based on button presses
void RaiseOrder() {
  for (int index = 0 ; index < sizeof(inputPin) / sizeof(int) ; index++) {
    // Debounce button input
    debouncer[index].debounce(!digitalRead(inputPin[index]));
    // Check for rising edge (button press) and enqueue the corresponding item
    if (debouncer[index].rising()) {
      order.enqueue(myMenu[index]);
    }
  }
}

// Simulate cooking process based on incoming orders
void Cooking() {
  int count = order.count;
  // If there are pending orders
  if (!order.isEmpty() ) {
    // Control cooking LED based on blink state
    digitalWrite(COOKING_LED_PIN, blinkState);
    // Get the current order from the queue
    Menu currOrder = order.peek();
    // Start the cooking timer for the current order
    cookingTimer.timeDelay(currOrder.duration);
    cookingTimer.timerOn(true);
    // If cooking timer is active
    if (cookingTimer.state) {
      // Turn off the cooking timer and dequeue the completed order
      cookingTimer.timerOn(false);
      order.dequeue();
    }
    // Calculate the remaining time for the current order
    remainingTime = (cookingTimer.duration - cookingTimer.elapsed) * 0.001;
  }
  // If no pending orders
  else {
    remainingTime = 0.0;
    // Turn off the cooking LED
    digitalWrite(COOKING_LED_PIN, 0);
  }
}


// Update LCD display with current and next orders from the queue
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];
  }

  // Prepare LCD buffer with order information
  lcdBuffer[0] = PadMid(String(count), "(MQ" + String(order.count) + ")", 13, ' ');  // Total order count
  lcdBuffer[1] = PadMid(nexOrder.name, (order.count > 1 ? " " + String(nexOrder.duration * 0.001, 1) + "s" : "-"), 13, ' ');  // Next order name and duration
  lcdBuffer[2] = PadMid(currOrder.name, (order.count > 0 ? " " + String(currOrder.duration * 0.001, 1) + "s" : "-"), 13, ' ');  // Current order name and duration
  lcdBuffer[3] = PadMid(order.count > 0 ? (blinkState == 1 ? currOrder.name + " " : "") : " ", order.count > 0 ? String(remainingTime, 1) + "s" : "-", 13, ' ');  // Display remaining time if an order is being processed
}

// Handle queue state changes and log events
void QueueChanged(QueueEventArgs e, Menu m) {
  switch (e.state) {
    case ENQUEUE:
      // Notify new order added
      // newTimer.TimerOff(true);
      Serial.println("enqueue: " + String(m.name) + " New Order!");
      break;
    case DEQUEUE:
      // Notify order ready for serving
      // readyTimer.TimerOff(true);
      Serial.println("dequeue: " + String(m.name) + " Ready to serve!");
      break;
  };
}

// Display menu and buffer information on the LCD
void LCDDisplay() {
  String strLCD = "";
  for (int index = 0; index < 4; index++) {
    myLCD.setCursor(0, index);
    strLCD = menuList[index];  // Store menu information
    strLCD += lcdBuffer[index];  // Append buffer information
    myLCD.print(strLCD);  // Display combined information
  }
}

// Returns a string by padding characters in the middle of two strings
String PadMid(String leftString, String rightString, int length, char paddingChar) {
  int lenPad = length - leftString.length() - rightString.length();  // Calculate padding length
  for (int index = 0; index < lenPad; index++) {
    leftString += paddingChar;  // Add padding characters to left string
  }
  return (leftString + rightString);  // Return combined string with padding
}