#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <RemoteDebug.h>
#include <FastLED.h>
#include <Ticker.h>

#include "common.h"
#include "rmtdbghelp.h"
#include "lubeapp.h"
#include "webui.h"
#include "config.h"
#include "globals.h"

#ifdef WIFI_CLIENT
#include <ESP8266WiFiMulti.h>

const char *ssid = QUOTE(WIFI_SSID);
const char *password = QUOTE(WIFI_PASSWORD);
const uint32_t connectTimeoutMs = 5000;

ESP8266WiFiMulti wifiMulti;

#endif

#ifdef DEBUG
const bool debug_flag = true;
#else
const bool debug_flag = false;
#endif

bool startSetupMode = false;
char DeviceName[33];

Globals_t globals;
uint32_t TravelDistance_highRes;
volatile uint32_t wheel_pulse = 0;

U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(-1);
RemoteDebug Debug;
CRGB leds[1];

// Function-Prototypes
String IpAddress2String(const IPAddress &ipAddress);
void processCmdRemoteDebug();
void RemotDebug_printSystemInfo();
void RemoteDebug_printWifiInfo();
void updateWebUITicker_callback();
void IRAM_ATTR trigger_ISR();
void LED_Process(uint8_t override = false, CRGB setColor = CRGB::White);
void Display_Process();
void Button_Process();
void startWiFiAP();

#ifdef WIFI_CLIENT
void wifiMaintainConnectionTicker_callback();
Ticker WiFiMaintainConnectionTicker(wifiMaintainConnectionTicker_callback, 1000, 0, MILLIS);
#endif
Ticker UpdateWebUITicker(updateWebUITicker_callback, 5000, 0, MILLIS);

void setup()
{
  system_update_cpu_freq(SYS_CPU_80MHZ);

  snprintf(DeviceName, 32, HOST_NAME, ESP.getChipId());

  WiFi.persistent(false);

#ifdef WIFI_CLIENT
  WiFi.mode(WIFI_STA);
  WiFi.setHostname(DeviceName);
  wifiMulti.addAP(QUOTE(WIFI_SSID), QUOTE(WIFI_PASSWORD));
  WiFiMaintainConnectionTicker.start();
#else
  WiFi.mode(WIFI_OFF);
#endif

  Serial.begin(115200);
  Serial.setDebugOutput(true);

  Serial.println("Souko's ChainLube Mk1");
  Serial.println(DeviceName);

  GetConfig_EEPROM();

  u8x8.begin();
  u8x8.setFont(u8x8_font_chroma48medium8_r);

  FastLED.addLeds<WS2811, GPIO_LED, GRB>(leds, 1); // GRB ordering is assumed

  pinMode(GPIO_TRIGGER, INPUT_PULLUP);
  pinMode(GPIO_BUTTON, INPUT_PULLUP);
  pinMode(GPIO_PUMP, OUTPUT);

  attachInterrupt(digitalPinToInterrupt(GPIO_TRIGGER), trigger_ISR, FALLING);

  if (MDNS.begin(DeviceName))
    MDNS.addService("telnet", "tcp", 23);

  Debug.begin(DeviceName);        // Initialize the WiFi server
  Debug.setResetCmdEnabled(true); // Enable the reset command
  Debug.showProfiler(true);       // Profiler (Good to measure times, to optimize codes)
  Debug.showColors(true);         // Colors
  Debug.setPassword(QUOTE(ADMIN_PASSWORD));

  Debug.setHelpProjectsCmds(helpCmd);
  Debug.setCallBackProjectCmds(&processCmdRemoteDebug);

  ArduinoOTA.setPort(8266);
  ArduinoOTA.setHostname(DeviceName);
  ArduinoOTA.setPassword(QUOTE(ADMIN_PASSWORD));

  ArduinoOTA.onStart([]()
                     {
                       u8x8.clearDisplay();
                       u8x8.drawString(0, 0, "OTA-Update");
                       u8x8.refreshDisplay(); });

  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total)
                        {
                          static bool refreshed = false;
                          if (!refreshed)
                          {
                            u8x8.clearDisplay();
                            refreshed = true;
                            u8x8.drawString(0, 0, "OTA Upload");
                          }
                          uint32_t percent = progress / (total / 100);
                          u8x8.setCursor(0, 1);
                          u8x8.printf("%d %%", percent);
                          u8x8.refreshDisplay(); });

  ArduinoOTA.onEnd([]()
                   {
                     u8x8.clearDisplay();
                     u8x8.drawString(0, 0, "OTA-Restart");
                     u8x8.refreshDisplay(); });

  ArduinoOTA.begin();

  u8x8.clearDisplay();
  u8x8.drawString(4, 0, "Souko's");
  u8x8.drawString(1, 1, "ChainLube Mk1");
  u8x8.refreshDisplay();

  initWebUI();
  UpdateWebUITicker.start();
}

