/** @brief Parse ASCII config file.
 *
 * @copyright 2020 Remsdaq Ltd.
 *
 * @file c4_parser.c
 *
 * A simple parser built using strtok to identify keywords.
 *
 *  @date 20 May 2020
 *  @author: Dan Smithers <dwhs@remsdaq.com>
 *
 */

/*******************************************************************************
 * Includes
 ******************************************************************************/

#include "c4_privilege_modes.h"
#include "c4_parser.h"
#include "c4_qspi.h"
#include "platform-memory-map.h"
#include "fsl_debug_console.h"
#include <string.h>
#include <stdio.h>
#include <ctype.h>

/*******************************************************************************
 * External Definitions
 ******************************************************************************/

extern const char *const config_addr;

/*******************************************************************************
 * Defines
 ******************************************************************************/

/// @brief Config file major version numbers
static const uint8_t CFG_VER_MAJ = 0;

/// @brief Config file minor version number
static const uint8_t CFG_VER_MIN = 1;

/// @brief Multiplier for hash function
static const uint32_t HASH_MULTIPLIER=37;

/// @brief Salt for hash function
static const uint32_t HASH_SALT=0xfeedb4be;

/*******************************************************************************
 * Structures
 ******************************************************************************/

/*******************************************************************************
 * Prototypes
 ******************************************************************************/

/** @brief Print out config file example.
 *
 */
void printConfigExample();

/** @brief Print out config file description.
 *
 */
void printConfigHelp();

/** @brief Check the file version.
 *
 * @param[in] ver Version string
 *
 * @return Whether the config file read is compatible.
 * @retval C4_ERR_OK File is compatible.
 * @retval C4_ERR_VERSION File is not compatible
 */
exit_code_t checkConfigVersion(const char *ver);

/** @brief Verify the checksum
 *
 * @param[in] cfg_file Start of file to check.
 * @param[in] file_len Length in bytes of file to check.
 *
 * @return Whether checksum matches.
 * @retval C4_ERR_OK Checksum matches.
 * @retval C4_ERR_FILE_CORRUPT Checksum does not match.
 */
exit_code_t verifyConfigChecksum(const char * cfg_file, size_t file_len);

/** @brief Write checksum
 *
 * @param[in] cfg_file Start of file being written
 * @param[in] file_len Length in bytes of file to check.
 *
 * @return whether checksum has been written.
 * @retval C4_ERR_OK Checksum written.
 * @retval C4_ERR_FAILED Checksum has not been written.
 */
exit_code_t writeConfigChecksum(const char * cfg_file, size_t file_len);

/**
 * @brief Calculate a config header checksum
 *
 * @param[in] pvBase        Base address of configuration block
 * @param[in] uiNumBytes    Length (in bytes) of the configuration
 *
 * @return                  32-bit additive checksum value
 *
 * Calculate the checksum of a platform configuration EEPROM header
 */
static uint32_t calc_checksum(volatile const unsigned char *pvBase, uint32_t uiNumBytes);

/*******************************************************************************
 * Variables
 ******************************************************************************/

/** @brief Array of strings to use in parser. */
const char C4_parseStrings[NUM_PARSE_TERMS+1][32] = {
#define CALLISTO_PARSER(Id, Text, Desc, Exa) \
    Text,
        PARSER_KEYWORDS
#undef CALLISTO_PARSER
    "Number of Parse terms"
};


/** @brief Array of description strings to use in parser help. */
static const char C4_helpStrings[NUM_PARSE_TERMS+1][512] = {
#define CALLISTO_PARSER(Id, Text, Desc, Exa) \
    Desc,
        PARSER_KEYWORDS
#undef CALLISTO_PARSER
    "Total number of parse terms"
};
/** @brief Array of strings to construct example config file. */
static const char C4_exampleStrings[NUM_PARSE_TERMS+1][64] = {
#define CALLISTO_PARSER(Id, Text, Desc, Exa) \
    Text""Exa,
        PARSER_KEYWORDS
#undef CALLISTO_PARSER
    "N/A"
};

/*******************************************************************************
 * Interface Implementation
 ******************************************************************************/

