/**
 * @file webui.cpp
 *
 * @brief Implementation file for web-based user interface (WebUI) functions in the ChainLube application.
 *
 * This file contains the implementation of functions related to the initialization and processing of the
 * web-based user interface (WebUI). It includes the setup of LittleFS, handling of firmware version checks,
 * initialization of mDNS, setup of web server routes, and handling of various HTTP events.
 *
 * @author Marcel Peterkau
 * @date   09.01.2024
 */

#include "webui.h"

AsyncWebServer webServer(80);

const char *PARAM_MESSAGE = "message";

SpeedSource_t speedsourcePreselect; /**< Preselect Memory for change SourceAdress */

String processor(const String &var);
void WebserverNotFound_Callback(AsyncWebServerRequest *request);
void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final);
void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final);
void WebServerEEJSON_Callback(AsyncWebServerRequest *request);
void GetFlashVersion(char *buff, size_t buff_size);

AsyncWebSocket webSocket("/ws");

void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len);
void Websocket_RefreshClientData_DTCs(uint32_t client_id);
void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping = false);
void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping = false);
void Websocket_HandleButtons(uint8_t *data);
void Websocket_HandleSettings(uint8_t *data);
void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierBufferSize, char *valueBuffer, size_t valueBufferSize);
int findIndexByString(const char *searchString, const char *const *array, int arraySize);

/**
 * @brief Initializes the web-based user interface (WebUI) for the ChainLube application.
 *
 * This function sets up the necessary components for the WebUI, including mounting LittleFS,
 * performing flash version checks, initializing mDNS, and configuring the web server with
 * routes and event handlers. If any errors occur during setup, appropriate diagnostic messages
 * are pushed to the debugging system, and potential error conditions are recorded as Diagnostic
 * Trouble Codes (DTCs).
 *
 * @note This function should be called during the initialization phase of the application.
 */
