/*
Fct  Name  Pin  GPIO
Up     D3    4     0
Dw     D4    5     2

.attach(servoPin, 500, 2500) 
0 .. 180  =90 deg

MG90S
Orange signal
Red    5 V
Brown  0  
*/

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <time.h>
#include <SolarCalculator.h>
#include <Arduino.h>
#include <Servo.h> 
#include <LittleFS.h>

// Configurations Wi-Fi
const char* ssid = "BRG-FLOOR0-2G";
const char* password = "0499185306";

//const char* ssid = "WiFi-2.4-C9C0-JH";
//const char* password = "0485626211";

// Intervalles de synchronisations
//                                            heures         minutes       secondes     
const unsigned long NTPD_UPDATE_INTERVAL   = (  (12 * 3600UL) + (0 * 60UL) +    0     ) * 1000UL;
const unsigned long CONSOLE_UPDATE_INTERVAL= (  (0 * 3600UL) + (10 * 60UL) +   0     ) * 1000UL;

// Création du serveur Web sur le port 80
ESP8266WebServer server(80);

// Variable d'état pour le mode Auto/Manuel
bool AutoMode = true;
bool IsBoot = true ;  // to avoid first 'close' at boot

// Coordonnées géographiques (Exemple : Evere, Belgique)
// 50°51'49.68"N 4°22'18.46"E
//  Brussels Airport are 50.9014° N latitude and 4.4844° E longitude.
//  Brussels are 50.8476° N latitude and 4.3572° E longitude
const double latitude  = 50.8476;
const double longitude = 4.3572;

double transit, sunrise, sunset;

// Configuration NTP et Fuseau Horaire
const char* MY_TZ = "CET-1CEST-2,M3.5.0/2,M10.5.0/3";

///// servo
const uint8_t servoPin = D2;  // servo pin
Servo myservo;  // create servo object to control a servo

int stepinc = 1;      // steps
int SrvRest = 90;     // rest pos 
int SrvAngle = 33;    // angle to access button
int delayPush = 200;  // real push button action duration and wait as 'button rebound'

//// buttons
const int buttonOpen  = 0;   // pin D3 the GPIO# of the pushbutton pin
const int buttonClose = 2;  // pin D4 the GPIO# of the pushbutton pin

// CONFIGURATION OUVERTURE ALEATOIRE (MODIFIABLE VIA WEB)
String heure_cible_ouverture = "08:15"; // Heure cible par défaut
int offset_ouverture_min     = 15;      // Delta aléatoire par défaut (+/- min)

// VARIABLES GLOBALES (Stockage des horaires cibles calculés)
int heure_fermeture  = 0;
int minute_fermeture = 0;

int heure_ouverture  = 0;
int minute_ouverture = 0;

// VARIABLES GLOBALES (HH:MM) pour serveur
int local_heure  = 0;
int local_minute = 0;

// Variables de sécurité pour éviter de renvoyer l'ordre en boucle
bool volet_ferme_aujourdhui   = true;  // évite fermeture intempestive au boot
bool volet_ouvert_aujourdhui  = false;
int  dernier_jour_calcul      = -1;


unsigned long previousMillisConsole = 0;
unsigned long previousMillisNTP = 0;

/////////////////////////////////////////////////////////////////////	
// GESTION MEMORISATION VIA LITTLEFS
/////////////////////////////////////////////////////////////////////	
void sauvegarderConfiguration() {
  File configFile = LittleFS.open("/config.txt", "w");
  if (!configFile) {
    Serial.println(F("[LittleFS] Échec de l'ouverture du fichier pour écriture"));
    return;
  }
  configFile.println(heure_cible_ouverture);
  configFile.println(offset_ouverture_min);
  configFile.close();
  Serial.println(F("[LittleFS] Configuration sauvegardée avec succès."));
}

