#include "ESPUI.h"

#include <functional>

#include <ESPAsyncWebServer.h>

#include "dataControlsJS.h"
#include "dataGraphJS.h"
#include "dataIndexHTML.h"
#include "dataNormalizeCSS.h"
#include "dataSliderJS.h"
#include "dataStyleCSS.h"
#include "dataTabbedcontentJS.h"
#include "dataZeptoJS.h"

uint16_t Control::idCounter = 1;

// ################# LITTLEFS functions
#if defined(ESP32)
void listDir(const char* dirname, uint8_t levels)
{
#if defined(DEBUG_ESPUI)
    if (ESPUI.verbosity)
    {
        Serial.printf_P(PSTR("Listing directory: %s\n"), dirname);
    }
#endif

#if defined(ESP32)
    File root = LITTLEFS.open(dirname);
#else
    File root = LittleFS.open(dirname);
#endif

    if (!root)
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Failed to open directory"));
        }
#endif

        return;
    }

    if (!root.isDirectory())
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Not a directory"));
        }
#endif

        return;
    }

    File file = root.openNextFile();

    while (file)
    {
        if (file.isDirectory())
        {
#if defined(DEBUG_ESPUI)
            if (ESPUI.verbosity)
            {
                Serial.print(F("  DIR : "));
                Serial.println(file.name());
            }
#endif

            if (levels)
            {
                listDir(file.name(), levels - 1);
            }
        }
        else
        {
#if defined(DEBUG_ESPUI)
            if (ESPUI.verbosity)
            {
                Serial.print(F("  FILE: "));
                Serial.print(file.name());
                Serial.print(F("  SIZE: "));
                Serial.println(file.size());
            }
#endif
        }

        file = root.openNextFile();
    }
}
#else

void listDir(const char* dirname, uint8_t levels)
{
    // ignoring levels for esp8266
    Serial.printf_P(PSTR("Listing directory: %s\n"), dirname);

    String str = "";
    Dir dir = LittleFS.openDir("/");

    while (dir.next())
    {
        Serial.print(F("  FILE: "));
        Serial.print(dir.fileName());
        Serial.print(F("  SIZE: "));
        Serial.println(dir.fileSize());
    }
}

#endif

void ESPUIClass::list()
{
#if defined(ESP32)
    if (!LITTLEFS.begin())
    {
        Serial.println(F("LITTLEFS Mount Failed"));
        return;
    }
#else
    if (!LittleFS.begin())
    {
        Serial.println(F("LittleFS Mount Failed"));
        return;
    }
#endif

    listDir("/", 1);
#if defined(ESP32)

    Serial.println(LITTLEFS.totalBytes());
    Serial.println(LITTLEFS.usedBytes());

#else
    FSInfo fs_info;
    LittleFS.info(fs_info);

    Serial.println(fs_info.totalBytes);
    Serial.println(fs_info.usedBytes);

#endif
}

void deleteFile(const char* path)
{
#if defined(ESP32)
    bool exists = LITTLEFS.exists(path);
#else
    bool exists = LittleFS.exists(path);
#endif

    if (!exists)
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.printf_P(PSTR("File: %s does not exist, not deleting\n"), path);
        }
#endif

        return;
    }

#if defined(DEBUG_ESPUI)
    if (ESPUI.verbosity)
    {
        Serial.printf_P(PSTR("Deleting file: %s\n"), path);
    }
#endif

#if defined(ESP32)
    bool didRemove = LITTLEFS.remove(path);
#else
    bool didRemove = LittleFS.remove(path);
#endif
    if (didRemove)
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("File deleted"));
        }
#endif
    }
    else
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Delete failed"));
        }
#endif
    }
}

void writeFile(const char* path, const char* data)
{
#if defined(DEBUG_ESPUI)
    if (ESPUI.verbosity)
    {
        Serial.printf_P(PSTR("Writing file: %s\n"), path);
    }
#endif

#if defined(ESP32)
    File file = LITTLEFS.open(path, FILE_WRITE);
#else
    File file = LittleFS.open(path, FILE_WRITE);
#endif

    if (!file)
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Failed to open file for writing"));
        }
