/** 
 * @file    fs.c
 * @brief   File system using non-volatile external memory.
 * @author  Kenny Tran
 * @date    Mar 04 2024
 * @version 0.0.1
 *
 * @section LICENSES
 * Copyright (C) Morgan Advanced Programmable Systems, Inc.
 */


/*--------------------------------------------------------------------------------------------------
 * INCLUDE FILES
 *------------------------------------------------------------------------------------------------*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include <zephyr/kernel.h>

#include "../lib/crc.h"
#include "../lib/errno.h"
#include "../lib/print.h"

#include "../bsp/nvm_ext.h"

#include "fs.h"


/*--------------------------------------------------------------------------------------------------
 * MACROS & CONSTANTS
 *------------------------------------------------------------------------------------------------*/
/**
 * @brief Size of flash space allocated to store metadata.
 */
#define _FS_META_SIZE               (0x1000)
/**
 * @brief Starting address of metadata.
 */
#define _FS_META_START_ADDR         (NVM_EXT_START_ADDR)
/**
 * @brief Ending address of metadata.
 */
#define _FS_META_END_ADDR           (_FS_META_START_ADDR + _FS_META_SIZE - 1)
/**
 * @brief Starting address for file storage.
 */
#define _FS_START_ADDR              (_FS_META_END_ADDR + 1)
/**
 * @brief Ending address for file storage.
 */
#define _FS_END_ADDR                (NVM_EXT_START_ADDR + NVM_EXT_SIZE - 1)
/**
 * @brief Size of flash space allocated to store files.
 */
#define _FS_SIZE                    (_FS_END_ADDR - _FS_START_ADDR + 1)
/**
 * @brief Minimum sectors allocated for a file for wear-leveling.
 */
#define _FS_SECTOR_ALLOCATION_MIN   (1)


/*--------------------------------------------------------------------------------------------------
 * TYPE DEFINITIONS & ENUMERATIONS
 *------------------------------------------------------------------------------------------------*/
/**
 * @brief Struct containing necessary metadata for each file.
 * 
 * @note  Packed and align to nearest 1 byte to save padding/alignment space.  No need to align by
 *        word size for performance as this is read only once at startup.
 */
typedef struct
{
    char     name[(FS_FILENAME_LEN + 1)];
    uint16_t size;
    uint32_t start_addr;
    uint32_t end_addr;
    uint32_t checksum;
} __attribute__((packed, aligned(1))) _fs_meta_s;

/**
 * @brief File structure used to keep records and wear leveling.
 */
typedef struct
{
    _fs_meta_s meta;
    uint32_t   newest_idx_addr;
    uint32_t   empty_idx_addr;
    uint32_t   newest_idx;
    uint16_t   entries_per_sector;
    uint16_t   sectors;
} _fs_file_ctx_s;


/*--------------------------------------------------------------------------------------------------
 * PRIVATE CONSTANTS & VARIABLES
 *------------------------------------------------------------------------------------------------*/
static K_MUTEX_DEFINE (_fs_mutex);
static uint32_t       _fs_meta_empty_addr = _FS_META_START_ADDR;
static uint8_t        _fs_file_cnt        = 0;
static _fs_file_ctx_s _fs_files[FS_MAX_FILES];
static bool           _fs_init            = false;


/*--------------------------------------------------------------------------------------------------
 * PRIVATE FUNCTION PROTOTYPES
 *------------------------------------------------------------------------------------------------*/
/**
 * @fn     ERRNO_e _fs_meta_init (void)
 * @brief  Scans the dedicated metadata section in flash for file table.
 *         Then scans each file to update the file context structure.
 *
 * @note   Flash reserved space for metadata is assumed to be sector aligned.
 *
 * @return ERRNO_SUCCESS if successful, error code if not.
 */
static ERRNO_e _fs_meta_init (void);

/**
 * @fn     ERRNO_e _fs_file_meta_add (_fs_meta_s file_meta)
 * @brief  Adds a new file entry to the metadata space.
 *
 * @param  file_meta - The file's metadata information.
 *
 * @return ERRNO_SUCCESS if successful, error code if not.
 */
static ERRNO_e _fs_file_meta_add (_fs_meta_s file_meta);

/**
 * @fn     ERRNO_e _fs_is_empty (uint32_t start_addr, uint16_t len)
 * @brief  Checks for empty space in the provided flash segment.
 *
 * @param  start_addr - The starting address of the flash segment.
 * @param         len - The number of bytes to check.
 *
 * @return ERRNO_SUCCESS if empty, ERRNO_MEMORY_NOT_AVAILABLE if not, and error code if
 *         failure.
 */
static ERRNO_e _fs_is_empty (uint32_t start_addr, uint16_t len);

/**
 * @fn     static uint32_t _fs_prev_entry_addr_get (uint32_t start_addr, _fs_file_ctx_s* file_ctx)
 * @brief  Returns the previous entry's starting address.
 *
 * @param  start_addr - The current starting address.
 * @param        file - pointer to the file context.
 *
 * @return The starting address of the previous entry.
 */
static uint32_t _fs_prev_entry_addr_get (uint32_t start_addr, _fs_file_ctx_s* file_ctx);