void initWebUI()
{
  // Attempt to mount LittleFS
  if (!LittleFS.begin())
  {
    Debug_pushMessage("An Error has occurred while mounting LittleFS\n");
    MaintainDTC(DTC_FLASHFS_ERROR, true);
    return;
  }

  // Retrieve the flash version
  GetFlashVersion(globals.FlashVersion, sizeof(globals.FlashVersion));

  // Compare the flash version with the required version
  char buffer[6];
  snprintf(buffer, sizeof(buffer), "%d.%02d", constants.Required_Flash_Version_major, constants.Required_Flash_Version_minor);
  if (strcmp(globals.FlashVersion, buffer))
  {
    MaintainDTC(DTC_FLASHFS_VERSION_ERROR, true);
  }

  // Initialize mDNS and add service
  MDNS.begin(globals.DeviceName);
  MDNS.addService("http", "tcp", 80);

  // Set up WebSocket event handler and attach to web server
  webSocket.onEvent(WebsocketEvent_Callback);
  webServer.addHandler(&webSocket);

  // Serve static files and define routes
  webServer.serveStatic("/static/", LittleFS, "/static/").setCacheControl("max-age=360000");
  webServer.serveStatic("/index.htm", LittleFS, "/index.htm").setCacheControl("max-age=360000");
  webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
               { request->redirect("/index.htm"); });
  webServer.onNotFound(WebserverNotFound_Callback);
  webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback);
  webServer.on(
      "/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback);
  webServer.on(
      "/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);

  // Start the web server
  webServer.begin();
}

/**
 * @brief Processes the web server functionality for the ChainLube application.
 *
 * This function performs periodic processing tasks for the web server, including cleaning up
 * WebSocket clients and refreshing client data when WebSocket connections are active. It ensures
 * that WebSocket client data related to Diagnostic Trouble Codes (DTCs) and system status is
 * updated at regular intervals.
 *
 * @note This function should be called in the main loop of the application.
 */
void Webserver_Process()
{
  static uint32_t previousMillis = 0;

  webSocket.cleanupClients();

  if ((webSocket.count() > 0) && (millis() - previousMillis >= 10000))
  {
    Websocket_RefreshClientData_DTCs(0);
    Websocket_RefreshClientData_Status(0);
    previousMillis = millis();
  }
}

/**
 * @brief Shuts down the web server functionality for the ChainLube application.
 *
 * This function closes all WebSocket connections and terminates the web server. It is intended
 * to be called when the application is being shut down or when there is a need to deactivate the
 * web server.
 *
 * @details This function ensures a graceful shutdown of the web server by closing all active
 *          WebSocket connections and ending the web server instance.
 *
 * @note This function should be called before shutting down the application to properly
 *       deactivate the web server.
 */
void Webserver_Shutdown()
{
  if (webSocket.count() > 0)
    webSocket.closeAll();
  webServer.end();
}

/**
 * @brief Callback function for handling HTTP 404 (Not Found) errors on the web server.
 *
 * This function is invoked when an HTTP request results in a 404 error (Not Found). It sends
 * a simple "Not found" text response with an HTTP status code of 404.
 *
 * @param request Pointer to the AsyncWebServerRequest object representing the HTTP request.
 */
void WebserverNotFound_Callback(AsyncWebServerRequest *request)
{
  request->send(404, "text/html", "Not found");
}

/**
 * @brief Reads the flash version information from a file in LittleFS.
 *
 * This function reads the flash version information stored in a file named "version" in the
 * LittleFS filesystem. It opens the file, reads the content until a carriage return ('\r') is
 * encountered, and stores the result in the provided buffer. The buffer is null-terminated.
 *
 * @param buff Pointer to the buffer where the flash version information will be stored.
 * @param buff_size Size of the buffer.
 */
void GetFlashVersion(char *buff, size_t buff_size)
{
  File this_file = LittleFS.open("version", "r");
  if (!this_file)
  { // failed to open the file, retrn empty result
    buff[0] = '\0';
    return;
  }
  if (this_file.available())
  {
    int bytes_read;
    bytes_read = this_file.readBytesUntil('\r', buff, buff_size - 1);
    buff[bytes_read] = '\0';
  }
  this_file.close();
}

/**
 * @brief Callback function for handling firmware updates via the web server.
 *
 * This function is invoked during the firmware update process when a new firmware file
 * is received. It handles the update process using the ESPAsyncHTTPUpdate library. The update
 * process involves checking the firmware type, initializing the update, writing data, and finalizing
 * the update. If the update is successful, it triggers a system shutdown.
 *
 * @param request   Pointer to the AsyncWebServerRequest object.
 * @param filename  The name of the file being updated.
 * @param index     The index of the file being updated.
 * @param data      Pointer to the data buffer.
 * @param len       The length of the data buffer.
 * @param final     Boolean indicating if this is the final chunk of data.
 */
void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
{

  if (!index)
  {
    Debug_pushMessage("Update\n");
    size_t content_len = request->contentLength();
    int cmd = (filename.indexOf(".fs") > -1) ? U_FS : U_FLASH;
    Update.runAsync(true);
    if (!Update.begin(content_len, cmd))
    {
      Update.printError(Serial);
    }
  }

  if (Update.write(data, len) != len)
  {
    Update.printError(Serial);
  }
  else
  {
    Debug_pushMessage("Progress: %d%%\n", (Update.progress() * 100) / Update.size());
  }

  if (final)
  {
    AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots");
    response->addHeader("Refresh", "20");
    response->addHeader("Location", "/");
    request->send(response);
    if (!Update.end(true))
    {
      Update.printError(Serial);
    }
    else
    {
      Debug_pushMessage("Update complete\n");
      globals.systemStatus = sysStat_Shutdown;
    }
  }
}

/**
 * @brief Callback function for handling EEPROM restore via the web server.
 *
 * This function is invoked during the EEPROM restore process when a new EEPROM file
 * is received. It handles the restore process by reading the data from the received file,
 * deserializing the JSON data, and updating the configuration and persistence data accordingly.
 * If the restore is successful, it triggers a system shutdown.
 *
 * @param request   Pointer to the AsyncWebServerRequest object.
 * @param filename  The name of the file being restored.
 * @param index     The index of the file being restored.
 * @param data      Pointer to the data buffer.
 * @param len       The length of the data buffer.
 * @param final     Boolean indicating if this is the final chunk of data.
 */
void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
{
  bool ee_done = false;
  static bool validext = false;
  static char *buffer = NULL;
  static uint32_t read_ptr = 0;
  DeserializationError error;

  if (!index)
  {
    validext = (filename.indexOf(".ee.json") > -1);
    if (validext)
    {
      buffer = (char *)malloc(1536);
      read_ptr = 0;
      if (buffer == NULL)
        Debug_pushMessage("malloc() failed for EEPROM-Restore\n");
    }
  }

  if (buffer != NULL)
  {
    memcpy(buffer + read_ptr, data, len);
    read_ptr = read_ptr + len;
  }

  if (final)
  {
    if (buffer != NULL)
    {
      Serial.print(buffer);
      JsonDocument json;
      error = deserializeJson(json, buffer);
      if (error)
      {
        Debug_pushMessage("deserializeJson() failed: %s\n", error.f_str());
      }
      else
      {

        LubeConfig.DistancePerLube_Default = json["config"]["DistancePerLube_Default"].as<uint32_t>();
        LubeConfig.DistancePerLube_Rain = json["config"]["DistancePerLube_Rain"].as<uint32_t>();
        LubeConfig.tankCapacity_ml = json["config"]["tankCapacity_ml"].as<uint32_t>();
        LubeConfig.amountPerDose_microL = json["config"]["amountPerDose_microL"].as<uint32_t>();
        LubeConfig.TankRemindAtPercentage = json["config"]["TankRemindAtPercentage"].as<uint8_t>();
        LubeConfig.PulsePerRevolution = json["config"]["PulsePerRevolution"].as<uint8_t>();
        LubeConfig.TireWidth_mm = json["config"]["TireWidth_mm"].as<uint32_t>();
        LubeConfig.TireWidthHeight_Ratio = json["config"]["TireWidthHeight_Ratio"].as<uint32_t>();
        LubeConfig.RimDiameter_Inch = json["config"]["RimDiameter_Inch"].as<uint32_t>();
        LubeConfig.DistancePerRevolution_mm = json["config"]["DistancePerRevolution_mm"].as<uint32_t>();
        LubeConfig.BleedingPulses = json["config"]["BleedingPulses"].as<uint16_t>();
        LubeConfig.SpeedSource = (SpeedSource_t)json["config"]["SpeedSource"].as<int>();
        LubeConfig.GPSBaudRate = (GPSBaudRate_t)json["config"]["GPSBaudRate"].as<int>();
        LubeConfig.CANSource = (CANSource_t)json["config"]["CANSource"].as<int>();
        LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as<bool>();
        LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as<uint8_t>();
        LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>();

        PersistenceData.writeCycleCounter = json["persis"]["writeCycleCounter"].as<uint16_t>();
        PersistenceData.tankRemain_microL = json["persis"]["tankRemain_microL"].as<uint32_t>();
        PersistenceData.TravelDistance_highRes_mm = json["persis"]["TravelDistance_highRes_mm"].as<uint32_t>();
        PersistenceData.odometer_mm = json["persis"]["odometer_mm"].as<uint32_t>();
        PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>();
        PersistenceData.checksum = json["persis"]["checksum"].as<uint32_t>();

        ee_done = true;
      }
    }

    free(buffer);

    AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots");
    response->addHeader("Refresh", "20");
    response->addHeader("Location", "/");
    request->send(response);

    if (ee_done)
    {
      Debug_pushMessage("Update complete\n");
      globals.systemStatus = sysStat_Shutdown;
    }
  }
}

/**
 * @brief Callback function for handling EEPROM JSON request via the web server.
 *
 * This function is invoked when a request for EEPROM JSON data is received. It constructs a JSON
 * response containing information about the firmware, configuration, and persistence data.
 *
 * @param request Pointer to the AsyncWebServerRequest object.
 */
void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
{
  AsyncResponseStream *response = request->beginResponseStream("application/json");
  JsonDocument json;
  JsonObject info = json.to<JsonObject>();

  char buffer[16];

  info["DeviceName"] = globals.DeviceName;
  sprintf(buffer, "%d.%02d", constants.Required_Flash_Version_major, constants.Required_Flash_Version_minor);
  info["FW-Version"] = buffer;
  info["FS-Version"] = globals.FlashVersion;
  snprintf_P(buffer, sizeof(buffer), "%s", constants.GitHash);
  info["Git-Hash"] = buffer;

  JsonObject config = json.to<JsonObject>();
  generateJsonObject_LubeConfig(config);
  JsonObject persis = json.to<JsonObject>();
  generateJsonObject_PersistenceData(persis);

  JsonObject eepart = json.to<JsonObject>();
  sprintf(buffer, "0x%04X", globals.eePersistanceAdress);
  eepart["PersistanceAddress"] = buffer;

  serializeJsonPretty(json, *response);

  response->addHeader("Content-disposition", "attachment; filename=backup.ee.json");

  request->send(response);
}

/**
 * @brief Callback function for handling WebSocket events.
 *
 * This function is invoked when events occur in the WebSocket communication, such as client connection,
 * disconnection, reception of data, and others. It dispatches the events to the appropriate handlers.
 *
 * @param server Pointer to the AsyncWebSocket object.
 * @param client Pointer to the AsyncWebSocketClient object representing the WebSocket client.
 * @param type Type of WebSocket event.
 * @param arg Event-specific argument.
 * @param data Pointer to the received data (if applicable).
 * @param len Length of the received data.
 */
void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{
  switch (type)
  {
  case WS_EVT_CONNECT:
    Debug_pushMessage("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
    Websocket_RefreshClientData_Status(client->id(), true);
    Websocket_RefreshClientData_Static(client->id(), true);
    Websocket_RefreshClientData_DTCs(client->id());
    break;
  case WS_EVT_DISCONNECT:
    Debug_pushMessage("WebSocket client #%u disconnected\n", client->id());
    break;
  case WS_EVT_DATA:
    Websocket_HandleMessage(arg, data, len);
    break;
  case WS_EVT_PONG:
  case WS_EVT_ERROR:
    break;
  }
}

/**
 * @brief Handles WebSocket messages received from clients.
 *
 * This function processes WebSocket messages, such as starting or stopping debugging,
 * and provides appropriate responses.
 *
 * @param arg Pointer to the WebSocket frame information.
 * @param data Pointer to the received data.
 * @param len Length of the received data.
 */
void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len)
{
  AwsFrameInfo *info = (AwsFrameInfo *)arg;
  if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
  {
    data[len] = 0;
    Debug_pushMessage("Websocket-Message (len: %d): %s\n", len, (char *)data);

    if (strncmp((char *)data, "btn-", strlen("btn-")) == 0)
    {
      Websocket_HandleButtons(data + strlen("btn-"));
    }
    else if (strncmp((char *)data, "set-", strlen("set-")) == 0)
    {
      Websocket_HandleSettings(data + strlen("set-"));
    }
    else
    {
      Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", (char *)data);
    }
  }
}

/**
 * @brief Handle button commands received via WebSocket.
 *
 * This function parses a WebSocket string representing button commands, extracts
 * the identifier and value components, and performs corresponding actions based on
 * the received commands.
 *
 * @param data The WebSocket data containing button commands.
 */
void Websocket_HandleButtons(uint8_t *data)
{
  char identifier[32];
  char value[32];

  parseWebsocketString((char *)data, identifier, sizeof(identifier), value, sizeof(value));

  if (strcmp(identifier, "debugstart") == 0)
  {
    SetDebugportStatus(dbg_Webui, enabled);
  }
  else if (strcmp(identifier, "debugstop") == 0)
  {
    SetDebugportStatus(dbg_Webui, disabled);
  }
  else if (strcmp(identifier, "measurereset") == 0)
  {
    globals.measuredPulses = 0;
  }
  else if (strcmp(identifier, "measurestartstop") == 0)
  {
    globals.measurementActive = !globals.measurementActive;
  }
  else if (strcmp(identifier, "purgenow") == 0)
  {
    globals.systemStatus = sysStat_Purge;
    globals.purgePulses = LubeConfig.BleedingPulses;
  }
  else if (strcmp(identifier, "sourcesave") == 0)
  {
    LubeConfig.SpeedSource = speedsourcePreselect;
    globals.requestEEAction = EE_CFG_SAVE;
    globals.systemStatus = sysStat_Shutdown;
  }
  else if (strcmp(identifier, "settingssave") == 0)
  {
    globals.requestEEAction = EE_CFG_SAVE;
  }
  else if (strcmp(identifier, "reboot") == 0)
  {
    globals.systemStatus = sysStat_Shutdown;
  }
  else if (strcmp(identifier, "resettank") == 0)
  {
    PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000;
  }
  else
  {
    Debug_pushMessage("Got unknown Button-id '%s' from ws-client\n", identifier);
  }
}

/**
 * @brief Handle settings commands received via WebSocket.
 *
 * This function parses a WebSocket string representing settings commands, extracts
 * the identifier and value components, and updates the system settings accordingly.
 *
 * @param data The WebSocket data containing settings commands.
 */
void Websocket_HandleSettings(uint8_t *data)
{
  char identifier[32];
  char value[32];

  parseWebsocketString((char *)data, identifier, sizeof(identifier), value, sizeof(value));

  if (strcmp(identifier, "bleedingpulses") == 0)
  {
    LubeConfig.BleedingPulses = atoi(value);
  }
  else if (strcmp(identifier, "speedsource") == 0)
  {
    int index = findIndexByString(value, SpeedSourceString, SpeedSourceString_Elements);
    speedsourcePreselect = (SpeedSource_t)index;
  }
  else if (strcmp(identifier, "cansource") == 0)
  {
    int index = findIndexByString(value, CANSourceString, CANSourceString_Elements);
    LubeConfig.CANSource = (CANSource_t)index;
  }
  else if (strcmp(identifier, "gpsbaud") == 0)
  {
    int index = findIndexByString(value, GPSBaudRateString, GPSBaudRateString_Elements);
    LubeConfig.GPSBaudRate = (GPSBaudRate_t)index;
  }
  else if (strcmp(identifier, "ledmaxbrightness") == 0)
  {
    LubeConfig.LED_Max_Brightness = atoi(value);
  }
  else if (strcmp(identifier, "ledminbrightness") == 0)
  {
    LubeConfig.LED_Min_Brightness = atoi(value);
  }
  else if (strcmp(identifier, "pumppulse") == 0)
  {
    LubeConfig.BleedingPulses = atoi(value);
  }
  else if (strcmp(identifier, "tankwarn") == 0)
  {
    LubeConfig.TankRemindAtPercentage = atoi(value);
  }
  else if (strcmp(identifier, "tankcap") == 0)
  {
    LubeConfig.tankCapacity_ml = atoi(value);
  }
  else if (strcmp(identifier, "lubedistancerain") == 0)
  {
    LubeConfig.DistancePerLube_Rain = atoi(value);
  }
  else if (strcmp(identifier, "lubedistancenormal") == 0)
  {
    LubeConfig.DistancePerLube_Default = atoi(value);
  }
  else if (strcmp(identifier, "ledmodeflash") == 0)
  {
    LubeConfig.LED_Mode_Flash = value[0] == '1' ? true : false;
  }
  else
  {
    Debug_pushMessage("Got unknown Settings-id and value '%s' from ws-client\n", identifier);
  }
}

/**
 * @brief Pushes live debug messages to all WebSocket clients.
 *
 * This function sends a live debug message to all connected WebSocket clients.
 *
 * @param Message The debug message to be sent.
 */
void Websocket_PushLiveDebug(String Message)
{
  webSocket.textAll("DEBUG:" + Message);
}

/**
 * @brief Refreshes client data related to Diagnostic Trouble Codes (DTCs) on WebSocket clients.
 *
 * This function constructs a DTC-related string and sends it to a specific WebSocket client or
 * broadcasts it to all connected WebSocket clients.
 *
 * @param client_id The ID of the WebSocket client to which the data should be sent. If 0, the data
 * will be broadcasted to all connected clients.
 */
void Websocket_RefreshClientData_DTCs(uint32_t client_id)
{
  String temp = "DTC:";

  // Build DTC-String
  if (globals.hasDTC != true)
  {
    temp.concat(String(DTC_NO_DTC) + ";");
  }
  else
  {
    for (uint32_t i = 0; i < MAX_DTC_STORAGE; i++)
    {
      if (DTCStorage[i].Number < DTC_LAST_DTC)
      {
        temp.concat(String(DTCStorage[i].timestamp) + ",");
        temp.concat(String(DTCStorage[i].Number) + ",");
        temp.concat(String(getSeverityForDTC(DTCStorage[i].Number)) + ",");
        temp.concat(String(DTCStorage[i].active) + ",");
        temp.concat(String(DTCStorage[i].debugVal) + ";");
      }
    }
  }
  if (client_id > 0)
  {
    webSocket.text(client_id, temp);
  }
  else
  {
    webSocket.textAll(temp);
  }
}

/**
 * @brief Refreshes client data related to system status and relevant parameters on WebSocket clients.
 *
 * This function constructs a status-related string and sends it to a specific WebSocket client or
 * broadcasts it to all connected WebSocket clients. It also sends a mapping of the status parameters.
 *
 * @param client_id The ID of the WebSocket client to which the data should be sent. If 0, the data
 * will be broadcasted to all connected clients.
 * @param send_mapping Flag indicating whether to send the parameter mapping to the client(s).
 */
void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
{

  if (send_mapping)
  {
    const char mapping[] = "MAPPING_STATUS:"
                           "systemstatus;"
                           "tankremain;"
                           "odometer;";

    if (client_id > 0)
      webSocket.text(client_id, mapping);
    else
      webSocket.textAll(mapping);
  }

  String temp = "STATUS:";

  temp.concat(String(globals.systemStatustxt) + ";");
  temp.concat(String((PersistenceData.tankRemain_microL / 10) / LubeConfig.tankCapacity_ml) + ";");
  temp.concat(String(PersistenceData.odometer + (PersistenceData.odometer_mm / 1000)) + ";");

  if (client_id > 0)
  {
    webSocket.text(client_id, temp);
  }
  else
  {
    webSocket.textAll(temp);
  }
}

/**
 * @brief Refreshes client data related to static configuration parameters on WebSocket clients.
 *
 * This function constructs a static configuration-related string and sends it to a specific WebSocket client or
 * broadcasts it to all connected WebSocket clients. It also sends a mapping of the static configuration parameters.
 *
 * @param client_id The ID of the WebSocket client to which the data should be sent. If 0, the data
 * will be broadcasted to all connected clients.
 * @param send_mapping Flag indicating whether to send the parameter mapping to the client(s).
 */
void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping)
{

  if (send_mapping)
  {
    const char mapping[] = "MAPPING_STATIC:"
                           "lubedistancenormal;"
                           "lubedistancerain;"
                           "tankcap;"
                           "pumppulse;"
                           "tankwarn;"
                           "pulserev;"
                           "tirewidth;"
                           "tireratio;"
                           "tiredia;"
                           "speedsource;"
                           "gpsbaud;"
                           "cansource;"
                           "ledmodeflash;"
                           "ledmaxbrightness;"
                           "ledminbrightness;"
                           "showimpulse;"
                           "showgps;"
                           "showcan;"
                           "bleedingpulses;";

    if (client_id > 0)
      webSocket.text(client_id, mapping);
    else
      webSocket.textAll(mapping);
  }

  String temp = "STATIC:";

  temp.concat(String(LubeConfig.DistancePerLube_Default) + ";");
  temp.concat(String(LubeConfig.DistancePerLube_Rain) + ";");
  temp.concat(String(LubeConfig.tankCapacity_ml) + ";");
  temp.concat(String(LubeConfig.amountPerDose_microL) + ";");
  temp.concat(String(LubeConfig.TankRemindAtPercentage) + ";");
  temp.concat(String(LubeConfig.PulsePerRevolution) + ";");
  temp.concat(String(LubeConfig.TireWidth_mm) + ";");
  temp.concat(String(LubeConfig.TireWidthHeight_Ratio) + ";");
  temp.concat(String(LubeConfig.RimDiameter_Inch) + ";");
  temp.concat(String(SpeedSourceString[LubeConfig.SpeedSource]) + ";");
  temp.concat(String(GPSBaudRateString[LubeConfig.GPSBaudRate]) + ";");
  temp.concat(String(CANSourceString[LubeConfig.CANSource]) + ";");
  temp.concat(String(LubeConfig.LED_Mode_Flash == true ? "1" : "0") + ";");
  temp.concat(String(LubeConfig.LED_Max_Brightness) + ";");
  temp.concat(String(LubeConfig.LED_Min_Brightness) + ";");
  temp.concat(String(LubeConfig.SpeedSource == SOURCE_IMPULSE ? "1" : "0") + ";");
  temp.concat(String(LubeConfig.SpeedSource == SOURCE_GPS ? "1" : "0") + ";");
  temp.concat(String(LubeConfig.SpeedSource == SOURCE_CAN ? "1" : "0") + ";");
  temp.concat(String(LubeConfig.BleedingPulses) + ";");

  for (uint32_t i = 0; i < SpeedSourceString_Elements; i++)
  {
    temp.concat(String(SpeedSourceString[i]) + ",");
  }
  temp.concat(";");

  if (client_id > 0)
  {
    webSocket.text(client_id, temp);
  }
  else
  {
    webSocket.textAll(temp);
  }
}

