//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------

#ifndef __WEB_H__
#define __WEB_H__

#include "../utils/dbg.h"
#ifdef ESP32
#include "AsyncTCP.h"
#include "Update.h"
#else
#include "ESPAsyncTCP.h"
#endif
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#include "ESPAsyncWebServer.h"
#include "html/h/api_js.h"
#include "html/h/colorBright_css.h"
#include "html/h/colorDark_css.h"
#include "html/h/favicon_ico.h"
#include "html/h/index_html.h"
#include "html/h/login_html.h"
#include "html/h/serial_html.h"
#include "html/h/setup_html.h"
#include "html/h/style_css.h"
#include "html/h/system_html.h"
#include "html/h/save_html.h"
#include "html/h/update_html.h"
#include "html/h/visualization_html.h"

#define WEB_SERIAL_BUF_SIZE 2048

const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1", "pinCsb", "pinFcsb", "pinGpio3"};

template <class HMSYSTEM>
class Web {
   public:
        Web(void) : mWeb(80), mEvts("/events") {
            mProtected     = true;
            mLogoutTimeout = 0;

            memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
            mSerialBufFill     = 0;
            mSerialAddTime     = true;
            mSerialClientConnnected = false;
        }

        void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
            mApp     = app;
            mSys     = sys;
            mConfig  = config;

            DPRINTLN(DBG_VERBOSE, F("app::setup-on"));
            mWeb.on("/",               HTTP_GET,  std::bind(&Web::onIndex,        this, std::placeholders::_1));
            mWeb.on("/login",          HTTP_ANY,  std::bind(&Web::onLogin,        this, std::placeholders::_1));
            mWeb.on("/logout",         HTTP_GET,  std::bind(&Web::onLogout,       this, std::placeholders::_1));
            mWeb.on("/colors.css",     HTTP_GET,  std::bind(&Web::onColor,        this, std::placeholders::_1));
            mWeb.on("/style.css",      HTTP_GET,  std::bind(&Web::onCss,          this, std::placeholders::_1));
            mWeb.on("/api.js",         HTTP_GET,  std::bind(&Web::onApiJs,        this, std::placeholders::_1));
            mWeb.on("/favicon.ico",    HTTP_GET,  std::bind(&Web::onFavicon,      this, std::placeholders::_1));
            mWeb.onNotFound (                     std::bind(&Web::showNotFound,   this, std::placeholders::_1));
            mWeb.on("/reboot",         HTTP_ANY,  std::bind(&Web::onReboot,       this, std::placeholders::_1));
            mWeb.on("/system",         HTTP_ANY,  std::bind(&Web::onSystem,       this, std::placeholders::_1));
            mWeb.on("/erase",          HTTP_ANY,  std::bind(&Web::showErase,      this, std::placeholders::_1));
            mWeb.on("/factory",        HTTP_ANY,  std::bind(&Web::showFactoryRst, this, std::placeholders::_1));

            mWeb.on("/setup",          HTTP_GET,  std::bind(&Web::onSetup,        this, std::placeholders::_1));
            mWeb.on("/save",           HTTP_POST, std::bind(&Web::showSave,       this, std::placeholders::_1));

            mWeb.on("/live",           HTTP_ANY,  std::bind(&Web::onLive,         this, std::placeholders::_1));
            //mWeb.on("/api1",           HTTP_POST, std::bind(&Web::showWebApi,     this, std::placeholders::_1));

        #ifdef ENABLE_PROMETHEUS_EP
            mWeb.on("/metrics",        HTTP_ANY,  std::bind(&Web::showMetrics,    this, std::placeholders::_1));
        #endif