/**
 * @fn     uint32_t _fs_sector_boundary_addr_get (uint32_t addr)
 * @brief  Gets the starting address of the next sector starting at the provided address.
 *
 * @param  addr - The address to start at.
 *
 * @return The next sector boundary address, starting at the provided address.
 */
static uint32_t _fs_sector_boundary_addr_get (uint32_t addr);

/**
 * @fn     bool _fs_is_entry_valid (uint32_t start_addr, uint16_t len)
 * @brief  Checks if the entry is valid.
 *
 * @note   Need to include index and checksum sizes into the length.
 *
 * @param  start_addr - The starting address of the data to verify.
 * @param         len - The length of the data to verify, including index and checksum.
 *
 * @return True if entry is valid, false if not.
 */
static bool _fs_is_entry_valid (uint32_t start_addr, uint16_t len);

/**
 * @fn     _fs_file_ctx_s* _fs_file_get (char* filename)
 * @brief  Gets file context for specified file.
 *
 * @param  filename - The file name.
 *
 * @return The file context pointer, NULL if failure.
 */
static _fs_file_ctx_s *_fs_file_get (char *filename);

/**
 * @fn     ERRNO_e _fs_meta_rewrite (void)
 * @brief  Erases the metadata sector and rewrites all current file metadata.
 *
 * @return ERRNO_SUCCESS if successful, error code if not.
 */
static ERRNO_e _fs_meta_rewrite (void);


/*--------------------------------------------------------------------------------------------------
 * PUBLIC FUNCTION DEFINITIONS
 *------------------------------------------------------------------------------------------------*/
ERRNO_e FS_init (void)
{
    ERRNO_e err = ERRNO_FAILURE;

    if (_fs_init)
    {
        return ERRNO_ALREADY_INITIALIZED;
    }

    /* Initialize CRC.*/
    CRC_init ();

    if (NVM_EXT_init () < ERRNO_SUCCESS)
    {
        return ERRNO_NVM_FAILURE;
    }

    /* Initialized metadata space. */
    err = _fs_meta_init ();
    if (err != ERRNO_SUCCESS)
    {
        return err;
    }

    /* Set init flag in case this gets called more than once. */
    _fs_init = true;

    return ERRNO_SUCCESS;
}

ERRNO_e FS_open_or_create (char *p_filename, FS_file_t *p_file, uint16_t size, uint16_t depth)
{
    ERRNO_e err;

    /* Check filesystem has been initialized. */
    if (!_fs_init)
    {
        return ERRNO_NOT_INITIALIZED;
    }

    /* Check if we've exceeded file limit. */
    if (_fs_file_cnt >= FS_MAX_FILES)
    {
        *p_file = NULL;
        return ERRNO_LIMIT_REACHED;
    }

    /* Check if we've exceeded size limit, including checksum. */
    if (size > FS_MAX_FILE_ENTRY_SIZE)
    {
        *p_file = NULL;
        return ERRNO_NOT_ENOUGH_MEMORY;
    }

    /* Depth minimum is 1. */
    if (depth < 1)
    {
        *p_file = NULL;
        return ERRNO_INVALID_DEPTH;
    }

    /* Fetch file. */
    *p_file = _fs_file_get (p_filename);

    /* Create file if it does not exist. */
    if (*p_file == NULL)
    {
        /* Calculate number of entries per sector. */
        _fs_files[_fs_file_cnt].entries_per_sector = (uint16_t) (FS_SECTOR_SIZE /
            (size + FS_FILE_INDEX_SIZE + FS_FILE_CHECKSUM_SIZE));

        /* Calculate number of sectors needed. */
        if (((depth / _fs_files[_fs_file_cnt].entries_per_sector) + 1) >=
            _FS_SECTOR_ALLOCATION_MIN)
        {
            _fs_files[_fs_file_cnt].sectors =
                ((depth / _fs_files[_fs_file_cnt].entries_per_sector) + 1);
        }
        else
        {
            _fs_files[_fs_file_cnt].sectors = _FS_SECTOR_ALLOCATION_MIN;
        }

        /* Assign file name. */
        strcpy (_fs_files[_fs_file_cnt].meta.name, p_filename);

        /* Calculate and set starting address. */
        _fs_files[_fs_file_cnt].meta.start_addr =
            (_fs_file_cnt) ? (_fs_files[(_fs_file_cnt - 1)].meta.end_addr + 1) : _FS_START_ADDR;

        /* Calculate size with depth (aligned by sector) and determine ending address. */
        _fs_files[_fs_file_cnt].meta.end_addr = _fs_files[_fs_file_cnt].meta.start_addr +
            (_fs_files[_fs_file_cnt].sectors * FS_SECTOR_SIZE) - 1;

        /* Set size. */
        _fs_files[_fs_file_cnt].meta.size = size;

        /* Calculate new checksum for file metadata. */
        if (CRC_32_calculate ((uint8_t *) &_fs_files[_fs_file_cnt],
                             (sizeof (_fs_meta_s) - FS_FILE_CHECKSUM_SIZE),
                             CRC_32_CHECKSUM_INIT,
                             &_fs_files[_fs_file_cnt].meta.checksum))
        {
            return ERRNO_CRC_FAILURE;
        }

        /* Set newest index address to starting address since this is a new file. */
        _fs_files[_fs_file_cnt].newest_idx_addr = _fs_files[_fs_file_cnt].meta.start_addr;

        /* Set empty index address to starting address since this is a new file. */
        _fs_files[_fs_file_cnt].empty_idx_addr = _fs_files[_fs_file_cnt].meta.start_addr;

        /* Initialize entry index to -1 since we roll over to 0 for first write. */
        _fs_files[_fs_file_cnt].newest_idx = (uint32_t) -1;

        /* Add file information to flash metadata. */
        err = _fs_file_meta_add (_fs_files[_fs_file_cnt].meta);
        if (err)
        {
            *p_file = NULL;
            return err;
        }

        /* Set file address and increment file count. */
        *p_file = (FS_file_t)&_fs_files[_fs_file_cnt++];
    }
    else
    {
        /* Verify existing file size matches. */
        if (((_fs_file_ctx_s*)*p_file)->meta.size != size)
        {
            *p_file = NULL;
            return ERRNO_SIZE_MISMATCH;
        }
    }

    return ERRNO_SUCCESS;
}