exit_code_t GetConfig(const char *mem, uint32_t mem_len, E_parseIndices_t field, char *val)
{
    exit_code_t rc = C4_ERR_FIELD_NOT_FOUND;

    char str[mem_len];// Calculate maximum length field 526 (256 bytes + 1 (:) + 1 (data length) + 2 (address) + 1 (checksum) + 2 (\r\n) ) * 2

    strncpy(str, (char *)mem, mem_len);

    // Check that we have a printable character or space character at the beginning
    // of the file string.
    if (!isgraph((int)(*str)) && !isspace((int)(*str)))
    {
        printConfigHelp();
        return C4_ERR_FILE_NOT_FOUND;
    }

    *val = '\0';
    char * pch;

    pch = strtok (str,"=\r\n");

    // File must start with file identifier.
    if (strcmp(pch, C4_parseStrings[C4_BOOT_CONFIG_PARSER_START]) == 0)
    {
        pch = strtok (NULL, "\r\n");
        rc = checkConfigVersion(pch);
        pch = strtok (NULL, "=\r\n");
    }
    while ((NULL != pch) && (C4_ERR_OK == rc))
    {
        E_parseIndices_t i;
        for (i = C4_BOOT_CONFIG_PARSER_START; i <= C4_BOOT_CONFIG_PARSER_END; ++i)
        {
            // If this string matches the current index in the list of strings
            if(strcmp(pch, C4_parseStrings[i]) == 0)
            {
                if (field == i) // this is the one that we are looking for
                {
                    // get token and add to return string.
                    pch = strtok (NULL, "\r\n");
                    strcat(val, pch);
                    strcat(val, " ");
                    rc = C4_ERR_OK;
                }
                else if (C4_BOOT_CONFIG_PARSER_END == i)
                {
                    // get checksum and test
                    pch = strtok (NULL, "=\r\n");
                    if (pch)
                    {
                        rc = ((C4_ERR_OK == rc) ?
                              verifyConfigChecksum(mem, (size_t)(pch - str)) :
                              rc);
                    }
                }
                else
                {
                    // get next token
                    pch = strtok (NULL, "\r\n");
                }
                break;
            }
        }

        // have we exhausted the list of parse items?
        if (NUM_PARSE_TERMS == i)
        {
            printf("Field \"%s\"\n", pch);
            printConfigHelp();
            rc = C4_ERR_FIELD_UNDEFINED;
            break;
        }

        pch = strtok (NULL,"=\r\n");
    }

    return rc;
}


/*******************************************************************************/

