/**
 * eeprom_emulation.c
 *
 * Uses the microcontroller Flash memory as if it were EEPROM. All "EEPROM"
 * variables are saved in Flash with an associated variable
 * address. Saving a new value to a particular address actually
 * just stores a new variable/address combo.
 * When reading, the most recently written value is returned. If
 * no value was written, a default value (passed to the read
 * function) is returned instead.
 * Erases are only performed when a page becomes completely full.
 * The most recent saved value for each variable is transferred to
 * the alternate storage page, and the alternate page is marked as
 * active.
 * 
 * Copyright (c) 2022 Bigscreen, Inc.
 */

#include <asf.h>
#include <stdlib.h>
#include "flash_mutex.h"
#include "eeprom_emulation.h"

SemaphoreHandle_t EEPROM_Mutex_Handle; // Only used for read/write to eeprom
// separate flash mutex used for all flash operations ("flash_mutex.h/c")

uint32_t EEPROM_Keys[] = {
    (uint32_t)EEKEY_TotalPoweredOnTime10min,
    (uint32_t)EEKEY_TotalDisplaysOnTime10min_Left,
    (uint32_t)EEKEY_LongestDisplaysOnTime10min,
	(uint32_t)EEKEY_TotalDisplaysOnTime10min_Right,
    (uint32_t)EEKEY_NumTimesBlock0Erased,
    (uint32_t)EEKEY_NumTimesBlock1Erased
};

uint32_t Num_EEPROM_Keys = 5;

bool EEPROM_Finished_Init = false;

/* Forward declaration of private functions */
static uint32_t write64bit(uint32_t addr, uint32_t data0, uint32_t data1);
static uint32_t eraseblock(EEPROM_Block_T block);
static EEPROM_Status_T getblockstatus(EEPROM_Block_T block);
static uint32_t setblockstatus(EEPROM_Block_T block, EEPROM_Status_T status);
static uint32_t checkblockerasedandfix(EEPROM_Block_T block);
static uint32_t formateeprom(void);
static uint32_t blocktransfer(EEPROM_Block_T source, EEPROM_Block_T dest);
static EEPROM_Read_Status_T readvariable(EEPROM_Block_T block, uint32_t key, uint32_t* data);
static EEPROM_Write_Status_T writevariable(EEPROM_Block_T block, uint32_t key, uint32_t data);
static EEPROM_Block_T findvalidblock(EEPROM_Operation_T oper);

/* Private function definitions */
static uint32_t write64bit(uint32_t addr, uint32_t data0, uint32_t data1) {
    uint32_t retval;
    // writes two 32-bit words to a 64-bit section of Flash. 
    // 64 bits is the minimum write size

    uint32_t place_within_page = addr % PAGE_SIZE;
    uint32_t flash_page = addr / PAGE_SIZE;
    // First write to the latch buffer. Any page in Flash works the 
    // same. Just using the first page for simplicity

    Flash_Mutex_Lock();
    uint32_t* first_flash_page = (uint32_t*)(IFLASH_ADDR);
    first_flash_page[(place_within_page/sizeof(uint32_t))] = data0;
    first_flash_page[(place_within_page/sizeof(uint32_t)) + 1] = data1;

    // then issue the flash write command
    retval =  efc_perform_command(EFC, EFC_FCMD_WP, flash_page);
    Flash_Mutex_Unlock();

    return retval;
}

static uint32_t eraseblock(EEPROM_Block_T block) {
    uint32_t retval;
	uint32_t farg;
    if(block == EEPROM_Block0) {
        farg = ((EEPROM_BLOCK0_START_PAGE / 16)<<4) | 0x02;
    } else {
        farg = ((EEPROM_BLOCK1_START_PAGE / 16)<<4) | 0x02;
    }
	
	Flash_Mutex_Lock();
	retval = efc_perform_command(EFC, EFC_FCMD_EPA, farg);
	Flash_Mutex_Unlock();
	return retval;
}

