/*
 * Copyright 2017 NXP
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * o Redistributions of source code must retain the above copyright notice, this list
 *   of conditions and the following disclaimer.
 *
 * o Redistributions in binary form must reproduce the above copyright notice, this
 *   list of conditions and the following disclaimer in the documentation and/or
 *   other materials provided with the distribution.
 *
 * o Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from this
 *   software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "fsl_spifi.h"
#include "mflash_drv.h"
#include "pin_mux.h"
#include <stdbool.h>

/* Command ID */
#define COMMAND_NUM                 (6)
#define READ                        (0)
#define PROGRAM_PAGE                (1)
#define GET_STATUS                  (2)
#define ERASE_SECTOR                (3)
#define WRITE_ENABLE                (4)
#define WRITE_REGISTER              (5)


//#ifdef XIP_IMAGE
//#warning NOTE: MFLASH driver expects that application runs from XIP
//#else
//#warning NOTE: MFLASH driver expects that application runs from SRAM
//#endif


/* Temporary sector storage. Use uint32_t type to force 4B alignment and 
 * improve copy operation */
static uint32_t g_flashm_sector[MFLASH_SECTOR_SIZE / sizeof(uint32_t)];


/* Commands definition, taken from SPIFI demo */
static spifi_command_t command[COMMAND_NUM] = {
    /* read */
    {MFLASH_PAGE_SIZE, false, kSPIFI_DataInput, 1, kSPIFI_CommandAllSerial, kSPIFI_CommandOpcodeAddrThreeBytes, 0x0B},
    /* program */
    {MFLASH_PAGE_SIZE, false, kSPIFI_DataOutput, 0, kSPIFI_CommandAllSerial, kSPIFI_CommandOpcodeAddrThreeBytes, 0x2},
    /* status */
    {1, false, kSPIFI_DataInput, 0, kSPIFI_CommandAllSerial, kSPIFI_CommandOpcodeOnly, 0x05},
    /* erase */
    {0, false, kSPIFI_DataOutput, 0, kSPIFI_CommandAllSerial, kSPIFI_CommandOpcodeAddrThreeBytes, 0x20},
    /* write enable */
    {0, false, kSPIFI_DataOutput, 0, kSPIFI_CommandAllSerial, kSPIFI_CommandOpcodeOnly, 0x06},
    /* write register */
    {4, false, kSPIFI_DataOutput, 0, kSPIFI_CommandAllSerial, kSPIFI_CommandOpcodeOnly, 0x01}
};


/* Wait until command finishes */
static inline void mflash_drv_check_if_finish(void)
{
    uint8_t val = 0;
    do
    {
        SPIFI_SetCommand(MFLASH_SPIFI, &command[GET_STATUS]);
        while ((MFLASH_SPIFI->STAT & SPIFI_STAT_INTRQ_MASK) == 0U)
        {
        }
        val = SPIFI_ReadDataByte(MFLASH_SPIFI);
    } while (val & 0x1);
}

/* return offset from sector */
static void mflash_drv_read_mode(void)
{
    /* Switch back to read mode */
    SPIFI_ResetCommand(MFLASH_SPIFI);
    SPIFI_SetMemoryCommand(MFLASH_SPIFI, &command[READ]);
}


/* Initialize SPIFI & flash peripheral, 
 * cannot be invoked directly, requires calling wrapper in non XIP memory */
