/** \file
 *  \brief This file contains functions that manage the external EEPROM.
 *
 */

/* Include Files */
#include "KTag.h"

/* Local Definitions and Constants */

//! This is the same for both the MCP98243 and the CAT24C256.
#define EXTERNAL_EEPROM_I2C_ADDRESS    0x50
#define EXTERNAL_EEPROM_TEMP_SENSOR_I2C_ADDRESS 0x18

//! Read-only register used to identify the temperature sensor capability.
#define MCP98243_REGISTER_CAPABILITY        0x00
//! Sensor configuration register.
#define MCP98243_REGISTER_CONFIG            0x01
//! Upper temperature limit register.
#define MCP98243_REGISTER_T_UPPER           0x02
//! Lower temperature limit register.
#define MCP98243_REGISTER_T_LOWER           0x03
//! Critical temperature limit register.
#define MCP98243_REGISTER_T_CRIT            0x04
//! Ambient temperature register.
#define MCP98243_REGISTER_T_A               0x05
//! Read-only register used to identify the manufacturer of the device.
#define MCP98243_REGISTER_MANUFACTURER_ID   0x06
//! Read-only register indicating the device identification and device revision.
#define MCP98243_REGISTER_DEVICE_ID         0x07
//! Temperature sensor resolution register.
#define MCP98243_REGISTER_RESOLUTION        0x08


/* External Variables [Only if necessary!] */

/* External Function Prototypes [Only if necessary!] */

/* Public Variables */

//! Mutex controlling access to the EEPROM to ensure data/CRC integrity.
SemaphoreHandle_t xSemaphoreExternalEEPROMLock;

TaskHandle_t NVM_ExternalEEPROM_Task_Handle;

volatile bool NVM_IsExternalEEPROMInitialized = false;

/* Private Variables */

static QueueHandle_t xQueueExternalEEPROM;

//! Shared master transfer configuration variable.
static cy_stc_scb_i2c_master_xfer_config_t Master_Transfer_Config =
{
    .slaveAddress = EXTERNAL_EEPROM_I2C_ADDRESS,
    .buffer       = NULL,
    .bufferSize   = 0U,
    .xferPending  = false
};


/* Private Function Prototypes */

/* Inline Functions */

//! Waits a given time for an I²C transfer to complete.
/*!
 *  \param  timeout_in_ms  The time (in milliseconds) to wait for the transfer to complete.
 *  \return                #true if the transfer completed, or #false if the time ran out without
 *                         a successful transfer.
 */
static inline bool Wait_For_Transfer_To_Complete(uint16_t timeout_in_ms)
{
    bool success = false;
    
    // Time to wait for an in-process transfer before looking again.  This wait grows longer as time
    // passes, until timeout_in_ms runs out.
    uint16_t HOLDOFF_TIME_IN_ms = 1;
        
    while ((success == false) && (timeout_in_ms > 0))
    {
        vTaskDelay(pdMS_TO_TICKS(HOLDOFF_TIME_IN_ms));
        
        if (timeout_in_ms > HOLDOFF_TIME_IN_ms)
        {
            timeout_in_ms -= HOLDOFF_TIME_IN_ms;
            
            // Wait a little longer next time.
            HOLDOFF_TIME_IN_ms++;
        }
        else
        {
            timeout_in_ms = 0;
        }
        
        if ((I2C_MasterGetStatus() & CY_SCB_I2C_MASTER_BUSY) != CY_SCB_I2C_MASTER_BUSY)
        {
            success = true;
        }
    }
    
    return success;
}

