
/*
  wifi is started in STATION mode and tries to connected to AP saved 
  in the creds.json file. It will keep trying to connect forever. If
  it connects then the board LEDS will toggle, the buzzer will play a tune
  and the IP address will be sent to the serial port. Also the device will
  be discoverable as "http://atadev.local/" 

  If credentials need to be changed then double reset can be done to 
  trigger the AP mode and a wifi config page will be available on address
  192.168.4.1. Credentials can be entered and applied then you can reset 
  the device so it connects with the correct credentials.

*/
#include "my_wifi.h"

#include <cstdint>
#include <cstddef>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <FS.h>
#include <LittleFS.h>
#include <SD.h>

#include <Wifi.h>
#include <ESPmDNS.h>
#include <DNSServer.h>
#include <esp_task_wdt.h>

#include <Update.h>
#include <ArduinoJson.h>

#include "global.h"
#include <esp_wifi.h>
#include "my_buzzer.h"
#include "my_board.h"
#include "common/led_animation.h"
#include "led_strip.h"
#include "common/fileSystem.h"
#include "JsonConstrain.h"
#include "led_strip.h"
#include "my_oled.h"
#include "BTSerial.h"
#include "esp_log.h"

#include "firmware_html.h"

int RebootSystem = 0;
#define WIFI_TIMEOUT_MS 10000

static const char* tag = "wifi";

bool WifiClientConnected = false;
AsyncWebServer webServer(80);
DNSServer *dnsServer;
#define DNS_PORT 53

#define StartDelayedRebooth RebootSystem = 1000/BUTTON_UPDATE_PERIOD; // start reboot countdown for 1sec

String ssid;
String passPhrase;

String AP_SSID;
String AP_Pass;
String mDnsName;
String HostName;

IPAddress local_IP(192,168,10,1);
IPAddress gateway(192,168,10,200);
IPAddress subnet(255,255,255,0);

// for file manager page
String filesDropdownOptions((char*)0);
String dirDropdownOptions((char*)0);
String savePath((char*)0); // needed for storing file when editing a file
String savePathInput((char*)0);
const char* http_username = "admin";
const char* http_password = "admin";
const char* param_delete_path = "delete-path";
const char* param_edit_path = "edit-path";
const char* param_dir_pad = "dir-path";
const char* param_edit_textarea = "edit-textarea";
const char* param_save_path = "save-path";
String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg";
//const char* jquery = "/jquery-3.6.3.min.js";

void Init_Wifi_Task(void)
{
  File file = LittleFS.open("/cfg/wifi.json", "r");
  if(!file){
    ESP_LOGE(tag, "Failed to open wifi.json!");
    file.close();
    return;
  }

  // Parse the JSON file
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, file);
  if(!error){
    JsonObject wifiJson = doc["wifi"];
    file.close();
    mDnsName = jsonConstrainString(tag, wifiJson,"mdns-name", "atadev");
    ESP_LOGD(tag, "mDnsName: %s", mDnsName.c_str());

    if(jsonConstrainBool(tag, wifiJson, "en", false)){
      JsonObject j = doc["cred1"];
      Wifi_Read_Credentials( j );
      xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*10, NULL, 1, NULL, CONFIG_ARDUINO_RUNNING_CORE);
      ESP_LOGD(tag, "Initialized  WiFi (task created)...");
    }

    JsonObject ap = doc["ap"];
    AP_SSID = jsonConstrainString(tag, ap, "ssid", "ATA_AP");
    if(jsonConstrainBool(tag, ap, "append-id", true)){
      String macHex = String(chipInfo.macByte[1], HEX) + String(chipInfo.macByte[0], HEX);
      AP_SSID += "_";
      macHex.toUpperCase();
      AP_SSID += macHex;
    }
    AP_Pass = jsonConstrainString(tag, ap, "pass", "12345678");
  }else{
    ESP_LOGE(tag, "Deserialization error for wifi.json");
  }
}

void onWiFiEvent(WiFiEvent_t event)
{
    //Serial.printf("[WiFi-event] event: %d\n", event);
    switch (event) {
        case ARDUINO_EVENT_WIFI_READY: 
            //Serial.println("WiFi interface ready");
            break;
        case ARDUINO_EVENT_WIFI_SCAN_DONE:
            //Serial.println("Completed scan for access points");
            break;
        case ARDUINO_EVENT_WIFI_STA_START:
            //Serial.println("WiFi client started");
            break;
        case ARDUINO_EVENT_WIFI_STA_STOP:
            //Serial.println("WiFi clients stopped");
            break;
        case ARDUINO_EVENT_WIFI_STA_CONNECTED:
            WifiClientConnected = true;
            //Serial.println("Connected to access point");
            break;
        case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
            WifiClientConnected = false;
            ESP_LOGI(tag,"WiFi Disconnected");
            //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); 
            break;
        case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
            //Serial.println("Authentication mode of access point has changed");
            break;
        case ARDUINO_EVENT_WIFI_STA_GOT_IP:
            ESP_LOGI(tag,"My IP: %s", WiFi.localIP().toString());
            Wifi_Start_MDNS();
            Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); 
            break;
        case ARDUINO_EVENT_WIFI_STA_LOST_IP:
            //Serial.println("Lost IP address and IP address is reset to 0");
            break;
        case ARDUINO_EVENT_WPS_ER_SUCCESS:
            //Serial.println("WiFi Protected Setup (WPS): succeeded in enrollee mode");
            break;
        case ARDUINO_EVENT_WPS_ER_FAILED:
            //Serial.println("WiFi Protected Setup (WPS): failed in enrollee mode");
            break;
        case ARDUINO_EVENT_WPS_ER_TIMEOUT:
           // Serial.println("WiFi Protected Setup (WPS): timeout in enrollee mode");
            break;
        case ARDUINO_EVENT_WPS_ER_PIN:
            //Serial.println("WiFi Protected Setup (WPS): pin code in enrollee mode");
            break;
        case ARDUINO_EVENT_WIFI_AP_START:
            //Serial.println("WiFi access point started");
            break;
        case ARDUINO_EVENT_WIFI_AP_STOP:
            //Serial.println("WiFi access point  stopped");
            break;
        case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
            //Serial.println("Client connected");
            break;
        case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
            ESP_LOGI(tag,"SoftAP Client Disconnected");
            //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); 
            break;
        case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
            ESP_LOGI(tag,"SoftAP Client Connected");
            Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); 
            break;
        case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
            //Serial.println("Received probe request");
            break;
        case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
            //Serial.println("AP IPv6 is preferred");
            break;
        case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
            //Serial.println("STA IPv6 is preferred");
            break;
        case ARDUINO_EVENT_ETH_GOT_IP6:
            //Serial.println("Ethernet IPv6 is preferred");
            break;
        case ARDUINO_EVENT_ETH_START:
            //Serial.println("Ethernet started");
            break;
        case ARDUINO_EVENT_ETH_STOP:
            //Serial.println("Ethernet stopped");
            break;
        case ARDUINO_EVENT_ETH_CONNECTED:
            //Serial.println("Ethernet connected");
            break;
        case ARDUINO_EVENT_ETH_DISCONNECTED:
            //Serial.println("Ethernet disconnected");
            break;
        case ARDUINO_EVENT_ETH_GOT_IP:
           // Serial.println("Obtained IP address");
            break;
        default: break;
    }
}