ERRNO_e FS_write (FS_file_t file, uint8_t *p_src)
{
    ERRNO_e         err;
    _fs_file_ctx_s *file_ctx             = (_fs_file_ctx_s *) file;
    uint16_t        entry_len            = file_ctx->meta.size + FS_FILE_INDEX_SIZE + FS_FILE_CHECKSUM_SIZE;
    uint32_t        checksum;
    uint32_t        sector_boundary_addr = _fs_sector_boundary_addr_get (file_ctx->empty_idx_addr);
    uint8_t         buf[entry_len];

    /* Check filesystem has been initialized. */
    if (!_fs_init)
    {
        return ERRNO_NOT_INITIALIZED;
    }

    /* Check if source pointer is valid. */
    if (p_src == NULL)
    {
        return ERRNO_BAD_POINTER;
    }

    k_mutex_lock (&_fs_mutex, K_FOREVER);

    /* Verify space is empty prior to writing. */
    err = _fs_is_empty (file_ctx->empty_idx_addr, entry_len);

    if (err)
    {
        k_mutex_unlock (&_fs_mutex);
        return err;
    }

    /* Increment and check if we've exceeded number of entries. */
    if (++file_ctx->newest_idx >= FS_MAX_FILE_ENTRIES)
    {
        k_mutex_unlock (&_fs_mutex);
        return ERRNO_LIMIT_REACHED;
    }

    /* Copy data to buffer and append index. */
    memcpy (buf, p_src, file_ctx->meta.size);
    memcpy (&buf[file_ctx->meta.size ], (uint8_t *) &file_ctx->newest_idx, FS_FILE_INDEX_SIZE);

    /* Calculate checksum of source data and newest index. */
    if (CRC_32_calculate (buf, (file_ctx->meta.size + FS_FILE_INDEX_SIZE), CRC_32_CHECKSUM_INIT, &checksum))
    {
        k_mutex_unlock (&_fs_mutex);
        return ERRNO_CRC_FAILURE;
    }
    
    /* Copy calculated CRC to buffer. */
    memcpy (&buf[(file_ctx->meta.size + FS_FILE_INDEX_SIZE)], (uint8_t *) &checksum, FS_FILE_CHECKSUM_SIZE);

    /* Write the entire entry. */
    if (NVM_EXT_write (file_ctx->empty_idx_addr, buf, entry_len))
    {
        k_mutex_unlock (&_fs_mutex);
        return ERRNO_NVM_FAILURE;
    }

    /* Update newest index address. */
    file_ctx->newest_idx_addr = file_ctx->empty_idx_addr;

    /* Check if next write will span sectors. */
    if ((file_ctx->empty_idx_addr + (2 * entry_len)) > sector_boundary_addr)
    {
        /* Check if we have reached the end of the file. */
        if (sector_boundary_addr > file_ctx->meta.end_addr)
        {
            /* Wrap to beginning of file. */
            sector_boundary_addr = file_ctx->meta.start_addr;

            /* Update empty index address to start address for the next write. */
            file_ctx->empty_idx_addr = file_ctx->meta.start_addr;

            /* If sector allocation is only 1, we will erase our own data that was just written above.
               and rewrite to starting address. */
            if (file_ctx->sectors == 1)
            {
                /* Erase the sector. */
                if (NVM_EXT_erase_block (sector_boundary_addr, NVM_EXT_BLOCK_SIZE_4KB))
                {
                    k_mutex_unlock (&_fs_mutex);
                    return ERRNO_NVM_FAILURE;
                }

                /* Write the entire entry. */
                if (NVM_EXT_write (file_ctx->empty_idx_addr, buf, entry_len))
                {
                    k_mutex_unlock (&_fs_mutex);
                    return ERRNO_NVM_FAILURE;
                }

                /* Update newest and empty index address. 
                 * The empty_idx_addr must be updated to prepare for the next write */
                file_ctx->newest_idx_addr = file_ctx->empty_idx_addr;
                file_ctx->empty_idx_addr += entry_len;

                k_mutex_unlock (&_fs_mutex);
                return ERRNO_SUCCESS;
            }
        }
        else
        {
            /* Update empty index address to start at the next sector. */
            file_ctx->empty_idx_addr = sector_boundary_addr;
        }

        /* Erase the sector. */
        if (NVM_EXT_erase_block (sector_boundary_addr, NVM_EXT_BLOCK_SIZE_4KB))
        {
            k_mutex_unlock (&_fs_mutex);
            return ERRNO_NVM_FAILURE;
        }
    }
    else
    {
        /* Update empty index address for the next write. */
        file_ctx->empty_idx_addr += entry_len;
    }

    k_mutex_unlock (&_fs_mutex);

    return ERRNO_SUCCESS;
}

