#ifndef esp_fs_webserver_H
#define esp_fs_webserver_H

#include <Arduino.h>
#include <memory>
#include <typeinfo>
#include <base64.h>
//#include <FS.h>
#include <LittleFS.h>

#define INCLUDE_EDIT_HTM
#ifdef INCLUDE_EDIT_HTM
#include "edit_htm.h"
#endif

#define INCLUDE_SETUP_HTM
#ifdef INCLUDE_SETUP_HTM
#define ARDUINOJSON_USE_LONG_LONG 1
#include <ArduinoJson.h>
#include "setup_htm.h"
#endif

#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ESP8266HTTPUpdateServer.h> // from Arduino core, OTA update via webbrowser
using WebServerClass = ESP8266WebServer;
using UpdateServerClass = ESP8266HTTPUpdateServer;
#elif defined(ESP32)
#include <esp_wifi.h>
#include <WebServer.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <HTTPUpdateServer.h> // from Arduino core, OTA update via webbrowser
using WebServerClass = WebServer;
using UpdateServerClass = HTTPUpdateServer;
#endif
#include <DNSServer.h>

#ifndef DEBUG_ESP_PORT
#define DBG_OUTPUT_PORT Serial
#define DEBUG_MODE_WS false
#else
#define DBG_OUTPUT_PORT DEBUG_ESP_PORT
#endif

#if DEBUG_MODE_WS
#define DebugPrint(x) DBG_OUTPUT_PORT.print(x)
#define DebugPrintln(x) DBG_OUTPUT_PORT.println(x)
#define DebugPrintf(fmt, ...) DBG_OUTPUT_PORT.printf(fmt, ##__VA_ARGS__)
#define DebugPrintf_P(fmt, ...) DBG_OUTPUT_PORT.printf_P(fmt, ##__VA_ARGS__)
#else
#define DebugPrint(x)
#define DebugPrintln(x)
#define DebugPrintf(x, ...)
#define DebugPrintf_P(x, ...)
#endif

enum
{
    MSG_OK,
    CUSTOM,
    NOT_FOUND,
    BAD_REQUEST,
    ERROR
};
#define TEXT_PLAIN "text/plain"
#define FS_INIT_ERROR "FS INIT ERROR"
#define FILE_NOT_FOUND "FileNotFound"

class FSWebServer
{

    // using CallbackF = std::function<void(void)>;

public:
    WebServerClass *webserver;

    FSWebServer(fs::FS &fs, WebServerClass &server);

    bool begin(const char *path = nullptr);

    void run();

    void addHandler(const Uri &uri, HTTPMethod method, WebServerClass::THandlerFunction fn);

    void addHandler(const Uri &uri, WebServerClass::THandlerFunction handler);

    void setCaptiveWebage(const char *url);

    IPAddress setAPmode(const char *ssid, const char *psk);

    IPAddress startWiFi(uint32_t timeout, const char *apSSID, const char *apPsw);

    WebServerClass *getRequest();

#ifdef INCLUDE_SETUP_HTM

#define MIN_F -3.4028235E+38
#define MAX_F 3.4028235E+38

    inline bool clearOptions()
    {
        File file = m_filesystem->open("/config.json", "r");
        if (file)
        {
            file.close();
            m_filesystem->remove("/config.json");
            return true;
        }
        return false;
    }

    inline void addOptionBox(const char *boxTitle)
    {
        addOption("param-box", boxTitle, false);
    }

    inline void addHTML(const char *html, const char *id)
    {
        String elementId = "raw-html-";
        elementId += id;
        String trimmed = html;
        removeWhiteSpaces(trimmed);
        addOption(elementId.c_str(), trimmed.c_str(), false);
    }

    inline void addCSS(const char *css)
    {
        String trimmed = css;
        removeWhiteSpaces(trimmed);
        addOption("raw-css", trimmed.c_str(), false);
    }

    inline void addJavascript(const char *script)
    {
        // String trimmed = script;
        //  removeWhiteSpaces(trimmed);
        addOption("raw-javascript", script, true);
    }

    void addDropdownList(const char *label, const char **array, size_t size);

    // Only for backward-compatibility
    template <typename T>
    inline void addOption(fs::FS &fs, const char *label, T val, bool hidden = false)
    {
        addOption(label, val, hidden);
    }

    // Add custom option to config webpage (float values)
    template <typename T>
    inline void addOption(const char *label, T val, double d_min, double d_max, double step)
    {
        addOption(label, val, false, d_min, d_max, step);
    }

