/**
 * @file main.cpp
 *
 * @brief Main source file for the Souko's ChainLube Mk1 ESP8266 project.
 *
 * This file includes necessary libraries, defines configuration options, and declares global variables
 * and function prototypes. It sets up essential components, initializes peripherals, and defines
 * callbacks for interrupt service routines (ISRs) and timers. The main setup function configures the
 * project, and the loop function handles the main execution loop, performing various tasks based on
 * the configured options.
 *
 * @author Marcel Peterkau
 * @date   09.01.2024
 */

#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"
#include "can.h"
#include "gps.h"
#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(bool shutdown = false);
void SystemShutdown(bool restart = false);
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);

/**
 * @brief Initializes the ESP8266 project, configuring various components and setting up required services.
 *
 * This setup function is responsible for initializing the ESP8266 project, including setting the CPU frequency,
 * configuring WiFi settings, initializing DTC storage, handling WiFi client functionality (if enabled),
 * initializing the Serial communication, setting up an OLED display (if enabled), initializing EEPROM,
 * loading configuration and persistence data from EEPROM, initializing LEDs, setting up the chosen speed source
 * (CAN, GPS, Impulse), configuring GPIO pins, setting up Over-The-Air (OTA) updates, initializing the web user interface,
 * initializing global variables, starting cyclic EEPROM updates for Persistence Data Structure (PDS), and printing
 * initialization status messages to Serial.
 */
void setup()
{
  // Set CPU frequency to 80MHz
  system_update_cpu_freq(SYS_CPU_80MHZ);

  // Generate a unique device name based on ESP chip ID
  snprintf(globals.DeviceName, 32, HOST_NAME, ESP.getChipId());

  // Disable WiFi persistent storage
  WiFi.persistent(false);

  // Initialize and clear Diagnostic Trouble Code (DTC) storage
  ClearAllDTC();

#ifdef FEATURE_ENABLE_WIFI_CLIENT
  // Configure WiFi settings for client mode if enabled
  WiFi.mode(WIFI_STA);
  WiFi.setHostname(globals.DeviceName);
  wifiMulti.addAP(QUOTE(WIFI_SSID_CLIENT), QUOTE(WIFI_PASSWORD_CLIENT));
  WiFiMaintainConnectionTicker.start();
#else
  // Disable WiFi if WiFi client feature is not enabled
  WiFi.mode(WIFI_OFF);
#endif

  // Initialize Serial communication
  Serial.begin(115200);
  Serial.print("\n\nSouko's ChainLube Mk1\n");
  Serial.print(globals.DeviceName);

#ifdef FEATURE_ENABLE_OLED
  // Initialize OLED display if enabled
  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

  // Initialize EEPROM, load configuration, and persistence data from EEPROM
  InitEEPROM();
  GetConfig_EEPROM();
  GetPersistence_EEPROM();
  Serial.print("\nEE-Init done");

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

  // Initialize based on the chosen speed source (CAN, GPS, Impulse)
  switch (LubeConfig.SpeedSource)
  {
  case SOURCE_CAN:
    Init_CAN();
    Serial.print("\nCAN-Init done");
    break;
  case SOURCE_GPS:
    Init_GPS();
    Serial.print("\nGPS-Init done");
    break;
  case SOURCE_IMPULSE:
    pinMode(GPIO_TRIGGER, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(GPIO_TRIGGER), trigger_ISR, FALLING);
    Serial.print("\nPulse-Input Init done");
    break;
  default:
    break;
  }

  Serial.print("\nSource-Init done");

  // Configure GPIO pins for button and pump control
  pinMode(GPIO_BUTTON, INPUT_PULLUP);
  pinMode(GPIO_PUMP, OUTPUT);

  // Set up OTA updates
  ArduinoOTA.setPort(8266);
  ArduinoOTA.setHostname(globals.DeviceName);
  ArduinoOTA.setPassword(QUOTE(ADMIN_PASSWORD));

#ifdef FEATURE_ENABLE_OLED
  // Set up OTA callbacks for OLED display if enabled
  ArduinoOTA.onStart([]()
                     {
                       u8x8.clearDisplay();
                       u8x8.drawString(0, 6, "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, 6, "OTA Upload");
                          }
                          uint32_t percent = progress / (total / 100);
                          u8x8.setCursor(0, 7);
                          u8x8.printf("%d %%", percent);
                          u8x8.refreshDisplay(); });

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

  // Begin OTA updates
  ArduinoOTA.begin();
  Serial.print("\nOTA-Init done");

  // Initialize the web user interface
  initWebUI();
  Serial.print("\nWebUI-Init done");

  // Initialize global variables
  initGlobals();
  Serial.print("\nglobals-Init done");

  // Start cyclic EEPROM updates for Persistence Data Structure (PDS)
  EEPROMCyclicPDSTicker.start();
  Serial.print("\nSetup Done\n");
}

