#!/usr/bin/env node
/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

var fs = require("fs");
var path = require("path");
var copy = require('architect-build/copy');
var build = require('architect-build/build');

var ACE_HOME = __dirname;
var BUILD_DIR = ACE_HOME + "/build";
var CACHE = {};

function main(args) {
    if (args.indexOf("updateModes") !== -1) {
        return updateModes();
    }
    var type = "minimal";
    args = args.map(function(x) {
        if (x[0] == "-" && x[1] != "-")
            return "-" + x;
        return x;
    }).filter(Boolean);

    if (args[2] && (args[2][0] != "-" || args[2].indexOf("h") != -1))
        type = args[2];

    var i = args.indexOf("--target");
    if (i != -1 && args[i+1])
        BUILD_DIR = args[i+1];

    if (args.indexOf("--h") != -1 || args.indexOf("-h") != -1 || args.indexOf("--help") != -1) {
        return showHelp();
    }
    if (type == "minimal") {
        buildAce({
            compress: args.indexOf("--m") != -1,
            noconflict: args.indexOf("--nc") != -1,
            shrinkwrap: args.indexOf("--s") != -1
        });
    } else if (type == "normal") {
        ace();
    } else if (type == "demo") {
        demo();
    } else if (type == "full") {
        ace();
        demo();
    } else if (type == "highlighter") {
        // TODO
    }
}

function showHelp(type) {
    console.log("--- Ace Dryice Build Tool ---");
    console.log("");
    console.log("Options:");
    console.log("  minimal      Places necessary Ace files out in build dir; uses configuration flags below [default]");
    console.log("  normal       Runs four Ace builds--minimal, minimal-noconflict, minimal-min, and minimal-noconflict-min");
    console.log("  demo         Runs demo build of Ace");
    console.log("  full         all of above");
    console.log("  highlighter  ");
    console.log("args:");
    console.log("  --target ./path   path to build folder");
    console.log("flags:");
    console.log("  --h                print this help");
    console.log("  --m                minify");
    console.log("  --nc               namespace require");
    console.log("  --s                shrinkwrap (combines all output files into one)");
    console.log("");
    if (type)
        console.log("  output for " + type + " generated in " + BUILD_DIR);
}

function ace() {
    console.log('# ace License | Readme | Changelog ---------');

    copy.file(ACE_HOME + "/build_support/editor.html",  BUILD_DIR + "/editor.html");
    copy.file(ACE_HOME + "/LICENSE", BUILD_DIR + "/LICENSE");
    copy.file(ACE_HOME + "/ChangeLog.txt", BUILD_DIR + "/ChangeLog.txt");
    
    console.log('# ace ---------');
    for (var i = 0; i < 4; i++) {
        buildAce({compress: i & 2, noconflict: i & 1, check: true});
    }
}

function buildTypes() {
    copy.file(ACE_HOME + "/ace.d.ts", BUILD_DIR + "/ace.d.ts");

    var paths = fs.readdirSync(BUILD_DIR + '/src-noconflict');

    var pathModules = paths.map(function(path) {
        if (/^(mode|theme|ext|keybinding)-/.test(path)) {
            var moduleName = path.split('.')[0];
            return "declare module 'ace-builds/src-noconflict/" + moduleName + "';";
        }
    }).filter(Boolean).join('\n')
        + "\ndeclare module 'ace-builds/webpack-resolver';\n";

    fs.appendFileSync(BUILD_DIR + '/ace.d.ts', '\n' + pathModules);
    
    var loader = paths.map(function(path) {
        if (/\.js$/.test(path) && !/^ace\.js$/.test(path)) {
            var moduleName = path.split('.')[0].replace(/-/, "/");
            if (/^worker/.test(moduleName))
                moduleName = "mode" + moduleName.slice(6) + "_worker";
            moduleName = moduleName.replace(/keybinding/, "keyboard");
            return "ace.config.setModuleUrl('ace/" + moduleName + "', require('file-loader!./src-noconflict/" + path + "'))";
        }
    }).join('\n');
    
    fs.writeFileSync(BUILD_DIR + '/webpack-resolver.js', loader, "utf8");
}

