#include <WiFi.h>
#include <time.h>
#include <ESP32Ping.h>
#include <iostream>
#include <string>
// #include <string.h>
#include <EEPROM.h>



#define EEPROM_SIZE 512

#define word_s unsigned short int
#define EE_HAY_DATOS (byte)0                                       // byte. 0xab si hay datos
#define EE_SSID_SIZE (byte)(EE_HAY_DATOS + sizeof(byte))           // byte. Total de bytes que ocupan las SSID con sus pass (incluye los 0 con los que terminan los strings)
#define EE_SSID_TOTAL (byte)(EE_SSID_SIZE + sizeof(byte))          // byte. Total de SSID con sus password guardados
#define EE_SSID_PREFERIDA (byte)(EE_SSID_TOTAL + sizeof(byte))     // byte. Numero de la SSID preferida para conectar
#define EE_SSID_GUARDADAS (byte)(EE_SSID_PREFERIDA + sizeof(byte)) // variable, tiene que estar al final. A partir de aca se guardan las SSID y sus password. Son stringz
// #define EE_TOTAL_VALORES (EE_SSID_PREFERIDA + sizeof(byte)) // byte, ojo, aca tengo que poner en sizeof, el tamaño de la linea anterior

#define WIFI_TIME_OUT_CONECTAR (word_s)10000 // 10 segundos
#define WIFI_TIME_OUT_INTERNET (word_s)10000 // 10 segundos
#define MAX_CANTIDAD_SSID (byte)5
#define WIFI_TIME_OUT_DATE_ERROR (word_s)30 * 1000
#define WIFI_TIME_OUT_DATE_NEXT (unsigned long)3600 * 1000
#define WIFI_AVISO_INTENTANDO (word_s)500 // indica que estoy intentando conectar
#define findeThisSSID(ii) ((void)findSSID(ii, ii + 1))
#define findSSIDcircular(x,y) ((byte)findSSID(x, y,true))
// #define MAX_BYTES_SSID (byte)100

struct WifiCredentials
{
  const char *ssid;
  const char *pass;
};

// funciones
void printLocalTime(void);
bool hayInternet(void);
bool estoyConectado(void);
byte findSSID(byte ssidInicial, byte ssidFinal, bool flgCircular=false);
void wifiIni(void);
bool updateDate(void);
word_s getByteEEPROM(byte posicion);
void getStringEEPROM(byte dondeInicia, byte *dondeGuardo);
bool conectarWifi(void);
void writeArrayEEPROM(void);
void writeStringToEEPROM(byte address, char * data);

// variables
// const char *ssid = "HT";
// const char *password = "ElGlobito1973Me";
// bool flgSSIDencontrada = false;
// bool flgHoraActualizada = false;
// bool flgWifiSSIDok;
const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -3 * 3600; // Ajusta esta variable a tu huso horario (en segundos)
const int daylightOffset_sec = 0;     // Ajusta esta variable si tu región tiene horario de verano (en segundos)
const IPAddress remote_ip(8, 8, 8, 8);
bool flgHayInternet = false;
bool flgWiFiSSIDfound = false;
bool flgWiFiconectado = false;
bool flgEEPROMbegin = false;
bool flgDateOK = false;
byte ssidPreferida;
byte totalSSID;
WifiCredentials arrayWifiCredentials[MAX_CANTIDAD_SSID];
WifiCredentials ht_wifi_credentials = {
    .ssid = "HT",
    .pass = "ElGlobito1973Me"
};

void setup()
{

  Serial.begin(57600);
  writeArrayEEPROM(); 
  wifiIni(); // inicio el Wifi
  findSSID(0, 4);
  for(;;);
  while(true){
    conectarWifi();
  }

  for(;;);


  Serial.println("Try");
  findeThisSSID(ssidPreferida);
  if (flgWiFiSSIDfound) {
    Serial.println("Found");
    while (estoyConectado() == false) {
      Serial.print('.');
      delay(100);
    }
  }
  else {
    Serial.println("No esta la SSID");
    for (;;);
  }
  Serial.println("");
  Serial.println("hayInternet()");
  for (;;) {
    hayInternet();
  }
}


void loop()
{
  while (flgHayInternet == false) {
    conectarWifi();
    hayInternet();
  }
}