/**
 * @brief Main execution loop for the ESP8266 project, performing various tasks based on configuration.
 *
 * This loop function handles different tasks based on the configured source of speed data (impulse, CAN, time, GPS).
 * It calculates wheel distance, runs the lubrication application, updates the OLED display (if enabled),
 * processes CAN messages, handles button input, manages LED behavior, performs EEPROM-related tasks, handles
 * webserver operations, processes Diagnostic Trouble Codes (DTC), and manages debugging. Additionally, it
 * integrates functionalities such as Over-The-Air (OTA) updates, cyclic EEPROM updates for Persistence Data
 * Structure (PDS), WiFi connection maintenance, and system shutdown handling.
 */
void loop()
{
  // Variable to store calculated wheel distance
  uint32_t wheelDistance = 0;

  // Switch based on the configured speed source
  switch (LubeConfig.SpeedSource)
  {
  case SOURCE_IMPULSE:
    wheelDistance = Process_Impulse_WheelSpeed();
    break;
  case SOURCE_CAN:
    wheelDistance = Process_CAN_WheelSpeed();
    break;

#ifdef FEATURE_ENABLE_TIMER
  case SOURCE_TIME:
    break;
#endif
  case SOURCE_GPS:
    wheelDistance = Process_GPS_WheelSpeed();
    break;
  }

  // Run lubrication application with the calculated wheel distance
  RunLubeApp(wheelDistance);

#ifdef FEATURE_ENABLE_OLED
  // Update OLED display if enabled
  Display_Process();
#endif

  // Process CAN messages if the speed source is not impulse
  if (LubeConfig.SpeedSource != SOURCE_IMPULSE)
  {
    CAN_Process();
  }

  // Process button input, manage LED behavior, perform EEPROM tasks, handle webserver operations,
  // process Diagnostic Trouble Codes (DTC), and manage debugging
  Button_Process();
  LED_Process();
  EEPROM_Process();
  Webserver_Process();
  DTC_Process();
  Debug_Process();

  // Handle OTA updates and update cyclic EEPROM tasks for Persistence Data Structure (PDS)
  ArduinoOTA.handle();
  EEPROMCyclicPDSTicker.update();

#ifdef FEATURE_ENABLE_WIFI_CLIENT
  // Update WiFi connection maintenance ticker if WiFi client feature is enabled
  WiFiMaintainConnectionTicker.update();
#endif

  // Perform system shutdown if the status is set to shutdown
  if (globals.systemStatus == sysStat_Shutdown)
    SystemShutdown(false);

  // Yield to allow other tasks to run
  yield();
}

/**
 * @brief Converts an IPAddress object to a String representation.
 *
 * This function takes an IPAddress object and converts it into a String representing
 * the IPv4 address. Each octet of the address is separated by a dot.
 *
 * @param ipAddress The IPAddress object to be converted.
 * @return A String representing the IPv4 address.
 */
String IpAddress2String(const IPAddress &ipAddress)
{
  // Concatenate each octet of the IPAddress with dots in between
  return String(ipAddress[0]) + String(".") +
         String(ipAddress[1]) + String(".") +
         String(ipAddress[2]) + String(".") +
         String(ipAddress[3]);
}

#ifdef FEATURE_ENABLE_WIFI_CLIENT
/**
 * @brief Callback function for maintaining WiFi connection and handling connection failures.
 *
 * This callback function is used by a ticker to periodically check the WiFi connection status.
 * If the device is not connected to WiFi, it counts connection failures. If the number of failures
 * exceeds a defined threshold, the function triggers the initiation of an Access Point (AP) mode
 * using the `toggleWiFiAP` function.
 */
