/*
 * Copyright 2017-2018 NXP
 * To be used with MCUXpresso Config Tools under its Software License Agreement.
 */

// This object opens a file and writes into the file
OutputFile = function(fileName, coreId) {
    var self = this;

    // name of the generated file
    self.fileName = fileName;

    // output stream
    self.output = scriptApi.createFile(fileName, coreId);

    // write file content and 'close' file object
    self.close = function(content) {
        self.output.write(content);
        self.fileName = "";
        self.output = null;
    }
}

// Object contains frequently used helper functions for processing arrays, strings and comments
Utils = function() {
    var self = this;

    // Regex for matching empty strings or strings containing non-printable characters only
    self.emptyStringRegex = new RegExp(/^\s*$/);

    // function creates a string consisting of characters defined by first argument with length defined by second argument (e. g. utils.makeString('*', 5) will return '*****')
    self.makeString = function(character, count) {
        return Array(count + 1).join(character);
    }

    // function determines, if input string defined by argument contains no printable character (return value will be true for strings "", "  ", "  \n  " etc...)
    self.isEmpty = function(str) {
        return self.emptyStringRegex.test(str);
    }

    // function determines, if input string defined by argument contains at least one printable character (return value will be true for strings "f", "  f", "f  \n  " etc...)
    self.notEmpty = function(str) {
        return !self.isEmpty(str);
    }

    // function trims newlines from the beginning and the end of string
    self.trimNewlines = function(str) {
        return str.replace(/^\n*|\n*$/g, '');
    }

    // function creates and returns usual comment like '/* comment */'
    self.makeComment = function(content) {
        return '/* ' + content + ' */';
    }

    // function creates and returns initialization comment from array of strings defined by content in argument
    self.makeInitComment = function(content) {
        if (self.isEmpty(content)) {
            return "";
        }
        return (
            "/" +
            self.makeString("*", 119) +
            [""].concat(content).join("\n * ") +
            "\n " +
            self.makeString("*", 118) +
            "/"
        );
    }

    // function removes duplicit lines from string defined by argument
    self.removeDuplicitLines = function(str) {
        var linesArray = str.split("\n");
        var uniqueArray = linesArray.filter(function(item, pos) {
            return linesArray.indexOf(item) == pos;
        });
        return uniqueArray.join("\n");
    }

    // function will return an array containing values of object defined by argument (converts associative array to array indexed by numbers)
    self.objectValues = function(o) {
        var keys = Object.keys(o);
        values = [];
        for (var k in keys) {
            values.push(o[keys[k]]);
        }
        return values;
    }

    // function returns indentation string (spaces) of level defined by argument, tab width is set to 2 spaces by default
    self.indent = function(level) {
        var tab = "  ";
        return self.makeString(tab, level);
    }

    // function renders section of generated code consisting of a comment in header of section and its content (C source code - second argument)
    self.renderSection = function(content, sectionName) {
        if (self.isEmpty(content)) {
            return "";
        }

        if (self.isEmpty(sectionName)) {
            return content;
        }

        initComment = self.makeInitComment([sectionName]);
        return [initComment, content].join("\n");
    }

    // function wraps input string into clang-off and clang-on directives in order to ignore the fragment code by clang formatter (used for YAML sections)
    self.clangOff = function(str) {
        return self.notEmpty(str)
            ? [self.makeComment('clang-format off'), str, self.makeComment('clang-format on')].join("\n")
            : "";
    }
};