static EEPROM_Status_T getblockstatus(EEPROM_Block_T block) {
    uint32_t* block_start;
    EEPROM_Status_T retval = EEPROM_Invalid;
    if(block == EEPROM_Block0) {
        block_start = (uint32_t*)(EEPROM_BLOCK0_START);
    } else {
        block_start = (uint32_t*)(EEPROM_BLOCK1_START);
    }
    // Status flags - 
    // When page is empty, it will just have the default Flash
    // erased values of 0xFF for all bytes. We can check for empty
    // by comparing the first 8 bytes (64-bits, minimum write size)
    // to 0xFF
    if((block_start[0] == FLASH_ERASED_32B) && (block_start[1] == FLASH_ERASED_32B)) {
        retval = EEPROM_Erased;
    }
    // When the page has been marked to transfer data, the first 8 bytes
    // are flagged with a special value
    if((block_start[0] == FLASH_TAGGED_32B) && (block_start[1] == FLASH_TAGGED_32B)) {
        retval = EEPROM_Transferring;
    }
    // And when the first 8 bytes are set to zero, this is the active page
    // and is receiving any new data values saved
    if((block_start[0] == 0u) && (block_start[1] == 0u)) {
        retval = EEPROM_Active;
    }

    // If any other combination is set for those first 8 bytes, then 
    // the status is invalid. retval will not have been changed from
    // its initial "EE_Page_Invalid" value

    return retval;
}

static uint32_t setblockstatus(EEPROM_Block_T block, EEPROM_Status_T status) {
    // Sets the first 64 bit section of a block to the status value.
    // Status is a 32-bit value, so it's doubled (copying the value) 
    // to make it 64-bit.

    // Only two statuses can actually be set: Transferring and Active
    // Other statuses (Erased and Inactive) cannot be set. Erased can
    // only occur after the Flash block is physically erased.
    // Invalid cannot be set, it's a status for any other value that was
    // erroneously set.

    uint32_t wval;
    switch(status) {
        case EEPROM_Active:
            wval = 0;
        break;

        case EEPROM_Transferring:
            wval = FLASH_TAGGED_32B;
        break;

        default:
            // Error if not Active or Transferring trying to be set
            return EFC_RC_ERROR;
        break;
    }

    uint32_t addr;
    // Figure out the page number we are programming.
    if(block == EEPROM_Block0) {
        addr = EEPROM_BLOCK0_START;
    } else {
        addr = EEPROM_BLOCK1_START;
    }

    return write64bit(addr, wval, wval);

}

static uint32_t checkblockerasedandfix(EEPROM_Block_T block) {
    // On startup, check if a block that is labeled as erased really truly
    // is erased. Some weird shutdown or other error could cause a
    // non-erased block to appear to be erased. 
    // This function confirms that the entire block actually is erased
    // by checking that every byte is 0xFF (erased Flash). If anything doesn't match
    // the expected value for erased Flash then the block is erased.
	uint32_t retval;
    uint32_t* block_mem;
    uint32_t block_erase_count = 0;
    uint32_t block_erase_key = 0;
	EEPROM_Block_T otherblock;
    EEPROM_Write_Status_T writestatus;
    if(block == EEPROM_Block0) {
		otherblock = EEPROM_Block1;
        block_mem = (uint32_t*) EEPROM_BLOCK0_START;
        block_erase_key = EEKEY_NumTimesBlock0Erased;
    } else {
		otherblock = EEPROM_Block0;
        block_mem = (uint32_t*) EEPROM_BLOCK1_START;
        block_erase_key = EEKEY_NumTimesBlock1Erased;
    }

    for(uint32_t i = 0; i < (BLOCK_SIZE / sizeof(uint32_t)); i++) {
        if(block_mem[i] != FLASH_ERASED_32B) {
            retval = eraseblock(block);
            if(retval != EFC_RC_OK) return retval;

            // Increment the block erasure count
            //if(EEPROM_Found == EEPROM_Read(block_erase_key, &block_erase_count)) {
			if(EEPROM_Found == readvariable(otherblock, block_erase_key, &block_erase_count)) {
                //writestatus = EEPROM_Write(block_erase_key, block_erase_count + 1);
				writestatus = writevariable(otherblock, block_erase_key, block_erase_count + 1);
            } else {
                //writestatus = EEPROM_Write(block_erase_key, 1);
				writestatus = writevariable(otherblock, block_erase_key, 1);
            }
            if(writestatus != EEPROM_Write_Success) return EFC_RC_ERROR;

            return EFC_RC_OK;
        }
    }

    return EFC_RC_OK;
}

