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

#include <Adafruit_NeoPixel.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"
#include "led_colors.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;

Adafruit_NeoPixel leds(1, GPIO_LED, NEO_RGB + NEO_KHZ800);

// Function-Prototypes
void IRAM_ATTR trigger_ISR();
void LED_Process(uint8_t override = false, uint32_t setColor = LED_DEFAULT_COLOR);
#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.print("\n\nSouko's ChainLube Mk1\n");
  Serial.print(globals.DeviceName);

#ifdef FEATURE_ENABLE_OLED
  u8x8.begin();
  u8x8.setFont(u8x8_font_chroma48medium8_r);
  u8x8.clearDisplay();
  u8x8.drawString(0, 0, "KTM ChainLube V1");
  u8x8.refreshDisplay();
  Serial.print("\nDisplay-Init done");
#endif

  InitEEPROM();
  GetConfig_EEPROM();
  GetPersistence_EEPROM();
  Serial.print("\nEE-Init done");

  leds.begin();
  Serial.print("\nLED-Init done");

  pinMode(GPIO_TRIGGER, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(GPIO_TRIGGER), trigger_ISR, FALLING);
  Serial.print("\nPulse-Input Init done");
#ifdef FEATURE_ENABLE_GPS
  Init_GPS();
  Serial.print("\nGPS-Init done");
#endif
#ifdef FEATURE_ENABLE_CAN
  if (LubeConfig.SpeedSource != SOURCE_IMPULSE)
  {
    Init_CAN();
    Serial.print("\nCAN-Init done");
  }
#endif

  Serial.print("\nSource-Init done");
  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();
  Serial.print("\nOTA-Init done");
  initWebUI();
  Serial.print("\nWebUI-Init done");
  initGlobals();
  Serial.print("\nglobals-Init done");
  EEPROMCyclicPDSTicker.start();
  Serial.print("\nSetup Done\n");
}

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
#ifdef FEATURE_ENABLE_TIMER
  case SOURCE_TIME:
    break;
#endif
#ifdef FEATURE_ENABLE_GPS
  case SOURCE_GPS:
    wheelDistance = Process_GPS_WheelSpeed();
    break;
#endif
  }

  RunLubeApp(wheelDistance);
#ifdef FEATURE_ENABLE_OLED
  Display_Process();
#endif
#ifdef FEATURE_ENABLE_CAN
  if (LubeConfig.SpeedSource != SOURCE_IMPULSE)
  {
    CAN_Process();
  }
