"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());
    });
};
const path = require("path");
const shell = require("shelljs");
const constants = require("../constants");
const semver = require("semver");
const projectServiceBaseLib = require("./platform-project-service-base");
const device_android_debug_bridge_1 = require("../common/mobile/android/device-android-debug-bridge");
const os_1 = require("os");
class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase {
    constructor($androidEmulatorServices, $androidToolsInfo, $childProcess, $errors, $fs, $hostInfo, $logger, $options, $projectData, $projectDataService, $sysInfo, $injector, $pluginVariablesService, $devicePlatformsConstants, $config, $npm) {
        super($fs, $projectData, $projectDataService);
        this.$androidEmulatorServices = $androidEmulatorServices;
        this.$androidToolsInfo = $androidToolsInfo;
        this.$childProcess = $childProcess;
        this.$errors = $errors;
        this.$hostInfo = $hostInfo;
        this.$logger = $logger;
        this.$options = $options;
        this.$sysInfo = $sysInfo;
        this.$injector = $injector;
        this.$pluginVariablesService = $pluginVariablesService;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$config = $config;
        this.$npm = $npm;
        this._platformData = null;
        this._androidProjectPropertiesManagers = Object.create(null);
    }
    get platformData() {
        if (!this._platformData) {
            let projectRoot = path.join(this.$projectData.platformsDir, "android");
            let packageName = this.getProjectNameFromId();
            this._platformData = {
                frameworkPackageName: "tns-android",
                normalizedPlatformName: "Android",
                appDestinationDirectoryPath: path.join(projectRoot, "src", "main", "assets"),
                platformProjectService: this,
                emulatorServices: this.$androidEmulatorServices,
                projectRoot: projectRoot,
                deviceBuildOutputPath: path.join(projectRoot, "build", "outputs", "apk"),
                validPackageNamesForDevice: [
                    `${packageName}-debug.apk`,
                    `${packageName}-release.apk`,
                    `${this.$projectData.projectName}-debug.apk`,
                    `${this.$projectData.projectName}-release.apk`
                ],
                frameworkFilesExtensions: [".jar", ".dat", ".so"],
                configurationFileName: "AndroidManifest.xml",
                configurationFilePath: path.join(projectRoot, "src", "main", "AndroidManifest.xml"),
                relativeToFrameworkConfigurationFilePath: path.join("src", "main", "AndroidManifest.xml"),
                fastLivesyncFileExtensions: [".jpg", ".gif", ".png", ".bmp", ".webp"]
            };
        }
        return this._platformData;
    }
    validateOptions() {
        return Promise.resolve(true);
    }
    getAppResourcesDestinationDirectoryPath(frameworkVersion) {
        if (this.canUseGradle(frameworkVersion)) {
            return path.join(this.platformData.projectRoot, "src", "main", "res");
        }
        return path.join(this.platformData.projectRoot, "res");
    }
    validate() {
        return __awaiter(this, void 0, void 0, function* () {
            this.validatePackageName(this.$projectData.projectId);
            this.validateProjectName(this.$projectData.projectName);
            yield this.$androidToolsInfo.getPathToAndroidExecutable({ showWarningsAsErrors: true });
            let javaCompilerVersion = yield this.$sysInfo.getJavaCompilerVersion();
            yield this.$androidToolsInfo.validateJavacVersion(javaCompilerVersion, { showWarningsAsErrors: true });
        });
    }
    createProject(frameworkDir, frameworkVersion, pathToTemplate) {
        return __awaiter(this, void 0, void 0, function* () {
            if (semver.lt(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE)) {
                this.$errors.failWithoutHelp(`The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.`);
            }
            this.$fs.ensureDirectoryExists(this.platformData.projectRoot);
            yield this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true });
            let androidToolsInfo = yield this.$androidToolsInfo.getToolsInfo();
            let targetSdkVersion = androidToolsInfo.targetSdkVersion;
            this.$logger.trace(`Using Android SDK '${targetSdkVersion}'.`);
            this.copy(this.platformData.projectRoot, frameworkDir, "libs", "-R");
            if (pathToTemplate) {
                let mainPath = path.join(this.platformData.projectRoot, "src", "main");
                this.$fs.createDirectory(mainPath);
                shell.cp("-R", path.join(path.resolve(pathToTemplate), "*"), mainPath);
            }
            else {
                this.copy(this.platformData.projectRoot, frameworkDir, "src", "-R");
            }
            this.copy(this.platformData.projectRoot, frameworkDir, "build.gradle settings.gradle build-tools", "-Rf");
            try {
                this.copy(this.platformData.projectRoot, frameworkDir, "gradle.properties", "-Rf");
            }
            catch (e) {
                this.$logger.warn(`\n${e}\nIt's possible, the final .apk file will contain all architectures instead of the ones described in the abiFilters!\nYou can fix this by using the latest android platform.`);
            }
            this.copy(this.platformData.projectRoot, frameworkDir, "gradle", "-R");
            this.copy(this.platformData.projectRoot, frameworkDir, "gradlew gradlew.bat", "-f");
            this.cleanResValues(targetSdkVersion, frameworkVersion);
            let npmConfig = {
                "save": true,
                "save-dev": true,
                "save-exact": true,
                "silent": true
            };
            let projectPackageJson = this.$fs.readJson(this.$projectData.projectFilePath);
            for (let dependency of AndroidProjectService.REQUIRED_DEV_DEPENDENCIES) {
                let dependencyVersionInProject = (projectPackageJson.dependencies && projectPackageJson.dependencies[dependency.name]) ||
                    (projectPackageJson.devDependencies && projectPackageJson.devDependencies[dependency.name]);
                if (!dependencyVersionInProject) {
                    yield this.$npm.install(`${dependency.name}@${dependency.version}`, this.$projectData.projectDir, npmConfig);
                }
                else {
                    let cleanedVerson = semver.clean(dependencyVersionInProject);
                    if (!cleanedVerson) {
                        let pathToPluginPackageJson = path.join(this.$projectData.projectDir, constants.NODE_MODULES_FOLDER_NAME, dependency.name, constants.PACKAGE_JSON_FILE_NAME);
                        dependencyVersionInProject = this.$fs.exists(pathToPluginPackageJson) && this.$fs.readJson(pathToPluginPackageJson).version;
                    }
                    if (!semver.satisfies(dependencyVersionInProject || cleanedVerson, dependency.version)) {
                        this.$errors.failWithoutHelp(`Your project have installed ${dependency.name} version ${cleanedVerson} but Android platform requires version ${dependency.version}.`);
                    }
                }
            }
            ;
        });
    }
    cleanResValues(targetSdkVersion, frameworkVersion) {
        let resDestinationDir = this.getAppResourcesDestinationDirectoryPath(frameworkVersion);
        let directoriesInResFolder = this.$fs.readDirectory(resDestinationDir);
        let directoriesToClean = directoriesInResFolder
            .map(dir => {
            return {
                dirName: dir,
                sdkNum: parseInt(dir.substr(AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX.length))
            };
        })
            .filter(dir => dir.dirName.match(AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX)
            && dir.sdkNum
            && (!targetSdkVersion || (targetSdkVersion < dir.sdkNum)))
            .map(dir => path.join(resDestinationDir, dir.dirName));
        this.$logger.trace("Directories to clean:");
        this.$logger.trace(directoriesToClean);
        _.map(directoriesToClean, dir => this.$fs.deleteDirectory(dir));
    }
    interpolateData() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.interpolateConfigurationFile();
            let stringsFilePath = path.join(this.getAppResourcesDestinationDirectoryPath(), 'values', 'strings.xml');
            shell.sed('-i', /__NAME__/, this.$projectData.projectName, stringsFilePath);
            shell.sed('-i', /__TITLE_ACTIVITY__/, this.$projectData.projectName, stringsFilePath);
            let gradleSettingsFilePath = path.join(this.platformData.projectRoot, "settings.gradle");
            shell.sed('-i', /__PROJECT_NAME__/, this.getProjectNameFromId(), gradleSettingsFilePath);
            let userAppGradleFilePath = path.join(this.$projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, "app.gradle");
            try {
                shell.sed('-i', /__PACKAGE__/, this.$projectData.projectId, userAppGradleFilePath);
            }
            catch (e) {
                this.$logger.warn(`\n${e}.\nCheck if you're using an outdated template and update it.`);
            }
        });
    }
    interpolateConfigurationFile() {
        return __awaiter(this, void 0, void 0, function* () {
            let manifestPath = this.platformData.configurationFilePath;
            shell.sed('-i', /__PACKAGE__/, this.$projectData.projectId, manifestPath);
            shell.sed('-i', /__APILEVEL__/, this.$options.sdk || (yield this.$androidToolsInfo.getToolsInfo()).compileSdkVersion.toString(), manifestPath);
        });
    }
    getProjectNameFromId() {
        let id;
        if (this.$projectData && this.$projectData.projectId) {
            id = this.$projectData.projectId.split(".")[2];
        }
        return id;
    }
    afterCreateProject(projectRoot) {
        return null;
    }
    canUpdatePlatform(newInstalledModuleDir) {
        return true;
    }
    updatePlatform(currentVersion, newVersion, canUpdate, addPlatform, removePlatforms) {
        return __awaiter(this, void 0, void 0, function* () {
            if (semver.eq(newVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE)) {
                let platformLowercase = this.platformData.normalizedPlatformName.toLowerCase();
                yield removePlatforms([platformLowercase.split("@")[0]]);
                yield addPlatform(platformLowercase);
                return false;
            }
            return true;
        });
    }
    buildProject(projectRoot, buildConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.canUseGradle()) {
                let buildOptions = yield this.getBuildOptions();
                if (this.$logger.getLevel() === "TRACE") {
                    buildOptions.unshift("--stacktrace");
                    buildOptions.unshift("--debug");
                }
                buildOptions.unshift("buildapk");
                let gradleBin = path.join(projectRoot, "gradlew");
                if (this.$hostInfo.isWindows) {
                    gradleBin += ".bat";
                }
                yield this.spawn(gradleBin, buildOptions, { stdio: "inherit", cwd: this.platformData.projectRoot });
            }
            else {
                this.$errors.failWithoutHelp("Cannot complete build because this project is ANT-based." + os_1.EOL +
                    "Run `tns platform remove android && tns platform add android` to switch to Gradle and try again.");
            }
        });
    }
    getBuildOptions() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true });
            let androidToolsInfo = yield this.$androidToolsInfo.getToolsInfo();
            let compileSdk = androidToolsInfo.compileSdkVersion;
            let targetSdk = this.getTargetFromAndroidManifest() || compileSdk;
            let buildToolsVersion = androidToolsInfo.buildToolsVersion;
            let appCompatVersion = androidToolsInfo.supportRepositoryVersion;
            let generateTypings = androidToolsInfo.generateTypings;
            let buildOptions = [
                `-PcompileSdk=android-${compileSdk}`,
                `-PtargetSdk=${targetSdk}`,
                `-PbuildToolsVersion=${buildToolsVersion}`,
                `-PsupportVersion=${appCompatVersion}`,
                `-PgenerateTypings=${generateTypings}`
            ];
            if (this.$options.release) {
                buildOptions.push("-Prelease");
                buildOptions.push(`-PksPath=${path.resolve(this.$options.keyStorePath)}`);
                buildOptions.push(`-Palias=${this.$options.keyStoreAlias}`);
                buildOptions.push(`-Ppassword=${this.$options.keyStoreAliasPassword}`);
                buildOptions.push(`-PksPassword=${this.$options.keyStorePassword}`);
            }
            return buildOptions;
        });
    }
    buildForDeploy(projectRoot, buildConfig) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.buildProject(projectRoot, buildConfig);
        });
    }
    isPlatformPrepared(projectRoot) {
        return this.$fs.exists(path.join(this.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME));
    }
    getFrameworkFilesExtensions() {
        return [".jar", ".dat"];
    }
    prepareProject() {
        return __awaiter(this, void 0, void 0, function* () {
        });
    }
    ensureConfigurationFileInAppResources() {
        let originalAndroidManifestFilePath = path.join(this.$projectData.appResourcesDirectoryPath, this.$devicePlatformsConstants.Android, this.platformData.configurationFileName);
        let manifestExists = this.$fs.exists(originalAndroidManifestFilePath);
        if (!manifestExists) {
            this.$logger.warn('No manifest found in ' + originalAndroidManifestFilePath);
            return;
        }
        this.$fs.copyFile(originalAndroidManifestFilePath, this.platformData.configurationFilePath);
    }
    prepareAppResources(appResourcesDirectoryPath) {
        let resourcesDirPath = path.join(appResourcesDirectoryPath, this.platformData.normalizedPlatformName);
        let valuesDirRegExp = /^values/;
        let resourcesDirs = this.$fs.readDirectory(resourcesDirPath).filter(resDir => !resDir.match(valuesDirRegExp));
        _.each(resourcesDirs, resourceDir => {
            this.$fs.deleteDirectory(path.join(this.getAppResourcesDestinationDirectoryPath(), resourceDir));
        });
    }
    preparePluginNativeCode(pluginData) {
        return __awaiter(this, void 0, void 0, function* () {
            let pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME);
            yield this.processResourcesFromPlugin(pluginData, pluginPlatformsFolderPath);
        });
    }
    processConfigurationFilesFromAppResources() {
        return __awaiter(this, void 0, void 0, function* () {
            return;
        });
    }
    processResourcesFromPlugin(pluginData, pluginPlatformsFolderPath) {
        return __awaiter(this, void 0, void 0, function* () {
            let configurationsDirectoryPath = path.join(this.platformData.projectRoot, "configurations");
            this.$fs.ensureDirectoryExists(configurationsDirectoryPath);
            let pluginConfigurationDirectoryPath = path.join(configurationsDirectoryPath, pluginData.name);
            if (this.$fs.exists(pluginPlatformsFolderPath)) {
                this.$fs.ensureDirectoryExists(pluginConfigurationDirectoryPath);
                let resourcesDestinationDirectoryPath = path.join(this.platformData.projectRoot, "src", pluginData.name);
                this.$fs.ensureDirectoryExists(resourcesDestinationDirectoryPath);
                shell.cp("-Rf", path.join(pluginPlatformsFolderPath, "*"), resourcesDestinationDirectoryPath);
                const filesForInterpolation = this.$fs.enumerateFilesInDirectorySync(resourcesDestinationDirectoryPath, file => this.$fs.getFsStats(file).isDirectory() || path.extname(file) === constants.XML_FILE_EXTENSION) || [];
                for (let file of filesForInterpolation) {
                    this.$logger.trace(`Interpolate data for plugin file: ${file}`);
                    yield this.$pluginVariablesService.interpolate(pluginData, file);
                }
            }
            let includeGradleFilePath = path.join(pluginPlatformsFolderPath, "include.gradle");
            if (this.$fs.exists(includeGradleFilePath)) {
                shell.cp("-f", includeGradleFilePath, pluginConfigurationDirectoryPath);
            }
        });
    }
    removePluginNativeCode(pluginData) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                let pluginConfigDir = path.join(this.platformData.projectRoot, "configurations", pluginData.name);
                if (this.$fs.exists(pluginConfigDir)) {
                    yield this.cleanProject(this.platformData.projectRoot, []);
                }
            }
            catch (e) {
                if (e.code === "ENOENT") {
                    this.$logger.debug("No native code jars found: " + e.message);
                }
                else {
                    throw e;
                }
            }
        });
    }
    afterPrepareAllPlugins() {
        return __awaiter(this, void 0, void 0, function* () {
            return;
        });
    }
    beforePrepareAllPlugins(dependencies) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.$config.debugLivesync) {
                if (dependencies) {
                    let platformDir = path.join(this.$projectData.platformsDir, "android");
                    let buildDir = path.join(platformDir, "build-tools");
                    let checkV8dependants = path.join(buildDir, "check-v8-dependants.js");
                    if (this.$fs.exists(checkV8dependants)) {
                        let stringifiedDependencies = JSON.stringify(dependencies);
                        yield this.spawn('node', [checkV8dependants, stringifiedDependencies, this.$projectData.platformsDir], { stdio: "inherit" });
                    }
                }
                let buildOptions = yield this.getBuildOptions();
                let projectRoot = this.platformData.projectRoot;
                yield this.cleanProject(projectRoot, buildOptions);
            }
        });
    }
    cleanProject(projectRoot, options) {
        return __awaiter(this, void 0, void 0, function* () {
            options.unshift("clean");
            let gradleBin = path.join(projectRoot, "gradlew");
            if (this.$hostInfo.isWindows) {
                gradleBin += ".bat";
            }
            yield this.spawn(gradleBin, options, { stdio: "inherit", cwd: this.platformData.projectRoot });
        });
    }
    deploy(deviceIdentifier) {
        return __awaiter(this, void 0, void 0, function* () {
            let adb = this.$injector.resolve(device_android_debug_bridge_1.DeviceAndroidDebugBridge, { identifier: deviceIdentifier });
            let deviceRootPath = `/data/local/tmp/${this.$projectData.projectId}`;
            yield adb.executeShellCommand(["rm", "-rf", deviceRootPath]);
        });
    }
    canUseGradle(frameworkVersion) {
        if (!this._canUseGradle) {
            if (!frameworkVersion) {
                this.$projectDataService.initialize(this.$projectData.projectDir);
                let frameworkInfoInProjectFile = this.$projectDataService.getValue(this.platformData.frameworkPackageName);
                frameworkVersion = frameworkInfoInProjectFile && frameworkInfoInProjectFile.version;
            }
            this._canUseGradle = !frameworkVersion || semver.gte(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE);
        }
        return this._canUseGradle;
    }
    copy(projectRoot, frameworkDir, files, cpArg) {
        let paths = files.split(' ').map(p => path.join(frameworkDir, p));
        shell.cp(cpArg, paths, projectRoot);
    }
    spawn(command, args, opts) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.$childProcess.spawnFromEvent(command, args, "close", opts || { stdio: "inherit" });
        });
    }
    validatePackageName(packageName) {
        if (!/^[a-zA-Z]+(\.[a-zA-Z0-9][a-zA-Z0-9_]*)+$/.test(packageName)) {
            this.$errors.fail("Package name must look like: com.company.Name");
        }
        if (/\b[Cc]lass\b/.test(packageName)) {
            this.$errors.fail("class is a reserved word");
        }
    }
    validateProjectName(projectName) {
        if (projectName === '') {
            this.$errors.fail("Project name cannot be empty");
        }
        if (/^[0-9]/.test(projectName)) {
            this.$errors.fail("Project name must not begin with a number");
        }
    }
    getTargetFromAndroidManifest() {
        let versionInManifest;
        if (this.$fs.exists(this.platformData.configurationFilePath)) {
            let targetFromAndroidManifest = this.$fs.readText(this.platformData.configurationFilePath);
            if (targetFromAndroidManifest) {
                let match = targetFromAndroidManifest.match(/.*?android:targetSdkVersion=\"(.*?)\"/);
                if (match && match[1]) {
                    versionInManifest = match[1];
                }
            }
        }
        return versionInManifest;
    }
}
AndroidProjectService.VALUES_DIRNAME = "values";
AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX = AndroidProjectService.VALUES_DIRNAME + "-v";
AndroidProjectService.ANDROID_PLATFORM_NAME = "android";
AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE = "1.5.0";
AndroidProjectService.REQUIRED_DEV_DEPENDENCIES = [
    { name: "babel-traverse", version: "^6.4.5" },
    { name: "babel-types", version: "^6.4.5" },
    { name: "babylon", version: "^6.4.5" },
    { name: "lazy", version: "^1.0.11" }
];
exports.AndroidProjectService = AndroidProjectService;
$injector.register("androidProjectService", AndroidProjectService);
