/**
 * Microcontroller specific code for CANopenNode nonvolatile variables.
 *
 * This file is a template for MM32MCU.
 *
 * @file        eeprom.h
 * @ingroup     CO_eeprom
 *
 * This file is part of CANopenNode, an opensource CANopen Stack.
 * Project home page is <https://github.com/CANopenNode/CANopenNode>.
 * For more information on CANopen see <http://www.can-cia.org/>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


#include "CO_driver.h"
#include "CO_SDO.h"
#include "CO_Emergency.h"
#include "CO_eeprom.h"
#include "crc16-ccitt.h"
#include "hal_i2c.h"

/*
 * Read block of data from eeprom.
 *
 * @param data Pointer to data buffer, where data will be stored.
 * @param addr Address in eeprom, from where data will be read.
 * @param len Length of the data block to be read.
 */
static void EE_readBlock(uint8_t *data, uint32_t addr, uint32_t len){
    I2C_MasterXfer_Type i2c_xfer;
    uint8_t txbuf[8] = {addr};

    i2c_xfer.WaitTimes = EE_TIMEOUT;
    i2c_xfer.TxBuf = txbuf; /* Put address which data will be read. */
    i2c_xfer.RxBuf = data;
    i2c_xfer.RxLen = len;

    I2C_MasterReadBlocking(BOARD_I2C_PORT, &i2c_xfer);
}

/*
 * Write block of data to eeprom. It is blocking function, so it waits, untill
 * all data are written.
 *
 * @param data Pointer to data to be written.
 * @param addr Address in eeprom, where data will be written. *If data are
 * stored accross multiple pages, address must be aligned with page.*
 * @param len Length of the data block.
 */
static void EE_writeBlock(uint8_t *data, uint32_t addr, uint32_t len){
    I2C_MasterXfer_Type i2c_xfer;
    uint8_t txbuf[9u];
    txbuf[0] = addr;
    for (uint32_t i = 1; i <= len; i++)
    {
        txbuf[i] = data[i-1];
    }
    i2c_xfer.WaitTimes = EE_TIMEOUT;
    i2c_xfer.TxBuf = txbuf; /* Put address which data will be read. */
    i2c_xfer.TxLen = len+1;

    I2C_MasterWriteBlocking(BOARD_I2C_PORT, &i2c_xfer);
}

void EE_writeBytes(uint8_t *data, uint32_t addr, uint32_t len)
{
    uint8_t page_offset;
    uint16_t num_page, num_singel;

    page_offset = EE_PAGE_SIZE - (addr % EE_PAGE_SIZE);

    /* If addres alignment by page. */
    if (page_offset == EE_PAGE_SIZE)
    {
        /* calculate need page numb */
        num_page = len / EE_PAGE_SIZE;
        /* calculate last page size. */
        num_singel = len % EE_PAGE_SIZE;

        /* if not enough one page, write now.*/
        if (num_page == 0)
        {
            EE_writeBlock(data, addr, num_singel);
        }
        else
        {
            while (num_page--)
            {
                EE_writeBlock(data, addr, EE_PAGE_SIZE);
                data += EE_PAGE_SIZE;
                addr += EE_PAGE_SIZE;
            }

            if (num_singel)
            {
                EE_writeBlock(data, addr, num_singel);
            }
        }
    }
    /* if not alignment by page. */
    else
    {
        printf("ee write addre not alignment \r\n");
    }
}

/*
 * Compare block of data with data stored in eeprom.
 *
 * @param data Pointer to data buffer, which will be compared.
 * @param addr Address of data in eeprom, which will be compared.
 * @param len Length of the data block to be compared.
 *
 * @return 0 - comparision failed.
 * @return 1 - data are equal.
 */
static uint8_t EE_verifyBlock(uint8_t *data, uint32_t addr, uint32_t len){
    uint8_t equal = 1;
    I2C_MasterXfer_Type i2c_xfer;
    uint8_t txbuf[2] = {addr, 0};
    uint8_t rxbuf[128u];

    i2c_xfer.WaitTimes = EE_TIMEOUT;
    i2c_xfer.TxBuf = txbuf; /* Put address which data will be read. */
    i2c_xfer.RxBuf = rxbuf;
    i2c_xfer.RxLen = len;

    I2C_MasterReadBlocking(BOARD_I2C_PORT, &i2c_xfer);

    for (uint32_t i = 0; i < len; i++)
    {
        if (rxbuf[i] != data[i])
        {
            equal = 0;
            return equal;
        }
    }

    return equal;
}

