/**
 * Example for the ESP32 HTTP(S) Webserver
 *
 * IMPORTANT NOTE:
 * This example is a bit more complex than the other ones, so be careful to 
 * follow all steps.
 * 
 * Make sure to check out the more basic examples like Static-Page to understand
 * the fundamental principles of the API before proceeding with this sketch.
 * 
 * To run this script, you need to
 *  1) Enter your WiFi SSID and PSK below this comment
 *  2) Install the SPIFFS File uploader into your Arduino IDE to be able to
 *     upload static data to the webserver.
 *     Follow the instructions at:
 *     https://github.com/me-no-dev/arduino-esp32fs-plugin
 *  3) Upload the static files from the data/ directory of the example to your
 *     module's SPIFFs by using "ESP32 Sketch Data Upload" from the tools menu.
 *     If you face any problems, read the description of the libraray mentioned
 *     above.
 *     Note: If mounting SPIFFS fails, the script will wait for a serial connection
 *     (open your serial monitor!) and ask if it should format the SPIFFS partition.
 *     You may need this before uploading the data
 *     Note: Make sure to select a partition layout that allows for SPIFFS in the
 *     boards menu
 *  4) Have the ArduinoJSON library installed and available. (Tested with Version 5.13.4)
 *     You'll find it at:
 *     https://arduinojson.org/
 *
 * This script will install an HTTPS Server on your ESP32 with the following
 * functionalities:
 *  - Serve static files from the SPIFFS's data/public directory
 *  - Provide a REST API at /api to receive the asynchronous http requests
 *    - /api/uptime provides access to the current system uptime
 *    - /api/events allows to register or delete events to turn PINs on/off
 *      at certain times.
 *  - Use Arduino JSON for body parsing and generation of responses.
 *  - The certificate is generated on first run and stored to the SPIFFS in
 *    the cert directory (so that the client cannot retrieve the private key)
 */

// TODO: Configure your WiFi here
#define WIFI_SSID "<your ssid goes here>"
#define WIFI_PSK  "<your pre-shared key goes here>"

// We will use wifi
#include <WiFi.h>

// We will use SPIFFS and FS
#include <SPIFFS.h>
#include <FS.h>

// We use JSON as data format. Make sure to have the lib available
#include <ArduinoJson.h>

// Working with c++ strings
#include <string>

// Define the name of the directory for public files in the SPIFFS parition
#define DIR_PUBLIC "/public"

// We need to specify some content-type mapping, so the resources get delivered with the
// right content type and are displayed correctly in the browser
char contentTypes[][2][32] = {
  {".html", "text/html"},
  {".css",  "text/css"},
  {".js",   "application/javascript"},
  {".json", "application/json"},
  {".png",  "image/png"},
  {".jpg",  "image/jpg"},
  {"", ""}
};

// Includes for the server
#include <HTTPSServer.hpp>
#include <SSLCert.hpp>
#include <HTTPRequest.hpp>
#include <HTTPResponse.hpp>
#include <util.hpp>

// We use the following struct to store GPIO events:
#define MAX_EVENTS 20
struct {
  // is this event used (events that have been run will be set to false)
  bool active;
  // when should it be run?
  unsigned long time;
  // which GPIO should be changed?
  int gpio;
  // and to which state?
  int state;
} events[MAX_EVENTS];

// The HTTPS Server comes in a separate namespace. For easier use, include it here.
using namespace httpsserver;

// We just create a reference to the server here. We cannot call the constructor unless
// we have initialized the SPIFFS and read or created the certificate
HTTPSServer * secureServer;