static int32_t mflash_drv_init_internal(void)
{
    /* NOTE: Multithread access is not supported for SRAM target.
     *       XIP target MUST be protected by disabling global interrupts 
     *       since all ISR (and API that is used inside) is placed at XIP. 
     *       It is necessary to place at least "mflash_drv_drv.o", "fsl_spifi.o" to SRAM */
    /* disable interrupts when running from XIP 
     * TODO: store/restore previous PRIMASK on stack to avoid
     * failure in case of nested critical sections !! */
    __asm("cpsid i");
    spifi_config_t config = {0};

#ifndef XIP_IMAGE
    uint32_t sourceClockFreq;
    BOARD_InitSPIFI();
    /* Reset peripheral */
    RESET_PeripheralReset(kSPIFI_RST_SHIFT_RSTn);
    /* Set SPIFI clock source */
    CLOCK_AttachClk(kFRO_HF_to_SPIFI_CLK);
    sourceClockFreq = CLOCK_GetSpifiClkFreq();
    /* Set the clock divider */
    CLOCK_SetClkDiv(kCLOCK_DivSpifiClk, sourceClockFreq / MFLASH_BAUDRATE, false);
    /* Enable SPIFI clock */
    CLOCK_EnableClock(kCLOCK_Spifi);
#endif

    SPIFI_GetDefaultConfig(&config);
    config.dualMode = kSPIFI_DualMode;
#ifdef XIP_IMAGE
    config.disablePrefetch = false; // true;
    config.disableCachePrefech = false; // true;
#else
    config.disablePrefetch = false; // true;
    config.disableCachePrefech = false; // true;
#endif

    /* Reset the Command register */
    SPIFI_ResetCommand(MFLASH_SPIFI);

    /* Set time delay parameter */
    MFLASH_SPIFI->CTRL = SPIFI_CTRL_TIMEOUT(config.timeout) | SPIFI_CTRL_CSHIGH(config.csHighTime) |
                 SPIFI_CTRL_D_PRFTCH_DIS(config.disablePrefetch) | SPIFI_CTRL_MODE3(config.spiMode) |
                 SPIFI_CTRL_PRFTCH_DIS(config.disableCachePrefech) | SPIFI_CTRL_DUAL(config.dualMode) |
                 SPIFI_CTRL_RFCLK(config.isReadFullClockCycle) | SPIFI_CTRL_FBCLK(config.isFeedbackClock);

    mflash_drv_read_mode();

    __asm("cpsie i");
    return 0;
}


/* API - initialize 'mflash' */
int32_t mflash_drv_init(void)
{
    volatile int32_t result;
    /* Necessary to have double wrapper call in non_xip memory */
    result = mflash_drv_init_internal();
    return result;
}


/* Internal - erase single sector */
static int32_t mflash_drv_sector_erase(uint32_t sector_addr)
{
    if (false == mflash_drv_is_sector_aligned((uint32_t)sector_addr))
        return -1;

    __asm("cpsid i");

    /* Reset the SPIFI to switch to command mode */
    SPIFI_ResetCommand(MFLASH_SPIFI);

    /* Write enable */
    SPIFI_SetCommand(MFLASH_SPIFI, &command[WRITE_ENABLE]);
    /* Set address */
    SPIFI_SetCommandAddress(MFLASH_SPIFI, sector_addr);
    /* Erase sector */
    SPIFI_SetCommand(MFLASH_SPIFI, &command[ERASE_SECTOR]);
    /* Check if finished */
    mflash_drv_check_if_finish();
    /* Switch to read mode to enable interrupts as soon ass possible */
    mflash_drv_read_mode();

    __asm("cpsie i");
    /* Flush pipeline to allow pending interrupts take place 
     * before starting next loop */
    __ISB();

    return 0;
}


/* Internal - write single sector */
static int32_t mflash_drv_sector_program(uint32_t sector_addr, uint32_t *sector_data)
{
    if (false == mflash_drv_is_sector_aligned((uint32_t)sector_addr))
        return -1;

    uint32_t max_page = MFLASH_SECTOR_SIZE / MFLASH_PAGE_SIZE;
    for (uint32_t page_idx = 0, page_i = 0; page_idx < max_page; page_idx++)
    {
        __asm("cpsid i");
        /* Program page */
        SPIFI_ResetCommand(MFLASH_SPIFI);
        SPIFI_SetCommand(MFLASH_SPIFI, &command[WRITE_ENABLE]);
        SPIFI_SetCommandAddress(MFLASH_SPIFI, sector_addr + page_idx * MFLASH_PAGE_SIZE);
        SPIFI_SetCommand(MFLASH_SPIFI, &command[PROGRAM_PAGE]);
        page_i = page_idx * (MFLASH_PAGE_SIZE/sizeof(sector_data[0]));
        /* Store 4B in each loop. Sector has always 4B alignment and size multiple of 4 */
        for (uint32_t i = 0; i < MFLASH_PAGE_SIZE/sizeof(sector_data[0]); i++)
        {
            SPIFI_WriteData(MFLASH_SPIFI, sector_data[page_i + i]);
        }
        mflash_drv_check_if_finish();
        /* Switch to read mode to enable interrupts as soon ass possible */
        mflash_drv_read_mode();
        __asm("cpsie i");
        /* Flush pipeline to allow pending interrupts take place 
         * before starting next loop */
        __ISB();
    }



    return 0;
}


