/**
 * @file config.cpp
 * @brief Implementation of EEPROM and configuration-related functions.
 *
 * This file contains functions for managing EEPROM storage and handling configuration data.
 * It includes the definitions of configuration structures, EEPROM access, and utility functions.
 */

#include "config.h"
#include "debugger.h"
#include "globals.h"

// Instance of I2C_eeprom for EEPROM access
I2C_eeprom ee(0x50, EEPROM_SIZE_BYTES);

// Configuration and persistence data structures
LubeConfig_t LubeConfig;
persistenceData_t PersistenceData;

// EEPROM version identifier
const uint16_t eeVersion = 2; // Increment this version when changing EEPROM structures

// Flag indicating whether EEPROM is available
boolean eeAvailable = false;

// Offsets within EEPROM for LubeConfig and PersistenceData
const uint16_t startofLubeConfig = 16;
const uint16_t startofPersistence = 16 + sizeof(LubeConfig) + (sizeof(LubeConfig) % 16);

// Function prototype to check EEPROM availability
boolean checkEEPROMavailable();

/**
 * @brief Initializes EEPROM and checks its availability.
 *
 * This function initializes the EEPROM using the I2C_eeprom instance and checks if it's available.
 */
void InitEEPROM()
{
  LubeConfig = LubeConfig_defaults;
  PersistenceData = {0};
  ee.begin();
  checkEEPROMavailable();
}

/**
 * @brief Processes EEPROM actions based on the request from the global state.
 *
 * This function processes EEPROM actions based on the request from the global state.
 * It performs actions such as saving, loading, and formatting EEPROM data for both configuration and persistence.
 */
void EEPROM_Process()
{
  switch (globals.requestEEAction)
  {
  case EE_CFG_SAVE:
    StoreConfig_EEPROM();
    globals.requestEEAction = EE_IDLE;
    Debug_pushMessage("Stored EEPROM CFG\n");
    break;
  case EE_CFG_LOAD:
    GetConfig_EEPROM();
    globals.requestEEAction = EE_IDLE;
    Debug_pushMessage("Loaded EEPROM CFG\n");
    break;
  case EE_CFG_FORMAT:
    FormatConfig_EEPROM();
    globals.requestEEAction = EE_IDLE;
    GetConfig_EEPROM();
    Debug_pushMessage("Formatted EEPROM CFG\n");
    break;
  case EE_PDS_SAVE:
    StorePersistence_EEPROM();
    globals.requestEEAction = EE_IDLE;
    Debug_pushMessage("Stored EEPROM PDS\n");
    break;
  case EE_PDS_LOAD:
    GetPersistence_EEPROM();
    globals.requestEEAction = EE_IDLE;
    Debug_pushMessage("Loaded EEPROM PDS\n");
    break;
  case EE_PDS_FORMAT:
    FormatPersistence_EEPROM();
    globals.requestEEAction = EE_IDLE;
    GetPersistence_EEPROM();
    Debug_pushMessage("Formatted EEPROM PDS\n");
    break;
  case EE_FORMAT_ALL:
    FormatConfig_EEPROM();
    FormatPersistence_EEPROM();
    GetConfig_EEPROM();
    GetPersistence_EEPROM();
    globals.requestEEAction = EE_IDLE;
    Debug_pushMessage("Formatted EEPROM ALL\n");
    break;
  case EE_ALL_SAVE:
    StorePersistence_EEPROM();
    StoreConfig_EEPROM();
    globals.requestEEAction = EE_IDLE;
    Debug_pushMessage("Stored EEPROM ALL\n");
    break;
  case EE_IDLE:
  default:
    globals.requestEEAction = EE_IDLE;
  }
}

/**
 * @brief Stores the configuration data in EEPROM.
 *
 * This function calculates the checksum for the configuration data, updates it, and stores it in EEPROM.
 * It also performs a sanity check on the configuration and raises a diagnostic trouble code (DTC) if needed.
 */
void StoreConfig_EEPROM()
{
  LubeConfig.checksum = 0;
  LubeConfig.checksum = Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig));

  if (!checkEEPROMavailable())
    return;

  ee.updateBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig));

  uint32_t ConfigSanityCheckResult = ConfigSanityCheck(false);

  if (ConfigSanityCheckResult > 0)
  {
    MaintainDTC(DTC_EEPROM_CFG_SANITY, true, ConfigSanityCheckResult);
  }
}