/**
 * OD function for accessing _Store parameters_ (index 0x1010) from SDO server.
 *
 * For more information see file CO_SDO.h.
 */
static CO_SDO_abortCode_t CO_ODF_1010(CO_ODF_arg_t *ODF_arg){
    CO_EE_t *ee;
    uint32_t value;
    CO_SDO_abortCode_t ret = CO_SDO_AB_NONE;

    ee = (CO_EE_t*) ODF_arg->object;
    value = CO_getUint32(ODF_arg->data);

    if(!ODF_arg->reading){
        /* don't change the old value */
        CO_memcpy(ODF_arg->data, (const uint8_t*)ODF_arg->ODdataStorage, 4U);

        if(ODF_arg->subIndex == 1U){
            if(value == PARAM_STORE_PASSWORD){
                EE_MBR_t MBR;
                /* read the master boot record from the last page in eeprom */
                EE_readBlock((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR));
                /* if EEPROM is not yet initilalized, enable it now */
                if(MBR.OD_EEPROMSize != ee->OD_EEPROMSize)
                    ee->OD_EEPROMWriteEnable = true;

                /* prepare MBR */
                MBR.OD_ROMCRC = crc16_ccitt(ee->OD_ROMAddress, ee->OD_ROMSize, 0);
                MBR.OD_EEPROMSize = ee->OD_EEPROMSize;
                MBR.OD_ROMSize = ee->OD_ROMSize;

                /* write MBR and OD_ROM  to eeprom (blocking function) */
                EE_writeBytes((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR));
                EE_writeBytes(ee->OD_ROMAddress, EE_ROMADDR, ee->OD_ROMSize);
                /* write ee->OD_ROMAddress, ee->OD_ROMSize to eeprom (blocking function) */

                /* verify data */
                if( EE_verifyBlock(ee->OD_ROMAddress, EE_ROMADDR, ee->OD_ROMSize) == 1
                    && EE_verifyBlock((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR)) == 1){
                    /* write successfull */
                    return CO_SDO_AB_NONE;
                }
                return CO_SDO_AB_HW;
            }
            else{
                ret = CO_SDO_AB_DATA_TRANSF;
            }
        }
    }

    return ret;
}


/**
 * OD function for accessing _Restore default parameters_ (index 0x1011) from SDO server.
 *
 * For more information see file CO_SDO.h.
 */
static CO_SDO_abortCode_t CO_ODF_1011(CO_ODF_arg_t *ODF_arg){
    CO_EE_t *ee;
    uint32_t value;
    CO_SDO_abortCode_t ret = CO_SDO_AB_NONE;

    ee = (CO_EE_t*) ODF_arg->object;
    value = CO_getUint32(ODF_arg->data);

    if(!ODF_arg->reading){
        /* don't change the old value */
        CO_memcpy(ODF_arg->data, (const uint8_t*)ODF_arg->ODdataStorage, 4U);

        if(ODF_arg->subIndex >= 1U){
            if(value == PARAM_RESTORE_PASSWORD){
                /* Clear the eeprom */
                EE_MBR_t MBR;

                /* read the master boot record from the last page in eeprom */
                EE_readBlock((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR));
                /* verify MBR for safety */
                if(EE_verifyBlock((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR)) == 0){
                    return CO_SDO_AB_HW;
                }

                switch(ODF_arg->subIndex){
                    case 0x01: MBR.OD_ROMSize = 0;              break; /* clear the ROM */
                    /* following don't work, if not enabled in object dictionary */
                    case 0x77: MBR.OD_ROMSize = ee->OD_ROMSize; break; /* restore the ROM back */
                    case 0x7F: MBR.OD_EEPROMSize = 0;           break; /* clear EEPROM */
                    default: return 0x06090011;                        /* Sub-index does not exist. */
                }

                /* write changed MBR */
                EE_writeBytes((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR));

                /* verify MBR and status register */
                if(EE_verifyBlock((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR)) == 1){
                    /* write successfull */
                    return CO_SDO_AB_NONE;
                }
                else{
                    return CO_SDO_AB_HW;
                }

            }
            else{
                ret = CO_SDO_AB_DATA_TRANSF;
            }
        }
    }

    return ret;
}

