var Promise = require("bluebird");
var path = require("path");
var jobRunner = require("./job-runner/job-runner.js");
Promise.longStackTraces();
var utils = require("./utils.js");
var glob = Promise.promisify(require("glob"));
var fs = Promise.promisifyAll(require("fs"));
var mkdirp = Promise.promisify(require("mkdirp"));
var rimraf = Promise.promisify(require("rimraf"));

jobRunner.init(path.join(__dirname, ".."), function() {
    var fs = Promise.promisifyAll(require("fs"));
    var utils = require("./tools/utils.js");
    var path = require("path");
    var astPasses = require("./tools/ast_passes.js");
    var Mocha = require("mocha");
    astPasses.readConstants(
        fs.readFileSync("./src/constants.js", "utf8"),
        "constants.js"
    );
    function applyOptionalRequires(code, depsRequireCode) {
        return code.replace( /};([^}]*)$/, depsRequireCode + "\n};$1");
    }
});

var optionalModuleRequireMap = {
    "race.js": true,
    "call_get.js": true,
    "generators.js": true,
    "map.js": true,
    "nodeify.js": true,
    "promisify.js": true,
    "props.js": true,
    "reduce.js": true,
    "settle.js": true,
    "some.js": true,
    "using.js": true,
    "timers.js": true,
    "filter.js": ["map.js"],
    "any.js": ["some.js"],
    "each.js": ["reduce.js"]
};

var lastLineCode = "                                                         \n\
    util.toFastProperties(Promise);                                          \n\
    util.toFastProperties(Promise.prototype);                                \n\
    function fillTypes(value) {                                              \n\
        var p = new Promise(INTERNAL);                                       \n\
        p._fulfillmentHandler0 = value;                                      \n\
        p._rejectionHandler0 = value;                                        \n\
        p._promise0 = value;                                                 \n\
        p._receiver0 = value;                                                \n\
    }                                                                        \n\
    // Complete slack tracking, opt out of field-type tracking and           \n\
    // stabilize map                                                         \n\
    fillTypes({a: 1});                                                       \n\
    fillTypes({b: 2});                                                       \n\
    fillTypes({c: 3});                                                       \n\
    fillTypes(1);                                                            \n\
    fillTypes(function(){});                                                 \n\
    fillTypes(undefined);                                                    \n\
    fillTypes(false);                                                        \n\
    fillTypes(new Promise(INTERNAL));                                        \n\
    debug.setBounds(Async.firstLineError, util.lastLineError);               \n\
    return Promise;                                                          \n\
";

function getOptionalRequireCode(srcs) {
    return srcs.sort(function(a, b) {
        var aHasDeps = Array.isArray(optionalModuleRequireMap[a.sourceFileName]);
        var bHasDeps = Array.isArray(optionalModuleRequireMap[b.sourceFileName]);
        return aHasDeps - bHasDeps;
    }).reduce(function(ret, cur, i) {
        if(optionalModuleRequireMap[cur.sourceFileName]) {
            ret += "require('./"+cur.sourceFileName+"')("+ cur.deps.join(", ") +");\n";
        }
        return ret;
    }, "") +  lastLineCode;
}

function getBrowserBuildHeader(sources, npmPackage) {
    var header = "/**\n * bluebird build version " + npmPackage.version + "\n";
    var enabledFeatures = ["core"];
    var disabledFeatures = [];
    featureLoop: for (var key in optionalModuleRequireMap) {
        for (var i = 0, len = sources.length; i < len; ++i) {
            var source = sources[i];
            if(source.sourceFileName === key) {
                enabledFeatures.push(key.replace( ".js", ""));
                continue featureLoop;
            }
        }
        disabledFeatures.push(key.replace(".js", ""));
    }

    header += (" * Features enabled: " + enabledFeatures.join(", ") + "\n");

    if (disabledFeatures.length) {
        header += " * Features disabled: " + disabledFeatures.join(", ") + "\n";
    }
    header += "*/\n";
    return header;
}