void setup() {
  // For logging
  Serial.begin(115200);

  // Set the pins that we will use as output pins
  pinMode(25, OUTPUT);
  pinMode(26, OUTPUT);
  pinMode(27, OUTPUT);
  pinMode(32, OUTPUT);
  pinMode(33, OUTPUT);

  // Try to mount SPIFFS without formatting on failure
  if (!SPIFFS.begin(false)) {
    // If SPIFFS does not work, we wait for serial connection...
    while(!Serial);
    delay(1000);

    // Ask to format SPIFFS using serial interface
    Serial.print("Mounting SPIFFS failed. Try formatting? (y/n): ");
    while(!Serial.available());
    Serial.println();

    // If the user did not accept to try formatting SPIFFS or formatting failed:
    if (Serial.read() != 'y' || !SPIFFS.begin(true)) {
      Serial.println("SPIFFS not available. Stop.");
      while(true);
    }
    Serial.println("SPIFFS has been formated.");
  }
  Serial.println("SPIFFS has been mounted.");

  // Now that SPIFFS is ready, we can create or load the certificate
  SSLCert *cert = getCertificate();
  if (cert == NULL) {
    Serial.println("Could not load certificate. Stop.");
    while(true);
  }

  // Initialize event structure:
  for(int i = 0; i < MAX_EVENTS; i++) {
    events[i].active = false;
    events[i].gpio = 0;
    events[i].state = LOW;
    events[i].time = 0;
  }

  // Connect to WiFi
  Serial.println("Setting up WiFi");
  WiFi.begin(WIFI_SSID, WIFI_PSK);
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.print("Connected. IP=");
  Serial.println(WiFi.localIP());

  // Create the server with the certificate we loaded before
  secureServer = new HTTPSServer(cert);

  // We register the SPIFFS handler as the default node, so every request that does
  // not hit any other node will be redirected to the file system.
  ResourceNode * spiffsNode = new ResourceNode("", "", &handleSPIFFS);
  secureServer->setDefaultNode(spiffsNode);

  // Add a handler that serves the current system uptime at GET /api/uptime
  ResourceNode * uptimeNode = new ResourceNode("/api/uptime", "GET", &handleGetUptime);
  secureServer->registerNode(uptimeNode);

  // Add the handler nodes that deal with modifying the events:
  ResourceNode * getEventsNode = new ResourceNode("/api/events", "GET", &handleGetEvents);
  secureServer->registerNode(getEventsNode);
  ResourceNode * postEventNode = new ResourceNode("/api/events", "POST", &handlePostEvent);
  secureServer->registerNode(postEventNode);
  ResourceNode * deleteEventNode = new ResourceNode("/api/events/*", "DELETE", &handleDeleteEvent);
  secureServer->registerNode(deleteEventNode);

  Serial.println("Starting server...");
  secureServer->start();
  if (secureServer->isRunning()) {
    Serial.println("Server ready.");
  }
}

void loop() {
  // This call will let the server do its work
  secureServer->loop();

  // Here we handle the events
  unsigned long now = millis() / 1000;
  for (int i = 0; i < MAX_EVENTS; i++) {
    // Only handle active events:
    if (events[i].active) {
      // Only if the counter has recently been exceeded
      if (events[i].time < now) {
      // Apply the state change
      digitalWrite(events[i].gpio, events[i].state);

      // Deactivate the event so it doesn't fire again
      events[i].active = false;
      }
    }
  }

  // Other code would go here...
  delay(1);
}

/**
 * This function will either read the certificate and private key from SPIFFS or
 * create a self-signed certificate and write it to SPIFFS for next boot
 */
SSLCert * getCertificate() {
  // Try to open key and cert file to see if they exist
  File keyFile = SPIFFS.open("/key.der");
  File certFile = SPIFFS.open("/cert.der");

  // If now, create them 
  if (!keyFile || !certFile || keyFile.size()==0 || certFile.size()==0) {
    Serial.println("No certificate found in SPIFFS, generating a new one for you.");
    Serial.println("If you face a Guru Meditation, give the script another try (or two...).");
    Serial.println("This may take up to a minute, so please stand by :)");

    SSLCert * newCert = new SSLCert();
    // The part after the CN= is the domain that this certificate will match, in this
    // case, it's esp32.local.
    // However, as the certificate is self-signed, your browser won't trust the server
    // anyway.
    int res = createSelfSignedCert(*newCert, KEYSIZE_1024, "CN=esp32.local,O=acme,C=DE");
    if (res == 0) {
      // We now have a certificate. We store it on the SPIFFS to restore it on next boot.

      bool failure = false;
      // Private key
      keyFile = SPIFFS.open("/key.der", FILE_WRITE);
      if (!keyFile || !keyFile.write(newCert->getPKData(), newCert->getPKLength())) {
        Serial.println("Could not write /key.der");
        failure = true;
      }
      if (keyFile) keyFile.close();

      // Certificate
      certFile = SPIFFS.open("/cert.der", FILE_WRITE);
      if (!certFile || !certFile.write(newCert->getCertData(), newCert->getCertLength())) {
        Serial.println("Could not write /cert.der");
        failure = true;
      }
      if (certFile) certFile.close();

      if (failure) {
        Serial.println("Certificate could not be stored permanently, generating new certificate on reboot...");
      }

      return newCert;

    } else {
      // Certificate generation failed. Inform the user.
      Serial.println("An error occured during certificate generation.");
      Serial.print("Error code is 0x");
      Serial.println(res, HEX);
      Serial.println("You may have a look at SSLCert.h to find the reason for this error.");
      return NULL;
    }

	} else {
    Serial.println("Reading certificate from SPIFFS.");

    // The files exist, so we can create a certificate based on them
    size_t keySize = keyFile.size();
    size_t certSize = certFile.size();

    uint8_t * keyBuffer = new uint8_t[keySize];
    if (keyBuffer == NULL) {
      Serial.println("Not enough memory to load privat key");
      return NULL;
    }
    uint8_t * certBuffer = new uint8_t[certSize];
    if (certBuffer == NULL) {
      delete[] keyBuffer;
      Serial.println("Not enough memory to load certificate");
      return NULL;
    }
    keyFile.read(keyBuffer, keySize);
    certFile.read(certBuffer, certSize);

    // Close the files
    keyFile.close();
    certFile.close();
    Serial.printf("Read %u bytes of certificate and %u bytes of key from SPIFFS\n", certSize, keySize);
    return new SSLCert(certBuffer, certSize, keyBuffer, keySize);
  }
}

