// NB. I am using the Arduino IDE v1.8.19,
// and the ESP32 software from "https://github.com/espressif/arduino-esp32", BUT the ESP32 software version downloaded on 28May21.
// (Note that the version as at now (19Aug22) seems to have a bug in it that stops the camera giving a valid photo.)
// I am using Board="AI Thinker ESP32-CAM"

#include "FS.h"
#include <SD.h>
#include <SPI.h>
#include <Adafruit_GFX.h>    // https://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_ILI9341.h> // https://github.com/adafruit/Adafruit_ILI9341
#include <TJpg_Decoder.h>    // https://github.com/Bodmer/TJpg_Decoder
#include "esp_camera.h"
#define CAMERA_MODEL_AI_THINKER // Has PSRAM
#include "camera_pins.h"
#define BLOCK_SERIAL // comment-out to use GPIO-1 & GPIO-3 for Serial-output instead (useful for debugging Camera & TFT)

#ifdef BLOCK_SERIAL
 #define Serial tft
 #define SD_CS 1     // via 1k for safety
 #define SWITCH_PIN 3 // via 1k for safety
#else
 // GPIO-1 is ESP32-Tx
 // GPIO-3 is ESP32-Rx
#endif

#define RED_LED 33
#define SPI_MOSI 13
#define SPI_MISO 12
#define SPI_CLK 14
#define TFT_DC   2 // Data Command control pin
#define TFT_CS  15 // Chip select control pin
#define TFT_RST -1 // or can connect to GPIO-16 via 1k for safety (but GPIO also used for the PSRAM)

SPIClass MySPI(HSPI); // Declare an HSPI bus object (to use or BOTH TFT and SD-card)
Adafruit_ILI9341 tft = Adafruit_ILI9341(&MySPI, TFT_DC, TFT_CS, TFT_RST);
// Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, SPI_MOSI, SPI_CLK, TFT_RST,SPI_MISO);
#define TFT_ROTATION 0 // 0 to 3
boolean camera_ok;

// **************************************************
void setup() { // **************************************
 tft.begin();
 tft.setRotation(TFT_ROTATION);
 tft_clear();
 tft.println("Hello World");
 #ifdef RED_LED
   pinMode(RED_LED, OUTPUT);
   LED_flash(2); 
 #endif
 #ifdef BLOCK_SERIAL
   #ifdef SWITCH_PIN
     pinMode(SWITCH_PIN, INPUT_PULLUP);
   #endif
   #ifdef SD_CS
     Serial.print("SDcard: ");
     boolean success=SD.begin(SD_CS,MySPI);
     if (success) {Serial.println("OK");} // works!
     else {Serial.println("Fail: check formatted SD-card is in!"); return;} // dont do the rest of setup()!
   #endif
 #else
   Serial.begin(115200);
   Serial.println("ESP32-CAM WITHOUT SD-Card");
 #endif
 camera_ok=camera_init();
 TJpgDec.setJpgScale(1);
 TJpgDec.setCallback(tft_output); // "tft_output" is passed to the decoder
 Serial.print("\nRunning ");
 if (camera_ok) Serial.println("OK"); else Serial.println("but without Camera");
 delay(5000);
} // end of setup() // ***********************************
// **************************************************

// **************************************************
void loop() { //****************************************
 #define photo_count_max 12 // 0 to 11
 static int photo_next_free=photo_count_max; // 0=none, -1=none-already-reported // set to "photo_count_max" on boot so will count number of photos!
 #ifdef SWITCH_PIN
   boolean push_switch=!digitalRead(SWITCH_PIN);
   if (push_switch) {
     tft_clear();
     Serial.println("Release for photo OR");
     Serial.println("Hold 3s = delete ALL");
     int x;
     for (x=0;x<12;x++) {delay(250); if (digitalRead(SWITCH_PIN)) break;} // allow for switch bounce
     if (x==12) {push_switch=false; sd_wipe_all(); photo_next_free=0;}
   }
 #else
   delay(5000);
   boolean push_switch=true;
 #endif
 if (push_switch) photo_next_free=take_picture(photo_next_free);
 else if (photo_next_free>0) {photo_next_free=sd_load_and_display(photo_next_free);} // NEXT-EMPTY photo-count
 else if (photo_next_free==0) {photo_next_free=-1; tft_clear(); Serial.println("No photos!");} // only report once
} // end of loop() // ************************************
// **************************************************

#ifdef RED_LED
 void LED_flash(int count) {
   count*=2;
   boolean state=LOW; // LOW is ON
   while (count--) {digitalWrite(RED_LED,state); state=!state; delay(1000);}
 }
#endif

// **************************************************

#if ((TFT_ROTATION&B1)==1)
 #define tft_text_row 0
#else
 #define tft_text_row 260 // 240x320
#endif

void tft_top() {tft.setCursor(0,0);}
void tft_text() {tft.setCursor(0,tft_text_row);}

void tft_clear() {
 tft.fillScreen(ILI9341_BLACK);
 tft.setCursor(0,0); // top
 tft.setTextColor(ILI9341_WHITE);
 tft.setTextSize(2); // 1 to 5 // was 4
}

