#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHTesp.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <ESP32Servo.h>
#include <ArduinoJson.h>

// Pin Definitions
#define BUZZER 4
#define LED_1 15
#define LED_TEMP 13
#define LED_HUM 2
#define PB_CANCEL 34
#define PB_OK 32
#define PB_UP 33
#define PB_DOWN 35
#define DHTPIN 14
#define LDR1 5 //left
#define LDR2 25 //right
#define MOTOR 18

// Define variables for controlling the servo motor
float minAngle = 30.0;       // Minimum angle
float controllingFac = 0.75; // Controlling factor
float customAngle;
float customControlFac;

//Initialise clients and objects
Servo motor;
WiFiClient espClient;
PubSubClient mqttClient(espClient);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
JsonDocument packet;

// NTP Configuration
#define NTP_SERVER     "pool.ntp.org"
#define UTC_OFFSET_DST 0
int UTC_OFFSET = 19800;
int offsets[] = {0, 0} ; //Default set for IST time zone
int offset_hours = 5;
int offset_minutes = 30;

// Initialize DHT Sensor
DHTesp dhtSensor;

// OLED Display Configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Global variables for time
int days = 0;
int hours = 0;
int minutes = 0;
int seconds = 0;

unsigned long timeNow = 0;
unsigned long timeLast = 0;

// Alarm configuration
bool alarm_enabled = true;
int n_alarms = 3;
//centred number for easy choose
int alarm_hours[] = {12, 12, 12};
int alarm_minutes[] = {27, 27, 27};
bool alarm_triggered[] = {false, false, false};
//TODO: make isScheduledON like above and make all 3 alarms schedulable

// Variables for schedule
bool isScheduledON = false; // Indicate if the schedule is enabled
unsigned long scheduledOnTime;

// Musical notes for the buzzer
int n_notes = 8;
int C = 262;
int D = 294;
int E = 330;
int F = 349;
int G = 392;
int A = 440;
int B = 494;
int C_H = 523;
int notes[] = {C, D, E, F, G, A, B, C_H};

// Arrays to store data for MQTT messages
char tempAr[6];
char motorAr[6];
char angleAr[6];
char ctrlAr[6];

// Constants for analog reading
const float analogMinValue = 0.0;
const float analogMaxValue = 1023.0;
const float intensityMin = 0.0;
const float intensityMax = 1.0;

// User interface configuration
int current_mode = 0;
int max_modes = 5;
String modes[] = {"1 - Set   Time Zone", "2 - Set   Alarm 1", "3 - Set   Alarm 2", "4 - Set   Alarm 3", "5- Disable Alarms"};

void setup() {
  // Set up pin modes
  pinMode(BUZZER, OUTPUT);
  digitalWrite(BUZZER, LOW);
  pinMode(LED_1, OUTPUT);
  pinMode(LED_TEMP, OUTPUT);
  pinMode(LED_HUM, OUTPUT);
  pinMode(PB_CANCEL, INPUT);
  pinMode(PB_OK, INPUT);
  pinMode(PB_UP, INPUT);
  pinMode(PB_DOWN, INPUT);
  pinMode(LDR1, INPUT);
  pinMode(LDR2, INPUT);
  motor.attach(MOTOR, 600, 2400);

  // Initialize serial monitor and OLED display
  Serial.begin(9600);
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);
  }

  setupWifi();
  setupMqtt();

  dhtSensor.setup(DHTPIN, DHTesp::DHT22);
  timeClient.begin();
  timeClient.setTimeOffset(5.5 * 3600);

  // Turn on OLED display
  display.display();
  delay(2000);

  // Configure NTP time synchronization
  configTime(UTC_OFFSET, UTC_OFFSET_DST, NTP_SERVER);

  display.clearDisplay();

  print_line("Welcome to MediBox!", 0, 10, 2);
  display.clearDisplay();

  // Publish initial configuration to MQTT
  // connectToBroker();
  customAngle = 30;
  customControlFac = 0;
  mqttClient.publish("MQTT-SET-ANG", "30");
  mqttClient.publish("MQTT-SET-FAC", "0.75");
}

void loop() {
  //TODO: optimise all these processes
  if (!mqttClient.connected()){
    connectToBroker();
  }
  // Update time and check for alarms
  update_time_with_check_alarm();

  checkSchedule(); //TODO: Integrate check schedule with update_time_with_check_alarm

  updateLightIntensity(); // Read light intensity from LDR

  // Check if the OK button is pressed to navigate to the menu
  if (digitalRead(PB_OK) == LOW) {
    delay(200); // Debouncing push button
    go_to_menu();
  }

  // Check temperature and humidity
  check_temp_and_hum(); //Try to remove this
  updateTemperature();
  //TODO: Implement updateHumidity();
}