void Wifi_Task(void *parameters)
{
  // Extend watchdog timer
  esp_task_wdt_init(2, false);

  Setup_WebServer_Handlers(&webServer);
  WiFi.onEvent(onWiFiEvent);
  WiFi.setHostname(mDnsName.c_str());
  WiFi.softAPConfig(local_IP, gateway, subnet);
  WiFi.softAP(AP_SSID.c_str(), AP_Pass.c_str());
  Wifi_Start_MDNS();
  vTaskDelay(100);
  webServer.begin();

  // Choose WIFI Mode
  if(commMode == COMM_WIFI_AP_BLE){ // AP Only
    WiFi.mode(WIFI_AP); 
    while(1){ vTaskDelay(500 / portTICK_PERIOD_MS); }
  }else{ // AP & Client  (dslrbooth)
    esp_wifi_set_ps(WIFI_PS_NONE); 
    WiFi.mode(WIFI_AP_STA);
    Wifi_Client_Loop();
  }
}

void Wifi_Client_Loop(){
  while(true){
    // Wifi status check loop
    if(WiFi.status() == WL_CONNECTED){     
      vTaskDelay(250);
      continue; 
    }

    // Start WiFi Client
    ESP_LOGD(tag, "Wifi trying stored creds: %s..%s",ssid.c_str(), passPhrase.c_str());
    WiFi.begin(ssid.c_str(), passPhrase.c_str());
    
    //Wait until connected or timeout
    ESP_LOGV(tag, "WiFi Connecting...");
    unsigned long startAttemptTime = millis();
    while(WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS){ 
      vTaskDelay(100 / portTICK_PERIOD_MS);
    }

    // Delay if wifi connection fails and continue
    if(WiFi.status() != WL_CONNECTED){
      ESP_LOGW(tag, "WIFI Connection Failed!");
      for(int i = 0; i < WIFI_TIMEOUT_MS/100; i++){
        vTaskDelay(100);
      }
      continue; 
    }
  }
}

void Wifi_Start_MDNS(void)
{
  ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str());
  if (!MDNS.begin(mDnsName.c_str())) {
    ESP_LOGE(tag, "Error setting up MDNS responder!");
  }else{
    ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName);
  }
}

void Wifi_Read_Credentials(const JsonObject &credJson){
  if(credJson.isNull()){ // set generic defaults on error
    ESP_LOGE(tag, "Failed to find wifi key");  
    
    ssid = "Generic";
    passPhrase = "password";
    //strncpy(ssid, "Generic", sizeof(ssid)-1);
    //strncpy(passPhrase, "password", sizeof(passPhrase)-1);
    return;
  }
  else{
    // TODO Restore String
    ssid = jsonConstrainString(tag, credJson, "ssid", "default");
    passPhrase = jsonConstrainString(tag, credJson, "pass", "password");
    //strncpy(ssid, jsonConstrainString(tag, credJson, "ssid", "default").c_str(), sizeof(ssid)-1);
    //strncpy(passPhrase, jsonConstrainString(tag, credJson, "pass", "password").c_str(), sizeof(passPhrase)-1);
  }
}

void Wifi_Save_Credentials(String newSSID, String newPass)
{
 // Open the credentials file writing
  File credsFile = LittleFS.open("/cfg/wifi.json", "r+");
  if(!credsFile){
    ESP_LOGE(tag, "Failed to open wifi.json\n");
    return;
  }

  // Parse the JSON file
  JsonDocument doc;
  DeserializationError error = deserializeJson(doc, credsFile);
  if(!error){
    JsonObject cred1Json = doc["cred1"].to<JsonObject>();

    if(cred1Json){
      cred1Json["ssid"] = newSSID;
      cred1Json["pass"] = newPass;

      // Clear file contents before saving the modified JSON
      credsFile.seek(0);

      int written = serializeJson(doc, credsFile); // save to file
      ESP_LOGD(tag, "Credentials saved... ssid: %s, pass: %s, size written: %d", newSSID, newPass, written);
    }
    credsFile.close();
  }else{
    ESP_LOGE(tag, "Deserialization error for wifi.json");
  }
}