/**
 * This handler function will try to load the requested resource from SPIFFS's /public folder.
 * 
 * If the method is not GET, it will throw 405, if the file is not found, it will throw 404.
 */
void handleSPIFFS(HTTPRequest * req, HTTPResponse * res) {
	
  // We only handle GET here
  if (req->getMethod() == "GET") {
    // Redirect / to /index.html
    std::string reqFile = req->getRequestString()=="/" ? "/index.html" : req->getRequestString();

    // Try to open the file
    std::string filename = std::string(DIR_PUBLIC) + reqFile;

    // Check if the file exists
    if (!SPIFFS.exists(filename.c_str())) {
      // Send "404 Not Found" as response, as the file doesn't seem to exist
      res->setStatusCode(404);
      res->setStatusText("Not found");
      res->println("404 Not Found");
      return;
    }

    File file = SPIFFS.open(filename.c_str());

    // Set length
    res->setHeader("Content-Length", httpsserver::intToString(file.size()));

    // Content-Type is guessed using the definition of the contentTypes-table defined above
    int cTypeIdx = 0;
    do {
      if(reqFile.rfind(contentTypes[cTypeIdx][0])!=std::string::npos) {
        res->setHeader("Content-Type", contentTypes[cTypeIdx][1]);
        break;
      }
      cTypeIdx+=1;
    } while(strlen(contentTypes[cTypeIdx][0])>0);

    // Read the file and write it to the response
    uint8_t buffer[256];
    size_t length = 0;
    do {
      length = file.read(buffer, 256);
      res->write(buffer, length);
    } while (length > 0);

    file.close();
  } else {
    // If there's any body, discard it
    req->discardRequestBody();
    // Send "405 Method not allowed" as response
    res->setStatusCode(405);
    res->setStatusText("Method not allowed");
    res->println("405 Method not allowed");
  }
}

/**
 * This function will return the uptime in seconds as JSON object:
 * {"uptime": 42}
 */
void handleGetUptime(HTTPRequest * req, HTTPResponse * res) {
  // Create a buffer of size 1 (pretty simple, we have just one key here)
  StaticJsonBuffer<JSON_OBJECT_SIZE(1)> jsonBuffer;
  // Create an object at the root
  JsonObject& obj = jsonBuffer.createObject();
  // Set the uptime key to the uptime in seconds
  obj["uptime"] = millis()/1000;
  // Set the content type of the response
  res->setHeader("Content-Type", "application/json");
  // As HTTPResponse implements the Print interface, this works fine. Just remember
  // to use *, as we only have a pointer to the HTTPResponse here:
  obj.printTo(*res);
}

/**
 * This handler will return a JSON array of currently active events for GET /api/events
 */
