'''
Update/generate 'buildData.json' file in '.vscode' subfolder from new Makefile.
This file also handles 'toolsPaths.json' file.
New Makefile is not updated by this script - it is updated with 'updateMakefile.py' or 'updateWorkspaceSources.py'
'''
import os
import json
import datetime

import utilities as utils
import templateStrings as tmpStr

import updatePaths as pth
import updateMakefile as mkf
import updateWorkspaceSources as wks

__version__ = utils.__version__


class BuildDataStrings():
    # project sources, includes, defines, ....
    cSources = 'cSources'
    asmSources = 'asmSources'
    ldSources = 'ldSources'

    cIncludes = 'cIncludes'
    asmIncludes = 'asmIncludes'
    ldIncludes = 'ldIncludes'

    cDefines = 'cDefines'
    asmDefines = 'asmDefines'

    cFlags = 'cFlags'
    asmFlags = 'asmFlags'
    ldFlags = 'ldFlags'

    buildDirPath = 'buildDir'

    # build/interface tools paths, configuration files
    gccInludePath = 'gccInludePath'  # GCC standard libraries root folder path
    gccExePath = 'gccExePath'  # path to 'gcc.exe'

    buildToolsPath = 'buildToolsPath'  # path to 'make.exe'
    targetExecutablePath = 'targetExecutablePath'  # path to downloadable '*.elf' file

    pythonExec = 'pythonExec'

    openOcdPath = 'openOcdPath'  # path to 'openocd.exe'
    openOcdInterfacePath = "openOcdInterfacePath"  # path to OpenOCD interface cofniguration file (currently 'stlink.cfg')

    openOcdConfig = 'openOcdConfig'  # path to target '*.cfg' file
    stm32SvdPath = 'stm32SvdPath'  # path to target '*.svd' file

    cubeMxProjectPath = 'cubeMxProjectPath'

    # list of paths that are automatically built (default, system or once their 'parent' paths are valid)
    derivedPaths = [
        pythonExec,
        gccInludePath
    ]

    # list of target-specific configuration paths that must exist in 'buildData.json'
    targetConfigurationPaths = [
        openOcdConfig,
        stm32SvdPath
    ]

    # list of paths that can be cached in 'toolsPaths.json'
    toolsPaths = [
        gccExePath,
        buildToolsPath,
        pythonExec,
        openOcdPath,
        openOcdInterfacePath
    ]