void chargerConfiguration() {
  if (!LittleFS.exists("/config.txt")) {
    Serial.println(F("[LittleFS] Fichier non trouvé. Utilisation des valeurs par défaut."));
    return;
  }

  File configFile = LittleFS.open("/config.txt", "r");
  if (!configFile) {
    Serial.println(F("[LittleFS] Échec de l'ouverture du fichier de config."));
    return;
  }

  String str_heure = configFile.readStringUntil('\n');
  String str_offset = configFile.readStringUntil('\n');
  configFile.close();

  str_heure.trim();
  str_offset.trim();

  if (str_heure.length() > 0 && str_offset.length() > 0) {
    heure_cible_ouverture = str_heure;
    offset_ouverture_min = str_offset.toInt();
    Serial.printf("[LittleFS] Restauration réussie -> Cible: %s, Offset: %d min\n\r", 
                  heure_cible_ouverture.c_str(), offset_ouverture_min);
  } else {
    Serial.println(F("[LittleFS] Données invalides, conservation des valeurs par défaut."));
  }
}

/////////////////////////////////////////////////////////////////////	
// FONCTION D'HORAIRE ALEATOIRE
/////////////////////////////////////////////////////////////////////	
void calculerHoraireAleatoire(const String& horaire_str, int offset_minutes, int &heure_out, int &minute_out) {
  int hh = 0, mm = 0;
  sscanf(horaire_str.c_str(), "%d:%d", &hh, &mm);

  int total_minutes = hh * 60 + mm;
  int delta = random(-offset_minutes, offset_minutes + 1);
  total_minutes += delta;

  if (total_minutes < 0) total_minutes += 24 * 60;
  total_minutes %= (24 * 60);

  heure_out = total_minutes / 60;
  minute_out = total_minutes % 60;

  Serial.printf("\n\r[Aleatoire] Cible %s (+/-%d min) -> Planifie: %02d:%02d (Delta: %d min)\n\r", 
                horaire_str.c_str(), offset_minutes, heure_out, minute_out, delta);
}
/////////////////////////////////////////////////////////////////////////////////////
String mconvertMillsToHHMMSS(long int mils) {  // millis to  HH:MM:SS
  char ret[9];
  long int seconds = mils / 1000;
  long int hours = seconds / 3600;
  seconds %= 3600;
  long int minutes = seconds / 60;
  seconds %= 60;
  long int secs = seconds;
  sprintf(ret, "%02d:%02d.%02d", hours, minutes, seconds);  //8 chars
  return ret;
}
/////////////////////////////////////////////////////////////////////	
void afficherHeureNodeMCU() {
  time_t now;
  struct tm timeinfo;
  time(&now); 
  localtime_r(&now, &timeinfo); 
  if (timeinfo.tm_year > 70) { 
    local_heure  = timeinfo.tm_hour;
    local_minute = timeinfo.tm_min;
    Serial.printf("\n\rafficherHeureNodeMCU()  %04d-%02d-%02d %02d:%02d:%02d\n\r", 
              timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
              timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);		

  } else { 
    Serial.println("\n\rafficherHeureNodeMCU() Horloge en attente de synchronisation NTP...");
  }
}
/////////////////////////////////////////////////////////////////////	
const char* printLocalTime(const char* label, double utc_hours) {
  static char bufferResult[64];

  if (isnan(utc_hours)) {
    Serial.print(label);
    Serial.println("Soleil permanent ou nuit polaire");
    snprintf(bufferResult, sizeof(bufferResult), "%s Nuit/Soleil permanent", label);
    return bufferResult;
  } 

  int hh = (int)utc_hours;
  int mm = (int)((utc_hours - hh) * 60);
  Serial.printf("\n\r printLocalTime() passed %.2f, %02ih%02i UTC ", utc_hours, hh, mm);

  time_t now = time(nullptr);

  struct tm tm_utc;
  gmtime_r(&now, &tm_utc);

  time_t minuit_utc = now - (tm_utc.tm_hour * 3600 + tm_utc.tm_min * 60 + tm_utc.tm_sec);
  time_t event_timestamp = minuit_utc + (long)(utc_hours * 3600.0);

  struct tm tm_local;
  localtime_r(&event_timestamp, &tm_local);

  char buffer[16];   
  strftime(buffer, sizeof(buffer), "%H:%M", &tm_local);
  
  Serial.print(label);
  Serial.println(buffer);
  snprintf(bufferResult, sizeof(bufferResult), "%s%s", label, buffer);
  
  return bufferResult;
}
////////////////////////////////////////////////////////////
void mettreAJourHoraireFermeture(time_t t) {    
  struct tm tm_utc_date;
  gmtime_r(&t, &tm_utc_date);   

  Serial.printf("\n\rmettreAJourHoraireFermeture(): received : %04d-%02d-%02d %02d:%02d:%02d\n\r", 
              tm_utc_date.tm_year + 1900, tm_utc_date.tm_mon + 1, tm_utc_date.tm_mday,
              tm_utc_date.tm_hour, tm_utc_date.tm_min, tm_utc_date.tm_sec);

  calcSunriseSunset(tm_utc_date.tm_year + 1900, tm_utc_date.tm_mon + 1, tm_utc_date.tm_mday, latitude, longitude, transit, sunrise, sunset);

  printLocalTime("\n\r calcSunriseSunset: Lever   : ", sunrise); 
  printLocalTime("\n\r calcSunriseSunset:  Transit: ", transit); 
  printLocalTime("\n\r calcSunriseSunset: Coucher : ", sunset ); 

  if (!isnan(sunrise)) {
    time_t minuit_utc_timestamp = t - (tm_utc_date.tm_hour * 3600 + tm_utc_date.tm_min * 60 + tm_utc_date.tm_sec);
    time_t lever_timestamp = minuit_utc_timestamp + (long)(sunrise * 3600.0);

    // Soustraction d'une heure (3600 secondes)
    time_t fermeture_timestamp = lever_timestamp - 3600;

    struct tm tm_locale;
    localtime_r(&fermeture_timestamp, &tm_locale);

    heure_fermeture  = tm_locale.tm_hour;
    minute_fermeture = tm_locale.tm_min;

    Serial.printf("\n\rHoraire de fermeture programmé (-1h) : %02d:%02d (Heure Locale)\n\r", heure_fermeture, minute_fermeture);
  } else {
    Serial.println("\n\rImpossible de calculer le lever du soleil.");
  }
}
////////////////////////////////////////////////////////////
void VoletOpen() {
  Serial.printf("\n\r >>>>>>>>>>>>>>  Open");
  myservo.attach(servoPin, 500, 2500);				
  myservo.write(SrvRest + SrvAngle);
  delay(delayPush);
  myservo.write(SrvRest);
  delay(delayPush * 2);
  myservo.detach();
}
////////////////////////////////////////////////////////////
void VoletClose() {
  Serial.printf("\n\r >>>>>>>>>>>>>> Close");		
  myservo.attach(servoPin, 500, 2500);		
  myservo.write(SrvRest - SrvAngle);
  delay(delayPush);		
  myservo.write(SrvRest);
  delay(delayPush * 2);		
  myservo.detach();
}
// ---------------------------------------------------------
// FONCTIONS DU SERVEUR WEB
String generateHTML(int local_heure, int local_minute, bool AutoMode, int heure_fermeture, int minute_fermeture, String infoLineText) {
  char timeStr[6];
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", local_heure, local_minute);

  char closeTimeStr[6];
  snprintf(closeTimeStr, sizeof(closeTimeStr), "%02d:%02d", heure_fermeture, minute_fermeture);

  char openTimeStr[6];
  snprintf(openTimeStr, sizeof(openTimeStr), "%02d:%02d", heure_ouverture, minute_ouverture);

  String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset='UTF-8'>
  <title>Controle Volet</title>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; margin-top: 20px; background-color: #f4f4f9; }
    h1 { color: #333; margin-bottom: 5px; }
    .btn { display: inline-block; padding: 12px 20px; font-size: 16px; color: white; border: none; border-radius: 5px; margin: 8px; cursor: pointer; text-decoration: none; }
    .btn-green { background-color: #4CAF50; }
    .btn-red { background-color: #f44336; }
    .btn-blue { background-color: #008CBA; }
    .btn-gray { background-color: #888888; }
    .btn-submit { background-color: #333; padding: 6px 12px; font-size: 14px; margin-left: 5px; }
    
    .params-box { 
      background: #ffffff; 
      display: inline-block; 
      padding: 15px 20px; 
      border-radius: 8px; 
      box-shadow: 0 2px 5px rgba(0,0,0,0.1); 
      margin: 15px 0; 
      text-align: left;
      max-width: 90%;
    }
    .params-box input[type='time'], .params-box input[type='number'] {
      padding: 5px;
      font-size: 14px;
      margin: 4px 0;
    }

    .infoline {
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
      text-align: left;
      padding: 8px 15px;
      font-size: 13px;
      color: #555;
      background-color: #e9e9ee;
      border-top: 1px solid #ccc;
      box-sizing: border-box;
    }
  </style>
</head>
<body>
  <h1>Volet Chambre</h1>
  <p style='font-size: 20px; margin-top: 0;'><strong>)rawliteral";

  html += timeStr;

  html += R"rawliteral(</strong></p>

  <!-- Formulaire de réglage de l'ouverture -->
  <div class='params-box'>
    <form action='/setOpening' method='POST'>
      <label for='target'><strong>HH:MM cible d'ouverture :</strong></label><br>
      <input type='time' id='target' name='target' value=')rawliteral";
  html += heure_cible_ouverture;
  html += R"rawliteral(' required><br>

      <label for='offset'><strong>Offset (± min) :</strong></label><br>
      <input type='number' id='offset' name='offset' min='0' max='120' value=')rawliteral";
  html += String(offset_ouverture_min);
  html += R"rawliteral(' required> min<br><br>

      <input type='submit' class='btn btn-submit' value='Valider & Recalculer'>
    </form>
    <hr style='border: 0; border-top: 1px solid #eee; margin: 10px 0;'>
    • Heure d'ouverture calculée aujourd'hui : <strong>)rawliteral";
  html += openTimeStr;
  html += R"rawliteral(</strong>
  </div><br>

)rawliteral";

  if (AutoMode) {
    html += "<a href='/toggleAuto' class='btn btn-green'>AUTO (Ouv: ";
    html += openTimeStr;
    html += " / Ferm: ";
    html += closeTimeStr;
    html += ")</a><br>\n";
  } else {
    html += "<a href='/toggleAuto' class='btn btn-gray'>MAN</a><br>\n";
  }

  html += R"rawliteral(
  <a href='/open' class='btn btn-blue'>Ouvrir</a>
  <a href='/close' class='btn btn-red'>Fermer</a>

  <div class='infoline'>
    )rawliteral";

  html += infoLineText;

  html += R"rawliteral(
  </div>
</body>
</html>
)rawliteral";

  return html;
}
////////////////////////////////////////////////////////////
void handleRoot() {
  afficherHeureNodeMCU();
  String infoLineText = printLocalTime("Lever du soleil: ", sunrise);
  infoLineText += printLocalTime("/  Midi: ", transit);
  infoLineText += printLocalTime("/  Coucher du soleil: ", sunset);

  String html = generateHTML(local_heure, local_minute, AutoMode, heure_fermeture, minute_fermeture, infoLineText);
  server.send(200, "text/html", html);
}
////////////////////////////////////////////////////////////
void handleSetOpening() {
  if (server.hasArg("target") && server.hasArg("offset")) {
    heure_cible_ouverture = server.arg("target");
    offset_ouverture_min = server.arg("offset").toInt();

    // Sauvegarde persistante des nouveaux paramètres dans LittleFS
    sauvegarderConfiguration();

    // Recalcul immédiat de l'heure d'ouverture planifiée avec les nouveaux paramètres
    calculerHoraireAleatoire(heure_cible_ouverture, offset_ouverture_min, heure_ouverture, minute_ouverture);
  }
  
  server.sendHeader("Location", "/");
  server.send(303);
}
////////////////////////////////////////////////////////////
void handleToggleAuto() {
  AutoMode = !AutoMode;
  server.sendHeader("Location", "/");
  server.send(303);
}
////////////////////////////////////////////////////////////
void handleOpen() {
  VoletOpen();
  server.sendHeader("Location", "/");
  server.send(303);
}
////////////////////////////////////////////////////////////
void handleClose() {
  VoletClose();
  server.sendHeader("Location", "/");
  server.send(303);
}
// END  FONCTIONS DU SERVEUR WEB
/////////////////////////////////////////////////////////////////////	
void fctNTPD_UPDATE_INTERVAL() {
  Serial.printf("\n\rfctNTPD_UPDATE_INTERVAL:  event NTPD_UPDATE_INTERVAL %lu, %s ", NTPD_UPDATE_INTERVAL, mconvertMillsToHHMMSS(NTPD_UPDATE_INTERVAL));
  Serial.println("fctNTPD_UPDATE_INTERVAL Avant synchro NTPd :" );
	afficherHeureNodeMCU();  
  Serial.println("fctNTPD_UPDATE_INTERVAL Envoi re-synchronisation NTP configTime()...");
  configTime(MY_TZ, "fr.pool.ntp.org", "pool.ntp.org");
  Serial.println("fctNTPD_UPDATE_INTERVAL Apres synchro NTPd :");
  afficherHeureNodeMCU();
}
/////////////////////////////////////////////////////////////////////	
void fctCONSOLE_UPDATE_INTERVAL() {
  time_t now = time(nullptr);
  struct tm* local_time = localtime(&now);
  	
  if (local_time->tm_mday != dernier_jour_calcul) {
    Serial.printf("\n\r Nouveau jour détecté (%d). Recalcul des horaires...", local_time->tm_mday);
    afficherHeureNodeMCU();
    // 1. Calcul horaire astronomique de fermeture
    mettreAJourHoraireFermeture(now);
    // 2. Calcul aléatoire horaire d'ouverture
    calculerHoraireAleatoire(heure_cible_ouverture, offset_ouverture_min, heure_ouverture, minute_ouverture);
    dernier_jour_calcul = local_time->tm_mday;
    volet_ferme_aujourdhui  = false;
    volet_ouvert_aujourdhui = false;
  }  
	//store values for server
  int heure_actuelle  = local_time->tm_hour;
  int minute_actuelle = local_time->tm_min;

  if (AutoMode) {
    // LOGIQUE D'OUVERTURE AUTOMATIQUE
    if (!volet_ouvert_aujourdhui) {
      if ((heure_actuelle > heure_ouverture) || (heure_actuelle == heure_ouverture && minute_actuelle >= minute_ouverture)) {
        Serial.printf("\n\r Ouverture automatique du volet..");
        VoletOpen();
        volet_ouvert_aujourdhui = true;
      }
    }
    // LOGIQUE DE FERMETURE AUTOMATIQUE
    if (!volet_ferme_aujourdhui) {
      if ((heure_actuelle > heure_fermeture) || (heure_actuelle == heure_fermeture && minute_actuelle >= minute_fermeture)) {
        Serial.printf("\n\r Fermeture automatique du volet..");
        if (IsBoot) { 
          Serial.printf(" IsBoot, fermeture annulée !"); 
          IsBoot = false; 
        } else { 	
          VoletClose(); 
        }
        volet_ferme_aujourdhui = true;
      }
    }
  }
}
/////////////////////////////////////////////////////////////////////	
void setup() {
  Serial.begin(115200);
  delay(500);
  randomSeed(os_random()); // Initialisation du générateur aléatoire hardware ESP8266

  Serial.println("");
  Serial.println("━━━━━━━━━━━━━━━━━━━━━");
  Serial.println(__FILE__);
  Serial.println(String("Compiled ") + String(__DATE__ " " __TIME__));
  Serial.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");

  // Initialisation du système de fichiers LittleFS
  if (LittleFS.begin()) {
    Serial.println(F("[LittleFS] Système de fichiers monté avec succès."));
    chargerConfiguration(); // Restaure les variables ou conserve les valeurs par défaut
  } else {
    Serial.println(F("[LittleFS] Échec du montage du système de fichiers."));
  }

  afficherHeureNodeMCU();
  Serial.print(F("Searching Wifi .."));
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.printf("."); }
  Serial.println("\n\rWi-Fi connecté ");
  Serial.printf("%s, %s, MAC:%s, %i dBm\n\r", WiFi.SSID().c_str(), WiFi.localIP().toString().c_str(), WiFi.macAddress().c_str(), WiFi.RSSI() );

  Serial.println("\n\rSync local time, call configTime()...");
  configTime(MY_TZ, "fr.pool.ntp.org", "pool.ntp.org");
  while (time(nullptr) < 1000000000) { delay(1000); Serial.print("s"); }
  Serial.println("\n\rdone ");    
  afficherHeureNodeMCU(); 
  
  Serial.println("\n\rSetup server ..");  
  // Routes du serveur Web
  server.on("/", handleRoot);
  server.on("/setOpening", HTTP_POST, handleSetOpening);
  server.on("/toggleAuto", handleToggleAuto);
  server.on("/open", handleOpen);
  server.on("/close", handleClose);
  
  server.begin();
  Serial.println("\n\rServeur Web démarré sur le port 80.");

  Serial.printf("\n\rParams events:");
  Serial.printf("\n\r NTPD_UPDATE_INTERVAL    %lu ms, %s", NTPD_UPDATE_INTERVAL, mconvertMillsToHHMMSS(NTPD_UPDATE_INTERVAL) );
  Serial.printf("\n\r CONSOLE_UPDATE_INTERVAL %lu ms, %s", CONSOLE_UPDATE_INTERVAL, mconvertMillsToHHMMSS(CONSOLE_UPDATE_INTERVAL) );
  
  Serial.println(F("\n\rSet pins.."));
  pinMode(buttonOpen, INPUT_PULLUP);
  pinMode(buttonClose, INPUT_PULLUP);

  Serial.println(F("Set servo.."));	
  myservo.attach(servoPin, 500, 2500);
  Serial.printf("   \n\rstepinc = %i",  stepinc);   
  Serial.printf("   \n\rSrvRest = %i",  SrvRest);   
  Serial.printf("   \n\rSrvAngle = %i", SrvAngle);   
  Serial.printf("   \n\rdelayPush = %i", delayPush);   
  
  Serial.println(F("\n\rCall fctCONSOLE_UPDATE_INTERVAL ----"));
  fctCONSOLE_UPDATE_INTERVAL();
  Serial.println(F("\n\rCall fctCONSOLE_UPDATE_INTERVAL done ----"));

  Serial.println(F("\n\r------End setup()."));  
}
/////////////////////////////////////////////////////////////////////	
void loop() {
  server.handleClient();
  
  unsigned long currentMillis = millis();

  // Re-synchronisation NTP
  if (currentMillis - previousMillisNTP >= NTPD_UPDATE_INTERVAL) {
    fctNTPD_UPDATE_INTERVAL();
    previousMillisNTP = currentMillis;
  }

  // Traitement régulier (10 min)
  if (currentMillis - previousMillisConsole >= CONSOLE_UPDATE_INTERVAL) {
    fctCONSOLE_UPDATE_INTERVAL();
    previousMillisConsole = currentMillis;
  }
}
