/**
* ******************************************************
*                                                       *
*   Copyright (C) Microsoft. All rights reserved.       *
*                                                       *
* ******************************************************
*/
/// <reference path="../../typings/node.d.ts" />
/// <reference path="../../typings/Q.d.ts" />
/// <reference path="../../typings/vcremote-utils.d.ts" />
/// <reference path="../../typings/vcremote-lib.d.ts" />
/// <reference path="../../typings/express.d.ts" />
/// <reference path="../../typings/semver.d.ts" />
/// <reference path="../../typings/rimraf.d.ts" />
"use strict";
var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var utils = require("vcremote-utils");
var util = utils.UtilHelper;

var q = require("q");

var path = require("path");
var child_process = require("child_process");
var fs = require("fs");
var events = require("events");
var archiver = require("archiver");
var uuid = require("node-uuid");
var hash = require('object-hash');
var rimraf = require("rimraf");
var xmlbuilder = require("xmlbuilder");
var ioslib = require("ioslib");

var VCRemoteLib;
(function (VCRemoteLib) {
    (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";
    })(VCRemoteLib.BuildState || (VCRemoteLib.BuildState = {}));
    var BuildState = VCRemoteLib.BuildState;

    (function (BuildResult) {
        BuildResult[BuildResult["UNAVAILABLE"] = 0] = "UNAVAILABLE";
        BuildResult[BuildResult["SUCCEEDED"] = 1] = "SUCCEEDED";
        BuildResult[BuildResult["FAILED"] = 2] = "FAILED";
    })(VCRemoteLib.BuildResult || (VCRemoteLib.BuildResult = {}));
    var BuildResult = VCRemoteLib.BuildResult;

    var ProjectBuildManager = (function () {
        function ProjectBuildManager(prj) {
            this._project = prj;
            this._builds = {};
        }
        Object.defineProperty(ProjectBuildManager.prototype, "building", {
            get: function () {
                for (var id in this._builds) {
                    if (this._builds[id].building) {
                        return true;
                    }
                }
                return false;
            },
            enumerable: true,
            configurable: true
        });

        Object.defineProperty(ProjectBuildManager.prototype, "all", {
            get: function () {
                var builds = [];
                for (var id in this._builds) {
                    builds.push(this._builds[id]);
                }
                return builds;
            },
            enumerable: true,
            configurable: true
        });

        ProjectBuildManager.prototype.getBuildByCfgId = function (id) {
            for (var buildId in this._builds) {
                if (id === buildId) {
                    return this._builds[id];
                }
            }
            return undefined;
        };

        ProjectBuildManager.prototype.containsBuild = function (id) {
            return this.getBuildByCfgId(id) != undefined;
        };

        ProjectBuildManager.prototype.buildConfig = function (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.");
        };

        ProjectBuildManager.prototype.cleanConfig = function (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.");
        };

        ProjectBuildManager.prototype.rebuildConfig = function (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.");
        };

        ProjectBuildManager.prototype.stop = function () {
            for (var id in this._builds) {
                if (this._builds[id].building) {
                    this._builds[id].stop();
                }
            }
        };
        return ProjectBuildManager;
    })();
    VCRemoteLib.ProjectBuildManager = ProjectBuildManager;

    var BuildableConfig = (function (_super) {
        __extends(BuildableConfig, _super);
        function BuildableConfig(prj, config) {
            _super.call(this);
            this._state = 8 /* READY */;
            this._stateList = [];
            this._result = 0 /* UNAVAILABLE */;
            this._id = uuid.v4();
            this._project = prj;
            this._config = config;
            this.state = 8 /* READY */;
            this.state = 0 /* 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);
        }
        Object.defineProperty(BuildableConfig.prototype, "id", {
            get: function () {
                return this._id;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "project", {
            get: function () {
                return this._project;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "config", {
            get: function () {
                return this._config;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "commandLine", {
            get: function () {
                return this._commandLine;
            },
            set: function (cmdLine) {
                this._commandLine = cmdLine;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "outputDirectory", {
            get: function () {
                return this._outputDirectory;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "provisioningProfile", {
            get: function () {
                return this._provisioningProfile;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "pbxProject", {
            get: function () {
                return this._pbxProject;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "tempDirectory", {
            get: function () {
                return this._tempDirectory;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "archiveFileName", {
            get: function () {
                return this._archiveFileName;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "logFileName", {
            get: function () {
                return this._logFileName;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "building", {
            get: function () {
                return this._xcodebuild.running;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "state", {
            get: function () {
                return this._state;
            },
            set: function (state) {
                this._state = state;
                this._stateList.push(state);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "result", {
            get: function () {
                return this._result;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "history", {
            get: function () {
                return this._stateList;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildableConfig.prototype, "log", {
            get: function () {
                return this._log;
            },
            enumerable: true,
            configurable: true
        });

        BuildableConfig.prototype.start = function () {
            var self = this;
            if (this._xcodebuild.running) {
                this.state = 0 /* PENDING */;
                self.emit("error", new Error("A build is already running for this project and for this configuration."));
                return q({});
            }
            console.info("Build started for project: %s, project directory: %s", this._project.id, this._project.directory);
            this.state = 3 /* BUILDING */;
            this._log.clear();
            return this._xcodebuild.run("build").then(function () {
                self.state = 8 /* READY */;
                self._result = self._xcodebuild.exitCode == 0 ? 1 /* SUCCEEDED */ : 2 /* FAILED */;
                self.emit("done");
            }).fail(function (err) {
                self.state = 9 /* ERROR */;
                self._result = 0 /* UNAVAILABLE */;
                self.emit("error", new Error("An error occurred building the project: " + err));
            });
        };

        BuildableConfig.prototype.clean = function () {
            var self = this;
            if (this._xcodebuild.running) {
                this.state = 0 /* PENDING */;
                self._result = 0 /* UNAVAILABLE */;
                self.emit("error", new Error("A build is already running for this project and for this configuration."));
                return q({});
            }
            this.state = 4 /* CLEANING */;
            this._log.clear();
            return this._xcodebuild.run("clean").then(function () {
                self.state = 8 /* READY */;
                self._result = self._xcodebuild.exitCode == 0 ? 1 /* SUCCEEDED */ : 2 /* FAILED */;
                self.emit("done");
            }).fail(function (err) {
                self.state = 9 /* ERROR */;
                self._result = 0 /* UNAVAILABLE */;
                self.emit("error", new Error("An error occurred running xcodebuild for this project and configuration: " + err));
            });
        };

        BuildableConfig.prototype.stop = function () {
            var self = this;
            if (this._xcodebuild.running) {
                return this._xcodebuild.kill().then(function () {
                    self.emit("done");
                }).fail(function (err) {
                    self.emit("error", new Error("An error occurred stopping xcodebuild: " + err));
                });
            } else {
                return q({});
            }
        };

        BuildableConfig.prototype.delete = function () {
            if (!this.building) {
                return q.all([
                    q.nfcall(rimraf, this._outputDirectory),
                    q.nfcall(rimraf, this._tempDirectory)
                ]);
            } else {
                return q.reject(new Error("Cannot delete build, build is currently running."));
            }
        };

        BuildableConfig.prototype.archive = function () {
            var deferred = q.defer();
            var zipFile = fs.createWriteStream(this._archiveFileName);
            var archive = archiver("zip");
            var self = this;
            zipFile.on("close", function () {
                console.info("Archived build output directory to '%s'", 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;
        };
        return BuildableConfig;
    })(events.EventEmitter);
    VCRemoteLib.BuildableConfig = BuildableConfig;

    var BuildLog = (function () {
        function BuildLog(logFileName) {
            this._logFileName = logFileName;
        }
        Object.defineProperty(BuildLog.prototype, "logFileName", {
            get: function () {
                return this._logFileName;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(BuildLog.prototype, "size", {
            get: function () {
                return this._log.length;
            },
            enumerable: true,
            configurable: true
        });

        BuildLog.prototype.read = function (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);
        };

        BuildLog.prototype.append = function (value) {
            this._log += value;
        };

        BuildLog.prototype.clear = function () {
            this._log = "";
        };
        return BuildLog;
    })();
    VCRemoteLib.BuildLog = BuildLog;

    var XCodeBuildTask = (function () {
        function XCodeBuildTask(configBuild) {
            this._process = null;
            this._running = false;
            this._exitCode = 0;
            this._configBuild = configBuild;
        }
        Object.defineProperty(XCodeBuildTask.prototype, "running", {
            get: function () {
                return this._running;
            },
            enumerable: true,
            configurable: true
        });

        Object.defineProperty(XCodeBuildTask.prototype, "exitCode", {
            get: function () {
                return this._exitCode;
            },
            enumerable: true,
            configurable: true
        });

        XCodeBuildTask.prototype.run = function (buildAction) {
            var deferred = q.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("xcodebuild exited with code: %d", code);
            });
            this._process.on("error", function (err) {
                self._running = false;
                deferred.reject(err);
                console.info("An error occurred running xcodebuild: %o", 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;
        };

        XCodeBuildTask.prototype.kill = function () {
            if (this._running) {
                console.info("Attempting to stop xcodebuild");
                var deferred = q.defer();
                this._process.on("exit", deferred.resolve);
                this._process.on("error", deferred.reject);
                this._process.kill("SIGINT");
                return deferred.promise;
            } else {
                return Q({});
            }
        };

        XCodeBuildTask.prototype.makeCommandline = function (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);
            }
            return args;
        };
        return XCodeBuildTask;
    })();
    VCRemoteLib.XCodeBuildTask = XCodeBuildTask;

    var ProvisioningProfile = (function () {
        function ProvisioningProfile(type, name, uuid) {
            this.type = type;
            this.name = name;
            this.uuid = uuid;
        }
        return ProvisioningProfile;
    })();
    VCRemoteLib.ProvisioningProfile = ProvisioningProfile;

    var ProvisioningProfiles = (function () {
        function ProvisioningProfiles() {
        }
        Object.defineProperty(ProvisioningProfiles.prototype, "provisioningProfiles", {
            get: function () {
                var deferred = q.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;
            },
            enumerable: true,
            configurable: true
        });
        return ProvisioningProfiles;
    })();
    VCRemoteLib.ProvisioningProfiles = ProvisioningProfiles;
})(VCRemoteLib || (VCRemoteLib = {}));

module.exports = VCRemoteLib;