void loop()
{
  RunLubeApp(&wheel_pulse);
  UpdateWebUITicker.update();

  Display_Process();
  Button_Process();
  LED_Process();

  ArduinoOTA.handle();
  Debug.handle();
#ifdef WIFI_CLIENT
  WiFiMaintainConnectionTicker.update();
#endif
  yield();
}

String IpAddress2String(const IPAddress &ipAddress)
{
  return String(ipAddress[0]) + String(".") +
         String(ipAddress[1]) + String(".") +
         String(ipAddress[2]) + String(".") +
         String(ipAddress[3]);
}

void processCmdRemoteDebug()
{
  String lastCmd = Debug.getLastCommand();

  if (lastCmd == "sysinfo")
    RemotDebug_printSystemInfo();
  else if (lastCmd == "netinfo")
    RemoteDebug_printWifiInfo();
}

void RemotDebug_printSystemInfo()
{
  debugA("Souko's ChainOiler Mk1");
  debugA("Hostname: %s", DeviceName);

  FlashMode_t ideMode = ESP.getFlashChipMode();
  debugA("Sdk version: %s", ESP.getSdkVersion());
  debugA("Core Version: %s", ESP.getCoreVersion().c_str());
  debugA("Boot Version: %u", ESP.getBootVersion());
  debugA("Boot Mode: %u", ESP.getBootMode());
  debugA("CPU Frequency: %u MHz", ESP.getCpuFreqMHz());
  debugA("Reset reason: %s", ESP.getResetReason().c_str());
  debugA("Flash Size: %d", ESP.getFlashChipRealSize());
  debugA("Flash Size IDE: %d", ESP.getFlashChipSize());
  debugA("Flash ide mode:  %s", (ideMode == FM_QIO ? "QIO" : ideMode == FM_QOUT ? "QOUT"
                                                         : ideMode == FM_DIO    ? "DIO"
                                                         : ideMode == FM_DOUT   ? "DOUT"
                                                                                : "UNKNOWN"));
  debugA("OTA-Pass: %s", QUOTE(ADMIN_PASSWORD));
}

void RemoteDebug_printWifiInfo()
{
}

#ifdef WIFI_CLIENT
void wifiMaintainConnectionTicker_callback()
{
  static uint32_t WiFiFailCount = 0;
  const uint32_t WiFiFailMax = 20;

  if (wifiMulti.run(connectTimeoutMs) == WL_CONNECTED)
  {
    return;
  }
  else
  {
    if (WiFiFailCount < WiFiFailMax)
    {
      WiFiFailCount++;
    }
    else
    {
      debugV("WiFi not connected! - Start AP");
      startWiFiAP();
    }
  }
}
#endif

void updateWebUITicker_callback()
{
  UpdateWebUI();
}

void trigger_ISR()
{
  wheel_pulse++;
}