void Setup_WebServer_Handlers(AsyncWebServer* serv)
{
  serv->on("/dslrbooth", HTTP_GET, handleGET_DSLRBooth);

  serv->on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    if(WiFi.getMode() == WIFI_AP && !WifiClientConnected){
      request->redirect("/wifi");
    }else{
      request->redirect("/home");
    }
  });
  serv->on("/home", HTTP_GET, [](AsyncWebServerRequest *request){
    sendHtmlFile("/www/home.html", request, HomeHtmlProcessor);
  });
  serv->on("/lights", HTTP_GET, [](AsyncWebServerRequest *request){
    sendHtmlFile("/www/lights.html", request, htmlProcessor);
  });
  serv->on("/setup", HTTP_GET, [](AsyncWebServerRequest *request){
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }
    sendHtmlFile("/www/setup.html", request, htmlProcessor);
  });
  serv->on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){
    if(WiFi.getMode() == WIFI_MODE_APSTA){
      // TODO Disable navigation bar
    }
    sendHtmlFile("/www/wifi.html", request, htmlProcessor);
  });  
  serv->on("/files", HTTP_GET, [](AsyncWebServerRequest *request){
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }
    sendHtmlFile("/www/files.html", request, htmlProcessor);
  });
  serv->on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handlePOST_Upload);
  
  serv->on("/download", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam("file")) {
      String filename = request->getParam("file")->value();
      if (LittleFS.exists(filename)) {
        fs::File file = LittleFS.open(filename, "r");
        if (file) {
          request->send(LittleFS, filename, "application/octet-stream");
          file.close();
          return;
        }
      }
    }
    request->send(404, "text/plain", "File not found!");
  });
  serv->on("/delete", HTTP_GET, [](AsyncWebServerRequest *request){
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }

    String filename = request->getParam(param_delete_path)->value();
    if(filename !="choose"){
      LittleFS.remove(filename.c_str());
      ESP_LOGD(tag, "Deleted file: %s", filename.c_str()); 
    }

    request->redirect("/files");
  });
  serv->on("/edit", HTTP_GET, [](AsyncWebServerRequest *request){
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }

    String fileName = request->getParam(param_edit_path)->value();
    if(fileName =="new"){
      savePath = "/new.txt";
    }
    else{
      savePath = fileName;
    }

    ESP_LOGD(tag, "Edit file: %s", savePath.c_str());
    sendHtmlFile("/www/edit.html", request, htmlProcessor);
  });
  serv->on("/save", HTTP_GET, [](AsyncWebServerRequest *request){
    if(!request->authenticate(http_username, http_password)){
      return request->requestAuthentication();
    }
    String inputMessage((char*)0);
    if (request->hasParam(param_edit_textarea)) {
      inputMessage = request->getParam(param_edit_textarea)->value();
    }
    if (request->hasParam(param_save_path)) {
      savePath = request->getParam(param_save_path)->value();
    }
    writeFile(LittleFS, savePath.c_str(), inputMessage.c_str());

    request->redirect("/files");
  });
  serv->on("/update", HTTP_POST, handlePOST_Update, updateCallback);

  // Request Data
  serv->on("/get", HTTP_GET, handleGET_Get);
  // Post Data
  serv->on("/post", HTTP_POST, handlePOST_Post, postFileUpload, postBody);

  serv->on("/generate_204", HTTP_GET, [](AsyncWebServerRequest *request){
    ESP_LOGD(tag, "/generate_204 ... redirect to /wifi");
    request->redirect("/wifi");
  });

  serv->on("/hotspot-detect.html", HTTP_GET, [](AsyncWebServerRequest *request){
    ESP_LOGD(tag, "/hotspot-detect.html ... redirect to /wifi");
    request->redirect("/wifi");
  });

  serv->on("/msftconnecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request){
    ESP_LOGD(tag, "/msftconnecttest.txt ... redirect to /wifi");
    request->redirect("/wifi");
  });

  serv->on("/reg-stick", HTTP_POST, handlePOST_RegStick); 

  serv->on("/firmware", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", firmware_html_page);
  });

  serv->onNotFound([](AsyncWebServerRequest *request){ 
    if(WiFi.getMode() == WIFI_MODE_APSTA){
      ESP_LOGD(tag, "OnNotFound Redirect to /wifi");
      request->redirect("/wifi");
    }else if(WiFi.getMode() == WIFI_MODE_AP){
      ESP_LOGD(tag, "OnNotFound Redirect to /home");
      request->redirect("/home");
    }else{
      request->send(404); 
    }
  });

  serv->on("/*", HTTP_GET, handleGET_SendFile);  // send any file

}

void handlePOST_RegStick(AsyncWebServerRequest *request){

}

bool getpostSuccess = false;
// 
void handleGET_Get(AsyncWebServerRequest *request){
  if(request->hasParam("type", false, false)) {
    const char* dataType = request->getParam("type", false, false)->value().c_str();

    if(!strcmp(dataType, "app-events")){ // send json file
      request->send(LittleFS, "/cfg/app-events.json", "application/json");
      return;
    }

    if(!strcmp(dataType, "anim-profiles")){ // send json file
      request->send(LittleFS, "/cfg/anim-profiles.json", "application/json");
      return;
    }

    if(!strcmp(dataType, "anim-list")){
      request->send(LittleFS, "/cfg/anim-list.json", "application/json");
      return;
    }
    
    if(!strcmp(dataType, "sys-summary")){
      JsonDocument doc;
      CreateSysSummmaryPacket(doc);
      String summary;
      serializeJson(doc, summary); 
      request->send(200, "application/json", summary);
      return;
    }
    
    if(!strcmp(dataType, "wifi")){
      JsonDocument jsdoc;
      jsdoc["ssid"] = ssid;
      //jsdoc["pass"] = (int)strlen(passPhrase); 
      jsdoc["pass"] = passPhrase;

      String jsStr;
      serializeJson(jsdoc, jsStr);

      request->send(200, "application/json", jsStr);
      return;
    }
  }

  request->send(400, "text/plain", "Failed");
}