function getSourcePaths(features) {
    return glob("./src/*.js").map(function(v) {
        return path.basename(v);
    }).then(function(results) {
        if (features) features = features.toLowerCase().split(/\s+/g);
        return results.filter(function(fileName) {
            if (features && optionalModuleRequireMap[fileName] !== undefined) {
                for (var i = 0; i < features.length; ++i) {
                    if (fileName.indexOf(features[i]) >= 0) {
                        return true;
                    }
                }
                return false;
            }
            return fileName !== "constants.js";
        });
    });
}

function ensureDirectory(dir, isUsed, clean) {
    return (clean ? rimraf(dir) : Promise.resolve()).then(function() {
        if (!isUsed) return dir;
        return mkdirp(dir).thenReturn(dir);
    });
}

function buildRelease(sources, depsRequireCode, dir) {
    return dir.then(function(dir) {
        return Promise.map(sources, function(source) {
            return jobRunner.run(function() {
                var code = source.source;
                var sourceFileName = source.sourceFileName;
                code = astPasses.removeAsserts(code, sourceFileName);
                code = astPasses.inlineExpansion(code, sourceFileName);
                code = astPasses.expandConstants(code, sourceFileName);
                code = code.replace( /__DEBUG__/g, "false" );
                code = code.replace( /__BROWSER__/g, "false" );
                if (sourceFileName === "promise.js") {
                    code = applyOptionalRequires(code, depsRequireCode);
                }
                return fs.writeFileAsync(path.join(root, sourceFileName), code);
            }, {
                context: {
                    depsRequireCode: depsRequireCode,
                    source: source,
                    root: dir
                }
            });
        });
    });
}

function buildDebug(sources, depsRequireCode, dir) {
    return dir.then(function(dir) {
        return Promise.map(sources, function(source) {
            return jobRunner.run(function() {
                var code = source.source;
                var sourceFileName = source.sourceFileName;
                code = astPasses.expandAsserts(code, sourceFileName);
                code = astPasses.inlineExpansion(code, sourceFileName);
                code = astPasses.expandConstants(code, sourceFileName);
                code = code.replace( /__DEBUG__/g, "true" );
                code = code.replace( /__BROWSER__/g, "false" );
                if (sourceFileName === "promise.js") {
                    code = applyOptionalRequires(code, depsRequireCode);
                }
                return fs.writeFileAsync(path.join(root, sourceFileName), code);
            }, {
                context: {
                    depsRequireCode: depsRequireCode,
                    source: source,
                    root: dir
                }
            });
        });
    });
}

function buildBrowser(sources, dir, tmpDir, depsRequireCode, minify, npmPackage, license) {
    return Promise.join(dir, tmpDir, npmPackage, license, function(dir, tmpDir, npmPackage, license) {
        return Promise.map(sources, function(source) {
            return jobRunner.run(function() {
                var code = source.source;
                var sourceFileName = source.sourceFileName;
                code = astPasses.removeAsserts(code, sourceFileName);
                code = astPasses.inlineExpansion(code, sourceFileName, true);
                code = astPasses.expandConstants(code, sourceFileName);
                code = code.replace( /__BROWSER__/g, "true" );
                if (sourceFileName === "promise.js") {
                    code = applyOptionalRequires(code, depsRequireCode);
                }
                return fs.writeFileAsync(path.join(root, sourceFileName), code);
            }, {
                context: {
                    depsRequireCode: depsRequireCode,
                    source: source,
                    root: tmpDir
                }
            });
        }).then(function() {
            var header = getBrowserBuildHeader(sources, npmPackage);
            return jobRunner.run(function() {
                var UglifyJS = require("uglify-js");
                var browserify = require("browserify");
                var dest = path.join(root, "bluebird.js");
                var minDest = path.join(root, "bluebird.min.js");
                var b = browserify({
                    entries: entries,
                    detectGlobals: false,
                    standalone: "Promise"
                });
                return Promise.promisify(b.bundle, b)().then(function(src) {
                    var alias = "\
                    ;if (typeof window !== 'undefined' && window !== null) {       \
                        window.P = window.Promise;                                 \
                    } else if (typeof self !== 'undefined' && self !== null) {     \
                        self.P = self.Promise;                                     \
                    }";
                    src = src + alias;
                    src = src.replace(/\brequire\b/g, "_dereq_");
                    var minWrite, write;
                    if (minify) {
                        var minSrc = src.replace( /__DEBUG__/g, "false" );
                        minSrc = UglifyJS.minify(minSrc, {
                            comments: false,
                            compress: true,
                            fromString: true
                        }).code;
                        minSrc  = license + header + minSrc;
                        minWrite = fs.writeFileAsync(minDest, minSrc);
                    }
                    src = src.replace( /__DEBUG__/g, "true" );
                    src = license + header + src;
                    write = fs.writeFileAsync(dest, src);

                    return Promise.all([write, minWrite]);
                })
            }, {
                context: {
                    header: header,
                    root: dir,
                    entries: path.join(tmpDir, "bluebird.js"),
                    license: license,
                    minify: minify
                }
            })
        });
    });
}