// Setup WiFi connection
void setupWifi()
{
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println("Wokwi-GUEST");
    WiFi.begin("Wokwi-GUEST", "");

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }

    Serial.println("Wifi connected");
    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());
}

// Setup MQTT client
void setupMqtt()
{
    mqttClient.setServer("test.mosquitto.org", 1883);
    mqttClient.setCallback(receiveCallback);
}

// Function to print a line on the OLED display
void print_line(String text, int column, int row, int text_size) {
  display.setTextSize(text_size);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(column, row);
  display.println(text);
  display.display();
}

// Connect to MQTT broker
void connectToBroker()
{
    while (!mqttClient.connected())
    {
        Serial.println("Attempting MQTT connetion...");
        if (mqttClient.connect("ESP-55200255"))
        {
            Serial.println("Connected");
            mqttClient.subscribe("MQTT-ON-OFF");
            mqttClient.subscribe("MQTT-SCH-ON");
            mqttClient.subscribe("MQTT-DROP-DOWN");
            mqttClient.subscribe("MQTT-MIN-ANG");
            mqttClient.subscribe("MQTT-CTRL-FAC");
        }
        else
        {
            Serial.print("Connection failed with state: ");
            Serial.println(mqttClient.state());
            delay(5000);
        }
    }
}

// Function to turn the buzzer on or off
void buzzerOn(bool on)
{
  if (on)
  {
    tone(BUZZER, 256);
  }
  else
  {
    noTone(BUZZER);
  }
}

// MQTT receive callback function
void receiveCallback(char *topic, byte *payload, unsigned int length)
{
    Serial.print("Message arrived [");
    Serial.print(topic);
    Serial.print("] ");

    char payloadCharAr[length];

    for (int i = 0; i < length; i++)
    {
        Serial.print((char)payload[i]);
        payloadCharAr[i] = (char)payload[i];
    }

    Serial.println();

    // Process received MQTT messages
    if (strcmp(topic, "MQTT-ON-OFF") == 0)
    {
        buzzerOn(payloadCharAr[0] == '1');
    }
    else if (strcmp(topic, "MQTT-SCH-ON") == 0)
    {
        if (payloadCharAr[0] == 'N')
        {
            isScheduledON = false;
        }
        else
        {
            isScheduledON = true;
            scheduledOnTime = atol(payloadCharAr);
            Serial.println("Schedule ON");
            Serial.println("Scheduled Time is " + String(scheduledOnTime));
        }
    }
    // Update minimum angle
    if (strcmp(topic, "MQTT-MIN-ANG") == 0)
    {
        minAngle = atof(payloadCharAr);
        Serial.println(minAngle);
    }
    // Update controlling factor
    if (strcmp(topic, "MQTT-CTRL-FAC") == 0)
    {
        controllingFac = atof(payloadCharAr);
        Serial.println(controllingFac);
    }
    // Update minimum angle and controlling factor based on drop-down selection
    if (strcmp(topic, "MQTT-DROP-DOWN") == 0)
    {
        // Handle different drop-down options
        if (payloadCharAr[0] == 'D') //default
        {
            minAngle = 30;
            controllingFac = 0.75;
            Serial.println("Angle:" + String(minAngle) + " and Control Factor: " + String(controllingFac));
            mqttClient.publish("MQTT-SET-ANG", "30");
            mqttClient.publish("MQTT-SET-FAC", "0.75");
        }
        else if (payloadCharAr[0] == 'A')
        {
            minAngle = 30;
            controllingFac = 0.5;
            Serial.println("Angle:" + String(minAngle) + " and Control Factor: " + String(controllingFac));
            mqttClient.publish("MQTT-SET-ANG", "30");
            mqttClient.publish("MQTT-SET-FAC", "0.5");
        }
        else if (payloadCharAr[0] == 'B')
        {
            minAngle = 45;
            controllingFac = 0.3;
            Serial.println("Angle:" + String(minAngle) + " and Control Factor: " + String(controllingFac));
            mqttClient.publish("MQTT-SET-ANG", "45");
            mqttClient.publish("MQTT-SET-FAC", "0.3");
        }
        else if (payloadCharAr[0] == 'C')
        {
            minAngle = 60;
            controllingFac = 0.8;
            Serial.println("Angle:" + String(minAngle) + " and Control Factor: " + String(controllingFac));
            mqttClient.publish("MQTT-SET-ANG", "60");
            mqttClient.publish("MQTT-SET-FAC", "0.8");
        }
        else if (payloadCharAr[0] == 'X') //Custom
        {
            String(customAngle, 2).toCharArray(angleAr, 6);
            String(customControlFac, 2).toCharArray(ctrlAr, 6);
            mqttClient.publish("MQTT-SET-ANG", angleAr);
            mqttClient.publish("MQTT-SET-FAC", ctrlAr);

            if (strcmp(topic, "MQTT-MIN-ANG") == 0)
            {
                minAngle = atof(payloadCharAr);
                customAngle = minAngle;
                //Serial.println("New custom angle is: " + String(minAngle));
                String(minAngle, 2).toCharArray(angleAr, 6);
                mqttClient.publish("MQTT-SET-ANG", angleAr);
            }
            if (strcmp(topic, "MQTT-CTRL-FAC") == 0)
            {
                controllingFac = atof(payloadCharAr);
                customControlFac = controllingFac;
                String(customControlFac, 2).toCharArray(ctrlAr, 6);
                //Serial.println("New custom factor is: " + String(controllingFac));
                mqttClient.publish("MQTT-SET-FAC", ctrlAr);
            }
        }
    }
}

