"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
const path = require("path");
const os = require("os");
const constants = require("../../constants");
const helpers_1 = require("../../helpers");
const decorators_1 = require("../../decorators");
const url = require("url");
class NpmService {
    constructor($childProcess, $errors, $fs, $hostInfo, $httpClient, $logger, $projectConstants) {
        this.$childProcess = $childProcess;
        this.$errors = $errors;
        this.$fs = $fs;
        this.$hostInfo = $hostInfo;
        this.$httpClient = $httpClient;
        this.$logger = $logger;
        this.$projectConstants = $projectConstants;
        this._hasCheckedNpmProxy = false;
    }
    get npmBinary() {
        if (!this._npmBinary) {
            try {
                require(NpmService.NPM_MODULE_NAME);
                let npmMainJsFile = require.resolve(NpmService.NPM_MODULE_NAME);
                let pathToNpmBinary = path.join(npmMainJsFile.substring(0, npmMainJsFile.lastIndexOf(constants.NODE_MODULES_DIR_NAME) + constants.NODE_MODULES_DIR_NAME.length), ".bin", this.npmExecutableName);
                if (!this.$fs.exists(pathToNpmBinary)) {
                    throw new Error(`The npm binary is not in ${pathToNpmBinary} as expected.`);
                }
                this._npmBinary = pathToNpmBinary;
            }
            catch (err) {
                this.$logger.trace(`Error while trying to get the npm binary: ${err}`);
                this._npmBinary = this.npmExecutableName;
            }
        }
        return this._npmBinary;
    }
    get npmExecutableName() {
        if (!this._npmExecutableName) {
            this._npmExecutableName = "npm";
            if (this.$hostInfo.isWindows) {
                this._npmExecutableName += ".cmd";
            }
        }
        return this._npmExecutableName;
    }
    install(projectDir, dependencyToInstall) {
        return (() => {
            let npmInstallResult = {};
            if (dependencyToInstall) {
                npmInstallResult.result = {
                    isInstalled: false,
                    isTypesInstalled: false
                };
                try {
                    this.npmInstall(projectDir, dependencyToInstall.name, dependencyToInstall.version, ["--save", "--save-exact"]).wait();
                    npmInstallResult.result.isInstalled = true;
                }
                catch (err) {
                    npmInstallResult.error = err;
                }
                if (dependencyToInstall.installTypes && npmInstallResult.result.isInstalled && this.hasTypesForDependency(dependencyToInstall.name).wait()) {
                    try {
                        this.installTypingsForDependency(projectDir, dependencyToInstall.name).wait();
                        npmInstallResult.result.isTypesInstalled = true;
                    }
                    catch (err) {
                        npmInstallResult.error = err;
                    }
                }
            }
            else {
                try {
                    this.npmPrune(projectDir).wait();
                    this.npmInstall(projectDir).wait();
                }
                catch (err) {
                    npmInstallResult.error = err;
                }
            }
            this.generateReferencesFile(projectDir);
            return npmInstallResult;
        }).future()();
    }
    uninstall(projectDir, dependency) {
        return (() => {
            let packageJsonContent = this.getPackageJsonContent(projectDir);
            if (packageJsonContent && packageJsonContent.dependencies && packageJsonContent.dependencies[dependency]) {
                this.npmUninstall(projectDir, dependency, ["--save"]).wait();
            }
            if (packageJsonContent && packageJsonContent.devDependencies && packageJsonContent.devDependencies[`${NpmService.TYPES_DIRECTORY}${dependency}`]) {
                this.npmUninstall(projectDir, `${NpmService.TYPES_DIRECTORY}${dependency}`, ["--save-dev"]).wait();
            }
            this.generateReferencesFile(projectDir);
        }).future()();
    }
    search(projectDir, keywords, args) {
        return (() => {
            args = args === undefined ? [] : args;
            let result = [];
            let commandArguments = _.concat(["search"], args, keywords);
            let spawnResult = this.executeNpmCommandCore(projectDir, commandArguments).wait();
            if (spawnResult.stderr) {
                let splitError = spawnResult.stderr.trim().split("\n");
                if (splitError.length > 1 || splitError[0].indexOf("Building the local index for the first time") === -1) {
                    this.$errors.failWithoutHelp(spawnResult.stderr);
                }
            }
            let pluginsRows = spawnResult.stdout.split("\n");
            pluginsRows.shift();
            let npmNameGroup = "(\\S+)";
            let npmDateGroup = "(\\d+-\\d+-\\d+)\\s";
            let npmFreeTextGroup = "([^=]+)";
            let npmAuthorsGroup = "((?:=\\S+\\s?)+)\\s+";
            let pluginRowRegExp = new RegExp(`${npmNameGroup}\\s+${npmFreeTextGroup}${npmAuthorsGroup}${npmDateGroup}${npmNameGroup}(\\s+${npmFreeTextGroup})?`);
            _.each(pluginsRows, (pluginRow) => {
                let matches = pluginRowRegExp.exec(pluginRow.trim());
                if (!matches || !matches[0]) {
                    return;
                }
                result.push({
                    name: matches[1],
                    description: matches[2],
                    author: matches[3],
                    version: matches[5]
                });
            });
            return result;
        }).future()();
    }
    getPackageJsonFromNpmRegistry(packageName, version) {
        return (() => {
            const timeout = 6000;
            let packageJsonContent;
            version = version || "latest";
            try {
                let url = this.buildNpmRegistryUrl(packageName, version).wait(), proxySettings = this.getNpmProxySettings().wait();
                let result = this.$httpClient.httpRequest({ url, timeout }, proxySettings).wait().body;
                packageJsonContent = JSON.parse(result);
            }
            catch (err) {
                this.$logger.trace("Error caught while checking the NPM Registry for plugin with id: %s", packageName);
                this.$logger.trace(err.message);
            }
            return packageJsonContent;
        }).future()();
    }
    isScopedDependency(dependency) {
        let matches = dependency.match(NpmService.SCOPED_DEPENDENCY_REGEXP);
        return !!(matches && matches[0]);
    }
    getDependencyInformation(dependency) {
        let regExp = this.isScopedDependency(dependency) ? NpmService.SCOPED_DEPENDENCY_REGEXP : NpmService.DEPENDENCY_REGEXP;
        let matches = dependency.match(regExp);
        return {
            name: matches[1],
            version: matches[2]
        };
    }
    hasTypesForDependency(packageName) {
        return (() => {
            return !!this.getPackageJsonFromNpmRegistry(`${NpmService.TYPES_DIRECTORY}${packageName}`).wait();
        }).future()();
    }
    buildNpmRegistryUrl(packageName, version) {
        return (() => {
            let registryUrl = this.getNpmRegistryUrl().wait();
            if (!_.endsWith(registryUrl, "/")) {
                registryUrl += "/";
            }
            return `${registryUrl}${packageName.replace("/", "%2F")}?version=${encodeURIComponent(version)}`;
        }).future()();
    }
    getNpmRegistryUrl() {
        return (() => {
            if (!this._npmRegistryUrl) {
                let currentNpmRegistry;
                try {
                    currentNpmRegistry = (this.$childProcess.exec("npm config get registry").wait() || "").toString().trim();
                }
                catch (err) {
                    this.$logger.trace(`Unable to get registry from npm config. Error is ${err.message}.`);
                }
                this._npmRegistryUrl = currentNpmRegistry || NpmService.NPM_REGISTRY_URL;
                this.$logger.trace(`Npm registry is: ${this._npmRegistryUrl}.`);
            }
            return this._npmRegistryUrl;
        }).future()();
    }
    getPackageJsonContent(projectDir) {
        let pathToPackageJson = this.getPathToPackageJson(projectDir);
        try {
            return this.$fs.readJson(pathToPackageJson);
        }
        catch (err) {
            if (err.code === "ENOENT") {
                this.$errors.failWithoutHelp(`Unable to find ${this.$projectConstants.PACKAGE_JSON_NAME} in ${projectDir}.`);
            }
            throw err;
        }
    }
    getPathToPackageJson(projectDir) {
        return path.join(projectDir, this.$projectConstants.PACKAGE_JSON_NAME);
    }
    getPathToReferencesFile(projectDir) {
        return path.join(projectDir, this.$projectConstants.REFERENCES_FILE_NAME);
    }
    installTypingsForDependency(projectDir, dependency) {
        return this.npmInstall(projectDir, `${NpmService.TYPES_DIRECTORY}${dependency}`, null, ["--save-dev", "--save-exact"]);
    }
    generateReferencesFile(projectDir) {
        let packageJsonContent = this.getPackageJsonContent(projectDir);
        let pathToReferenceFile = this.getPathToReferencesFile(projectDir), lines = [];
        if (packageJsonContent && packageJsonContent.dependencies && packageJsonContent.dependencies[constants.TNS_CORE_MODULES]) {
            let relativePathToTnsCoreModulesDts = `./${constants.NODE_MODULES_DIR_NAME}/${constants.TNS_CORE_MODULES}/${NpmService.TNS_CORE_MODULES_DEFINITION_FILE_NAME}`;
            if (this.$fs.exists(path.join(projectDir, relativePathToTnsCoreModulesDts))) {
                lines.push(this.getReferenceLine(relativePathToTnsCoreModulesDts));
            }
        }
        _(packageJsonContent.devDependencies)
            .keys()
            .each(devDependency => {
            if (this.isFromTypesRepo(devDependency)) {
                let nodeModulesDirectory = path.join(projectDir, constants.NODE_MODULES_DIR_NAME);
                let definitionFiles = this.$fs.enumerateFilesInDirectorySync(path.join(nodeModulesDirectory, devDependency), (file, stat) => _.endsWith(file, constants.FileExtensions.TYPESCRIPT_DEFINITION_FILE) || stat.isDirectory(), { enumerateDirectories: false });
                let defs = _.map(definitionFiles, def => this.getReferenceLine(helpers_1.fromWindowsRelativePathToUnix(path.relative(projectDir, def))));
                this.$logger.trace(`Adding lines for definition files: ${definitionFiles.join(", ")}`);
                lines = lines.concat(defs);
            }
        });
        if (lines.length) {
            this.$logger.trace("Updating reference file with new entries...");
            this.$fs.writeFile(pathToReferenceFile, lines.join(os.EOL), "utf8");
            this.removeOldAbReferencesFile(projectDir);
        }
        else {
            this.$logger.trace(`Could not find any .d.ts files for ${this.$projectConstants.REFERENCES_FILE_NAME} file. Deleting the old file.`);
            this.$fs.deleteFile(pathToReferenceFile);
        }
    }
    removeOldAbReferencesFile(projectDir) {
        const pathToOldReferencesFile = path.join(projectDir, this.$projectConstants.OLD_REFERENCES_FILE_NAME);
        if (this.$fs.exists(pathToOldReferencesFile)) {
            this.$fs.deleteFile(pathToOldReferencesFile);
        }
    }
    isFromTypesRepo(dependency) {
        return !!dependency.match(/^@types\//);
    }
    getReferenceLine(pathToReferencedFile) {
        return `/// <reference path="${pathToReferencedFile}" />`;
    }
    getNpmArguments(command, npmArguments) {
        npmArguments = npmArguments === undefined ? [] : npmArguments;
        return npmArguments.concat([command]);
    }
    npmInstall(projectDir, dependency, version, npmArguments) {
        return this.executeNpmCommand(projectDir, this.getNpmArguments("install", npmArguments), dependency, version);
    }
    npmUninstall(projectDir, dependency, npmArguments) {
        return this.executeNpmCommand(projectDir, this.getNpmArguments("uninstall", npmArguments), dependency, null);
    }
    npmPrune(projectDir, dependency, version) {
        return this.executeNpmCommand(projectDir, this.getNpmArguments("prune"), dependency, version);
    }
    executeNpmCommand(projectDir, npmArguments, dependency, version) {
        return (() => {
            if (dependency) {
                let dependencyToInstall = dependency;
                if (version) {
                    dependencyToInstall += `@${version}`;
                }
                npmArguments.push(dependencyToInstall);
            }
            return this.executeNpmCommandCore(projectDir, npmArguments).wait();
        }).future()();
    }
    executeNpmCommandCore(projectDir, npmArguments) {
        return this.$childProcess.spawnFromEvent(this.npmExecutableName, npmArguments, "close", { cwd: projectDir });
    }
    getNpmProxySettings() {
        return (() => {
            if (!this._hasCheckedNpmProxy) {
                try {
                    let npmProxy = (this.$childProcess.exec("npm config get proxy").wait() || "").toString().trim();
                    if (npmProxy && npmProxy !== "null") {
                        let uri = url.parse(npmProxy);
                        this._proxySettings = {
                            hostname: uri.hostname,
                            port: uri.port
                        };
                    }
                }
                catch (err) {
                    this.$logger.trace(`Unable to get npm proxy configuration. Error is: ${err.message}.`);
                }
                this.$logger.trace("Npm proxy is: ", this._proxySettings);
                this._hasCheckedNpmProxy = true;
            }
            return this._proxySettings;
        }).future()();
    }
}
NpmService.NPM_MODULE_NAME = "npm";
NpmService.TYPES_DIRECTORY = "@types/";
NpmService.TNS_CORE_MODULES_DEFINITION_FILE_NAME = `${constants.TNS_CORE_MODULES}${constants.FileExtensions.TYPESCRIPT_DEFINITION_FILE}`;
NpmService.NPM_REGISTRY_URL = "http://registry.npmjs.org";
NpmService.SCOPED_DEPENDENCY_REGEXP = /^(@.+?)(?:@(.+?))?$/;
NpmService.DEPENDENCY_REGEXP = /^(.+?)(?:@(.+?))?$/;
__decorate([
    decorators_1.exportedPromise("npmService")
], NpmService.prototype, "install", null);
__decorate([
    decorators_1.exportedPromise("npmService")
], NpmService.prototype, "uninstall", null);
exports.NpmService = NpmService;
$injector.register("npmService", NpmService);
