/// <reference path="../typings/globals/node/index.d.ts"/>
/// <reference path="../built/pxtlib.d.ts"/>
/// <reference path="../built/pxtsim.d.ts"/>
"use strict";
global.pxt = pxt;
var nodeutil = require('./nodeutil');
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
var hid = require('./hid');
var U = pxt.Util;
function noopAsync() { return Promise.resolve(); }
exports.buildEngines = {
    yotta: {
        updateEngineAsync: function () { return runYottaAsync(["update"]); },
        buildAsync: function () { return runYottaAsync(["build"]); },
        setPlatformAsync: function () {
            return runYottaAsync(["target", pxt.appTarget.compileService.yottaTarget]);
        },
        patchHexInfo: patchYottaHexInfo,
        prepBuildDirAsync: noopAsync,
        buildPath: "built/yt",
        moduleConfig: "module.json",
        deployAsync: msdDeployCoreAsync,
        appPath: "source"
    },
    platformio: {
        updateEngineAsync: noopAsync,
        buildAsync: function () { return runPlatformioAsync(["run"]); },
        setPlatformAsync: noopAsync,
        patchHexInfo: patchPioHexInfo,
        prepBuildDirAsync: noopAsync,
        buildPath: "built/pio",
        moduleConfig: "platformio.ini",
        deployAsync: platformioDeployAsync,
        appPath: "src"
    },
    codal: {
        updateEngineAsync: updateCodalBuildAsync,
        buildAsync: function () { return runBuildCmdAsync("python", "build.py"); },
        setPlatformAsync: noopAsync,
        patchHexInfo: patchCodalHexInfo,
        prepBuildDirAsync: prepCodalBuildDirAsync,
        buildPath: "built/codal",
        moduleConfig: "codal.json",
        deployAsync: msdDeployCoreAsync,
        appPath: "pxtapp"
    },
    dockermake: {
        updateEngineAsync: function () { return runBuildCmdAsync("npm", "install"); },
        buildAsync: function () { return runDockerAsync(["make"]); },
        setPlatformAsync: noopAsync,
        patchHexInfo: patchDockermakeHexInfo,
        prepBuildDirAsync: noopAsync,
        buildPath: "built/dockermake",
        moduleConfig: "package.json",
        deployAsync: msdDeployCoreAsync,
        appPath: "pxtapp"
    },
};
// once we have a different build engine, set this appropriately
exports.thisBuild = exports.buildEngines['yotta'];
function patchYottaHexInfo(extInfo) {
    var buildEngine = exports.buildEngines['yotta'];
    var hexPath = buildEngine.buildPath + "/build/" + pxt.appTarget.compileService.yottaTarget
        + "/source/" + pxt.appTarget.compileService.yottaBinary;
    return {
        hex: fs.readFileSync(hexPath, "utf8").split(/\r?\n/)
    };
}
function patchCodalHexInfo(extInfo) {
    var hexPath = exports.thisBuild.buildPath + "/build/" + pxt.appTarget.compileService.codalBinary + ".hex";
    return {
        hex: fs.readFileSync(hexPath, "utf8").split(/\r?\n/)
    };
}
function patchDockermakeHexInfo(extInfo) {
    var hexPath = exports.thisBuild.buildPath + "/bld/pxt-app.hex";
    return {
        hex: fs.readFileSync(hexPath, "utf8").split(/\r?\n/)
    };
}
function pioFirmwareHex() {
    var buildEngine = exports.buildEngines['platformio'];
    return buildEngine.buildPath + "/.pioenvs/myenv/firmware.hex";
}
function patchPioHexInfo(extInfo) {
    return {
        hex: fs.readFileSync(pioFirmwareHex(), "utf8").split(/\r?\n/)
    };
}
function platformioDeployAsync(r) {
    if (pxt.appTarget.compile.useUF2)
        return msdDeployCoreAsync(r);
    else
        return platformioUploadAsync(r);
}
function platformioUploadAsync(r) {
    // TODO maybe platformio has some option to do this?
    var buildEngine = exports.buildEngines['platformio'];
    var prevHex = fs.readFileSync(pioFirmwareHex());
    fs.writeFileSync(pioFirmwareHex(), r.outfiles[pxtc.BINARY_HEX]);
    return runPlatformioAsync(["run", "--target", "upload", "--target", "nobuild", "-v"])
        .finally(function () {
        pxt.log('Restoring ' + pioFirmwareHex());
        fs.writeFileSync(pioFirmwareHex(), prevHex);
    });
}
function buildHexAsync(buildEngine, mainPkg, extInfo) {
    var tasks = Promise.resolve();
    var buildCachePath = buildEngine.buildPath + "/buildcache.json";
    var buildCache = {};
    if (fs.existsSync(buildCachePath)) {
        buildCache = nodeutil.readJson(buildCachePath);
    }
    if (buildCache.sha == extInfo.sha) {
        pxt.debug("Skipping C++ build.");
        return tasks;
    }
    pxt.debug("writing build files to " + buildEngine.buildPath);
    var allFiles = U.clone(extInfo.generatedFiles);
    U.jsonCopyFrom(allFiles, extInfo.extensionFiles);
    var writeFiles = function () {
        for (var _i = 0, _a = nodeutil.allFiles(buildEngine.buildPath + "/" + buildEngine.appPath, 8, true); _i < _a.length; _i++) {
            var f = _a[_i];
            var bn = f.slice(buildEngine.buildPath.length);
            bn = bn.replace(/\\/g, "/").replace(/^\//, "/");
            if (U.startsWith(bn, "/" + buildEngine.appPath + "/") && !allFiles[bn]) {
                pxt.log("removing stale " + bn);
                fs.unlinkSync(f);
            }
        }
        U.iterMap(allFiles, function (fn, v) {
            fn = buildEngine.buildPath + fn;
            nodeutil.mkdirP(path.dirname(fn));
            var existing = null;
            if (fs.existsSync(fn))
                existing = fs.readFileSync(fn, "utf8");
            if (existing !== v)
                fs.writeFileSync(fn, v);
        });
    };
    tasks = tasks
        .then(buildEngine.prepBuildDirAsync)
        .then(writeFiles);
    var saveCache = function () { return fs.writeFileSync(buildCachePath, JSON.stringify(buildCache, null, 4) + "\n"); };
    var modSha = U.sha256(extInfo.generatedFiles["/" + buildEngine.moduleConfig]);
    var needDal = false;
    if (buildCache.modSha !== modSha) {
        tasks = tasks
            .then(buildEngine.setPlatformAsync)
            .then(buildEngine.updateEngineAsync)
            .then(function () {
            buildCache.sha = "";
            buildCache.modSha = modSha;
            saveCache();
            needDal = true;
        });
    }
    else {
        pxt.debug("Skipping C++ build update.");
    }
    tasks = tasks
        .then(buildEngine.buildAsync)
        .then(function () {
        buildCache.sha = extInfo.sha;
        saveCache();
        if (needDal)
            buildDalConst(buildEngine, mainPkg, true);
    });
    return tasks;
}
exports.buildHexAsync = buildHexAsync;
function runYottaAsync(args) {
    var ypath = process.env["YOTTA_PATH"];
    var ytCommand = "yotta";
    var env = U.clone(process.env);
    if (/;[A-Z]:\\/.test(ypath)) {
        for (var _i = 0, _a = ypath.split(";"); _i < _a.length; _i++) {
            var pp = _a[_i];
            var q = path.join(pp, "yotta.exe");
            if (fs.existsSync(q)) {
                ytCommand = q;
                env["PATH"] = env["PATH"] + ";" + ypath;
                break;
            }
        }
    }
    pxt.log("*** " + ytCommand + " " + args.join(" "));
    var child = child_process.spawn("yotta", args, {
        cwd: exports.thisBuild.buildPath,
        stdio: "inherit",
        env: env
    });
    return new Promise(function (resolve, reject) {
        child.on("close", function (code) {
            if (code === 0)
                resolve();
            else
                reject(new Error("yotta " + args.join(" ") + ": exit code " + code));
        });
    });
}
function runPlatformioAsync(args) {
    pxt.log("*** platformio " + args.join(" "));
    var child = child_process.spawn("platformio", args, {
        cwd: exports.thisBuild.buildPath,
        stdio: "inherit",
        env: process.env
    });
    return new Promise(function (resolve, reject) {
        child.on("close", function (code) {
            if (code === 0)
                resolve();
            else
                reject(new Error("platformio " + args.join(" ") + ": exit code " + code));
        });
    });
}
function runDockerAsync(args) {
    var fullpath = process.cwd() + "/" + exports.thisBuild.buildPath + "/";
    var cs = pxt.appTarget.compileService;
    return nodeutil.spawnAsync({
        cmd: "docker",
        args: ["run", "--rm", "-v", fullpath + ":/src", "-w", "/src", "-u", "build",
            cs.dockerImage].concat(args),
        cwd: exports.thisBuild.buildPath
    });
}
var parseCppInt = pxt.cpp.parseCppInt;
function codalGitAsync() {
    var args = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        args[_i - 0] = arguments[_i];
    }
    return nodeutil.spawnAsync({
        cmd: "git",
        args: args,
        cwd: exports.thisBuild.buildPath
    });
}
exports.codalGitAsync = codalGitAsync;
function prepCodalBuildDirAsync() {
    if (fs.existsSync(exports.thisBuild.buildPath + "/.git/config"))
        return Promise.resolve();
    var cs = pxt.appTarget.compileService;
    var pkg = "https://github.com/" + cs.githubCorePackage;
    nodeutil.mkdirP("built");
    return nodeutil.runGitAsync("clone", pkg, exports.thisBuild.buildPath)
        .then(function () { return codalGitAsync("checkout", cs.gittag); });
}
function runBuildCmdAsync(cmd) {
    var args = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        args[_i - 1] = arguments[_i];
    }
    return nodeutil.spawnAsync({
        cmd: cmd,
        args: args,
        cwd: exports.thisBuild.buildPath,
    });
}
function updateCodalBuildAsync() {
    var cs = pxt.appTarget.compileService;
    return codalGitAsync("checkout", cs.gittag)
        .then(function () { }, function (e) { })
        .then(function () { return codalGitAsync("pull"); })
        .then(function () { return codalGitAsync("checkout", cs.gittag); });
}
// TODO: DAL specific code should be lifted out
function buildDalConst(buildEngine, mainPkg, force) {
    if (force === void 0) { force = false; }
    var constName = "dal.d.ts";
    var vals = {};
    var done = {};
    function isValidInt(v) {
        return /^-?(\d+|0[xX][0-9a-fA-F]+)$/.test(v);
    }
    function extractConstants(fileName, src, dogenerate) {
        if (dogenerate === void 0) { dogenerate = false; }
        var lineNo = 0;
        // let err = (s: string) => U.userError(`${fileName}(${lineNo}): ${s}\n`)
        var outp = "";
        var inEnum = false;
        var enumVal = 0;
        var defineVal = function (n, v) {
            v = v.trim();
            if (parseCppInt(v) != null) {
                var curr = U.lookup(vals, n);
                if (curr == null || curr == v) {
                    vals[n] = v;
                    if (dogenerate && !done[n]) {
                        outp += "    " + n + " = " + v + ",\n";
                        done[n] = v;
                    }
                }
                else {
                    vals[n] = "?";
                    // TODO: DAL-specific code
                    if (dogenerate && !/^MICROBIT_DISPLAY_(ROW|COLUMN)_COUNT$/.test(n))
                        pxt.log(fileName + "(" + lineNo + "): #define conflict, " + n);
                }
            }
            else {
                vals[n] = "?"; // just in case there's another more valid entry
            }
        };
        src.split(/\r?\n/).forEach(function (ln) {
            ++lineNo;
            ln = ln.replace(/\/\/.*/, "").replace(/\/\*.*/g, "");
            var m = /^\s*#define\s+(\w+)\s+(.*)$/.exec(ln);
            if (m) {
                defineVal(m[1], m[2]);
            }
            if (inEnum && /}/.test(ln))
                inEnum = false;
            if (/^\s*enum\s+(\w+)/.test(ln)) {
                inEnum = true;
                enumVal = -1;
            }
            if (inEnum && (m = /^\s*(\w+)\s*(=\s*(.*?))?,?\s*$/.exec(ln))) {
                var v = m[3];
                if (v) {
                    enumVal = parseCppInt(v);
                    if (enumVal == null) {
                        pxt.log(fileName + "(" + lineNo + "): invalid enum initializer, " + ln);
                        inEnum = false;
                        return;
                    }
                }
                else {
                    enumVal++;
                    v = enumVal + "";
                }
                defineVal(m[1], v);
            }
        });
        return outp;
    }
    if (mainPkg && mainPkg.getFiles().indexOf(constName) >= 0 &&
        (force || !fs.existsSync(constName))) {
        pxt.log("rebuilding " + constName + "...");
        // TODO: DAL-specific code
        var incPath = buildEngine.buildPath + "/yotta_modules/microbit-dal/inc/";
        if (!fs.existsSync(incPath))
            incPath = buildEngine.buildPath + "/yotta_modules/codal/inc/";
        if (!fs.existsSync(incPath))
            incPath = buildEngine.buildPath;
        if (!fs.existsSync(incPath))
            U.userError("cannot find " + incPath);
        var files = nodeutil.allFiles(incPath, 20).filter(function (fn) { return U.endsWith(fn, ".h"); });
        files.sort(U.strcmp);
        var fc = {};
        for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
            var fn = files_1[_i];
            if (U.endsWith(fn, "Config.h"))
                continue;
            if (fn.indexOf("/mbed-classic/") >= 0)
                continue;
            fc[fn] = fs.readFileSync(fn, "utf8");
        }
        files = Object.keys(fc);
        // pre-pass - detect conflicts
        for (var _a = 0, files_2 = files; _a < files_2.length; _a++) {
            var fn = files_2[_a];
            extractConstants(fn, fc[fn]);
        }
        var consts = "// Auto-generated. Do not edit.\ndeclare const enum DAL {\n";
        for (var _b = 0, files_3 = files; _b < files_3.length; _b++) {
            var fn = files_3[_b];
            consts += "    // " + fn.replace(/\\/g, "/") + "\n";
            consts += extractConstants(fn, fc[fn], true);
        }
        consts += "}\n";
        fs.writeFileSync(constName, consts);
    }
}
exports.buildDalConst = buildDalConst;
var writeFileAsync = Promise.promisify(fs.writeFile);
var execAsync = Promise.promisify(child_process.exec);
var readDirAsync = Promise.promisify(fs.readdir);
function msdDeployCoreAsync(res) {
    var firmware = pxt.outputName();
    var encoding = pxt.isOutputText() ? "utf8" : "base64";
    if (pxt.appTarget.serial && pxt.appTarget.serial.useHF2) {
        var f = res.outfiles[pxtc.BINARY_UF2];
        var blocks_1 = pxtc.UF2.parseFile(U.stringToUint8Array(atob(f)));
        return hid.initAsync()
            .then(function (dev) { return dev.flashAsync(blocks_1); });
    }
    return getBoardDrivesAsync()
        .then(function (drives) { return filterDrives(drives); })
        .then(function (drives) {
        if (drives.length == 0) {
            pxt.log("cannot find any drives to deploy to");
            return Promise.resolve(0);
        }
        pxt.log(("copying " + firmware + " to ") + drives.join(", "));
        var writeHexFile = function (filename) {
            return writeFileAsync(path.join(filename, firmware), res.outfiles[firmware], encoding)
                .then(function () { return pxt.log("   wrote hex file to " + filename); });
        };
        return Promise.map(drives, function (d) { return writeHexFile(d); })
            .then(function () { return drives.length; });
    }).then(function () { });
}
function getBoardDrivesAsync() {
    if (process.platform == "win32") {
        var rx_1 = new RegExp("^([A-Z]:)\\s+(\\d+).* " + pxt.appTarget.compile.deployDrives);
        return execAsync("wmic PATH Win32_LogicalDisk get DeviceID, VolumeName, FileSystem, DriveType")
            .then(function (buf) {
            var res = [];
            buf.toString("utf8").split(/\n/).forEach(function (ln) {
                var m = rx_1.exec(ln);
                if (m && m[2] == "2") {
                    res.push(m[1] + "/");
                }
            });
            return res;
        });
    }
    else if (process.platform == "darwin") {
        var rx_2 = new RegExp(pxt.appTarget.compile.deployDrives);
        return readDirAsync("/Volumes")
            .then(function (lst) { return lst.filter(function (s) { return rx_2.test(s); }).map(function (s) { return "/Volumes/" + s + "/"; }); });
    }
    else if (process.platform == "linux") {
        var rx_3 = new RegExp(pxt.appTarget.compile.deployDrives);
        var user_1 = process.env["USER"];
        return readDirAsync("/media/" + user_1)
            .then(function (lst) { return lst.filter(function (s) { return rx_3.test(s); }).map(function (s) { return ("/media/" + user_1 + "/" + s + "/"); }); });
    }
    else {
        return Promise.resolve([]);
    }
}
function filterDrives(drives) {
    var marker = pxt.appTarget.compile.deployFileMarker;
    if (!marker)
        return drives;
    return drives.filter(function (d) {
        try {
            return fs.existsSync(path.join(d, marker));
        }
        catch (e) {
            return false;
        }
    });
}
