#include "esp_camera.h"
#include <WiFi.h>
#include <ESPAsyncWebServer.h> //https://github.com/me-no-dev/ESPAsyncWebServer#why-should-you-care
#include <SPIFFS.h>
#include <FS.h>

//OPCIONAL
#include "soc/soc.h"           // Deshabilita problemas de caída de tensión 
#include "soc/rtc_cntl_reg.h"  // Deshabilita problemas de caída de tensión 

// WARNING!!! PSRAM IC required for UXGA resolution and high JPEG quality
//            Ensure ESP32 Wrover Module or other board with PSRAM is selected
//            Partial images will be transmitted if image exceeds buffer size
//
//            You must select partition scheme from the board menu that has at least 3MB APP space.
//            Face Recognition is DISABLED for ESP32 and ESP32-S2, because it takes up from 15 
//            seconds to process single frame. Face Detection is ENABLED if PSRAM is enabled as well

#include "pines.h"

// ===========================
// Enter your WiFi credentials
// ===========================
#include "secrets.h"
const char* ssid = SS_ID;
const char* password = PASS;
const char* ssid_AP = SS_ID_AP;
const char* password_AP = PASS_AP;

void connectWifi(){
  Serial.print("Conectando a WiFi");
  unsigned int i=0;
  WiFi.begin(ssid,password);
  WiFi.setSleep(false); //matiene la conexión wifi y por ende el servidor
  while(WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(".");
    i++;
    if(i>50){
      WiFi.begin(ssid,password); 
      i = 0;
    }
  }
  Serial.println("Wifi conectado");
}

// Crear un objeto AsyncWebServer en el puerto 80
AsyncWebServer server(80);

// Nombre de archivo para guardar en SPIFFS
#define FILE_PHOTO "/foto.jpg"

//boolean takeNewPhoto = false; para otro programa en donde se decide tomar una foto
// cuando se quiera

bool cameraAvailable = false;
#define RESPONSE_TIMEOUT 5000
#define MAX_BLOCK_SIZE 16384
#define FREC_FOTOS 1*60000 // TIEMPO ENTRE FOTOS EN MIN
unsigned long T_ULT_FOTO = 0;

bool initCamera(){
  camera_config_t config;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;  
  config.pin_d5 = Y7_GPIO_NUM;  
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;

  config.xclk_freq_hz = 20000000;
  config.ledc_timer = LEDC_TIMER_0;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.pixel_format = PIXFORMAT_JPEG; 
    /*PIXFORMAT_ + 
      RGB565,    // 2BPP/RGB565 for face detection/recognition
      YUV422,    // 2BPP/YUV422
      GRAYSCALE, // 1BPP/GRAYSCALE
      JPEG,      // JPEG/COMPRESSED
      RGB888,    // 3BPP/RGB888
      RAW,       // RAW
      RGB444,    // 3BP2P/RGB444
      RGB555,    // 3BP2P/RGB555
    */
  config.frame_size = FRAMESIZE_UXGA; 
    /*FRAMESIZE_ +
      96X96,    // 96x96
      QQVGA,    // 160x120
      QCIF,     // 176x144
      HQVGA,    // 240x176
      240X240,  // 240x240 best option for face detection/recognition
      QVGA,     // 320x240
      CIF,      // 400x296
      HVGA,     // 480x320
      VGA,      // 640x480
      SVGA,     // 800x600
      XGA,      // 1024x768
      HD,       // 1280x720
      SXGA,     // 1280x1024
      UXGA,     // 1600x1200 
    */
  config.jpeg_quality = 10; //0-63 lower means higher quality
  config.fb_count = 1; //Number of frame buffers to be allocated. If more than one, then each frame will be acquired (double speed)
  config.grab_mode = CAMERA_GRAB_LATEST;
    //CAMERA_GRAB_WHEN_EMPTY, llena los búferes cuando están vacíos. Menos recursos, pero los primeros fotogramas 'fb_count' pueden ser antiguos
    //CAMERA_GRAB_LATEST, excepto cuando se usa 1 búfer de cuadro, la cola siempre contendrá los últimos cuadros 'fb_count'
  config.fb_location = CAMERA_FB_IN_PSRAM;

  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Inicio de cámara fallido: error 0x%x", err);
    return false;
  }

  sensor_t * s = esp_camera_sensor_get();
  /*s->set_brightness(s, 0); // -2 to 2
  s->set_contrast(s, 2); // -2 to 2
  s->set_saturation(s, -2); // -2 to 2
  s->set_whitebal(s, 1); // 0 = disable , 1 = enable
  s->set_awb_gain(s, 1); // 0 = disable , 1 = enable
  s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
  s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable
  s->set_aec2(s, 1); // 0 = disable , 1 = enable
  s->set_gain_ctrl(s, 0); // 0 = disable , 1 = enable
  s->set_agc_gain(s, 0); // 0 to 30
  s->set_gainceiling(s, (gainceiling_t)6); // 0 to 6
  s->set_bpc(s, 1); // 0 = disable , 1 = enable
  s->set_wpc(s, 1); // 0 = disable , 1 = enable
  s->set_raw_gma(s, 1); // 0 = disable , 1 = enable (makes much lighter and noisy)
  s->set_lenc(s, 1); // 0 = disable , 1 = enable
  s->set_hmirror(s, 0); // 0 = disable , 1 = enable
  s->set_vflip(s, 0); // 0 = disable , 1 = enable
  s->set_dcw(s, 0); // 0 = disable , 1 = enable*/

  return true;
}