static uint32_t formateeprom(void) {
    uint32_t retval;
    EEPROM_Write_Status_T writestatus;
    // Deletes both blocks.
    // Assigns block 0 as the active block.
    retval = eraseblock(EEPROM_Block0);
    if(retval != EFC_RC_OK) return retval;

    retval = eraseblock(EEPROM_Block1);
    if(retval != EFC_RC_OK) return retval;

    retval = setblockstatus(EEPROM_Block0, EEPROM_Active);
    if(retval != EFC_RC_OK) return retval;

    // Now set the erasure counts to 1 for both blocks.
    //writestatus = EEPROM_Write(EEKEY_NumTimesBlock0Erased, 1);
	writestatus = writevariable(EEPROM_Block0, EEKEY_NumTimesBlock0Erased, 1);
    if(writestatus != EEPROM_Write_Success) return EFC_RC_ERROR;
    
    //retval = EEPROM_Write(EEKEY_NumTimesBlock1Erased, 1);
	writestatus = writevariable(EEPROM_Block0, EEKEY_NumTimesBlock1Erased, 1);
    if(writestatus != EEPROM_Write_Success) return EFC_RC_ERROR;

    return EFC_RC_OK;
}

static uint32_t blocktransfer(EEPROM_Block_T source, EEPROM_Block_T dest) {
    uint32_t retval;
	uint32_t eeprom_dat;
    EEPROM_Read_Status_T readstatus;
    EEPROM_Write_Status_T writestatus;
    // Transfers all data members in the list EEPROM_Keys from "source" block to
    // "dest" block.
    
    // Check if dest has already been tagged as transferring. If not, tag it.
    if(getblockstatus(dest) != EEPROM_Transferring) {
        retval = setblockstatus(dest, EEPROM_Transferring);
        if(retval != EFC_RC_OK) return retval;
    }

    // Check for every variable in our list. If found, transfer it to the new block
    for(uint32_t var_id = 0; var_id < Num_EEPROM_Keys; var_id++) {
        readstatus = readvariable(source, EEPROM_Keys[var_id], &eeprom_dat);
        if(readstatus == EEPROM_Read_Error) return EFC_RC_ERROR;

        if(readstatus == EEPROM_Found) {
            writestatus = writevariable(dest, EEPROM_Keys[var_id], eeprom_dat);
            if(writestatus != EEPROM_Write_Success) return EFC_RC_ERROR;
        }
    }

    // Erase the old block
    retval = eraseblock(source);
    if(retval != EFC_RC_OK) return retval;

    // Set the new block as active
    retval = setblockstatus(dest, EEPROM_Active);
    if(retval != EFC_RC_OK) return retval;

    // Increment the erased count
    uint32_t block_erase_key = (source == EEPROM_Block0) ? EEKEY_NumTimesBlock0Erased : EEKEY_NumTimesBlock1Erased;
    uint32_t block_erase_count = 0;
    //if(EEPROM_Found == EEPROM_Read(block_erase_key, &block_erase_count)) {
	if(EEPROM_Found == readvariable(dest, block_erase_key, &block_erase_count)) {
        //writestatus = EEPROM_Write(block_erase_key, block_erase_count + 1);
		writestatus = writevariable(dest, block_erase_key, block_erase_count + 1);
    } else {
        //writestatus = EEPROM_Write(block_erase_key, 1);
		writestatus = writevariable(dest, block_erase_key, 1);
    }
    if(writestatus != EEPROM_Write_Success) return EFC_RC_ERROR;

    return EFC_RC_OK;
}

static EEPROM_Read_Status_T readvariable(EEPROM_Block_T block, uint32_t key, uint32_t* data) {
    // Since data is written from low addresses to high addresses, we should search from
    // high to low to find the key. That way the first key found will be the most recent
    // data value.
    uint32_t offset_addr;
    uint32_t* memptr;

    switch(block) {
        case EEPROM_Block0:
            offset_addr = EEPROM_BLOCK0_START;
        break;

        case EEPROM_Block1:
            offset_addr = EEPROM_BLOCK1_START;
        break;

        default:
            return EEPROM_Read_Error;
        break;
    }

    for(uint32_t addr = (BLOCK_SIZE - EEPROM_VARIABLE_SIZE); addr > 0; addr -= EEPROM_VARIABLE_SIZE) {
        memptr = (uint32_t*)(offset_addr + addr);
        if(memptr[0] == key) {
            *data = memptr[1];
            return EEPROM_Found;
        }
    }

    return EEPROM_Not_Found;
}