SharedResources = function() {
    var self = this;
    // global profile object
    self.profile = scriptApi.getProfile();

    // collection of functional groups
    self.functionalGroups = self.profile.getFunctionalGroups();

    // coresList is an array containing list of coreIds of each MCU cores, e. g.: ['core0', 'core1']
    self.coresList = Object.keys(
        JSON.parse(self.profile.getMcuInfo().getCoresList())
    );

    // flag defining, if user selected to generate YAML in GUI
    self.enableYaml = self.profile.getYaml();

    // define peripherals priority in order to call initialization functions in proper order (lower number represents higher priority)
    self.peripheralsPriority = {
        'xrdc'  : 0,
        'dma'   : 1,
        'edma'  : 1,
        'other' : 100
    };

    // function returns global YAML of profile together with YAML of global config sets of all components wrapped in clang-off/clang-on directives
    // if generate YAML option is disabled in GUI, returns empty string
    self.getGlobalYaml = function() {
        var componentConfigurations = self.profile.getComponentConfigurations();
        var globalYaml = self.getYaml(self.profile);

        for (var iterator = componentConfigurations.iterator(); iterator.hasNext();) {
            var componentConfiguration = iterator.next();
            globalYaml += componentConfiguration.getGlobalConfigSet()
                ? '\n\n' + self.getYaml(componentConfiguration)
                : "";
        }
        return utils.clangOff(globalYaml);
    };

    // function checks, if generate YAML option is enabled in GUI and returns YAML section of element (component configuration or profile)
    // if generate YAML option is disabled in GUI, returns empty string
    self.getYaml = function(element) {
        return self.enableYaml ? element.getYaml() : "";
    };

    // string containing C comment with initial notification about manual modification of generated files (will be generated at the top of each generated files)
    self.notification = utils.makeInitComment([
        "This file was generated by the MCUXpresso Config Tools. Any manual edits made to this file",
        "will be overwritten if the respective MCUXpresso Config Tools is used to update this file."
    ]);
};

// Object maps java ComponentInstance objects and describes all attributes and helper functions needed for generation of code related to components
ComponentDescriptor = function(componentInstance, functionalGroup) {
    var self = this;

    // name of the component
    self.name = componentInstance.getName();

    // prefix of the functional group passed to constructor
    self.prefix = functionalGroup.prefix;

    // YAML describing the component configuration
    self.yaml = utils.clangOff(sharedResources.getYaml(componentInstance));

    // save peripheral type of the component (for example 'UART', 'ADC', 'DMA' etc...)
    self.peripheralType = componentInstance.getMcu().getPeripheralType(componentInstance.getPeripheral()).toLowerCase();

    // priority of the component in order to call component initialization functions in proper order
    //lower number means higher priority
    self.priority = (self.peripheralType in sharedResources.peripheralsPriority)
                     ? sharedResources.peripheralsPriority[self.peripheralType]
                     : sharedResources.peripheralsPriority['other'];

    // names of C code sections related to the component, which will be generated into peripherals.c file
    self.cFileSections = {
        global_vars: "",
        init_function_open: "\nvoid " + self.prefix + self.name + "_init(void) {\n",
        init_function_vars: "",
        init_function_preinit3: "",
        init_function_preinit2: "",
        init_function_preinit1: "",
        init_function_preinit0: "",
        init_function_body: "",
        init_function_postinit0: "",
        init_function_postinit1: "",
        init_function_postinit2: "",
        init_function_postinit3: "",
        init_function_close: "}"
    };

    // names of C code sections related to the component, which will be generated into peripherals.h file
    self.hFileSections = {
        includes: "",
        defines: "",
        global_vars_extern: "",
        callback_function_extern: ""
    }

    // names of C code sections related to the component, which should be generated at the beginning or an the end of functional group body
    self.globalSections = {
        global_preinit5: "",
        global_preinit4: "",
        global_preinit3: "",
        global_preinit2: "",
        global_preinit1: "",
        global_preinit0: "",
        global_dma_preinit: "",
        global_xrdc_preinit: "",
        global_postinit0: "",
        global_postinit1: "",
        global_postinit2: "",
        global_postinit3: "",
        global_postinit4: "",
        global_postinit5: ""
    }

    // function returns string containing call of the init function related to the component (e. g. 'BOARD_I2C_1_init();') 
    self.renderCall = function() {
        return self.prefix + self.name + "_init();";
    }

    // function adds initial comment and component YAML section to initialization C code related to the component configuration (to be used in peripherals.c)
    self.wrap = function(mainContent) {
        return [
            utils.makeInitComment([self.name + " initialization code"]),
            self.yaml,
            mainContent
        ].join("\n");
    }

    // function returns string containing initialization C code of the component with YAML, initial comment and the source code (to be used in peripherals.c)
    self.renderInitialization = function() {
        return self.wrap(utils.objectValues(self.cFileSections).join(''));
    }

    // function fills file sections with corresponding code emitted by java configSet.emit function
    self.processSections = function(configSet, sections) {
        var codeEmitter = configSet.getCodeEmitter();
        for (var sectionId in sections) {
            if (utils.notEmpty(codeFragment = codeEmitter.emit(sectionId))) {
                sections[sectionId] += codeEmitter.emit(sectionId);
            }
        }
    }

    // if component instance has an error (has not valid configuration), log warning and generate comment with error into peripherals.c file
    // else fill all corresponding peripherals.h and peripherals.c file sections with C code corresponding to the component configuration
    if (componentInstance.getError()) {
        scriptApi.logWarning("Configuration of the component " + self.name + " of functional group " + functionalGroup.name + " is not valid.");
        self.cFileSections['init_function_body'] = utils.indent(1) + utils.makeComment("Configuration of the component " + self.name + " of functional group " + functionalGroup.name + " is not valid.") + '\n';
    } else {
        var configSets = componentInstance.getChildren();
        for (componentSettingIndex in configSets) {
            self.processSections(configSets[componentSettingIndex], self.cFileSections);
            self.processSections(configSets[componentSettingIndex], self.hFileSections);
            self.processSections(configSets[componentSettingIndex], self.globalSections);
        }
    }
}

