
const conditionMap = require('./condition_map.json');
const dependencyMap = require('./dependency_map.json');
const defaultDefines = require('./default_defines.json');
const moduleLibrary = require('./modules');

/**
 * Flatten defines keys to be single level object
 *
 * @param {Object} defines
 * @returns {Object} {"key.with.joined.dots": "fuu"}
 */
function flattenDefines(defines) {
    let toReturn = {};

    Object.keys(defines).forEach(key => {
        if ((typeof defines[key]) === 'object' && defines[key] !== null) {
            let flatObject = flattenDefines(defines[key]);
            Object.keys(flatObject).forEach(subKey => {
                toReturn[key + '.' + subKey] = flatObject[subKey];
            });
        } else {
            toReturn[key] = defines[key];
        }
    });

    return toReturn;
}


/**
 *
 * Calculate basic boolean operation using arrays and strings
 *
 * IE an "AND":
 * ["AND", "condition_1", "condition_2"]
 *
 * an "OR":
 * ["OR", "condition_1", "condition_2"]
 *
 * and maybe nested
 * [
 *  "AND",
 * ["OR", "condition_1", "condition_2"],
 * ["OR", "condition_3", "condition_4"]
 * ]
 *
 * It also replace the string conditions with the defines
 *
 * @param {Array|String} condition
 * @param {Object} defines
 * @returns {Boolean} Result of the calculation of the conditions
 */
function calculate(condition, defines) {
    if (typeof condition === 'string') {
        return Boolean(defines[condition]);
    }

    if (!(condition instanceof Array)) {
        throw Error('Not supported type: ' + condition);
    }

    const operation = condition[0];

    switch (operation) {
        case 'AND':
            return condition.slice(1).every((item) => calculate(item, defines));
        case 'OR':
            return condition.slice(1).some((item) => calculate(item, defines));
        case '==':
            return defines[condition[1]] === condition[2];
        default:
            throw Error('wrong operation ' + operation);
    }
}

/**
 * Return a list of the modules to skip
 *
 * @param {Object} iConditionMap
 * @param {Object} defines
 * @param {Object} dictionary
 * @returns {Set} List of ids of the modules
 */
function resolveSkipModules(iConditionMap, defines, dictionary) {
    let skipList = new Set();
    for (const key in iConditionMap) {
        if (!calculate(iConditionMap[key], defines)) {
            skipList.add(dictionary[key]);
        }
    }
    return skipList;
}

/**
 *  This function transform or add all the dependencies into an accumulator
 *
 * @param {Map} acc  Accumulator of all the dependencises
 * @param {String|Number} moduleId
 * @param {Object} dependencies Object containing all the
 * @param {Set} skipList  List of modules ids to skip
 */
function getDependencies(acc, moduleId, dependencies, skipList) {
    acc.set(moduleId, 'processing');
    let list = dependencies[moduleId] || [];
    list.filter(item => !skipList.has(item))
        .forEach(depModuleId => {
            if (!dependencies[depModuleId]) {
                acc.set(depModuleId, 'done');
                return;
            }
            switch (acc.get(depModuleId)) {
                case 'processing':
                    // console.warn('Circular dependency: ', depModuleId, moduleId);
                    return;
                case 'done':
                    // already processed
                    return;
                case undefined:
                    // not processed yet
                    getDependencies(acc, depModuleId, dependencies, skipList);
            }
        });
    acc.set(moduleId, 'done');
}

/**
 * Will merge source 'object' and replace childs and elements on 'target'.
 * It mutate target.
 *
 * @param {Object} source
 * @param {Object} target
 */
function merge(source, target) {
    for (const [key, val] of Object.entries(source)) {
        if (val !== null && typeof val === 'object') {
            target[key] ??= Object.create(Object.getPrototypeOf(val));
            merge(val, target[key]);
        } else {
            target[key] = val;
        }
    }
}


/**
 * Reconstruct the snippet and return the string.
 *
 * @param {Object} defines.   Please check ./defines.js to get definitions of this
 */
module.exports = function reconstruct(defines) {

    // merge default defines with user defines
    if(typeof defines === 'string') {
        defines = JSON.parse(defines);
    }

    let finalDefines = {};
    merge(defaultDefines, finalDefines);
    merge(defines, finalDefines);

    // flatten defines
    const flatDefines = flattenDefines(finalDefines);

    // Use defines to get conditiona modules mapping.
    const skipModuleList = resolveSkipModules(conditionMap, flatDefines, dependencyMap.dictionary);
    const entryPointModuleId = dependencyMap.dictionary['entrypoint'];

    // loop through main js dependencies and skiping selected modules.
    let selectedModules = new Map();
    getDependencies(selectedModules, entryPointModuleId, dependencyMap.dependencies, skipModuleList);
    selectedModules.delete(entryPointModuleId);

    let allRequiredModules = [];
    selectedModules.forEach((_, selectedModule) => {
        const modulePath = String(selectedModule).replace(/\?/g, './i/').replace(/ /g, '___');
        const moduleDataSring = moduleLibrary[modulePath];

        // TODO: selectedModule may be a number when node environment is production
        //       not stringify in that case
        allRequiredModules.push(JSON.stringify(selectedModule) + ': ' + moduleDataSring);

    });

    // Module object sepatator for production packages
    const spacer = typeof entryPointModuleId === 'number' ? ' ' : '\n';

    // load base
    // replace placeholder with module object
    let preDataString = require('./base.js').replace('___MODULES_PLACEHOLDER___', () => `{${spacer}${allRequiredModules.join(`,${spacer}`)}${spacer}}`);

    const definesRegex = new RegExp('([^_])(' + Object.keys(flatDefines).join('|') + ')(\\W)', 'g');

    // replace defines on module strings
    var data = preDataString.replace(definesRegex, (_, g0, g1, g2) => g0 + flatDefines[g1] + g2);

    return data;
};
