#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <HX711.h>
#include <Servo.h>
#include <RotaryEncoder.h>
#include <math.h>
#include <EEPROM.h>

#define LB2KG 0.45352
#define CALWEIGHT 1.00  // Set known value for calibration (In Kg 1.0 = 1000 gram)
#define DEFAULT_CALIFACTOR -7050

// Pins
#define ENCODER_CLK 2
#define ENCODER_DT 3
#define BUTTON_CALIBRATE 4
#define BUTTON_POUR 5
#define SERVO_PIN 6
#define HX711_DOUT 7
#define HX711_SCK 8


#define LONG_PRESS_DURATION 1000  // 1 second for long press
#define CALIBRATION_WEIGHT 100    // 100 grams calibration weight

#define SCREEN_WIDTH 128  // OLED display width, in pixels
#define SCREEN_HEIGHT 64  // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET -1  // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

HX711 scale;
Servo gateServo;

// Setup a RotaryEncoder with 2 steps per latch for the 2 signal input pins:
RotaryEncoder encoder(ENCODER_CLK, ENCODER_DT, RotaryEncoder::LatchMode::FOUR3 );

double totalAmount = 1000;  // Total coffee in grams
long selectedAmount = 200;  // Amount to pour in grams
long consumedAmount = 0;
long currentWeight = 0;
bool inMenu = false;
unsigned long buttonPressTime = 0;

long currentOffset;
float calibration_factor;

unsigned long lastValueTime = millis();  // Variable to hold last time when value was taken

// Creatign a menu list
enum MenuState { IDLE,
                 MAIN_MENU,
                 SET_TOTAL,
                 SET_AMOUNT,
                 BACK
};
MenuState menuState = IDLE;

void setup() {
  Serial.begin(9600);

  // Initialize components
  pinMode(BUTTON_CALIBRATE, INPUT_PULLUP);
  pinMode(BUTTON_POUR, INPUT_PULLUP);
  scale.begin(HX711_DOUT, HX711_SCK);
  gateServo.attach(SERVO_PIN);
  gateServo.write(0);  // Close gate initially

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    // Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;  // Don't proceed, loop forever
  }
  display.clearDisplay();
  display.setTextSize(1);              // Normal 1:1 pixel scale
  display.setTextColor(WHITE, BLACK);  // Draw white text
  display.display();

  // eeprom
  if (EEPROM.read(0x00) != 0x01) {
    // Serial.println("NOT INIT !!!!");
    currentOffset = 0;
    calibration_factor = DEFAULT_CALIFACTOR;
    // show instructions
    showMessage(0, 0, "First Calibrate Scale");
    delay(1000);
  } else {
    EEPROM.get(0x01, currentOffset);
    EEPROM.get(0x01 + sizeof(long), calibration_factor);
    // Serial.println("currentOffset = " + String(currentOffset));
    // Serial.println("calibration_factor = " + String(calibration_factor));
  }


  //scale
  delay(100);
  // Serial.println("calibration_factor = " + String(calibration_factor));
  scale.set_scale(calibration_factor / LB2KG);
  scale.set_offset(currentOffset);  // Apply saved offset value
  Serial.println("setup done ...");

  delay(1000);                           // Wait for 1 second to stabilize
  totalAmount = abs(scale.get_units());  // Get current scale value
  // Show main screen
  showMainScreen();
}

void loop() {
  // Check if user has pressed Calibrate button and we are not in menu
  if (digitalRead(BUTTON_CALIBRATE) == LOW) {
    if (menuState == IDLE)
      menuState = SET_TOTAL;  // Set the menu state to main menu
    showMenu();
    delay(300);
  }

  // Handle menu navigation with encoder
  if (inMenu) {
    handleMenu();
  }

  // Pour coffee and check for scale calibration
  if (digitalRead(BUTTON_POUR) == LOW && !inMenu) {
    unsigned long cTime = millis();  // Note current button press time
    // Serial.println("Button Pressed");
    // showMessage(0, 0, "Button Prssed", HIGH);
    while (digitalRead(BUTTON_POUR) == LOW) {
      ;
    }
    // showMessage(0, 15, "Button Released");
    Serial.println("Button Released");
    Serial.println(millis() - cTime);
    pourCoffee();
  }
}
void showMessage(byte x, byte y, char *message) {
  display.setCursor(x, y);
  display.print(message);
  display.display();
}

void showMessage(byte x, byte y, char *message, bool clearDisplay) {
  if (clearDisplay)
    display.clearDisplay();
  display.setCursor(x, y);
  display.print(message);
  display.display();
}
void showMultiMessage(byte x, byte y, char *message1, int value, char *message2, bool clearDisplay) {
  if (clearDisplay)
    display.clearDisplay();
  display.setCursor(x, y);
  display.print(message1);
  display.print(value);
  display.print(message2);
  display.display();
}

void showMainScreen() {
  showMultiMessage(0, 0, "Total: ", totalAmount, " g ", HIGH);

  showMultiMessage(0, 15, "Selected: ", selectedAmount, " g ", LOW);

  showMultiMessage(0, 30, "Weight: ", currentWeight, " g ", LOW);
}

void updateMainScreen(long currentWeight) {
  display.setCursor(0, 30);
  display.print("Weight: ");
  display.print(currentWeight);
  display.print(" g  ");
  display.display();
}

void pourCoffee() {
  //scale.tare(); // clear scale
  long measuredWeight = 0;                 // Variable to hold value to measure weight
  long currentWeight = scale.get_units();  // Note current scale measurement
  gateServo.write(90);                     // Open the gate
  while (abs(scale.get_units() - currentWeight) < selectedAmount) {
    measuredWeight = abs(scale.get_units() - currentWeight);
    updateMainScreen(measuredWeight);
    delay(10);  // Introduce some small delays
  }
  gateServo.write(0);  // Close the gate
  measuredWeight = abs(scale.get_units() - currentWeight);
  updateMainScreen(measuredWeight);
  totalAmount = abs(scale.get_units());
  updateMainScreen(measuredWeight);
  showMultiMessage(0, 0, "Total: ", totalAmount, " g  ", LOW);
  showMultiMessage(0, 30, "Weight: ", currentWeight, " g ", LOW);
}

void displayCursor(byte x, byte y) {
  display.setCursor(x, y);  // Set curosor position
  display.print(">");
  display.display();
}
void showMenu() {
  inMenu = false;
  // menuState = SET_TOTAL;  // Set the menu state to set total
  // Reset encoder position value
  // encoder.setPosition(0);
  display.clearDisplay();
  display.setCursor(3, 5);
  display.print("1. Set Amount");
  display.setCursor(3, 20);
  display.print("2. Back");
  display.display();
  switch (menuState) {
    case SET_TOTAL:
      inMenu = true;
      encoder.setPosition(selectedAmount);  // Set the encoder value to selected amount
      delay(500);
      display.clearDisplay();
      menuState = SET_AMOUNT;
      displayCursor(0, 10);
      break;
    case SET_AMOUNT:
      menuState = BACK;
      displayCursor(0, 20);
      break;
    case BACK:
      menuState = IDLE;
      showMainScreen();
      break;
    default:
      break;
  }
}

void handleMenu() {
  long encoderPosition = encoder.getPosition();

  switch (menuState) {
    case SET_AMOUNT:
      selectedAmount = encoderPosition;  // Adjust the amount with encoder
      showMultiMessage(0, 10, "Pour Amount: ", selectedAmount, " g ", LOW);
      break;
    case BACK:
      inMenu = false;
      showMainScreen();
      break;
  }
  display.display();
}