//! Reads a block of \a n bytes from EEPROM address \a source to SRAM \a destination.
static inline void EEPROM_read_block(uint8_t * destination, uint16_t source, size_t n)
{
    uint8_t xfer_buffer[5];

    if (xSemaphoreTake(COMM_I2C_Bus_Mutex, portMAX_DELAY) == pdTRUE)
    {
        // Write the initial address to the EEPROM.
        xfer_buffer[0] = (source >> 8);
        xfer_buffer[1] = source & 0xFF;
    
        Master_Transfer_Config.buffer = (uint8_t *)xfer_buffer;
        Master_Transfer_Config.bufferSize = 2;

        cy_en_scb_i2c_status_t errStatus = I2C_MasterWrite(&Master_Transfer_Config);

        if (errStatus == CY_SCB_I2C_SUCCESS)
        {
            (void) Wait_For_Transfer_To_Complete(100);
        }
        else
        {
            // What?
        }
    
        // Read n bytes at EEPROM[source].
        Master_Transfer_Config.buffer = (uint8_t *)destination;
        Master_Transfer_Config.bufferSize = n;

        errStatus = I2C_MasterRead(&Master_Transfer_Config);

        if (errStatus == CY_SCB_I2C_SUCCESS)
        {
            (void) Wait_For_Transfer_To_Complete(100);
        }
        else
        {
            // What?
        }
                
        xSemaphoreGive(COMM_I2C_Bus_Mutex);
    }
}

//! Writes a block of \a n bytes from SRAM \a source to EEPROM address \a destination.
static inline void EEPROM_write_block(uint8_t * source, uint16_t destination, size_t n)
{
    uint8_t xfer_buffer[4];

    if (xSemaphoreTake(COMM_I2C_Bus_Mutex, portMAX_DELAY) == pdTRUE)
    {
        // Write the data one byte at a time.
        for (uint8_t i = 0; i < n; i++)
        {
            uint16_t destination_address = destination + i;
            xfer_buffer[0] = (destination_address >> 8);
            xfer_buffer[1] = destination_address & 0xFF;
            xfer_buffer[2] = *(source + i);

            Master_Transfer_Config.buffer = (uint8_t *)xfer_buffer;
            Master_Transfer_Config.bufferSize = 3;

            cy_en_scb_i2c_status_t errStatus = I2C_MasterWrite(&Master_Transfer_Config);

            if (errStatus == CY_SCB_I2C_SUCCESS)
            {
                (void) Wait_For_Transfer_To_Complete(100);
            }
            else
            {
                // What?
            }
            
            // The CAT24C256 has a nominal Write Cycle time (t_WR) of 5ms (no maximum specified).
            // Wait 6ms between writes to have some margin (and avoid being NAKed).
            vTaskDelay(pdMS_TO_TICKS(6));
        }

        xSemaphoreGive(COMM_I2C_Bus_Mutex);
    }
}


/* Public Functions */

//! Sets up the external EEPROM, but does not read from it (yet).
void NVM_InitExternalEEPROM(void)
{
    /// Create a mutex-type semaphore.
    xSemaphoreExternalEEPROMLock = xSemaphoreCreateMutex();

    if (xSemaphoreExternalEEPROMLock == NULL)
    {
       CY_ASSERT(0);
    }
    
    xQueueExternalEEPROM = xQueueCreate(5, sizeof(uint8_t));
}

//! Handles the ongoing external EEPROM tasks.
/*!
 *  First, it loops through all the external EEPROM entries, and reads them in to RAM.
 *  Then, it priodically loops through all the external EEPROM entries, and saves the ones that have been flagged.
 */