#endif

        return;
    }

#if defined(ESP32)

    if (file.print(data))
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("File written"));
        }
#endif
    }
    else
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Write failed"));
        }
#endif
    }

#else

    if (file.print(FPSTR(data)))
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("File written"));
        }
#endif
    }
    else
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Write failed"));
        }
#endif
    }

#endif
    file.close();
}

// end LITTLEFS functions

void ESPUIClass::prepareFileSystem()
{
    // this function should only be used once

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        Serial.println(F("About to prepare filesystem..."));
    }
#endif

#if defined(ESP32)
    LITTLEFS.format();

    if (!LITTLEFS.begin(true))
    {
#if defined(DEBUG_ESPUI)
        if (this->verbosity)
        {
            Serial.println(F("LITTLEFS Mount Failed"));
        }
#endif

        return;
    }

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        listDir("/", 1);
        Serial.println(F("LITTLEFS Mount ESP32 Done"));
    }
#endif

#else
    LittleFS.format();
    LittleFS.begin();

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        Serial.println(F("LITTLEFS Mount ESP8266 Done"));
    }
#endif

#endif

    deleteFile("/index.htm");

    deleteFile("/css/style.css");
    deleteFile("/css/normalize.css");

    deleteFile("/js/zepto.min.js");
    deleteFile("/js/controls.js");
    deleteFile("/js/slider.js");
    deleteFile("/js/graph.js");
    deleteFile("/js/tabbedcontent.js");

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        Serial.println(F("Cleanup done"));
    }
#endif

    // Now write
    writeFile("/index.htm", HTML_INDEX);

    writeFile("/css/style.css", CSS_STYLE);
    writeFile("/css/normalize.css", CSS_NORMALIZE);

    writeFile("/js/zepto.min.js", JS_ZEPTO);
    writeFile("/js/controls.js", JS_CONTROLS);
    writeFile("/js/slider.js", JS_SLIDER);
    writeFile("/js/graph.js", JS_GRAPH);

    writeFile("/js/tabbedcontent.js", JS_TABBEDCONTENT);

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        Serial.println(F("Done Initializing filesystem :-)"));
    }
#endif

#if defined(ESP32)

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        listDir("/", 1);
    }
#endif

#endif

#if defined(ESP32)
    LITTLEFS.end();
#else
    LittleFS.end();
#endif
}

// Handle Websockets Communication
void onWsEvent(
    AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
{
    switch (type)
    {
    case WS_EVT_DISCONNECT: {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.print(F("Disconnected!\n"));
        }
#endif

        break;
    }

    case WS_EVT_PONG: {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.print(F("Received PONG!\n"));
        }
#endif

        break;
    }

    case WS_EVT_ERROR: {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.print(F("WebSocket Error!\n"));
        }
#endif

        break;
    }

    case WS_EVT_CONNECT: {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.print(F("Connected: "));
            Serial.println(client->id());
        }
#endif

        ESPUI.jsonDom(0, client);

#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("JSON Data Sent to Client!"));
        }
