#include "config.hpp"

#define MAX_ATTEMPTS_WIFI_CONNECTION 20

Preferences preferences;
Adafruit_MCP23X17 mcp1;
#ifdef IS_BTCLOCK_S3
Adafruit_MCP23X17 mcp2;
#endif
std::vector<std::string> screenNameMap(SCREEN_COUNT);
std::mutex mcpMutex;

void setup()
{
    setupPreferences();
    setupHardware();
    setupDisplays();
    if (preferences.getBool("ledTestOnPower", true))
    {
        queueLedEffect(LED_POWER_TEST);
    }
    {
        std::lock_guard<std::mutex> lockMcp(mcpMutex);
        if (mcp1.digitalRead(3) == LOW)
        {
            preferences.putBool("wifiConfigured", false);
            preferences.remove("txPower");

            WiFi.eraseAP();
            queueLedEffect(LED_EFFECT_WIFI_ERASE_SETTINGS);
        }
    }

    tryImprovSetup();

    setupWebserver();

    // setupWifi();
    setupTime();
    finishSetup();

    setupTasks();
    setupTimers();

    xTaskCreate(setupWebsocketClients, "setupWebsocketClients", 4096, NULL, tskIDLE_PRIORITY, NULL);

    setupButtonTask();
    setupOTA();

    waitUntilNoneBusy();
    forceFullRefresh();
}

void tryImprovSetup()
{
    WiFi.onEvent(WiFiEvent);

    
    if (!preferences.getBool("wifiConfigured", false))
    {
        setFgColor(GxEPD_BLACK);
        setBgColor(GxEPD_WHITE);
        queueLedEffect(LED_EFFECT_WIFI_WAIT_FOR_CONFIG);

        uint8_t x_buffer[16];
        uint8_t x_position = 0;

        bool buttonPress = false;
        {
            std::lock_guard<std::mutex> lockMcp(mcpMutex);
            buttonPress = (mcp1.digitalRead(2) == LOW);
        }
        
        {
            WiFiManager wm;

            byte mac[6];
            WiFi.macAddress(mac);
            String softAP_SSID = String("BTClock" + String(mac[5], 16) + String(mac[1], 16));
            WiFi.setHostname(softAP_SSID.c_str());
            String softAP_password = base64::encode(String(mac[2], 16) + String(mac[4], 16) + String(mac[5], 16) + String(mac[1], 16)).substring(2, 10);

          // wm.setConfigPortalTimeout(preferences.getUInt("wpTimeout", 600));
            wm.setWiFiAutoReconnect(false);
            wm.setDebugOutput(false);
            wm.setConfigPortalBlocking(true);

            wm.setAPCallback([&](WiFiManager *wifiManager)
                             {

               // Serial.printf("Entered config mode:ip=%s, ssid='%s', pass='%s'\n", 
                // WiFi.softAPIP().toString().c_str(), 
                // wifiManager->getConfigPortalSSID().c_str(), 
                // softAP_password.c_str()); 
                //delay(6000);

                const String qrText = "qrWIFI:S:" + wifiManager->getConfigPortalSSID() + ";T:WPA;P:" + softAP_password.c_str() + ";;";
                const String explainText = "*SSID: *\r\n" + wifiManager->getConfigPortalSSID() + "\r\n\r\n*Password:*\r\n" + softAP_password;
                std::array<String, NUM_SCREENS> epdContent = {"Welcome!", "Bienvenidos!", "To setup\r\nscan QR or\r\nconnect\r\nmanually", "Para\r\nconfigurar\r\nescanear QR\r\no conectar\r\nmanualmente", explainText, " ", qrText};
                setEpdContent(epdContent); });

            wm.setSaveConfigCallback([]()
                                     {
                preferences.putBool("wifiConfigured", true);

                delay(1000);
                // just restart after succes
                ESP.restart(); });

            bool ac = wm.autoConnect(softAP_SSID.c_str(), softAP_password.c_str());
        
            //waitUntilNoneBusy();
            //std::array<String, NUM_SCREENS> epdContent = {"Welcome!", "Bienvenidos!", "Use\r\nweb-interface\r\nto configure", "Use\r\nla interfaz web\r\npara configurar", "Or restart\r\nwhile\r\nholding\r\n2nd button\r\r\nto start\r\n QR-config", "O reinicie\r\nmientras\r\n mantiene presionado\r\nel segundo botón\r\r\npara iniciar\r\nQR-config", ""};
            //setEpdContent(epdContent);
            // esp_task_wdt_init(30, false);
            // uint count = 0;
            // while (WiFi.status() != WL_CONNECTED)
            // {
            //     if (Serial.available() > 0)
            //     {
            //         uint8_t b = Serial.read();

            //         if (parse_improv_serial_byte(x_position, b, x_buffer, onImprovCommandCallback, onImprovErrorCallback))
            //         {
            //             x_buffer[x_position++] = b;
            //         }
            //         else
            //         {
            //             x_position = 0;
            //         }
            //     }
            //     count++;

            //     if (count > 2000000) {
            //         queueLedEffect(LED_EFFECT_HEARTBEAT);
            //         count = 0;
            //     }
            // }
            // esp_task_wdt_deinit();
            // esp_task_wdt_reset();
            
        }
        setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
        setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
    }
    else
    {
        WiFi.setAutoConnect(true);
        WiFi.setAutoReconnect(true);
        WiFi.begin();
        if(preferences.getInt("txPower", 0)) {
            if(WiFi.setTxPower(static_cast<wifi_power_t>(preferences.getInt("txPower", 0)))) {
                Serial.printf("WiFi max tx power set to %d\n", preferences.getInt("txPower", 0));
            }
        }


        while (WiFi.status() != WL_CONNECTED)
        {
            vTaskDelay(pdMS_TO_TICKS(400));
        }
    }
    // queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
}