// Function to print the current time on the OLED display
void print_time_now(void) {
  display.clearDisplay();

  struct tm timeinfo;
  getLocalTime(&timeinfo);

  char timeDay[4];
  strftime(timeDay, 4, "%a", &timeinfo);  //day of the week
  print_line(String(timeDay), 10, 0, 2);

  print_line(" ", 30, 0, 2);

  char timeDate[3];
  strftime(timeDate, 3, "%d", &timeinfo);  //day of the month
  print_line(String(timeDate), 50, 0, 2);

  char timeHour[3];
  strftime(timeHour, 3, "%H", &timeinfo);  //hour in 24-hour format
  print_line(String(timeHour), 10, 25, 2);

  print_line(":", 30, 25, 2);

  char timeMinute[3];
  strftime(timeMinute, 3, "%M", &timeinfo);  //minute
  print_line(String(timeMinute), 40, 25, 2);

  print_line(":", 60, 25, 2);

  char timeSecond[3];
  strftime(timeSecond, 3, "%S", &timeinfo);  //second
  print_line(String(timeSecond), 70, 25, 2);

  display.display();
}

// Function to update the time
void update_time() {
  struct tm timeinfo;
  getLocalTime(&timeinfo, UTC_OFFSET);

  hours = timeinfo.tm_hour;
  minutes = timeinfo.tm_min;
  seconds = timeinfo.tm_sec;
}

// Function to ring the alarm
void ring_alarm() {
  display.clearDisplay();
  print_line("TAKE YOUR MEDICINE!", 0, 0, 2);

  digitalWrite(LED_1, HIGH);

  bool break_happened = false;

  // Ring the buzzer
  while (break_happened == false && digitalRead(PB_CANCEL) == HIGH) {
    for (int i = 0; i < n_notes; i++) {
      if (digitalRead(PB_CANCEL) == LOW) {
        delay(200);
        break_happened = true;
        break;
      }
      tone(BUZZER, notes[i]);
      delay(500);
      noTone(BUZZER);
      delay(2);
    }
  }

  digitalWrite(LED_1, LOW);
  display.clearDisplay();
}

// Function to update time and check for alarms
void update_time_with_check_alarm(void) {
  update_time();
  print_time_now();

  if (alarm_enabled == true) {
    for (int i = 0; i < n_alarms; i++) {
      if (alarm_triggered[i] == false && alarm_hours[i] == hours && alarm_minutes[i] == minutes) {
        ring_alarm();
        alarm_triggered[i] = true;
      }
    }
  }
}

void checkSchedule()
{
    if (isScheduledON)
    {
        unsigned long currentTime = getTime();
        if (currentTime > scheduledOnTime)
        {
            buzzerOn(true);
            mqttClient.publish("MQTT-SCH-OFF-ESP", "0");
            Serial.println("Current Time is " + String(currentTime));
            Serial.println("Scheduled Time is " + String(scheduledOnTime));
            isScheduledON = false;
        }
    }
}

// Get current time from NTP server
unsigned long getTime()
{
    timeClient.update();
    return timeClient.getEpochTime();
}