#endif
    }
    break;

    case WS_EVT_DATA: {
        String msg = "";
        msg.reserve(len + 1);

        for (size_t i = 0; i < len; i++)
        {
            msg += (char)data[i];
        }

        if (msg.startsWith(F("uiok:")))
        {
            int idx = msg.substring(msg.indexOf(':') + 1).toInt();
            ESPUI.jsonDom(idx, client);
        } else 
        {
            uint16_t id = msg.substring(msg.lastIndexOf(':') + 1).toInt();

    #if defined(DEBUG_ESPUI)
            if (ESPUI.verbosity >= Verbosity::VerboseJSON)
            {
                Serial.print(F("WS rec: "));
                Serial.println(msg);
                Serial.print(F("WS recognised ID: "));
                Serial.println(id);
            }
    #endif

            Control* c = ESPUI.getControl(id);

            if (c == nullptr)
            {
    #if defined(DEBUG_ESPUI)
                if (ESPUI.verbosity)
                {
                    Serial.print(F("No control found for ID "));
                    Serial.println(id);
                }
    #endif

                return;
            }

            if (c->callback == nullptr)
            {
    #if defined(DEBUG_ESPUI)
                if (ESPUI.verbosity)
                {
                    Serial.print(F("No callback found for ID "));
                    Serial.println(id);
                }
    #endif

                return;
            }

            if (msg.startsWith(F("bdown:")))
            {
                c->callback(c, B_DOWN);
            }
            else if (msg.startsWith(F("bup:")))
            {
                c->callback(c, B_UP);
            }
            else if (msg.startsWith(F("pfdown:")))
            {
                c->callback(c, P_FOR_DOWN);
            }
            else if (msg.startsWith(F("pfup:")))
            {
                c->callback(c, P_FOR_UP);
            }
            else if (msg.startsWith(F("pldown:")))
            {
                c->callback(c, P_LEFT_DOWN);
            }
            else if (msg.startsWith(F("plup:")))
            {
                c->callback(c, P_LEFT_UP);
            }
            else if (msg.startsWith(F("prdown:")))
            {
                c->callback(c, P_RIGHT_DOWN);
            }
            else if (msg.startsWith(F("prup:")))
            {
                c->callback(c, P_RIGHT_UP);
            }
            else if (msg.startsWith(F("pbdown:")))
            {
                c->callback(c, P_BACK_DOWN);
            }
            else if (msg.startsWith(F("pbup:")))
            {
                c->callback(c, P_BACK_UP);
            }
            else if (msg.startsWith(F("pcdown:")))
            {
                c->callback(c, P_CENTER_DOWN);
            }
            else if (msg.startsWith(F("pcup:")))
            {
                c->callback(c, P_CENTER_UP);
            }
            else if (msg.startsWith(F("sactive:")))
            {
                c->value = "1";
                ESPUI.updateControl(c, client->id());
                c->callback(c, S_ACTIVE);
            }
            else if (msg.startsWith(F("sinactive:")))
            {
                c->value = "0";
                ESPUI.updateControl(c, client->id());
                c->callback(c, S_INACTIVE);
            }
            else if (msg.startsWith(F("slvalue:")))
            {
                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
                ESPUI.updateControl(c, client->id());
                c->callback(c, SL_VALUE);
            }
            else if (msg.startsWith(F("nvalue:")))
            {
                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
                ESPUI.updateControl(c, client->id());
                c->callback(c, N_VALUE);
            }
            else if (msg.startsWith(F("tvalue:")))
            {
                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
                ESPUI.updateControl(c, client->id());
                c->callback(c, T_VALUE);
            }
            else if (msg.startsWith("tabvalue:"))
            {
                c->callback(c, client->id());
            }
            else if (msg.startsWith(F("svalue:")))
            {
                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
                ESPUI.updateControl(c, client->id());
                c->callback(c, S_VALUE);
            }
            else if (msg.startsWith(F("time:")))
            {
                c->value = msg.substring(msg.indexOf(':') + 1, msg.lastIndexOf(':'));
                ESPUI.updateControl(c, client->id());
                c->callback(c, TM_VALUE);
            }
            else
            {
    #if defined(DEBUG_ESPUI)
                if (ESPUI.verbosity)
                {
                    Serial.println(F("Malformated message from the websocket"));
                }
    #endif
            }
        }
    }
    break;

    default:
        break;
    }
}

uint16_t ESPUIClass::addControl(ControlType type, const char* label, const String& value, ControlColor color,
    uint16_t parentControl, void (*callback)(Control*, int))
{
    Control* control = new Control(type, label, callback, value, color, true, parentControl);

    if (this->controls == nullptr)
    {
        this->controls = control;
    }
    else
    {
        Control* iterator = this->controls;

        while (iterator->next != nullptr)
        {
            iterator = iterator->next;
        }

        iterator->next = control;
    }

    this->controlCount++;

    return control->id;
}