void NVM_ExternalEEPROMTask(void * pvParameters)
{
    portBASE_TYPE xStatus;
    static TickType_t xTicksToWait = pdMS_TO_TICKS(NVM_EXTERNAL_EEPROM_TASK_RATE_IN_ms);
    
    for (uint8_t i = 0; i < NVM_N_EXTERNAL_EEPROM_ENTRIES; i++)
    {
        NVM_CRC_t calculated_crc;
        NVM_CRC_t stored_crc = 0;
  
        EEPROM_read_block(NVM_ExternalEEPROMEntries[i]->Value, NVM_ExternalEEPROMEntries[i]->EE_Address, NVM_ExternalEEPROMEntries[i]->Size);
        EEPROM_read_block((uint8_t *)&stored_crc, NVM_ExternalEEPROMEntries[i]->EE_CRC_Address, sizeof(NVM_CRC_t)); 
        
        calculated_crc = NVM_CRC_init();
        calculated_crc = NVM_CRC_update(calculated_crc, NVM_ExternalEEPROMEntries[i]->Value, NVM_ExternalEEPROMEntries[i]->Size);
        calculated_crc = NVM_CRC_finalize(calculated_crc);
        
        if (calculated_crc == stored_crc)
        {
            NVM_ExternalEEPROMEntries[i]->State = NVM_STATE_IDLE;
        }
        else
        {
            NVM_ExternalEEPROMEntries[i]->State = NVM_STATE_CRC_FAILED;
            
            COMM_Console_Print_String("[NVMEx ");
            COMM_Console_Print_UInt16((uint16_t) i);
            COMM_Console_Print_String("] Calculated/Stored CRCs: ");
            COMM_Console_Print_UInt16((uint16_t) calculated_crc);
            COMM_Console_Print_String("/");
            COMM_Console_Print_UInt16((uint16_t) stored_crc);
            COMM_Console_Print_String("\n");
            
            COMM_Console_Print_String("[NVMEx ");
            COMM_Console_Print_UInt16((uint16_t) i);
            COMM_Console_Print_String("] Applying defaults.\n");
            
            memcpy(NVM_ExternalEEPROMEntries[i]->Value, NVM_ExternalEEPROMEntries[i]->Default, NVM_ExternalEEPROMEntries[i]->Size);

            // Auto-fix the CRC.
            NVM_SaveExternalEEPROMEntry(NVM_ExternalEEPROMEntries[i]);
        }
    }
    
    taskENTER_CRITICAL();
    NVM_IsExternalEEPROMInitialized = true;
    taskEXIT_CRITICAL();
    
    while(true)
    {
        uint8_t dummy;
        
        // Wait for a call to NVM_SaveExternalEEPROMEntry().
        xStatus = xQueueReceive(xQueueExternalEEPROM, &dummy, xTicksToWait);
        
        if (xStatus == pdPASS)
        {
            for (uint8_t i = 0; i < NVM_N_EXTERNAL_EEPROM_ENTRIES; i++)
            {
                NVM_CRC_t crc;
                
                if (NVM_ExternalEEPROMEntries[i]->State == NVM_STATE_SAVE_REQUESTED)
                {
                    if (xSemaphoreTake(xSemaphoreExternalEEPROMLock, portMAX_DELAY) == pdTRUE)
                    {
                        EEPROM_write_block(NVM_ExternalEEPROMEntries[i]->Value, NVM_ExternalEEPROMEntries[i]->EE_Address, NVM_ExternalEEPROMEntries[i]->Size);
                        
                        // Calculate the CRC.
                        crc = NVM_CRC_init();
                        crc = NVM_CRC_update(crc, NVM_ExternalEEPROMEntries[i]->Value, NVM_ExternalEEPROMEntries[i]->Size);
                        crc = NVM_CRC_finalize(crc);
                        EEPROM_write_block((uint8_t *)&crc, NVM_ExternalEEPROMEntries[i]->EE_CRC_Address, sizeof(uint16_t));
                        NVM_ExternalEEPROMEntries[i]->State = NVM_STATE_IDLE;
                        xSemaphoreGive(xSemaphoreExternalEEPROMLock);
                    }
                }
            }
        }
    }
}

//! Flags the given external EEPROM entry to be saved next time the NVM_ExternalEEPROMTask() is run.
void NVM_SaveExternalEEPROMEntry(NVM_EEPROMEntry_T * const this)
{
    if (xSemaphoreTake(xSemaphoreExternalEEPROMLock, portMAX_DELAY) == pdTRUE)
    {
        this->State = NVM_STATE_SAVE_REQUESTED;
        xSemaphoreGive(xSemaphoreExternalEEPROMLock);
        uint8_t dummy = 0;
        xQueueSend(xQueueExternalEEPROM, &dummy, 0);
    }
}

/* Private Functions */