/**
 * @brief Parse a WebSocket string into identifier and value components.
 *
 * This function takes a WebSocket string, separates it into identifier and value
 * components using the ":" delimiter, and stores them in the specified buffers.
 * If no ":" is found, the entire string is considered as the value, and the
 * identifier buffer is set to an empty string.
 *
 * @param data The WebSocket string to parse.
 * @param identifierBuffer The buffer to store the identifier component.
 * @param identifierBufferSize The size of the identifier buffer.
 * @param valueBuffer The buffer to store the value component.
 * @param valueBufferSize The size of the value buffer.
 */
void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierBufferSize,
                          char *valueBuffer, size_t valueBufferSize)
{
  // Zerlegen des Strings anhand des Trennzeichens ":"
  char *token = strtok(data, ":");

  // Falls der erste Teil des Strings vorhanden ist
  if (token != NULL)
  {
    // Kopieren des ersten Teils in den Buffer für Identifier
    strncpy(identifierBuffer, token, identifierBufferSize - 1);
    identifierBuffer[identifierBufferSize - 1] = '\0'; // Null-Terminierung sicherstellen

    // Weitere Aufrufe von strtok, um den nächsten Teil zu erhalten
    token = strtok(NULL, ":");

    // Falls der zweite Teil des Strings vorhanden ist
    if (token != NULL)
    {
      // Kopieren des zweiten Teils in den Buffer für Value
      strncpy(valueBuffer, token, valueBufferSize - 1);
      valueBuffer[valueBufferSize - 1] = '\0'; // Null-Terminierung sicherstellen
    }
    else
    {
      // Kein zweiter Teil vorhanden, setzen Sie den Buffer für Value auf leer
      valueBuffer[0] = '\0';
    }
  }
  else
  {
    // Der erste Teil des Strings fehlt, setzen Sie den Buffer für Identifier auf leer
    identifierBuffer[0] = '\0';

    // Der gesamte String wird als Value betrachtet
    strncpy(valueBuffer, data, valueBufferSize - 1);
    valueBuffer[valueBufferSize - 1] = '\0'; // Null-Terminierung sicherstellen
  }
}