void LED_Process(uint8_t override, CRGB SetColor)
{

  typedef enum
  {
    LED_Startup,
    LED_Normal,
    LED_Confirm_Normal,
    LED_Rain,
    LED_Confirm_Rain,
    LED_Purge,
    LED_Error,
    LED_Override
  } tLED_Status;

  static tSystem_Status oldSysStatus = sysStat_NOP;
  static tLED_Status LED_Status = LED_Startup;
  static CRGB LED_override_color = 0;
  static tLED_Status LED_ResumeOverrideStatus = LED_Startup;

  uint8_t color = 0;
  uint32_t timer = 0;
  static uint32_t timestamp = 0;
  timer = millis();

  if (override == 1)
  {
    if (LED_Status != LED_Override)
    {
      LED_ResumeOverrideStatus = LED_Status;
      debugV("Override LED_Status");
    }
    LED_Status = LED_Override;
    LED_override_color = SetColor;
  }

  if (override == 2)
  {
    if (LED_Status == LED_Override)
    {
      LED_Status = LED_ResumeOverrideStatus;
      debugV("Resume LED_Status");
    }
  }

  if (oldSysStatus != globals.systemStatus)
  {
    switch (globals.systemStatus)
    {
    case sysStat_Startup:
      LED_Status = LED_Startup;
      debugV("sysStat: Startup");
      break;
    case sysStat_Normal:
      timestamp = timer + 3500;
      LED_Status = LED_Confirm_Normal;
      debugV("sysStat: Normal");
      break;
    case sysStat_Rain:
      timestamp = timer + 3500;
      LED_Status = LED_Confirm_Rain;
      debugV("sysStat: Rain");
      break;
    case sysStat_Purge:
      LED_Status = LED_Purge;
      debugV("sysStat: Purge");
      break;
    case sysStat_Error:
      LED_Status = LED_Error;
      debugV("sysStat: Error");
      break;
    case sysStat_NOP:
    default:
      break;
    }
    oldSysStatus = globals.systemStatus;
  }

  uint32_t percentage = LubeConfig.tankRemain_µl / (LubeConfig.tankCapacity_ml * 10);

  switch (LED_Status)
  {
  case LED_Startup:
    FastLED.setBrightness(255);

    if (percentage < LubeConfig.TankRemindAtPercentage)
      leds[0] = CRGB::OrangeRed;
    else
      leds[0] = CRGB::White;
    break;

  case LED_Confirm_Normal:
    FastLED.setBrightness(255);
    leds[0] = timer % 250 > 125 ? CRGB(0, 255, 0) : CRGB(0, 4, 0);
    if (timestamp < timer)
    {
      LED_Status = LED_Normal;
      FastLED.setBrightness(64);
      debugV("LED_Status: Confirm -> Normal");
    }
    break;

  case LED_Normal:
    leds[0] = timer % 2000 > 1950 ? CRGB(0, 255, 0) : CRGB(0, 4, 0);
    break;

  case LED_Confirm_Rain:
    FastLED.setBrightness(255);
    leds[0] = timer % 250 > 125 ? CRGB(0, 0, 255) : CRGB(0, 0, 4);
    if (timestamp < timer)
    {
      LED_Status = LED_Rain;
      FastLED.setBrightness(64);
      debugV("LED_Status: Confirm -> Rain");
    }
    break;

  case LED_Rain:
    leds[0] = timer % 2000 > 1950 ? CRGB(0, 0, 255) : CRGB(0, 0, 4);
    break;

  case LED_Purge:
    timer = timer % 500;
    color = timer / 2;
    leds[0] = CRGB::DeepPink;
    if (timer < 250)
      FastLED.setBrightness(color);
    else
      FastLED.setBrightness(250 - color);
    break;

  case LED_Error:
    leds[0] = timer % 500 > 250 ? CRGB::Red : CRGB::Black;
    break;

  case LED_Override:
    leds[0] = LED_override_color;
    break;

  default:
    break;
  }
  FastLED.show();
}

