#include <WiFi.h>
#include <WiFiMulti.h>
#include <WebServer.h>
#include <FS.h>
#include <SD_MMC.h> 

WiFiMulti WiFiMulti;
WebServer server(80); //Starting a server on port 80
//Home Dynamic IoT Communication Protocol Headers and Information
const String uuid = "d9ed122a-b359-4564-8bf1-6cd27ec517b2"; //Please change this UUIDv4 if you have more than one identical setup in the same subnet
const String driver="3dprcu.ext.hd.imuslab"; 
const String devName="3D Printer Remote Control Unit";
//WebServer settings
const String webroot = "/web";

//System Settings
bool forceCORS = true; //Some versions of WebServer.h do not include Access Control Allow Origin * by default. Set this to true to send it out manually
void setup(){
    Serial.begin(115200);
    delay(10);
    Serial.println("");
    Serial.println("[info] ESP32 3D Printer Remote Controller Unit");
    Serial.println("[info] Open source prototype for the aCloud 3D Printer Control System.");
    // Add more WiFi AP for backups 
    WiFiMulti.addAP("Toby Room Automation", "homedynamicsystem");
    //WiFiMulti.addAP("Backup-SSID", "password");
    if (initSDcard() == false){
      //Init SD card failed. Terminate the startup process.
      return;
    }
    
    Serial.print("[info] Waiting for WiFi... ");

    while(WiFiMulti.run() != WL_CONNECTED) {
        Serial.print(".");
        delay(500);
    }

    Serial.println("");
    Serial.println("[OK] WiFi connected");
    Serial.print("[info] IP address: ");
    Serial.println(WiFi.localIP());
    delay(500);
    //Define handler for index
    server.on("/", handle_OnConnect);
    //Define Web Server handler for Home Dynamic Protocol
    server.on("/uuid", handle_uuid);
    server.on("/info", handle_info);
    server.on("/status", handle_status);
    //Define handler for file operations
    server.on("/list", HTTP_GET, printDirectory); //Usage: /list?dir=/{dirname}
    server.on("/upload", HTTP_POST, []() {
        sendCORS();
        server.send(200, "text/plain", "");
    }, handleFileUpload);
    //Define handler for NOT FOUND exception
    server.onNotFound(handle_NotFound);
    server.begin();
    Serial.println("[info] RESTFUL API server started");
    Serial.println("[OK] System Ready!");
}

void loop()
{
 server.handleClient();
}

//Home Dynamic Communication Protocol related response handler
void handle_uuid(){
  if (forceCORS){
    server.sendHeader("Access-Control-Allow-Origin", "*");
  }
  server.send(200, "text/plain", uuid);
}

void handle_info(){
  if (forceCORS){
    server.sendHeader("Access-Control-Allow-Origin", "*");
  }
  server.send(200, "text/plain", devName + "_" + driver);
}

void handle_status(){
  //Report printing progress here. Work in progress
}

void handle_OnConnect(){
  server.sendHeader("Location", "/web/index.html",true);
  server.send(302, "text/plane",""); 
}

File uploadFile;
void handleFileUpload() {
  if (server.uri() != "/upload") {
    return;
  }
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    if (SD_MMC.exists((char *)upload.filename.c_str())) {
      SD_MMC.remove((char *)upload.filename.c_str());
    }
    uploadFile = SD_MMC.open(upload.filename.c_str(), FILE_WRITE);
    Serial.print("[info] Upload: START, filename: "); Serial.println(upload.filename);
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (uploadFile) {
      uploadFile.write(upload.buf, upload.currentSize);
    }
    Serial.print("[info] Upload: WRITE, Bytes: "); Serial.println(upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    if (uploadFile) {
      uploadFile.close();
    }
    Serial.print("[info] Upload: END, Size: "); Serial.println(upload.totalSize);
  }
}

void handleHTMLDelivery(String path){
  if (!loadFromSdCard(path)){
    //This file do not exists or it is empty. Deliver 404 page instead.
    sendCORS();
    server.send(404, "text/plain", "Page not found");
  }
}

void handle_NotFound(){
  String path = server.uri();
  int index = path.indexOf("/web");
  int deleteIndicator = path.indexOf("/delete");
  if (index >= 0 && deleteIndicator == -1) {
    handleHTMLDelivery(path);
  }else if (deleteIndicator >= 0 ){
    handleDeleteRequest(path);
  }else{
    sendCORS();
    server.send(404, "text/plain", "Page not found");
  }
}

bool initSDcard(){
    Serial.println("[info] Trying to initiate SD card file system.");

   if(!SD_MMC.begin()){
        Serial.println("[error] Card Mount Failed");
        return false;
    }
    uint8_t cardType = SD_MMC.cardType();

    if(cardType == CARD_NONE){
        Serial.println("[error] No SD_MMC card attached");
        return false;
    }

    Serial.print("[info] SD_MMC Card Type: ");
    if(cardType == CARD_MMC){
        Serial.println("MMC");
    } else if(cardType == CARD_SD){
        Serial.println("SDSC");
    } else if(cardType == CARD_SDHC){
        Serial.println("SDHC");
    } else {
        Serial.println("UNKNOWN");
    }

    uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
    Serial.printf("[OK] SD_MMC Card Size: %lluMB\n", cardSize);
    return true;
}