function demo() {
    console.log('# kitchen sink ---------');

    var version = "", ref = "";
    try {
        version = JSON.parse(fs.readFileSync(ACE_HOME + "/package.json")).version;
        ref = fs.readFileSync(ACE_HOME + "/.git-ref").toString();
    } catch(e) {}

    function changeComments(data) {
        return (data
            .replace("doc/site/images/ace-logo.png", "demo/kitchen-sink/ace-logo.png")
            .replace(/<!\-\-DEVEL[\d\D]*?DEVEL\-\->/g, "")
            .replace(/PACKAGE\-\->|<!\-\-PACKAGE/g, "")
            .replace(/\/\*DEVEL[\d\D]*?DEVEL\*\//g, "")
            .replace(/PACKAGE\*\/|\/\*PACKAGE/g, "")
            .replace("%version%", version)
            .replace("%commit%", ref)
        );
    }
    
    copy(ACE_HOME +"/demo/kitchen-sink/docs/", BUILD_DIR + "/demo/kitchen-sink/docs/");
    
    copy.file(ACE_HOME + "/demo/kitchen-sink/logo.png", BUILD_DIR + "/demo/kitchen-sink/logo.png");
    copy.file(ACE_HOME + "/demo/kitchen-sink/styles.css", BUILD_DIR + "/demo/kitchen-sink/styles.css");
    copy.file(ACE_HOME + "/kitchen-sink.html", BUILD_DIR + "/kitchen-sink.html", changeComments);

    buildSubmodule({}, {
        require: ["kitchen-sink/demo"],
        projectType: "demo"
    }, BUILD_DIR + "/demo/kitchen-sink/demo");

    copy(ACE_HOME + "/demo/", BUILD_DIR + "/demo/", {
        shallow: true,
        exclude: /\s|requirejs/,
        include: /\.(js|html)$/,
        replace: function(source) {
            if (!/^\s*</.test(source))
                return source;
            var removeRequireJS;
            source = source.replace(/<script src="kitchen-sink\/require.js"[\s\S]+?require\(\[([^\]]+).*/, function(e, m) {
                removeRequireJS = true;
                var scripts = m.split(/,\s*/);
                var result = [];
                function comment(str) {result.push("<!-- " + str + " -->")}
                function script(str) {result.push('<script src="../src/' + str + '.js"></script>')}
                scripts.forEach(function(s) {
                    s = s.replace(/"/g, "");
                    if (s == "ace/ace") {
                        comment("load ace");
                        script("ace");
                    } else {
                        var extName = s.match(/[^/]*$/)[0];
                        comment("load ace " + extName + " extension");
                        script("ext-" + extName);
                    }
                });
                result.push("<script>");
                return result.join("\n");
            });
            if (removeRequireJS)
                source = source.replace(/\s*\}\);?\s*(<\/script>)/, "\n$1");
            source = source.replace(/"\.\.\/build\//g, function(e) {
                console.log(e); return '"../';
            });
            return source;
        }
    });
}

function jsFileList(path, filter) {
    path = ACE_HOME + "/" + path;
    if (!filter)
        filter = /_test/;

    return fs.readdirSync(path).map(function(x) {
        if (x.slice(-3) == ".js" && !filter.test(x) && !/\s|BASE|(\b|_)dummy(\b|_)/.test(x))
            return x.slice(0, -3);
    }).filter(Boolean);
}

function workers(path) {
    return jsFileList(path).map(function(x) {
        if (x.slice(-7) == "_worker")
            return x.slice(0, -7);
    }).filter(function(x) { return !!x; });
}

function modeList() {
    return jsFileList("lib/ace/mode", /_highlight_rules|_test|_worker|xml_util|_outdent|behaviour|completions/);
}

function buildAceModule(opts, callback) {
    // calling buildAceModuleInternal many times in parallel is slow, so we use queue
    if (!buildAceModule.queue) {
        buildAceModule.queue = [];
        buildAceModule.dequeue = function() {
            if (buildAceModule.running) return;
            var call = buildAceModule.queue.shift();
            buildAceModule.running = call;
            if (call)
                buildAceModuleInternal.apply(null, call);
        };
    }
    
    buildAceModule.queue.push([opts, function(err, result) {
        callback && callback(err, result);
        buildAceModule.running = null;
        buildAceModule.dequeue();
    }]);

    if (!buildAceModule.running) {
        buildAceModule.dequeue();
    } else {
        process.nextTick(buildAceModule.dequeue);
    }
}