/******************************************************************************/
CO_ReturnError_t CO_EE_init_1(
        CO_EE_t                *ee,
        uint8_t                *OD_EEPROMAddress,
        uint32_t                OD_EEPROMSize,
        uint8_t                *OD_ROMAddress,
        uint32_t                OD_ROMSize)
{
    /* verify arguments */
    if(ee==NULL || OD_EEPROMAddress==NULL || OD_ROMAddress==NULL){
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }

    /* verify variables */
    if(OD_EEPROMSize > EE_SIZE/2){
        OD_EEPROMSize = EE_SIZE/2;
    }
    if(OD_ROMSize > (EE_SIZE/2 - EE_MBR_SIZE)) {
        OD_ROMSize = EE_SIZE/2 - EE_MBR_SIZE;
    }

    /* Configure eeprom */
    /* Setup I2C initialization values. */
    I2C_Master_Init_Type i2c_init;
    i2c_init.ClockFreqHz = BOARD_I2C_FREQ;
    i2c_init.BaudRate = I2C_BaudRate_100K;

    /* Initialize I2C master. */
    I2C_InitMaster(BOARD_I2C_PORT, &i2c_init);

    /* The target device address needs to be configured before enabling. */
    I2C_SetTargetAddr(BOARD_I2C_PORT, EE_DEVICE_ADDR);

    /* Enable I2C. */
    I2C_Enable(BOARD_I2C_PORT, true);

    /* configure object variables */
    ee->OD_EEPROMSize = OD_EEPROMSize;
    ee->OD_EEPROMAddress = OD_EEPROMAddress;
    ee->OD_ROMAddress = OD_ROMAddress;
    ee->OD_ROMSize = OD_ROMSize;
    ee->OD_EEPROMCurrentIndex = 0U;
    ee->OD_EEPROMWriteEnable = false;

    /* read the master boot record from the last page in eeprom */
    EE_MBR_t MBR;
    EE_readBlock((uint8_t*)&MBR, EE_SIZE - EE_MBR_SIZE, sizeof(MBR));

    /* read the CO_OD_EEPROM from EEPROM, first verify, if data are OK */
    if(MBR.OD_EEPROMSize == OD_EEPROMSize && (MBR.OD_ROMSize == OD_ROMSize || MBR.OD_ROMSize == 0)){
        uint32_t firstWordRAM = *((uint32_t*)OD_EEPROMAddress);
        uint32_t firstWordEE, lastWordEE;
        EE_readBlock((uint8_t*)&firstWordEE, 0, 4);
        EE_readBlock((uint8_t*)&lastWordEE, OD_EEPROMSize-4, 4);
        if(firstWordRAM == firstWordEE && firstWordRAM == lastWordEE){
            EE_readBlock(OD_EEPROMAddress, 0, OD_EEPROMSize);
            ee->OD_EEPROMWriteEnable = true;
        }
        else{
            return CO_ERROR_DATA_CORRUPT;
        }
    }
    else{
        return CO_ERROR_DATA_CORRUPT;
    }

    /* read the CO_OD_ROM from EEPROM and verify CRC */
   if(MBR.OD_ROMSize == OD_ROMSize){
       EE_readBlock(OD_ROMAddress, EE_ROMADDR, OD_ROMSize);
       if(crc16_ccitt(OD_ROMAddress, OD_ROMSize, 0) != MBR.OD_ROMCRC){
           return CO_ERROR_CRC;
       }
   }

    return CO_ERROR_NO;
}


/******************************************************************************/
void CO_EE_init_2(
        CO_EE_t                *ee,
        CO_ReturnError_t        eeStatus,
        CO_SDO_t               *SDO,
        CO_EM_t                *em)
{
    CO_OD_configure(SDO, OD_H1010_STORE_PARAM_FUNC, CO_ODF_1010, (void*)ee, 0, 0U);
    CO_OD_configure(SDO, OD_H1011_REST_PARAM_FUNC, CO_ODF_1011, (void*)ee, 0, 0U);
    if(eeStatus != CO_ERROR_NO){
        CO_errorReport(em, CO_EM_NON_VOLATILE_MEMORY, CO_EMC_HARDWARE, (uint32_t)eeStatus);
    }
}


/******************************************************************************/
void CO_EE_process(CO_EE_t *ee){
    if((ee != 0) && (ee->OD_EEPROMWriteEnable)){
        uint32_t i;
        uint8_t RAMdata, eeData;

        /* verify next word */
        if(++ee->OD_EEPROMCurrentIndex == ee->OD_EEPROMSize){
            ee->OD_EEPROMCurrentIndex = 0U;
        }
        i = ee->OD_EEPROMCurrentIndex;

        /* read eeprom */
        RAMdata = ee->OD_EEPROMAddress[i];
        EE_readBlock(&eeData, i, 1U);

        /* if bytes in EEPROM and in RAM are different, then write to EEPROM */
        if(eeData != RAMdata){
            EE_writeBlock(&RAMdata, i, 1U);
        }
    }
}