void CreateSysSummmaryPacket(JsonDocument &doc){
  JsonObject booth = doc["booth"].to<JsonObject>();
  booth["mode"] = "";
  booth["app"] = sysProps.appName;
  booth["strip1"] = (strip1) ? true : false,
  booth["strip2"] = (strip2) ? true : false,
  booth["front-light"] = true;
  booth["rear-light"] = false;
  booth["oled"] = (oled) ? true : false;

  //ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getHeapSize()-ESP.getFreeHeap());

  JsonObject Temperature = doc["temperature"].to<JsonObject>();
  Temperature["temp"] = sysProps.t_sensor.temperature;
  Temperature["sp1"] = sysProps.t_sensor.Setpoint1;
  doc["system"]["firm-ver"] = FIRMWARE_VER;

  JsonObject chip = doc["chip"].to<JsonObject>();
  chip["flash-size"] = "";
  chip["flash-free"] = "";
  chip["ram-size"] = ESP.getHeapSize();
  chip["ram-free"] = ESP.getFreeHeap();



  // Get the Wi-Fi RSSI
  wifi_ap_record_t wifi_ap_info;
  esp_err_t rssi_result = esp_wifi_sta_get_ap_info(&wifi_ap_info);
  int rssi = 0; int wifi_ch = 0;
  String encryp = "";
  if (rssi_result == ESP_OK) { 
    rssi = wifi_ap_info.rssi; 
    wifi_ch = wifi_ap_info.primary;

    wifi_auth_mode_t auth_mode = wifi_ap_info.authmode;
    switch (auth_mode) {
        case WIFI_AUTH_OPEN:
            encryp = "Open";
            break;
        case WIFI_AUTH_WEP:
            encryp = "WEP";
            break;
        case WIFI_AUTH_WPA_PSK:
            encryp = "WPA_PSK";
            break;
        case WIFI_AUTH_WPA2_PSK:
            encryp = "WPA2_PSK";
            break;
        case WIFI_AUTH_WPA_WPA2_PSK:
            encryp = "WPA_WPA2_PSK";
            break;
        case WIFI_AUTH_WPA2_ENTERPRISE:
            encryp = "WPA2_ENT";
            break;
        default:
            encryp = "UNKNOWN";
            break;
    }
  }

  JsonObject wifi = doc["wifi"].to<JsonObject>();
  wifi["client-ip"] = "XXX.XXX.XXX.XXX";
  wifi["mac-addr"] = chipInfo.macStr;
  wifi["ch"] = wifi_ch;
  wifi["rssi"] = rssi;
  wifi["encryp"] = encryp;

  JsonObject ble = doc["ble"].to<JsonObject>();
  ble["en"] = (sysProps.appIndex == DSLRBOOTH_INDEX)? false : true;
  ble["clients"] = (BTDeviceConnected)? 1 : 0;
  ble["ssid"] = BLEDeviceName;

}

void handlePOST_Post(AsyncWebServerRequest *request){
  ESP_LOGD(tag, "posts request..");
  if(!getpostSuccess) { request->send(400, "text/plain", "Error"); }

  request->send(200, "text/plain", "Ok");
}

void postFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
  getpostSuccess = false;
  Serial.println("post file");
}