exit_code_t SetConfig(const char *mem, uint32_t mem_len, E_parseIndices_t field, const char *val/*, uint32_t val_len*/, file_op_t op)
{
    exit_code_t rc = C4_ERR_FIELD_NOT_FOUND;

    char str[mem_len];// Calculate maximum length field 526 (256 bytes + 1 (:) + 1 (data length) + 2 (address) + 1 (checksum) + 2 (\r\n) ) * 2

    strncpy(str, (char *)mem, mem_len);

    // Check that we have a printable character or space character at the beginning
    // of the file string.
    if (!isgraph((int)(*str))&& !isspace((int)(*str)))
        return C4_ERR_FILE_NOT_FOUND;

    char *pch;
    C4_PRINT ("Splitting string \"%s\" into tokens:\n", str);
    pch = strtok (str,"=\r\n");
    // File must start with file identifier.
    if (strcmp(pch, C4_parseStrings[C4_BOOT_CONFIG_PARSER_START]) == 0)
    {
        pch = strtok (NULL, "\r\n");
        rc = checkConfigVersion(pch);
        pch = strtok (NULL, "=\r\n");
    }
    while ((NULL != pch) && (C4_ERR_OK == rc))
    {
        E_parseIndices_t i;
        for (i = C4_BOOT_CONFIG_PARSER_START; i <= C4_BOOT_CONFIG_PARSER_END; ++i)
        {
            // If this string matches the current index in the list of strings
            if(strcmp(pch, C4_parseStrings[i]) == 0)
            {
                if (field == i) // this is the one that we are looking for
                {
                    pch = strtok (NULL, "\r\n");
                    switch(op) {
                    case E_NOOP:
                        // do nothing
                        break;

                    case E_APPEND:
                        if (0 == strcmp(val, pch))
                        {
                            C4_PRINT("match found \"%s\" \"%s\"\n", val, pch);
                            op = E_NOOP;
                            break;
                        }
                        // replace existing value.
                        // fall through
                    case E_REPLACE:
                        if (strlen(pch) < strlen(val))
                            rc = C4_ERR_SYNTAX;
                        else
                        {
        //                    strncpy(pch, val, mem_len);
                            uintptr_t dst = (uint32_t)mem + (uint32_t)pch - (uint32_t)str;
                            C4_PRINT("\"%s\"\n - writing \"%s\" %d bytes at 0x%08x\n",
                                    pch, val, strlen(val), dst);
                            rc = C4_QSPI_write(dst, (const uint8_t *)val, strlen(val));
                        }
                        break;
                    case E_EXTEND:
                        if (0 == strcmp(val, pch))
                        {
                            C4_PRINT("match found \"%s\" \"%s\"\n", val, pch);
                            op = E_NOOP;
                        }
                        break;
                    case E_REMOVE:
                        // pch points to the start of the field value and is NULL terminated.
                        // Only delete it if the value matches.
                        if (0 != strcmp(val, pch))
                        {
                            // destination is the beginning of the current keyword
                            // Offset in QSPI (mem) is calculated by adding the difference between pch and str and subtracting the keyword length.
                            const uintptr_t dst = (const uintptr_t)mem + (const uintptr_t)pch - (const uintptr_t)str - strlen(C4_parseStrings[i]) - 1;

                            // source is the beginning of the next keyword
                            // Offset in QSPI (mem) is calculated by adding the difference between pch and str and adding the value length.
                            const char * src = (const char *)((const uintptr_t)mem + (const uintptr_t)pch - (const uintptr_t)str + strlen(pch) + 1);
                            C4_PRINT("removing from 0x%08x to 0x%p\r\n", dst, src);

                            // Add one to the string length to write NULL as well
                            rc = C4_QSPI_write(dst, (const uint8_t *)src, strlen(src) + 1);
                        }
                        break;

                    default:
        //                C4_PRINT(" field  = \"%s\"\n", val);
                        break;
                    }
                }
                else if (C4_BOOT_CONFIG_PARSER_END == i)
                {
                    // check whether there is anything left for append or extend operations to perform.
                    if ((E_EXTEND == op) || (E_APPEND == op))
                    {
                        // destination is the beginning of the current keyword
                        uintptr_t dst = (uint32_t)mem + (uint32_t)pch - (uint32_t)str;
                        // source is constructed from new keyword and remaining values.
                        char src[64] = "\0";
                        sprintf(src, "%s=%s\n%s", C4_parseStrings[field], val,(char *)dst);
                        C4_PRINT("appending from \"%s\" to 0x%08x\r\n", src, dst);

                        // Add one to the string length to write NULL as well
                        rc = C4_QSPI_write(dst, (const uint8_t *)src, strlen(src) + 1);
                    }
                    // get checksum and test
                    pch = strtok (NULL, "=\r\n");
                    if (pch)
                    {
                        rc = ((C4_ERR_OK == rc) ?
                              writeConfigChecksum(mem, (size_t)(pch - str)) :
                              rc);
                    }
                }
                else
                {
                    // get next token
                    pch = strtok (NULL, "\r\n");
                }
                break;
            }
        }

        // have we exhausted the list of parse items?
        if (NUM_PARSE_TERMS == i)
        {
            printf("Field \"%s\"\n", pch);
            printConfigHelp();
            rc = C4_ERR_FIELD_UNDEFINED;
            break;
        }

        pch = strtok (NULL,"=\r\n");
    }
#if 0
    while (pch != NULL)
    {
//        C4_PRINT ("%s\n",pch);
        if (strcmp(pch, "RemsdaqBootConfig") == 0)
        {
            pch = strtok (NULL, "=\r\n");
            C4_PRINT(" version = %s\n", pch);
            rc = C4_ERR_VERSION;
        }
        else if (strcmp(pch, field) == 0)
        {
            pch = strtok (NULL, "\r\n");
//           C4_PRINT(" field  = \"%s\" -> \"%s\"\n", pch, val);
            switch(op) {
            case E_REPLACE:
                if (strlen(pch) < strlen(val))
                    rc = C4_ERR_SYNTAX;
                else
                {
//                    strncpy(pch, val, mem_len);
                    uintptr_t dst = (uint32_t)mem + (uint32_t)pch - (uint32_t)str;
                    C4_PRINT("\"%s\"\n - writing \"%s\" %d bytes at 0x%08x\n",
                           pch, val, strlen(val), dst);
                    C4_QSPI_write(dst, strlen(val), (const uint8_t *)val);
                }
                break;
            case E_APPEND:
                // fall through
            case E_EXTEND:
                C4_PRINT(" operation %d Not Implemented\n", op);
                rc = C4_ERR_NOT_IMPLEMENTED;
                break;
            default:
//                C4_PRINT(" field  = \"%s\"\n", val);
                break;

            }
            rc = C4_ERR_OK;
        }
        else if (strcmp(pch, "EndConfig") == 0)
        {
            C4_PRINT("end of config\n");
            break;
        }

        pch = strtok (NULL,"=\r\n");
    }
#endif /* 0 */
    return rc;
}