bool ESPUIClass::removeControl(uint16_t id, bool force_reload_ui)
{
    if (nullptr == this->controls)
        return false;

    Control* it = this->controls;

    if (id == it->id)
    {
        this->controls = it->next;
        delete it;
        this->controlCount--;
        if (force_reload_ui)
        {
            jsonReload();
        }
        else
        {
            jsonDom(0);
        }
        return true;
    }

    Control* it_next = it->next;
    while (nullptr != it_next && id != it_next->id)
    {
        it = it_next;
        it_next = it_next->next;
    }

    if (nullptr != it_next)
    {
        it->next = it_next->next;
        delete it_next;
        this->controlCount--;
        if (force_reload_ui)
        {
            jsonReload();
        }
        else
        {
            jsonDom(0); // resends to all
        }
        return true;
    }

    return false;
}

uint16_t ESPUIClass::label(const char* label, ControlColor color, const String& value)
{
    return addControl(ControlType::Label, label, value, color);
}

uint16_t ESPUIClass::graph(const char* label, ControlColor color)
{
    return addControl(ControlType::Graph, label, "", color);
}

uint16_t ESPUIClass::slider(
    const char* label, void (*callback)(Control*, int), ControlColor color, int value, int min, int max)
{
    uint16_t sliderId = addControl(ControlType::Slider, label, String(value), color, Control::noParent, callback);
    addControl(ControlType::Min, label, String(min), ControlColor::None, sliderId);
    addControl(ControlType::Max, label, String(max), ControlColor::None, sliderId);

    return sliderId;
}

uint16_t ESPUIClass::button(const char* label, void (*callback)(Control*, int), ControlColor color, const String& value)
{
    return addControl(ControlType::Button, label, value, color, Control::noParent, callback);
}

uint16_t ESPUIClass::switcher(const char* label, void (*callback)(Control*, int), ControlColor color, bool startState)
{
    return addControl(ControlType::Switcher, label, startState ? "1" : "0", color, Control::noParent, callback);
}

uint16_t ESPUIClass::pad(const char* label, void (*callback)(Control*, int), ControlColor color)
{
    return addControl(ControlType::Pad, label, "", color, Control::noParent, callback);
}
uint16_t ESPUIClass::padWithCenter(const char* label, void (*callback)(Control*, int), ControlColor color)
{
    return addControl(ControlType::PadWithCenter, label, "", color, Control::noParent, callback);
}

uint16_t ESPUIClass::number(
    const char* label, void (*callback)(Control*, int), ControlColor color, int number, int min, int max)
{
    uint16_t numberId = addControl(ControlType::Number, label, String(number), color, Control::noParent, callback);
    addControl(ControlType::Min, label, String(min), ControlColor::None, numberId);
    addControl(ControlType::Max, label, String(max), ControlColor::None, numberId);
    return numberId;
}

uint16_t ESPUIClass::gauge(const char* label, ControlColor color, int number, int min, int max)
{
    uint16_t numberId = addControl(ControlType::Gauge, label, String(number), color, Control::noParent);
    addControl(ControlType::Min, label, String(min), ControlColor::None, numberId);
    addControl(ControlType::Max, label, String(max), ControlColor::None, numberId);
    return numberId;
}

uint16_t ESPUIClass::separator(const char* label) {
    return addControl(ControlType::Separator, label, "", ControlColor::Alizarin, Control::noParent, nullptr);
}

uint16_t ESPUIClass::accelerometer(const char* label, void (*callback)(Control*, int), ControlColor color)
{
    return addControl(ControlType::Accel, label, "", color, Control::noParent, callback);
}

uint16_t ESPUIClass::text(const char* label, void (*callback)(Control*, int), ControlColor color, const String& value)
{
    return addControl(ControlType::Text, label, value, color, Control::noParent, callback);
}

Control* ESPUIClass::getControl(uint16_t id)
{
    Control* control = this->controls;

    while (control != nullptr)
    {
        if (control->id == id)
        {
            return control;
        }

        control = control->next;
    }

    return nullptr;
}