/* Internal - write data of 'data_len' to single sector 'sector_addr', starting from 'sect_off' */
int32_t mflash_drv_sector_write(uint32_t sector_addr, uint32_t sect_off, uint8_t *data, uint32_t data_len)
{
    /* Address not aligned to sector boundary */
    if (false == mflash_drv_is_sector_aligned((uint32_t)sector_addr))
        return -1;
    /* Offset + length exceeed sector size */
    if (sect_off + data_len > MFLASH_SECTOR_SIZE)
        return -1;

    /* Switch back to read mode */
    SPIFI_ResetCommand(MFLASH_SPIFI);
    SPIFI_SetMemoryCommand(MFLASH_SPIFI, &command[READ]);

    /* Copy old sector data by 4B in each loop to buffer */
    for (uint32_t i = 0; i < sizeof(g_flashm_sector)/sizeof(g_flashm_sector[0]); i++)
    {
        g_flashm_sector[i] = *((uint32_t*)(sector_addr) + i);
    }

    /* Copy custom data ( 1B in each loop ) to buffer at specific position */
    for (uint32_t i = 0; i < data_len; i++)
    {
        ((uint8_t*)g_flashm_sector)[sect_off + i] = data[i];
    }

    /* Erase flash */
    if (0 != mflash_drv_sector_erase(sector_addr))
        return -2;

    /* Program data */
    if (0 != mflash_drv_sector_program(sector_addr, g_flashm_sector))
        return -2;

    /* Switch back to read mode */
    SPIFI_ResetCommand(MFLASH_SPIFI);
    SPIFI_SetMemoryCommand(MFLASH_SPIFI, &command[READ]);
    return 0;
}


/* Write data to flash, cannot be invoked directly, requires calling wrapper in non XIP memory */
int32_t mflash_drv_write_internal(void *any_addr, uint8_t *data, uint32_t data_len)
{

    uint32_t sect_size = MFLASH_SECTOR_SIZE;
    /* Interval <0, sector_size) */
    uint32_t to_write = 0;
    /* Interval (data_len, 0>  */
    uint32_t to_remain = data_len;
    int32_t result = 0;

    for (
         /* Calculate address of first sector */
         uint32_t sect_a = mflash_drv_addr_to_sector_addr((uint32_t)any_addr),
         /* and first sector offset */
         sect_of = mflash_drv_addr_to_sector_of((uint32_t)any_addr),
         /* and set first data offset to 0*/
         data_of = 0;
         /* Continue until sector address exceed target adddress + data_length */
         sect_a < ((uint32_t)any_addr) + data_len;
         /* Move to next sector */
         sect_a += sect_size,
         /* and move pointer to data */
         data_of += to_write
    )
    {
        /* If remaining data is exceed 'sector_size', write 'sector_size' length */
        if (to_remain > sect_size - sect_of)
        {
            to_write = sect_size - sect_of;
            to_remain = to_remain - to_write;
        }
        /* else write remaining data length */
        else
        {
            to_write = to_remain;
            to_remain = 0;
        }

        /* Write at 'sect_a' sector, starting at 'sect_of' using '&data[data_of]' of length 'to_write' */
        result = mflash_drv_sector_write(sect_a, sect_of, &data[data_of], to_write);
        if (0 != result)
            return -1;
        /* Only first sector is allowed to have an offset */
        sect_of = 0;
    }

    return 0;
}


/* Calling wrapper for 'mflash_drv_write_internal'.
 * Write 'data' of 'data_len' to 'any_addr' - which doesn't have to be sector aligned.
 * NOTE: Don't try to store constant data that are located in XIP !!
 */
int32_t mflash_drv_write(void *any_addr, uint8_t *data, uint32_t data_len)
{
    volatile int32_t result;
    result = mflash_drv_write_internal(any_addr, data, data_len);
    return result;
}



#if 0
/* Dummy test to prove functionality */
volatile uint32_t lock2 = 1;
char * tmp_string = "Curabitur sit amet justo ac velit consectetur lobortis. Donec porttitor, eros sed sollicitudin viverra, massa enim placerat sapien, et vestibulum lacus quam et leo. Proin gravida";

void mflash_drv_do_test(void)
{
    lock2 = 1;
    while (lock2);

    // place to SRAM !!!
#ifndef XIP_IMAGE
    BOARD_InitSPIFI();
#endif

    /* Reset peripheral */
    lock2 = 1;
    while (lock2);
    mflash_drv_init();

    lock2 = 1;
    while (lock2)
    mflash_drv_write((void*)(0x10800010), tmp_string, 30);//TODO force read to recover

    lock2 = 1;
    while (lock2);
}
#endif