void wifiMaintainConnectionTicker_callback()
{
  // Static variables to track WiFi connection failure count and maximum allowed failures
  static uint32_t WiFiFailCount = 0;
  const uint32_t WiFiFailMax = 20;

  // Check if the device is connected to WiFi
  if (wifiMulti.run(connectTimeoutMs) == WL_CONNECTED)
  {
    return; // Exit if connected
  }
  else
  {
    // Increment WiFi connection failure count
    if (WiFiFailCount < WiFiFailMax)
    {
      WiFiFailCount++;
    }
    else
    {
      // Trigger AP mode if the maximum failures are reached
      Debug_pushMessage("WiFi not connected! - Start AP\n");
      toggleWiFiAP();
    }
  }
}

#endif

/**
 * @brief Callback function for cyclically storing Persistence Data Structure (PDS) to EEPROM.
 *
 * This callback function is invoked periodically to store the Persistence Data Structure (PDS)
 * to the EEPROM. It ensures that essential data is saved persistently, allowing the system to
 * recover its state after power cycles or resets.
 */
void EEPROMCyclicPDS_callback()
{
  StorePersistence_EEPROM();
}

/**
 * @brief Interrupt Service Routine (ISR) triggered by wheel speed sensor pulses.
 *
 * This ISR is called whenever a pulse is detected from the wheel speed sensor. It increments
 * the `wheel_pulse` variable, which is used to track the number of pulses received.
 */
void trigger_ISR()
{
  wheel_pulse++;
}

/**
 * @brief Manages LED behavior based on the current system status and user overrides.
 *
 * This function handles LED behavior, including startup animations, confirmation animations for
 * normal and rain modes, indication for purge, error, shutdown, and normal operation. It supports
 * user overrides to set a specific LED color. The LED status is determined by the current system
 * status, and specific LED patterns are displayed accordingly.
 *
 * @param override Flag indicating whether to override the LED behavior (0: No override, 1: Override, 2: Resume previous state).
 * @param SetColor The color to set when overriding the LED behavior.
 */
void LED_Process(uint8_t override, uint32_t SetColor)
{
  // Enumeration to represent LED status
  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 variables to track LED status, system status, override color, and previous LED 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;

  // Variables for managing LED animation timing
  uint8_t color = 0;
  uint32_t timer = 0;
  uint32_t animtimer = 0;
  static uint32_t timestamp = 0;
  timer = millis();

  // Handle LED overrides
  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");
    }
  }

  // Update LED status when system status changes
  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;
  }

  // Handle different LED statuses
  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
/**
 * @brief Manages the display content based on the current system status and updates the OLED display.
 *
 * This function handles the content to be displayed on the OLED screen, taking into account the
 * current system status. It clears the display and prints relevant information such as system mode,
 * remaining lubrication distance, tank level, WiFi status, speed source, and IP address. Additionally,
 * it refreshes the OLED display with the updated content.
 */
void Display_Process()
{
  // Static variable to track the previous system status
  static tSystem_Status oldSysStatus = sysStat_Startup;

  // Check if the system status has changed since the last update
  if (oldSysStatus != globals.systemStatus)
  {
    // Clear the display and print the system title when the status changes
    u8x8.clearDisplay();
    u8x8.drawString(0, 0, "KTM ChainLube V1");
    oldSysStatus = globals.systemStatus;
  }

  // Set the cursor position for displaying information on the OLED screen
  u8x8.setCursor(0, 1);

  // Calculate remaining lubrication distance based on system mode
  uint32_t DistRemain = globals.systemStatus == sysStat_Normal ? LubeConfig.DistancePerLube_Default : LubeConfig.DistancePerLube_Rain;
  DistRemain = DistRemain - (PersistenceData.TravelDistance_highRes_mm / 1000);

  // Display relevant information on the OLED screen based on system status
  u8x8.printf(PSTR("Mode: %10s\n"), globals.systemStatustxt);
  if (globals.systemStatus == sysStat_Error)
  {
    // Display the last Diagnostic Trouble Code (DTC) in case of an error
    u8x8.printf(PSTR("last DTC: %6d\n"), getlastDTC(false));
  }
  else
  {
    // Display information such as next lubrication distance, tank level, WiFi status, speed source, and IP address
    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());
  }

  // Refresh the OLED display with the updated content
  u8x8.refreshDisplay();
}
#endif

/**
 * @brief Processes the button input and performs corresponding actions based on button state and timing.
 *
 * This function handles the button input, detecting button presses and executing actions based on
 * predefined time delays. Actions include toggling WiFi, starting purge, toggling operating modes,
 * and displaying feedback through LEDs. The function utilizes an enumeration to track button actions
 * and manages the timing for different actions.
 */