function buildAceModuleInternal(opts, callback) {
    var cache = opts.cache == undefined ? CACHE : opts.cache;
    var key = opts.require + "|" + opts.projectType;
    if (cache && cache.configs && cache.configs[key])
        return write(null, cache.configs[key]);
        
    var pathConfig = {
        paths: {
            ace: ACE_HOME + "/lib/ace",
            "kitchen-sink": ACE_HOME + "/demo/kitchen-sink",
            build_support:  ACE_HOME + "/build_support"
        },
        root: ACE_HOME
    };
        
    function write(err, result) {
        if (cache && key && !(cache.configs && cache.configs[key])) {
            cache.configs = cache.configs || Object.create(null);
            cache.configs[key] = result;
            result.sources = result.sources.map(function(pkg) {
                return {deps: pkg.deps};
            });
        } 
        
        if (!opts.outputFile)
            return callback(err, result);
        
        var code = result.code;
        if (opts.compress) {
            if (!result.codeMin)
                result.codeMin = compress(result.code);
            code = result.codeMin;
        }
            
        var targetDir = getTargetDir(opts);
        
        var to = /^([\\/]|\w:)/.test(opts.outputFile)
            ? opts.outputFile
            : path.join(opts.outputFolder || targetDir, opts.outputFile);
    
        var filters = [];

        var ns = opts.ns || "ace";
        if (opts.filters)
            filters = filters.concat(opts.filters);
    
        if (opts.noconflict)
            filters.push(namespace(ns));
        var projectType = opts.projectType;
        if (projectType !== "worker") {
            filters.push(exportAce(ns, opts.require[0],
                opts.noconflict ? ns : "", projectType !== "main"));
        }
        
        filters.push(normalizeLineEndings);
        
        filters.forEach(function(f) { code = f(code); });
        
        build.writeToFile({code: code}, {
            outputFolder: path.dirname(to),
            outputFile: path.basename(to)
        }, function() {});
        
        callback && callback(err, result);
    }
    
    build(opts.require, {
        cache: cache,
        quiet: opts.quiet,
        pathConfig: pathConfig,
        additional: opts.additional,
        enableBrowser: true,
        keepDepArrays: opts.noconflict ? "" : "all",
        noArchitect: true,
        compress: false,
        ignore: opts.ignore || [],
        withRequire: false,
        basepath: ACE_HOME,
        transforms: [normalizeLineEndings],
        afterRead: [optimizeTextModules]
    }, write);
}

function buildCore(options, extra, callback) {
    options = extend(extra, options);
    options.additional = [{
        id: "build_support/mini_require", 
        order: -1000,
        literal: true
    }];
    options.require =["ace/ace"];
    options.projectType = "main";
    options.ns = "ace";
    buildAceModule(options, callback);
}

function buildSubmodule(options, extra, file, callback) {
    options = extend(extra, options);
    getLoadedFileList(options, function(coreFiles) {
        options.outputFile = file + ".js";
        options.ignore = options.ignore || coreFiles;
        options.quiet = true;
        buildAceModule(options, callback);
    });
}

function buildAce(options, callback) {
    var snippetFiles = jsFileList("lib/ace/snippets");
    var modeNames = modeList();

    buildCore(options, {outputFile: "ace.js"}, addCb());
    // modes
    modeNames.forEach(function(name) {
        buildSubmodule(options, {
            projectType: "mode",
            require: ["ace/mode/" + name]
        }, "mode-" + name, addCb());
    });
    // snippets
    modeNames.forEach(function(name) {
        if (snippetFiles.indexOf(name + ".js") == -1)
            addSnippetFile(name);
        
        buildSubmodule(options, {
            require: ["ace/snippets/" + name]
        }, "snippets/" + name, addCb());
    });
    // themes
    jsFileList("lib/ace/theme").forEach(function(name) {
        buildSubmodule(options, {
            projectType: "theme",
            require: ["ace/theme/" + name]
        }, "theme-" +  name, addCb());
    });
    // keybindings
    ["vim", "emacs"].forEach(function(name) {
        buildSubmodule(options, {
            projectType: "keybinding",
            require: ["ace/keyboard/" + name ]
        }, "keybinding-" + name, addCb());
    });
    // extensions
    jsFileList("lib/ace/ext").forEach(function(name) {
        buildSubmodule(options, {
            projectType: "ext",
            require: ["ace/ext/" + name]
        }, "ext-" + name, addCb());
    });
    // workers
    workers("lib/ace/mode").forEach(function(name) {
        buildSubmodule(options, {
            projectType: "worker",
            require: ["ace/mode/" + name + "_worker"],
            ignore: [],
            additional: [{
                id: "ace/worker/worker",
                transforms: [],
                order: -1000
            }]
        }, "worker-" + name, addCb());
    });
    // 
    function addCb() {
        addCb.count = (addCb.count || 0) + 1; 
        return done
    }
    function done() {
        if (--addCb.count > 0)
            return;
        if (options.check)
            sanityCheck(options, callback);
        if (options.noconflict && !options.compress)
            buildTypes();
            
        if (callback) 
            return callback();
        console.log("Finished building " + getTargetDir(options))
    }
}