boolean tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
 if (y>=tft.height()) return false;
 tft.drawRGBBitmap(x, y, bitmap, w, h);
 return true;
}

// **************************************************

char* sd_build_file_name(int photo) {
 if (photo>999) photo=999;
 static char text[]="/photoXXX.jpg";
 snprintf(text+6,3+1,"%d",photo);
 char *p1=text+strlen(text);
 strcpy(p1,".jpg");
 return text;
}

void sd_wipe_all() {
 tft_clear();
 Serial.println("Deleting photos:");
 boolean found=false;
 fs::FS &fs = SD;
 for (int x=(photo_count_max-1);x>=0;x--) {
   char *path=sd_build_file_name(x);
   boolean success=fs.remove(path);
   if (success) found=true;
   if (found) {
     Serial.print(path+1); Serial.print("->");
     if (success) Serial.println("Deleted"); else Serial.println("Failed");
 } }
 if (!found) Serial.println("No photos to delete!");
 delay(5000);
}

int sd_load_and_display(int photo_next) { // returns NEXT_EMPTY_photo_count (ie. 0 if no photos)
 #ifdef BLOCK_SERIAL
   if ((photo_next<0) or (photo_next>=photo_count_max)) photo_next=0;
   int first_blank=photo_count_max;
   for (int y=0;y<photo_count_max;y++) {
     char *path=sd_build_file_name(photo_next);
     uint16_t w=0, h=0;
     TJpgDec.getSdJpgSize(&w,&h,path);
     tft_clear();
     if (w) {
       TJpgDec.drawSdJpg(0,0,path);
       #if (tft_text_row==0)
         delay(5000); // bodge!
       #endif
       tft_text();
       Serial.println(path+1);
       if (!camera_ok) Serial.println("(camera failed)");
       boolean quit=false;
       for (uint32_t x=0;x<20;x++) {if (!digitalRead(SWITCH_PIN)) {quit=true; break;} delay(500);} // show for upto 10secs
       if (quit) break;
     } else if (first_blank==photo_count_max) first_blank=photo_next;
     if (photo_next>=(photo_count_max-1)) photo_next=0; else photo_next++; // next photo
   }
   return first_blank; // NEXT_EMPTY photo_count (0 if empty, photo_count_max if full) 
 #else
   Serial.println("Can't use SD-Card as uses GPIO-1");
   return 0;
 #endif
}

// **************************************************

boolean camera_init() {
 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;
 Serial.print("PSRAM: ");
 if (psramFound()){ // larger bufer
   Serial.println("OK");
   config.frame_size = FRAMESIZE_QVGA;
   config.jpeg_quality = 10;
   config.fb_count = 1;
 } else { // smaller buffer
   Serial.println("FAILED");
   config.frame_size = FRAMESIZE_QQVGA;
   config.jpeg_quality = 12;
   config.fb_count = 1;
 }
 esp_err_t camera_status;
 for (int x=0;x<2;x++) { // try twice as sometimes get error 0x20004
   delay(2000);
   camera_status = esp_camera_init(&config);
   Serial.print("Camera init: ");
   if (camera_status!=ESP_OK) {Serial.print("failed with error 0x"); Serial.println(camera_status,HEX);}
   else {Serial.println("OK"); break;}
 }
 if (camera_status!=ESP_OK) return false; // exit
 sensor_t *sensor = esp_camera_sensor_get();
 if (sensor->id.PID == OV3660_PID) Serial.println("*** WARNING: Old-PID Sensor Detected - need to adjust settings ***");
 return true;
}

int take_picture(int photo_next) {
 int photo_try=photo_next;
 if ((photo_try<0) or (photo_try>=photo_count_max)) photo_try=0;
 boolean success=false;
 Serial.print("Taking photo...");
 #ifndef BLOCK_SERIAL
   tft_clear();
   tft.println("Taking photo...");
 #endif
 delay(1000);
 camera_fb_t * fb=nullptr;
 fb = esp_camera_fb_get();
 if (!fb) Serial.println("Fail?\nTry rebooting!");
 else {
   Serial.println();
   char *path=sd_build_file_name(photo_try);
   #ifdef BLOCK_SERIAL
     fs::FS &fs = SD;
     File file = fs.open(path, FILE_WRITE); // will overwrite any existing file!!
     if (file) {
       file.write(fb->buf, fb->len);
       photo_try++; photo_next=photo_try; // can give "photo_count_max" and return this to calling sub!
       success=true;
     } 
     file.close(); 
   #else
     success=true;
   #endif
   uint16_t w=0, h=0;
   TJpgDec.getJpgSize(&w, &h, fb->buf, fb->len);
   TJpgDec.drawJpg(0, 0, fb->buf, fb->len); // Display photo
   #if (tft_text_row==0)
     delay(5000); // bodge
   #endif
   if (success) tft_text();
   #ifdef BLOCK_SERIAL // ensure following photo is deleted (so on re-start we know where to start)!
    Serial.print(path+1);
    Serial.print("->");
    if (success) {Serial.println("Saved"); char *path=sd_build_file_name(photo_next); fs.remove(path);} else Serial.println("Fail");
   #else
     tft.print(path+1);
     tft.println("->No Save");
   #endif
 }
 delay(5000);
 return photo_next;
}