void Display_Process()
{
  u8x8.setCursor(0, 3);
  uint32_t DistRemain = globals.systemStatus == sysStat_Normal ? LubeConfig.DistancePerLube_Default : LubeConfig.DistancePerLube_Rain;
  DistRemain -= TravelDistance_highRes / 1000;
  u8x8.printf("Mode: %10s\n\n", globals.systemStatustxt);
  u8x8.printf("next Lube: %4dm\n\n", DistRemain);
  u8x8.printf("Tank: %8dml\n\n", LubeConfig.tankRemain_µl / 1000);
  u8x8.refreshDisplay();
}

void Button_Process()
{

#define BUTTON_ACTION_DELAY_TOGGLEMODE 500
#define BUTTON_ACTION_DELAY_PURGE 3500
#define BUTTON_ACTION_DELAY_WIFI 6500
#define BUTTON_ACTION_DELAY_NOTHING 9500

  typedef enum buttonAction_e
  {
    BTN_INACTIVE,
    BTN_NOTHING,
    BTN_TOGGLEMODE,
    BTN_TOGGLEWIFI,
    BTN_STARTPURGE
  } buttonAction_t;

  static uint32_t buttonTimestamp = 0;
  static buttonAction_t buttonAction = BTN_INACTIVE;

  if (digitalRead(GPIO_BUTTON) == LOW)
  {

    if (buttonTimestamp == 0)
      buttonTimestamp = millis();

    if (buttonTimestamp + BUTTON_ACTION_DELAY_NOTHING < millis())
    {
      LED_Process(1, CRGB::White);
      buttonAction = BTN_NOTHING;
    }
    else if (buttonTimestamp + BUTTON_ACTION_DELAY_WIFI < millis())
    {
      LED_Process(1, CRGB::Yellow);
      buttonAction = BTN_TOGGLEWIFI;
    }
    else if (buttonTimestamp + BUTTON_ACTION_DELAY_PURGE < millis())
    {
      LED_Process(1, CRGB::DeepPink);
      buttonAction = BTN_STARTPURGE;
    }
    else if (buttonTimestamp + BUTTON_ACTION_DELAY_TOGGLEMODE < millis())
    {
      CRGB color = globals.systemStatus == sysStat_Normal ? CRGB::Blue : CRGB::Green;
      LED_Process(1, color);
      buttonAction = BTN_TOGGLEMODE;
    }
  }
  else
  {
    if (buttonAction != BTN_INACTIVE)
    {
      switch (buttonAction)
      {
      case BTN_TOGGLEWIFI:
        startWiFiAP();
        debugV("Starting WiFi AP");
        break;

      case BTN_STARTPURGE:
        globals.systemStatus = sysStat_Purge;
        globals.purgePulses = LubeConfig.BleedingPulses;
        debugV("Starting Purge");
        break;

      case BTN_TOGGLEMODE:
        switch (globals.systemStatus)
        {
        case sysStat_Normal:
          globals.systemStatus = sysStat_Rain;
          globals.resumeStatus = sysStat_Rain;
          break;

        case sysStat_Rain:
          globals.systemStatus = sysStat_Normal;
          globals.resumeStatus = sysStat_Normal;
          break;
        default:
          break;
        }
        debugV("Toggling Mode");
        break;

      case BTN_NOTHING:
      default:
        debugV("Nothing or invalid");
        break;
      }
      LED_Process(2);
    }
    buttonAction = BTN_INACTIVE;
    buttonTimestamp = 0;
  }
}

void startWiFiAP()
{
  if (WiFi.getMode() != WIFI_OFF)
  {
    WiFi.mode(WIFI_OFF);
    debugV("WiFi turned off");
  }
  else
  {
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(IPAddress(WIFI_AP_IP_GW), IPAddress(WIFI_AP_IP_GW), IPAddress(255, 255, 255, 0));
    WiFi.softAP(DeviceName, QUOTE(WIFI_AP_PASSWORD));
#ifdef WIFI_CLIENT
    WiFiMaintainConnectionTicker.stop();
    debugV("WiFi AP started, stopped Maintain-Timer");
#else
    debugV("WiFi AP started");
#endif
  }
}