/*
  webinterface.cpp - ESP3D configuration class

  Copyright (c) 2014 Luc Lebosse. All rights reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include <pgmspace.h>
#include "config.h"
#include "webinterface.h"
#include "wificonf.h"
#include <WiFiClient.h>
#include <WiFiServer.h>
#include <WiFiUdp.h>
#include <StreamString.h>
#ifndef FS_NO_GLOBALS
#define FS_NO_GLOBALS
#endif
#include <FS.h>
#if defined (ASYNCWEBSERVER)
#include <ESPAsyncWebServer.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include "ESP8266WiFi.h"
#if defined (ASYNCWEBSERVER)
#include <ESPAsyncTCP.h>
#else
#include <ESP8266WebServer.h>
#endif
#else //ESP32
#include <WiFi.h>
#if defined (ASYNCWEBSERVER)
#include <AsyncTCP.h>
#else
#include <WebServer.h>
#endif
#include "SPIFFS.h"
#include "Update.h"
#endif

#include "GenLinkedList.h"
#include "command.h"
#include "espcom.h"

#ifdef SSDP_FEATURE
#ifdef ARDUINO_ARCH_ESP32
#include <ESP32SSDP.h>
#else
#include <ESP8266SSDP.h>
#endif
#endif

#if defined(ASYNCWEBSERVER)
#include "asyncwebserver.h"
#else
#include "syncwebserver.h"
#endif

#define MAX_AUTH_IP 10

long id_connection = 0;

uint8_t Checksum(const char * line, uint16_t lineSize)
{
    uint8_t checksum_val =0;
    for (uint16_t i=0; i < lineSize; i++) {
        checksum_val = checksum_val ^ ((uint8_t)line[i]);
    }
    return checksum_val;
}

String CheckSumLine(const char* line, uint32_t linenb)
{
    String linechecksum = "N" + String(linenb)+ " " + line;
    uint8_t crc = Checksum(linechecksum.c_str(), linechecksum.length());
    linechecksum+="*"+String(crc);
    return linechecksum;
}

bool purge_serial()
{
    uint32_t start = millis();
    uint8_t buf [51];
    ESPCOM::flush (DEFAULT_PRINTER_PIPE);
    CONFIG::wait (5);
    LOG("Purge Serial\r\n")
    while (ESPCOM::available(DEFAULT_PRINTER_PIPE) > 0 ) {
        if ((millis() - start ) > 2000) {
            LOG("Purge timeout\r\n")
            return false;
        }
        size_t len = ESPCOM::readBytes (DEFAULT_PRINTER_PIPE, buf, 50);
        buf[len] = '\0';
        LOG("Purge: \r\n************\r\n")
        LOG((const char *)buf)
        LOG("\r\n************\r\n")
        if ( ( CONFIG::GetFirmwareTarget() == REPETIER4DV) || (CONFIG::GetFirmwareTarget() == REPETIER) ) {
            String s = (const char *)buf;
            //repetier never stop sending data so no need to wait if have 'wait' or 'busy'
            if((s.indexOf ("wait") > -1) || (s.indexOf ("busy") > -1)) {
                return true;
            }
            LOG("Purge interrupted\r\n")
        }
        CONFIG::wait (5);
    }
    CONFIG::wait (0);
    LOG("Purge done\r\n")
    return true;
}

size_t wait_for_data(uint32_t timeout)
{
    uint32_t start = millis();
    while ((ESPCOM::available(DEFAULT_PRINTER_PIPE) < 2) && ((millis()-start) < timeout)) {
        CONFIG::wait (10);
    }
    return ESPCOM::available(DEFAULT_PRINTER_PIPE);
}

uint32_t Get_lineNumber(String & response)
{
    int32_t l = 0;
    String sresend = "Resend:";
    if ( CONFIG::GetFirmwareTarget() == SMOOTHIEWARE) {
        sresend = "rs N";
    }
    int pos = response.indexOf(sresend);
    if (pos == -1 ) {
        return -1;
    }
    pos+=sresend.length();
    int pos2 = response.indexOf("\n", pos);
    String snum = response.substring(pos, pos2);
    //remove potential unwished char
    snum.replace("\r", "");
    l = snum.toInt();
    LOG("Line requested is ")
    LOG(String(l))
    LOG("\r\n")
    return l;
}

//function to send line to serial///////////////////////////////////////
//if newlinenb is NULL no auto correction of line number in case of resend
bool sendLine2Serial (String &  line, int32_t linenb,  int32_t * newlinenb)
{
    LOG ("Send line ")
    LOG (line )
    LOG ("  NB:")
    LOG (String(linenb))
    LOG ("\r\n")
    String line2send;
    String sok = "ok";
    String sresend = "Resend:";
    if (newlinenb) {
        *newlinenb = linenb;
    }
    if ( CONFIG::GetFirmwareTarget() == SMOOTHIEWARE) {
        sresend = "rs N";
    }
    if (linenb != -1) {
        if ( ( CONFIG::GetFirmwareTarget() == REPETIER4DV) || (CONFIG::GetFirmwareTarget() == REPETIER) ) {
            sok+=" " + String(linenb);
        }
        line2send = CheckSumLine(line.c_str(),linenb);
    } else {
        line2send = line;
    }
    //purge serial as nothing is supposed to interfere with upload
    purge_serial();
    LOG ("Send line ")
    LOG (line2send )
    LOG ("\r\n")
    //send line
    ESPCOM::println (line2send, DEFAULT_PRINTER_PIPE);
    ESPCOM::flush(DEFAULT_PRINTER_PIPE);
    //check answer
    if (wait_for_data(2000) > 0 ) {
        bool done = false;
        uint32_t timeout = millis();
        uint8_t count = 0;
        //got data check content
        String response ;
        while (!done) {
            size_t len = ESPCOM::available(DEFAULT_PRINTER_PIPE);
            //get size of buffer
            if (len > 0) {
                uint8_t * sbuf = (uint8_t *)malloc(len+1);
                if(!sbuf) {
                    return false;
                }
                //read buffer
                ESPCOM::readBytes (DEFAULT_PRINTER_PIPE, sbuf, len);
                //convert buffer to zero end array
                sbuf[len] = '\0';
                //use string because easier to handle and allow to re-assemble cutted answer
                response += (const char*) sbuf;
                LOG ("Response:\r\n************\r\n")
                LOG (response)
                LOG ("\r\n************\r\n")
                //in that case there is no way to know what is the right number to use and so send should be failed
                if (( ( CONFIG::GetFirmwareTarget() == REPETIER4DV) || (CONFIG::GetFirmwareTarget() == REPETIER) ) && (response.indexOf ("skip") != -1)) {
                    LOG ("Wrong line requested\r\n")
                    count = 5;
                }
                //it is resend ?
                int pos = response.indexOf (sresend);
                //be sure we get full line to be able to process properly
                if (( pos > -1) && (response.lastIndexOf("\n") > pos)) {
                    LOG ("Resend detected\r\n")
                    uint32_t line_number = Get_lineNumber(response);
                    //this part is only if have newlinenb variable
                    if (newlinenb != nullptr) {
                        *newlinenb = line_number;
                        free(sbuf);
                        //no need newlinenb in this one in theory, but just in case...
                        return sendLine2Serial (line, line_number, newlinenb);
                    } else {
                        //the line requested is not the current one so we stop
                        if (line_number !=linenb) {
                            LOG ("Wrong line requested\r\n")
                            count = 5;
                        }
                    }
                    count++;
                    if (count > 5) {
                        free(sbuf);
                        LOG ("Exit too many resend or wrong line\r\n")
                        return false;
                    }
                    purge_serial();
                    LOG ("Resend ")
                    LOG (String(count))
                    LOG ("\r\n")
                    response="";
                    ESPCOM::println (line2send, DEFAULT_PRINTER_PIPE);
                    ESPCOM::flush (DEFAULT_PRINTER_PIPE);
                    wait_for_data(1000);
                    timeout = millis();

                } else {
                    if ( (response.indexOf (sok) > -1) ) { //we have ok so it is done
                        free(sbuf);
                        LOG ("Got ok\r\n")
                        purge_serial();
                        return true;
                    }
                }
                free(sbuf);
            }
            //no answer or over buffer  exit
            if ( (millis() - timeout > 2000) ||  (response.length() >200)) {
                LOG ("Time out\r\n")
                done = true;
            }
            CONFIG::wait (5);
        }
    }
    LOG ("Send line error\r\n")
    return false;
}

//send M29 / M30 command to close file on SD////////////////////////////
void CloseSerialUpload (bool iserror, String & filename, int32_t linenb)
{
    purge_serial();
    String command = "M29";
    purge_serial();
    if (!sendLine2Serial (command,linenb, &linenb)) {
        wait_for_data(2000);
        if (!sendLine2Serial (command,linenb, &linenb)) {
            if ((CONFIG::GetFirmwareTarget() == REPETIER4DV) || (CONFIG::GetFirmwareTarget() == REPETIER) ) {
                sendLine2Serial (command,-1, NULL);
            }
        }
    }
    if (iserror) {
        String cmdfilename = "M30 " + filename;
        sendLine2Serial (cmdfilename,-1, NULL);
        ESPCOM::println (F ("SD upload failed"), PRINTER_PIPE);
        ESPCOM::flush (DEFAULT_PRINTER_PIPE);
        web_interface->_upload_status = UPLOAD_STATUS_FAILED;
    }  else {
        ESPCOM::println (F ("SD upload done"), PRINTER_PIPE);
        ESPCOM::flush (DEFAULT_PRINTER_PIPE);
        web_interface->_upload_status = UPLOAD_STATUS_SUCCESSFUL;
    }
    //lets give time to FW to proceed
    wait_for_data(2000);
    purge_serial();
    web_interface->blockserial = false;
}


//constructor
WEBINTERFACE_CLASS::WEBINTERFACE_CLASS (int port) : web_server (port)
#if defined(ASYNCWEBSERVER)
    , web_events("/events")
#ifdef WS_DATA_FEATURE
    , web_socket("/ws")
#endif
#endif
{
    //that handle "/" and default index.html.gz
#if defined(ASYNCWEBSERVER)
    //trick to catch command line on "/" before file being processed
    web_server.serveStatic ("/", SPIFFS, "/").setDefaultFile ("index.html").setFilter (filterOnRoot);
    web_server.serveStatic ("/", SPIFFS, "/Nowhere");
    //events functions
    web_events.onConnect(handle_onevent_connect);
    //events management
    web_server.addHandler(&web_events);
#ifdef WS_DATA_FEATURE
    //Websocket function
    web_socket.onEvent(handle_Websocket_Event);
    //Websocket management
    web_server.addHandler(&web_socket);
#endif
#else
    web_server.on("/",HTTP_ANY, handle_web_interface_root);
#endif
    //need to be there even no authentication to say to UI no authentication
    web_server.on("/login", HTTP_ANY, handle_login);
#ifdef SSDP_FEATURE
    web_server.on ("/description.xml", HTTP_GET, handle_SSDP);
#endif
#ifdef CAPTIVE_PORTAL_FEATURE
#if defined(ASYNCWEBSERVER)
    web_server.on ("/generate_204", HTTP_ANY,  [] (AsyncWebServerRequest * request) {
        request->redirect ("/");
    });
    web_server.on ("/gconnectivitycheck.gstatic.com", HTTP_ANY,  [] (AsyncWebServerRequest * request) {
        request->redirect ("/");
    });
    //do not forget the / at the end
    web_server.on ("/fwlink/", HTTP_ANY,  [] (AsyncWebServerRequest * request) {
        request->redirect ("/");
    });
#else
    web_server.on("/generate_204",HTTP_ANY, handle_web_interface_root);
    web_server.on("/gconnectivitycheck.gstatic.com",HTTP_ANY, handle_web_interface_root);
    //do not forget the / at the end
    web_server.on("/fwlink/",HTTP_ANY, handle_web_interface_root);
#endif
#endif
    //SPIFFS
    web_server.on ("/files", HTTP_ANY, handleFileList, SPIFFSFileupload);
#ifdef WEB_UPDATE_FEATURE
    web_server.on ("/updatefw", HTTP_ANY, handleUpdate, WebUpdateUpload);
#endif
    //Page not found handler
    web_server.onNotFound ( handle_not_found);
    //web commands
    web_server.on ("/command", HTTP_ANY, handle_web_command);
    web_server.on ("/command_silent", HTTP_ANY, handle_web_command_silent);
    //Serial SD management
    web_server.on ("/upload_serial", HTTP_ANY, handle_serial_SDFileList, SDFile_serial_upload);

    blockserial = false;
    restartmodule = false;
    _head = NULL;
    _nb_ip = 0;
    _upload_status = UPLOAD_STATUS_NONE;
}
//Destructor
WEBINTERFACE_CLASS::~WEBINTERFACE_CLASS()
{
    while (_head) {
        auth_ip * current = _head;
        _head = _head->_next;
        delete current;
    }
    _nb_ip = 0;
}
//check authentification
level_authenticate_type  WEBINTERFACE_CLASS::is_authenticated()
{
#ifdef AUTHENTICATION_FEATURE
    if (web_server.hasHeader ("Cookie") ) {
        String cookie = web_server.header ("Cookie");
        int pos = cookie.indexOf ("ESPSESSIONID=");
        if (pos != -1) {
            int pos2 = cookie.indexOf (";", pos);
            String sessionID = cookie.substring (pos + strlen ("ESPSESSIONID="), pos2);
            IPAddress ip = web_server.client().remoteIP();
            //check if cookie can be reset and clean table in same time
            return ResetAuthIP (ip, sessionID.c_str() );
        }
    }
    return LEVEL_GUEST;
#else
    return LEVEL_ADMIN;
#endif
}

#ifdef AUTHENTICATION_FEATURE
//add the information in the linked list if possible
bool WEBINTERFACE_CLASS::AddAuthIP (auth_ip * item)
{
    if (_nb_ip > MAX_AUTH_IP) {
        return false;
    }
    item->_next = _head;
    _head = item;
    _nb_ip++;
    return true;
}

//Session ID based on IP and time using 16 char
char * WEBINTERFACE_CLASS::create_session_ID()
{
    static char  sessionID[17];
//reset SESSIONID
    for (int i = 0; i < 17; i++) {
        sessionID[i] = '\0';
    }
//get time
    uint32_t now = millis();
//get remote IP
    IPAddress remoteIP = web_server.client().remoteIP();
//generate SESSIONID
    if (0 > sprintf (sessionID, "%02X%02X%02X%02X%02X%02X%02X%02X", remoteIP[0], remoteIP[1], remoteIP[2], remoteIP[3], (uint8_t) ( (now >> 0) & 0xff), (uint8_t) ( (now >> 8) & 0xff), (uint8_t) ( (now >> 16) & 0xff), (uint8_t) ( (now >> 24) & 0xff) ) ) {
        strcpy (sessionID, "NONE");
    }
    return sessionID;
}



bool WEBINTERFACE_CLASS::ClearAuthIP (IPAddress ip, const char * sessionID)
{
    auth_ip * current = _head;
    auth_ip * previous = NULL;
    bool done = false;
    while (current) {
        if ( (ip == current->ip) && (strcmp (sessionID, current->sessionID) == 0) ) {
            //remove
            done = true;
            if (current == _head) {
                _head = current->_next;
                _nb_ip--;
                delete current;
                current = _head;
            } else {
                previous->_next = current->_next;
                _nb_ip--;
                delete current;
                current = previous->_next;
            }
        } else {
            previous = current;
            current = current->_next;
        }
    }
    return done;
}

//Get info
auth_ip * WEBINTERFACE_CLASS::GetAuth (IPAddress ip, const char * sessionID)
{
    auth_ip * current = _head;
    //auth_ip * previous = NULL;
    //get time
    //uint32_t now = millis();
    while (current) {
        if (ip == current->ip) {
            if (strcmp (sessionID, current->sessionID) == 0) {
                //found
                return current;
            }
        }
        //previous = current;
        current = current->_next;
    }
    return NULL;
}

//Review all IP to reset timers
level_authenticate_type WEBINTERFACE_CLASS::ResetAuthIP (IPAddress ip, const char * sessionID)
{
    auth_ip * current = _head;
    auth_ip * previous = NULL;
    //get time
    //uint32_t now = millis();
    while (current) {
        if ( (millis() - current->last_time) > 180000) {
            //remove
            if (current == _head) {
                _head = current->_next;
                _nb_ip--;
                delete current;
                current = _head;
            } else {
                previous->_next = current->_next;
                _nb_ip--;
                delete current;
                current = previous->_next;
            }
        } else {
            if (ip == current->ip) {
                if (strcmp (sessionID, current->sessionID) == 0) {
                    //reset time
                    current->last_time = millis();
                    return (level_authenticate_type) current->level;
                }
            }
            previous = current;
            current = current->_next;
        }
    }
    return LEVEL_GUEST;
}
#endif

//Check what is the content tye according extension file
String WEBINTERFACE_CLASS::getContentType (String filename)
{
    if (filename.endsWith (".htm") ) {
        return "text/html";
    } else if (filename.endsWith (".html") ) {
        return "text/html";
    } else if (filename.endsWith (".css") ) {
        return "text/css";
    } else if (filename.endsWith (".js") ) {
        return "application/javascript";
    } else if (filename.endsWith (".png") ) {
        return "image/png";
    } else if (filename.endsWith (".gif") ) {
        return "image/gif";
    } else if (filename.endsWith (".jpeg") ) {
        return "image/jpeg";
    } else if (filename.endsWith (".jpg") ) {
        return "image/jpeg";
    } else if (filename.endsWith (".ico") ) {
        return "image/x-icon";
    } else if (filename.endsWith (".xml") ) {
        return "text/xml";
    } else if (filename.endsWith (".pdf") ) {
        return "application/x-pdf";
    } else if (filename.endsWith (".zip") ) {
        return "application/x-zip";
    } else if (filename.endsWith (".gz") ) {
        return "application/x-gzip";
    } else if (filename.endsWith (".txt") ) {
        return "text/plain";
    }
    return "application/octet-stream";
}


WEBINTERFACE_CLASS * web_interface;