/**
 * @brief Retrieves the configuration data from EEPROM.
 *
 * This function reads the configuration data from EEPROM, performs a checksum validation,
 * and conducts a sanity check on the configuration. It raises a diagnostic trouble code (DTC) if needed.
 */
void GetConfig_EEPROM()
{
  if (!checkEEPROMavailable())
    return;

  ee.readBlock(startofLubeConfig, (uint8_t *)&LubeConfig, sizeof(LubeConfig));

  uint32_t checksum = LubeConfig.checksum;
  LubeConfig.checksum = 0;

  if (Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)) != checksum)
  {
    MaintainDTC(DTC_EEPROM_CFG_BAD, true);
  }

  LubeConfig.checksum = checksum;

  uint32_t ConfigSanityCheckResult = ConfigSanityCheck(false);

  if (ConfigSanityCheckResult > 0)
  {
    MaintainDTC(DTC_EEPROM_CFG_SANITY, true, ConfigSanityCheckResult);
  }
}

/**
 * @brief Stores the persistence data in EEPROM.
 *
 * This function increments the write cycle counter, performs a checksum calculation on the persistence data,
 * and stores it in EEPROM. It also handles EEPROM page movement when needed.
 */
void StorePersistence_EEPROM()
{
  if (PersistenceData.writeCycleCounter >= 0xFFF0)
    MovePersistencePage_EEPROM(false);
  else
    PersistenceData.writeCycleCounter++;

  PersistenceData.checksum = 0;
  PersistenceData.checksum = Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData));

  if (!checkEEPROMavailable())
    return;

  ee.updateBlock(globals.eePersistanceAdress, (uint8_t *)&PersistenceData, sizeof(PersistenceData));
}

/**
 * @brief Retrieves the persistence data from EEPROM.
 *
 * This function reads the EEPROM to get the start address of the persistence data.
 * If the start address is out of range, it resets and stores defaults. Otherwise,
 * it reads from EEPROM and checks if the data is correct.
 */
void GetPersistence_EEPROM()
{
  if (!checkEEPROMavailable())
    return;

  ee.readBlock(0, (uint8_t *)&globals.eePersistanceAdress, sizeof(globals.eePersistanceAdress));
  // if we got the StartAdress of Persistance and it's out of Range - we Reset it and store defaults
  // otherwise we Read from eeprom and check if everything is correct
  if (globals.eePersistanceAdress < startofPersistence || globals.eePersistanceAdress > ee.getDeviceSize())
  {
    MovePersistencePage_EEPROM(true);
    FormatPersistence_EEPROM();
    MaintainDTC(DTC_EEPROM_PDSADRESS_BAD, true);
  }
  else
  {
    ee.readBlock(globals.eePersistanceAdress, (uint8_t *)&PersistenceData, sizeof(PersistenceData));

    uint32_t checksum = PersistenceData.checksum;
    PersistenceData.checksum = 0;

    if (Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)) != checksum)
    {
      MaintainDTC(DTC_EEPROM_PDS_BAD, true);
    }
    PersistenceData.checksum = checksum;
  }
}

/**
 * @brief Formats the configuration partition in EEPROM.
 *
 * This function resets the configuration data to defaults and stores it in EEPROM.
 */
void FormatConfig_EEPROM()
{
  Debug_pushMessage("Formatting Config-Partition\n");
  LubeConfig = LubeConfig_defaults;
  LubeConfig.EEPROM_Version = eeVersion;
  StoreConfig_EEPROM();
}

/**
 * @brief Formats the persistence partition in EEPROM.
 *
 * This function resets the persistence data to defaults and stores it in EEPROM.
 */
void FormatPersistence_EEPROM()
{
  Debug_pushMessage("Formatting Persistance-Partition\n");
  PersistenceData = {0};
  // memset(&PersistenceData, 0, sizeof(PersistenceData));
  StorePersistence_EEPROM();
}
/**
 * @brief Moves the persistence page in EEPROM.
 *
 * This function adjusts the persistence page address and resets the write cycle counter.
 *
 * @param reset If true, the function resets the persistence page address to the start of the partition.
 */
void MovePersistencePage_EEPROM(boolean reset)
{
  if (!checkEEPROMavailable())
    return;

  globals.eePersistanceAdress += sizeof(PersistenceData);
  PersistenceData.writeCycleCounter = 0;

  // Check if we reached the end of the EEPROM and start over at the beginning
  if ((globals.eePersistanceAdress + sizeof(PersistenceData)) > ee.getDeviceSize() || reset)
  {
    globals.eePersistanceAdress = startofPersistence;
  }

  ee.updateBlock(0, (uint8_t *)&globals.eePersistanceAdress, sizeof(globals.eePersistanceAdress));
}