word_s getByteEEPROM(byte posicion)
{
  byte byteEnEEPROM;
  EEPROM.get(posicion, byteEnEEPROM);
  return byteEnEEPROM;
}

void getStringEEPROM(byte dondeInicia, byte *dondeGuardo)
{
  /*
    obtengo strings terminados en 0 de la EEPROM de la posicion dondeInicia y los guardo en el string dondeGuardo
  */
  byte byteLeido, ii;

//  Serial.println("getStringEEPROM()");
//  Serial.println("dondeInicia: "+String(dondeInicia));
//  for(;;);
  byteLeido = 255;
  for (ii = 0; byteLeido != 0; ++ii)
  {
//    byteLeido = getByteEEPROM(dondeInicia + ii);
    byteLeido = EEPROM.read((byte)(dondeInicia + ii));
    dondeGuardo[ii]=byteLeido;
//    Serial.println(byteLeido);
//    *(dondeGuardo + ii) = byteLeido;
  }
  *(dondeGuardo + ii) = 0;
}

void wifiIni(void)
{
  /*
    Cargo los SSID/pass si los hubiera desde ROM.
    Cargo el Wifi de HT
    Cargo la SSID favorita si la hubiera, si no, la de HT.
    EE_HAY_DATOS == 0xab -> hay datos en EEPROM
  */
  byte *ptrSSIDdata;
  byte sizeString;
  byte posicionEEPROM;
  byte hayDatos;
  byte size;

  Serial.println("wifiIni()");
  if (flgEEPROMbegin == false)
  {
    // EEPROM.begin(EE_TOTAL_VALORES);
    EEPROM.begin(EEPROM_SIZE);
    flgEEPROMbegin = true;
  }

  //hayDatos = getByteEEPROM(EE_HAY_DATOS);
  hayDatos=EEPROM.read((byte)EE_HAY_DATOS);
  totalSSID = 0;

  if (hayDatos == 0xab)
  { // hay datos grabados en flash?
//    Serial.print("hay datos: ");  
    totalSSID = EEPROM.read((byte)EE_SSID_TOTAL);
    //totalSSID = getByteEEPROM(EE_SSID_TOTAL);            // cuando grabo la EEPROM, tengo que darle 0 o el total de las SSID.
    size=EEPROM.read((byte)EE_SSID_SIZE);
    //EEPROM.get(EE_SSID_SIZE, size);
    ptrSSIDdata = new byte[size]; // creo suficiente memoria para cargar los SSID/Pass que estan en EEPROM
//    ptrSSIDdata = new byte[getByteEEPROM(EE_SSID_SIZE)]; // creo suficiente memoria para cargar los SSID/Pass que estan en EEPROM
    posicionEEPROM = EE_SSID_GUARDADAS;
    // ptrSSIDdata = 0;

    // cargo en memoria los SSID/Pass que estan en EEPROM
    if (totalSSID > 0)
    {
      for (byte ii = 0; ii < totalSSID; ++ii)
      {
        getStringEEPROM(posicionEEPROM, ptrSSIDdata);
        arrayWifiCredentials[ii + 1].ssid = (char *)ptrSSIDdata;
        sizeString = strlen((char *)ptrSSIDdata) + 1;
        posicionEEPROM += sizeString;
        ptrSSIDdata += sizeString;

        getStringEEPROM(posicionEEPROM, ptrSSIDdata);
        arrayWifiCredentials[ii + 1].pass = (char *)ptrSSIDdata;
        sizeString = strlen((char *)ptrSSIDdata) + 1;
        posicionEEPROM += sizeString;
        ptrSSIDdata += sizeString;
      }
    }
    ssidPreferida = EEPROM.get(EE_SSID_PREFERIDA, ssidPreferida);
    //EEPROM.read((byte)EE_SSID_PREFERIDA)
//    ssidPreferida = getByteEEPROM(EE_SSID_PREFERIDA);
  } else {
    ssidPreferida = 0; // favorita es la de HT (no hay datos en EEPROM)
/*    
    Serial.print("No hay datos: ");
    Serial.println(hayDatos);
*/    
  }
  arrayWifiCredentials[0] = ht_wifi_credentials; // cargo la de la fabrica
  ++totalSSID;
/*  
  Serial.print("totalSSID:");
  Serial.println(totalSSID);
  Serial.print("ssidPreferida:");
  Serial.println(ssidPreferida);  
    Serial.println(arrayWifiCredentials[ssidPreferida].ssid);    
    Serial.println(arrayWifiCredentials[ssidPreferida].pass);      
  Serial.print("size:");
  Serial.println(size);    
  for(int ii=0;ii < totalSSID;++ii){
    Serial.println(arrayWifiCredentials[ii].ssid);    
    Serial.println(arrayWifiCredentials[ii].pass);    
  }  
*/  
}