#endif
  Button_Process();
  LED_Process();
  EEPROM_Process();
  Webserver_Process();
  DTC_Process();
  Debug_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, uint32_t SetColor)
{
  typedef enum
  {
    LED_Startup,
    LED_Normal,
    LED_Confirm_Normal,
    LED_Rain,
    LED_Confirm_Rain,
    LED_Purge,
    LED_Error,
    LED_Shutdown,
    LED_Override
  } tLED_Status;

  static tSystem_Status oldSysStatus = sysStat_Startup;
  static tLED_Status LED_Status = LED_Startup;
  static uint32_t LED_override_color = 0;
  static tLED_Status LED_ResumeOverrideStatus = LED_Startup;

  uint8_t color = 0;
  uint32_t timer = 0;
  uint32_t animtimer = 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\n");
    }
    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\n");
    }
  }

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

  switch (LED_Status)
  {
  case LED_Startup:
    leds.setBrightness(LubeConfig.LED_Max_Brightness);

    if (globals.TankPercentage < LubeConfig.TankRemindAtPercentage)
      leds.setPixelColor(0, LED_STARTUP_TANKWARN);
    else
      leds.setPixelColor(0, LED_STARTUP_NORMAL);
    break;

  case LED_Confirm_Normal:
    animtimer = timer % 500;
    color = map(animtimer / 2, 0, 250, 0, LubeConfig.LED_Max_Brightness);
    leds.setPixelColor(0, LED_NORMAL_COLOR);
    if (animtimer < 250)
      leds.setBrightness(color);
    else
      leds.setBrightness(LubeConfig.LED_Max_Brightness - color);

    if (timestamp < timer)
    {
      LED_Status = LED_Normal;
      Debug_pushMessage("LED_Status: Confirm -> Normal\n");
    }
    break;

  case LED_Normal:
    leds.setBrightness(LubeConfig.LED_Min_Brightness);
    leds.setPixelColor(0, LED_NORMAL_COLOR);

    if (timer % 2000 > 1950 && LubeConfig.LED_Mode_Flash == true)
      leds.setBrightness(LubeConfig.LED_Max_Brightness);
    else if (timer % 2000 > 1500 && WiFi.getMode() != WIFI_OFF)
      leds.setPixelColor(0, LED_WIFI_BLINK);

    break;

  case LED_Confirm_Rain:
    animtimer = timer % 500;
    color = map(animtimer / 2, 0, 250, 0, LubeConfig.LED_Max_Brightness);
    leds.setPixelColor(0, LED_RAIN_COLOR);
    if (animtimer < 250)
      leds.setBrightness(color);
    else
      leds.setBrightness(LubeConfig.LED_Max_Brightness - color);
    if (timestamp < timer)
    {
      LED_Status = LED_Rain;
      Debug_pushMessage("LED_Status: Confirm -> Rain\n");
    }
    break;

  case LED_Rain:
    leds.setBrightness(LubeConfig.LED_Min_Brightness);
    leds.setPixelColor(0, LED_RAIN_COLOR);

    if (timer % 2000 > 1950 && LubeConfig.LED_Mode_Flash == true)
      leds.setBrightness(LubeConfig.LED_Max_Brightness);
    else if (timer % 2000 > 1500 && WiFi.getMode() != WIFI_OFF)
      leds.setPixelColor(0, LED_WIFI_BLINK);

    break;

  case LED_Purge:
    timer = timer % 500;
    color = map(timer / 2, 0, 250, LubeConfig.LED_Min_Brightness, LubeConfig.LED_Max_Brightness);
    leds.setPixelColor(0, LED_PURGE_COLOR);
    if (timer < 250)
      leds.setBrightness(color);
    else
      leds.setBrightness(LubeConfig.LED_Max_Brightness - color);
    break;

  case LED_Error:
    leds.setBrightness(LubeConfig.LED_Max_Brightness);
    leds.setPixelColor(0, timer % 500 > 250 ? LED_ERROR_BLINK : 0);
    break;

  case LED_Shutdown:
    timer = timer % 600;
    leds.setPixelColor(0, LED_SHUTDOWN_BLINK);
    if (timer < 500)
    {
      color = map(timer, 0, 500, LubeConfig.LED_Max_Brightness, LubeConfig.LED_Min_Brightness);
      leds.setBrightness(color);
    }
    else
    {
      leds.setBrightness(LubeConfig.LED_Min_Brightness);
    }
    break;

  case LED_Override:
    leds.setBrightness(LubeConfig.LED_Max_Brightness);
    leds.setPixelColor(0, LED_override_color);
    break;

  default:
    break;
  }
  leds.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, COLOR_WARM_WHITE);
      buttonAction = BTN_NOTHING;
    }
    else if (buttonTimestamp + BUTTON_ACTION_DELAY_WIFI < millis())
    {
      LED_Process(1, LED_WIFI_BLINK);
      buttonAction = BTN_TOGGLEWIFI;
    }
    else if (buttonTimestamp + BUTTON_ACTION_DELAY_PURGE < millis())
    {
      LED_Process(1, LED_PURGE_COLOR);
      buttonAction = BTN_STARTPURGE;
    }
    else if (buttonTimestamp + BUTTON_ACTION_DELAY_TOGGLEMODE < millis())
    {
      uint32_t color = globals.systemStatus == sysStat_Normal ? LED_RAIN_COLOR : LED_NORMAL_COLOR;
      LED_Process(1, color);
      buttonAction = BTN_TOGGLEMODE;
    }
  }
  else
  {
    if (buttonAction != BTN_INACTIVE)
    {
      switch (buttonAction)
      {
      case BTN_TOGGLEWIFI:
        toggleWiFiAP();
        Debug_pushMessage("Starting WiFi AP\n");
        break;

      case BTN_STARTPURGE:
        globals.systemStatus = sysStat_Purge;
        globals.purgePulses = LubeConfig.BleedingPulses;
        Debug_pushMessage("Starting Purge\n");
        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\n");
        break;

      case BTN_NOTHING:
      default:
        Debug_pushMessage("Nothing or invalid\n");
        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\n");
#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\n");
#else
    Debug_pushMessage("WiFi AP started\n");
#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 = 0;
  // Calculate traveled Distance in mm
  if (LubeConfig.PulsePerRevolution != 0)
    add_milimeters = (wheel_pulse * (LubeConfig.DistancePerRevolution_mm / LubeConfig.PulsePerRevolution));

  if (globals.measurementActive == true)
    globals.measuredPulses = globals.measuredPulses + wheel_pulse;

  wheel_pulse = 0;

  return add_milimeters;
}