/**
 * @brief Calculate CRC-32 checksum for a block of data.
 *
 * This function implements the CRC-32 algorithm.
 *
 * @param data Pointer to the data block.
 * @param len Length of the data block in bytes.
 * @return CRC-32 checksum.
 */
uint32_t Checksum_EEPROM(uint8_t const *data, size_t len)
{
  if (data == NULL)
    return 0;

  uint32_t crc = 0xFFFFFFFF;
  uint32_t mask;

  while (len--)
  {
    crc ^= *data++;

    for (uint8_t k = 0; k < 8; k++)
    {
      mask = -(crc & 1);
      crc = (crc >> 1) ^ (0xEDB88320 & mask);
    }
  }

  return ~crc;
}

/**
 * @brief Dump a portion of EEPROM contents for debugging.
 *
 * This function prints the contents of a specified portion of EEPROM in a formatted way.
 *
 * @param memoryAddress Starting address in EEPROM.
 * @param length Number of bytes to dump.
 */
void dumpEEPROM(uint16_t memoryAddress, uint16_t length)
{
#define BLOCK_TO_LENGTH 16

  if (!checkEEPROMavailable())
    return;

  char ascii_buf[BLOCK_TO_LENGTH + 1];
  sprintf(ascii_buf, "%*s", BLOCK_TO_LENGTH, "ASCII");

  // Print column headers
  Debug_pushMessage(PSTR("\nAddress "));
  for (int x = 0; x < BLOCK_TO_LENGTH; x++)
    Debug_pushMessage("%3d", x);

  // Align address and length to BLOCK_TO_LENGTH boundaries
  memoryAddress = memoryAddress / BLOCK_TO_LENGTH * BLOCK_TO_LENGTH;
  length = (length + BLOCK_TO_LENGTH - 1) / BLOCK_TO_LENGTH * BLOCK_TO_LENGTH;

  // Iterate through the specified portion of EEPROM
  for (unsigned int i = 0; i < length; i++)
  {
    int blockpoint = memoryAddress % BLOCK_TO_LENGTH;

    // Print ASCII representation header for each block
    if (blockpoint == 0)
    {
      ascii_buf[BLOCK_TO_LENGTH] = 0;
      Debug_pushMessage("  %s", ascii_buf);
      Debug_pushMessage("\n0x%05X:", memoryAddress);
    }

    // Read and print each byte
    ascii_buf[blockpoint] = ee.readByte(memoryAddress);
    Debug_pushMessage(" %02X", ascii_buf[blockpoint]);

    // Replace non-printable characters with dots in ASCII representation
    if (ascii_buf[blockpoint] < 0x20 || ascii_buf[blockpoint] > 0x7E)
      ascii_buf[blockpoint] = '.';

    memoryAddress++;
  }

  // Print a new line at the end of the dump
  Debug_pushMessage("\n");
}

/**
 * @brief Check if EEPROM is available and connected.
 *
 * This function checks if the EEPROM is available and connected. If not, it triggers
 * a diagnostic trouble code (DTC) indicating the absence of EEPROM.
 *
 * @return true if EEPROM is available, false otherwise.
 */
boolean checkEEPROMavailable()
{
  // Check if EEPROM is connected
  if (!ee.isConnected())
  {
    // Trigger DTC for no EEPROM found
    MaintainDTC(DTC_NO_EEPROM_FOUND, true);
    return false;
  }

  // Clear DTC for no EEPROM found since it's available now
  MaintainDTC(DTC_NO_EEPROM_FOUND, false);

  // EEPROM is available
  return true;
}

/**
 * @brief Perform sanity check on configuration settings.
 *
 * This function checks the validity of various configuration settings and returns a bitmask
 * indicating which settings need to be reset. If autocorrect is enabled, it resets the settings
 * to their default values.
 *
 * @param autocorrect If true, automatically correct invalid settings by resetting to defaults.
 * @return A bitmask indicating which settings need to be reset.
 */