bool conectarWifi(void)
{
  static bool flgTimerConectarWiFi;
  static word_s timerConectarWiFi;
  static bool flgTimerTenerInternet;
  static word_s timerTenerInternet;
  static bool flgAvisoIntentandoIni;
  bool flgNoBusquePreferida;
  static word_s timerAvisoIntentando;
  byte startSSID;

  startSSID = 0;
  flgNoBusquePreferida = false;

  Serial.println("conectarWifi()");
  if (flgWiFiSSIDfound == true && (flgWiFiconectado == false || flgHayInternet == false)) // encontre la SSID pero todavia no me conecte o me conecte y no tengo Internet
  {
    Serial.println("encontre la SSID pero todavia no me conecte o me conecte y no tengo Internet");
    if (((word_s)millis() - timerConectarWiFi > WIFI_TIME_OUT_CONECTAR && (flgWiFiconectado == false)) || (((word_s)millis() - timerTenerInternet > WIFI_TIME_OUT_INTERNET) && (flgHayInternet == false))) // el tiempo paso y no me pude conectar o no tengo internet?
    {
      Serial.println("desconecto");
      if (((word_s)millis() - timerConectarWiFi > WIFI_TIME_OUT_CONECTAR && (flgWiFiconectado == false))) {
        Serial.println("1");
      }
      if ((((word_s)millis() - timerTenerInternet > WIFI_TIME_OUT_INTERNET) && (flgHayInternet == false))) {
        Serial.println("2");
      }
      WiFi.disconnect();             // desconecto la red
      flgTimerConectarWiFi = false;  // reseteo el timer
      flgTimerTenerInternet = false; // reseteo el timer
      flgNoBusquePreferida = true;   // no busque la preferida
      flgAvisoIntentandoIni = false; // reseteo el timer
      if (ssidPreferida == (totalSSID - 1))
      { // ya no puedo incrementar?
        startSSID = 0; // arranque desde el principio
      }
      else
      {
        startSSID++;
      }
    }
    else
    {
      // no paso el tiempo
      if (flgAvisoIntentandoIni == false)
      {
        Serial.println("inicio timer indicador de intentando conectar");
        timerAvisoIntentando = (word_s)millis(); // inicio timer indicador de intentando conectar
        flgAvisoIntentandoIni = true;
      }
      else
      {
        Serial.println("(word_s)millis() - timerAvisoIntentando > WIFI_AVISO_INTENTANDO");
        if ((word_s)millis() - timerAvisoIntentando > WIFI_AVISO_INTENTANDO)
        {
          flgAvisoIntentandoIni = false; // reseteo el timer
          Serial.print('.');
        }
      }
      return false; // no me conecte todavia
    }
  }
//  Serial.println("no encontre la SSID");
  flgWiFiSSIDfound = false; // no encontre SSID
  // si no hay una preferida, cuando grabo EEPROM tengo que poner 0 que es HT
  if (ssidPreferida != 0) // hay una SSID preferida?
  {
    Serial.println("hay una SSID preferida");
    findeThisSSID(ssidPreferida);
  }

  if (flgWiFiSSIDfound == false) // no encontre la SSID
  {
    // trato de conectar a cualquier SSID
    Serial.println("trato de conectar a cualquier SSID");
    ssidPreferida = findSSID(startSSID, totalSSID);
  }

  if (flgWiFiSSIDfound == false) // no encontre la SSID
  {
    Serial.println("reseteo el timer");
    flgAvisoIntentandoIni = false; // reseteo el timer
    return false;                  // no hay ninguna
  }

  // encontre una SSID valida
  if (estoyConectado())
  {
    Serial.println("Conectado a WiFi");
    if (hayInternet())
    {
      Serial.println("Hay Internet");
      updateDate();
      return true;
    }
    else
    {
      // estoy conectado, pero no tengo internet
      Serial.println("estoy conectado, pero no tengo internet");
      if (flgTimerTenerInternet == false) // no inicie el timer de time out conectando a WiFi ?
      {
        timerTenerInternet = (word_s)millis();
        flgTimerTenerInternet = true; // inicio el timer
      }
      return false; // no me conecte todavia
    }
  }
  else
  {
    // encontre una SSID pero todavia no me conecte
    Serial.println("encontre una SSID pero todavia no me conecte");
    if (flgTimerConectarWiFi == false) // no inicie el timer de time out conectando a WiFi ?
    {
      Serial.println("inicio el timer");
      timerConectarWiFi = (word_s)millis();
      flgTimerConectarWiFi = true; // inicio el timer
    }
    return false; // no me conecte todaviaF
  }
}