// POST Requests
int packets, postTotal;
char postType[24];
char *postData;
void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){
  if (!index){ 
    packets = 0;
    int sectors = (total + 1023 + 64) / 1024;
    postData = new char[1024 * sectors];
  }
  
  // Accumulate Data Chunks
  memcpy(postData + index, data, len);
  packets++;

  if((index + len) >= total){ 
    ESP_LOGD(tag, "index: %d, len: %d, total: %d", index, len, total);
    ESP_LOGD(tag, "packets: %d", packets);
    getpostSuccess = false;

    // Check if the request contains the necessary parameters
    if (request->hasParam("type", false, false)) { 
      const char* dataType = request->getParam("type", false, false)->value().c_str();
      ESP_LOGD(tag, "data type: %s", dataType);

      if(!strcmp(dataType, "anim-profile-common") || !strcmp(dataType, "set-countdown")){
        JsonDocument profJson;
        DeserializationError error = deserializeJson(profJson, postData, total);

        if(!error){
          animProps.profileIndex = profJson["profile-index"].as<int>();

          JsonObject countdownJson = profJson["countdown"];
          animProps.frontLight.minDuty = countdownJson["min"].as<float>();
          animProps.frontLight.maxDuty = countdownJson["max"].as<float>();
          animProps.frontLight.holdTime = countdownJson["hold"].as<int>();
          animProps.frontLight.rampTime = countdownJson["ramp"].as<int>();
            
          int indexChanged = animProps.profileIndex - countdownJson["profile-index"].as<int>();
          animProps.profileIndex = countdownJson["profile-index"].as<int>();
          if(indexChanged){
            load_animation_profileByIndex(animProps.profileIndex);
          }  

          // Save to disk/flash
          if(dataType[0] == 'a'){
            if(updateJsonDocument(profJson, "/cfg/anim-profile-common.json")){
              Buzzer_Beep(150, 3000);
            }
          }
          else{
            ESP_LOGD(tag, "Post:set-countdown, index: %d, min: %.1f, max: %.1f, hold: %d, ramp: %d", animProps.profileIndex, animProps.frontLight.minDuty, animProps.frontLight.maxDuty, animProps.frontLight.holdTime, animProps.frontLight.rampTime); 
            Buzzer_Beep(150, 3000);
          }
        }else{
          ESP_LOGE(tag, "Deserialization error for wifi.json");
        }

        getpostSuccess = true;
        //goto done;
      }
      else if(!strncmp(dataType, "anim-profile", 12)){
        if (strlen(dataType) > 12) {
          int profile_index = (dataType[12] - '0' - 1) % 8; // extract index, assuming dataType has enough characters
          JsonDocument profJson;
          DeserializationError error = deserializeJson(profJson, postData);

          if(!error){
            JsonArray eventsArray = profJson["events"];
            for(int i = 0; i < eventsArray.size(); i++ ){
              animProps.event[i].selfIndex = i;
              animProps.event[i].animIndex = eventsArray[i]["anim"].as<int>();
              animProps.event[i].hue = eventsArray[i]["hue"].as<int>();
              animProps.event[i].hueRange = eventsArray[i]["hue-range"].as<int>();
              animProps.event[i].speed = eventsArray[i]["speed"].as<int>();
              animProps.event[i].param1 = eventsArray[i]["param1"].as<int>();
              animProps.event[i].param2 = eventsArray[i]["param2"].as<int>();
              animProps.event[i].check1 = eventsArray[i]["check1"].as<bool>();
              animProps.event[i].check2 = eventsArray[i]["check2"].as<bool>();
              animProps.event[i].check3 = eventsArray[i]["check3"].as<bool>();
              animProps.event[i].check4 = eventsArray[i]["check4"].as<bool>();
              animProps.event[i].countDown = 0;
            }

            ESP_LOGD(tag, "Extracted/Updated active profile to animProps.");

            // Save to disk/flash
            String filePath = "/cfg/anim-profile" + String(profile_index + 1) + ".json";
            if(updateJsonDocument(profJson, filePath.c_str())){
              Buzzer_Beep(150, 3000);
            }

          }else{
            ESP_LOGE(tag, "Deserialization error for wifi.json");
          }

          getpostSuccess = true;
        }
      }
      else if(!strcmp(dataType, "play-anim")){
        JsonDocument animData;
        DeserializationError error = deserializeJson(animData, postData);
        if(!error){
          ANIMATION_EVENT testEvent;
          testEvent.selfIndex = animData["index"].as<int>();
          testEvent.animIndex = animData["anim"].as<int>();
          testEvent.hue = animData["hue"].as<int>();
          testEvent.hueRange = animData["hue-range"].as<int>();
          testEvent.speed = animData["speed"].as<int>();
          testEvent.param1 = animData["param1"].as<int>();
          testEvent.param2 = animData["param2"].as<int>();
          testEvent.check1 = animData["check1"].as<int>();
          testEvent.check2 = animData["check2"].as<int>();
          testEvent.check3 = animData["check3"].as<int>();
          testEvent.check4 = animData["check4"].as<int>();

          if(testEvent.selfIndex != 0){
            testEvent.selfIndex = 1;
          }
          ESP_LOGI(tag, "Post:play-anim, index: %d, anim: %d, hue: %d, rng: %d, speed: %d, par1: %d, par2: %d, chk1: %d, chk2: %d, chk3: %d, chk4: %d", testEvent.selfIndex, testEvent.animIndex, testEvent.hue, testEvent.hueRange, testEvent.speed, testEvent.param1, testEvent.param2, testEvent.check1, testEvent.check2, testEvent.check3, testEvent.check4);
          testEvent.countDown = 0; // set to zero so param1 determines countdown time
          testEvent.type = EV_TEST;
          PostNewEvent(testEvent); // eventIndex always = 1
          Buzzer_Beep(150, 3000);
        }else{
          ESP_LOGE(tag, "Deserialization error for play-anim");
        }

        getpostSuccess = true;
      }
      else if(!strcmp(dataType, "wifi")){
        JsonDocument wifiCreds;
        DeserializationError error = deserializeJson(wifiCreds, postData, total);
        if(!error){
          //read credentials
          //const char* new_ssid = wifiCreds["ssid"];
          //const char* new_pass = wifiCreds["pass"];

          String new_ssid = wifiCreds["ssid"];
          String new_pass = wifiCreds["pass"];
          ESP_LOGV(tag, "SSID: %s PASS: %s", new_ssid, new_pass);
          // Save Credentials if different
          //if(strcmp(new_ssid, ssid)!=0 || strcmp(new_pass, passPhrase)!=0){
          if(new_ssid != ssid  || new_pass != passPhrase){
            Wifi_Save_Credentials(new_ssid, new_pass);
            StartDelayedRebooth;
          }
        }else{
          ESP_LOGE(tag, "Deserialization error for wifi");
        }

        getpostSuccess = true;
      }
      else if(!strcmp(dataType, "set-pixel")){
        JsonDocument js;
        DeserializationError error = deserializeJson(js, postData);
        if(!error){
          ANIMATION_EVENT testEvent;
          testEvent.selfIndex = 1;
          testEvent.animIndex = 81; 
          testEvent.hue = js["hue"].as<int>();
          testEvent.param1 = js["index"].as<int>();
          testEvent.type = EV_TEST;
          PostNewEvent(testEvent); // eventIndex always = 1
          ESP_LOGD(tag, "Post: set-pixel, hue: %d, pixel: %d", testEvent.hue, testEvent.param1);
          Buzzer_Beep(150, 3000);
        }else{
          ESP_LOGE(tag, "Deserialization error for set-pixel");
        }

        Buzzer_Beep(50, 3000);
        getpostSuccess = true;
      }  
      else if(!strcmp(dataType, "clear-strip")){
        ANIMATION_EVENT testEvent;
        testEvent.selfIndex = 1;
        testEvent.animIndex = 80; 
        ESP_LOGD(tag, "Post: clear-strip");
        testEvent.type = EV_TEST;
        PostNewEvent(testEvent); // eventIndex always = 1

        getpostSuccess = true;
        Buzzer_Beep(50, 3000);
      }  
      else if(!strcmp(dataType, "setup-save")){
        JsonDocument js;
        DeserializationError error = deserializeJson(js, postData);
        if(!error){
          // If app index is different open app-events.json and update
          if(sysProps.appIndex != js["appindex"].as<int>()){
            File file = LittleFS.open("/cfg/app-events.json", "r+");
            if(!file){
              ESP_LOGE(tag, "Failed to open app-events.json");
              file.close();
              return;
            }

            JsonDocument doc;
            DeserializationError error = deserializeJson(doc, file);
            file.close();

            if(!error){
              // Update index value
              doc["index"] = js["appindex"].as<int>();
              ESP_LOGD(tag, "New App Index = %d", doc["index"].as<int>());

              if(updateJsonDocument(doc, "/cfg/app-events.json")){
                Buzzer_Beep(150, 3000);
              }
            }else{
               ESP_LOGE(tag, "Deserialization error for app-events.json");
            }
          }
        }else{
          ESP_LOGE(tag, "Deserialization error for seteup-save");
        }

        // if any of the values are diiferent then open led-devices.json and update
        //if app index is different open app-events.json and update
        bool editJson = false;
        //if(js["en1"].as<int>() != strip1->size) { editJson = true; }
        if(js["count1"].as<int>() != strip1->size) { editJson = true; }
        else if(js["shift1"].as<int>() != strip1->shift) { editJson = true; }
        else if(js["offset1"].as<int>() != strip1->offset) { editJson = true; }
        else if(js["rgb1"].as<int>() != strip1->ledOrder) { editJson = true; }
        else if(js["power1"].as<int>() != strip1->powerDiv) { editJson = true; }

        if(editJson){
          File file = LittleFS.open("/cfg/led-devices.json", "r+");
          if(!file){
            ESP_LOGE(tag, "Failed to open led-devices.json");
            file.close();
            return;
          }

          JsonDocument doc;
          DeserializationError error = deserializeJson(doc, file);
          file.close();

          if(!error){
            JsonObject jsStrip1 = doc["strip1"].to<JsonObject>(); // nested so it doesn't create a copy

            jsStrip1["en"] = true;
            jsStrip1["size"] = js["count1"].as<int>();
            jsStrip1["shift"] = js["shift1"].as<int>();
            jsStrip1["offset"] = js["offset1"].as<int>();
            jsStrip1["power-div"] = js["power1"].as<int>();
            jsStrip1["rgb-order"] = js["rgb1"];

            // Save Json File
            if(updateJsonDocument(doc, "/cfg/led-devices.json")){
              Buzzer_Beep(150, 3000);
            }
          }else{
            ESP_LOGE(tag, "Deserialization error for led-devices.json");
          }
        }

        ESP_LOGD(tag, "Post: setup-save");
        getpostSuccess = true;
        Buzzer_Beep(150, 3000);
      } 
      else if(!strcmp(dataType, "restart")){
        StartDelayedRebooth;

        ESP_LOGD(tag, "Post: restart");
        getpostSuccess = true;
        Buzzer_Beep(150, 3000);
      } 
    }

    // Delete the allocated buffer (owned by the request->_tempObject)
    delete[] postData;
  }
}