void ESPUIClass::updateControl(Control* control, int clientId)
{
    if (!control)
    {
        return;
    }

    if (this->ws == nullptr)
    {
        return;
    }

    String json;
    DynamicJsonDocument document(jsonUpdateDocumentSize);
    JsonObject root = document.to<JsonObject>();

    root["type"] = (int)control->type + ControlType::UpdateOffset;
    root["value"] = control->value;
    root["id"] = control->id;
    root["visible"] = control->visible;
    root["color"] = (int)control->color;
    root["enabled"] = control->enabled;
    if (control->panelStyle.length())
        root["panelStyle"] = control->panelStyle;
    if (control->elementStyle.length())
        root["elementStyle"] = control->elementStyle;
    serializeJson(document, json);

#if defined(DEBUG_ESPUI)
    if (this->verbosity >= Verbosity::VerboseJSON)
    {
        Serial.println(json);
    }
#endif

    if (clientId < 0)
    {
#if defined(DEBUG_ESPUI)
        if (this->verbosity >= Verbosity::VerboseJSON)
        {
            Serial.println(F("TextAll"));
        }
#endif
        this->ws->textAll(json);
        return;
    }
    // This is a hacky workaround because ESPAsyncWebServer does not have a
    // function like this and it's clients array is private
    int tryId = 0;

    for (size_t count = 0; tryId < (int)this->ws->count() && count < this->ws->count();)
    {
        if (this->ws->hasClient(tryId))
        {
            if (clientId != tryId)
            {
                this->ws->client(tryId)->text(json);
            }

            count++;
        }

        tryId++;
    }
}

void ESPUIClass::setPanelStyle(uint16_t id, String style, int clientId)
{
    Control* control = getControl(id);
    if (control)
    {
        control->panelStyle = style;
        updateControl(control, clientId);
    }
}

void ESPUIClass::setElementStyle(uint16_t id, String style, int clientId)
{
    Control* control = getControl(id);
    if (control)
    {
        control->elementStyle = style;
        updateControl(control, clientId);
    }
}

void ESPUIClass::setPanelWide(uint16_t id, bool wide) {
    Control* control = getControl(id);
    if (control)
    {
        control->wide = wide;
    }
}

void ESPUIClass::setEnabled(uint16_t id, bool enabled, int clientId) {
    Control* control = getControl(id);
    if (control)
    {
        control->enabled = enabled;
        updateControl(control, clientId);
    }
}


void ESPUIClass::setVertical(uint16_t id, bool vert) {
    Control* control = getControl(id);
    if (control)
    {
        control->vertical = vert;
    }
}

void ESPUIClass::updateControl(uint16_t id, int clientId)
{
    Control* control = getControl(id);

    if (!control)
    {
#if defined(DEBUG_ESPUI)
        if (this->verbosity)
        {
            Serial.printf_P(PSTR("Error: There is no control with ID %d"), id);
        }
#endif
        return;
    }

    updateControl(control, clientId);
}

void ESPUIClass::updateControlValue(Control* control, const String& value, int clientId)
{
    if (!control)
    {
        return;
    }

    control->value = value;
    updateControl(control, clientId);
}

void ESPUIClass::updateControlValue(uint16_t id, const String& value, int clientId)
{
    Control* control = getControl(id);

    if (!control)
    {
#if defined(DEBUG_ESPUI)
        if (this->verbosity)
        {
            Serial.printf_P(PSTR("Error: There is no control with ID %d"), id);
        }
#endif
        return;
    }

    updateControlValue(control, value, clientId);
}

void ESPUIClass::updateVisibility(uint16_t id, bool visibility, int clientId) {
    Control* control = getControl(id);
    if(control)
    {
        control->visible = visibility;
        updateControl(id);
    }
}

void ESPUIClass::print(uint16_t id, const String& value)
{
    updateControlValue(id, value);
}

void ESPUIClass::updateLabel(uint16_t id, const String& value)
{
    updateControlValue(id, value);
}

void ESPUIClass::updateButton(uint16_t id, const String& value) {
    updateControlValue(id, value);
}

