#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Replace with your network credentials
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
// Define OLED specs
#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 to pin 21, SCL to pin 22)
#define OLED_RESET -1  // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Enable builtin LED
#define LedBuiltIn 2
#define BUTTON_PIN 18      //Button to switch time format 24hrs 12hrs
#define DST_BUTTON_PIN 23  //DST daylight saving time
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);
String IP;
int xPos = SCREEN_WIDTH;
int xPos2 = SCREEN_WIDTH;
int timeZoneOffset = -18000;  // for Eeatern Time USA
// Variables to save date and time
String formattedDate;
String dayStamp;
String timeStamp;
bool is24HourFormat = true;  // true for 24-hour format, false for 12-hour format
bool isDST = false;          // true if DST is enabled
// Store day and month names
const String Days[] = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
const String Months[] = { "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" };
uint8_t secondsIndex;  // Stores seconds
uint8_t minutesIndex;  // Stores minutes
uint8_t hoursIndex;    // Stores hours
uint8_t dayIndex;      // Stores day of week
uint8_t monthDay;      // Stores day of month
uint8_t monthIndex;    // Stores month
uint16_t year;         // Stores year
// Functions
void Time(bool forceUpdate);
int getMonth();     // returns month in integer form 1-12
int getMonthDay();  // returns day of the month in integer form 1 - 31
int getYear();      // returns year in integer form 2020+
void DefaultFrame();
// Draws line on x axis with animation at different heights
// starting y position, Ending y position
void DrawLineAnimated(int j, int k);
//  Draws line on defined sSarting x, Starting y, Ending y positions
void DrawXLine(int i, int j, int k);
// General string display functions
// String to be displayed, X cursor, Y, cursor, Font size
void DisplayRawText(String s, int i, int j, int k);
void AutoTextDisplay(String s, int i, int j, int k);
// Define states for the scrolling text
enum ScrollState {
  SCROLLING,
  PAUSED
};
ScrollState scrollState = SCROLLING;
unsigned long pauseStartTime = 0;
// Define a debounce delay time in milliseconds
const unsigned long debounceDelay = 50;
bool buttonState = HIGH;             // Current state of the button
bool lastButtonState = HIGH;         // Previous state of the button
unsigned long lastDebounceTime = 0;  // The last time the button state was toggled
bool dstButtonState = HIGH;          // Current state of the button
bool lastDSTButtonState = HIGH;
unsigned long lastDSTDebounceTime = 0;
void setup() {
  pinMode(LedBuiltIn, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);      // Configure button pin
  pinMode(DST_BUTTON_PIN, INPUT_PULLUP);  // Configure DST button pin
 
  // Initialize Serial Monitor
  Serial.begin(115200);
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {  // Address 0x3D for 128x64
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;  // Don't proceed, loop forever
  }
  display.setTextWrap(false);
  display.clearDisplay();
  display.display();
  AutoTextDisplay("Connecting to: ", 0, 0, 1);
  AutoTextDisplay(ssid, 0, 8, 1);
  //WiFi.begin(ssid, password);
  //while (WiFi.status() != WL_CONNECTED) {
  //  delay(500);
  //  Serial.print(".");
  //}
  //String ipaddress = WiFi.localIP().toString();
  // Print local IP address and start web server
  AutoTextDisplay("Wifi Connected!", 0, 16, 1);
  //AutoTextDisplay("IP address: ", 0, 24, 1);
  //AutoTextDisplay(ipaddress, 0, 32, 1);
  delay(1000);
  display.clearDisplay();
  // Initialize a NTPClient to get time
  timeClient.begin();
  timeClient.setTimeOffset(timeZoneOffset);
  DisplayRawTime("LOADING...", 30, 1);
  DrawLineAnimated(45, 45);
  display.clearDisplay();
  delay(500);
}
void loop() {
  static unsigned long PassedTime{ 0 };  // Variable to check passed time without using delay
  static unsigned long PassedTime2{ 0 };
  // update the time
  timeClient.update();
  if (PassedTime <= millis() - 1000) {  // update time once per second
    Time(false);
    // display.clearDisplay();
    DefaultFrame();
    PassedTime = millis();
  }
  if (WiFi.status() == WL_CONNECTED && PassedTime2 <= millis() - 5) {
    PassedTime2 = millis();
    //DisplayScrollText("WiFi Connected!", 57, 1);
    DisplayScrollTextPaused("WiFi Connected!", 57, 1);
    //DisplayRawTime("WiFi Connected!", 57, 1);
    //DisplayRawText("WiFi", 0, 57, 1);
  }
  display.display();
  readButtons();
}
void readButtons() {
  // Read the state of the button
  int reading = digitalRead(BUTTON_PIN);
  // If the button state has changed, reset the debounce timer
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
  // Check if the button state has been stable for the debounce delay
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // If the button state has changed
    if (reading != buttonState) {
      buttonState = reading;
      // If the button is pressed
      if (buttonState == LOW) {
        is24HourFormat = !is24HourFormat;  // Toggle the time format
        timeClient.update();
        Time(true);  // Force update time
      }
    }
  }
  // Save the reading. Next time through the loop, it'll be the lastButtonState
  lastButtonState = reading;
  // Check if the DST button is pressed to toggle DST
  int dstReading = digitalRead(DST_BUTTON_PIN);
  if (dstReading != lastDSTButtonState) {
    lastDSTDebounceTime = millis();
  }
  if ((millis() - lastDSTDebounceTime) > debounceDelay) {
    if (dstReading != dstButtonState) {
      dstButtonState = dstReading;
      if (dstButtonState == LOW) {
        //Time();
        isDST = !isDST;  // Toggle DST
        if (!isDST) {
          timeZoneOffset += 3600;  // Add 1 hour for DST
        } else {
          timeZoneOffset -= 3600;  // Subtract 1 hour for standard time
        }
        timeClient.setTimeOffset(timeZoneOffset);
        //timeClient.forceUpdate(); // Force immediate update
        timeClient.update();
        Time(true);  // Force update time
        // Manually adjust the time
        //hoursIndex = (hoursIndex + (isDST ? 1 : -1)) % 24;
        //if (hoursIndex < 0) {
        //  hoursIndex += 24;
        //}
      }
    }
  }
  lastDSTButtonState = dstReading;
}
void DefaultFrame() {
  static bool blinkFlag = false;  // Flag to alternate the blinking colons
  //display.clearDisplay();
  //DisplayRawText("NTP CLOCK", 38, 0, 1);
  DisplayRawTime("ESP32 NTP CLOCK", 0, 1);
  DrawXLine(0, 9, 9);
  //DisplayRawText(Days[dayIndex] + ", the ", 0, 12, 1);
  DisplayRawTime(Days[dayIndex], 12, 1);
  //if (monthDay == 1) DisplayRawText(String(monthDay) + "st of " + Months[monthIndex - 1] + " " + year, 0, 21, 1);
  //else if (monthDay == 2) DisplayRawText(String(monthDay) + "nd of " + Months[monthIndex - 1] + " " + year, 0, 21, 1);
  //else if (monthDay == 3) DisplayRawText(String(monthDay) + "rd of " + Months[monthIndex - 1] + " " + year, 0, 21, 1);
  //else DisplayRawText(String(monthDay) + "th of " + Months[monthIndex - 1] + " " + year, 0, 21, 1);
  // if (monthDay == 1) DisplayRawText(Months[monthIndex - 1] + " " + String(monthDay) + "st, " + year, 0, 21, 1);
  // else if (monthDay == 2) DisplayRawText(Months[monthIndex - 1] + " " + String(monthDay) + "nd, " + year, 0, 21, 1);
  // else if (monthDay == 3) DisplayRawText(Months[monthIndex - 1] + " " + String(monthDay) + "rd, " + year, 0, 21, 1);
  // else DisplayRawText(Months[monthIndex - 1] + " " + String(monthDay) + "th, " + year, 0, 21, 1);
  if (monthDay == 1) DisplayRawTime(Months[monthIndex - 1] + " " + String(monthDay) + "st, " + year, 21, 1);
  else if (monthDay == 2) DisplayRawTime(Months[monthIndex - 1] + " " + String(monthDay) + "nd, " + year, 21, 1);
  else if (monthDay == 3) DisplayRawTime(Months[monthIndex - 1] + " " + String(monthDay) + "rd, " + year, 21, 1);
  else DisplayRawTime(Months[monthIndex - 1] + " " + String(monthDay) + "th, " + year, 21, 1);
  // Clear the field part for the time
  display.fillRect(0, 34, SCREEN_WIDTH, 16, SSD1306_BLACK);
  // Display time with blinking colons
  display.setTextSize(2);  // Set text size to 2 for time
  if (!is24HourFormat) display.setCursor(6, 34);
  if (is24HourFormat) display.setCursor(15, 34);
  uint8_t displayHours = hoursIndex;  // Store the hours to display
  String ampm = "";
  if (!is24HourFormat) {
    // Convert hours to 12-hour format
    if (hoursIndex >= 12) {
      ampm = "PM";
      if (hoursIndex > 12) {
        displayHours = hoursIndex - 12;
      }
    } else {
      ampm = "AM";
      if (hoursIndex == 0) {
        displayHours = 12;
      }
    }
  }
  // Handle 12-hour format without leading zero for hours
  if (!is24HourFormat && displayHours < 10) {
    display.print(" ");
  } else {
    display.print(displayHours < 10 ? "0" : "");  // Leading zero for hours in 24-hour format
  }
  display.print(displayHours);
  if (blinkFlag) {
    display.print(":");
  } else {
    display.print(" ");
  }
  display.print(minutesIndex < 10 ? "0" : "");  // Leading zero for minutes
  display.print(minutesIndex);
  if (!blinkFlag) {
    display.print(":");
  } else {
    display.print(" ");
  }
  display.print(secondsIndex < 10 ? "0" : "");  // Leading zero for seconds
  display.print(secondsIndex);
  // Display AM/PM for 12-hour format
  if (!is24HourFormat) {
    display.setTextSize(1);  // Set text size to 1 for AM/PM
    display.print(" ");
    display.print(ampm);  // Display AM/PM
  }
  // Update blink flag
  blinkFlag = !blinkFlag;
  DrawXLine(0, 52, 52); 
}
void AutoTextDisplay(String s, int x, int y, int txtSize) {
  DisplayRawText(s, x, y, txtSize);
  display.display();
}
void DisplayRawText(String s, int x, int y, int txtSize) {
  display.setTextSize(txtSize);  // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(x, y);
  display.println(s);
}
void DisplayRawTime(String s, int y, byte txtSize) {
  byte txtWidth;
  if (txtSize == 1) txtWidth = 6;
  if (txtSize == 2) txtWidth = 12;
  int center = display.width() / 2;
  int textLength = s.length() * txtWidth;
  display.setTextSize(txtSize);  // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(center - (textLength / 2), y);
  display.println(s);
}
void DisplayScrollText(String s, int y, byte txtSize) {
  byte txtWidth;
  if (txtSize == 1) txtWidth = 6;
  if (txtSize == 2) txtWidth = 12;
  int center = display.width() / 2;
  int textLength = s.length() * txtWidth;
  xPos--;
  display.fillRect(0, y, SCREEN_WIDTH, 8, SSD1306_BLACK);
  display.setTextSize(txtSize);  // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(xPos, y);
  display.println(s);
  if (xPos <= -textLength) {
    xPos = SCREEN_WIDTH + 20;
  }
}
void DisplayScrollTextPaused(String texMessage, int y, byte txtSize) {
  byte txtWidth;
  if (txtSize == 1) txtWidth = 6;
  if (txtSize == 2) txtWidth = 12;
  
  int TxtLength = texMessage.length() * txtWidth;
  // State machine to manage scrolling and pausing
  switch (scrollState) {
    case SCROLLING:
      xPos2--;
      // Check if the text should pause in the center of the display
      if (xPos2 == (SCREEN_WIDTH - TxtLength) / 2) {
        scrollState = PAUSED;
        pauseStartTime = millis();  // Record the start time of the pause
      }
      // Clears the bottom part of the display for the scrolling text
      display.fillRect(0, y, SCREEN_WIDTH, 8, SSD1306_BLACK);
      
      display.setTextSize(txtSize);
      display.setCursor(xPos2, y);
      display.print(texMessage);
      if (xPos2 <= -TxtLength - 10) {
        xPos2 = SCREEN_WIDTH + 10;
      }      
      break;
    case PAUSED:
      // Check if the pause duration has elapsed
      if (millis() - pauseStartTime >= 10000) {
        scrollState = SCROLLING;  // Resume scrolling
      }
      break;
  }  
}
void Time(bool forceUpdate) {
  digitalWrite(LedBuiltIn, HIGH);
  // Initialize at non-valid numbers so that the function updates their values on first call
  static uint8_t lastminute{ 61 };
  static uint8_t lasthour{ 25 };
  static uint8_t lastday;
  static uint8_t lastmonth;
  // The formattedDate comes with the following format:
  // 2018-05-28T16:00:13Z
  // We need to extract date and time
  formattedDate = timeClient.getFormattedTime();
  secondsIndex = timeClient.getSeconds();
  minutesIndex = timeClient.getMinutes();
  if (forceUpdate || lastminute != minutesIndex) {
    //if (lastminute != minutesIndex) {
    hoursIndex = timeClient.getHours();
    lastminute = minutesIndex;
    if (lasthour != hoursIndex) {
      dayIndex = timeClient.getDay();
      monthDay = getMonthDay();
      lasthour = hoursIndex;
      if (lastday != monthDay) {
        monthIndex = getMonth();
        lastday = monthDay;
        if (lastmonth != monthIndex) {
          year = getYear();
          lastmonth = monthIndex;
        }
      }
    }
  }
  digitalWrite(LedBuiltIn, LOW);
}
int getMonthDay() {
  int monthday;
  time_t rawtime = timeClient.getEpochTime();
  struct tm* ti;
  ti = localtime(&rawtime);
  monthday = (ti->tm_mday) < 10 ? 0 + (ti->tm_mday) : (ti->tm_mday);
  return monthday;
}
int getMonth() {
  int month;
  time_t rawtime = timeClient.getEpochTime();
  struct tm* ti;
  ti = localtime(&rawtime);
  month = (ti->tm_mon + 1) < 10 ? 0 + (ti->tm_mon + 1) : (ti->tm_mon + 1);
  return month;
}
int getYear() {
  int year;
  time_t rawtime = timeClient.getEpochTime();
  struct tm* ti;
  ti = localtime(&rawtime);
  year = ti->tm_year + 1900;
  return year;
}
void DrawLineAnimated(int y, int k) {
  uint8_t i;
  //display.clearDisplay();
  display.fillRect(0, y, SCREEN_WIDTH, 1, SSD1306_BLACK);
  for (i = 0; i < display.width(); i += 2) {
    display.drawLine(0, y, i, k, SSD1306_WHITE);
    display.display();  // Update screen with each newly-drawn line
  }
}
void DrawXLine(int x, int y, int k) {
  for (x; x < display.width(); x++) {
    display.drawLine(x, y, x, k, SSD1306_WHITE);
  }
}