void printDirectory() {
  if (!server.hasArg("dir")) {
    server.send(500, "text/plain", "Not directory.");
    return;
  }
  String path = server.arg("dir");
  if (path != "/" && !SD_MMC.exists((char *)path.c_str())) {
    server.send(500, "text/plain", "Bad path");
    return;
  }
  File dir = SD_MMC.open((char *)path.c_str());
  path = String();
  if (!dir.isDirectory()) {
    dir.close();
    server.send(500, "text/plain", "Not directory");
    return;
  }
  dir.rewindDirectory();
  if (forceCORS){
    server.sendHeader("Access-Control-Allow-Origin", "*");
  }
  server.setContentLength(CONTENT_LENGTH_UNKNOWN);
  server.send(200, "application/json", "");
  WiFiClient client = server.client();

  server.sendContent("[");
  for (int cnt = 0; true; ++cnt) {
    File entry = dir.openNextFile();
    if (!entry) {
      break;
    }

    String output;
    if (cnt > 0) {
      output = ',';
    }

    output += "{\"type\":\"";
    output += (entry.isDirectory()) ? "dir" : "file";
    output += "\",\"name\":\"";
    output += entry.name();
    output += "\"";
    output += "}";
    server.sendContent(output);
    entry.close();
  }
  server.sendContent("]");
  dir.close();
}

//Test read and write speed using test.txt file
void testFileIO(fs::FS &fs, const char * path){
    File file = fs.open(path);
    static uint8_t buf[512];
    size_t len = 0;
    uint32_t start = millis();
    uint32_t end = start;
    if(file){
        len = file.size();
        size_t flen = len;
        start = millis();
        while(len){
            size_t toRead = len;
            if(toRead > 512){
                toRead = 512;
            }
            file.read(buf, toRead);
            len -= toRead;
        }
        end = millis() - start;
        Serial.printf("%u bytes read for %u ms\n", flen, end);
        file.close();
    } else {
        Serial.println("Failed to open file for reading");
    }


    file = fs.open(path, FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }

    size_t i;
    start = millis();
    for(i=0; i<2048; i++){
        file.write(buf, 512);
    }
    end = millis() - start;
    Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
    file.close();
}

bool loadFromSdCard(String path){
  String dataType = "text/plain";
  if(path.endsWith("/")) path += "index.htm";

  if(path.endsWith(".src")) path = path.substring(0, path.lastIndexOf("."));
  else if(path.endsWith(".htm")) dataType = "text/html";
  else if(path.endsWith(".html")) dataType = "text/html";
  else if(path.endsWith(".css")) dataType = "text/css";
  else if(path.endsWith(".js")) dataType = "application/javascript";
  else if(path.endsWith(".png")) dataType = "image/png";
  else if(path.endsWith(".gif")) dataType = "image/gif";
  else if(path.endsWith(".jpg")) dataType = "image/jpeg";
  else if(path.endsWith(".ico")) dataType = "image/x-icon";
  else if(path.endsWith(".svg")) dataType = "image/svg+xml";
  else if(path.endsWith(".xml")) dataType = "text/xml";
  else if(path.endsWith(".pdf")) dataType = "application/pdf";
  else if(path.endsWith(".zip")) dataType = "application/zip";
  else if(path.endsWith(".mp3")) dataType = "audio/mpeg";
  else if(path.endsWith(".mp4")) dataType = "video/mp4";
  else if(path.endsWith(".otf")) dataType = "font/otf";
  else if(path.endsWith(".ttf")) dataType = "font/ttf";

  File dataFile = SD_MMC.open(path.c_str());
  if(dataFile.isDirectory()){
    path += "/index.html";
    dataType = "text/html";
    dataFile = SD_MMC.open(path.c_str());
  }

  if (!dataFile)
    return false;

  if (server.hasArg("download")) dataType = "application/octet-stream";

  if (server.streamFile(dataFile, dataType) != dataFile.size()) {
    Serial.println("[error] Sent less data than expected!");
  }

  dataFile.close();
  return true;
}

void deleteRecursive(String path) {
  File file = SD_MMC.open((char *)path.c_str());
  if (!file.isDirectory()) {
    file.close();
    SD_MMC.remove((char *)path.c_str());
    return;
  }

  file.rewindDirectory();
  while (true) {
    File entry = file.openNextFile();
    if (!entry) {
      break;
    }
    String entryPath = path + "/" + entry.name();
    if (entry.isDirectory()) {
      entry.close();
      deleteRecursive(entryPath);
    } else {
      entry.close();
      SD_MMC.remove((char *)entryPath.c_str());
    }
    yield();
  }

  SD_MMC.rmdir((char *)path.c_str());
  file.close();
}

void handleDeleteRequest(String fullPath) {

  int index = fullPath.indexOf(",");
  String path = fullPath.substring(index + 1, fullPath.length());
  Serial.println("[info] Request delete path: " + path);
  if (path == "/" || !SD_MMC.exists((char *)path.c_str())) {
    sendCORS();
    server.send(500, "text/plain", "Bad path");
    return;
  }
  deleteRecursive(path);
  sendCORS();
  server.send(200, "text/plain", "DONE");
}


void sendCORS(){
  if (forceCORS){
    server.sendHeader("Access-Control-Allow-Origin", "*");
    server.sendHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
    server.sendHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  }
}