bool updateDate(void)
{
  static bool flgUpdateDateIni = false;
  static unsigned long timerUpdateDate;
  static unsigned long timeOtroIntento;
  struct tm timeinfo;

  if (flgUpdateDateIni == false) // no inicie el timer de time out conectando a WiFi ?
  {
    timerUpdateDate = millis();
    flgUpdateDateIni = true;                                  // inicio el timer
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); // Configurar el RTC con el servidor NTP
    if (!getLocalTime(&timeinfo))                             // error obteniendo el tiempo?
    {
      timeOtroIntento = WIFI_TIME_OUT_DATE_ERROR;
      flgDateOK = false; // error actualizando la fecha
      return false;      // los datos recibidos no fueron validos
    }
    else
    {
      printLocalTime();
      timeOtroIntento = WIFI_TIME_OUT_DATE_NEXT;
      flgDateOK = true;
      return true;
    }
  }
  else
  {
    // inicie el timer
    if (millis() - timerUpdateDate > timeOtroIntento)
    {
      flgUpdateDateIni = false; // es hora de intentar actualizar nuevamente
    }
    return false; // no me conecte todavia
  }
}

byte findSSID(byte ssidInicial, byte ssidFinal, bool flgCircular)
{
  byte ii;

  //  flgWiFiconectado = false;
  //  WiFi.disconnect(); // desconecto la red

  Serial.println("findSSID(ssidInicial="+String(ssidInicial)+",ssidFinal="+String(ssidFinal)+",flgCircular="+String(flgCircular)+")");
  for (byte ii = ssidInicial; ii < ssidFinal; ++ii)
  {
    WiFi.begin(arrayWifiCredentials[ii].ssid, arrayWifiCredentials[ii].pass);
    Serial.println("begin: "+ String(arrayWifiCredentials[ii].ssid));
    if (WiFi.status() != WL_NO_SSID_AVAIL)
    {
      flgWiFiSSIDfound = true; // encontre la SSID
      Serial.println("status: "+String(WiFi.status()));
      return ii;               // encontre una SSID valida
    }
    else
    {
      continue;
    }
  }
  
  if (flgCircular == false || ssidInicial == 0)
  {
    flgWiFiSSIDfound = false; // no encontre la SSID
    return 0xff;              // no esta la SSID
  }else{
    return findSSID(0, ssidInicial, false);
  }
}

