/**
  * ******************************************************
  *                                                       *
  *   Copyright (C) Microsoft. All rights reserved.       *
  *                                                       *
  * ******************************************************
  */
/// <reference path="../customTypings/vcremote-utils.d.ts" />
"use strict";
const utils = require("vcremote-utils");
var util = utils.UtilHelper;
const path = require("path");
const child_process = require("child_process");
const fs = require("fs");
const events = require("events");
var spawnSync = require("child_process").spawnSync;
var archiver = require("archiver");
var uuid = require("uuid");
const rimraf = require("rimraf");
var ioslib = require("ioslib");
const nconf = require("nconf");
var resources = utils.ResourcesManager;
var VCRemoteLib;
(function (VCRemoteLib) {
    // Will use /localization directory with translated strings if exists in npm installation, otherwise use default english strings.
    var pathToLocalizedStrings = path.join(__dirname + "../../../../", "localization/app/localization/i18n");
    if (fs.existsSync(pathToLocalizedStrings)) {
        resources.init(nconf.get("lang"), pathToLocalizedStrings);
    }
    else {
        resources.init(nconf.get("lang"), path.join(__dirname, "./resources"));
    }
    Promise.defer = function () {
        var result = {};
        result.promise = new Promise(function (resolve, reject) {
            result.resolve = resolve;
            result.reject = reject;
        });
        return result;
    };
    Promise.all = function (promises) {
        return new Promise(function (success, fail) {
            var successArr = new Array(promises.length);
            if (promises.length == 0)
                success(successArr);
            var pending = promises.length;
            promises.forEach(function (promise, i) {
                promise.then(function (result) {
                    successArr[i] = result;
                    pending -= 1;
                    if (pending == 0)
                        success(successArr);
                }, function (error) {
                    fail(error);
                });
            });
        });
    };
    let BuildState;
    (function (BuildState) {
        BuildState[BuildState["PENDING"] = 0] = "PENDING";
        BuildState[BuildState["DOWNLOADING"] = 1] = "DOWNLOADING";
        BuildState[BuildState["DOWNLOADED"] = 2] = "DOWNLOADED";
        BuildState[BuildState["BUILDING"] = 3] = "BUILDING";
        BuildState[BuildState["CLEANING"] = 4] = "CLEANING";
        BuildState[BuildState["DEBUGGING"] = 5] = "DEBUGGING";
        BuildState[BuildState["RUNNING"] = 6] = "RUNNING";
        BuildState[BuildState["INSTALLED"] = 7] = "INSTALLED";
        BuildState[BuildState["READY"] = 8] = "READY";
        BuildState[BuildState["ERROR"] = 9] = "ERROR";
    })(BuildState = VCRemoteLib.BuildState || (VCRemoteLib.BuildState = {})); // status
    let BuildResult;
    (function (BuildResult) {
        BuildResult[BuildResult["UNAVAILABLE"] = 0] = "UNAVAILABLE";
        BuildResult[BuildResult["SUCCEEDED"] = 1] = "SUCCEEDED";
        BuildResult[BuildResult["FAILED"] = 2] = "FAILED";
    })(BuildResult = VCRemoteLib.BuildResult || (VCRemoteLib.BuildResult = {}));
    class ProjectBuildManager {
        get building() {
            for (var id in this._builds) {
                if (this._builds[id].building) {
                    return true;
                }
            }
            return false;
        }
        get all() {
            var builds = [];
            for (var id in this._builds) {
                builds.push(this._builds[id]);
            }
            return builds;
        }
        constructor(prj) {
            this._project = prj;
            this._builds = {};
        }
        getBuildByCfgId(id) {
            for (var buildId in this._builds) {
                if (id === buildId) {
                    return this._builds[id];
                }
            }
            return undefined;
        }
        containsBuild(id) {
            return this.getBuildByCfgId(id) != undefined;
        }
        buildConfig(config) {
            if (!this.building) {
                var build = new BuildableConfig(this._project, config);
                this._builds[config.id] = build;
                build.start();
                return build;
            }
            throw new Error("Cannot start a build because the project is currently building.");
        }
        cleanConfig(config) {
            if (!this.building) {
                var build = new BuildableConfig(this._project, config);
                this._builds[config.id] = build;
                build.clean();
                return build;
            }
            throw new Error("Cannot clean the build because the project is currently building.");
        }
        rebuildConfig(config) {
            if (!this.building) {
                var build = new BuildableConfig(this._project, config);
                this._builds[config.id] = build;
                build.clean().then(build.start);
                return build;
            }
            throw new Error("Cannot rebuild the project because the project is currently building.");
        }
        stop() {
            for (var id in this._builds) {
                if (this._builds[id].building) {
                    this._builds[id].stop();
                }
            }
        }
    }
    VCRemoteLib.ProjectBuildManager = ProjectBuildManager;
    class BuildableConfig extends events.EventEmitter {
        get id() {
            return this._id;
        }
        get project() {
            return this._project;
        }
        get config() {
            return this._config;
        }
        get commandLine() {
            return this._commandLine;
        }
        set commandLine(cmdLine) {
            this._commandLine = cmdLine;
        }
        get outputDirectory() {
            return this._outputDirectory;
        }
        get provisioningProfile() {
            return this._provisioningProfile;
        }
        get pbxProject() {
            return this._pbxProject;
        }
        get tempDirectory() {
            return this._tempDirectory;
        }
        get archiveFileName() {
            return this._archiveFileName;
        }
        get logFileName() {
            return this._logFileName;
        }
        get building() {
            return this._xcodebuild.running;
        }
        get state() {
            return this._state;
        }
        set state(state) {
            this._state = state;
            this._stateList.push(state);
        }
        get result() {
            return this._result;
        }
        get history() {
            return this._stateList;
        }
        get log() {
            return this._log;
        }
        constructor(prj, config) {
            super();
            this._state = BuildState.READY;
            this._stateList = [];
            this._result = BuildResult.UNAVAILABLE;
            this._id = uuid.v4();
            this._project = prj;
            this._config = config;
            this.state = BuildState.READY;
            this.state = BuildState.PENDING;
            // NOTE: should not peek into IBuildEnvParams as that's solely for
            // configuring the xcodebuild build and passing env to the build system
            // it should be entirely opaque inside here.
            this._outputDirectory = config.outputDirectory;
            this._provisioningProfile = config.provisioningProfile;
            this._pbxProject = config.pbxProject;
            this._tempDirectory = path.join(config.projectDirectory, "tmp", config.config, config.arch);
            // create those directories here as clean/build et all needs them
            // the directories are mostly needed for writting the log as xcodebuild will create them
            util.createDirectoryIfNecessary(this._outputDirectory);
            util.createDirectoryIfNecessary(this._tempDirectory);
            this._archiveFileName = path.join(this._tempDirectory, "build.zip");
            this._logFileName = path.join(this._outputDirectory, "buildlog.log");
            this._log = new BuildLog(this._logFileName);
            this._xcodebuild = new XCodeBuildTask(this);
        }
        start() {
            var self = this;
            if (this._xcodebuild.running) {
                this.state = BuildState.PENDING;
                self.emit("error", new Error("A build is already running for this project and for this configuration."));
                return new Promise(function (resolve, reject) { resolve({}); });
            }
            console.info(resources.getStringForLanguage(nconf.get("lang"), "BuildStarted", this._project.id, this._project.directory));
            this.state = BuildState.BUILDING;
            this._log.clear();
            return this._xcodebuild.run("build")
                .then(function () {
                self.state = BuildState.READY;
                self._result = self._xcodebuild.exitCode == 0 ? BuildResult.SUCCEEDED : BuildResult.FAILED;
                self.emit("done");
            })
                .catch(function (err) {
                self.state = BuildState.ERROR;
                self._result = BuildResult.UNAVAILABLE;
                self.emit("error", new Error("An error occurred building the project: " + err));
            });
        }
        clean() {
            var self = this;
            if (this._xcodebuild.running) {
                this.state = BuildState.PENDING;
                self._result = BuildResult.UNAVAILABLE;
                self.emit("error", new Error("A build is already running for this project and for this configuration."));
                return new Promise(function (resolve, reject) { resolve({}); });
            }
            this.state = BuildState.CLEANING;
            this._log.clear();
            return this._xcodebuild.run("clean")
                .then(function () {
                self.state = BuildState.READY;
                self._result = self._xcodebuild.exitCode == 0 ? BuildResult.SUCCEEDED : BuildResult.FAILED;
                self.emit("done");
            })
                .catch(function (err) {
                self.state = BuildState.ERROR;
                self._result = BuildResult.UNAVAILABLE;
                self.emit("error", new Error("An error occurred running xcodebuild for this project and configuration: " + err));
            });
        }
        stop() {
            var self = this;
            if (this._xcodebuild.running) {
                return this._xcodebuild.kill().then(function () {
                    self.emit("done");
                }).catch(function (err) {
                    self.emit("error", new Error("An error occurred stopping xcodebuild: " + err));
                });
            }
            else {
                return new Promise(function (resolve, reject) { resolve({}); });
            }
        }
        delete() {
            if (!this.building) {
                return Promise.all([
                    new Promise(function (resolve, reject) { rimraf(this._outputDirectory, () => { }); }),
                    new Promise(function (resolve, reject) { rimraf(this._tempDirectory, () => { }); })
                ]);
            }
            else {
                return new Promise(function (resolve, reject) { reject(new Error("Cannot delete build, build is currently running.")); });
            }
        }
        archive() {
            var deferred = new Promise.defer;
            var zipFile = fs.createWriteStream(this._archiveFileName);
            var archive = archiver("zip");
            var self = this;
            zipFile.on("close", function () {
                console.info(resources.getStringForLanguage(nconf.get("lang"), "ArchiveBuildOutputDir", self._archiveFileName));
                deferred.resolve(self._archiveFileName);
            });
            archive.on("error", deferred.reject);
            archive.pipe(zipFile);
            archive.bulk([
                { expand: true, cwd: this._outputDirectory, src: ["**"] }
            ]);
            archive.finalize();
            return deferred.promise;
        }
    }
    VCRemoteLib.BuildableConfig = BuildableConfig;
    class BuildLog {
        get logFileName() {
            return this._logFileName;
        }
        get size() {
            return this._log.length;
        }
        constructor(logFileName) {
            this._logFileName = logFileName;
        }
        read(pos, length) {
            // the string extractions functions in JavaScript have funky behavior with
            // negative inputs so avoid that
            pos = Math.abs(pos);
            if (length == 0) {
                length = undefined;
            }
            else {
                length = Math.abs(length);
            }
            // do not error out in case of mistakes, just return empty
            if (pos >= this._log.length) {
                return "";
            }
            if (pos + length >= this._log.length) {
                return "";
            }
            return this._log.substr(pos, length);
        }
        append(value) {
            this._log += value;
        }
        clear() {
            this._log = "";
        }
    }
    VCRemoteLib.BuildLog = BuildLog;
    class XCodeBuildTask {
        get running() {
            return this._running;
        }
        get exitCode() {
            return this._exitCode;
        }
        constructor(configBuild) {
            this._process = null;
            this._running = false;
            this._exitCode = 0;
            this._configBuild = configBuild;
        }
        run(buildAction) {
            var deferred = new Promise.defer;
            var args = this.makeCommandline(buildAction);
            var self = this;
            // if log file exists delete
            if (fs.existsSync(this._configBuild.logFileName)) {
                fs.unlinkSync(this._configBuild.logFileName);
            }
            var logStream = fs.createWriteStream(this._configBuild.logFileName, { encoding: 'utf8', flags: 'a' });
            this._running = true;
            // run xcodebuild and set the working directory to the project directory
            // expect one single .xcodeproj to be found in the project directory
            this._process = child_process.spawn("xcodebuild", args, { cwd: this._configBuild.config.projectDirectory });
            this._process.stdout.pipe(logStream);
            // redirect the standard error to the log to catch bizzare errors like exit code 65
            this._process.stderr.pipe(logStream);
            this._process.on("exit", function (code) {
                logStream.close();
                self._running = false;
                self._exitCode = code;
                deferred.resolve(code);
                console.info(resources.getStringForLanguage(nconf.get("lang"), "XcodebuildExitCode", code.toString()));
            });
            this._process.on("error", function (err) {
                self._running = false;
                deferred.reject(err);
                console.info(resources.getStringForLanguage(nconf.get("lang"), "ErrorRunningXcodebuild", err));
            });
            this._process.stdout.on("data", function (d) {
                self._configBuild.log.append(d.toString("utf8", 0, d.length));
            });
            this._process.stderr.on("data", function (d) {
                self._configBuild.log.append(d.toString("utf8", 0, d.length));
            });
            return deferred.promise;
        }
        kill() {
            if (this._running) {
                console.info(resources.getStringForLanguage(nconf.get("lang"), "StoppingXcodebuild"));
                var deferred = new Promise.defer;
                this._process.on("exit", deferred.resolve);
                this._process.on("error", deferred.reject);
                this._process.kill("SIGINT");
                return deferred.promise;
            }
            else {
                return new Promise(function (resolve, reject) { resolve({}); });
            }
        }
        getXcodebuildVersion() {
            var version = 0;
            var process = spawnSync("xcodebuild", ["-version"]);
            var output = process.stdout.toString("utf8", 0, process.stdout.length);
            var re = /(\d+(\.\d+)?)/;
            var m = re.exec(output);
            if (m && m.length > 0) {
                version = Number(m[0]);
            }
            console.info(resources.getStringForLanguage(nconf.get("lang"), "XcodebuildVersion", version.toString()));
            return version;
        }
        makeCommandline(buildAction) {
            var args = [];
            args.push(buildAction);
            if (this._configBuild.config.config && this._configBuild.config.config.length > 0) {
                args.push("-configuration");
                args.push(this._configBuild.config.config);
            }
            if (this._configBuild.config.sdk && this._configBuild.config.sdk.length > 0) {
                args.push("-sdk");
                args.push(this._configBuild.config.sdk);
            }
            if (this._configBuild.config.arch && this._configBuild.config.arch.length > 0) {
                args.push("-arch");
                args.push(this._configBuild.config.arch);
            }
            if (this._configBuild.config.pbxProject && this._configBuild.config.pbxProject.length > 0) {
                // add the .xcodeproj to build in case there is more than one
                args.push("-project");
                args.push(this._configBuild.config.pbxProject);
            }
            // the output directory for the build
            args.push("CONFIGURATION_BUILD_DIR=" + this._configBuild.outputDirectory);
            // the temp directory for the build
            args.push("CONFIGURATION_TEMP_DIR=" + this._configBuild.tempDirectory);
            // user selected provisioning profile (if any) for build
            if (this._configBuild.config.provisioningProfile !== undefined &&
                this._configBuild.config.provisioningProfile !== "" &&
                this._configBuild.config.provisioningProfile.toLowerCase() !== "default") {
                args.push("PROVISIONING_PROFILE=" + this._configBuild.config.provisioningProfile);
            }
            args.push("-allowProvisioningUpdates");
            // use legacy build system for xcodebuild versions 11+ so that rebuild works
            if (this.getXcodebuildVersion() >= 11) {
                args.push("-UseModernBuildSystem=NO");
                console.info(resources.getStringForLanguage(nconf.get("lang"), "UsingXcodebuildLegacy"));
            }
            return args;
        }
    }
    VCRemoteLib.XCodeBuildTask = XCodeBuildTask;
    class ProvisioningProfile {
        constructor(type, name, uuid) {
            this.type = type;
            this.name = name;
            this.uuid = uuid;
        }
    }
    VCRemoteLib.ProvisioningProfile = ProvisioningProfile;
    class ProvisioningProfiles {
        get provisioningProfiles() {
            var deferred = new Promise.defer;
            try {
                ioslib.provisioning.detect(function (err, profiles) {
                    if (err) {
                        deferred.reject(err);
                    }
                    else {
                        var _provisioning_profiles = [];
                        var push_back = function (pp, type) {
                            _provisioning_profiles.push(new ProvisioningProfile(type, pp.name, pp.uuid));
                        };
                        profiles.provisioning.development.forEach(function (pp) { push_back(pp, "development"); });
                        profiles.provisioning.distribution.forEach(function (pp) { push_back(pp, "distribution"); });
                        profiles.provisioning.adhoc.forEach(function (pp) { push_back(pp, "adhoc"); });
                        deferred.resolve(_provisioning_profiles);
                    }
                });
            }
            catch (e) {
                deferred.reject(e);
            }
            return deferred.promise;
        }
    }
    VCRemoteLib.ProvisioningProfiles = ProvisioningProfiles;
})(VCRemoteLib || (VCRemoteLib = {}));
module.exports = VCRemoteLib;