ERRNO_e FS_write_at_offset (FS_file_t file, uint8_t *p_src, uint16_t offset, uint16_t len)
{
    _fs_file_ctx_s *file_ctx = (_fs_file_ctx_s *) file;
    uint8_t         buf[file_ctx->meta.size];
    ERRNO_e      err;

    /* Check filesystem has been initialized. */
    if (!_fs_init)
    {
        return ERRNO_NOT_INITIALIZED;
    }

    err = FS_read (file, buf, 0, file_ctx->meta.size);

    if (err)
    {
        return err;
    }

    /* Update contents. */
    memcpy (&buf[offset], p_src, len);

    err = FS_write (file, buf);

    return err;
}

ERRNO_e FS_read (FS_file_t file, uint8_t *p_dest, uint16_t offset, uint16_t len)
{
    _fs_file_ctx_s *file_ctx         = (_fs_file_ctx_s *) file;
    uint32_t        entry_start_addr = file_ctx->newest_idx_addr;
    uint32_t        entry_len        = file_ctx->meta.size + FS_FILE_INDEX_SIZE + FS_FILE_CHECKSUM_SIZE;
    uint16_t        entries          = (file_ctx->sectors * file_ctx->entries_per_sector);
    uint16_t        i;

    /* Check filesystem has been initialized. */
    if (!_fs_init)
    {
        return ERRNO_NOT_INITIALIZED;
    }

    /* Check if p_destination pointer is valid. */
    if (p_dest == NULL)
    {
        return ERRNO_BAD_POINTER;
    }

    /* Check if we are over file size limit. */
    if (len > file_ctx->meta.size)
    {
        return ERRNO_NOT_ENOUGH_MEMORY;
    }

    if (offset + len > file_ctx->meta.size)
    {
        return ERRNO_LIMIT_REACHED;
    }

    k_mutex_lock (&_fs_mutex, K_FOREVER);

    /* Loop through entries until we find a valid entry. */
    for (i = 0; i < entries; i++) {
        /* Check if entry is valid. */
        if (_fs_is_entry_valid (entry_start_addr, entry_len)) {
            break;
        } else {
            if (i == (entries - 1)) {
                return ERRNO_NO_VALID_ENTRIES;
            }
        }

        /* Get starting address for the previous entry. */
        entry_start_addr = _fs_prev_entry_addr_get (entry_start_addr, file_ctx);
    }

    /* Read the entry. */
    if (NVM_EXT_read (entry_start_addr + offset, p_dest, len))
    {
        return ERRNO_NVM_FAILURE;
    }

    k_mutex_unlock (&_fs_mutex);

    return ERRNO_SUCCESS;
}


ERRNO_e FS_clear (FS_file_t file)
{
    _fs_file_ctx_s *file_ctx   = (_fs_file_ctx_s *) file;
    // uint32_t        sector_len = (file_ctx->meta.end_addr - file_ctx->meta.start_addr + 1) %
    //                              FS_SECTOR_SIZE + 1;
    
    /* Check filesystem has been initialized. */
    if (!_fs_init)
    {
        return ERRNO_NOT_INITIALIZED;
    }

    if (NVM_EXT_erase_range (file_ctx->meta.start_addr, file_ctx->meta.end_addr))
    {
        return ERRNO_NVM_FAILURE;
    }

    /* Reset index addresses and entries. */
    file_ctx->newest_idx_addr = file_ctx->meta.start_addr;
    file_ctx->empty_idx_addr  = file_ctx->meta.start_addr;
    file_ctx->newest_idx      = (uint32_t) -1;

    return ERRNO_SUCCESS;
}

