/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-cam-pir-motion-detector-photo-capture/
 
  IMPORTANT!!!
   - Select Board "AI Thinker ESP32-CAM"
   - GPIO 0 must be connected to GND to upload a sketch
   - After connecting GPIO 0 to GND, press the ESP32-CAM on-board RESET button to put your board in flashing mode
 
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/
 
#include "esp_camera.h"
#include "Arduino.h"
#include "FS.h"                //Obsługa karty SD ESP32
#include "SD_MMC.h"            //Obsługa karty SD ESP32
#include "soc/soc.h"           //Ignoruj problemy z zasilaniem
#include "soc/rtc_cntl_reg.h"  //Ignoruj problemy z zasilaniem
#include "driver/rtc_io.h"
#include <EEPROM.h>            //Obsługa odczytu i zapisu z/do pamięci flash
#include "SPI.h"
#include "html.h"
#include "ESP32_MailClient.h"
#include <SPIFFS.h>
#include "json.h"                 //"util.h" (dołącza credentials.h settings.h
#include <AsyncTCP.h>             //pomocnicza
#include <ESPAsyncWebServer.h>    //do tworzenia WebServera
#include "settings.h"
#include <WiFi.h>                 //obsluga czasu z serwera NTP
#include "time.h"

int pictureNumber = 0;

AsyncWebServer serverWEB(80);     //Tworzenie obiektu AsyncWebServer na porcie 80

RTC_DATA_ATTR int bootCount = 0;

SMTPData smtpData;                //Obiekt danych Email Sending zawiera konfigurację i dane do wysłania

unsigned int  hourx;              //zmienne dla czasu 
unsigned int  minutex; 

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 3600;
const int   daylightOffset_sec = 3600;


/***************************************************
 *  odczyt czasu z sieci
 ***************************************************
 */ 
void getTimeNTP()
{
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo))
  {
    Serial.println("Nie można pobrać czasu");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
  hourx = timeinfo.tm_hour;
  minutex = timeinfo.tm_min;
}


