#include <Arduino.h>
#include <ESPTelnet.h>
#include "config.h"
#include "utils.h"
#include "network.h"
#include "display.h"
#include "gameoflife.h"
#include "settings.h"

MDNSResponder mdns;
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
ESPTelnet telnet;
IPAddress ip;

int otaProgress = 0;

void setupNetwork()
{
  setupWifi();
  setupMDNS();
  setupOTA();
  setupTelnet();
  setupWebserver();
}

void setupWifi()
{
  ESPConnect.autoConnect(HOSTNAME);
  if (ESPConnect.begin(&server))
  {
    ip = WiFi.localIP();
    logLine("Connected to WiFi: ", false);
    logLine(WiFi.localIP().toString());
  }
  else
  {
    logLine("Failed to connect to WiFi");
  }
}

void setupMDNS()
{
  if (mdns.begin(HOSTNAME))
  {
    logLine("MDNS responder started");
    mdns.addService("http", "tcp", 80);
  }
  else
  {
    logLine("MDNS.begin failed");
  }
}

void setupOTA()
{
  ArduinoOTA.onStart([]()
                     {
                      runGame = false;
                      ws.enable(false);
                      ws.closeAll();
                      logLine("OTA Update Start");
                      clearDisplay(); });
  ArduinoOTA.onEnd([]()
                   {
                    otaProgress = 0;
                    runGame = true; 
                    logLine("OTA Update End");
                    clearDisplay(); });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total)
                        {
                          int percent = ceil(progress/(total/100));
                          if (percent > otaProgress) {
                            char msg[10];
                            sprintf(msg, "OTA:%3d%%", percent);
                            showMessage(msg);
                            logLine("OTA: ", false);
                            logLine((String)percent, false);
                            logLine("%");
                            otaProgress = percent;
                          } });
  ArduinoOTA.onError([](ota_error_t error)
                     {
    if(error == OTA_AUTH_ERROR) logLine("OTA Auth Failed");
    else if(error == OTA_BEGIN_ERROR) logLine("OTA Begin Failed");
    else if(error == OTA_CONNECT_ERROR) logLine("OTA Connect Failed");
    else if(error == OTA_RECEIVE_ERROR) logLine("OTA Receive Failed");
    else if(error == OTA_END_ERROR) logLine("OTA End Failed");
    clearDisplay();
    otaProgress = 0;
    runGame = true; });
  ArduinoOTA.setHostname(HOSTNAME);
  ArduinoOTA.begin();
}

void setupTelnet()
{
  telnet.onConnect(onTelnetConnect);
  telnet.onConnectionAttempt(onTelnetConnectionAttempt);
  telnet.onReconnect(onTelnetReconnect);
  telnet.onDisconnect(onTelnetDisconnect);
  telnet.onInputReceived(onTelnetInput);

  Serial.print("- Telnet: ");
  if (telnet.begin())
  {
    Serial.println("running");
  }
  else
  {
    Serial.println("error.");
  }
}

void setupWebserver()
{
  LittleFS.begin();
  ws.onEvent(onEvent);
  server.addHandler(&ws);
  server.on("/heap", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send(200, "text/plain", String(ESP.getFreeHeap())); });
  server.onNotFound([](AsyncWebServerRequest *request)
                    { request->send(404); });
  server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
  server.begin();
}

void networkLoop()
{
  ArduinoOTA.handle();
  telnet.loop();
}

void onTelnetConnect(String ip)
{
  Serial.print("- Telnet: ");
  Serial.print(ip);
  Serial.println(" connected");

  telnet.println("\nWelcome " + telnet.getIP());
  telnet.println("Use 'CTRL+Q, ENTER' to disconnect.\n");
}

void onTelnetDisconnect(String ip)
{
  Serial.print("- Telnet: ");
  Serial.print(ip);
  Serial.println(" disconnected");
}

void onTelnetReconnect(String ip)
{
  Serial.print("- Telnet: ");
  Serial.print(ip);
  Serial.println(" reconnected");
}

void onTelnetConnectionAttempt(String ip)
{
  Serial.print("- Telnet: ");
  Serial.print(ip);
  Serial.println(" tried to connected");
}

void onTelnetInput(String str)
{
  if (str == "ping")
  {
    telnet.println("> pong");
    Serial.println("- Telnet: pong");
  }
  else if (str == "quit" || str == "exit")
  {
    telnet.println("> disconnecting...");
    telnet.disconnectClient();
  }
  else if (str == "reboot" || str == "reset")
  {
    telnet.println("> rebooting...");
    ESP.restart();
  }
}

DynamicJsonDocument getConfigJson()
{
  DynamicJsonDocument doc(200);
  JsonObject config = doc.createNestedObject("config");
  config["brightness"] = defaultBrightness;
  config["interval"] = gameInterval;
  return doc;
}

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{
  if (type == WS_EVT_CONNECT)
  {
    // client connected
    logLine("WS<< connect");
    logLine("WS>> ping");
    client->ping();
  }
  else if (type == WS_EVT_DISCONNECT)
  {
    logLine("WS<< disconnect");
  }
  else if (type == WS_EVT_ERROR)
  {
    // error was received from the other end
    char msg[64];
    snprintf_P(msg, sizeof(msg), PSTR("WS[%u] error(%u): %s"), client->id(), *((uint16_t *)arg), (char *)data);
    logLine(msg);
  }
  else if (type == WS_EVT_PONG)
  {
    // pong message was received (in response to a ping request maybe)
    logLine("WS<< pong");
  }
  else if (type == WS_EVT_DATA)
  {
    // data packet
    data[len] = 0;
    logLine("WS<< ", false);
    logLine((char *)data);
    handleJson(data);
  }
}

void handleJson(uint8_t *data)
{
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, (char *)data);
  if (error)
  {
    logLine("deserializeJson() failed: ", false);
    logLine(error.c_str());
    return;
  }

  if (doc.containsKey("config"))
  {
    updateConfig(doc);
  }

  if (doc.containsKey("action"))
  {
    if (doc["action"] == "load")
    {
      loadSettings();
      sendConfig();
    }
    else if (doc["action"] == "save")
    {
      saveSettings();
    }
    else if (doc["action"] == "addGlider")
    {
      addGlider();
    }
    else if (doc["action"] == "reboot")
    {
      ESP.restart();
    }
  }
}

void updateConfig(StaticJsonDocument<200U> doc)
{
  gameInterval = doc["config"]["interval"];
  defaultBrightness = doc["config"]["brightness"];
  displayBrightness(defaultBrightness);
}

void sendConfig()
{
  DynamicJsonDocument doc = getConfigJson();
  size_t strsize = measureJson(doc) + 1;
  char json[strsize];
  serializeJson(doc, json, strsize);
  ws.textAll(json);
}