ERRNO_e FS_delete (char *p_filename)
{
    ERRNO_e err;
    int32_t del_idx = -1;
    uint8_t buf[FS_SECTOR_SIZE];

    /* Check filesystem has been initialized. */
    if (!_fs_init)
    {
        return ERRNO_NOT_INITIALIZED;
    }

    /* Find the file index. */
    for (uint8_t i = 0; i < _fs_file_cnt; i++)
    {
        if (!strcmp (p_filename, _fs_files[i].meta.name))
        {
            del_idx = i;
            break;
        }
    }

    if (del_idx == -1)
    {
        return ERRNO_FILE_NOT_FOUND;
    }

    /* Erase the deleted file's data range. */
    err = NVM_EXT_erase_range (_fs_files[del_idx].meta.start_addr, _fs_files[del_idx].meta.end_addr);
    if (err)
    {
        return err;
    }

    /* Shift the in-memory array to remove the deleted file. */
    for (uint8_t i = del_idx + 1; i < _fs_file_cnt; i++)
    {
        _fs_files[i - 1] = _fs_files[i];
    }
    _fs_file_cnt--;

    /* Reclaim space by shifting subsequent files' addresses and copying their data. */
    uint32_t next_start = (del_idx == 0) ? _FS_START_ADDR : (_fs_files[del_idx - 1].meta.end_addr + 1);

    for (uint8_t i = del_idx; i < _fs_file_cnt; i++)
    {
        uint32_t old_start = _fs_files[i].meta.start_addr;
        uint32_t old_end   = _fs_files[i].meta.end_addr;
        uint32_t file_size = old_end - old_start + 1;
        uint32_t new_start = next_start;
        uint32_t new_end   = new_start + file_size - 1;

        /* Erase the new range for the file. */
        err = NVM_EXT_erase_range (new_start, new_end);
        if (err)
        {
            return err;
        }

        /* Copy data from old address to new address in chunks. */
        for (uint32_t offset = 0; offset < file_size; offset += FS_SECTOR_SIZE)
        {
            uint32_t chunk_size = (file_size - offset < FS_SECTOR_SIZE) ? (file_size - offset) : FS_SECTOR_SIZE;

            err = NVM_EXT_read (old_start + offset, buf, chunk_size);
            if (err)
            {
                return err;
            }

            err = NVM_EXT_write (new_start + offset, buf, chunk_size);
            if (err)
            {
                return err;
            }
        }

        /* Update the file's metadata with new addresses. */
        _fs_files[i].meta.start_addr = new_start;
        _fs_files[i].meta.end_addr   = new_end;

        /* Adjust internal addresses by the offset difference. */
        int32_t addr_offset = new_start - old_start;
        _fs_files[i].newest_idx_addr += addr_offset;
        _fs_files[i].empty_idx_addr += addr_offset;

        /* Recalculate metadata checksum. */
        if (CRC_32_calculate ((uint8_t *) &_fs_files[i].meta,
                              (sizeof (_fs_meta_s) - FS_FILE_CHECKSUM_SIZE),
                              CRC_32_CHECKSUM_INIT,
                              &_fs_files[i].meta.checksum))
        {
            return ERRNO_CRC_FAILURE;
        }

        /* Update next start for the following file. */
        next_start = new_end + 1;
    }

    /* Rewrite the metadata sector without the deleted file. */
    err = _fs_meta_rewrite ();
    if (err)
    {
        return err;
    }

    return ERRNO_SUCCESS;
}

uint32_t FS_memory_used (void)
{
    return (_fs_file_cnt ?
        (_fs_files[(_fs_file_cnt - 1)].meta.end_addr - _FS_START_ADDR + 1) : 0);
}

uint32_t FS_memory_available (void)
{
    return _FS_SIZE - FS_memory_used ();
}