/***********************************************************************************
 * definiowanie reakcji na wysłanie/odbiór danych z/do Asynchronicznego Web Servera 
 *********************************************************************************** 
 */
 void obslugaKlienta()
{
  // ========== Wyslanie strony logowania =================
  serverWEB.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
  {                                                                   //na otrzymane od klienta zapytania pod adresem "/" typu GET, 
    request->send(200, "text/html", logowanie());                     //odpowiedz wywołaniem formularza logowania ze skatcha
  });


  // ========= Sprawdzenie parametru logowania ============
  serverWEB.on("/login", HTTP_POST, [](AsyncWebServerRequest *request)
  {                                                                   //sprawdzenie danych z POST z formularza logowania
    String input_pass;
    int paramNr = request->params();  //liczba parametrów (od 1)
    if (paramNr > 0)
    {
      AsyncWebParameter * p = request->getParam(0);
      input_pass = p->value().c_str();
      
      if(input_pass == logPass)  
      {                                                               //gdy hasło jest poprawne
        if (wifi_conn)                                                //i połączenie przez AP wifi
        {
          request->send(SPIFFS, "/index.html", "text/html");          //wysłanie index.html ze SPIFFS
          Serial.println("Otwarcie index.html");
        }
        else
        {                                                             //brak połączenia przez AP wifi (połączono przez Soft AP)
          request->send(SPIFFS, "/index_min.html", "text/html");      //wysłanie index_min.html ze SPIFFS
          Serial.println("Otwarcie index_min.html");
        }
        sesja=1;
        Serial.println("Zalogowanie klienta");
      }
      else
      {
        request->send(401, "text/plain", "401: Brak autoryzacji");   //wyślij informację ze statusem 401
        Serial.println("Próba logowania z błędnym hasłem!");
      }
    }
    else
    {                                                               //gdy hasło nie pasuje do wzorca
      //gdy brak hasła, wyślij informację ze statusem 400
      request->send(400, "text/plain", "400: Bledne zadanie w logowaniu");
      Serial.println("Nie podano hasła!");
    }
  });    


  // ======== Wysłanie SETUP w postaci JSON do klienta ============
  serverWEB.on("/setup.json", HTTP_GET, [](AsyncWebServerRequest *request)
  {
    if (sesja)
    {                                                               //wskaźnik sesji aktywny
      String json=odp_setup();
      request->send(200, "application/json", json);
      Serial.println("Setup wysłany do klienta");                   //potwierdzenie wysłania
    }      
  });    


  // ========= Odebranie od klienta parametrów ustawien SETUP w postaci JSON              ============
  // ========= i załadowanie do zmiennej strukturalnej parametry oraz zapisanie do EEPROM ============
  serverWEB.on("/z_sz", HTTP_POST, [](AsyncWebServerRequest *request)
  {
    String input_naz, input_par;
      
    int paramIl = request->params();                                //liczba odebranych parametrów 
    Serial.println("Odebrane parametry JSON:");
    for (int i = 0; i < paramIl; i++)
    {
      AsyncWebParameter * p = request->getParam(i);
      input_naz=p->name().c_str();                                  //nazwa otrzymanego parametru
      input_par=p->value().c_str();                                 //zawartosc otrzymanego parametru
      Serial.println(input_naz+": "+input_par);
      aktualizuj_par(i, input_naz, input_par);                      //umieszcza input_par w tablicy parametry
    }
    if (sesja && (paramIl==il_parametrow))                          //ilosc przesłanych parametrów poprawić na wlasciwą !!!!
    {
      eepromWriteParametry();                                       //wpisanie do eeprom wartości z tablicy parametry
      String json=odp_para();
      request->send(200, "application/json", json);                 //wysłanie potwierdzenia aktualizacji do klienta
      Serial.println("Zaktualizowano parametry szczegółowe.");
    }
    else
    {
      request->send(200, "text/html", "Niezgodna ilość parametrow JSON!");
    }
  });
    

  // ======== Wysłanie jquery_min.js ze SPIFF do klienta ============
  serverWEB.on("/jquery_min.js", HTTP_GET, [](AsyncWebServerRequest *request)
  {                                                                 //sciezka do lokalnego zaladowania pliku jquery_min.js ze SPIFFS
    request->send(SPIFFS, "/jquery_min.js", "text/javascript");     //wyslanie jquery_min.js do klenta (przegladarki)
  });
    

  // ================ Wylogowanie ==================
  serverWEB.on("/logout", HTTP_POST, [](AsyncWebServerRequest *request)
  { 
    sesja=0;                                                        //wyzerowanie wskaźnika sesji
    String json=odp_para();
    request->send(200, "application/json", json);                   //wysłanie potwierdzenia
    Serial.println("Wylogowano");
  });
}  


