#include <WiFi.h>
	#include <WiFiClient.h>
	#include <ArduinoOTA.h>
	#include <ESP32WebServer.h>
	#include <ESPmDNS.h>
	#include <SPIFFS.h>
	#include <FS.h>
	#include <WebSocketsServer.h>
	#include <Servo.h>
	 
	ESP32WebServer server(80);    // create a web server on port 80
	WebSocketsServer webSocket(81);  // create a websocket server on port 81
	 
	File fsUploadFile;                  // a File variable to temporarily store the received file
	 
	const char* path_root  = "/index.html";
	#define BUFFER_SIZE 16384
	uint8_t buf[BUFFER_SIZE];
	 
	const char *ssid = "SSID"; // Wifi_STA network
	const char *password = "PASSPHRASE"; // Wifi_STA passphrase
	 
	const char common_name[40] = "esp32pantilt";
	const char *OTAName = common_name;      // A name and a password for the OTA service
	const char *mdnsName = common_name; // Domain name for the mDNS responder
	 
	#define INTERVAL_VS_BASE 500
	 
	const int x_axis = 12;
	const int y_axis = 13;
	 
	Servo x_axisServo;
	Servo y_axisServo;
	 
	const int base = 0;
	int x = 0;
	int y = 0;
	 
	/*__________________________________________________________SETUP__________________________________________________________*/
	 
	void setup() {
	 delay(1000);
	 Serial.begin(115200);
	 delay(10);
	 Serial.println("\r\n");
	 
	 pinMode(x_axis, OUTPUT);
	 pinMode(y_axis, OUTPUT);
	 
	 x_axisServo.attach(x_axis);
	 y_axisServo.attach(y_axis);
	 
	 startWiFi();
	 startOTA();
	 startSPIFFS();
	 startWebSocket();
	 startMDNS();
	 startServer();
	}
	 
	/*__________________________________________________________LOOP__________________________________________________________*/
	 
	void loop() {
	 webSocket.loop();              // constantly check for websocket events
	 server.handleClient();           // run the server
	 ArduinoOTA.handle();            // listen for OTA events
	}
	 
	/*__________________________________________________________SETUP_FUNCTIONS__________________________________________________________*/
	 
	void startWiFi() { // Start a Wi-Fi access point, and try to connect to some given access points. Then wait for either an AP or STA connection
	 // WiFi.softAP(ssid, password);       // Start the access point
	 WiFi.mode(WIFI_STA);       // Start the access point
	 WiFi.begin(ssid, password);       // Start the access point
	 while ( WiFi.status() != WL_CONNECTED) {
	  delay(500);
	  Serial.print(".");
	 }
	 Serial.println("");
	 Serial.print("SSID \"");
	 Serial.print(ssid);
	 Serial.println("\" started\r\n");
	 
	 Serial.print("Connected to ");
	 Serial.println(ssid);
	 
	 Serial.print("IP address: ");
	 Serial.println(WiFi.localIP());
	 
	 Serial.print("hostname : ");
	 // Serial.println(WiFi.hostname());
	 Serial.println("");
	}
	 
	void startOTA() { // Start the OTA service
	 ArduinoOTA.setHostname(OTAName);
	 // ArduinoOTA.setPassword(OTAPassword);
	 
	 ArduinoOTA.onStart([]() {
	  Serial.println("Start");
	  // turn off the LEDs
	  for (int i = 0; i < 6; i++) {
	   //   digitalWrite(LED_BUILTIN, HIGH);
	   //   digitalWrite(LED_BUILTIN, LOW);
	  }
	 });
	 ArduinoOTA.onEnd([]() {
	  Serial.println("\r\nEnd");
	 });
	 ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
	  Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
	 });
	 ArduinoOTA.onError([](ota_error_t error) {
	  Serial.printf("Error[%u]: ", error);
	  if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
	  else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
	  else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
	  else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
	  else if (error == OTA_END_ERROR) Serial.println("End Failed");
	 });
	 ArduinoOTA.begin();
	 Serial.println("OTA ready\r\n");
	}
	 
	//
	// https://github.com/zhouhan0126/WebServer-esp32/blob/master/examples/FSBrowser/FSBrowser.ino
	//
	void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
	 // Serial.printf("Listing directory: %s\n", dirname);
	 
	 File root = fs.open(dirname);
	 if (!root) {
	  Serial.println("Failed to open directory");
	  return;
	 }
	 if (!root.isDirectory()) {
	  Serial.println("Not a directory");
	  return;
	 }
	 
	 File file = root.openNextFile();
	 while (file) {
	  if (file.isDirectory()) {
	   Serial.print(" DIR : ");
	   Serial.println(file.name());
	   if (levels) {
	    listDir(fs, file.name(), levels - 1);
	   }
	  } else {
	   Serial.print(" FILE: ");
	   Serial.print(file.name());
	   Serial.print(" SIZE: ");
	   Serial.println(file.size());
	  }
	  file = root.openNextFile();
	 }
	}
	 
	void startSPIFFS() { // Start the SPIFFS and list all contents
	 SPIFFS.begin();               // Start the SPI Flash File System (SPIFFS)
	 Serial.println("SPIFFS started. Contents:");
	 {
	  listDir(SPIFFS, "/", 0);
	 }
	 Serial.printf("\n");
	/*
	  Serial.println("SPIFFS started. Contents:");
	  {
	   Dir dir = SPIFFS.openDir("/");
	   while (dir.next()) {           // List the file system contents
	    String fileName = dir.fileName();
	    size_t fileSize = dir.fileSize();
	    Serial.printf("\tFS File: %s, size: %s\r\n", fileName.c_str(), formatBytes(fileSize).c_str());
	   }
	   Serial.printf("\n");
	  }
	*/
	}
	 
	void startWebSocket() { // Start a WebSocket server
	 webSocket.begin();             // start the websocket server
	 webSocket.onEvent(webSocketEvent);     // if there's an incomming websocket message, go to function 'webSocketEvent'
	 Serial.println("WebSocket server started.");
	}
	 
	void startMDNS() { // Start the mDNS responder
	 MDNS.begin(mdnsName);            // start the multicast domain name server
	 Serial.print("mDNS responder started: http://");
	 Serial.print(mdnsName);
	 Serial.println(".local");
	}
	 
	void startServer() { // Start a HTTP server with a file read handler and an upload handler
	 server.on("/", handleRoot);
	 server.on("/edit.html", HTTP_POST, []() { // If a POST request is sent to the /edit.html address,
	  server.send(200, "text/plain", "");
	 }, handleFileUpload);            // go to 'handleFileUpload'
	 server.onNotFound(handleNotFound);     // if someone requests any other file or page, go to function 'handleNotFound'
	 // and check if the file exists
	 
	 server.begin();               // start the HTTP server
	 Serial.println("HTTP server started.");
	}
	 
	/*__________________________________________________________Motor_FUNCTIONS__________________________________________________________*/
	 
	void adjust_base() {
	 delay(INTERVAL_VS_BASE);
	 x_axisServo.write(base);
	 delay(INTERVAL_VS_BASE);
	 y_axisServo.write(base);
	}
	 
	/*__________________________________________________________HELPER_FUNCTIONS__________________________________________________________*/
	 
	String formatBytes(size_t bytes) { // convert sizes in bytes to KB and MB
	 if (bytes < 1024) {
	  return String(bytes) + "B";
	 } else if (bytes < (1024 * 1024)) {
	  return String(bytes / 1024.0) + "KB";
	 } else if (bytes < (1024 * 1024 * 1024)) {
	  return String(bytes / 1024.0 / 1024.0) + "MB";
	 }
	}
	 
	String getContentType(String filename) { // determine the filetype of a given filename, based on the extension
	 if (filename.endsWith(".html")) return "text/html";
	 else if (filename.endsWith(".css")) return "text/css";
	 else if (filename.endsWith(".json")) return "text/css";
	 else if (filename.endsWith(".js")) return "application/javascript";
	 else if (filename.endsWith(".ico")) return "image/x-icon";
	 else if (filename.endsWith(".png")) return "image/x-icon";
	 else if (filename.endsWith(".gz")) return "application/x-gzip";
	 return "text/plain";
	}
	/*__________________________________________________________SERVER_HANDLERS__________________________________________________________*/
	 
	void handleNotFound() { // if the requested file or page doesn't exist, return a 404 not found error
	 if (!handleFileRead(server.uri())) {    // check if the file exists in the flash memory (SPIFFS), if so, send it
	  server.send(404, "text/plain", "404: File Not Found");
	 }
	}
	 
	boolean readHTML() {
	 File htmlFile = SPIFFS.open(path_root, "r");
	 if (!htmlFile) {
	  Serial.println("Failed to open index.html");
	  return false;
	 }
	 size_t size = htmlFile.size();
	 if (size >= BUFFER_SIZE) {
	  Serial.print("File Size Error:");
	  Serial.println((int)size);
	 } else {
	  Serial.print("File Size OK:");
	  Serial.println((int)size);
	 }
	 // htmlFile.read(buf, size);
	 htmlFile.close();
	 return true;
	}
	 
	bool handleFileRead(String path) { // send the right file to the client (if it exists)
	 Serial.println("handleFileRead: " + path);
	 if (path.endsWith("/")) path += "index.html";     // If a folder is requested, send the index file
	 String contentType = getContentType(path);       // Get the MIME type
	 String pathWithGz = path + ".gz";
	 if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { // If the file exists, either as a compressed archive, or normal
	  if (SPIFFS.exists(pathWithGz))             // If there's a compressed version available
	   path += ".gz";                     // Use the compressed verion
	  File file = SPIFFS.open(path, "r");          // Open the file
	  size_t sent = server.streamFile(file, contentType);  // Send it to the client
	  file.close();                     // Close the file again
	  Serial.println(String("\tSent file: ") + path);
	  return true;
	 }
	 Serial.println(String("\tFile Not Found: ") + path);  // If the file doesn't exist, return false
	 return false;
	}
	 
	void handleRoot() {
	 Serial.println("Access");
	 char message[20];
	 String(server.arg(0)).toCharArray(message, 20);
	 server.send(200, "text/html", (char *)buf);
	}
	 
	void handleFileUpload() { // upload a new file to the SPIFFS
	 HTTPUpload& upload = server.upload();
	 String path;
	 if (upload.status == UPLOAD_FILE_START) {
	  path = upload.filename;
	  if (!path.startsWith("/")) path = "/" + path;
	  if (!path.endsWith(".gz")) {             // The file server always prefers a compressed version of a file
	   String pathWithGz = path + ".gz";         // So if an uploaded file is not compressed, the existing compressed
	   if (SPIFFS.exists(pathWithGz))           // version of that file must be deleted (if it exists)
	    SPIFFS.remove(pathWithGz);
	  }
	  Serial.print("handleFileUpload Name: "); Serial.println(path);
	  fsUploadFile = SPIFFS.open(path, "w");      // Open the file for writing in SPIFFS (create if it doesn't exist)
	  path = String();
	 } else if (upload.status == UPLOAD_FILE_WRITE) {
	  if (fsUploadFile)
	   fsUploadFile.write(upload.buf, upload.currentSize); // Write the received bytes to the file
	 } else if (upload.status == UPLOAD_FILE_END) {
	  if (fsUploadFile) {                  // If the file was successfully created
	   fsUploadFile.close();                // Close the file again
	   Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize);
	   server.sendHeader("Location", "/success.html");   // Redirect the client to the success page
	   server.send(303);
	  } else {
	   server.send(500, "text/plain", "500: couldn't create file");
	  }
	 }
	}
	 
	void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght) { // When a WebSocket message is received
	 switch (type) {
	  case WStype_DISCONNECTED:       // if the websocket is disconnected
	   Serial.printf("[%u] Disconnected!\n", num);
	   break;
	  case WStype_CONNECTED: {       // if a new websocket connection is established
	    IPAddress ip = webSocket.remoteIP(num);
	    Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
	    //    rainbow = false;         // Turn rainbow off when a new connection is established
	   }
	   break;
	  case WStype_TEXT:           // if new text data is received
	   Serial.printf("[%u] get Text: %s\n", num, payload);
	   Serial.print("payload[0] : ");
	   Serial.println(payload[0]);
	 
	   String payloadtxt = String((char *) &payload[0]);
	   Serial.print("payloadtxt : ");
	   Serial.println(payloadtxt);
	 
	   int colonpos = payloadtxt.indexOf(":");
	   x = payloadtxt.substring(0, colonpos).toInt();
	   y = payloadtxt.substring(colonpos + 1).toInt();
	   Serial.print("x : y = ");
	   Serial.print(x);
	   Serial.print(" : ");
	   Serial.println(y);
	 
	   int xp = map(x, 0, 320, 0, 255);
	   int yp = map(y, 0, 320, 0, 255);
	   Serial.print("xp : yp = ");
	   Serial.print(xp);
	   Serial.print(" : ");
	   Serial.println(yp);
	 
	   delay(INTERVAL_VS_BASE);
	   x_axisServo.write(xp);
	   y_axisServo.write(yp);
	   delay(INTERVAL_VS_BASE);
	   break;
	 }
	}