function getLoadedFileList(options, callback, result) {
    if (!result) {
        return buildCore({}, {}, function(e, result) {
            getLoadedFileList(options, callback, result);
        });
    }
    var deps = Object.create(null);
    result.sources.forEach(function(pkg) {
        pkg.deps && pkg.deps.forEach(function(p) {
            if (!deps[p]) deps[p] = 1;
        });
    });
    delete deps["ace/theme/textmate"];
    deps["ace/ace"] = 1;
    callback(Object.keys(deps));
}

function normalizeLineEndings(module) {
    if (typeof module == "string") 
        module = {source: module};
    return module.source = module.source.replace(/\r\n/g, "\n");
}

function optimizeTextModules(sources) {
    var textModules = {};
    return sources.filter(function(pkg) {
        if (!pkg.id) {
            return true;
        }
        else if (pkg.id.indexOf("text!") > -1) {
            detectTextModules(pkg);
            return false;
        }
        else {
            pkg.source = rewriteTextImports(pkg.source, pkg.deps);
            return true;
        }
    }).map(function(pkg) {
        if (pkg && pkg.deps) {
            pkg.deps = pkg.deps && pkg.deps.filter(function(p) {
                return p.indexOf("text!") == -1;
            });
        }
        return pkg;
    });
    
    function rewriteTextImports(text, deps) {
        return text.replace(/ require\(['"](?:ace|[.\/]+)\/requirejs\/text!(.*?)['"]\)/g, function(_, call) {
            if (call) {
                var dep;
                deps.some(function(d) {
                    if (d.split("/").pop() == call.split("/").pop()) {
                        dep = d;
                        return true;
                    }
                });
    
                call = textModules[dep];
                if (call)
                    return " " + call;
            }
        });
    }
    function detectTextModules(pkg) {
        var input = pkg.source.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
        if (/\.css$/.test(pkg.id)) {
            // remove unnecessary whitespace from css
            input = input.replace(/\n\s+/g, "\n");
            input = '"' + input.replace(/\n/g, '\\\n') + '"';
        } else {
            // but don't break other files!
            input = '"' + input.replace(/\n/g, '\\n\\\n') + '"';
        }
        textModules[pkg.id] = input;
    }
}

function namespace(ns) {
    return function(text) {
        text = text
            .toString()
            .replace(/ACE_NAMESPACE\s*=\s*""/, 'ACE_NAMESPACE = "' + ns +'"')
            .replace(/\bdefine\(/g, function(def, index, source) {
                if (/(^|[;}),])\s*$/.test(source.slice(0, index)))
                    return ns + "." + def;
                return def;
            });

        return text;
    };
}