// *********************************
// *    S E T U P
// *********************************
void setup() 
{
  String czlonyS[4];                            //zmienna na człony IP serwera
  String czlonyG[4];                            //zmienna na człony IP bramy
  String czlonyK[4];                            //zmienna na człony IP klienta

  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);    //wyłączenie detektora zaniku zasilania
  Serial.begin(115200);
  Serial.println(); 
  Serial.setDebugOutput(true);

  EEPROM.begin(EEPROM_SIZE);                    //wielkosc pamieci EEPROM musi byc 
  delay(100);

  pinMode(parPin, INPUT_PULLUP);                //do ustawiania parametrów SETUP
  
  if (!SPIFFS.begin(true)) 
  {
    Serial.println("Wystąpił błąd podczas montowania SPIFFS");
    ESP.restart();
  }
  else 
  {
    delay(500);
    Serial.println("SPIFFS został pomyślnie zamontowany");
  }
 
 byte wsk=EEPROM.read(adr_par);               //odczyt wskaźnika ustawionych parametrów z EEPROM
  if (wsk!=wsk_eeprom)                        //wart. wskaźnika różna od oczekiwanej
  {                                           //nie ustawiono parametrów
    Serial.println("\nBrak ustaw. parametrów - ustawienie defaultowe");
    ustawParametryDefault();                  //wstawienie wartości defaultowych
    eepromWriteParametry();                   //zapis do eeprom
  }
  else
  {
    Serial.println("Odczyt parametrów z eeprom");
    eepromReadParametry();                    //wczytanie tablicy "parametry" z eeprom
  }  

  /* przygotowanie parametrów do połączenia z domowym AP sieci wifi */
  parametry.ap_ssid.toCharArray(par_n_AP, (parametry.ap_ssid.length()+1));      //nazwa SSID domowego AP
  parametry.ap_haslo.toCharArray(par_h_AP, (parametry.ap_haslo.length()+1));    //hasło domowego AP 
  podzielIP(parametry.ip_klient, czlonyK);                                      //IP statyczny klienta 
  podzielIP(parametry.ip_brama, czlonyG);                                       //IP bramy AP
  for (byte i1=0; i1<4; i1++)
  {
    klientIP[i1]=czlonyK[i1].toInt();
    gateway[i1]=czlonyG[i1].toInt();
  }

  if (!digitalRead(parPin))
  {
    // ****** naciśnięto przycisk ustawiania parametrów ******
    ustawianie=true;
    WiFi.disconnect();                      //zamknięcie ewentualnego innego połączenia
    polacz_do_wifi();                       //Łączenie do domowej sieci wi-fi    
    if (timeout<10)
    {
      wifi_conn=true;
      Serial.println("------------------------------------");
      Serial.println("|  SETUP");
      Serial.print  ("|  IP klienta wifi: ");
      Serial.println(parametry.ip_klient);
      Serial.print  ("|  Hasło do SETUP:");
      Serial.println(logPass);
      Serial.println("------------------------------------");      
    }
    else    
    {
      Serial.println("\nBrak połączenia z lokalną siecią wifi!!!");
      /* przygotowanie parametrów dla Soft AP  */
      parametry.wb_ssid.toCharArray(par_n_AP,  (parametry.wb_ssid.length()+1));     //SSID Web Serwera SoftAP
      parametry.wb_haslo.toCharArray(par_h_AP, (parametry.wb_haslo.length()+1));    //hasło Web Serwera SoftAP
      WiFi.softAP(par_n_AP, par_h_AP);                //uruchomienie punktu dostępowego wifi (soft Access Point) dla Web Serwera

      Serial.println("------------------------------------");      
      Serial.print  ("|  Soft-AP sdres IP=");
      Serial.println(WiFi.softAPIP());
      Serial.println("|  nazwa=" + parametry.wb_ssid);
      Serial.println("|  hasło=" + parametry.wb_haslo);        
      Serial.println("------------------------------------");
    }
    // ==================  

    obslugaKlienta();                           //definiowanie reakcji na wysłanie/odbiór danych z/do Async Web Servera 
    serverWEB.begin();                          //start serwera http
    Serial.println ("Start serwera HTTP");
  }
  else
  {// ***** brak zwory ustawiania parametrów - zwykły tryb pracy  *****  
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM;
    config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM;
    config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM;
    config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM;
    config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk = XCLK_GPIO_NUM;
    config.pin_pclk = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href = HREF_GPIO_NUM;
    config.pin_sscb_sda = SIOD_GPIO_NUM;
    config.pin_sscb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 20000000;
    config.pixel_format = PIXFORMAT_JPEG;
  
    if(psramFound())
    {
      config.frame_size = FRAMESIZE_UXGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
      config.jpeg_quality = 10;
      config.fb_count = 2;
    } 
    else 
    {
      config.frame_size = FRAMESIZE_SVGA;
      config.jpeg_quality = 12;
      config.fb_count = 1;
    }
 
    //Inicjowanie kamery
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) 
    {
      Serial.printf("Inicjalizacja kamery nie powiodła się z powodu błędu 0x%x", err);
      return;
    }
 
    Serial.println("Start montowania karty SD"); 
    delay(500);
    if(!SD_MMC.begin("/sdcard", true))    //parametry uwalniają GPIO4 (lampa błyskowa) przy obsłudze karty SD
    {
      Serial.println("Zamontowanie karty SD nie powiodło się");
      //return;
    }
 
    uint8_t cardType = SD_MMC.cardType();
    if(cardType == CARD_NONE)
    {
      Serial.println("Brak karty SD");
      return;
    }

    capturePhotoSaveSpiffs();             //zrobienie zdjęcia i zapisanie w SPIFFS i na SD
    
    //Podłączanie do WiFi
    Serial.printf("Podłączanie do %s ", mySSID);
    WiFi.begin(mySSID, myPASSWORD);
    while (WiFi.status() != WL_CONNECTED) 
    {
      delay(500);
      Serial.print(".");
    }
    Serial.println("\nPODŁĄCZONO do WIFI");

    //inicjowanie pobrania czasu
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

    getTimeNTP();                     //wczytanie czasu z NTP    

    int suma_minut_NTP = hourx*60 + minutex;
    int suma_minut_par = ((parametry.godzina.toInt())*60) + parametry.minuta.toInt();
    int zwloka = parametry.min_czas.toInt();
    Serial.print("Zwloka=");
    Serial.println(zwloka);
    Serial.print("Zapisana wartosc czasu wysl. emaila=");
    Serial.println(suma_minut_par);
    Serial.print("Wartosc czasu z NTP=");
    Serial.println(suma_minut_NTP);
    
    if ((suma_minut_NTP >= (suma_minut_par + zwloka)) || (suma_minut_par > suma_minut_NTP))     //czas przekroczony lub zmiana czasu o północy
    {    
      sendPhoto();                    //przygotowanie i wysłanie maila z plikiem obrazu    

      parametry.godzina=String(hourx);
      parametry.minuta=String(minutex); 
      eepromWriteParametry();         //zapis parametrów do EEPROM z czasem wysłania zdjęcia
      Serial.println("Zapisano parametry z czasem wysłania e-maila");
    }
    //przygotowanie do głębokiego uspienia
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_13, 1);  //zdefiniowanie nr pinu i stanu budzenia
 
    Serial.println("Idę teraz spać\n\n");
    delay(1000);
    esp_deep_sleep_start();
    Serial.println("To nigdy nie zostanie wydrukowane, bo ESP32 śpi i obudzi się wykonując skatch od początku");  
  } 
}