void FS_cli_cmd (int32_t argc, char **argv)
{
    if (argc == 0)
    {
        PRINT (INFO, "FS\r\n");
        PRINT (INFO, "-------------------------------------------\r\n");
        PRINT (INFO, "Memory used (bytes)      : %d\r\n", FS_memory_used ());
        PRINT (INFO, "Memory available (bytes) : %d\r\n", FS_memory_available ());
        PRINT (INFO, "Total memory (bytes)     : %d\r\n", _FS_SIZE);
    }
    else if (argc == 1)
    {
        if (!strcmp (argv[0], "--meta"))
        {
            PRINT (INFO, "FS Metadata Space\r\n");
            PRINT (INFO, "-------------------------------------------\r\n");
            PRINT (INFO, "Start address        : %d\r\n", _FS_META_START_ADDR);
            PRINT (INFO, "End address          : %d\r\n", _FS_META_END_ADDR);
            PRINT (INFO, "Total memory (bytes) : %d\r\n", _FS_META_SIZE);
            PRINT (INFO, "Next entry address   : %d\r\n", _fs_meta_empty_addr);
        }
        else if (!strcmp (argv[0], "--list"))
        {
            uint8_t i;

            PRINT (INFO, "FILES\r\n");
            PRINT (INFO, "-------------------------------------------\r\n");
            for (i = 0; i < _fs_file_cnt; i++)
            {
                PRINT (INFO, "%s\r\n", _fs_files[i].meta.name);
            }
        }
        else
        {
            goto USAGE;
        }
    }
    else if (argc == 2)
    {
        _fs_file_ctx_s *p_file;

        if (!strcmp (argv[0], "--details"))
        {
            p_file = _fs_file_get (argv[1]);

            if (p_file != NULL)
            {
                PRINT (INFO, "%s : \r\n", p_file->meta.name);
                PRINT (INFO, "file start address   : %d\r\n", p_file->meta.start_addr);
                PRINT (INFO, "file end address     : %d\r\n", p_file->meta.end_addr);
                PRINT (INFO, "file size (bytes)    : %d\r\n", p_file->meta.size);
                PRINT (INFO, "checksum             : 0x%08X\r\n", p_file->meta.checksum);
                PRINT (INFO, "newest index address : %d\r\n", p_file->newest_idx_addr);
                PRINT (INFO, "empty index address  : %d\r\n", p_file->empty_idx_addr);
                PRINT (INFO, "newest index         : %d\r\n", p_file->newest_idx);
                PRINT (INFO, "entries per sector   : %d\r\n", p_file->entries_per_sector);
                PRINT (INFO, "sectors              : %d\r\n", p_file->sectors);
            }
            else
            {
                PRINT (INFO, "ERROR : %s does not exist.\r\n", argv[1]);
            }
        }
        else if (!strcmp (argv[0], "--delete"))
        {
            ERRNO_e err = FS_delete (argv[1]);
            if (err == ERRNO_SUCCESS)
            {
                PRINT (INFO, "%s deleted successfully.\r\n", argv[1]);
            }
            else
            {
                PRINT (INFO, "ERROR : Failed to delete %s (error: %d).\r\n", argv[1], err);
            }
        }
        else
        {
            goto USAGE;
        }
    }
    else if (argc == 4)
    {
        if (!strcmp (argv[0], "--create"))
        {
            ERRNO_e   err;
            FS_file_t file;
            char     *p_filename = argv[1];
            uint16_t  size       = (uint16_t) strtoul(argv[2], NULL, 0);
            uint16_t  depth      = (uint16_t) strtoul(argv[3], NULL, 0);

            err = FS_open_or_create (p_filename, &file, size, depth);
            if (err == ERRNO_SUCCESS)
            {
                PRINT (INFO, "%s created successfully.\r\n", p_filename);
            }
            else
            {
                PRINT (INFO, "ERROR : Failed to create %s (error: %d).\r\n", p_filename, err);
            }
        }
        else if (!strcmp (argv[0], "--read"))
        {
            ERRNO_e   err;
            FS_file_t file;
            char     *p_filename = argv[1];
            uint16_t  size       = (uint16_t) strtoul(argv[2], NULL, 0);
            uint16_t  depth      = (uint16_t) strtoul(argv[3], NULL, 0);
            uint8_t   buf[size];
            uint16_t  i;
            
            err = FS_open_or_create (p_filename, &file, size, depth);
            if (err != ERRNO_SUCCESS)
            {
                PRINT (INFO, "ERROR : Failed to open %s (error: %d).\r\n", p_filename, err);
                return;
            }

            err = FS_read (file, buf, 0, size);
            if (err != ERRNO_SUCCESS)
            {
                PRINT (INFO, "ERROR : Failed to read %s (error: %d).\r\n", p_filename, err);
                return;
            }

            for (i = 0; i < size; i++)
            {
                PRINT (INFO, "%X", buf[i]);
            }
            PRINT (INFO, "\r\n");
        }
        else
        {
            goto USAGE;
        }
    }
    else if (argc == 5)
    {
        if (!strcmp (argv[0], "--write"))
        {
            ERRNO_e    err;
            FS_file_t  file;
            char      *p_filename = argv[1];
            uint16_t   size        = (uint16_t) strtoul(argv[2], NULL, 0);
            uint16_t   depth       = (uint16_t) strtoul(argv[3], NULL, 0);
            
            err = FS_open_or_create (p_filename, &file, size, depth);
            if (err != ERRNO_SUCCESS)
            {
                PRINT (INFO, "ERROR : Failed to open or create %s (error: %d).\r\n", p_filename, err);
                return;
            }
            
            err = FS_write (file, argv[4]);
            if (err == ERRNO_SUCCESS)
            {
                PRINT (INFO, "Data written to %s successfully.\r\n", p_filename);
            }
            else
            {
                PRINT (INFO, "ERROR : Failed to write to %s (error: %d).\r\n", p_filename, err);
            }
        }
        else
        {
            goto USAGE;
        }
    }
    else
    {
USAGE:
        PRINT (INFO, "Usage: fs [OPTION...]\r\n");
        PRINT (INFO, "  --meta           gets metadata boundary addresses and indexes\r\n");
		PRINT (INFO, "  --list           lists all files\r\n");
        PRINT (INFO, "  --details        gets file details\r\n");
		PRINT (INFO, "    <%%s>           filename\r\n");
        PRINT (INFO, "  --delete         deletes a file\r\n");
		PRINT (INFO, "    <%%s>           filename\r\n");
        PRINT (INFO, "  --create         creates a file\r\n");
        PRINT (INFO, "    <%%s>           filename\r\n");
        PRINT (INFO, "    <%%u>           size\r\n");
        PRINT (INFO, "    <%%u>           depth\r\n");
        PRINT (INFO, "  --write          writes hex data to a file\r\n");
        PRINT (INFO, "    <%%s>           filename\r\n");
        PRINT (INFO, "    <%%d>           size\r\n");
        PRINT (INFO, "    <%%d>           depth\r\n");
        PRINT (INFO, "    <%%s>           string\r\n");
        PRINT (INFO, "  --read          prints the contents of a file in hex\r\n");
		PRINT (INFO, "    <%%s>           filename\r\n");
        PRINT (INFO, "    <%%d>           size\r\n");
        PRINT (INFO, "    <%%d>           depth\r\n");
    }
}


/*--------------------------------------------------------------------------------------------------
 * PRIVATE FUNCTION DEFINITIONS
 *------------------------------------------------------------------------------------------------*/