void Button_Process()
{
  // Time delays for different button actions
#define BUTTON_ACTION_DELAY_TOGGLEMODE 500
#define BUTTON_ACTION_DELAY_PURGE 3500
#define BUTTON_ACTION_DELAY_WIFI 6500
#define BUTTON_ACTION_DELAY_NOTHING 9500

  // Enumeration to represent button actions
  typedef enum buttonAction_e
  {
    BTN_INACTIVE,
    BTN_NOTHING,
    BTN_TOGGLEMODE,
    BTN_TOGGLEWIFI,
    BTN_STARTPURGE
  } buttonAction_t;

  // Static variables to track button state and timing
  static uint32_t buttonTimestamp = 0;
  static buttonAction_t buttonAction = BTN_INACTIVE;

  // Check if button is pressed (LOW)
  if (digitalRead(GPIO_BUTTON) == LOW)
  {
    // Update button timestamp on the first button press
    if (buttonTimestamp == 0)
      buttonTimestamp = millis();

    // Check and execute actions based on predefined time delays
    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 // Button is released
  {
    // Execute corresponding actions based on the detected button action
    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;
      }

      // Display feedback through LEDs
      LED_Process(2);
    }

    // Reset button state and timestamp
    buttonAction = BTN_INACTIVE;
    buttonTimestamp = 0;
  }
}

/**
 * @brief Toggles the WiFi functionality based on the current status.
 *
 * This function manages the WiFi state, either turning it off or starting it as an Access Point (AP),
 * depending on the current mode. If the WiFi is turned off, it can be started in AP mode with the
 * device name and password configured. Additionally, it may stop certain operations related to WiFi
 * maintenance or display debug messages based on the defined features.
 *
 * @param shutdown Flag indicating whether the system is in a shutdown state.
 */
void toggleWiFiAP(bool shutdown)
{
  // Check if WiFi is currently active
  if (WiFi.getMode() != WIFI_OFF)
  {
    // Turn off WiFi
    WiFi.mode(WIFI_OFF);
    Debug_pushMessage("WiFi turned off\n");

    // Stop WiFi maintenance connection ticker if enabled
#ifdef FEATURE_ENABLE_WIFI_CLIENT
    WiFiMaintainConnectionTicker.stop();
#endif
  }
  else
  {
    // Start WiFi in Access Point (AP) mode
    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));

    // Stop WiFi maintenance connection ticker if enabled and display debug messages
#ifdef FEATURE_ENABLE_WIFI_CLIENT
    WiFiMaintainConnectionTicker.stop();
    Debug_pushMessage("WiFi AP started, stopped Maintain-Timer\n");
#else
    Debug_pushMessage("WiFi AP started\n");
#endif
  }
}

/**
 * @brief Performs necessary tasks before shutting down and optionally restarts the ESP.
 *
 * This function initiates a system shutdown, performing tasks such as storing configuration
 * and persistence data to EEPROM before shutting down. If a restart is requested, the ESP
 * will be restarted; otherwise, the system will enter an indefinite loop.
 *
 * @param restart Flag indicating whether to restart the ESP after shutdown (default: false).
 */
void SystemShutdown(bool restart)
{
  static uint32_t shutdown_delay = 0;

  // Initialize shutdown delay on the first call
  if (shutdown_delay == 0)
  {
    shutdown_delay = millis() + SHUTDOWN_DELAY_MS;
    Serial.printf("Shutdown requested - Restarting in %d seconds\n", SHUTDOWN_DELAY_MS / 1000);
  }

  // Check if the shutdown delay has elapsed
  if (shutdown_delay < millis())
  {
    Webserver_Shutdown();

    // Store persistence data to EEPROM
    StorePersistence_EEPROM();

    // Perform restart if requested, otherwise enter an indefinite loop
    if (restart)
      ESP.restart();
    else
      while (1)
        ;
  }
}

/**
 * @brief Processes the impulses from the wheel speed sensor and converts them into traveled distance.
 *
 * This function takes the pulse count from the wheel speed sensor and converts it into distance
 * traveled in millimeters. The conversion is based on the configured parameters such as the number
 * of pulses per revolution and the distance traveled per revolution.
 *
 * @return The calculated distance traveled in millimeters.
 */
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;
}