void setupTime()
{
    configTime(preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS), 0, NTP_SERVER);
    struct tm timeinfo;

    while (!getLocalTime(&timeinfo))
    {
        configTime(preferences.getInt("gmtOffset", TIME_OFFSET_SECONDS), 0, NTP_SERVER);
        delay(500);
        Serial.println(F("Retry set time"));
    }
}

void setupPreferences()
{
    preferences.begin("btclock", false);

    setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
    setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
    setBlockHeight(preferences.getUInt("blockHeight", 816000));
    setPrice(preferences.getUInt("lastPrice", 30000));

    screenNameMap[SCREEN_BLOCK_HEIGHT] = "Block Height";
    screenNameMap[SCREEN_MSCW_TIME] = "Sats per dollar";
    screenNameMap[SCREEN_BTC_TICKER] = "Ticker";
    screenNameMap[SCREEN_TIME] = "Time";
    screenNameMap[SCREEN_HALVING_COUNTDOWN] = "Halving countdown";
    screenNameMap[SCREEN_MARKET_CAP] = "Market Cap";
}

void setupWebsocketClients(void *pvParameters)
{
    setupBlockNotify();

    if (preferences.getBool("fetchEurPrice", false))
    {
        setupPriceFetchTask();
    }
    else
    {
        setupPriceNotify();
    }

    vTaskDelete(NULL);
}

void setupTimers()
{
    xTaskCreate(setupTimeUpdateTimer, "setupTimeUpdateTimer", 2048, NULL, tskIDLE_PRIORITY, NULL);
    xTaskCreate(setupScreenRotateTimer, "setupScreenRotateTimer", 2048, NULL, tskIDLE_PRIORITY, NULL);
}

void finishSetup()
{

    if (preferences.getBool("ledStatus", false))
    {
        restoreLedState();
    }
    else
    {
        clearLeds();
    }
}

std::vector<std::string> getScreenNameMap()
{
    return screenNameMap;
}


void setupMcp()
{
    #ifdef IS_BTCLOCK_S3
    const int mcp1AddrPins[] = {MCP1_A0_PIN, MCP1_A1_PIN, MCP1_A2_PIN};
    const int mcp1AddrValues[] = {LOW, LOW, LOW};

    const int mcp2AddrPins[] = {MCP2_A0_PIN, MCP2_A1_PIN, MCP2_A2_PIN};
    const int mcp2AddrValues[] = {LOW, LOW, HIGH};

    pinMode(MCP_RESET_PIN, OUTPUT);
    digitalWrite(MCP_RESET_PIN, HIGH);

    for (int i = 0; i < 3; ++i) {
        pinMode(mcp1AddrPins[i], OUTPUT);
        digitalWrite(mcp1AddrPins[i], mcp1AddrValues[i]);

        pinMode(mcp2AddrPins[i], OUTPUT);
        digitalWrite(mcp2AddrPins[i], mcp2AddrValues[i]);
    }

    digitalWrite(MCP_RESET_PIN, LOW);
    delay(30);
    digitalWrite(MCP_RESET_PIN, HIGH);
    #endif
}

