#!/usr/bin/env python
"""Generates C code for the nonvolatile memory (NVM) data structures.
"""

# Imports
import argparse
import codecs
import datetime
import glob
import logging
import os
import pathlib
import pprint
import shutil

import colorama
import openpyxl

APP_NAME = "KTag Autocode Generator"
__author__ = "Joe Kearney"
__version__ = "00.03"

NVM_SPREADSHEET_FILENAME = "2020TPC Nonvolatile Memory.xlsx"
SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__))
DESTINATION_PATHS = ("2020TPCApp1.cydsn/NVM", "2020TPCAppNoDFU.cydsn/NVM")

# See https://developer.arm.com/documentation/dui0472/m/c-and-c---implementation-details/basic-data-types-in-arm-c-and-c--
# and
# https://en.wikibooks.org/wiki/C_Programming/inttypes.h
CortexM_type_sizes_in_bytes = {
    "bool":     1,
    "int8_t":   1,
    "uint8_t":  1,
    "int16_t":  2,
    "uint16_t": 2,
    "int32_t":  4,
    "uint32_t": 4,
    "float":    4,
    "int64_t":  8,
    "uint64_t": 8,
    "double":   8,
}

# Initialize the pretty-printer.
pp = pprint.PrettyPrinter(indent=4)


# Openpyxl helper function from https://stackoverflow.com/questions/23562366/how-do-i-get-value-present-in-a-merged-cell
def getValueWithMergeLookup(sheet, cell):
    idx = cell.coordinate
    for my_range in sheet.merged_cells.ranges:
        merged_cells = list(openpyxl.utils.rows_from_range(str(my_range)))
        for row in merged_cells:
            if idx in row:
                # If this is a merged cell,
                # return  the first cell of the merge range
                coordinates_of_first_cell = merged_cells[0][0]
                return sheet[coordinates_of_first_cell].value

    return sheet[idx].value


# Set up logging.
rootLogger = logging.getLogger()
rootLogger.setLevel(logging.DEBUG)
fileHandler = logging.FileHandler('Autocode.log', mode='w')
logFormatter = logging.Formatter(
    '{asctime} {name} {levelname:8s} {message}', style='{')
fileHandler.setFormatter(logFormatter)
rootLogger.addHandler(fileHandler)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(logFormatter)
rootLogger.addHandler(consoleHandler)
logger = logging.getLogger('autocode.py')


def main():
    version_information = APP_NAME + ' version ' + __version__

    logger.info(version_information)

    colorama.init(autoreset=True)

    parser = argparse.ArgumentParser(
        description='Generate autocode', epilog=version_information)
    commands = parser.add_mutually_exclusive_group()
    commands.add_argument(
        '-n', '--nvm', help='Generate nonvolatile memory autocode', action='store_true')
    parser.add_argument('-v', '--version', action='version',
                        version=version_information)

    args = parser.parse_args()
    d = vars(args)

    if (d['nvm'] == True):
        generate_NVM_autocode()
    else:
        # If no commands were specified, show the help and exit.
        parser.parse_args(['-h'])