void ESPUIClass::updateSlider(uint16_t id, int nValue, int clientId)
{
    updateControlValue(id, String(nValue), clientId);
}

void ESPUIClass::updateSwitcher(uint16_t id, bool nValue, int clientId)
{
    updateControlValue(id, String(nValue ? "1" : "0"), clientId);
}

void ESPUIClass::updateNumber(uint16_t id, int number, int clientId)
{
    updateControlValue(id, String(number), clientId);
}

void ESPUIClass::updateText(uint16_t id, const String& text, int clientId)
{
    updateControlValue(id, text, clientId);
}

void ESPUIClass::updateSelect(uint16_t id, const String& text, int clientId)
{
    updateControlValue(id, text, clientId);
}

void ESPUIClass::updateGauge(uint16_t id, int number, int clientId)
{
    updateControlValue(id, String(number), clientId);
}

void ESPUIClass::updateTime(uint16_t id, int clientId) 
{
    updateControl(id, clientId);
}

void ESPUIClass::clearGraph(uint16_t id, int clientId) { }

void ESPUIClass::addGraphPoint(uint16_t id, int nValue, int clientId)
{
    Control* control = getControl(id);
    if (!control)
    {
        return;
    }

    String json;
    DynamicJsonDocument document(jsonUpdateDocumentSize);
    JsonObject root = document.to<JsonObject>();

    root["type"] = (int)ControlType::GraphPoint;
    root["value"] = nValue;
    root["id"] = control->id;
    serializeJson(document, json);

#if defined(DEBUG_ESPUI)
    if (this->verbosity >= Verbosity::VerboseJSON)
    {
        Serial.println(json);
    }
#endif

    if (clientId < 0)
    {
        this->ws->textAll(json);
        return;
    }
    // This is a hacky workaround because ESPAsyncWebServer does not have a
    // function like this and it's clients array is private
    int tryId = 0;

    for (size_t count = 0; tryId < (int)this->ws->count() && count < this->ws->count();)
    {
        if (this->ws->hasClient(tryId))
        {
            if (clientId != tryId)
            {
                this->ws->client(tryId)->text(json);

#if defined(DEBUG_ESPUI)
                if (this->verbosity >= Verbosity::VerboseJSON)
                {
                    Serial.println(json);
                }
#endif
            }

            count++;
        }

        tryId++;
    }
}

/*
Convert & Transfer Arduino elements to JSON elements. This function sends a chunk of 
JSON describing the controls of the UI, starting from the control at index startidx.
If startidx is 0 then a UI_INITIAL_GUI message will be sent, else a UI_EXTEND_GUI.
Both message types contain a list of serialised UI elements. Only a portion of the UI
will be sent in order to avoid websocket buffer overflows. The client will acknowledge 
receipt of a partial message by requesting the next chunk of UI.

The protocol is:
SERVER: jsonDom(0): 
    "UI_INITIAL_GUI: n serialised UI elements"
CLIENT: controls.js:handleEvent()
    "uiok:n"
SERVER: jsonDom(n):
    "UI_EXTEND_GUI: n serialised UI elements"
CLIENT: controls.js:handleEvent()
    "uiok:2*n"
etc. 
*/
void ESPUIClass::jsonDom(uint16_t startidx, AsyncWebSocketClient* client)
{
    if(startidx >= this->controlCount)
    {
        return;
    }

    DynamicJsonDocument document(jsonInitialDocumentSize);
    document["type"] = startidx ? (int)UI_EXTEND_GUI : (int)UI_INITIAL_GUI;
    document["sliderContinuous"] = sliderContinuous;
    document["startindex"] = startidx;
    document["totalcontrols"] = this->controlCount;
    JsonArray items = document.createNestedArray("controls");
    JsonObject titleItem = items.createNestedObject();
    titleItem["type"] = (int)UI_TITLE;
    titleItem["label"] = ui_title;

    prepareJSONChunk(client, startidx, &items);

    String json;
    serializeJson(document, json);
#if defined(DEBUG_ESPUI)
    if (this->verbosity >= Verbosity::VerboseJSON)
    {
        Serial.println("Sending elements --------->");
        Serial.println(json);
    }
#endif
    if (client != nullptr)
        client->text(json);
    else
        this->ws->textAll(json);
}