var root = process.cwd();
// Since rm -rf is called, better be sure...
if (path.basename(root).toLowerCase().indexOf("bluebird") !== 0) {
    throw new Error("cwd must be set to bluebird project root. cwd is currently\n\n" +
        "         " + process.cwd() + "\n");
}
var dirs = {
    release: path.join(root, "js", "release"),
    debug: path.join(root, "js", "debug"),
    browser: path.join(root, "js", "browser"),
    browserTmp: path.join(root, "js", "tmp"),
    instrumented: path.join(root, "js", "instrumented"),
    coverage: path.join(root, "coverage")
};

function build(options) {
    options = Object(options);
    var clean = (typeof options.clean !== "boolean" ? true : options.clean);
    var npmPackage = fs.readFileAsync("./package.json").then(JSON.parse);
    var version = npmPackage.get("version");
    var sourceFileNames = getSourcePaths(options.features);
    var license = utils.getLicense();
    var releaseDir = ensureDirectory(dirs.release, options.release, clean);
    var debugDir = ensureDirectory(dirs.debug, options.debug, clean);
    var browserDir = ensureDirectory(dirs.browser, options.browser, clean);
    var browserTmpDir = ensureDirectory(dirs.browserTmp, options.browser, clean);
    return Promise.join(license, version, function(license, version) {
        return sourceFileNames.map(function(sourceFileName) {
            return jobRunner.run(function() {
                var sourcePath = path.join("./src", sourceFileName);
                var source = fs.readFileAsync(sourcePath, "utf8");
                return source.then(function(source) {
                    utils.checkAscii(sourceFileName, source);
                    var deps = null;
                    if (optionalModuleRequireMap[sourceFileName] !== undefined) {
                        deps = utils.parseDeps(source);
                    }
                    source = astPasses.removeComments(source, sourceFileName);
                    source = source.replace(/__VERSION__/g, version);
                    return {
                        sourceFileName: sourceFileName,
                        source: source,
                        deps: deps
                    };
                });
            }, {
                context: {
                    sourceFileName: sourceFileName,
                    optionalModuleRequireMap: optionalModuleRequireMap,
                    license: license,
                    version: version
                }
            });
        });
    }).then(function(results) {
        var depsRequireCode = getOptionalRequireCode(results);
        var release, debug, browser;
        if (options.release)
            release = buildRelease(results, depsRequireCode, releaseDir);
        if (options.debug)
            debug = buildDebug(results, depsRequireCode, debugDir);
        if (options.browser)
            browser = buildBrowser(results, browserDir, browserTmpDir, depsRequireCode, options.minify, npmPackage, license);

        return Promise.all([release, debug, browser]);
    });
}

module.exports = build;
module.exports.ensureDirectory = ensureDirectory;
module.exports.dirs = dirs;


if (require.main === module) {
    var argv = require("optimist").argv;
    var browser = (typeof argv.browser !== "boolean" ? false : argv.browser) || !!argv.features;
    var clean = (typeof argv.clean !== "boolean" ? true : argv.clean);
    module.exports({
        minify: browser && (typeof argv.minify !== "boolean" ? true : argv.minify),
        browser: browser,
        debug: !!argv.debug,
        release: !!argv.release,
        features: argv.features || null,
        clean: clean
    });
}