uint32_t ConfigSanityCheck(bool autocorrect)
{
  uint32_t setting_reset_bits = 0;

  if (!(LubeConfig.DistancePerLube_Default > 0) || !(LubeConfig.DistancePerLube_Default < 50000))
  {
    SET_BIT(setting_reset_bits, 0);
    if (autocorrect)
      LubeConfig.DistancePerLube_Default = LubeConfig_defaults.DistancePerLube_Default;
  }

  if (!(LubeConfig.DistancePerLube_Rain > 0) || !(LubeConfig.DistancePerLube_Rain < 50000))
  {
    SET_BIT(setting_reset_bits, 1);
    if (autocorrect)
      LubeConfig.DistancePerLube_Rain = LubeConfig_defaults.DistancePerLube_Rain;
  }

  if (!(LubeConfig.tankCapacity_ml > 0) || !(LubeConfig.tankCapacity_ml < 5000))
  {
    SET_BIT(setting_reset_bits, 2);
    if (autocorrect)
      LubeConfig.tankCapacity_ml = LubeConfig_defaults.tankCapacity_ml;
  }

  if (!(LubeConfig.amountPerDose_microL > 0) || !(LubeConfig.amountPerDose_microL < 100))
  {
    SET_BIT(setting_reset_bits, 3);
    if (autocorrect)
      LubeConfig.amountPerDose_microL = LubeConfig_defaults.amountPerDose_microL;
  }

  if (!(LubeConfig.TankRemindAtPercentage >= 0) || !(LubeConfig.TankRemindAtPercentage <= 100))
  {
    SET_BIT(setting_reset_bits, 4);
    if (autocorrect)
      LubeConfig.TankRemindAtPercentage = LubeConfig_defaults.TankRemindAtPercentage;
  }

  if (LubeConfig.SpeedSource == SOURCE_IMPULSE)
  {
    if (!(LubeConfig.PulsePerRevolution > 0) || !(LubeConfig.PulsePerRevolution < 1000))
    {
      SET_BIT(setting_reset_bits, 5);
      if (autocorrect)
        LubeConfig.PulsePerRevolution = LubeConfig_defaults.PulsePerRevolution;
    }

    if (!(LubeConfig.TireWidth_mm > 0) || !(LubeConfig.TireWidth_mm < 500))
    {
      SET_BIT(setting_reset_bits, 6);
      if (autocorrect)
        LubeConfig.TireWidth_mm = LubeConfig_defaults.TireWidth_mm;
    }

    if (!(LubeConfig.TireWidthHeight_Ratio > 0) || !(LubeConfig.TireWidthHeight_Ratio < 150))
    {
      SET_BIT(setting_reset_bits, 7);
      if (autocorrect)
        LubeConfig.TireWidthHeight_Ratio = LubeConfig_defaults.TireWidthHeight_Ratio;
    }

    if (!(LubeConfig.RimDiameter_Inch > 0) || !(LubeConfig.RimDiameter_Inch < 30))
    {
      SET_BIT(setting_reset_bits, 8);
      if (autocorrect)
        LubeConfig.RimDiameter_Inch = LubeConfig_defaults.RimDiameter_Inch;
    }

    if (!(LubeConfig.DistancePerRevolution_mm > 0) || !(LubeConfig.DistancePerRevolution_mm < 10000))
    {
      SET_BIT(setting_reset_bits, 9);
      if (autocorrect)
        LubeConfig.DistancePerRevolution_mm = LubeConfig_defaults.DistancePerRevolution_mm;
    }
  }

  if (!(LubeConfig.BleedingPulses > 0) || !(LubeConfig.BleedingPulses < 1001))
  {
    SET_BIT(setting_reset_bits, 10);
    if (autocorrect)
      LubeConfig.BleedingPulses = LubeConfig_defaults.BleedingPulses;
  }

  if (!(LubeConfig.SpeedSource >= 0) || !(LubeConfig.SpeedSource < SpeedSourceString_Elements))
  {
    SET_BIT(setting_reset_bits, 11);
    if (autocorrect)
      LubeConfig.SpeedSource = LubeConfig_defaults.SpeedSource;
  }

  if (!(LubeConfig.GPSBaudRate >= 0) || !(LubeConfig.GPSBaudRate < GPSBaudRateString_Elements))
  {
    SET_BIT(setting_reset_bits, 12);
    if (autocorrect)
      LubeConfig.GPSBaudRate = LubeConfig_defaults.GPSBaudRate;
  }

  if (!(LubeConfig.CANSource >= 0) || !(LubeConfig.CANSource < CANSourceString_Elements))
  {
    SET_BIT(setting_reset_bits, 13);
    if (autocorrect)
      LubeConfig.CANSource = LubeConfig_defaults.CANSource;
  }
  // Return the bitmask indicating which settings need to be reset
  return setting_reset_bits;
}