/**
 * @brief Find the index of a string in an array.
 *
 * This function searches for the given string in the provided array and returns
 * the index of the first occurrence. If the string is not found, it returns -1.
 *
 * @param searchString The string to search for in the array.
 * @param array The array of strings to search within.
 * @param arraySize The size of the array.
 *
 * @return The index of the first occurrence of the string in the array,
 *         or -1 if the string is not found.
 */
int findIndexByString(const char *searchString, const char *const *array, int arraySize)
{
  // Durchlaufe das Array und vergleiche jeden String
  for (int i = 0; i < arraySize; ++i)
  {
    if (strcmp(array[i], searchString) == 0)
    {
      // String gefunden, gib den Index zurück
      return i;
    }
  }
  // String nicht gefunden, gib -1 zurück
  return -1;
}

/**
 * @brief Pushes a notification to all WebSocket clients.
 *
 * This function sends a live debug message to all connected WebSocket clients.
 *
 * @param Message The debug message to be sent.
 * @param type The type of notification (info, success, warning, error).
 *             - Use NotificationType_t::info for informational messages.
 *             - Use NotificationType_t::success for successful operation messages.
 *             - Use NotificationType_t::warning for warning messages.
 *             - Use NotificationType_t::error for error messages.
 */
void Websocket_PushNotification(String Message, NotificationType_t type)
{
  String typeString = "";
  switch (type)
  {
  case info:
    typeString = "info";
    break;
  case success:
    typeString = "success";
    break;
  case warning:
    typeString = "warning";
    break;
  case error:
    typeString = "danger";
    break;
  }
  webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
  Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString);
}