static EEPROM_Write_Status_T writevariable(EEPROM_Block_T block, uint32_t key, uint32_t data) {
    // Find the first available spot to write the new data. Start searching from lowest
    // address excluding the first 8 bytes, which are the block tag.

    uint32_t offset_addr;
    uint32_t* memptr;

        switch(block) {
        case EEPROM_Block0:
            offset_addr = EEPROM_BLOCK0_START;
        break;

        case EEPROM_Block1:
            offset_addr = EEPROM_BLOCK1_START;
        break;

        default:
            return EEPROM_Write_Error;
        break;
    }

    for(uint32_t addr = EEPROM_VARIABLE_SIZE; addr < BLOCK_SIZE; addr += EEPROM_VARIABLE_SIZE) {
        memptr = (uint32_t*)(offset_addr + addr);
        if(memptr[0] == FLASH_ERASED_32B) {
            // This is a blank spot, now we can write the data here
            if(write64bit((uint32_t)memptr, key, data) == EFC_RC_OK) {
                return EEPROM_Write_Success;
            } else {
                return EEPROM_Write_Error;
            }
        }
    }

    // If we went all the way through the block and couldn't find a blank spot, the block is full
    // and it's time to transfer to the other block
    return EEPROM_Full;
}

static EEPROM_Block_T findvalidblock(EEPROM_Operation_T oper) {
    EEPROM_Status_T block0status, block1status;

    // Get block statuses
    block0status = getblockstatus(EEPROM_Block0);
    block1status = getblockstatus(EEPROM_Block1);

    // Write or read operation
    switch (oper) {
    case EEPROM_Write_Operation: 
        if (block1status == EEPROM_Active) {
            if (block0status == EEPROM_Transferring) {
                return EEPROM_Block0;
            } else {
                return EEPROM_Block1;
            }
        } else if (block0status == EEPROM_Active) {
            if (block1status == EEPROM_Transferring) {
                return EEPROM_Block1;
            } else {
                return EEPROM_Block0;
            }
        } else {
            return EEPROM_No_Block;
        }

    case EEPROM_Read_Operation:
        if (block0status == EEPROM_Active) {
            return EEPROM_Block0; 
        } else if (block1status == EEPROM_Active) {
            return EEPROM_Block1; 
        } else {
            return EEPROM_No_Block;
        }

    default:
        return EEPROM_No_Block;
    }
}

