#include <Arduino.h>
#include <Wire.h>
#ifdef FEATURE_ENABLE_OLED
#include <U8g2lib.h>
#endif
#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>

#include <FastLED.h>
#include <Ticker.h>

#include "common.h"

#include "sanitycheck.h"

#include "lubeapp.h"
#include "webui.h"
#include "config.h"
#include "globals.h"
#include "debugger.h"
#ifdef FEATURE_ENABLE_CAN
#include "can.h"
#endif
#ifdef FEATURE_ENABLE_GPS
#include "gps.h"
#endif
#include "dtc.h"

#ifdef FEATURE_ENABLE_WIFI_CLIENT
#include <ESP8266WiFiMulti.h>

const char *ssid = QUOTE(WIFI_SSID_CLIENT);
const char *password = QUOTE(WIFI_PASSWORD_CLIENT);
const uint32_t connectTimeoutMs = 5000;

ESP8266WiFiMulti wifiMulti;
#endif

bool startSetupMode = false;
volatile uint32_t wheel_pulse = 0;

CRGB leds[1];

// Function-Prototypes
void IRAM_ATTR trigger_ISR();
void LED_Process(uint8_t override = false, CRGB setColor = CRGB::White);
#ifdef FEATURE_ENABLE_OLED
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(-1);
void Display_Process();
#endif
void Button_Process();
void toggleWiFiAP(boolean shutdown = false);
void SystemShutdown();
uint32_t Process_Impulse_WheelSpeed();
void EEPROMCyclicPDS_callback();

#ifdef FEATURE_ENABLE_WIFI_CLIENT
void wifiMaintainConnectionTicker_callback();
Ticker WiFiMaintainConnectionTicker(wifiMaintainConnectionTicker_callback, 1000, 0, MILLIS);
#endif
Ticker EEPROMCyclicPDSTicker(EEPROMCyclicPDS_callback, 60000, 0, MILLIS);

void setup()
{
  system_update_cpu_freq(SYS_CPU_80MHZ);
  snprintf(globals.DeviceName, 32, HOST_NAME, ESP.getChipId());
  WiFi.persistent(false);

  ClearAllDTC(); // Init DTC-Storage

#ifdef FEATURE_ENABLE_WIFI_CLIENT
  WiFi.mode(WIFI_STA);
  WiFi.setHostname(globals.DeviceName);
  wifiMulti.addAP(QUOTE(WIFI_SSID_CLIENT), QUOTE(WIFI_PASSWORD_CLIENT));
  WiFiMaintainConnectionTicker.start();
#else
  WiFi.mode(WIFI_OFF);
#endif

  Serial.begin(115200);
  Serial.println("\n\nSouko's ChainLube Mk1");
  Serial.println(globals.DeviceName);

  InitEEPROM();
  GetConfig_EEPROM();
  GetPersistence_EEPROM();
#ifdef FEATURE_ENABLE_OLED
  u8x8.begin();
  u8x8.setFont(u8x8_font_chroma48medium8_r);
#endif
  FastLED.addLeds<WS2811, GPIO_LED, RGB>(leds, 1); // GRB ordering is assumed

  switch (LubeConfig.SpeedSource)
  {
  case SOURCE_IMPULSE:
    pinMode(GPIO_TRIGGER, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(GPIO_TRIGGER), trigger_ISR, FALLING);
    break;
#ifdef FEATURE_ENABLE_GPS
  case SOURCE_GPS:
    Init_GPS();
    break;
#endif
  case SOURCE_TIME:

    break;
#ifdef FEATURE_ENABLE_CAN
  case SOURCE_CAN:
    Init_CAN();
    break;
#endif
  default:
    Debug_pushMessage("Source Setting N/A");
    break;
  }

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

  ArduinoOTA.setPort(8266);
  ArduinoOTA.setHostname(globals.DeviceName);
  ArduinoOTA.setPassword(QUOTE(ADMIN_PASSWORD));
#ifdef FEATURE_ENABLE_OLED
  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(); });
#endif
  ArduinoOTA.begin();

#ifdef FEATURE_ENABLE_OLED
  u8x8.clearDisplay();
  u8x8.drawString(0, 0, "KTM ChainLube V1");
  u8x8.refreshDisplay();
#endif

  initWebUI();
  initGlobals();
  EEPROMCyclicPDSTicker.start();
  Serial.println("Setup Done");
}

void loop()
{
  uint32_t wheelDistance = 0;

  switch (LubeConfig.SpeedSource)
  {
  case SOURCE_IMPULSE:
    wheelDistance = Process_Impulse_WheelSpeed();
    break;
#ifdef FEATURE_ENABLE_CAN
  case SOURCE_CAN:
    wheelDistance = Process_CAN_WheelSpeed();
    break;
#endif
  case SOURCE_TIME:
    break;
#ifdef FEATURE_ENABLE_GPS
  case SOURCE_GPS:
    wheelDistance = Process_GPS_WheelSpeed();
    break;
#endif
  }

  RunLubeApp(wheelDistance);
#ifdef FEATURE_ENABLE_OLED
  Display_Process();
#endif
  Button_Process();
  LED_Process();
  EEPROM_Process();
  Webserver_Process();
  DTC_Process();

  ArduinoOTA.handle();
  EEPROMCyclicPDSTicker.update();
#ifdef FEATURE_ENABLE_WIFI_CLIENT
  WiFiMaintainConnectionTicker.update();
#endif
  if (globals.systemStatus == sysStat_Shutdown)
    SystemShutdown();
  yield();
}

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