    // Add custom option to config webpage (type of parameter will be deduced from variable itself)
    template <typename T>
    inline void addOption(const char *label, T val, bool hidden = false,
                          double d_min = MIN_F, double d_max = MAX_F, double step = 1.0)
    {
        File file = m_filesystem->open("/config.json", "r");
        int sz = file.size() * 1.33;
        int docSize = max(sz, 2048);
        DynamicJsonDocument doc((size_t)docSize);
        if (file)
        {
            // If file is present, load actual configuration
            DeserializationError error = deserializeJson(doc, file);
            if (error)
            {
                DebugPrintln(F("Failed to deserialize file, may be corrupted"));
                DebugPrintln(error.c_str());
                file.close();
                return;
            }
            file.close();
        }
        else
        {
            DebugPrintln(F("File not found, will be created new configuration file"));
        }

        numOptions++;

        String key = label;
        if (hidden)
            key += "-hidden";

        // Univoque key name
        if (key.equals("param-box"))
        {
            key += numOptions;
        }
        if (key.equals("raw-javascript"))
        {
            key += numOptions;
        }

        // If key is present in json, we don't need to create it.
        if (doc.containsKey(key.c_str()))
            return;

        // if min, max, step != from default, treat this as object in order to set other properties
        if (d_min != MIN_F || d_max != MAX_F || step != 1.0)
        {
            JsonObject obj = doc.createNestedObject(key);
            obj["value"] = static_cast<T>(val);
            obj["min"] = d_min;
            obj["max"] = d_max;
            obj["step"] = step;
        }
        else
        {
            doc[key] = static_cast<T>(val);
        }

        file = m_filesystem->open("/config.json", "w");
        if (serializeJsonPretty(doc, file) == 0)
        {
            DebugPrintln(F("Failed to write to file"));
        }
        file.close();
    }

    // Get current value for a specific custom option (true on success)
    template <typename T>
    bool getOptionValue(const char *label, T &var)
    {
        File file = m_filesystem->open("/config.json", "r");
        DynamicJsonDocument doc(file.size() * 1.33);
        if (file)
        {
            DeserializationError error = deserializeJson(doc, file);
            if (error)
            {
                DebugPrintln(F("Failed to deserialize file, may be corrupted"));
                DebugPrintln(error.c_str());
                file.close();
                return false;
            }
            file.close();
        }
        else
            return false;

        if (doc[label]["value"])
            var = doc[label]["value"].as<T>();
        else if (doc[label]["selected"])
            var = doc[label]["selected"].as<T>();
        else
            var = doc[label].as<T>();
        return true;
    }


    template <typename T>
    bool saveOptionValue(const char *label, T val)
    {
        // Öffne die Datei im Lesemodus, um den Inhalt des Dokuments beizubehalten
        File file = m_filesystem->open("/config.json", "r");
        DynamicJsonDocument doc(file.size()* 1.33);

        if (file)
        {
            // Deserialisiere das JSON-Dokument aus der Datei
            DeserializationError error = deserializeJson(doc, file);
            if (error)
            {
                DebugPrintln(F("Failed to deserialize file, may be corrupted"));
                DebugPrintln(error.c_str());
                file.close();
                return false;
            }
            file.close();
        }

        // Füge den Schlüssel mit dem neuen Wert hinzu, falls er nicht vorhanden ist
        if (!doc.containsKey(label))
        {
            doc[label] = val;
        }
        else
        {
            // Ändere den Wert mit dem Schlüssel "label"
            JsonVariant option = doc[label];
            option.set(val);
        }

        // Öffne die Datei im Schreibmodus und speichere das geänderte Dokument
        file = m_filesystem->open("/config.json", "w");
        if (!file)
            return false;
        serializeJsonPretty(doc, file);
        file.close();

        return true;
    }

#endif

private:
    char m_basePath[16];
    UpdateServerClass m_httpUpdater;
    DNSServer m_dnsServer;
    fs::FS *m_filesystem;
    File m_uploadFile;
    bool m_fsOK = false;
    bool m_apmode = false;
    char *m_apWebpage = (char *)"/setup";
    uint32_t m_timeout = 10000;

    // Default handler for all URIs not defined above, use it to read files from filesystem
    bool checkDir(char *dirname, uint8_t levels);
    void doWifiConnection();
    void doRestart();
    void replyOK();
    void getIpAddress();
    void handleRequest();
#ifdef INCLUDE_SETUP_HTM
    void removeWhiteSpaces(String &str);
    void handleSetup();
    uint8_t numOptions = 0;
#endif
    void handleIndex();
    bool handleFileRead(const String &path);
    void handleFileUpload();
    void replyToCLient(int msg_type, const char *msg);
    void checkForUnsupportedPath(String &filename, String &error);
    void setCrossOrigin();
    void handleScanNetworks();
    const char *getContentType(const char *filename);
    bool captivePortal();

    // edit page, in usefull in some situation, but if you need to provide only a web interface, you can disable
#ifdef INCLUDE_EDIT_HTM
    void handleGetEdit();
    void handleFileCreate();
    void handleFileDelete();
    void handleStatus();
    void handleFileList();
#endif
};

#endif