void setupHardware()
{
    if (!LittleFS.begin(true))
    {
        Serial.println(F("An Error has occurred while mounting LittleFS"));
    } 

    if(!LittleFS.open("/index.html.gz", "r")) {
        Serial.println("Error loading WebUI");
    }

    setupLeds();

    WiFi.setHostname(getMyHostname().c_str());
    ;
    if (!psramInit())
    {
        Serial.println(F("PSRAM not available"));
    }

    setupMcp();

    Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000);

    if (!mcp1.begin_I2C(0x20))
    {
        Serial.println(F("Error MCP23017"));

        // while (1)
        //         ;
    }
    else
    {
        pinMode(MCP_INT_PIN, INPUT_PULLUP);
        mcp1.setupInterrupts(false, false, LOW);

        for (int i = 0; i < 4; i++)
        {
            mcp1.pinMode(i, INPUT_PULLUP);
            mcp1.setupInterruptPin(i, LOW);
        }
        #ifndef IS_BTCLOCK_S3
        for (int i = 8; i <= 14; i++)
        {
            mcp1.pinMode(i, OUTPUT);
        }
        #endif
    }

    #ifdef IS_BTCLOCK_S3
    if (!mcp2.begin_I2C(0x21))
    {
        Serial.println(F("Error MCP23017"));

        // while (1)
        //         ;
    }
    #endif
}

void improvGetAvailableWifiNetworks()
{
    int networkNum = WiFi.scanNetworks();

    for (int id = 0; id < networkNum; ++id)
    {
        std::vector<uint8_t> data = improv::build_rpc_response(
            improv::GET_WIFI_NETWORKS, {WiFi.SSID(id), String(WiFi.RSSI(id)), (WiFi.encryptionType(id) == WIFI_AUTH_OPEN ? "NO" : "YES")}, false);
        improv_send_response(data);
    }
    // final response
    std::vector<uint8_t> data =
        improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector<std::string>{}, false);
    improv_send_response(data);
}

bool improv_connectWifi(std::string ssid, std::string password)
{
    uint8_t count = 0;

    WiFi.begin(ssid.c_str(), password.c_str());

    while (WiFi.status() != WL_CONNECTED)
    {
        blinkDelay(500, 2);

        if (count > MAX_ATTEMPTS_WIFI_CONNECTION)
        {
            WiFi.disconnect();
            return false;
        }
        count++;
    }

    return true;
}

void onImprovErrorCallback(improv::Error err)
{
    blinkDelayColor(100, 1, 255, 0, 0);
    // pixels.setPixelColor(0, pixels.Color(255, 0, 0));
    // pixels.setPixelColor(1, pixels.Color(255, 0, 0));
    // pixels.setPixelColor(2, pixels.Color(255, 0, 0));
    // pixels.setPixelColor(3, pixels.Color(255, 0, 0));
    // pixels.show();
    // vTaskDelay(pdMS_TO_TICKS(100));

    // pixels.clear();
    // pixels.show();
    // vTaskDelay(pdMS_TO_TICKS(100));
}

std::vector<std::string> getLocalUrl()
{
    return {
        // URL where user can finish onboarding or use device
        // Recommended to use website hosted by device
        String("http://" + WiFi.localIP().toString()).c_str()};
}

bool onImprovCommandCallback(improv::ImprovCommand cmd)
{

    switch (cmd.command)
    {
    case improv::Command::GET_CURRENT_STATE:
    {
        if ((WiFi.status() == WL_CONNECTED))
        {
            improv_set_state(improv::State::STATE_PROVISIONED);
            std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_CURRENT_STATE, getLocalUrl(), false);
            improv_send_response(data);
        }
        else
        {
            improv_set_state(improv::State::STATE_AUTHORIZED);
        }

        break;
    }

    case improv::Command::WIFI_SETTINGS:
    {
        if (cmd.ssid.length() == 0)
        {
            improv_set_error(improv::Error::ERROR_INVALID_RPC);
            break;
        }

        improv_set_state(improv::STATE_PROVISIONING);
        queueLedEffect(LED_EFFECT_WIFI_CONNECTING);

        if (improv_connectWifi(cmd.ssid, cmd.password))
        {

            queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);

            // std::array<String, NUM_SCREENS> epdContent = {"S", "U", "C", "C", "E", "S", "S"};
            // setEpdContent(epdContent);

            preferences.putBool("wifiConfigured", true);

            improv_set_state(improv::STATE_PROVISIONED);
            std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, getLocalUrl(), false);
            improv_send_response(data);

            delay(2500);
            ESP.restart();
            setupWebserver();
        }
        else
        {
            queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR);

            improv_set_state(improv::STATE_STOPPED);
            improv_set_error(improv::Error::ERROR_UNABLE_TO_CONNECT);
        }

        break;
    }

    case improv::Command::GET_DEVICE_INFO:
    {
        std::vector<std::string> infos = {
            // Firmware name
            "BTClock",
            // Firmware version
            "1.0.0",
            // Hardware chip/variant
            "ESP32S3",
            // Device name
            "BTClock"};
        std::vector<uint8_t> data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false);
        improv_send_response(data);
        break;
    }

    case improv::Command::GET_WIFI_NETWORKS:
    {
        improvGetAvailableWifiNetworks();
        // std::array<String, NUM_SCREENS> epdContent = {"W", "E", "B", "W", "I", "F", "I"};
        // setEpdContent(epdContent);
        break;
    }

    default:
    {
        improv_set_error(improv::ERROR_UNKNOWN_RPC);
        return false;
    }
    }

    return true;
}