def generate_NVM_autocode():
    logger.info('Generating nonvolatile memory autocode...')

    wb = openpyxl.load_workbook(
        filename=NVM_SPREADSHEET_FILENAME)
    ws = wb['NVM']

    headerColumnsByName = dict()
    for rowIndex, row in enumerate(ws.iter_rows(min_row=1, max_row=1), start=1):
        for columnIndex, cell in enumerate(row, start=0):
            headerColumnsByName[cell.value] = columnIndex

    NVMItemsByID = dict()
    NVMLocations = set()
    NVMEntriesByLocation = dict()

    for rowIndex, row in enumerate(ws.iter_rows(min_row=2)):
        if (row[headerColumnsByName['ID']].value is not None):
            item_ID = int(row[headerColumnsByName['ID']].value)

            nvm_item = dict()
            nvm_item['ID'] = item_ID
            nvm_item['Location'] = row[headerColumnsByName['Location']].value
            if nvm_item['Location'] != 'None':
                if nvm_item['Location'] not in NVMLocations:
                    # This is a new location.
                    NVMLocations.add(nvm_item['Location'])
                    NVMEntriesByLocation[nvm_item['Location']] = list()

                nvm_item['Name'] = row[headerColumnsByName['Item Name']].value
                nvm_item['Item Name in Code'] = nvm_item['Name'].replace(
                    ' ', '_')
                nvm_item['Item Shorthand Macro'] = 'NVM_' + \
                    nvm_item['Name'].upper().replace(' ', '_')
                nvm_item['Entry'] = row[headerColumnsByName['Entry Name']].value
                nvm_item['Entry Name in Code'] = 'NVM_' + \
                    nvm_item['Entry'].replace(' ', '_')
                nvm_item['Entry Type Name'] = nvm_item['Entry Name in Code'] + '_T'
                nvm_item['RAM Entry Name in Code'] = 'NVM_' + \
                    nvm_item['Entry'].replace(' ', '_')
                nvm_item['Datatype'] = row[headerColumnsByName['Datatype']].value

                # Validate the datatype and get the size.
                if nvm_item['Datatype'] not in CortexM_type_sizes_in_bytes.keys():
                    logger.error('NVM item ' + str(item_ID) + ' on row ' + str(rowIndex + 2) +
                                 ' has unknown datatype \"' + str(nvm_item['Datatype']) + '\"!')
                else:
                    nvm_item['Data Size in Bytes'] = CortexM_type_sizes_in_bytes[nvm_item['Datatype']]

                nvm_item['Default Value'] = row[headerColumnsByName['Default Value']].value
                nvm_item['Description'] = row[headerColumnsByName['Description']].value
                nvm_item['Notes'] = row[headerColumnsByName['Notes']].value

                # pp.pprint(nvm_item)
                if nvm_item['Entry'] not in NVMEntriesByLocation[nvm_item['Location']]:
                    NVMEntriesByLocation[nvm_item['Location']].append(
                        nvm_item['Entry'])
                NVMItemsByID[item_ID] = nvm_item

    for location in NVMLocations:
        logger.info('Generating code for ' + location + ' NVM.')

        with codecs.open('NVM_' + location + 'EEPROMEntries.h', 'w', "utf-8") as f:
            f.write("/*\n")
            f.write(
                r" *      __ ________               _____                               ______          __    " + "\n")
            f.write(
                r" *     / //_/_  __/___ _____ _   / ___/____  __  _______________     / ____/___  ____/ /__  " + "\n")
            f.write(
                r" *    / ,<   / / / __ `/ __ `/   \__ \/ __ \/ / / / ___/ ___/ _ \   / /   / __ \/ __  / _ \ " + "\n")
            f.write(
                r" *   / /| | / / / /_/ / /_/ /   ___/ / /_/ / /_/ / /  / /__/  __/  / /___/ /_/ / /_/ /  __/ " + "\n")
            f.write(
                r" *  /_/ |_|/_/  \__,_/\__, /   /____/\____/\__,_/_/   \___/\___/   \____/\____/\__,_/\___/  " + "\n")
            f.write(
                r" *                   /____/                                                                 " + "\n")
            f.write(" *\n")
            f.write(
                " *  🃞 THIS FILE IS PART OF THE KTAG SOURCE CODE.  Visit https://ktag.clubk.club/ for more. 🃞\n")
            f.write(" *\n")
            f.write(" */\n")

            f.write('\n')
            f.write('/** \\file\n')
            f.write(
                ' *  \\brief [Autogenerated] This file declares the ' + location + ' EEPROM entries.\n')
            f.write(' *\n')
            f.write(
                ' *  \\note AUTOGENERATED: This file was generated automatically on ' +
                '{dt:%A}, {dt:%B} {dt.day}, {dt.year} at {dt:%r}'.format(dt=datetime.datetime.now()) +
                '.\n')
            f.write(' *                       DO NOT MODIFY THIS FILE MANUALLY!\n')
            f.write(' */\n')
            f.write('\n')
            f.write('#ifndef NVM_' + location.upper() + 'EEPROMENTRIES_H\n')
            f.write('#define NVM_' + location.upper() + 'EEPROMENTRIES_H\n')
            f.write('\n')
            f.write('#ifdef __cplusplus\n')
            f.write('extern "C" {\n')
            f.write('#endif\n')
            f.write('\n')
            f.write('/* Preprocessor and Type Definitions */\n')
            f.write('\n')

            for entry in NVMEntriesByLocation[location]:
                entry_name_in_code = 'NVM_' + entry.replace(' ', '_')
                entry_type_name = entry_name_in_code + '_T'
                f.write('typedef struct __attribute__((packed))\n')
                f.write('{\n')
                # List all the items in this entry.
                for item_ID in NVMItemsByID:
                    item = NVMItemsByID[item_ID]
                    if item['Entry'] == entry:
                        if item['Description'] is not None:
                            f.write('    //! ' + item['Description'] + '\n')
                        f.write('    ' + item['Datatype'] +
                                ' ' + item['Item Name in Code'] + ';\n')
                f.write('} ' + entry_type_name + ';\n')
                f.write('\n')

            f.write('\n')
            f.write('/* Include Files */\n')
            f.write('\n')
            f.write('/* Public Variables */\n')
            f.write('\n')

            for entry in NVMEntriesByLocation[location]:
                entry_name_in_code = 'NVM_' + entry.replace(' ', '_')
                entry_type_name = entry_name_in_code + '_T'
                f.write('extern NVM_EEPROMEntry_T ' +
                        entry_name_in_code + ';\n')

            f.write('\n')
            f.write('extern NVM_EEPROMEntry_T * const NVM_' +
                    location + 'EEPROMEntries[];\n')
            f.write('extern const uint8_t NVM_N_' +
                    location.upper() + '_EEPROM_ENTRIES;\n')
            f.write('\n')
            f.write('// Shorthand macros, to save you time.\n')

            for item_ID in NVMItemsByID:
                item = NVMItemsByID[item_ID]
                if item['Location'] == location:
                    f.write('#define ' + item['Item Shorthand Macro'] + '             (((' + item['Entry Type Name'] +
                            '*)' + item['Entry Name in Code'] + '.Value)->' + item['Item Name in Code'] + ')\n')
                    f.write('#define ' + item['Item Shorthand Macro'] +
                            '_ENTRY_PTR   (&' + item['Entry Name in Code'] + ')\n')
                    f.write('\n')

            f.write('\n')
            f.write('#ifdef __cplusplus\n')
            f.write('}\n')
            f.write('#endif\n')
            f.write('\n')
            f.write('#endif // NVM_' + location.upper() + 'EEPROMENTRIES_H\n')
            f.write('\n')

        with codecs.open('NVM_' + location + 'EEPROMEntries.c', 'w', "utf-8") as f:
            f.write("/*\n")
            f.write(
                r" *      __ ________               _____                               ______          __    " + "\n")
            f.write(
                r" *     / //_/_  __/___ _____ _   / ___/____  __  _______________     / ____/___  ____/ /__  " + "\n")
            f.write(
                r" *    / ,<   / / / __ `/ __ `/   \__ \/ __ \/ / / / ___/ ___/ _ \   / /   / __ \/ __  / _ \ " + "\n")
            f.write(
                r" *   / /| | / / / /_/ / /_/ /   ___/ / /_/ / /_/ / /  / /__/  __/  / /___/ /_/ / /_/ /  __/ " + "\n")
            f.write(
                r" *  /_/ |_|/_/  \__,_/\__, /   /____/\____/\__,_/_/   \___/\___/   \____/\____/\__,_/\___/  " + "\n")
            f.write(
                r" *                   /____/                                                                 " + "\n")
            f.write(" *\n")
            f.write(" * This file is part of the KTag project, a DIY laser tag game with customizable features and wide interoperability.\n")
            f.write(
                " * 🛡️ <https://ktag.clubk.club> 🃞\n")
            f.write(" *\n")
            f.write(" */\n")

            f.write('\n')
            f.write('/** \\file\n')
            f.write(
                ' *  \\brief [Autogenerated] This file defines the ' + location + ' EEPROM entries.\n')
            f.write(' *\n')
            f.write(
                ' *  \\note AUTOGENERATED: This file was generated automatically on ' +
                '{dt:%A}, {dt:%B} {dt.day}, {dt.year} at {dt:%r}'.format(dt=datetime.datetime.now()) +
                '.\n')
            f.write(' *                       DO NOT MODIFY THIS FILE MANUALLY!\n')
            f.write(' */\n')
            f.write('\n')

            f.write('/* Include Files */\n')
            f.write('#include "KTag.h"\n')
            f.write('\n')
            f.write('/* EEPROM Entries */\n')
            f.write('\n')
            f.write(r'/** \defgroup NVM_' + location.upper() +
                    '_EEPROM NVM ' + location + ' EEPROM\n')
            f.write(' *\n')
            f.write(' * The ' + location +
                    ' EEPROM is divided into logical "entries", represented by instances of the #NVM_EEPROMEntry_T type.\n')
            f.write(' * At startup, these entries are loaded into their respective RAM copies by NVM_Init' +
                    location + 'EEPROM().  The application\n')
            f.write(' * then updates the RAM copies directly, and requests that the NVM_' +
                    location + 'EEPROMTask() save these back to the EEPROM\n')
            f.write(' * when necessary.\n')
            f.write(' * @{ */\n')
            f.write('\n')

            # Here is the magic: keep track of the locations in EE as we create the data structures.
            current_EE_address_in_bytes = 0

            for entry in NVMEntriesByLocation[location]:
                entry_name_in_code = 'NVM_' + entry.replace(' ', '_')
                entry_RAM_name_in_code = 'RAM_' + entry.replace(' ', '_')
                entry_default_name_in_code = 'DEFAULT_' + \
                    entry.replace(' ', '_')
                entry_type_name = entry_name_in_code + '_T'
                f.write('static ' + entry_type_name + ' ' +
                        entry_RAM_name_in_code + ';\n')
                f.write('\n')
                f.write('static const ' + entry_type_name + ' ' +
                        entry_default_name_in_code + ' = \n')
                f.write('{\n')

                # Assign defaults to all the items in this entry, and calculate the total size.
                entry_size_in_bytes = 0
                CRC_size_in_bytes = 2
                for item_ID in NVMItemsByID:
                    item = NVMItemsByID[item_ID]
                    if item['Entry'] == entry:
                        if item['Description'] is not None:
                            f.write('    //! ' + item['Description'] + '\n')
                        f.write(
                            '    .' + item['Item Name in Code'] + ' = ' + str(item['Default Value']) + ',\n')
                        entry_size_in_bytes += item['Data Size in Bytes']

                f.write('};\n')
                f.write('\n')

                f.write('NVM_EEPROMEntry_T ' + entry_name_in_code + ' = \n')
                f.write('{\n')
                f.write('    //! Size == sizeof(' + entry_type_name + ')\n')
                f.write('    .Size = ' + str(entry_size_in_bytes) + ',\n')
                f.write('    .EE_Address = ' +
                        str(current_EE_address_in_bytes) + ',\n')
                current_EE_address_in_bytes += entry_size_in_bytes
                f.write('    .EE_CRC_Address = ' +
                        str(current_EE_address_in_bytes) + ',\n')
                current_EE_address_in_bytes += CRC_size_in_bytes
                f.write('    .Value = (uint8_t *)&' +
                        entry_RAM_name_in_code + ',\n')
                f.write('    .Default = (uint8_t *)&' +
                        entry_default_name_in_code + ',\n')
                f.write('    .State = NVM_STATE_UNINITIALIZED\n')
                f.write('};\n')

            f.write('\n')
            f.write('/** @} */\n')
            f.write('\n')
            f.write('NVM_EEPROMEntry_T * const NVM_' +
                    location + 'EEPROMEntries[] =\n')
            f.write('{\n')

            for entry in NVMEntriesByLocation[location]:
                entry_name_in_code = 'NVM_' + entry.replace(' ', '_')
                f.write('    &' + entry_name_in_code + ',\n')

            f.write('};\n')
            f.write('\n')
            f.write('//! Size of the #NVM_' + location +
                    'EEPROMEntries array (i.e. the number of ' + location + ' EEPROM entries).\n')
            f.write('const uint8_t NVM_N_' + location.upper() + '_EEPROM_ENTRIES = (uint8_t) (sizeof(NVM_' +
                    location + 'EEPROMEntries) / sizeof(NVM_EEPROMEntry_T *));\n')
            f.write('\n')

    for destination in DESTINATION_PATHS:
        destination = os.path.normpath(os.path.join(SCRIPT_PATH, destination))
        logger.info("Copying files to " + destination + ".")
        for file in glob.glob("NVM_*.[ch]"):
            shutil.copy2(file, destination)

    logger.info("Cleaning up.")
    for file in glob.glob("NVM_*.[ch]"):
        path = pathlib.Path(file)
        path.unlink()

    logger.info('Nonvolatile memory autocode generation completed.')


if __name__ == "__main__":
    main()