function exportAce(ns, modules, requireBase, extModules) {
    requireBase = requireBase || "window";
    return function(text) {
        /*globals REQUIRE_NS, MODULES, NS*/
        var template = function() {
            (function() {
                REQUIRE_NS.require(MODULES, function(a) {
                    if (a) {
                        a.config.init(true);
                        a.define = REQUIRE_NS.define;
                    }
                    if (!window.NS)
                        window.NS = a;
                    for (var key in a) if (a.hasOwnProperty(key))
                        window.NS[key] = a[key];
                    window.NS["default"] = window.NS;
                    if (typeof module == "object" && typeof exports == "object" && module) {
                        module.exports = window.NS;
                    }
                });
            })();
        };
        
        if (extModules) {
            template = function() {
                (function() {
                    REQUIRE_NS.require(MODULES, function(m) {
                        if (typeof module == "object" && typeof exports == "object" && module) {
                            module.exports = m;
                        }
                    });
                })();
            };
        }
        
        text = text.replace(/function init\(packaged\) {/, "init(true);$&\n");
        
        if (typeof modules == "string")
            modules = [modules];
            
        return (text.replace(/;\s*$/, "") + ";" + template
            .toString()
            .replace(/MODULES/g, JSON.stringify(modules))
            .replace(/REQUIRE_NS/g, requireBase)
            .replace(/NS/g, ns)
            .slice(13, -1)
        );
    };
}

function updateModes() {
    modeList().forEach(function(m) {
        var filepath = __dirname + "/lib/ace/mode/" + m + ".js";
        var source = fs.readFileSync(filepath, "utf8");
        if (!/this.\$id\s*=\s*"/.test(source))
            source = source.replace(/\n([ \t]*)(\}\).call\(\w*Mode.prototype\))/, '\n$1    this.$id = "";\n$1$2');
        
        source = source.replace(/(this.\$id\s*=\s*)"[^"]*"/,  '$1"ace/mode/' + m + '"');
        fs.writeFileSync(filepath, source, "utf8");
    });
}

function generateThemesModule(themes) {
    var themelist = [
        'define(function(require, exports, module) {',
        '\n\nmodule.exports.themes = ' + JSON.stringify(themes, null, '    '),
        ';\n\n});'
    ].join('');
    fs.writeFileSync(__dirname + '/lib/ace/ext/themelist_utils/themes.js', themelist, 'utf8');
}

function addSnippetFile(modeName) {
    var snippetFilePath = ACE_HOME + "/lib/ace/snippets/" + modeName;
    if (!fs.existsSync(snippetFilePath + ".js")) {
        copy.file(ACE_HOME + "/tool/templates/snippets.js", snippetFilePath + ".js", function(t) {
            return t.replace(/%modeName%/g, modeName);
        });
    }
    if (!fs.existsSync(snippetFilePath + ".snippets")) {
        fs.writeFileSync(snippetFilePath + ".snippets", "");
    }
}

function compress(text) {
    var uglify = require("dryice").copy.filter.uglifyjs;
    uglify.options.mangle_toplevel = {except: ["ACE_NAMESPACE", "requirejs"]};
    uglify.options.beautify = {ascii_only: true, inline_script: true}
    return asciify(uglify(text));
    // copy.filter.uglifyjs.options.ascii_only = true; doesn't work with some uglify.js versions
    function asciify(text) {
        return text.replace(/[\x00-\x08\x0b\x0c\x0e\x19\x80-\uffff]/g, function(c) {
            c = c.charCodeAt(0).toString(16);
            if (c.length == 1)
                return "\\x0" + c;
            if (c.length == 2)
                return "\\x" + c;
            if (c.length == 3)
                return "\\u0" + c;
            return "\\u" + c;
        });
    }
}

function extend(base, extra) {
    Object.keys(extra).forEach(function(k) {
        base[k] = extra[k];
    });
    return base;
}

function getTargetDir(opts) {
    var targetDir = BUILD_DIR + "/src";
    if (opts.compress)
        targetDir += "-min";
    if (opts.noconflict)
        targetDir += "-noconflict";
    return targetDir;
}

function sanityCheck(opts, callback) {
    var targetDir = getTargetDir(opts);
    require("child_process").execFile(process.execPath, ["-e", "(" + function() {
        window = global;
        require("./ace");
        if (typeof ace.edit != "function")
            process.exit(1);
        require("fs").readdirSync(".").forEach(function(p) {
            if (!/ace\.js$/.test(p) && /\.js$/.test(p))
                require("./" + p);
        });
        process.exit(0);
    } + ")()"], {
        cwd: targetDir
    }, function(err, stdout) {
        if (callback) return callback(err, stdout);
        if (err)
            throw err;
    });
}

if (!module.parent)
    main(process.argv);
else
    exports.buildAce = buildAce;