            mWeb.on("/update",         HTTP_GET,  std::bind(&Web::onUpdate,       this, std::placeholders::_1));
            mWeb.on("/update",         HTTP_POST, std::bind(&Web::showUpdate,     this, std::placeholders::_1),
                                                  std::bind(&Web::showUpdate2,    this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
            mWeb.on("/upload",         HTTP_POST, std::bind(&Web::onUpload,       this, std::placeholders::_1),
                                                  std::bind(&Web::onUpload2,      this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
            mWeb.on("/serial",         HTTP_GET,  std::bind(&Web::onSerial,       this, std::placeholders::_1));
            mWeb.on("/debug",          HTTP_GET,  std::bind(&Web::onDebug,        this, std::placeholders::_1));


            mEvts.onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1));
            mWeb.addHandler(&mEvts);

            mWeb.begin();

            registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h

            mUploadFail = false;
        }

        void tickSecond() {
            if (0 != mLogoutTimeout) {
                mLogoutTimeout -= 1;
                if (0 == mLogoutTimeout) {
                    if (strlen(mConfig->sys.adminPwd) > 0)
                        mProtected = true;
                }

                DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
            }

            if (mSerialClientConnnected) {
                if (mSerialBufFill > 0) {
                    mEvts.send(mSerialBuf, "serial", millis());
                    memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
                    mSerialBufFill = 0;
                }
            }
        }

        AsyncWebServer *getWebSrvPtr(void) {
            return &mWeb;
        }

        void setProtection(bool protect) {
            mProtected = protect;
        }

        bool getProtection() {
            return mProtected;
        }

        void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
            mApp->setOnUpdate();

            if (!index) {
                Serial.printf("Update Start: %s\n", filename.c_str());
                #ifndef ESP32
                Update.runAsync(true);
                #endif
                if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
                    Update.printError(Serial);
                }
            }
            if (!Update.hasError()) {
                if (Update.write(data, len) != len) {
                    Update.printError(Serial);
                }
            }
            if (final) {
                if (Update.end(true)) {
                    Serial.printf("Update Success: %uB\n", index + len);
                } else {
                    Update.printError(Serial);
                }
            }
        }

        void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
            if (!index) {
                mUploadFail = false;
                mUploadFp = LittleFS.open("/tmp.json", "w");
                if (!mUploadFp) {
                    DPRINTLN(DBG_ERROR, F("can't open file!"));
                    mUploadFail = true;
                    mUploadFp.close();
                    return;
                }
            }
            mUploadFp.write(data, len);
            if (final) {
                mUploadFp.close();
                char pwd[PWD_LEN];
                strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
                if (!mApp->readSettings("/tmp.json")) {
                    mUploadFail = true;
                    DPRINTLN(DBG_ERROR, F("upload JSON error!"));
                } else {
                    LittleFS.remove("/tmp.json");
                    strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
                    mApp->saveSettings(true);
                }
                if (!mUploadFail)
                    DPRINTLN(DBG_INFO, F("upload finished!"));
            }
        }

        void serialCb(String msg) {
            if (!mSerialClientConnnected)
                return;

            msg.replace("\r\n", "<rn>");
            if (mSerialAddTime) {
                if ((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
                    if (mApp->getTimestamp() > 0) {
                        strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9);
                        mSerialBufFill += 9;
                    }
                } else {
                    mSerialBufFill = 0;
                    mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
                    return;
                }
                mSerialAddTime = false;
            }

            if (msg.endsWith("<rn>"))
                mSerialAddTime = true;

            uint16_t length = msg.length();
            if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
                strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length);
                mSerialBufFill += length;
            } else {
                mSerialBufFill = 0;
                mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
            }
        }

    private:
        void checkRedirect(AsyncWebServerRequest *request) {
            if ((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX)
                request->redirect(F("/index"));
            else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE)
                request->redirect(F("/live"));
            else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL)
                request->redirect(F("/serial"));
            else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM)
                request->redirect(F("/system"));
            else
                request->redirect(F("/login"));
        }

        void onUpdate(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onUpdate"));

            if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) {
                if (mProtected) {
                    checkRedirect(request);
                    return;
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void showUpdate(AsyncWebServerRequest *request) {
            bool reboot = !Update.hasError();

            String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
            if (reboot)
                html += "success";
            else
                html += "failed";
            html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");

            AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
            response->addHeader("Connection", "close");
            request->send(response);
            mApp->setRebootFlag();
        }

        void onUpload(AsyncWebServerRequest *request) {
            bool reboot = !mUploadFail;

            String html = F("<!doctype html><html><head><title>Upload</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Upload: ");
            if (reboot)
                html += "success";
            else
                html += "failed";
            html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");

            AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
            response->addHeader("Connection", "close");
            request->send(response);
            mApp->setRebootFlag();
        }

        void onConnect(AsyncEventSourceClient *client) {
            DPRINTLN(DBG_VERBOSE, "onConnect");

            mSerialClientConnnected = true;

            if (client->lastId())
                DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId()));

            client->send("hello!", NULL, millis(), 1000);
        }

        void onIndex(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onIndex"));

            if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) {
                if (mProtected) {
                    checkRedirect(request);
                    return;
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onLogin(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onLogin"));

            if (request->args() > 0) {
                if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
                    mProtected = false;
                    request->redirect("/");
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onLogout(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onLogout"));

            if (mProtected) {
                checkRedirect(request);
                return;
            }

            mProtected = true;

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onColor(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onColor"));
            AsyncWebServerResponse *response;
            if (mConfig->sys.darkMode)
                response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len);
            else
                response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onCss(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onCss"));
            mLogoutTimeout = LOGOUT_TIMEOUT;
            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onApiJs(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onApiJs"));

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onFavicon(AsyncWebServerRequest *request) {
            static const char favicon_type[] PROGMEM = "image/x-icon";
            AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void showNotFound(AsyncWebServerRequest *request) {
            if (mProtected)
                checkRedirect(request);
            else
                request->redirect("/setup");
        }

        void onReboot(AsyncWebServerRequest *request) {
            mApp->setRebootFlag();
            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void showErase(AsyncWebServerRequest *request) {
            if (mProtected) {
                checkRedirect(request);
                return;
            }

            DPRINTLN(DBG_VERBOSE, F("showErase"));
            mApp->eraseSettings(false);
            onReboot(request);
        }

        void showFactoryRst(AsyncWebServerRequest *request) {
            if (mProtected) {
                checkRedirect(request);
                return;
            }

            DPRINTLN(DBG_VERBOSE, F("showFactoryRst"));
            String content = "";
            int refresh = 3;
            if (request->args() > 0) {
                if (request->arg("reset").toInt() == 1) {
                    refresh = 10;
                    if (mApp->eraseSettings(true))
                        content = F("factory reset: success\n\nrebooting ... ");
                    else
                        content = F("factory reset: failed\n\nrebooting ... ");
                } else {
                    content = F("factory reset: aborted");
                    refresh = 3;
                }
            } else {
                content = F("<h1>Factory Reset</h1>"
                    "<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
                refresh = 120;
            }
            request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
            if (refresh == 10)
                onReboot(request);
        }

        void onSetup(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onSetup"));

            if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) {
                if (mProtected) {
                    checkRedirect(request);
                    return;
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void showSave(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("showSave"));

            if (mProtected) {
                checkRedirect(request);
                return;
            }

            if (request->args() == 0)
                return;

            char buf[20] = {0};

            // general
            if (request->arg("ssid") != "")
                request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
            if (request->arg("pwd") != "{PWD}")
                request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
            if (request->arg("device") != "")
                request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
            mConfig->sys.darkMode = (request->arg("darkMode") == "on");

            // protection
            if (request->arg("adminpwd") != "{PWD}") {
                request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
                mProtected = (strlen(mConfig->sys.adminPwd) > 0);
            }
            mConfig->sys.protectionMask = 0x0000;
            for (uint8_t i = 0; i < 6; i++) {
                if (request->arg("protMask" + String(i)) == "on")
                    mConfig->sys.protectionMask |= (1 << i);
            }

            // static ip
            request->arg("ipAddr").toCharArray(buf, 20);
            ah::ip2Arr(mConfig->sys.ip.ip, buf);
            request->arg("ipMask").toCharArray(buf, 20);
            ah::ip2Arr(mConfig->sys.ip.mask, buf);
            request->arg("ipDns1").toCharArray(buf, 20);
            ah::ip2Arr(mConfig->sys.ip.dns1, buf);
            request->arg("ipDns2").toCharArray(buf, 20);
            ah::ip2Arr(mConfig->sys.ip.dns2, buf);
            request->arg("ipGateway").toCharArray(buf, 20);
            ah::ip2Arr(mConfig->sys.ip.gateway, buf);

            // inverter
            Inverter<> *iv;
            for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
                iv = mSys->getInverterByPos(i, false);
                // enable communication
                iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on");
                // address
                request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
                if (strlen(buf) == 0)
                    memset(buf, 0, 20);
                iv->config->serial.u64 = ah::Serial2u64(buf);
                switch(iv->config->serial.b[4]) {
                    case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
                    case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
                    case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
                    default:  break;
                }

                // name
                request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);

                // max channel power / name
                for (uint8_t j = 0; j < 4; j++) {
                    iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt();
                    iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
                    request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
                }
                iv->initialized = true;
            }

            if (request->arg("invInterval") != "")
                mConfig->nrf.sendInterval = request->arg("invInterval").toInt();
            if (request->arg("invRetry") != "")
                mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt();
            mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on");
            mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
            mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");

            // pinout
            uint8_t pin;
            for(uint8_t i = 0; i < 8; i ++) {
                pin = request->arg(String(pinArgNames[i])).toInt();
                switch(i) {
                    case 0: mConfig->nrf.pinCs    = ((pin != 0xff) ? pin : DEF_CS_PIN);  break;
                    case 1: mConfig->nrf.pinCe    = ((pin != 0xff) ? pin : DEF_CE_PIN);  break;
                    case 2: mConfig->nrf.pinIrq   = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break;
                    case 3: mConfig->led.led0     = pin; break;
                    case 4: mConfig->led.led1     = pin; break;
                    case 5: mConfig->cmt.pinCsb   = pin; break;
                    case 6: mConfig->cmt.pinFcsb  = pin; break;
                    case 7: mConfig->cmt.pinIrq   = pin; break;
                }
            }

            // nrf24 amplifier power
            mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03;
            mConfig->nrf.enabled = (request->arg("rf24Enable") == "on");

            // cmt
            mConfig->cmt.enabled = (request->arg("cmtEnable") == "on");

            // ntp
            if (request->arg("ntpAddr") != "") {
                request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN);
                mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff;
            }

            // sun
            if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
                mConfig->sun.lat = 0.0;
                mConfig->sun.lon = 0.0;
                mConfig->sun.disNightCom = false;
                mConfig->sun.offsetSec = 0;
            } else {
                mConfig->sun.lat = request->arg("sunLat").toFloat();
                mConfig->sun.lon = request->arg("sunLon").toFloat();
                mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on");
                mConfig->sun.offsetSec = request->arg("sunOffs").toInt() * 60;
            }

            // mqtt
            if (request->arg("mqttAddr") != "") {
                String addr = request->arg("mqttAddr");
                addr.trim();
                addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN);
            } else
                mConfig->mqtt.broker[0] = '\0';
            request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN);
            if (request->arg("mqttPwd") != "{PWD}")
                request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
            request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
            mConfig->mqtt.port = request->arg("mqttPort").toInt();
            mConfig->mqtt.interval = request->arg("mqttInterval").toInt();

            // serial console
            if (request->arg("serIntvl") != "") {
                mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff;

                mConfig->serial.debug = (request->arg("serDbg") == "on");
                mConfig->serial.showIv = (request->arg("serEn") == "on");
                // Needed to log TX buffers to serial console
                // mSys->Radio.mSerialDebug = mConfig->serial.debug;
            }

            // display
            mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
            mConfig->plugin.display.pxShift    = (request->arg("disp_pxshift") == "on");
            mConfig->plugin.display.rot        = request->arg("disp_rot").toInt();
            mConfig->plugin.display.type       = request->arg("disp_typ").toInt();
            mConfig->plugin.display.contrast   = (mConfig->plugin.display.type == 0) ? 60 : request->arg("disp_cont").toInt();
            mConfig->plugin.display.disp_data  = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_data").toInt();
            mConfig->plugin.display.disp_clk   = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : request->arg("disp_clk").toInt();
            mConfig->plugin.display.disp_cs    = (mConfig->plugin.display.type < 3)  ? DEF_PIN_OFF : request->arg("disp_cs").toInt();
            mConfig->plugin.display.disp_reset = (mConfig->plugin.display.type < 3)  ? DEF_PIN_OFF : request->arg("disp_rst").toInt();
            mConfig->plugin.display.disp_dc    = (mConfig->plugin.display.type < 3)  ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
            mConfig->plugin.display.disp_busy  = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();

            mApp->saveSettings((request->arg("reboot") == "on"));

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onLive(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onLive"));

            if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) {
                if (mProtected) {
                    checkRedirect(request);
                    return;
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            response->addHeader(F("content-type"), "text/html; charset=UTF-8");

            request->send(response);
        }

        void onDebug(AsyncWebServerRequest *request) {
            mApp->getSchedulerNames();
            AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok");
            request->send(response);
        }

        void onSerial(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onSerial"));

            if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) {
                if (mProtected) {
                    checkRedirect(request);
                    return;
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }

        void onSystem(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("onSystem"));

            if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) {
                if (mProtected) {
                    checkRedirect(request);
                    return;
                }
            }

            AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
            response->addHeader(F("Content-Encoding"), "gzip");
            request->send(response);
        }


#ifdef ENABLE_PROMETHEUS_EP
        enum {
            metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd
        } metricsStep;
        int metricsInverterId,metricsChannelId;

        void showMetrics(AsyncWebServerRequest *request) {
            DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));

            metricsStep = metricsStateStart;
            AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"),
                                                                             [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t
            {
                Inverter<> *iv;
                record_t<> *rec;
                statistics_t *stat;
                String promUnit, promType;
                String metrics;
                char type[60], topic[100], val[25];
                size_t len = 0;
                int alarmChannelId;

                switch (metricsStep) {
                    case metricsStateStart: // System Info & NRF Statistics : fit to one packet
                        snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n");
                        snprintf(topic,sizeof(topic),"ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1\n",
                            mApp->getVersion(), mConfig->sys.deviceName);
                        metrics = String(type) + String(topic);

                        snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n");
                        snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap());
                        metrics += String(type) + String(topic);

                        snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n");
                        snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime());
                        metrics += String(type) + String(topic);

                        snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n");
                        snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI());
                        metrics += String(type) + String(topic);

                        // NRF Statistics
                        stat = mApp->getStatistics();
                        uint32_t *nrfSendCnt, *nrfRetransmits;
                        mApp->getNrfRadioCounters(nrfSendCnt, nrfRetransmits);
                        metrics += radioStatistic(F("rx_success"),     stat->rxSuccess);
                        metrics += radioStatistic(F("rx_fail"),        stat->rxFail);
                        metrics += radioStatistic(F("rx_fail_answer"), stat->rxFailNoAnser);
                        metrics += radioStatistic(F("frame_cnt"),      stat->frmCnt);
                        metrics += radioStatistic(F("tx_cnt"),         *nrfSendCnt);
                        metrics += radioStatistic(F("retrans_cnt"),    *nrfRetransmits);

                        len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
                        // Start Inverter loop
                        metricsInverterId = 0;
                        metricsStep = metricsStateInverter;
                        break;

                    case metricsStateInverter: // Inverter loop
                        if (metricsInverterId < mSys->getNumInverters()) {
                            iv = mSys->getInverterByPos(metricsInverterId);
                            if(NULL != iv) {
                                // Inverter info : fit to one packet
                                snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n");
                                snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n",
                                    iv->config->name, iv->config->serial.u64);
                                metrics = String(type) + String(topic);

                                snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n");
                                snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled);
                                metrics += String(type) + String(topic);

                                snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n");
                                snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp()));
                                metrics += String(type) + String(topic);

                                snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n");
                                snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp()));
                                metrics += String(type) + String(topic);

                                len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());

                                // Start Realtime Data Channel loop for this inverter
                                metricsChannelId = 0;
                                metricsStep = metricStateRealtimeData;
                            }
                        } else {
                            metricsStep = metricsStateEnd;
                        }
                        break;

                    case metricStateRealtimeData: // Realtime Data Channel loop
                        iv = mSys->getInverterByPos(metricsInverterId);
                        rec = iv->getRecordStruct(RealTimeRunData_Debug);
                        if (metricsChannelId < rec->length) {
                            uint8_t channel = rec->assign[metricsChannelId].ch;
                            std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec));
                            snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
                            if (0 == channel) {
                                snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name);
                            } else {
                                snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]);
                            }
                            snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec));
                            len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val);

                            metricsChannelId++;
                        } else {
                            len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.

                            // All realtime data channels processed --> try alarm data
                            metricsStep = metricsStateAlarmData;
                        }
                        break;

                    case metricsStateAlarmData: // Alarm Info loop
                        iv = mSys->getInverterByPos(metricsInverterId);
                        rec = iv->getRecordStruct(AlarmData);
                        // simple hack : there is only one channel with alarm data
                        // TODO: find the right one channel with the alarm id
                        alarmChannelId = 0;
                        // printf("AlarmData Length %d\n",rec->length);
                        if (alarmChannelId < rec->length) {
                            //uint8_t channel = rec->assign[alarmChannelId].ch;
                            std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec));
                            snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str());
                            snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name);
                            snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec));
                            len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val);
                        } else {
                            len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
                        }
                        // alarm channel processed --> try next inverter
                        metricsInverterId++;
                        metricsStep = metricsStateInverter;
                        break;

                    case metricsStateEnd:
                    default: // end of transmission
                        len = 0;
                        break;
                }
                return len;
            });
            request->send(response);
        }

        String radioStatistic(String statistic, uint32_t value) {
            char type[60], topic[80], val[25];
            snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str());
            snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str());
            snprintf(val, sizeof(val), "%d", value);
            return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n");
        }

        std::pair<String, String> convertToPromUnits(String shortUnit) {
            if(shortUnit == "A")    return {"_ampere", "gauge"};
            if(shortUnit == "V")    return {"_volt", "gauge"};
            if(shortUnit == "%")    return {"_ratio", "gauge"};
            if(shortUnit == "W")    return {"_watt", "gauge"};
            if(shortUnit == "Wh")   return {"_wattHours", "counter"};
            if(shortUnit == "kWh")  return {"_kilowattHours", "counter"};
            if(shortUnit == "°C")   return {"_celsius", "gauge"};
            if(shortUnit == "var")  return {"_var", "gauge"};
            if(shortUnit == "Hz")   return {"_hertz", "gauge"};
            return {"", "gauge"};
        }
#endif
        AsyncWebServer mWeb;
        AsyncEventSource mEvts;
        bool mProtected;
        uint32_t mLogoutTimeout;
        IApp *mApp;
        HMSYSTEM *mSys;

        settings_t *mConfig;

        bool mSerialAddTime;
        char mSerialBuf[WEB_SERIAL_BUF_SIZE];
        uint16_t mSerialBufFill;
        bool mSerialClientConnnected;

        File mUploadFp;
        bool mUploadFail;
};

#endif /*__WEB_H__*/