void improv_set_state(improv::State state)
{

    std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
    data.resize(11);
    data[6] = improv::IMPROV_SERIAL_VERSION;
    data[7] = improv::TYPE_CURRENT_STATE;
    data[8] = 1;
    data[9] = state;

    uint8_t checksum = 0x00;
    for (uint8_t d : data)
        checksum += d;
    data[10] = checksum;

    Serial.write(data.data(), data.size());
}

void improv_send_response(std::vector<uint8_t> &response)
{
    std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
    data.resize(9);
    data[6] = improv::IMPROV_SERIAL_VERSION;
    data[7] = improv::TYPE_RPC_RESPONSE;
    data[8] = response.size();
    data.insert(data.end(), response.begin(), response.end());

    uint8_t checksum = 0x00;
    for (uint8_t d : data)
        checksum += d;
    data.push_back(checksum);

    Serial.write(data.data(), data.size());
}

void improv_set_error(improv::Error error)
{
    std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
    data.resize(11);
    data[6] = improv::IMPROV_SERIAL_VERSION;
    data[7] = improv::TYPE_ERROR_STATE;
    data[8] = 1;
    data[9] = error;

    uint8_t checksum = 0x00;
    for (uint8_t d : data)
        checksum += d;
    data[10] = checksum;

    Serial.write(data.data(), data.size());
}

void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
    static bool first_connect = true;

    Serial.printf("[WiFi-event] event: %d\n", event);

    switch (event)
    {
    case ARDUINO_EVENT_WIFI_READY:
        Serial.println("WiFi interface ready");
        break;
    case ARDUINO_EVENT_WIFI_SCAN_DONE:
        Serial.println("Completed scan for access points");
        break;
    case ARDUINO_EVENT_WIFI_STA_START:
        Serial.println("WiFi client started");
        break;
    case ARDUINO_EVENT_WIFI_STA_STOP:
        Serial.println("WiFi clients stopped");
        break;
    case ARDUINO_EVENT_WIFI_STA_CONNECTED:
        Serial.println("Connected to access point");
        break;
    case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
        {
            if (!first_connect) {
                Serial.println("Disconnected from WiFi access point");
                queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
                uint8_t reason = info.wifi_sta_disconnected.reason;
                if(reason)
                    Serial.printf("Disconnect reason: %s, ", WiFi.disconnectReasonName((wifi_err_reason_t)reason));
            }
            break;
        }
    case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
        Serial.println("Authentication mode of access point has changed");
        break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP:
    {
        Serial.print("Obtained IP address: ");
        Serial.println(WiFi.localIP());
        if (!first_connect)
            queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
        first_connect = false;
        break;
    }
    case ARDUINO_EVENT_WIFI_STA_LOST_IP:
        Serial.println("Lost IP address and IP address is reset to 0");
        queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
        WiFi.reconnect();
        break;
    case ARDUINO_EVENT_WIFI_AP_START:
        Serial.println("WiFi access point started");
        break;
    case ARDUINO_EVENT_WIFI_AP_STOP:
        Serial.println("WiFi access point  stopped");
        break;
    case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
        Serial.println("Client connected");
        break;
    case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
        Serial.println("Client disconnected");
        break;
    case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
        Serial.println("Assigned IP address to client");
        break;
    case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED:
        Serial.println("Received probe request");
        break;
    case ARDUINO_EVENT_WIFI_AP_GOT_IP6:
        Serial.println("AP IPv6 is preferred");
        break;
    case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
        Serial.println("STA IPv6 is preferred");
        break;
    default:
        break;
    }
}