#define WIFI_TIME_TEST_HAY_INTERNET (word_s)30000 // 30". Cada este tiempo, verifico si hay Internet
#define WIFI_TIME_TEST_CORTO_HAY_INTERNET (word_s)10000
bool hayInternet(void)
{
  static bool flgtimerTestInternetIni = false;
  static word_s timerTestInternet;
  static word_s lapsoTestInternet;

  if (flgWiFiconectado == true)
  {
    if (flgtimerTestInternetIni == false)
    {
      // Realiza un ping al servidor DNS de Google (8.8.8.8)
      if (Ping.ping(remote_ip, 1))
      {
        flgHayInternet = true;
      }
      else
      {
        flgHayInternet = false;
      }
      timerTestInternet = (word_s)millis();
      flgtimerTestInternetIni = true;
      if (flgHayInternet == false) { // no tengo internet?
        lapsoTestInternet = WIFI_TIME_TEST_CORTO_HAY_INTERNET;
      } else {
        lapsoTestInternet = WIFI_TIME_TEST_HAY_INTERNET;
      }
      Serial.print("Ping: ");
      Serial.println(flgHayInternet ? "Si" : "No");
    }
    else
    {
      if ((word_s)((word_s)millis() - timerTestInternet) > lapsoTestInternet)
      {
        flgtimerTestInternetIni = false; // reinicio el timer
        Serial.println("Timer");
      } else {
        //        Serial.println((word_s)((word_s)millis() - timerTestInternet));
      }
    }
  }
  else
  {
    flgHayInternet = false;
  }
  //  Serial.println(flgHayInternet ? "Si" : "No");
  return flgHayInternet;
}

bool estoyConectado(void)
{
  if (WiFi.status() == WL_CONNECTED)
  {
    flgWiFiconectado = true;
    return true;
  }
  flgWiFiconectado = false;
  return false;
}

void printLocalTime()
{
  struct tm timeinfo;

  if (!getLocalTime(&timeinfo))
  {
    Serial.println("Error al obtener la hora");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

const char *ssdi1="Wokwi-GUEST";
const char *pass1="";
const char *ssdi2="HTFULL";
const char *pass2="PASS3";
const char *ssdi3="FUENTESDEENERGIA";
const char *pass3="PASS2";

void writeArrayEEPROM(void)
{
  /*
  Guardo en la EEPROM los valores de totalSSID, ssidPreferida y los SSID/Pass guardados en el array
  */
//  return;
  byte ptr;
  EEPROM.begin(EEPROM_SIZE);

  EEPROM.put((byte)EE_HAY_DATOS, 0xab); // hay datos
  EEPROM.put((byte)EE_SSID_TOTAL, 3);
  EEPROM.put((byte)EE_SSID_PREFERIDA, 1);

  ptr=(byte)EE_SSID_GUARDADAS;

  writeStringToEEPROM(ptr, ssdi1);
//  EEPROM.put(ptr, ssdi1);
  ptr+=(strlen(ssdi1)+1);
  writeStringToEEPROM(ptr, pass1);  
//  EEPROM.put(ptr, pass1);
  ptr+=(strlen(pass1)+1);


//  EEPROM.put(ptr, ssdi2);
writeStringToEEPROM(ptr, ssdi2);
  ptr+=(strlen(ssdi2)+1);
writeStringToEEPROM(ptr, pass2);   
//  EEPROM.put(ptr, pass2);
  ptr+=(strlen(pass2)+1);

//  EEPROM.put(ptr, ssdi3);
writeStringToEEPROM(ptr, ssdi3);
  ptr+=(strlen(ssdi3)+1);
//  EEPROM.put(ptr, pass3);
writeStringToEEPROM(ptr, pass3);    
  ptr+=(strlen(pass3)+1);   


  EEPROM.put((byte)EE_SSID_SIZE,(byte)(ptr-EE_SSID_GUARDADAS));  

  EEPROM.commit();
/*  
  Serial.println("EE_HAY_DATOS: "+ String(EEPROM.read((byte)EE_HAY_DATOS)));
  Serial.println("EE_SSID_TOTAL: "+ String(EEPROM.read((byte)EE_SSID_TOTAL)));
  Serial.println("EE_SSID_PREFERIDA: "+ String(EEPROM.read((byte)EE_SSID_PREFERIDA)));
  Serial.println("EE_SSID_SIZE: "+ String(EEPROM.read((byte)EE_SSID_SIZE)));
*/  
  flgEEPROMbegin = true;
/*  
  for(int ii=0;ii < totalSSID;++ii){
    Serial.println(arrayWifiCredentials[ii + 1].ssid);    
    Serial.println(arrayWifiCredentials[ii + 1].pass);    
  }
  for(;;);
*/  
}

void writeStringToEEPROM(byte address, const char * data) {
  byte size;

  size=strlen(data);
  for (byte i = 0; i < size + 1; i++) {
    EEPROM.write(address + i, data[i]);
  }
  EEPROM.commit();
}