bool isInternetConnected() 
{
  if (WiFi.status() == WL_CONNECTED) { // check if WiFi is connected
    WiFiClient client;
    const int httpPort = 80;
    if (client.connect("www.google.com", httpPort)) { // try to connect to Google
      client.stop();
      return true; // Internet access available
    }
  }
  return false; // Internet access not available
}

void handlePOST_Upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
  if (!index && request->hasParam("dir-path", true, false)) {
    AsyncWebParameter* p = request->getParam("dir-path",true, false);
    String path = p->value() + "/" + filename;
    ESP_LOGD(tag, "upload path: %s", path.c_str());
  
    request->_tempFile= LittleFS.open(path, "w");
    //fs::File file = LittleFS.open(path, "w");
    if (!request->_tempFile) {
      ESP_LOGE(tag, "Failed to create file!");
      return;
    }
    
  }else{
    ESP_LOGE(tag, "dir-path Param not found..");
  }

  if(len && request->_tempFile){
    request->_tempFile.write(data, len);
  }

  if(final){
    request->_tempFile.close();
    request->redirect("/files");
    ESP_LOGD(tag, "UploadEnd: %s, %u B", filename.c_str(), index+len);
  }
}

void handleGET_DSLRBooth(AsyncWebServerRequest *request) 
{
  static int lastCountdown = 1000;
  request->send(200, "text/plain"); // send this right away to avoid comm delays

  if(request->args() >= 1){
    int x = 0;
    const char* event_type = request->arg( x ).c_str();
    
    if(strcmp("countdown", event_type) == 0){
      int p = request->arg(1).toInt();
      if(p > 0 && p <= 100){
        animStatus.countStatus = p;
      }

      // in case "coundown_start" was missed
      if(animStatus.EventsIndex != ANIM_WHITEFILL_INDEX){
        animProps.event[0].countDown = lastCountdown;
        animProps.event[0].type = EV_NORMAL;
        PostNewEvent(animProps.event[0]);
      }
    }
    else if(strcmp("countdown_start", event_type) == 0){ // index=0
      int count = request->arg(1).toInt(); // retrieve countdown value
      //Log.traceln("<dslr> countdown = %d", count);
      if(count < 1){count = 1;}
      count *= 1000;
      lastCountdown = count - 500;
      animProps.event[0].countDown = lastCountdown;
      animProps.event[0].type = EV_NORMAL;
      PostNewEvent(animProps.event[0]);
    } 
    else if(strcmp("sharing_screen", event_type) == 0){
      animProps.event[2].type = EV_NORMAL;
      PostNewEvent(animProps.event[2]);
    }
    else if(strcmp("session_end", event_type) == 0){ //index=1
      animProps.event[1].type = EV_NORMAL;
      PostNewEvent(animProps.event[1]);
    }
    else{
   // else if(strcmp_P("session_start", event_type) == 0){
      // TODO determine if ramp down is active depending on session type
      
      /*
      const char* param1 = request->arg(1).c_str();
      if(strcmp("PrintOnly", param1)){
        animStatus.Animation = ANIM_START_PRINTONLY;
        xTaskNotifyGive( Strip1_Task_Handle );
        //Serial.println("ses_print..\n\r");
      }else if(strcmp("PrintAndGIF", param1)){
        animStatus.Animation = ANIM_START_PRINTGIF;
        xTaskNotifyGive( Strip1_Task_Handle );
        //Serial.println("ses_prnt_gif..\n\r");
      }else if(strcmp("OnlyGIF", param1)){
        animStatus.Animation = ANIM_START_GIFONLY;
        xTaskNotifyGive( Strip1_Task_Handle ); 
        //Serial.println("ses_gif..\n\r");
      }else if(strcmp("Boomerang", param1)){
        animStatus.Animation = ANIM_START_BOOM;
        xTaskNotifyGive( Strip1_Task_Handle ); 
        //Serial.println("ses_boom..\n\r");
      }else if(strcmp("Video", param1)){
        animStatus.Animation = ANIM_START_VIDEO;
        xTaskNotifyGive( Strip1_Task_Handle );
        //Serial.println("ses_vid..\n\r");
      }
      */
    }
  }
}

bool isFirmware = true;
bool updateSuccessful = false;
void handlePOST_Update(AsyncWebServerRequest *request)
{
  bool hasError = Update.hasError();
  String responseContent = hasError ? "failed" : "success";
  String responseType = hasError ? "text/plain" : "text/html";
  int responseCode = hasError ? 500 : 200;

  AsyncWebServerResponse *response = request->beginResponse(responseCode, responseType, responseContent);
  response->addHeader("status", responseContent);
  request->send(response);
}