void mountSPIFFS(){
  if(!SPIFFS.begin(true)){
    return;
  }
  Serial.println("SPIFFS montado con éxito");
}

void initServer(){
  //on(uri, HTTPMethod, función a ejecutar), uri=/ --> raíz de la página web
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    //send(File content, path, contentType, download=false);
    request->send(SPIFFS, FILE_PHOTO, "image/jpg", false);
  });
  
  server.onNotFound([](AsyncWebServerRequest *request) {
      request->send(400, "text/plain", "Not found");
   });
  // Start server
  server.begin();

  Serial.print("Conectarse a http://");
  Serial.println(WiFi.localIP());
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true); //activa la salida de depuración de la biblioteca WiFi para ver lo que está sucediendo con la conexión WiFi.
  
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //Apaga el 'detector de caídas de tensión' https://iotespresso.com/how-to-disable-brownout-detector-in-esp32-in-arduino/
  
  if (initCamera()){
    Serial.println("Cámara lista.");
    cameraAvailable = true;
   }
  mountSPIFFS();
  connectWifi();
  initServer();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWifi();
  }
  fotoToWebServer();
  delay(500);
}

void fotoToWebServer(){
  if( (cameraAvailable && (millis()-T_ULT_FOTO>FREC_FOTOS)) || T_ULT_FOTO==0){
    camera_fb_t * fb = NULL; //iniciamos el puntero a null

    bool ok = false;

    while(!ok){
      fb = esp_camera_fb_get(); //NUEVA CAPTURA
      if (!fb){
        Serial.println("Fallo en la captura");
        return;
      }

      File file = SPIFFS.open(FILE_PHOTO, FILE_WRITE);
      // Insertamos los datos
      if (!file) {
        Serial.println("Fallo al abrir el archivo en modo escritura");
      }
      else {
        file.write(fb->buf, fb->len); // payload (image), payload length
      }
      file.close();
      esp_camera_fb_return(fb); //una vez subida la imagen liberar la memoria asignada
      ok = checkPhoto(SPIFFS);
    }
    T_ULT_FOTO = millis();
  }
}

// Verificar que la captura haya sido exitosa
bool checkPhoto( fs::FS &fs ) {
  File f_pic = fs.open( FILE_PHOTO );
  unsigned int pic_sz = f_pic.size();
  return ( pic_sz > 100 );
}