/* Prepare a chunk of elements as a single JSON string. If the allowed number of elements is greater than the total
number this will represent the entire UI. More likely, it will represent a small section of the UI to be sent. The client
will acknoledge receipt by requesting the next chunk. */
void ESPUIClass::prepareJSONChunk(AsyncWebSocketClient* client, uint16_t startindex, JsonArray* items)
{
    //First check that there will be sufficient nodes in the list
    if(startindex >= this->controlCount) 
    {
        return;
    }

    //Follow the list until control points to the startindex'th node
    Control* control = this->controls;
    for(auto i = 0; i < startindex; i++) {
        control = control->next;
    }

    //To prevent overflow, keep track of the number of elements we have serialised into this message
    int elementcount = 0;
    while (control != nullptr && elementcount < 10)
    {
        JsonObject item = items->createNestedObject();

        item["id"] = String(control->id);
        item["type"] = (int)control->type;
        item["label"] = control->label;
        item["value"] = String(control->value);
        item["color"] = (int)control->color;
        item["visible"] = (int)control->visible;
        item["enabled"] = control->enabled;
        if (control->panelStyle.length())
            item["panelStyle"] = String(control->panelStyle);
        if (control->elementStyle.length())
            item["elementStyle"] = String(control->elementStyle);
        if (control->wide == true)
            item["wide"] = true;
        if (control->vertical == true)
            item["vertical"] = true;

        if (control->parentControl != Control::noParent)
        {
            item["parentControl"] = String(control->parentControl);
        }

        // special case for selects: to preselect an option, you have to add
        // "selected" to <option>
        if (control->type == ControlType::Option)
        {
            if (ESPUI.getControl(control->parentControl)->value == control->value)
            {
                item["selected"] = "selected";
            }
            else
            {
                item["selected"] = "";
            }
        }

        control = control->next;
        elementcount++;
    }
    return;
}

void ESPUIClass::jsonReload()
{
    String json;
    DynamicJsonDocument document(jsonUpdateDocumentSize);
    JsonObject root = document.to<JsonObject>();

    root["type"] = (int)UI_RELOAD;
    serializeJson(document, json);

#if defined(DEBUG_ESPUI)
    if (this->verbosity >= Verbosity::VerboseJSON)
    {
        Serial.println(json);
    }
#endif

    this->ws->textAll(json);
}

void ESPUIClass::beginSPIFFS(const char* _title, const char* username, const char* password, uint16_t port)
{
    // Backwards compatibility wrapper
    beginLITTLEFS(_title, username, password, port);
}

void ESPUIClass::beginLITTLEFS(const char* _title, const char* username, const char* password, uint16_t port)
{
    ui_title = _title;
    this->basicAuthUsername = username;
    this->basicAuthPassword = password;

    if (username == nullptr && password == nullptr)
    {
        basicAuth = false;
    }
    else
    {
        basicAuth = true;
    }

    server = new AsyncWebServer(port);
    ws = new AsyncWebSocket("/ws");

#if defined(ESP32)
    bool fsBegin = LITTLEFS.begin();
#else
    bool fsBegin = LittleFS.begin();
#endif
    if (!fsBegin)
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("LITTLEFS Mount Failed, PLEASE CHECK THE README ON HOW TO "
                             "PREPARE YOUR ESP!!!!!!!"));
        }
#endif

        return;
    }

#if defined(DEBUG_ESPUI)
    if (ESPUI.verbosity)
    {
        listDir("/", 1);
    }
#endif

#if defined(ESP32)
    bool indexExists = LITTLEFS.exists("/index.htm");
#else
    bool indexExists = LittleFS.exists("/index.htm");