// Function to wait for a button press
int wait_for_button_press() {
  while (true) {
    if (digitalRead(PB_UP) == LOW) {
      delay(200);
      return PB_UP;
    } else if (digitalRead(PB_DOWN) == LOW) {
      delay(200);
      return PB_DOWN;
    } else if (digitalRead(PB_OK) == LOW) {
      delay(200);
      return PB_OK;
    } else if (digitalRead(PB_CANCEL) == LOW) {
      delay(200);
      return PB_CANCEL;
    }

    update_time();
  }
}

// Function to navigate to the menu
void go_to_menu() {
  while (digitalRead(PB_CANCEL) == HIGH) {
    display.clearDisplay();
    print_line(modes[current_mode], 0, 0, 2);

    int pressed = wait_for_button_press();
    if (pressed == PB_UP) {
      delay(200);
      current_mode += 1;
      current_mode = current_mode % max_modes;
    } else if (pressed == PB_DOWN) {
      delay(200);
      current_mode -= 1;
      if (current_mode < 0) {
        current_mode = max_modes - 1;
      }
    } else if (pressed == PB_OK) {
      delay(200);
      run_mode(current_mode);
    } else if (pressed == PB_CANCEL) {
      delay(200);
      break;
    }
  }
}

// Function to set an alarm
void set_alarm(int alarm) {
  int temp_hour = alarm_hours[alarm];
  alarm_enabled = true;
  while (true) {
    display.clearDisplay();
    print_line("Enter hour: " + String(temp_hour), 0, 0, 2);

    int pressed = wait_for_button_press();
    if (pressed == PB_UP) {
      delay(200);
      temp_hour += 1;
      temp_hour = temp_hour % 24;
    } else if (pressed == PB_DOWN) {
      delay(200);
      temp_hour -= 1;
      if (temp_hour < 0) {
        temp_hour = 23;
      }
    } else if (pressed == PB_OK) {
      delay(200);
      alarm_hours[alarm] = temp_hour;
      break;
    } else if (pressed == PB_CANCEL) {
      delay(200);
      break;
    }
  }

  int temp_minute = alarm_minutes[alarm];
  while (true) {
    display.clearDisplay();
    print_line("Enter minute: " + String(temp_minute), 0, 0, 2);

    int pressed = wait_for_button_press();
    if (pressed == PB_UP) {
      delay(200);
      temp_minute += 1;
      temp_minute = temp_minute % 60;
    } else if (pressed == PB_DOWN) {
      delay(200);
      temp_minute -= 1;
      if (temp_minute < 0) {
        temp_minute = 59;
      }
    } else if (pressed == PB_OK) {
      delay(200);
      alarm_minutes[alarm] = temp_minute;
      break;
    } else if (pressed == PB_CANCEL) {
      delay(200);
      break;
    }
  }

  display.clearDisplay();
  print_line("Alarm is set", 0, 0, 2);
  delay(1000);
}

// Function to run the selected mode
void run_mode(int mode) {
  if (mode == 0) {
    set_timezone();
    UTC_OFFSET = 0;
    if (offsets[0] < 0 ) {
      UTC_OFFSET = (UTC_OFFSET + (offsets[0] * 60 * 60  - 1 * offsets[1] * 60));
    }
    else {
      UTC_OFFSET = UTC_OFFSET + (offsets[0] * 60 * 60 + offsets[1] * 60);
    }
    configTime(UTC_OFFSET, UTC_OFFSET_DST, NTP_SERVER);
  } else if (mode == 1 || mode == 2 || mode == 3) {
    set_alarm(mode - 1);
  } else if (mode == 4) {
    alarm_enabled = false;
    display.clearDisplay();
    delay(200);
    print_line("Alarms Disabled", 0, 0, 2);
    delay(1000);
    display.clearDisplay();
  }
}

// Function to check temperature and humidity
void check_temp_and_hum() {
  TempAndHumidity data = dhtSensor.getTempAndHumidity();
  if (data.temperature > 32) {
    digitalWrite(LED_TEMP, HIGH);
    print_line("TEMPETATURE HIGH", 10, 45, 1);
    digitalWrite(LED_TEMP,LOW);
  } else if (data.temperature < 26) {
    digitalWrite(LED_TEMP, HIGH);
    print_line("TEMPETATURE LOW", 10, 45, 1);
    digitalWrite(LED_TEMP,LOW);
  }

  if (data.humidity > 80) {
    digitalWrite(LED_HUM, HIGH);
    print_line("HUMIDITY HIGH", 10, 55, 1);
    digitalWrite(LED_HUM, LOW);
  } else if (data.humidity < 60) {
    digitalWrite(LED_HUM, HIGH);
    print_line("HUMIDITY LOW", 10, 55, 1);
    digitalWrite(LED_HUM, LOW);
  }
}