class BuildData():
    def __init__(self):
        self.mkfStr = mkf.MakefileStrings()
        self.cPStr = wks.CPropertiesStrings()
        self.bStr = BuildDataStrings()

    def prepareBuildData(self, request=False):
        '''
        This function is used in all 'update*.py' scripts and makes sure, that 'toolsPaths.json' and 'buildData.json' with a 
        valid tools/target cofniguration paths exist. Invalid paths are updated (requested from the user).
        Returns available, valid build data.

        Note: tools paths listed in 'BuildDataStrings.toolsPaths' are stored in system local 'toolsPaths.json' file, and are 
        copied (overwritten) to 'buildData.json' on first 'Update' task run. This makes it possible for multiple code contributors.
        '''
        paths = pth.UpdatePaths()

        self.checkBuildDataFile()
        buildData = self.getBuildData()

        if self.checkToolsPathFile():  # a valid toolsPaths.json exists
            toolsPathsData = self.getToolsPathsData()

        else:
            # no valid data from 'toolsPaths.json' file
            # try to get data from current 'buildData.json' - backward compatibility for paths that already exist in 'buildData.json'
            toolsPathsData = json.loads(tmpStr.toolsPathsTemplate)
            for path in self.bStr.toolsPaths:
                if path in buildData:
                    if utils.pathExists(buildData[path]):
                        toolsPathsData[path] = buildData[path]

        # update/overwrite tools paths file. Don't mind if paths are already valid.
        toolsPathsData = paths.verifyToolsPaths(toolsPathsData, request)
        self.createUserToolsFile(toolsPathsData)

        buildData = self.addToolsPathsToBuildData(buildData, toolsPathsData)

        templateBuildData = json.loads(tmpStr.buildDataTemplate)
        buildData = utils.mergeCurrentDataWithTemplate(buildData, templateBuildData)

        buildData = paths.verifyTargetConfigurationPaths(buildData, request)
        buildData = paths.copyTargetConfigurationFiles(buildData)

        return buildData

    def checkToolsPathFile(self):
        '''
        Returns True if 'toolsPaths.json' file exists and is a valid JSON file.
        If it is not a valid JSON, delete it and return False.
        '''
        if utils.pathExists(utils.toolsPaths):
            # file exists, check if it loads OK
            try:
                with open(utils.toolsPaths, 'r') as toolsFileHandler:
                    json.load(toolsFileHandler)
                    print("Valid 'toolsPaths.json' file found.")
                return True

            except Exception as err:
                errorMsg = "Invalid 'toolsPaths.json' file. Error:\n" + str(err)
                print(errorMsg)

                try:
                    os.remove(utils.toolsPaths)
                    msg = "\tDeleted. New 'toolsPaths.json' will be created on first workspace update."
                    print(msg)
                except Exception as err:
                    errorMsg = "Error deleting 'toolsPaths.json'. Error:\n" + str(err)
                    utils.printAndQuit(errorMsg)

        # else: toolsPaths.json does not exist
        return False

    def checkBuildDataFile(self):
        '''
        This function makes sure 'buildData.json' is available. 
        If existing 'buildData.json' file is a valid JSON, it returns immediately. 
        If it is not a valid JSON file OR it does not exist, new 'buildData.json' file is created from template.

        Note: There is no backup file for buildData.json, since it is always regenerated on Update task.
        '''
        if utils.pathExists(utils.buildDataPath):
            # file exists, check if it loads OK
            try:
                with open(utils.buildDataPath, 'r') as buildDataFileHandler:
                    json.load(buildDataFileHandler)
                    print("Valid 'buildData.json' file found.")

                return

            except Exception as err:
                errorMsg = "Invalid 'buildData.json' file. Error:\n" + str(err)
                print(errorMsg)

                try:
                    os.remove(utils.buildDataPath)
                    msg = "\tDeleted. New 'buildData.json' will be created on first workspace update."
                    print(msg)
                except Exception as err:
                    errorMsg = "Error deleting 'buildData.json'. Error:\n" + str(err)
                    utils.printAndQuit(errorMsg)

        # else: buildData.json does not exist
        self.createBuildDataFile()

    def createUserToolsFile(self, toolsPaths):
        '''
        Create 'toolsPaths.json' file with current tools paths.
        This pats are absolute and not project-specific.
        '''
        data = json.loads(tmpStr.toolsPathsTemplate)
        try:
            data["VERSION"] = __version__
            data["LAST_RUN"] = str(datetime.datetime.now())

            for path in self.bStr.toolsPaths:
                data[path] = toolsPaths[path]

            data = json.dumps(data, indent=4, sort_keys=False)
            with open(utils.toolsPaths, 'w+') as toolsPathsFile:
                toolsPathsFile.write(data)
            print("'toolsPaths.json' file updated!")

        except Exception as err:
            errorMsg = "Exception error overwriting 'toolsPaths.json' file:\n"
            errorMsg += str(err)
            print("WARNING:", errorMsg)

    def createBuildDataFile(self):
        '''
        Create fresh 'buildData.json' file.
        '''
        try:
            data = json.loads(tmpStr.buildDataTemplate)
            dataToWrite = json.dumps(data, indent=4, sort_keys=False)

            with open(utils.buildDataPath, 'w+') as buildDataFile:
                buildDataFile.truncate()
                buildDataFile.write(dataToWrite)

            print("New template 'buildData.json' file created.")
        except Exception as err:
            errorMsg = "Exception error creating new 'buildData.json' file:\n"
            errorMsg += str(err)
            utils.printAndQuit(errorMsg)

    def getToolsPathsData(self):
        '''
        Get data from current 'toolsPaths.json' file.
        File existance is previoulsy checked in 'checkToolsPathFile()'.
        '''
        with open(utils.toolsPaths, 'r') as toolsPathsFile:
            data = json.load(toolsPathsFile)

        return data

    def getBuildData(self):
        '''
        Get data from current 'buildData.json' file.
        File existance is previoulsy checked in 'checkBuildDataFile()'.
        '''
        with open(utils.buildDataPath, 'r') as buildDataFile:
            data = json.load(buildDataFile)

        return data

    def addToolsPathsToBuildData(self, buildData, toolsPaths):
        '''
        Get tools paths from 'toolsPaths.json' and add it to buildData
        Returns new data.
        '''
        allToolsPaths = []
        allToolsPaths.extend(self.bStr.toolsPaths)
        allToolsPaths.extend(self.bStr.derivedPaths)
        for path in allToolsPaths:
            try:
                buildData[path] = toolsPaths[path]
            except Exception as err:
                errorMsg = "Missing '" + path + "' key in tools paths data:\n" + str(toolsPaths)
                print("Warning:", errorMsg)

        return buildData

    def addMakefileDataToBuildDataFile(self, buildData, makefileData):
        '''
        This function fills buildData.json file with data from 'Makefile'.
        Returns new data.
        '''
        # sources
        cSources = makefileData[self.mkfStr.cSources]
        buildData[self.bStr.cSources] = cSources

        asmSources = makefileData[self.mkfStr.asmSources]
        buildData[self.bStr.ldSources] = asmSources

        ldSources = makefileData[self.mkfStr.ldSources]
        buildData[self.bStr.ldSources] = ldSources

        # includes
        cIncludes = makefileData[self.mkfStr.cIncludes]
        buildData[self.bStr.cIncludes] = cIncludes

        asmIncludes = makefileData[self.mkfStr.asmIncludes]
        buildData[self.bStr.asmIncludes] = asmIncludes

        ldIncludes = makefileData[self.mkfStr.ldIncludes]
        buildData[self.bStr.ldIncludes] = ldIncludes

        # defines
        cDefines = makefileData[self.mkfStr.cDefines]
        buildData[self.bStr.cDefines] = cDefines

        asmDefines = makefileData[self.mkfStr.asmDefines]
        buildData[self.bStr.asmDefines] = asmDefines

        # compiler flags and paths
        cFlags = makefileData[self.mkfStr.cFlags]
        buildData[self.bStr.cFlags] = cFlags

        asmFlags = makefileData[self.mkfStr.asmFlags]
        buildData[self.bStr.asmFlags] = asmFlags

        ldFlags = makefileData[self.mkfStr.ldFlags]
        buildData[self.bStr.ldFlags] = ldFlags

        # build folder must be always inside workspace folder
        buildDirPath = makefileData[self.mkfStr.buildDir]
        buildData[self.bStr.buildDirPath] = buildDirPath

        # Target executable '.elf' file
        projectName = makefileData[self.mkfStr.projectName]
        targetExecutablePath = utils.getBuildElfFilePath(buildDirPath, projectName)
        buildData[self.bStr.targetExecutablePath] = targetExecutablePath

        return buildData

    def addCubeMxProjectPathToBuildData(self, buildData):
        '''
        If utils.cubeMxProjectFilePath is not None, add/update 'cubeMxProjectPath' field to 'buildData.json'.
        '''
        if utils.cubeMxProjectFilePath is not None:
            buildData[self.bStr.cubeMxProjectPath] = utils.cubeMxProjectFilePath
        else:
            buildData.pop(self.bStr.cubeMxProjectPath)
        return buildData

    def overwriteBuildDataFile(self, data):
        '''
        Overwrite existing 'buildData.json' file with new data.
        '''
        try:
            with open(utils.buildDataPath, 'r+') as buildDataFile:
                data["VERSION"] = __version__
                data["LAST_RUN"] = str(datetime.datetime.now())

                buildDataFile.seek(0)
                buildDataFile.truncate()
                dataToWrite = json.dumps(data, indent=4, sort_keys=False)
                buildDataFile.write(dataToWrite)

            print("'buildData.json' file updated!")

        except Exception as err:
            errorMsg = "Exception error overwriting 'buildData.json' file:\n"
            errorMsg += str(err)
            utils.printAndQuit(errorMsg)


########################################################################################################################
if __name__ == "__main__":
    utils.verifyFolderStructure()

    paths = pth.UpdatePaths()
    makefile = mkf.Makefile()
    bData = BuildData()

    # Makefile must exist - # point in continuing if Makefile does not exist
    makefile.checkMakefileFile()

    # build data (update tools paths if neccessary)
    buildData = bData.prepareBuildData()

    # data from current Makefile
    makeExePath = buildData[bData.bStr.buildToolsPath]
    gccExePath = buildData[bData.bStr.gccExePath]
    makefileData = makefile.getMakefileData(makeExePath, gccExePath)

    # try to add CubeMX project file path
    buildData = bData.addCubeMxProjectPathToBuildData(buildData)

    buildData = bData.addMakefileDataToBuildDataFile(buildData, makefileData)

    bData.overwriteBuildDataFile(buildData)