void handleGetEvents(HTTPRequest * req, HTTPResponse * res) {
  // We need to calculate the capacity of the json buffer
  int activeEvents = 0;
  for(int i = 0; i < MAX_EVENTS; i++) {
    if (events[i].active) activeEvents++;
  }

  // For each active event, we need 1 array element with 4 objects
  const size_t capacity = JSON_ARRAY_SIZE(activeEvents) + activeEvents * JSON_OBJECT_SIZE(4);

  // DynamicJsonBuffer is created on the heap instead of the stack
  DynamicJsonBuffer jsonBuffer(capacity);
  JsonArray& arr = jsonBuffer.createArray();
  for(int i = 0; i < MAX_EVENTS; i++) {
    if (events[i].active) {
      JsonObject& eventObj = arr.createNestedObject();
      eventObj["gpio"] = events[i].gpio;
      eventObj["state"] = events[i].state;
      eventObj["time"] = events[i].time;
      // Add the index to allow delete and post to identify the element
      eventObj["id"] = i;
    }
  }

  // Print to response
  res->setHeader("Content-Type", "application/json");
  arr.printTo(*res);
}

void handlePostEvent(HTTPRequest * req, HTTPResponse * res) {
  // We expect an object with 4 elements and add some buffer
  const size_t capacity = JSON_OBJECT_SIZE(4) + 180;
  DynamicJsonBuffer jsonBuffer(capacity);

  // Create buffer to read request
  char * buffer = new char[capacity + 1];
  memset(buffer, 0, capacity+1);

  // Try to read request into buffer
  size_t idx = 0;
  // while "not everything read" or "buffer is full"
  while (!req->requestComplete() && idx < capacity) {
    idx += req->readChars(buffer + idx, capacity-idx);
  }

  // If the request is still not read completely, we cannot process it.
  if (!req->requestComplete()) {
    res->setStatusCode(413);
    res->setStatusText("Request entity too large");
    res->println("413 Request entity too large");
    // Clean up
    delete[] buffer;
    return;
  }

  // Parse the object
  JsonObject& reqObj = jsonBuffer.parseObject(buffer);

  // Check input data types
  bool dataValid = true;
  if (!reqObj.is<long>("time") || !reqObj.is<int>("gpio") || !reqObj.is<int>("state")) {
    dataValid = false;
  }
	
  // Check actual values
  unsigned long eTime = 0;
  int eGpio = 0;
  int eState = LOW;
  if (dataValid) {
    eTime = reqObj["time"];
    if (eTime < millis()/1000) dataValid = false;

    eGpio = reqObj["gpio"];
    if (!(eGpio == 25 || eGpio == 26 || eGpio == 27 || eGpio == 32 || eGpio == 33)) dataValid = false;

    eState = reqObj["state"];
    if (eState != HIGH && eState != LOW) dataValid = false;
  }

  // Clean up, we don't need the buffer any longer
  delete[] buffer;

  // If something failed: 400
  if (!dataValid) {
    res->setStatusCode(400);
    res->setStatusText("Bad Request");
    res->println("400 Bad Request");
    return;
  }

  // Try to find an inactive event in the list to write the data to
  int eventID = -1;
  for(int i = 0; i < MAX_EVENTS && eventID==-1; i++) {
    if (!events[i].active) {
      eventID = i;
      events[i].gpio = eGpio;
      events[i].time = eTime;
      events[i].state = eState;
      events[i].active = true;
    }
  }

  // Check if we could store the event
  if (eventID>-1) {
    // Create a buffer for the response
    StaticJsonBuffer<JSON_OBJECT_SIZE(4)> resBuffer;

    // Create an object at the root
    JsonObject& resObj = resBuffer.createObject();

    // Set the uptime key to the uptime in seconds
    resObj["gpio"] = events[eventID].gpio;
    resObj["state"] = events[eventID].state;
    resObj["time"] = events[eventID].time;
    resObj["id"] = eventID;

    // Write the response
    res->setHeader("Content-Type", "application/json");
    resObj.printTo(*res);

  } else {
    // We could not store the event, no free slot.
    res->setStatusCode(507);
    res->setStatusText("Insufficient storage");
    res->println("507 Insufficient storage");

  }
}

/**
 * This handler will delete an event (meaning: deactive the event)
 */
void handleDeleteEvent(HTTPRequest * req, HTTPResponse * res) {
  // Access the parameter from the URL. See Parameters example for more details on this
  ResourceParameters * params = req->getParams();
  uint16_t eid = params->getUrlParameterInt(0);

  if (eid < MAX_EVENTS) {
    // Set the inactive flag
    events[eid].active = false;
    // And return a successful response without body
    res->setStatusCode(204);
    res->setStatusText("No Content");
  } else {
    // Send error message
    res->setStatusCode(400);
    res->setStatusText("Bad Request");
    res->println("400 Bad Request");
  }
}
