/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var fs = require("fs");
var path = require("path");
var vm = require("vm");
/**
 * Bundle `entryPoints` given config `config`.
 */
function bundle(entryPoints, config, callback) {
    var entryPointsMap = {};
    entryPoints.forEach(function (module) {
        entryPointsMap[module.name] = module;
    });
    var allMentionedModulesMap = {};
    entryPoints.forEach(function (module) {
        allMentionedModulesMap[module.name] = true;
        (module.include || []).forEach(function (includedModule) {
            allMentionedModulesMap[includedModule] = true;
        });
        (module.exclude || []).forEach(function (excludedModule) {
            allMentionedModulesMap[excludedModule] = true;
        });
    });
    var code = require('fs').readFileSync(path.join(__dirname, '../../src/vs/loader.js'));
    var r = vm.runInThisContext('(function(require, module, exports) { ' + code + '\n});');
    var loaderModule = { exports: {} };
    r.call({}, require, loaderModule, loaderModule.exports);
    var loader = loaderModule.exports;
    config.isBuild = true;
    loader.config(config);
    loader(['require'], function (localRequire) {
        var resolvePath = function (path) {
            var r = localRequire.toUrl(path);
            if (!/\.js/.test(r)) {
                return r + '.js';
            }
            return r;
        };
        for (var moduleId in entryPointsMap) {
            var entryPoint = entryPointsMap[moduleId];
            if (entryPoint.append) {
                entryPoint.append = entryPoint.append.map(resolvePath);
            }
            if (entryPoint.prepend) {
                entryPoint.prepend = entryPoint.prepend.map(resolvePath);
            }
        }
    });
    loader(Object.keys(allMentionedModulesMap), function () {
        var modules = loader.getBuildInfo();
        var partialResult = emitEntryPoints(modules, entryPointsMap);
        var cssInlinedResources = loader('vs/css').getInlinedResources();
        callback(null, {
            files: partialResult.files,
            cssInlinedResources: cssInlinedResources,
            bundleData: partialResult.bundleData
        });
    }, function (err) { return callback(err, null); });
}
exports.bundle = bundle;
function emitEntryPoints(modules, entryPoints) {
    var modulesMap = {};
    modules.forEach(function (m) {
        modulesMap[m.id] = m;
    });
    var modulesGraph = {};
    modules.forEach(function (m) {
        modulesGraph[m.id] = m.dependencies;
    });
    var sortedModules = topologicalSort(modulesGraph);
    var result = [];
    var usedPlugins = {};
    var bundleData = {
        graph: modulesGraph,
        bundles: {}
    };
    Object.keys(entryPoints).forEach(function (moduleToBundle) {
        var info = entryPoints[moduleToBundle];
        var rootNodes = [moduleToBundle].concat(info.include || []);
        var allDependencies = visit(rootNodes, modulesGraph);
        var excludes = ['require', 'exports', 'module'].concat(info.exclude || []);
        excludes.forEach(function (excludeRoot) {
            var allExcludes = visit([excludeRoot], modulesGraph);
            Object.keys(allExcludes).forEach(function (exclude) {
                delete allDependencies[exclude];
            });
        });
        var includedModules = sortedModules.filter(function (module) {
            return allDependencies[module];
        });
        bundleData.bundles[moduleToBundle] = includedModules;
        var res = emitEntryPoint(modulesMap, modulesGraph, moduleToBundle, includedModules, info.prepend, info.append, info.dest);
        result = result.concat(res.files);
        for (var pluginName in res.usedPlugins) {
            usedPlugins[pluginName] = usedPlugins[pluginName] || res.usedPlugins[pluginName];
        }
    });
    Object.keys(usedPlugins).forEach(function (pluginName) {
        var plugin = usedPlugins[pluginName];
        if (typeof plugin.finishBuild === 'function') {
            var write = function (filename, contents) {
                result.push({
                    dest: filename,
                    sources: [{
                            path: null,
                            contents: contents
                        }]
                });
            };
            plugin.finishBuild(write);
        }
    });
    return {
        // TODO@TS 2.1.2
        files: extractStrings(removeDuplicateTSBoilerplate(result)),
        bundleData: bundleData
    };
}
function extractStrings(destFiles) {
    var parseDefineCall = function (moduleMatch, depsMatch) {
        var module = moduleMatch.replace(/^"|"$/g, '');
        var deps = depsMatch.split(',');
        deps = deps.map(function (dep) {
            dep = dep.trim();
            dep = dep.replace(/^"|"$/g, '');
            dep = dep.replace(/^'|'$/g, '');
            var prefix = null;
            var _path = null;
            var pieces = dep.split('!');
            if (pieces.length > 1) {
                prefix = pieces[0] + '!';
                _path = pieces[1];
            }
            else {
                prefix = '';
                _path = pieces[0];
            }
            if (/^\.\//.test(_path) || /^\.\.\//.test(_path)) {
                var res = path.join(path.dirname(module), _path).replace(/\\/g, '/');
                return prefix + res;
            }
            return prefix + _path;
        });
        return {
            module: module,
            deps: deps
        };
    };
    destFiles.forEach(function (destFile, index) {
        if (!/\.js$/.test(destFile.dest)) {
            return;
        }
        if (/\.nls\.js$/.test(destFile.dest)) {
            return;
        }
        // Do one pass to record the usage counts for each module id
        var useCounts = {};
        destFile.sources.forEach(function (source) {
            var matches = source.contents.match(/define\(("[^"]+"),\s*\[(((, )?("|')[^"']+("|'))+)\]/);
            if (!matches) {
                return;
            }
            var defineCall = parseDefineCall(matches[1], matches[2]);
            useCounts[defineCall.module] = (useCounts[defineCall.module] || 0) + 1;
            defineCall.deps.forEach(function (dep) {
                useCounts[dep] = (useCounts[dep] || 0) + 1;
            });
        });
        var sortedByUseModules = Object.keys(useCounts);
        sortedByUseModules.sort(function (a, b) {
            return useCounts[b] - useCounts[a];
        });
        var replacementMap = {};
        sortedByUseModules.forEach(function (module, index) {
            replacementMap[module] = index;
        });
        destFile.sources.forEach(function (source) {
            source.contents = source.contents.replace(/define\(("[^"]+"),\s*\[(((, )?("|')[^"']+("|'))+)\]/, function (_, moduleMatch, depsMatch) {
                var defineCall = parseDefineCall(moduleMatch, depsMatch);
                return "define(__m[" + replacementMap[defineCall.module] + "/*" + defineCall.module + "*/], __M([" + defineCall.deps.map(function (dep) { return replacementMap[dep] + '/*' + dep + '*/'; }).join(',') + "])";
            });
        });
        destFile.sources.unshift({
            path: null,
            contents: [
                '(function() {',
                "var __m = " + JSON.stringify(sortedByUseModules) + ";",
                "var __M = function(deps) {",
                "  var result = [];",
                "  for (var i = 0, len = deps.length; i < len; i++) {",
                "    result[i] = __m[deps[i]];",
                "  }",
                "  return result;",
                "};"
            ].join('\n')
        });
        destFile.sources.push({
            path: null,
            contents: '}).call(this);'
        });
    });
    return destFiles;
}
function removeDuplicateTSBoilerplate(destFiles) {
    // Taken from typescript compiler => emitFiles
    var BOILERPLATE = [
        { start: /^var __extends/, end: /^}\)\(\);$/ },
        { start: /^var __assign/, end: /^};$/ },
        { start: /^var __decorate/, end: /^};$/ },
        { start: /^var __metadata/, end: /^};$/ },
        { start: /^var __param/, end: /^};$/ },
        { start: /^var __awaiter/, end: /^};$/ },
    ];
    destFiles.forEach(function (destFile) {
        var SEEN_BOILERPLATE = [];
        destFile.sources.forEach(function (source) {
            var lines = source.contents.split(/\r\n|\n|\r/);
            var newLines = [];
            var IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE;
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i];
                if (IS_REMOVING_BOILERPLATE) {
                    newLines.push('');
                    if (END_BOILERPLATE.test(line)) {
                        IS_REMOVING_BOILERPLATE = false;
                    }
                }
                else {
                    for (var j = 0; j < BOILERPLATE.length; j++) {
                        var boilerplate = BOILERPLATE[j];
                        if (boilerplate.start.test(line)) {
                            if (SEEN_BOILERPLATE[j]) {
                                IS_REMOVING_BOILERPLATE = true;
                                END_BOILERPLATE = boilerplate.end;
                            }
                            else {
                                SEEN_BOILERPLATE[j] = true;
                            }
                        }
                    }
                    if (IS_REMOVING_BOILERPLATE) {
                        newLines.push('');
                    }
                    else {
                        newLines.push(line);
                    }
                }
            }
            source.contents = newLines.join('\n');
        });
    });
    return destFiles;
}
function emitEntryPoint(modulesMap, deps, entryPoint, includedModules, prepend, append, dest) {
    if (!dest) {
        dest = entryPoint + '.js';
    }
    var mainResult = {
        sources: [],
        dest: dest
    }, results = [mainResult];
    var usedPlugins = {};
    var getLoaderPlugin = function (pluginName) {
        if (!usedPlugins[pluginName]) {
            usedPlugins[pluginName] = modulesMap[pluginName].exports;
        }
        return usedPlugins[pluginName];
    };
    includedModules.forEach(function (c) {
        var bangIndex = c.indexOf('!');
        if (bangIndex >= 0) {
            var pluginName = c.substr(0, bangIndex);
            var plugin = getLoaderPlugin(pluginName);
            mainResult.sources.push(emitPlugin(entryPoint, plugin, pluginName, c.substr(bangIndex + 1)));
            return;
        }
        var module = modulesMap[c];
        if (module.path === 'empty:') {
            return;
        }
        var contents = readFileAndRemoveBOM(module.path);
        if (module.shim) {
            mainResult.sources.push(emitShimmedModule(c, deps[c], module.shim, module.path, contents));
        }
        else {
            mainResult.sources.push(emitNamedModule(c, deps[c], module.defineLocation, module.path, contents));
        }
    });
    Object.keys(usedPlugins).forEach(function (pluginName) {
        var plugin = usedPlugins[pluginName];
        if (typeof plugin.writeFile === 'function') {
            var req = (function () {
                throw new Error('no-no!');
            });
            req.toUrl = function (something) { return something; };
            var write = function (filename, contents) {
                results.push({
                    dest: filename,
                    sources: [{
                            path: null,
                            contents: contents
                        }]
                });
            };
            plugin.writeFile(pluginName, entryPoint, req, write, {});
        }
    });
    var toIFile = function (path) {
        var contents = readFileAndRemoveBOM(path);
        return {
            path: path,
            contents: contents
        };
    };
    var toPrepend = (prepend || []).map(toIFile);
    var toAppend = (append || []).map(toIFile);
    mainResult.sources = toPrepend.concat(mainResult.sources).concat(toAppend);
    return {
        files: results,
        usedPlugins: usedPlugins
    };
}
function readFileAndRemoveBOM(path) {
    var BOM_CHAR_CODE = 65279;
    var contents = fs.readFileSync(path, 'utf8');
    // Remove BOM
    if (contents.charCodeAt(0) === BOM_CHAR_CODE) {
        contents = contents.substring(1);
    }
    return contents;
}
function emitPlugin(entryPoint, plugin, pluginName, moduleName) {
    var result = '';
    if (typeof plugin.write === 'function') {
        var write = (function (what) {
            result += what;
        });
        write.getEntryPoint = function () {
            return entryPoint;
        };
        write.asModule = function (moduleId, code) {
            code = code.replace(/^define\(/, 'define("' + moduleId + '",');
            result += code;
        };
        plugin.write(pluginName, moduleName, write);
    }
    return {
        path: null,
        contents: result
    };
}
function emitNamedModule(moduleId, myDeps, defineCallPosition, path, contents) {
    // `defineCallPosition` is the position in code: |define()
    var defineCallOffset = positionToOffset(contents, defineCallPosition.line, defineCallPosition.col);
    // `parensOffset` is the position in code: define|()
    var parensOffset = contents.indexOf('(', defineCallOffset);
    var insertStr = '"' + moduleId + '", ';
    return {
        path: path,
        contents: contents.substr(0, parensOffset + 1) + insertStr + contents.substr(parensOffset + 1)
    };
}
function emitShimmedModule(moduleId, myDeps, factory, path, contents) {
    var strDeps = (myDeps.length > 0 ? '"' + myDeps.join('", "') + '"' : '');
    var strDefine = 'define("' + moduleId + '", [' + strDeps + '], ' + factory + ');';
    return {
        path: path,
        contents: contents + '\n;\n' + strDefine
    };
}
/**
 * Convert a position (line:col) to (offset) in string `str`
 */
function positionToOffset(str, desiredLine, desiredCol) {
    if (desiredLine === 1) {
        return desiredCol - 1;
    }
    var line = 1, lastNewLineOffset = -1;
    do {
        if (desiredLine === line) {
            return lastNewLineOffset + 1 + desiredCol - 1;
        }
        lastNewLineOffset = str.indexOf('\n', lastNewLineOffset + 1);
        line++;
    } while (lastNewLineOffset >= 0);
    return -1;
}
/**
 * Return a set of reachable nodes in `graph` starting from `rootNodes`
 */
function visit(rootNodes, graph) {
    var result = {}, queue = rootNodes;
    rootNodes.forEach(function (node) {
        result[node] = true;
    });
    while (queue.length > 0) {
        var el = queue.shift();
        var myEdges = graph[el] || [];
        myEdges.forEach(function (toNode) {
            if (!result[toNode]) {
                result[toNode] = true;
                queue.push(toNode);
            }
        });
    }
    return result;
}
/**
 * Perform a topological sort on `graph`
 */
function topologicalSort(graph) {
    var allNodes = {}, outgoingEdgeCount = {}, inverseEdges = {};
    Object.keys(graph).forEach(function (fromNode) {
        allNodes[fromNode] = true;
        outgoingEdgeCount[fromNode] = graph[fromNode].length;
        graph[fromNode].forEach(function (toNode) {
            allNodes[toNode] = true;
            outgoingEdgeCount[toNode] = outgoingEdgeCount[toNode] || 0;
            inverseEdges[toNode] = inverseEdges[toNode] || [];
            inverseEdges[toNode].push(fromNode);
        });
    });
    // https://en.wikipedia.org/wiki/Topological_sorting
    var S = [], L = [];
    Object.keys(allNodes).forEach(function (node) {
        if (outgoingEdgeCount[node] === 0) {
            delete outgoingEdgeCount[node];
            S.push(node);
        }
    });
    while (S.length > 0) {
        // Ensure the exact same order all the time with the same inputs
        S.sort();
        var n = S.shift();
        L.push(n);
        var myInverseEdges = inverseEdges[n] || [];
        myInverseEdges.forEach(function (m) {
            outgoingEdgeCount[m]--;
            if (outgoingEdgeCount[m] === 0) {
                delete outgoingEdgeCount[m];
                S.push(m);
            }
        });
    }
    if (Object.keys(outgoingEdgeCount).length > 0) {
        throw new Error('Cannot do topological sort on cyclic graph, remaining nodes: ' + Object.keys(outgoingEdgeCount));
    }
    return L;
}