uint32_t EEPROM_Init(void) {

    EEPROM_Status_T block0status, block1status;
    uint32_t status;

	EEPROM_Mutex_Handle = xSemaphoreCreateMutex();
	if(NULL == EEPROM_Mutex_Handle){
		return EFC_RC_ERROR;
	}

    // Get block statuses
    block0status = getblockstatus(EEPROM_Block0);
    block1status = getblockstatus(EEPROM_Block1);

    // Check for invalid header states and repair if necessary
    switch (block0status) {
    case EEPROM_Erased:
        switch(block1status) {
        case EEPROM_Active: 
            // Confirm block0 is properly erased, and erase it if necessary
            status = checkblockerasedandfix(EEPROM_Block0);
            if(status != EFC_RC_OK) {
                return status;
            }
        break;

        case EEPROM_Transferring: 
            // Possible to get to this state if block1 is done receiving,
            // block0 was erased, but power was lost before block1 was
            // tagged as active
            // Confirm block0 is properly erased, and erase it if necessary
            status = checkblockerasedandfix(EEPROM_Block0);
            if (status != EFC_RC_OK) {
                return status;
            }
            status = setblockstatus(EEPROM_Block1, EEPROM_Active);
            if (status != EFC_RC_OK) {
                return status;
            }
        break;

        default:
            // Could be erased (both pages) or invalid
            // in either case, format EEPROM
            status = formateeprom();
            // If erase/program operation was failed, a Flash error code is returned
            if (status != EFC_RC_OK) {
                return status;
            }
        break;
        }
    break;

    case EEPROM_Transferring:
        switch(block1status) {
        case EEPROM_Active:
            // Transfer data from block1 to block0
            status = blocktransfer(EEPROM_Block1, EEPROM_Block0);
            if (status != EFC_RC_OK) {
                return status;
            }
        break;

        case EEPROM_Erased:
            // Possible to get to this state if block0 is done receiving,
            // block1 was erased, but power was lost before block0 was
            // tagged as active
            // Confirm block1 is properly erased, and erase it if necessary
            status = checkblockerasedandfix(EEPROM_Block1);
            if (status != EFC_RC_OK) {
                return status;
            }
            status = setblockstatus(EEPROM_Block0, EEPROM_Active);
            if (status != EFC_RC_OK) {
                return status;
            }
        break;

        default:
            // Anything else is an invalid state
            status = formateeprom();
        
            if (status != EFC_RC_OK) {
                return status;
            }
        break;
        }
    break;

    case EEPROM_Active:
        switch(block1status) {

        case EEPROM_Active: // Invalid state -> format eeprom
        
            // Erase both block0 and block1 and set block0 as valid page
            status = formateeprom();
            // If erase/program operation was failed, a Flash error code is returned
            if (status != EFC_RC_OK) {
                return status;
            }
        break;
        
        case EEPROM_Erased: // block0 valid, block1 erased
        
            // Erase block1 only if it was improperly erased.
            status = checkblockerasedandfix(EEPROM_Block1);
            // If erase operation was failed, a Flash error code is returned
            if (status != EFC_RC_OK) {
                return status;
            }
        break;

        default:
            // Transfer data from block0 to block1
            status = blocktransfer(EEPROM_Block0, EEPROM_Block1);
            if (status != EFC_RC_OK) {
                return status;
            }
        break;

        }
    break;

    default: // Any other state -> format eeprom
        // Erase both block0 and block1 and set block0 as valid page
        status = formateeprom();
        // If erase/program operation was failed, a Flash error code is returned
        if (status != EFC_RC_OK) {
            return status;
        }
    break;
    }

	EEPROM_Finished_Init = true;
    return EFC_RC_OK;
}

EEPROM_Read_Status_T EEPROM_Read(uint32_t key, uint32_t* data) {

    EEPROM_Block_T block;
	EEPROM_Read_Status_T retstatus;
	
	if(!EEPROM_Finished_Init)
		return EEPROM_Read_Not_Init;
	
	xSemaphoreTake(EEPROM_Mutex_Handle, portMAX_DELAY);
    // check which block is active
    block = findvalidblock(EEPROM_Read_Operation);

    retstatus = readvariable(block, key, data);
	xSemaphoreGive(EEPROM_Mutex_Handle);
	
	return retstatus;
	
}

EEPROM_Write_Status_T EEPROM_Write(uint32_t key, uint32_t data) {
    EEPROM_Block_T block;
    EEPROM_Write_Status_T writestatus;
    uint32_t retval;

	if(!EEPROM_Finished_Init)
		return EEPROM_Write_Not_Init;

	xSemaphoreTake(EEPROM_Mutex_Handle, portMAX_DELAY);

    // check which block is active
    block = findvalidblock(EEPROM_Write_Operation);

    // Attempt to write the variable
    writestatus = writevariable(block, key, data);

    if(EEPROM_Full == writestatus) {
        // Try transferring blocks
        EEPROM_Block_T other_block = (block == EEPROM_Block0) ? EEPROM_Block1 : EEPROM_Block0;
        retval = blocktransfer(block, other_block);
        if(retval != EFC_RC_OK) {
			writestatus = EEPROM_Write_Error;
		} else {
			// Try writing again
			writestatus = writevariable(other_block, key, data);
			// And if at this point the other block is full, we have no chance to save this
			// variable anyway. Can't attempt transferring again.
		}
    }
	xSemaphoreGive(EEPROM_Mutex_Handle);
	
    return writestatus;
}

EEPROM_Block_T EEPROM_Which_Block_Active(void) {
	// Super simple calculation here. Just check if Block0 is active. If not
	// assume Block1 is. Don't need to get all complicated with edge cases here.
	
	if(getblockstatus(EEPROM_Block0) == EEPROM_Active) {
		return EEPROM_Block0;
	}
	return EEPROM_Block1;
}