/*******************************************************************************
 * Local Functions
 ******************************************************************************/

void printConfigHelp()
{
    printf("C4 Parse help:\n");
    for (E_parseIndices_t i = 0; i < NUM_PARSE_TERMS; ++i)
    {
        printf("%s:\n\t %s\n", C4_parseStrings[i], C4_helpStrings[i]);
    }

    printConfigExample();
}

/*******************************************************************************/

void printConfigExample()
{
    printf("CallistoBootloader_data to be located in QSPI Flash at 0x%08x\n -------\n", QSPI_BOOTLOADERx_CONFIG);
    for (E_parseIndices_t i = 0; i < NUM_PARSE_TERMS; ++i)
    {
        printf("%s\n", C4_exampleStrings[i]);
    }
}

/*******************************************************************************/

exit_code_t checkConfigVersion(const char *ver)
{
   exit_code_t rc = C4_ERR_OK;

   uint32_t version[3] = {0,0,0};

   sscanf(ver, "%ld.%ld", version, version + 1);

   if ((CFG_VER_MAJ != version[0]) ||
       (CFG_VER_MIN < version[1]))
       rc = C4_ERR_VERSION;

   return rc;
}

/*******************************************************************************/

exit_code_t verifyConfigChecksum(const char * cfg_file, size_t file_len)
{
   uint32_t chk = strtoul(cfg_file+file_len, NULL, 16);
   uint32_t sum = calc_checksum((const uint8_t *)cfg_file, file_len);
   PRINTF ("chksum 0x%08lx vs 0x%08lx\n", chk, sum);
   return ((sum == chk) ? C4_ERR_OK : C4_ERR_FAILED);
}


/*******************************************************************************/

exit_code_t writeConfigChecksum(const char * cfg_file, size_t file_len)
{
   exit_code_t rc = C4_ERR_OK;
   uint32_t chk = strtoul(cfg_file+file_len, NULL, 16);

   uint32_t sum = calc_checksum((const uint8_t *)cfg_file, file_len);
   PRINTF ("chksum 0x%08lx vs 0x%08lx\n", chk, sum);
   if (sum != chk)
   {
       uintptr_t dst = (uintptr_t)cfg_file + file_len;

       char val[11] = {'\0'};
       sprintf(val,"0x%lx", sum);
       C4_PRINT("\n - writing \"%s\" %d bytes at 0x%08x\n",
                val, strlen(val), dst);
       rc = C4_QSPI_write(dst, (const uint8_t *)val, strlen(val));
   }
   return rc;
}


/*******************************************************************************/

static uint32_t calc_checksum(volatile const unsigned char *pvBase, uint32_t uiNumBytes)
{
    unsigned int h = HASH_SALT;

    for (; uiNumBytes--; ++pvBase)
       h = HASH_MULTIPLIER * h + *pvBase;
    return h;
}