// *********************************
// *      L O O P
// *********************************
void loop() 
{ 
}

//----------- Sprawdzenie, czy zrobienie zdjęcia powiodło się
bool checkPhoto( fs::FS &fs ) 
{
  File f_pic = fs.open( FILE_PHOTO );
  unsigned int pic_sz = f_pic.size();
  return ( pic_sz > 100 );
}

//------------ Zrobienie zdjęcia i zapisanie w SPIFFS i na karcie SD
void capturePhotoSaveSpiffs( void ) 
{
  camera_fb_t * fb = NULL;        //pointer
  bool ok = 0;                    //wartość logiczna wskazująca, czy zdjęcie zostało wykonane prawidłowo

  do 
  {                               //powtarzanie przechwycenia zdjęcia z kamery do czasu uzyskania poprawnego obrazu
    fb = esp_camera_fb_get();     //przechwycenie zdjęcia z kamery
    if(!fb) 
    {
      Serial.println("Przechwytywanie z kamery nie powiodło się");
      return;
    }
    
    Serial.printf("Picture file name: %s\n", FILE_PHOTO);     //nazwa pliku foto w SPIFFS
    File file = SPIFFS.open(FILE_PHOTO, FILE_WRITE);

    //Wstawienie danych do pliku ze zdjęciem
    if (!file) 
    {
      Serial.println("Nie udało się otworzyć pliku w trybie zapisu");
    }
    else 
    {
      file.write(fb->buf, fb->len);                   //załącznik (obraz), wielkość załącznika
      Serial.print("Zdjęcie zostało zapisane w ");
      Serial.print(FILE_PHOTO);
      Serial.print(" - Size: ");
      Serial.print(file.size());
      Serial.println(" bytes");
    }    
    file.close();                                   //zamknięcie pliku
    
    ok = checkPhoto(SPIFFS);                        //sprawdzenie czy plik został poprawnie zapisany w SPIFFS    
    if (ok)                                         //plik obrazu poprawny czyli zapis pliku obrazu na karcie SD
    {           
      EEPROM.begin(EEPROM_SIZE);                    //inicjowanie EEPROM z predefiniowanym rozmiarem
      pictureNumber = EEPROM.read(0) + 1;
 
      //Ścieżka i nazwa pliku, w którym nowe zdjęcie zostanie zapisane na karcie SD
      String path = "/picture" + String(pictureNumber) +".jpg";
 
      fs::FS &fs = SD_MMC;
      //Serial.printf("Nazwa pliku obrazu: %s\n", path.c_str());
 
      File fileSD = fs.open(path.c_str(), FILE_WRITE);
      if(!fileSD)
      {
        Serial.println("Nie udało się otworzyć pliku w trybie zapisu");
      }
      else 
      {    
        fileSD.write(fb->buf, fb->len);           //załącznik (obraz), wielkość załącznika
        Serial.printf("Plik zapisano w: %s\n", path.c_str());
        EEPROM.write(0, pictureNumber);
        EEPROM.commit();
      }
      fileSD.close();      
    }
    
    esp_camera_fb_return(fb);                   //zwrot bufora ramek z powrotem do sterownika w celu ponownego użycia
  } while ( !ok );
  
  delay(1000);
}