#ifdef FEATURE_ENABLE_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
    {
      Debug_pushMessage("WiFi not connected! - Start AP");
      toggleWiFiAP();
    }
  }
}
#endif

void EEPROMCyclicPDS_callback()
{
  StorePersistence_EEPROM();
}

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_Startup;
  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;
      Debug_pushMessage("Override LED_Status");
    }
    LED_Status = LED_Override;
    LED_override_color = SetColor;
  }

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

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

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

    if (globals.TankPercentage < 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);
      Debug_pushMessage("LED_Status: Confirm -> Normal");
    }
    break;

  case LED_Normal:
#ifndef NO_MODE_FLASH
    if (timer % 2000 > 1950)
      leds[0] = CRGB(0, 255, 0);
    else if (WiFi.getMode() != WIFI_OFF && timer % 2000 > 1800 && timer % 2000 < 1850)
#else
    if (WiFi.getMode() != WIFI_OFF && timer % 2000 > 1950)
#endif
      leds[0] = CRGB(255, 128, 0);
    else
      leds[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);
      Debug_pushMessage("LED_Status: Confirm -> Rain");
    }
    break;

  case LED_Rain:
#ifndef NO_MODE_FLASH
    if (timer % 2000 > 1950)
      leds[0] = CRGB(0, 0, 255);
    else if (WiFi.getMode() != WIFI_OFF && timer % 2000 > 1800 && timer % 2000 < 1850)
#else
    if (WiFi.getMode() != WIFI_OFF && timer % 2000 > 1950)
#endif
      leds[0] = CRGB(255, 128, 0);
    else
      leds[0] = 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();
}
#ifdef FEATURE_ENABLE_OLED
void Display_Process()
{
  static tSystem_Status oldSysStatus = sysStat_Startup;

  if (oldSysStatus != globals.systemStatus)
  {
    u8x8.clearDisplay();
    u8x8.drawString(0, 0, "KTM ChainLube V1");
    oldSysStatus = globals.systemStatus;
  }

  u8x8.setCursor(0, 1);
  uint32_t DistRemain = globals.systemStatus == sysStat_Normal ? LubeConfig.DistancePerLube_Default : LubeConfig.DistancePerLube_Rain;
  DistRemain = DistRemain - (PersistenceData.TravelDistance_highRes_mm / 1000);
  u8x8.printf(PSTR("Mode: %10s\n"), globals.systemStatustxt);
  if (globals.systemStatus == sysStat_Error)
  {
    u8x8.printf(PSTR("last DTC: %6d\n"), getlastDTC(false));
  }
  else
  {
    u8x8.printf(PSTR("next Lube: %4dm\n"), DistRemain);
    u8x8.printf(PSTR("Tank: %8dml\n"), PersistenceData.tankRemain_microL / 1000);
    u8x8.printf(PSTR("WiFi: %10s\n"), (WiFi.getMode() == WIFI_AP ? "AP" : WiFi.getMode() == WIFI_OFF ? "OFF"
                                                                      : WiFi.getMode() == WIFI_STA   ? "CLIENT"
                                                                                                     : "UNKNOWN"));
    u8x8.printf(PSTR("Source: %8s\n"), SpeedSourceString[LubeConfig.SpeedSource]);
    u8x8.printf("%s\n", WiFi.localIP().toString().c_str());
  }
  u8x8.refreshDisplay();
}
#endif

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:
        toggleWiFiAP();
        Debug_pushMessage("Starting WiFi AP");
        break;

      case BTN_STARTPURGE:
        globals.systemStatus = sysStat_Purge;
        globals.purgePulses = LubeConfig.BleedingPulses;
        Debug_pushMessage("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;
        }
        Debug_pushMessage("Toggling Mode");
        break;

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

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

void SystemShutdown()
{
  static uint32_t shutdown_delay = 0;

  if (shutdown_delay == 0)
  {
    shutdown_delay = millis() + SHUTDOWN_DELAY_MS;
    Serial.printf("Shutdown requested - Restarting in %d seconds\n", SHUTDOWN_DELAY_MS / 1000);
  }
  if (shutdown_delay < millis())
  {
    StoreConfig_EEPROM();
    StorePersistence_EEPROM();
    ESP.restart();
  }
}

uint32_t Process_Impulse_WheelSpeed()
{
  uint32_t add_milimeters;
  // Calculate traveled Distance in mm
  add_milimeters = (wheel_pulse * (LubeConfig.DistancePerRevolution_mm / LubeConfig.PulsePerRevolution));
  wheel_pulse = 0;

  return add_milimeters;
}