// Object maps java FunctionalGroup objects and describes all attributes and helper functions needed for generation of code related to functional groups
FunctionalGroupDescriptor = function(functionalGroup) {
    var self = this;

    // name of the functional group
    self.name = functionalGroup.getName();

    // prefix of the functional group
    self.prefix = functionalGroup.getIdPrefix();

    // flag defining, if the functional group should be called from BOARD_InitBootPeripherals() function
    self.isCalledFromDefaultInit = functionalGroup.isCalledFromDefaultInit();

    // empty list of component instances corresponding to the functional group - will be filled with ComponentDescriptor objects
    self.components = [];

    // function returns callings to each components corresponding to current functional group with initialization comment in header
    // returns empty string, if no component belongs to the functional group
    self.renderComponentsCalls = function() {
        if (self.components.length > 0) {
            var componentCalls = self.components.map(function(component) {
                return utils.indent(1) + component.renderCall();
            }).join('\n');
            var componentsInitComment = utils.indent(1) + utils.makeComment('Initialize components');
            return [componentsInitComment, componentCalls].join('\n');
        }
        return '';
    }

    // function returns pre-initialization or post-initialization (defined by prefix argument - 'pre' or 'post') C code sections for functional group with initialization comment
    // returns empty string, if no global sections code is defined for the functional group
    self.renderGlobalSections = function(prefix) {
        var globalSections = self.getGlobalSections(prefix).filter(utils.notEmpty).join('');
        var globalInitComment = utils.indent(1) + utils.makeComment('Global initialization');

        if (globalSections.length > 0) {
            return [globalInitComment].concat(globalSections).join('\n');
        }
        return '';
    }

    // function returns string of the definition (head and body) of the functional group (containing high and low priority code and calls to components of the functional group)
    self.renderDefinition = function() {
        var open = ["void " + self.name + "(void)", "{"];
        var content = [self.renderGlobalSections('pre'), self.renderComponentsCalls(), self.renderGlobalSections('post')]
                       .filter(utils.notEmpty)
                       .map(utils.trimNewlines)
                       .join('\n\n');
        var close = ["}"];

        return open
            .concat(content)
            .concat(close)
            .filter(utils.notEmpty)
            .join("\n");
    }

    // function returns string of the declaration of the functional group (e. g. 'void BOARD_InitPeripherals(void);')
    self.renderDeclaration = function() {
        return "void " + self.name + "(void);";
    }

    // function returns string of the functional group call (e. g. 'BOARD_InitPeripherals();')
    self.renderCall = function() {
        return self.name + "();";
    }

    // function returns string containing initialization C code of the functional group with initial comment
    // and the source code containing initialization code for all its components (to be used in peripherals.c)
    self.renderInitialization = function() {
        var componentsDefinitions = self.components
            .map(function(component) {return component.renderInitialization();})
            .join("\n\n");
        return utils.renderSection(componentsDefinitions, self.name + " functional group");
    }

    // function returns string of processed C code section for peripherals.h file related to the functional group (e. g. returns all define directives for the functional group)
    self.getHSection = function(sectionId, comment) {
        var sectionCode = self.components
            .map(function(component) {return component.hFileSections[sectionId];})
            .join("");

        return ((utils.notEmpty(comment) && utils.notEmpty(sectionCode)) ? utils.makeComment(comment) + '\n' : "") + sectionCode;
    }

    // function returns an object of processed C code sections for peripherals.h file related to the functional group
    self.getHFileSections = function() {
        return {
            includes: self.getHSection("includes", ""),
            defines: self.getHSection("defines", "Definitions for " + self.name + " functional group"),
            global_vars_extern: self.getHSection("global_vars_extern", ""),
            callback_function_extern: self.getHSection("callback_function_extern", "")
        };
    }

    // function returns string of processed C code section for peripherals.c file related to the functional group initialization parts called with custom priority
    self.getGlobalSection = function(sectionId) {
        return self.components
            .map(function(component) {return component.globalSections[sectionId];})
            .join("");
    }

    // function returns an object of processed C code sections for peripherals.c file related to the functional group initialization parts called with higher priority
    self.getGlobalSections = function(prefix) {
        var sections = [
            self.getGlobalSection("global_" + prefix + "init5"),
            self.getGlobalSection("global_" + prefix + "init4"),
            self.getGlobalSection("global_" + prefix + "init3"),
            self.getGlobalSection("global_" + prefix + "init2"),
            self.getGlobalSection("global_" + prefix + "init1"),
            self.getGlobalSection("global_" + prefix + "init0")
        ];

        if (prefix == 'post') {
            sections = sections.reverse();
        }
        else {
            sections = [self.getGlobalSection("global_xrdc_preinit"), self.getGlobalSection("global_dma_preinit")].concat(sections);
        }
        return sections;
    }

    // get functional group component instances from scriptAPI
    var componentInstances = functionalGroup.getComponentInstances();

    // map scriptAPI component instances objects to ComponentDescriptor objects and fill components list with them
    for (componentInstanceIndex in componentInstances) {
        self.components.push(new ComponentDescriptor(componentInstances[componentInstanceIndex], self));
    }
    // sort components by priority
    self.components = self.components.sort(function(component1, component2) {return component1.priority - component2.priority});
}

