"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const shelljs = require("shelljs");
const semver = require("semver");
const constants = require("../constants");
class PluginsService {
    constructor($packageManager, $fs, $options, $logger, $errors, $filesHashService, $injector, $mobileHelper, $nodeModulesDependenciesBuilder) {
        this.$packageManager = $packageManager;
        this.$fs = $fs;
        this.$options = $options;
        this.$logger = $logger;
        this.$errors = $errors;
        this.$filesHashService = $filesHashService;
        this.$injector = $injector;
        this.$mobileHelper = $mobileHelper;
        this.$nodeModulesDependenciesBuilder = $nodeModulesDependenciesBuilder;
    }
    get $platformsDataService() {
        return this.$injector.resolve("platformsDataService");
    }
    get $projectDataService() {
        return this.$injector.resolve("projectDataService");
    }
    get npmInstallOptions() {
        return _.merge({
            disableNpmInstall: this.$options.disableNpmInstall,
            frameworkPath: this.$options.frameworkPath,
            ignoreScripts: this.$options.ignoreScripts,
            path: this.$options.path
        }, PluginsService.NPM_CONFIG);
    }
    add(plugin, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensure(projectData);
            const possiblePackageName = path.resolve(plugin);
            if (possiblePackageName.indexOf(".tgz") !== -1 && this.$fs.exists(possiblePackageName)) {
                plugin = possiblePackageName;
            }
            const name = (yield this.$packageManager.install(plugin, projectData.projectDir, this.npmInstallOptions)).name;
            const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule(name, projectData.projectDir);
            const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson);
            if (realNpmPackageJson.nativescript) {
                const pluginData = this.convertToPluginData(realNpmPackageJson, projectData.projectDir);
                const action = (pluginDestinationPath, platform, platformData) => __awaiter(this, void 0, void 0, function* () {
                    this.isPluginDataValidForPlatform(pluginData, platform, projectData);
                });
                yield this.executeForAllInstalledPlatforms(action, projectData);
                this.$logger.info(`Successfully installed plugin ${realNpmPackageJson.name}.`);
            }
            else {
                yield this.$packageManager.uninstall(realNpmPackageJson.name, { save: true }, projectData.projectDir);
                this.$errors.fail(`${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`);
            }
        });
    }
    remove(pluginName, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const removePluginNativeCodeAction = (modulesDestinationPath, platform, platformData) => __awaiter(this, void 0, void 0, function* () {
                const pluginData = this.convertToPluginData(this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir);
                yield platformData.platformProjectService.removePluginNativeCode(pluginData, projectData);
            });
            yield this.executeForAllInstalledPlatforms(removePluginNativeCodeAction, projectData);
            yield this.executeNpmCommand(PluginsService.UNINSTALL_COMMAND_NAME, pluginName, projectData);
            let showMessage = true;
            const action = (modulesDestinationPath, platform, platformData) => __awaiter(this, void 0, void 0, function* () {
                shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName));
                this.$logger.info(`Successfully removed plugin ${pluginName} for ${platform}.`);
                showMessage = false;
            });
            yield this.executeForAllInstalledPlatforms(action, projectData);
            if (showMessage) {
                this.$logger.info(`Successfully removed plugin ${pluginName}`);
            }
        });
    }
    addToPackageJson(plugin, version, isDev, projectDir) {
        const packageJsonPath = this.getPackageJsonFilePath(projectDir);
        let packageJsonContent = this.$fs.readJson(packageJsonPath);
        const collectionKey = isDev ? "devDependencies" : "dependencies";
        const oppositeCollectionKey = isDev ? "dependencies" : "devDependencies";
        if (packageJsonContent[oppositeCollectionKey] && packageJsonContent[oppositeCollectionKey][plugin]) {
            const result = this.removeDependencyFromPackageJsonContent(plugin, packageJsonContent);
            packageJsonContent = result.packageJsonContent;
        }
        packageJsonContent[collectionKey] = packageJsonContent[collectionKey] || {};
        packageJsonContent[collectionKey][plugin] = version;
        this.$fs.writeJson(packageJsonPath, packageJsonContent);
    }
    removeFromPackageJson(plugin, projectDir) {
        const packageJsonPath = this.getPackageJsonFilePath(projectDir);
        const packageJsonContent = this.$fs.readJson(packageJsonPath);
        const result = this.removeDependencyFromPackageJsonContent(plugin, packageJsonContent);
        if (result.hasModifiedPackageJson) {
            this.$fs.writeJson(packageJsonPath, result.packageJsonContent);
        }
    }
    preparePluginNativeCode({ pluginData, platform, projectData }) {
        return __awaiter(this, void 0, void 0, function* () {
            const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
            pluginData.pluginPlatformsFolderPath = (_platform) => path.join(pluginData.fullPath, "platforms", _platform.toLowerCase());
            const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(platform);
            if (this.$fs.exists(pluginPlatformsFolderPath)) {
                const pathToPluginsBuildFile = path.join(platformData.projectRoot, constants.PLUGINS_BUILD_DATA_FILENAME);
                const allPluginsNativeHashes = this.getAllPluginsNativeHashes(pathToPluginsBuildFile);
                const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name];
                const currentPluginNativeHashes = yield this.getPluginNativeHashes(pluginPlatformsFolderPath);
                if (!oldPluginNativeHashes || this.$filesHashService.hasChangesInShasums(oldPluginNativeHashes, currentPluginNativeHashes)) {
                    yield platformData.platformProjectService.preparePluginNativeCode(pluginData, projectData);
                    this.setPluginNativeHashes({
                        pathToPluginsBuildFile,
                        pluginData,
                        currentPluginNativeHashes,
                        allPluginsNativeHashes
                    });
                }
            }
        });
    }
    ensureAllDependenciesAreInstalled(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            let installedDependencies = this.$fs.exists(this.getNodeModulesPath(projectData.projectDir)) ? this.$fs.readDirectory(this.getNodeModulesPath(projectData.projectDir)) : [];
            _(installedDependencies)
                .filter(dependencyName => _.startsWith(dependencyName, "@"))
                .each(scopedDependencyDir => {
                const contents = this.$fs.readDirectory(path.join(this.getNodeModulesPath(projectData.projectDir), scopedDependencyDir));
                installedDependencies = installedDependencies.concat(contents.map(dependencyName => `${scopedDependencyDir}/${dependencyName}`));
            });
            const packageJsonContent = this.$fs.readJson(this.getPackageJsonFilePath(projectData.projectDir));
            const allDependencies = _.keys(packageJsonContent.dependencies).concat(_.keys(packageJsonContent.devDependencies));
            const notInstalledDependencies = _.difference(allDependencies, installedDependencies);
            if (this.$options.force || notInstalledDependencies.length) {
                this.$logger.trace("Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", notInstalledDependencies);
                yield this.$packageManager.install(projectData.projectDir, projectData.projectDir, {
                    disableNpmInstall: this.$options.disableNpmInstall,
                    frameworkPath: this.$options.frameworkPath,
                    ignoreScripts: this.$options.ignoreScripts,
                    path: this.$options.path
                });
            }
        });
    }
    getAllInstalledPlugins(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const nodeModules = (yield this.getAllInstalledModules(projectData)).map(nodeModuleData => this.convertToPluginData(nodeModuleData, projectData.projectDir));
            return _.filter(nodeModules, nodeModuleData => nodeModuleData && nodeModuleData.isPlugin);
        });
    }
    getAllProductionPlugins(projectData, dependencies) {
        const allProductionPlugins = [];
        dependencies = dependencies || this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir);
        if (_.isEmpty(dependencies)) {
            return allProductionPlugins;
        }
        _.forEach(dependencies, dependency => {
            const isPlugin = !!dependency.nativescript;
            if (isPlugin) {
                const pluginData = this.convertToPluginData(dependency, projectData.projectDir);
                allProductionPlugins.push(pluginData);
            }
        });
        return allProductionPlugins;
    }
    getDependenciesFromPackageJson(projectDir) {
        const packageJson = this.$fs.readJson(this.getPackageJsonFilePath(projectDir));
        const dependencies = this.getBasicPluginInformation(packageJson.dependencies);
        const devDependencies = this.getBasicPluginInformation(packageJson.devDependencies);
        return {
            dependencies,
            devDependencies
        };
    }
    isNativeScriptPlugin(pluginPackageJsonPath) {
        const pluginPackageJsonContent = this.$fs.readJson(pluginPackageJsonPath);
        return pluginPackageJsonContent && pluginPackageJsonContent.nativescript;
    }
    convertToPluginData(cacheData, projectDir) {
        const pluginData = {};
        pluginData.name = cacheData.name;
        pluginData.version = cacheData.version;
        pluginData.fullPath = cacheData.directory || path.dirname(this.getPackageJsonFilePathForModule(cacheData.name, projectDir));
        pluginData.isPlugin = !!cacheData.nativescript || !!cacheData.moduleInfo;
        pluginData.pluginPlatformsFolderPath = (platform) => path.join(pluginData.fullPath, "platforms", platform);
        const data = cacheData.nativescript || cacheData.moduleInfo;
        if (pluginData.isPlugin) {
            pluginData.platformsDataService = data.platforms;
            pluginData.pluginVariables = data.variables;
        }
        return pluginData;
    }
    removeDependencyFromPackageJsonContent(dependency, packageJsonContent) {
        let hasModifiedPackageJson = false;
        if (packageJsonContent.devDependencies && packageJsonContent.devDependencies[dependency]) {
            delete packageJsonContent.devDependencies[dependency];
            hasModifiedPackageJson = true;
        }
        if (packageJsonContent.dependencies && packageJsonContent.dependencies[dependency]) {
            delete packageJsonContent.dependencies[dependency];
            hasModifiedPackageJson = true;
        }
        return {
            hasModifiedPackageJson,
            packageJsonContent
        };
    }
    getBasicPluginInformation(dependencies) {
        return _.map(dependencies, (version, key) => ({
            name: key,
            version: version
        }));
    }
    getNodeModulesPath(projectDir) {
        return path.join(projectDir, "node_modules");
    }
    getPackageJsonFilePath(projectDir) {
        return path.join(projectDir, "package.json");
    }
    getPackageJsonFilePathForModule(moduleName, projectDir) {
        const pathToJsonFile = require.resolve(`${moduleName}/package.json`, {
            paths: [projectDir]
        });
        return pathToJsonFile;
    }
    getDependencies(projectDir) {
        const packageJsonFilePath = this.getPackageJsonFilePath(projectDir);
        return _.keys(require(packageJsonFilePath).dependencies);
    }
    getNodeModuleData(module, projectDir) {
        if (!this.$fs.exists(module) || path.basename(module) !== "package.json") {
            module = this.getPackageJsonFilePathForModule(module, projectDir);
        }
        const data = this.$fs.readJson(module);
        return {
            name: data.name,
            version: data.version,
            fullPath: path.dirname(module),
            isPlugin: data.nativescript !== undefined,
            moduleInfo: data.nativescript
        };
    }
    ensure(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensureAllDependenciesAreInstalled(projectData);
            this.$fs.ensureDirectoryExists(this.getNodeModulesPath(projectData.projectDir));
        });
    }
    getAllInstalledModules(projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.ensure(projectData);
            const nodeModules = this.getDependencies(projectData.projectDir);
            return _.map(nodeModules, nodeModuleName => this.getNodeModuleData(nodeModuleName, projectData.projectDir));
        });
    }
    executeNpmCommand(npmCommandName, npmCommandArguments, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) {
                yield this.$packageManager.install(npmCommandArguments, projectData.projectDir, this.npmInstallOptions);
            }
            else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) {
                yield this.$packageManager.uninstall(npmCommandArguments, PluginsService.NPM_CONFIG, projectData.projectDir);
            }
            return this.parseNpmCommandResult(npmCommandArguments);
        });
    }
    parseNpmCommandResult(npmCommandResult) {
        return npmCommandResult.split("@")[0];
    }
    executeForAllInstalledPlatforms(action, projectData) {
        return __awaiter(this, void 0, void 0, function* () {
            const availablePlatforms = this.$mobileHelper.platformNames.map(p => p.toLowerCase());
            for (const platform of availablePlatforms) {
                const isPlatformInstalled = this.$fs.exists(path.join(projectData.platformsDir, platform.toLowerCase()));
                if (isPlatformInstalled) {
                    const platformData = this.$platformsDataService.getPlatformData(platform.toLowerCase(), projectData);
                    const pluginDestinationPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME, "tns_modules");
                    yield action(pluginDestinationPath, platform.toLowerCase(), platformData);
                }
            }
        });
    }
    getInstalledFrameworkVersion(platform, projectData) {
        const platformData = this.$platformsDataService.getPlatformData(platform, projectData);
        const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName);
        return frameworkData.version;
    }
    isPluginDataValidForPlatform(pluginData, platform, projectData) {
        let isValid = true;
        const installedFrameworkVersion = this.getInstalledFrameworkVersion(platform, projectData);
        const pluginPlatformsData = pluginData.platformsDataService;
        if (pluginPlatformsData) {
            const versionRequiredByPlugin = pluginPlatformsData[platform];
            if (!versionRequiredByPlugin) {
                this.$logger.warn(`${pluginData.name} is not supported for ${platform}.`);
                isValid = false;
            }
            else if (semver.gt(versionRequiredByPlugin, installedFrameworkVersion)) {
                this.$logger.warn(`${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.`);
                isValid = false;
            }
        }
        return isValid;
    }
    getPluginNativeHashes(pluginPlatformsDir) {
        return __awaiter(this, void 0, void 0, function* () {
            let data = {};
            if (this.$fs.exists(pluginPlatformsDir)) {
                const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginPlatformsDir);
                data = yield this.$filesHashService.generateHashes(pluginNativeDataFiles);
            }
            return data;
        });
    }
    getAllPluginsNativeHashes(pathToPluginsBuildFile) {
        let data = {};
        if (this.$fs.exists(pathToPluginsBuildFile)) {
            data = this.$fs.readJson(pathToPluginsBuildFile);
        }
        return data;
    }
    setPluginNativeHashes(opts) {
        opts.allPluginsNativeHashes[opts.pluginData.name] = opts.currentPluginNativeHashes;
        this.$fs.writeJson(opts.pathToPluginsBuildFile, opts.allPluginsNativeHashes);
    }
}
PluginsService.INSTALL_COMMAND_NAME = "install";
PluginsService.UNINSTALL_COMMAND_NAME = "uninstall";
PluginsService.NPM_CONFIG = {
    save: true
};
exports.PluginsService = PluginsService;
$injector.register("pluginsService", PluginsService);