// Update temperature reading and publish to MQTT
void updateTemperature()
{
    TempAndHumidity data = dhtSensor.getTempAndHumidity();
    String(data.temperature, 2).toCharArray(tempAr, 6);

    //Serial.println("Temperature is " + String(tempAr) + "°C");
    mqttClient.publish("MQTT-TEMP", tempAr);
    delay((1000));
}

// Function to change timezone
void set_timezone() {
  int temp_offset_hours = offset_hours;
  while (true){
    display.clearDisplay();
    print_line("Enter offset hours: " + String(temp_offset_hours), 0, 0, 2);

    int pressed = wait_for_button_press();

    if (pressed == PB_UP) {
      delay(200);
      temp_offset_hours += 1;
      temp_offset_hours = temp_offset_hours % 15; //max time zone was 14
    }
    else if (pressed == PB_DOWN) {
      delay(200);
      temp_offset_hours -= 1;
      if (temp_offset_hours < -12) {
        temp_offset_hours = 0;
      }
    }

    else if (pressed == PB_OK) {
      delay(200);
      offset_hours = temp_offset_hours;
      offsets[0] = offset_hours;
      break;
    }
    else if (pressed == PB_CANCEL) {
      delay(200);
      break;
    }
  }

  int temp_offset_minutes = offset_minutes;
  while (true) {
    display.clearDisplay();
    print_line("Enter offset minutes: " + String(temp_offset_minutes), 0, 0, 2);

    int pressed = wait_for_button_press(); // when a button is pressed it is assigned to this

    if (pressed == PB_UP) {
      delay(200);
      temp_offset_minutes += 15;
      temp_offset_minutes = temp_offset_minutes % 60;
    }
    else if (pressed == PB_DOWN) {
      delay(200);
      temp_offset_minutes -= 15;
      if (temp_offset_minutes < 0) {
        temp_offset_minutes = 60;
      }
    }

    else if (pressed == PB_OK) {
      delay(200);
      offset_minutes = temp_offset_minutes;
      offsets[1] = offset_minutes;
      break;
    }
    else if (pressed == PB_CANCEL) {
      delay(200);
      break;
    }
  }

  display.clearDisplay();
  print_line("Timezone  is Set", 0, 0, 2);
  delay(1000);
}

void updateLightIntensity()
{
    float rightLDR = analogRead(LDR1);
    float leftLDR = analogRead(LDR2);
    float intensity = 0;
    char dataJson[50];

    if (rightLDR > leftLDR)
    {
        packet["LDR"] = "Right LED";
        if (rightLDR <= 1023)
        {
            intensity = (rightLDR - analogMinValue) / (analogMaxValue - analogMinValue);
           //Serial.print("Right LDR " + String(intensity) + " intensity");
            AdjustServoMotor(intensity, 0.5);
        }
        else
        {
            intensity = 1;
            //Serial.print("Right LDR " + String(intensity) + " intensity");
            AdjustServoMotor(intensity, 0.5);
        }
    }
    else
    {
        packet["LDR"] = "Left LED";
        if (leftLDR <= 1023)
        {
            intensity = (leftLDR - analogMinValue) / (analogMaxValue - analogMinValue);
            //Serial.print("Left LDR " + String(intensity) + " intensity");
            AdjustServoMotor(intensity, 1.5);
        }
        else
        {
            intensity = 1;
            //Serial.print("Left LDR " + String(intensity) + " intensity");
            AdjustServoMotor(intensity, 1.5);
        }
    }

    packet["Intensity"] = String(intensity);
    serializeJson(packet, dataJson);
    //Serial.println(dataJson);

    mqttClient.publish("MQTT-LIGHT-INTENSITY", dataJson);
}

// Adjust servo motor position based on light intensity and distance from minAngle
void AdjustServoMotor(double lightintensity, double D)
{
    double angle = minAngle * D + (180.0 - minAngle) * lightintensity * controllingFac; // Calculate the angle based on light intensity
    angle = min(angle, 180.0);
    // Serial.println(" and new angle: " + String(angle) + "°");
    motor.write(angle);
    String(angle-90, 2).toCharArray(motorAr, 6);
    mqttClient.publish("MQTT-MOTOR-ANG", motorAr);
}
$abcdeabcde151015202530354045505560fghijfghij
$abcdeabcde151015202530fghijfghij