#include "TFT_eSPI.h"
#include "ChronoBLE.h"
#include "ChronoVersion.h"

// External declaration for BLE reading flag and value
extern volatile bool newReadingAvailable;
extern volatile int lastReadFPS;
#include "ChronoReading.h"
#include <LinkedList.h>
#include "Display.h"
#include "ChronoReadingManager.h"
#include "ChronoWebServer.h"
#include "FileManager.h"
#include <WiFi.h>
#include <time.h>
#include <sys/time.h>

//testing mode variables
static bool testMode=false;
static long testModeLastAdd=0;

//general variables
static char currentTime[64];


//chronograph setup
static bool chronoStateConnected=false;
static ChronoBLE chronoBLE;
static ChronoWebServer webServer;



void speedCallback(int fps){
    addReading(fps);
}
void addReading(int fps){
    ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
    
    //add reading to manager
    chronoReadings.addReading(fps);

    //update display
    Display::updateEntireDisplay();

    if (chronoReadings.getShotCount() % 10 == 0){
        // Get filesystem information
        unsigned int totalBytes = LittleFS.totalBytes();
        unsigned int usedBytes = LittleFS.usedBytes();
        unsigned int freeBytes = totalBytes - usedBytes;

        Serial.printf("\t(%u) Available Disk / Memory: %u / %u\n", chronoReadings.getShotCount(), freeBytes, ESP.getFreeHeap());
    }


}
void toggleTestMode(){
    if (testMode){
        Serial.println("disabling test mode....");
        testMode=false;
    }
    else {
        Serial.println("enabling test mode....");
        testMode=true;
    }
}
void setup()
{
    Serial.begin(115200);
    Serial.println("starting......");

    //initialize the file manager
    FileManager& files = FileManager::getInstance();
    files.init();

    //initialize web portal (also connects to WiFi)
    webServer.init();
    webServer.setTestModeCallback(toggleTestMode);

    //sync time via NTP after WiFi is connected
    setDateTime();

    //initialize display
    Display::init();


    //initialize gun and projectile profile
    ChronoReadingManager& chronoReadings = ChronoReadingManager::getInstance();
    chronoReadings.initializeProfiles();
    //Display::updateProfile(chronoReadings.getProfile());


    //initialize bluetooth
    chronoBLE.init();
    chronoBLE.setSpeedCallback(speedCallback);
}
void setDateTime(){
  // NTP server configuration - ESP32 API uses gmtOffset (seconds) and dstOffset (seconds)
  // EST = UTC-5, EDT = UTC-4, so gmtOffset = -5*3600 = -18000
  configTime(-18000, 3600, "pool.ntp.org", "time.nist.gov");

  // Wait for time to be set
  time_t now;
  struct tm timeinfo;
  int retry = 0;
  const int maxRetry = 20; // 10 seconds max

  while (retry < maxRetry) {
    time(&now);
    localtime_r(&now, &timeinfo);

    // Check if we have a valid time (year > 2020)
    if (timeinfo.tm_year > 120) {  // 120 = year 2020
      Serial.println("Time synced via NTP");
      break;
    }
    Serial.print("Waiting for NTP sync... ");
    Serial.println(retry);
    delay(500);
    retry++;
  }

  // If still not synced, fall back to a default date
  if (timeinfo.tm_year <= 120) {
    Serial.println("NTP sync failed, using fallback date");
    timeinfo.tm_year = 2024 - 1900;
    timeinfo.tm_mon = 0;  // January
    timeinfo.tm_mday = 1;
    timeinfo.tm_hour = 0;
    timeinfo.tm_min = 0;
    timeinfo.tm_sec = 0;
    timeinfo.tm_isdst = -1;

    now = mktime(&timeinfo);
    struct timeval tv = {.tv_sec = now};
    settimeofday(&tv, NULL);
  }
}
void updateTime(){
    time_t now;
    char strftime_buf[64];
    struct tm timeinfo;
    time(&now);
    // Use the timezone that was configured via configTime
    localtime_r(&now, &timeinfo);
    strftime(strftime_buf, sizeof(strftime_buf), "%R", &timeinfo);

    if (strcmp(currentTime, strftime_buf) != 0) {
        strcpy(currentTime, strftime_buf);
        Display::updateTime(strftime_buf);
        Display::refreshDisplay();
    }
}
void checkBLE(){
    if (chronoBLE.isIdle()){
        if (chronoStateConnected){
            chronoStateConnected=false;
            Display::showChronographDisconnected();
            Display::refreshDisplay();
        }
        chronoBLE.startScan();
    }
    else if (chronoBLE.isConnected()){
        //update display
        if (!chronoStateConnected){
            chronoStateConnected=true;
            Display::showChronographConnected();
            Display::refreshDisplay();
        }
    }
    else if (chronoBLE.foundDevice()){
        chronoBLE.connectToDevice();
    }
}

void loop()
{
    //process BLE connection
    checkBLE();

    //process web server
    webServer.process();

    //update time
    updateTime();

    //////////////////////////////////////////////////////
    //run testmode commands if in test mode
    if (testMode){
        if (testModeLastAdd==0){Display::showChronographConnected();}
        if (millis()>(testModeLastAdd+5000)){
            long randomNumber = random(890, 920);
            addReading(randomNumber);
            testModeLastAdd=millis();
        }
    }
    //////////////////////////////////////////////////////

    // Check for new BLE readings and process them
    // This is done here instead of in the BLE callback to avoid heap fragmentation
    // and display updates in interrupt context
    if (newReadingAvailable) {
        newReadingAvailable = false;
        addReading(lastReadFPS);
    }

    //update display
    Display::refreshDisplay();

    delay(2);
}