// Object maps functional groups corresponding to core defined by argument into a list of FunctionalGroupDescirptor objects
FunctionalGroupsMapper = function(coreId) {
    var self = this;

    // empty list of functional group instances corresponding to the core - will be filled with FunctionalGroupDescirptor objects
    self.functionalGroups = [];

    // function maps scriptAPI functional group objects to FunctionalGroupDescriptor objects and fills functionalGroups list with them
    self.processFunctionalGroups = function() {
        var functionalGroupsInstances = sharedResources.functionalGroups;
        for (functionalGroupIndex in functionalGroupsInstances) {
            var functionalGroup = functionalGroupsInstances[functionalGroupIndex];
            if (functionalGroup.getCore() == coreId) {
                self.functionalGroups.push(new FunctionalGroupDescriptor(functionalGroup));
            }
        }
    };

    // map scriptAPI functional group objects to FunctionalGroupDescriptor objects and fill functionalGroups list with them
    self.processFunctionalGroups();
}

// Object assembles parts of generated peripherals.h file
HFileDescriptor = function() {
    var self = this;

    // string containing an initial part of peripherals.h file
    self.head = ["#ifndef _PERIPHERALS_H_", "#define _PERIPHERALS_H_"].join("\n");

    // string containing a declaration of BOARD_InitBootPeripherals() function with its initial comment
    self.initBoot = [
        utils.makeInitComment(["BOARD_InitBootPeripherals function"]),
        "void BOARD_InitBootPeripherals(void);"
    ].join("\n");

    // string containing a closing part of peripherals.h file
    self.tail = ['#if defined(__cplusplus)',
                 '}',
                 '#endif',
                 '',
                 '#endif /* _PERIPHERALS_H_ */'
                 ].join('\n');

    // function adds a notification, initial and closing parts of the peripherals.h file content (defined by argument) and returns wrapped content of the file
    self.wrap = function(mainContent) {
        return [
            sharedResources.notification,
            self.head,
            mainContent,
            self.initBoot,
            self.tail
        ].join("\n\n");
    };

    self.groupSections = function(sectionsArray, sectionId) {
        return sectionsArray
            .map(function(functionalGroupSection) {return functionalGroupSection[sectionId];})
            .join("");
    }

    // function collects all code fragments in sections of the peripherals.h file from functional groups mapper array and returns final content of the file
    self.render = function() {
        // collect all sections of the peripherals.h file from all functional groups, then group the same sections from all functional groups
        var sectionsArray = functionalGroupsMapper.functionalGroups.map(function(functionalGroup) {
            return functionalGroup.getHFileSections();
        });

        // group declarations of all functional groups
        var initFunctions = functionalGroupsMapper.functionalGroups
            .map(function(functionalGroup) {return functionalGroup.renderDeclaration();})
            .join("\n");

        // group include directives from all functional groups
        var includes = self.groupSections(sectionsArray, 'includes');

        // group define directives from all functional groups
        var defines = sectionsArray
            .map(function(functionalGroupSection) {return functionalGroupSection['defines'];})
            .filter(utils.notEmpty)
            .join("\n");

        // group all extern global variables from all functional groups
        var globalVarsExtern = self.groupSections(sectionsArray, 'global_vars_extern');

        // group all extern callback functions from all functional groups
        var callbackFunctionExtern = self.groupSections(sectionsArray, 'callback_function_extern');

        var cppSections = [
            '#if defined(__cplusplus)',
            'extern "C" {',
            '#endif /* __cplusplus */',
            ''
        ].join('\n');

        // assembly collected sections and return final content of the peripherals.h file
        return self.wrap(
              [
                  utils.renderSection(utils.removeDuplicitLines(includes), "Included files"),
                  cppSections,
                  utils.renderSection(defines, "Definitions"),
                  utils.renderSection(globalVarsExtern, "Global variables"),
                  utils.renderSection(callbackFunctionExtern, "Callback functions"),
                  utils.renderSection(initFunctions, "Initialization functions")
              ]
              .filter(utils.notEmpty)
              .join("\n")
        );
    };
};