//  handlePOST_Update Callback function to install the firmware or file system bin files
void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) 
{
  // Do Once Code
  if (!index) {
    size_t fileSize = UPDATE_SIZE_UNKNOWN;
    // Retrieve file-size parameter
    if(request->hasParam("file-size", true, false)) { // make sure param is from POST request or it will not find it
      fileSize = request->getParam("file-size", true, false)->value().toInt();
    }

    // register progress callback function
    Update.onProgress(updateFirmwareProgress);
    updateSuccessful = false;

    //file name checks
    if (filename.startsWith("ata_fw")) {
      isFirmware = true;
      if (!Update.begin(fileSize, U_FLASH, BoardLED2)) {
        ESP_LOGD(tag, "Firmware update failed to start");
        return;
      }
      ESP_LOGD(tag, "Updating Firmware with: %s  Size:%d ", filename.c_str(), fileSize);
    }
    else if (filename.startsWith("ata_fs")) {
      isFirmware = false;
      if (!Update.begin(fileSize, U_SPIFFS, BoardLED2)) {
        ESP_LOGD(tag, "LittleFS update failed to start");
        return;
      }
      ESP_LOGD(tag, "Updating File System with: %s  Size:%d ", filename.c_str(), fileSize);
    }
    else {
      ESP_LOGD(tag, "Unsupported file type");
      return;
    }
  }
  
  // Writing...
  if (!Update.hasError()) {
    if (Update.write(data, len) != len) {
      Update.printError(Serial);
    }
  }
  
  // All Done...
  if (final) {
    vTaskDelay(100);
    if(isFirmware) { // firmware update
      if (Update.end(true)) {
        RebootSystem = true;
        updateSuccessful = true;
        ESP_LOGD(tag, "firmware update successful: %d", index + len);
      }
      else {
        Update.printError(Serial);
      }
    }
    else{ // file system update
      if(Update.end(true)) {
        updateSuccessful = true;
        ESP_LOGD(tag, "file system update successful!");
      }
      else {
        Update.printError(Serial);
      }
    }
  }
}

// Server requested files that aren't template processed
void handleGET_SendFile(AsyncWebServerRequest *request)// try this later "^/(img|favicon).*"
{  
  String filePath = request->url();
  const char* ext = getFileExtension(filePath.c_str());
  const char* contentType = getFileType(ext);

  if( contentType == NULL ){ 
    request->send(404); 
    return; 
  }

  if(filePath.c_str()[0] != '/'){
    filePath = '/' + filePath;
  }

  if (!strcmp(ext,"html") || !strcmp(ext, "htm") || !strcmp(ext, "css") || !strcmp(ext, "js")) {
    if(!filePath.startsWith("/www/")){
      filePath = "/www" + filePath;
    }
  } 
  
  ESP_LOGD(tag, "Sent: %s", filePath.c_str());
  request->send(LittleFS, filePath, contentType);
}

String listDir(String directoryList[], int count) 
{
  String listedFiles;

  for (int i = 0; i < count; i++) {
    // directory html
    listedFiles += "<tr id=\"file-row\"><td>Dir: ";
      listedFiles += directoryList[i];
      listedFiles += "/</td><td>-</td></tr>\n";

    filesDropdownOptions += "<option value=\"";
      filesDropdownOptions += directoryList[i];
      filesDropdownOptions += "\">";
      filesDropdownOptions += directoryList[i];
      filesDropdownOptions += "/</option>\n";

    dirDropdownOptions += "<option value=\"";
      dirDropdownOptions += directoryList[i];
      dirDropdownOptions += "\">";
      dirDropdownOptions += directoryList[i];
      dirDropdownOptions += "/</option>\n";

    File dir = LittleFS.open(directoryList[i]);
    File file = dir.openNextFile();
    while (file) {
      String fileName = file.name();
      if (!file.isDirectory()) {
        //Serial.println("    File: " + String(file.name()));
        listedFiles += "<tr id=\"file-row\"><td>&emsp;<button onclick=\"window.location.href='/download?file=";
          listedFiles += file.path();
          listedFiles += "\" \"\">&#8681;</button>&emsp;";
          listedFiles += fileName;
          listedFiles += "</td><td>";
          listedFiles += convertFileSize(file.size());
          listedFiles += "</td></tr>\n";

        filesDropdownOptions += "<option value=\"";
          filesDropdownOptions += file.path();
          filesDropdownOptions += "\">&emsp;&emsp;";
          filesDropdownOptions += fileName;
          filesDropdownOptions += "</option>\n";
      }
      file = dir.openNextFile();
    }
    dir.close();
  }

  return listedFiles;
}

char* readFile(fs::FS &fs, const char* path) {
  File file = fs.open(path, "r");
  if (!file || file.isDirectory()) {
    return nullptr;
  }

  size_t fileSize = file.size();
  char* fileContent = new char[fileSize + 1];  // +1 for null-terminator

  size_t bytesRead = file.readBytes(fileContent, fileSize);
  file.close();

  fileContent[bytesRead] = '\0';  // Set null-terminating character

  if (bytesRead != fileSize) {
    delete[] fileContent;
    return nullptr;
  }

  return fileContent;
}

void writeFile(fs::FS &fs, const char * path, const char * message)
{
  File file = fs.open(path, "w");
  if(!file){
    return;
  }
  file.print(message);
  file.close();
}

// Send html file with template processing {{VAR}}
void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&))
{
  ESP_LOGD(tag, "Sent file: %s", filePath);
  File file = LittleFS.open(filePath, "r");
  const char* htmlFile = readFile(LittleFS, filePath);

  //String processedData = varReplace(htmlFile, htmlProcessor);
  String processedData = varReplace(htmlFile, callback);
  request->send(200, "text/html", processedData);
}

const char* convertFileSize(const size_t bytes) {
  static char fileSizeBuffer[16]; // PreAllocated buffer for the file size
  
  if (bytes < 1024) {
    snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes);
  } else if (bytes < 1024*1024) {
    snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", static_cast<double>(bytes) / 1024.0);
  } else if (bytes < 1024*1024*1024) {
    snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", static_cast<double>(bytes) / 1048576.0);
  } else {
    fileSizeBuffer[0] = '\0'; // Empty string if the file size is too large
  }

  return fileSizeBuffer;
}