static ERRNO_e _fs_meta_init (void)
{
    ERRNO_e  err;
    uint32_t addr;
    uint8_t  file_idx;
    uint32_t calc_checksum;

    /* Scan and update empty metadata address for next write. */
    for (_fs_meta_empty_addr = _FS_META_START_ADDR;
         _fs_meta_empty_addr < _FS_META_END_ADDR;
         _fs_meta_empty_addr += sizeof(_fs_meta_s))
    {
        /* Check if metadata space is empty. */
        err = _fs_is_empty (_fs_meta_empty_addr, sizeof (_fs_meta_s));

        if (!err)
        {
            /* Break if we've reached an empty slot. */
            break;
        }
        else if (err != ERRNO_MEMORY_NOT_AVAILABLE)
        {
            return err;
        }
    }

    /* Scan the entire metadata space. */
    /* TODO : Further analysis needed:
              Previously scanned entire space, now only scanning til next empty space.  This might be sufficient.
              for (addr = _FS_META_START_ADDR; addr < _FS_META_END_ADDR; addr += sizeof(_fs_meta_s)) */
    for (addr = _FS_META_START_ADDR; addr < _fs_meta_empty_addr; addr += sizeof(_fs_meta_s))
    {
        uint8_t  buf[sizeof(_fs_meta_s)];
        uint16_t checksum_idx = sizeof(_fs_meta_s) - FS_FILE_CHECKSUM_SIZE;

        /* Read entry length. */
        if (NVM_EXT_read (addr, buf, sizeof(_fs_meta_s)))
        {
            return ERRNO_NVM_FAILURE;
        }

        /* Verify if entry is valid by checking CRC checksum. */
        if (CRC_32_calculate (buf, checksum_idx, CRC_32_CHECKSUM_INIT, &calc_checksum))
        {
            return ERRNO_CRC_FAILURE;
        }
        if (calc_checksum == *(uint32_t*)&buf[checksum_idx])
        {
            /* Check if we've reached max files. */
            if (_fs_file_cnt == FS_MAX_FILES)
            {
                return ERRNO_LIMIT_REACHED;
            }
            else
            {
                /* Copy to local file table and increment file count. */
                _fs_files[_fs_file_cnt++].meta = *(_fs_meta_s*)buf;
            }
        }
    }

    /* Loop through each file's metadata and scan for information. */
    for (file_idx = 0; file_idx < _fs_file_cnt; file_idx++)
    {
        uint16_t entry_len    = _fs_files[file_idx].meta.size + FS_FILE_INDEX_SIZE + FS_FILE_CHECKSUM_SIZE;
        uint16_t checksum_idx = _fs_files[file_idx].meta.size + FS_FILE_INDEX_SIZE;
        uint8_t  buf[entry_len];

        /* Calculate number of entries per sector. */
        _fs_files[file_idx].entries_per_sector = (uint16_t) (FS_SECTOR_SIZE / entry_len);

        /* Calculate number of sectors needed. */
        _fs_files[file_idx].sectors = ((_fs_files[file_idx].meta.end_addr - _fs_files[file_idx].meta.start_addr) /
                                       FS_SECTOR_SIZE) + 1;

        /* Set newest index address. */
        _fs_files[file_idx].newest_idx_addr = _fs_files[file_idx].meta.start_addr;

        /* Initialize entry index to -1 since we roll over to 0 for first write. */
        _fs_files[file_idx].newest_idx = (uint16_t) -1;

        /* Loop to find newest index and newest index address. */
        for (addr = _fs_files[file_idx].meta.start_addr;
             addr < _fs_files[file_idx].meta.end_addr; )
        {
            /* Read entry length. */
            if (NVM_EXT_read (addr, buf, entry_len))
            {
                return ERRNO_NVM_FAILURE;
            }

            if (CRC_32_calculate (buf, checksum_idx, CRC_32_CHECKSUM_INIT, &calc_checksum))
            {
                return ERRNO_CRC_FAILURE;
            }
            /* Verify if entry is valid by checking CRC checksum. */
            if (calc_checksum == *(uint32_t *) &buf[checksum_idx])
            {
                /* Check if index differences are greater than 0 to determine if index is newer. */
                if ((*(int16_t *) &buf[_fs_files[file_idx].meta.size] -
                    (int16_t) _fs_files[file_idx].newest_idx) > 0)
                {
                    /* Update newest index address. */
                    _fs_files[file_idx].newest_idx_addr = addr;

                    /* Update the newest index. */
                    _fs_files[file_idx].newest_idx = *(uint16_t *) &buf[_fs_files[file_idx].meta.size];
                }
            }

            /* Check if we will span sectors. */
            if ((addr + entry_len) > _fs_sector_boundary_addr_get (addr))
            {
                /* Update address to next sector. */
                addr = _fs_sector_boundary_addr_get (addr);
            }
            else
            {
                addr += entry_len;
            }
        }

        /* Loop to find empty index address. */
        for (addr = _fs_files[file_idx].meta.start_addr;
             addr < _fs_files[file_idx].meta.end_addr; )
        {
            /* Check if entry space is empty. */
            err = _fs_is_empty (addr, entry_len);

            if (!err)
            {
                /* Set empty index address if we've reached an empty slot. */
                _fs_files[file_idx].empty_idx_addr = addr;
                break;
            }
            else if (err != ERRNO_MEMORY_NOT_AVAILABLE)
            {
                return err;
            }

            /* Check if we will span sectors. */
            if ((addr + entry_len) > _fs_sector_boundary_addr_get (addr))
            {
                /* Update address to next sector. */
                addr = _fs_sector_boundary_addr_get (addr);
            }
            else
            {
                addr += entry_len;
            }
        }
    }

    return ERRNO_SUCCESS;
}