//------------- Przygotowanie i wysłanie maila z plikiem obrazu  
void sendPhoto( void ) 
{
  int portInt=parametry.nr_port.toInt();
  
    Serial.print("IP Address: http://");      //wydruk lokalnego adresu IP ESP32
    Serial.println(WiFi.localIP());  
    
    //Przygotowanie emaila
    Serial.println("Wysyłanie emaila...");
    Serial.print("par.n_smtp=");
    Serial.println(parametry.n_smtp + "|");
    Serial.print("portInt=");
    Serial.print(portInt); Serial.println("|");
    Serial.print("par.n_nadaw=");
    Serial.println(parametry.n_nadaw + "|");
    Serial.print("par.h_nadaw=");
    Serial.println(parametry.h_nadaw + "|");
    delay(500);

      //ustawienie hosta poczty e-mail serwera SMTP, port, konto i hasło
    smtpData.setLogin(parametry.n_smtp, portInt, parametry.n_nadaw, parametry.h_nadaw);  
  
    smtpData.setSender("ESP32-CAM", parametry.n_nadaw);  //ustawienie nazwy nadawcy i adresu e-mail  
  
    smtpData.setPriority("High");               //ustawienie priorytetu lub ważności wiadomości e-mail 
                                                //High, Normal, Low lub od 1 do 5 (1 to najwyższa)  
    smtpData.setSubject(parametry.temat);       //ustawienie tematu
  
      //ustawienie wiadomości e-mail w formacie HTML
    smtpData.setMessage("<h2>Zdjecie zrobione za pomoca ESP32-CAM dolaczone do tego e-maila.</h2>", true);
      //ustawienie wiadomości e-mail w formacie tekstowym
      //smtpData.setMessage("Zdjecie zrobione za pomoca ESP32-CAM dolaczone do tego e-maila.", false);
  
    smtpData.addRecipient(parametry.n_docel);        //dodaj odbiorców, można dodać więcej niż jednego odbiorcę
    //smtpData.addRecipient(emailRecipient2);
  
    smtpData.addAttachFile(FILE_PHOTO, "image/jpg");              //dodaj pliki załączników z SPIFFS
  
    smtpData.setFileStorageType(MailClientStorageType::SPIFFS);   //ustaw typ przechowywania, aby dołączyć pliki 
                                                                  //do wiadomości e-mail (SPIFFS)
    smtpData.setSendCallback(sendCallback);  
  
    if (!MailClient.sendMail(smtpData))       //rozpocznij wysyłanie wiadomości e-mail, można ustawić funkcję oddzwaniania, 
                                              //aby śledzić status
      Serial.println("Błąd wysyłania e-maila, " + MailClient.smtpErrorReason());
  
    smtpData.empty();                         //wyczyść wszystkie dane z obiektu Email, aby zwolnić pamięć
  
}

//------ Funkcja odpowiadania, aby uzyskać status wysłania wiadomości e-mail
void sendCallback(SendStatus msg) 
{  
  Serial.println(msg.info());   //wydruk aktualnego stanu
}