#endif
    if (!indexExists)
    {
#if defined(DEBUG_ESPUI)
        if (ESPUI.verbosity)
        {
            Serial.println(F("Please read the README!!!!!!!, Make sure to "
                             "ESPUI.prepareFileSystem() once in an empty sketch"));
        }
#endif

        return;
    }

    ws->onEvent(onWsEvent);
    server->addHandler(ws);

    if (basicAuth)
    {
        if (WS_AUTHENTICATION)
        {
            ws->setAuthentication(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword);
        }
#if defined(ESP32)
        server->serveStatic("/", LITTLEFS, "/").setDefaultFile("index.htm").setAuthentication(username, password);
#else
        server->serveStatic("/", LittleFS, "/").setDefaultFile("index.htm").setAuthentication(username, password);
#endif
    }
    else
    {
#if defined(ESP32)
        server->serveStatic("/", LITTLEFS, "/").setDefaultFile("index.htm");
#else
        server->serveStatic("/", LittleFS, "/").setDefaultFile("index.htm");
#endif
    }

    // Heap for general Servertest
    server->on("/heap", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        request->send(200, "text/plain", String(ESP.getFreeHeap()) + " In LITTLEFS mode");
    });

    server->onNotFound([](AsyncWebServerRequest* request) { request->send(404); });

    server->begin();

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        Serial.println(F("UI Initialized"));
    }
#endif
}

void ESPUIClass::begin(const char* _title, const char* username, const char* password, uint16_t port)
{
    basicAuthUsername = username;
    basicAuthPassword = password;

    if (username != nullptr && password != nullptr)
    {
        basicAuth = true;
    }
    else
    {
        basicAuth = false;
    }

    ui_title = _title;

    server = new AsyncWebServer(port);
    ws = new AsyncWebSocket("/ws");

    ws->onEvent(onWsEvent);
    server->addHandler(ws);

    if (basicAuth && WS_AUTHENTICATION)
        ws->setAuthentication(username, password);

    server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", HTML_INDEX);
        request->send(response);
    });

    // Javascript files

    server->on("/js/zepto.min.js", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response
            = request->beginResponse_P(200, "application/javascript", JS_ZEPTO_GZIP, sizeof(JS_ZEPTO_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    server->on("/js/controls.js", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response
            = request->beginResponse_P(200, "application/javascript", JS_CONTROLS_GZIP, sizeof(JS_CONTROLS_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    server->on("/js/slider.js", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response
            = request->beginResponse_P(200, "application/javascript", JS_SLIDER_GZIP, sizeof(JS_SLIDER_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    server->on("/js/graph.js", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response
            = request->beginResponse_P(200, "application/javascript", JS_GRAPH_GZIP, sizeof(JS_GRAPH_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    server->on("/js/tabbedcontent.js", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response = request->beginResponse_P(
            200, "application/javascript", JS_TABBEDCONTENT_GZIP, sizeof(JS_TABBEDCONTENT_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    // Stylesheets

    server->on("/css/style.css", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response
            = request->beginResponse_P(200, "text/css", CSS_STYLE_GZIP, sizeof(CSS_STYLE_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    server->on("/css/normalize.css", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        AsyncWebServerResponse* response
            = request->beginResponse_P(200, "text/css", CSS_NORMALIZE_GZIP, sizeof(CSS_NORMALIZE_GZIP));
        response->addHeader("Content-Encoding", "gzip");
        request->send(response);
    });

    // Heap for general Servertest
    server->on("/heap", HTTP_GET, [](AsyncWebServerRequest* request) {
        if (ESPUI.basicAuth && !request->authenticate(ESPUI.basicAuthUsername, ESPUI.basicAuthPassword))
        {
            return request->requestAuthentication();
        }

        request->send(200, "text/plain", String(ESP.getFreeHeap()) + " In Memorymode");
    });

    server->onNotFound([](AsyncWebServerRequest* request) { request->send(404); });

    server->begin();

#if defined(DEBUG_ESPUI)
    if (this->verbosity)
    {
        Serial.println(F("UI Initialized"));
    }
#endif
}

void ESPUIClass::setVerbosity(Verbosity v)
{
    this->verbosity = v;
}

ESPUIClass ESPUI;