static ERRNO_e _fs_file_meta_add (_fs_meta_s file_meta)
{
    /* Verify space is empty prior to writing. */
    ERRNO_e err = _fs_is_empty (_fs_meta_empty_addr, sizeof(_fs_meta_s));

    if (err)
    {
        return err;
    }

    /* Verify we have enough space in flash. */
    if ((_fs_meta_empty_addr + sizeof(_fs_meta_s)) > (_FS_META_END_ADDR + 1))
    {
        return ERRNO_NOT_ENOUGH_MEMORY;
    }

    /* Write new file to flash file table. */
    if (NVM_EXT_write (_fs_meta_empty_addr, (uint8_t *) &file_meta, sizeof(_fs_meta_s)))
    {
        return ERRNO_NVM_FAILURE;
    }

    /* Update metadata address for next write. */
    _fs_meta_empty_addr += sizeof(_fs_meta_s);

    return ERRNO_SUCCESS;
}

static ERRNO_e _fs_is_empty (uint32_t start_addr, uint16_t len)
{
    uint16_t i;
    uint8_t  buf[len];

    /* Read entry length. */
    if (NVM_EXT_read (start_addr, buf, len))
    {
        return ERRNO_NVM_FAILURE;
    }

    /* Loop through the read data. */
    for (i = 0; i < len; i++)
    {
        /* Scan for empty space. */
        if (buf[i] != 0xFF)
        {
            return ERRNO_MEMORY_NOT_AVAILABLE;
        }
    }

    return ERRNO_SUCCESS;
}

static uint32_t _fs_prev_entry_addr_get (uint32_t start_addr, _fs_file_ctx_s* file_ctx)
{
    uint16_t entry_len = file_ctx->meta.size + FS_FILE_INDEX_SIZE + FS_FILE_CHECKSUM_SIZE;

    /* Check for wraparound. */
    if ((int32_t) (start_addr - entry_len) < (int32_t) file_ctx->meta.start_addr)
    {
        /* Update address to entry before wraparound. */
        start_addr = file_ctx->meta.end_addr - entry_len - (FS_SECTOR_SIZE % entry_len) + 1;
    }
    else if (_fs_sector_boundary_addr_get (start_addr - entry_len) <
             _fs_sector_boundary_addr_get (start_addr))
    {
        /* Update address if we span sectors. */
        start_addr = _fs_sector_boundary_addr_get (start_addr - entry_len) -
            entry_len - (FS_SECTOR_SIZE % entry_len);
    }
    else
    {
        /* Update next entry start address. */
        start_addr -= entry_len;
    }

    return start_addr;
}

static bool _fs_is_entry_valid (uint32_t start_addr, uint16_t len)
{
    uint8_t  buf[len];
    uint16_t checksum_idx = len - FS_FILE_CHECKSUM_SIZE;
    uint32_t calc_checksum;

    /* Read entry length. */
    if (NVM_EXT_read (start_addr, buf, len))
    {
        return false;
    }

    /* Verify if entry is valid by checking CRC checksum. */
    if (CRC_32_calculate (buf, checksum_idx, CRC_32_CHECKSUM_INIT, &calc_checksum))
    {
        return false;
    }
    return (calc_checksum == *(uint32_t *) &buf[checksum_idx]);
}

static _fs_file_ctx_s *_fs_file_get (char *filename)
{
    uint8_t i;

    /* Loop through existing files. */
    for (i = 0; i < _fs_file_cnt; i++)
    {
        /* Compare file names. */
        if (!strcmp (filename, _fs_files[i].meta.name))
        {
            return &_fs_files[i];
        }
    }

    /* Return NULL if file does not exist. */
    return NULL;
}

static uint32_t _fs_sector_boundary_addr_get (uint32_t addr)
{
    uint16_t sectors = (uint16_t) (addr / FS_SECTOR_SIZE) + 1;

    return (sectors * FS_SECTOR_SIZE);
}

static ERRNO_e _fs_meta_rewrite (void)
{
    ERRNO_e err;

    /* Erase the metadata sector. */
    err = NVM_EXT_erase_block (_FS_META_START_ADDR, NVM_EXT_BLOCK_SIZE_4KB);
    if (err)
    {
        return err;
    }

    /* Reset the empty metadata address. */
    _fs_meta_empty_addr = _FS_META_START_ADDR;

    /* Rewrite metadata for all remaining files. */
    for (uint8_t i = 0; i < _fs_file_cnt; i++)
    {
        err = _fs_file_meta_add (_fs_files[i].meta);
        if (err)
        {
            return err;
        }
    }

    return ERRNO_SUCCESS;
}