// Object assembles parts of generated peripherals.c file
CFileDescriptor = function() {
    var self = this;

    // function adds a notification and initial parts of the peripherals.c file content (defined by argument) and returns wrapped content of the file
    self.wrap = function(mainContent) {
        return [sharedResources.notification, sharedResources.getGlobalYaml()]
            .concat(mainContent)
            .filter(utils.notEmpty)
            .join("\n\n");
    };

    // function assembles and returns a final content of the peripherals.c file from its parts
    // (inludes, functional groups initializations, functional groups definitions and boot fucntions)
    self.render = function() {
        return self.wrap([
            utils.renderSection(self.includes, "Included files"),
            utils.renderSection(self.functionalGroupsInitializations, ""),
            utils.renderSection(self.initFunctions, "Initialization functions"),
            utils.renderSection(self.initBootFunction, "BOARD_InitBootPeripherals function")
        ]);
    };

    // string representing all include directives generated to peripherals.c file
    self.includes = '#include "peripherals.h"';

    // get definitions of all functional groups
    self.initFunctions = functionalGroupsMapper.functionalGroups
        .map(function(functionalGroup) {return functionalGroup.renderDefinition();})
        .join("\n\n");

    // get initializations of all functional groups
    self.functionalGroupsInitializations = functionalGroupsMapper.functionalGroups
        .map(function(functionalGroup) {return functionalGroup.renderInitialization();})
        .filter(utils.notEmpty)
        .join("\n\n");

    // get a definition of BOARD_InitBootPeripherals() function (head and body with all functional groups calls selected by user in GUI)
    self.initBootFunction = ["void BOARD_InitBootPeripherals(void)", "{"]
        .concat(functionalGroupsMapper.functionalGroups
                    .filter(function(functionalGroup) {return functionalGroup.isCalledFromDefaultInit;})
                    .map(function(functionalGroup) {return utils.indent(1) + functionalGroup.renderCall();}))
        .concat(["}"])
        .join("\n");
};

// main function will iterate over all MCU cores, creates list of corresponding functional groups and if it is not empty, generates peripherals.c/.h source files
function main() {
    for (var coreIndex = 0; coreIndex < sharedResources.coresList.length; coreIndex++) {
        var coreId = sharedResources.coresList[coreIndex];

        // prepare FunctionalGroupDescriptor objects only for current core
        functionalGroupsMapper = new FunctionalGroupsMapper(coreId);
        if (functionalGroupsMapper.functionalGroups.length > 0) {
            new OutputFile("peripherals.c", coreId).close(new CFileDescriptor().render());
            new OutputFile("peripherals.h", coreId).close(new HFileDescriptor().render());
        }
    }
}

// global objects declarations
var utils = new Utils();
var sharedResources = new SharedResources();

// functional groups will be filled from main() function for each core separatelly
var functionalGroupsMapper;

// generation script entry point
main();