// Callback template processor
String htmlProcessor(const String& var)
{
  if(var == "NAVBAR"){ return String("<iframe src=\"/www/navbar.html\" style=\"display: block; width: 100%; max-height: 4vh; border: none;\" scrolling=\"no\"></iframe>\n"); }

  if(var == "ALLOWED_EXTENSIONS_EDIT"){ return allowedExtensionsForEdit; }

  if(var == "FS_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes()); }

  if(var == "FS_USED_BYTES"){ return convertFileSize(LittleFS.usedBytes()); }

  if(var == "FS_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); }
  
  if(var == "LISTED_FILES"){
    filesDropdownOptions = ""; // clear out 
    dirDropdownOptions = ""; // clear out
    String directories[8];
    int dirCount = 0;
    getAllDirectories(directories, dirCount);
    
    return listDir(directories, dirCount);
  }

  if(var == "EDIT-DEL_FILES"){ return filesDropdownOptions; }
  
  if(var == "DIR_LIST"){ return dirDropdownOptions; }
  
  if(var == "SAVE_PATH_INPUT"){
    if(savePath == "/new.txt"){
      return String("<input type=\"text\" id=\"save-path\" name=\"save-path\" value=\"") + savePath + "\">";
    }
    else{
      return String("<input type=\"text\" id=\"save-path\" name=\"save-path\" value=\"") + savePath + "\"disabled>";
    }
  }

  if(var == "FIRM_VER"){ return FIRMWARE_VER; }

  return var;
}

String HomeHtmlProcessor(const String& var) {
  if(var == "NAVBAR"){ return String("<iframe src=\"/www/navbar.html\" style=\"display: block; width: 100%; max-height: 3vh; border: none;\" scrolling=\"no\"></iframe>\n"); }

  if (var == "APP_NAME") {
    return sysProps.appName;
  }
  if (var == "OLED") {
    return "No";
  }
  if (var == "STRIP1") {
    return (strip1) ? "Yes" : "No";
  }
  if (var == "STRIP2") {
    return (strip2) ? "Yes" : "No";
  }
  if (var == "FRONT_LIGHT") {
    return (animProps.frontLight.enabled) ? "Yes" : "No";
  }
  if (var == "REAR_LIGHT") {
    return (animProps.rearLight.enabled) ? "Yes" : "No";
  }
  if (var == "FIRMWARE") {
    return FIRMWARE_VER;
  }
  if (var == "BOOTH_T") {
    return String(sysProps.t_sensor.temperature) + "F";
  }
  if (var == "SETPOINT") {
    return String(sysProps.t_sensor.Setpoint1) + "F";
  }
  if (var == "FLASH_SIZE") {
    return convertFileSize(ESP.getSketchSize());
  }
  if (var == "FLASH_FREE") {
    return convertFileSize(ESP.getFreeSketchSpace());
  }
  if (var == "HEAP_SIZE") {
    return convertFileSize(ESP.getHeapSize());
  }
  if (var == "HEAP_FREE") {
    return convertFileSize(ESP.getFreeHeap());
  }
  if (var == "CPU_FREQ") {
    return String(ESP.getCpuFreqMHz()) + "Mhz";
  }
  if (var == "IP") {
    return WiFi.localIP().toString();
  }
  if (var == "MAC") {
    return chipInfo.macStr;
  }
  if (var == "SSID") {
    return WiFi.SSID();
  }
  if (var == "RSSI") {
    return String(WiFi.RSSI());
  }
  if (var == "WIFI_CH") {
    return String(WiFi.channel());
  }
  if (var == "ENCRYP") {
    return String(WiFi.encryptionType(0));
  }
  if (var == "AP_SSID") {
    return WiFi.softAPSSID();
  }
  if (var == "AP_CLIENTS") {
    return String(WiFi.softAPgetStationNum());
  }
  if (var == "BLE") {
    return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No";
  }
  if (var == "BLE_SSID") {
    return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : "";
  }
  if (var == "BLE_CLIENTS") {
    return (BTDeviceConnected) ? "1" : "0";
  }
  if (var == "AP_MAC") {
    return getSoftAPMacAddress();
  }


  // Return an empty string if the variable is not recognized
  return var;
}

String getSoftAPMacAddress() {
  uint8_t mac[6];
  WiFi.softAPmacAddress(mac);

  String macString = "";
  for (int i = 0; i < 6; i++) {
      macString += String(mac[i], HEX);
      if (i < 5) macString += ":";
  }
  return macString;
}

// Finds segments between {{VAR}} and calls a callback function to replace VAR with new content
String varReplace(const String& input, String (*callback)(const String&)) {
  if (input.isEmpty()) {
    return input;
  }

  String result;
  int startPos = 0;
  int start = input.indexOf("{{", startPos);

  while (start != -1) {
    result += input.substring(startPos, start);

    int end = input.indexOf("}}", start + 2);
    if (end == -1) {
      break;
    }

    String segment = input.substring(start + 2, end);
    int segmentLength = segment.length();
    if (segmentLength <= 32) {
      String replacement = callback(segment);
      result += replacement;
    } else {
      result += input.substring(start, end + 2); // Include the original segment if it exceeds the limit
    }

    startPos = end + 2;
    start = input.indexOf("{{", startPos);
  }

  result += input.substring(startPos);
  return result;
}

const char* getFileExtension(const char* filename) {
  size_t dotPos = strlen(filename);
  while (dotPos > 0 && filename[dotPos - 1] != '.') {
    dotPos--;
  }

  if (dotPos != 0 && dotPos < strlen(filename) - 1) {
    return &filename[dotPos];
  }

  return "";  // Empty string if no extension found
}

const char* getFileType(const char* ext) {
  if (strcmp(ext, "png") == 0) {
    return "image/png";
  } else if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) {
    return "image/jpeg";
  } else if (strcmp(ext, "gif") == 0) {
    return "image/gif";
  } else if (strcmp(ext, "ico") == 0) {
    return "image/x-icon";
  } else if (strcmp(ext, "txt") == 0) {
    return "text/plain";
  } else if (strcmp(ext, "css") == 0) {
    return "text/css";
  } else if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) {
    return "text/html";
  } else if (strcmp(ext, "js") == 0) {
    return "text/javascript";
  } else if (strcmp(ext, "json") == 0) {
    return "application/json";
  } else {
    return "";
  }
}

// Serial print firmware or file system update progress
void updateFirmwareProgress(size_t progress, size_t total)
{
  static int lastProg = -1;
  int firmwareProgress = (progress * 100)/ total;

  if(firmwareProgress != lastProg){
    Serial.printf("Progress: %u%%\r", firmwareProgress);
    lastProg = firmwareProgress;
    //TODO Add buzzer tune while uploading firmware
    //Buzzer_Play_Tune(TUNE_DOWNLOADING,1,true,false);
  }
}


