/**
* ******************************************************
*                                                       *
*   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/fstream.d.ts" />
/// <reference path="../../typings/vcremote-utils.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 build = require("./build");

var fs = require("fs");
var tmp = require("tmp");
var unzip = require("unzip");
var fstream = require("fstream");
var uuid = require("node-uuid");
var xcode = require("xcode");
var _ = require("lodash");
var spawnSync = require("spawn-sync");
var from = require("fromjs");
var rimraf = require("rimraf");
var path = require("path");
var events = require("events");

var VCRemoteLib;
(function (VCRemoteLib) {
    (function (ProjectState) {
        ProjectState[ProjectState["UPLOADING"] = 0] = "UPLOADING";
        ProjectState[ProjectState["UPLOADED"] = 1] = "UPLOADED";
        ProjectState[ProjectState["EXTRACTING"] = 2] = "EXTRACTING";
        ProjectState[ProjectState["EXTRACTED"] = 3] = "EXTRACTED";
        ProjectState[ProjectState["CLEANING"] = 4] = "CLEANING";
        ProjectState[ProjectState["UPDATING"] = 5] = "UPDATING";
        ProjectState[ProjectState["UPDATED"] = 6] = "UPDATED";
        ProjectState[ProjectState["PENDING"] = 7] = "PENDING";
        ProjectState[ProjectState["ERROR"] = 8] = "ERROR";
        ProjectState[ProjectState["READY"] = 9] = "READY";
    })(VCRemoteLib.ProjectState || (VCRemoteLib.ProjectState = {}));
    var ProjectState = VCRemoteLib.ProjectState;

    (function (KeepPolicy) {
        KeepPolicy[KeepPolicy["KEEP_FOREVER"] = 0] = "KEEP_FOREVER";
        KeepPolicy[KeepPolicy["KEEP_UNTIL_EXPIRED"] = 1] = "KEEP_UNTIL_EXPIRED";
        KeepPolicy[KeepPolicy["TEMPORARY"] = 2] = "TEMPORARY";
    })(VCRemoteLib.KeepPolicy || (VCRemoteLib.KeepPolicy = {}));
    var KeepPolicy = VCRemoteLib.KeepPolicy;

    var SDK = (function () {
        function SDK(name) {
            this._name = "";
            this._platformName = "";
            this._version = 0;
            this._name = name;
            this.parseSdkPlatform();
            this.parseSdkVersion();
        }
        Object.defineProperty(SDK.prototype, "platformName", {
            get: function () {
                return this._platformName;
            },
            enumerable: true,
            configurable: true
        });

        Object.defineProperty(SDK.prototype, "version", {
            get: function () {
                return this._version;
            },
            enumerable: true,
            configurable: true
        });

        Object.defineProperty(SDK.prototype, "name", {
            get: function () {
                return this._name;
            },
            enumerable: true,
            configurable: true
        });

        SDK.prototype.parseSdkPlatform = function () {
            var platformName = "";
            if (this._name.indexOf("macosx") != -1) {
                this._platformName = "macosx";
            } else if (this._name.indexOf("iphone") != -1) {
                if (this._name.indexOf("simulator") != -1) {
                    this._platformName = "iphonesimulator";
                } else {
                    this._platformName = "iphoneos";
                }
            }
        };

        SDK.prototype.parseSdkVersion = function () {
            var re = /\d+(\.\d+)*$/img;
            var m = re.exec(this._name);
            if (m != null && m[0] != undefined) {
                this._version = parseFloat(m[0]);
            }
        };
        return SDK;
    })();
    VCRemoteLib.SDK = SDK;

    var SDKList = (function () {
        function SDKList() {
        }
        Object.defineProperty(SDKList, "list", {
            get: function () {
                var result = spawnSync("xcodebuild", ["-showsdks"]);
                var output = result.stdout.toString("utf8", 0, result.stdout.length);
                var re = /(macosx|iphone(os|simulator))(\d+(\.\d+)*)/img;
                var m;
                var sdks = [];
                while ((m = re.exec(output)) !== null) {
                    if (m.index === re.lastIndex) {
                        re.lastIndex++;
                    }
                    var sdk = new SDK(m[0].toLowerCase());
                    sdks.push(sdk);
                }
                return sdks;
            },
            enumerable: true,
            configurable: true
        });
        return SDKList;
    })();
    VCRemoteLib.SDKList = SDKList;

    var RemoteProjectBase = (function () {
        function RemoteProjectBase(req) {
            this._state = 9 /* READY */;
            this._stateList = [];
            this._req = req;
        }
        Object.defineProperty(RemoteProjectBase.prototype, "req", {
            get: function () {
                return this._req;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(RemoteProjectBase.prototype, "collection", {
            get: function () {
                return this._collection;
            },
            set: function (collection) {
                this._collection = collection;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(RemoteProjectBase.prototype, "state", {
            get: function () {
                return this._state;
            },
            set: function (state) {
                this._state = state;
                this._stateList.push(state);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(RemoteProjectBase.prototype, "history", {
            get: function () {
                return this._stateList;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(RemoteProjectBase.prototype, "project", {
            get: function () {
                return this._project;
            },
            set: function (project) {
                this._project = project;
            },
            enumerable: true,
            configurable: true
        });

        RemoteProjectBase.prototype.retrieve = function () {
            var deferred = q.defer();
            var self = this;

            // override the project location if asked
            if (this.req.query.loc != undefined) {
                this._project.directory = this.req.query.loc;
            }
            console.info("Uploading project %s to %s.", this._project.id, this._project.directory);
            this.state = 7 /* PENDING */;
            this._project.state = 7 /* PENDING */;
            var resource = new RemoteZipResource(this._req);
            resource.on("error", function (err) {
                self.state = 8 /* ERROR */;
                deferred.reject(err);
            });
            resource.on("uploading", function () {
                self.state = 0 /* UPLOADING */;
            });
            resource.on("uploaded", function () {
                self.state = 1 /* UPLOADED */;
            });
            resource.on("extracting", function () {
                self.state = 2 /* EXTRACTING */;
            });
            resource.on("extracted", function () {
                self.state = 3 /* EXTRACTED */;
                deferred.resolve(self._project);
            });
            resource.retrieve(self._project.directory);
            return deferred.promise;
        };

        RemoteProjectBase.prototype.update = function (project) {
            return project.update(this);
        };
        return RemoteProjectBase;
    })();
    VCRemoteLib.RemoteProjectBase = RemoteProjectBase;

    var RemoteZipResource = (function (_super) {
        __extends(RemoteZipResource, _super);
        function RemoteZipResource(req) {
            _super.call(this);
            this._req = req;
        }
        Object.defineProperty(RemoteZipResource.prototype, "req", {
            get: function () {
                return this._req;
            },
            enumerable: true,
            configurable: true
        });

        RemoteZipResource.prototype.retrieve = function (dir) {
            var self = this;

            try  {
                util.createDirectoryIfNecessary(dir);
            } catch (err) {
                console.error("An error occurred trying to create directory: " + dir);
                this.emit("error", new Error("An error occurred trying to create directory"));
                return;
            }

            this.emit("uploading");

            if (this.req.query.payload === "false") {
                self.emit("uploaded");
                self.emit("extracting");
                self.emit("extracted");
                return;
            }

            this.retrievePayload().then(function (fileName) {
                self.emit("uploaded");
                self.emit("extracting");
                return self.unzipPayload(fileName, dir).then(function () {
                    self.emit("extracted");
                });
            }).fail(function (reason) {
                self.emit("error", reason);
            });
        };

        RemoteZipResource.prototype.retrievePayload = function () {
            var deferred = q.defer();
            var self = this;
            tmp.file({ postfix: '.zip', keep: true }, function (err, tmpFileName, fd, cleanup) {
                deferred.promise.finally(cleanup);
                if (err) {
                    deferred.reject(err);
                }
                console.info("Uploading payload to %s.", tmpFileName);
                self.req.pipe(fs.createWriteStream(tmpFileName)).on("finish", function () {
                    if (fs.statSync(tmpFileName).size == 0) {
                        console.error("The POST request did not contain any data.");
                        deferred.reject(new Error("The POST request did not contain any data."));
                    } else {
                        console.info("Finished uploading payload.");
                        deferred.resolve(tmpFileName);
                    }
                }).on("error", function (err) {
                    console.error("An error occurred uploading the project payload to %s.", tmpFileName);
                    deferred.reject(err);
                });
            });
            return deferred.promise;
        };

        RemoteZipResource.prototype.unzipPayload = function (zipFileName, targetDir) {
            var deferred = q.defer();
            if (!fs.existsSync(zipFileName)) {
                deferred.reject("payload not found!");
                console.info("Error: payload file not found %s.", zipFileName);
            } else {
                fs.createReadStream(zipFileName).pipe(unzip.Parse()).on("end", function () {
                    console.info("Finished extracting payload.");

                    var path = targetDir + "/.lastSyncTime";
                    if (!fs.existsSync(path)) {
                        fs.writeFileSync(path, "");
                    } else {
                        var timeNow = Date.now() / 1000;
                        fs.utimesSync(path, timeNow, timeNow);
                    }

                    deferred.resolve(targetDir);
                }).on("error", function (err) {
                    console.error("An error occurred unzipping project payload %s to %s.", zipFileName, targetDir);
                    deferred.reject(err);
                }).pipe(fstream.Writer(targetDir));
            }
            console.info("Unzipping project payload %s to %s.", zipFileName, targetDir);
            return deferred.promise;
        };
        return RemoteZipResource;
    })(events.EventEmitter);
    VCRemoteLib.RemoteZipResource = RemoteZipResource;

    var RemoteSrcProject = (function (_super) {
        __extends(RemoteSrcProject, _super);
        function RemoteSrcProject(req) {
            _super.call(this, req);
        }
        RemoteSrcProject.prototype.retrieve = function () {
            this.project = new LocalSrcProject(this.collection);
            this.project.keepPolicy = KeepPolicy[this.req.query.keep];
            return _super.prototype.retrieve.call(this);
        };
        return RemoteSrcProject;
    })(RemoteProjectBase);
    VCRemoteLib.RemoteSrcProject = RemoteSrcProject;

    var RemoteCfgProject = (function (_super) {
        __extends(RemoteCfgProject, _super);
        function RemoteCfgProject(req) {
            _super.call(this, req);
        }
        Object.defineProperty(RemoteCfgProject.prototype, "srcProject", {
            get: function () {
                return this._srcProject;
            },
            set: function (prj) {
                this._srcProject = prj;
            },
            enumerable: true,
            configurable: true
        });

        RemoteCfgProject.prototype.retrieve = function () {
            var configName = this.req.query.config || "";
            var arch = this.req.query.arch || "";
            var sdk = this.req.query.sdk || "";
            var emulatorBuild = (this.req.query.emulatorBuild === "true") || false;
            var outputDirectory = this.req.query.outDir;
            var provisioningProfile = this.req.query.provisioning || "";
            var pbxProject = this.req.query.pbxProject || "";
            var config = new ProjectConfig(this._srcProject, {
                config: configName,
                arch: arch,
                sdk: sdk,
                emulatorBuild: emulatorBuild,
                outputDirectory: outputDirectory,
                provisioningProfile: provisioningProfile,
                pbxProject: pbxProject });
            if (this.srcProject.configs.hasConfig(config)) {
                return q.reject(new Error("Project config already exists! please use updateConfig."));
            }
            if (this.req.query.prjDir != undefined) {
                config.projectDirectory = this.req.query.prjDir;
            }
            this.project = new LocalCfgProject(this._srcProject, config);
            if (this.req.query.loc != undefined) {
                this.project.directory = this.req.query.loc;
            }
            return _super.prototype.retrieve.call(this);
        };
        return RemoteCfgProject;
    })(RemoteProjectBase);
    VCRemoteLib.RemoteCfgProject = RemoteCfgProject;

    var ProjectConfig = (function () {
        function ProjectConfig(project, context) {
            // syntesize the project config out of the user context
            this._id = uuid.v4();
            this._project = project;
            this._arch = context.arch;
            if (context.sdk == undefined || context.sdk.length == 0) {
                if (context.arch != undefined && (context.arch === "i386" || context.arch === "x86_64")) {
                    var sdk = from(SDKList.list).where(function (sdk) {
                        return sdk.name.indexOf("simulator") != -1;
                    }).orderByDesc("$version").firstOrDefault(null);
                    if (sdk != null) {
                        this._sdk = sdk.name;
                    }
                }
            } else {
                this._sdk = context.sdk;
            }
            this._config = context.config;
            this._projectDirectory = project.directory;
            this._outputDirectory = context.outputDirectory || path.join(project.directory, "bin", this._config, this._arch);
            this._provisioningProfile = context.provisioningProfile;
            this._pbxProject = context.pbxProject;
        }
        Object.defineProperty(ProjectConfig.prototype, "id", {
            get: function () {
                return this._id;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "project", {
            get: function () {
                return this._project;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "projectDirectory", {
            get: function () {
                return this._projectDirectory;
            },
            set: function (dir) {
                this._projectDirectory = dir;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "outputDirectory", {
            get: function () {
                return this._outputDirectory;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "provisioningProfile", {
            get: function () {
                return this._provisioningProfile;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "pbxProject", {
            get: function () {
                return this._pbxProject;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "arch", {
            get: function () {
                return this._arch;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "sdk", {
            get: function () {
                return this._sdk;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "target", {
            get: function () {
                return this._target;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "workspace", {
            get: function () {
                return this._workspace;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "config", {
            get: function () {
                return this._config;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(ProjectConfig.prototype, "scheme", {
            get: function () {
                return this._scheme;
            },
            enumerable: true,
            configurable: true
        });

        ProjectConfig.prototype.equals = function (config) {
            if (this._id === config.id) {
                return true;
            } else {
                if (this._arch.toLowerCase() === config.arch.toLowerCase() && this._config.toLowerCase() === config.config.toLowerCase()) {
                    return true;
                }
            }
            return false;
        };
        return ProjectConfig;
    })();
    VCRemoteLib.ProjectConfig = ProjectConfig;

    var LocalProjectBareBase = (function () {
        function LocalProjectBareBase(collection, directory) {
            this._state = 7 /* PENDING */;
            this._stateList = [];
            this._id = uuid.v4();
            this._directory = directory || util.findUniqueDirectoryName(collection.storeDirectory);
        }
        Object.defineProperty(LocalProjectBareBase.prototype, "id", {
            get: function () {
                return this._id;
            },
            set: function (id) {
                this._id = id;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectBareBase.prototype, "directory", {
            get: function () {
                return this._directory;
            },
            set: function (dir) {
                this._directory = dir;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectBareBase.prototype, "state", {
            get: function () {
                return this._state;
            },
            set: function (newState) {
                this._state = newState;
                this._stateList.push(newState);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectBareBase.prototype, "history", {
            get: function () {
                return this._stateList;
            },
            enumerable: true,
            configurable: true
        });

        LocalProjectBareBase.prototype.delete = function () {
            rimraf.sync(this._directory);
        };

        LocalProjectBareBase.prototype.update = function (remoteProject) {
            throw new Error("Not yet supported!");
        };
        return LocalProjectBareBase;
    })();
    VCRemoteLib.LocalProjectBareBase = LocalProjectBareBase;

    var LocalProjectSlimBase = (function (_super) {
        __extends(LocalProjectSlimBase, _super);
        function LocalProjectSlimBase(collection, directory) {
            _super.call(this, collection, directory);
            this._keepPolicy = 2 /* TEMPORARY */;
            this._collection = collection;
            this._timestamp = new Date();
            this._build = new build.ProjectBuildManager(this);
        }
        Object.defineProperty(LocalProjectSlimBase.prototype, "collection", {
            get: function () {
                return this._collection;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectSlimBase.prototype, "keepPolicy", {
            get: function () {
                return this._keepPolicy;
            },
            set: function (policy) {
                this._keepPolicy = policy;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectSlimBase.prototype, "timestamp", {
            get: function () {
                return this._timestamp;
            },
            set: function (date) {
                this._timestamp = date;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectSlimBase.prototype, "expired", {
            get: function () {
                // the project can be deleted if it expired or is temporary
                if (this._keepPolicy == 1 /* KEEP_UNTIL_EXPIRED */) {
                    return (new Date(new Date() - this.timestamp).getDate() - 1) > LocalProjectSlimBase.EXPIRATION_DAYS;
                } else if (this._keepPolicy == 2 /* TEMPORARY */) {
                    return true;
                }
                return false;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalProjectSlimBase.prototype, "build", {
            get: function () {
                return this._build;
            },
            enumerable: true,
            configurable: true
        });

        LocalProjectSlimBase.prototype.deleteIfExpired = function () {
            if (this.expired) {
                this.delete();
            }
        };

        LocalProjectSlimBase.prototype.deserialize = function (obj) {
            this.id = obj.id;
            this.directory = obj.directory;
            this.keepPolicy = obj.keepPolicy;
            this.timestamp = obj.timestamp;
        };

        LocalProjectSlimBase.prototype.serialize = function () {
            var obj = {};
            obj.id = this.id;
            obj.directory = this.directory;
            obj.keepPolicy = this.keepPolicy;
            obj.timestamp = this.timestamp;
            return obj;
        };
        LocalProjectSlimBase.EXPIRATION_DAYS = 7;
        return LocalProjectSlimBase;
    })(LocalProjectBareBase);
    VCRemoteLib.LocalProjectSlimBase = LocalProjectSlimBase;

    var LocalProject = (function (_super) {
        __extends(LocalProject, _super);
        function LocalProject(collection, directory) {
            _super.call(this, collection, this.directory);
        }
        return LocalProject;
    })(LocalProjectSlimBase);
    VCRemoteLib.LocalProject = LocalProject;

    var LocalCfgProjects = (function () {
        function LocalCfgProjects(project) {
            this._project = project;
            this._configs = {};
        }
        Object.defineProperty(LocalCfgProjects.prototype, "all", {
            get: function () {
                var configs = [];
                for (var id in this._configs) {
                    configs.push(this._configs[id].config);
                }
                return configs;
            },
            enumerable: true,
            configurable: true
        });

        LocalCfgProjects.prototype.hasConfig = function (config) {
            for (var id in this._configs) {
                if (this._configs[id].config.equals(config)) {
                    return true;
                }
            }
            return undefined;
        };

        LocalCfgProjects.prototype.hasConfigId = function (id) {
            return this.getCfgById(id) != undefined;
        };

        LocalCfgProjects.prototype.getCfgById = function (id) {
            return this._configs[id].config;
        };

        LocalCfgProjects.prototype.addConfig = function (projectCfg) {
            projectCfg.srcProject = this._project;
            var self = this;
            return projectCfg.retrieve().then(function (cfgProject) {
                self._configs[cfgProject.config.id] = cfgProject;
                return q(cfgProject.config);
            }).fail(function (err) {
                return q.reject(err);
            });
        };

        LocalCfgProjects.prototype.updateConfig = function (projectCfg) {
            throw new Error("Not yet implemented!");
        };
        return LocalCfgProjects;
    })();
    VCRemoteLib.LocalCfgProjects = LocalCfgProjects;

    var LocalSrcProject = (function (_super) {
        __extends(LocalSrcProject, _super);
        function LocalSrcProject(collection, directory) {
            this._rootDirectory = directory || util.findUniqueDirectoryName(collection.storeDirectory);
            this.directory = path.join(this._rootDirectory, "src");
            _super.call(this, collection, this.directory);
            this._configs = new LocalCfgProjects(this);
        }
        Object.defineProperty(LocalSrcProject.prototype, "rootDirectory", {
            get: function () {
                return this._rootDirectory;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalSrcProject.prototype, "configs", {
            get: function () {
                return this._configs;
            },
            enumerable: true,
            configurable: true
        });

        LocalSrcProject.prototype.deserialize = function (obj) {
            _super.prototype.deserialize.call(this, obj);
        };

        LocalSrcProject.prototype.serialize = function () {
            var obj = _super.prototype.serialize.call(this);
            obj.configs = [];
            var configs = this.configs.all;
            for (var i = 0; i < configs.length; i++) {
                obj.configs[i] = {};
                obj.configs[i].config = configs[i].config;
                obj.configs[i].arch = configs[i].arch;
                obj.configs[i].id = configs[i].id;
                obj.configs[i].projectDirectory = configs[i].projectDirectory;
                obj.configs[i].scheme = configs[i].scheme;
                obj.configs[i].outputDirectory = configs[i].outputDirectory;
                obj.configs[i].sdk = configs[i].sdk;
                obj.configs[i].target = configs[i].target;
                obj.configs[i].workspace = configs[i].workspace;
            }
            return obj;
        };
        return LocalSrcProject;
    })(LocalProjectSlimBase);
    VCRemoteLib.LocalSrcProject = LocalSrcProject;

    var LocalCfgProject = (function (_super) {
        __extends(LocalCfgProject, _super);
        function LocalCfgProject(project, config) {
            this._project = project;
            this._config = config;
            this.directory = config.projectDirectory;
            _super.call(this, project.collection, this.directory);
        }
        Object.defineProperty(LocalCfgProject.prototype, "project", {
            get: function () {
                return this._project;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(LocalCfgProject.prototype, "config", {
            get: function () {
                return this._config;
            },
            enumerable: true,
            configurable: true
        });
        return LocalCfgProject;
    })(LocalProjectBareBase);
    VCRemoteLib.LocalCfgProject = LocalCfgProject;
})(VCRemoteLib || (VCRemoteLib = {}));

module.exports = VCRemoteLib;
