"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;
};
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 constants_1 = require("../constants");
const helpers_1 = require("../common/helpers");
const xml2js_1 = require("xml2js");
class AndroidPluginBuildService {
    constructor($fs, $childProcess, $hostInfo, $androidToolsInfo, $logger, $packageManager, $projectDataService, $devicePlatformsConstants, $errors, $filesHashService, $hooksService, $injector) {
        this.$fs = $fs;
        this.$childProcess = $childProcess;
        this.$hostInfo = $hostInfo;
        this.$androidToolsInfo = $androidToolsInfo;
        this.$logger = $logger;
        this.$packageManager = $packageManager;
        this.$projectDataService = $projectDataService;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$errors = $errors;
        this.$filesHashService = $filesHashService;
        this.$hooksService = $hooksService;
        this.$injector = $injector;
    }
    get $platformsDataService() {
        return this.$injector.resolve("platformsDataService");
    }
    getAndroidSourceDirectories(source) {
        const directories = [constants_1.RESOURCES_DIR, "java", constants_1.ASSETS_DIR, "jniLibs"];
        const resultArr = [];
        this.$fs.enumerateFilesInDirectorySync(source, (file, stat) => {
            if (stat.isDirectory() && _.some(directories, (element) => file.endsWith(element))) {
                resultArr.push(file);
                return true;
            }
        });
        return resultArr;
    }
    getManifest(platformsDir) {
        const manifest = path.join(platformsDir, constants_1.MANIFEST_FILE_NAME);
        return this.$fs.exists(manifest) ? manifest : null;
    }
    updateManifestContent(oldManifestContent, defaultPackageName) {
        return __awaiter(this, void 0, void 0, function* () {
            let xml = yield this.getXml(oldManifestContent);
            let packageName = defaultPackageName;
            if (xml["manifest"]) {
                if (xml["manifest"]["$"]["package"]) {
                    packageName = xml["manifest"]["$"]["package"];
                }
                xml = xml["manifest"];
            }
            const newManifest = { manifest: {} };
            for (const prop in xml) {
                newManifest.manifest[prop] = xml[prop];
            }
            newManifest.manifest["$"]["package"] = packageName;
            const xmlBuilder = new xml2js_1.Builder();
            const newManifestContent = xmlBuilder.buildObject(newManifest);
            return newManifestContent;
        });
    }
    createManifestContent(packageName) {
        const newManifest = { manifest: AndroidPluginBuildService.MANIFEST_ROOT };
        newManifest.manifest["$"]["package"] = packageName;
        const xmlBuilder = new xml2js_1.Builder();
        const newManifestContent = xmlBuilder.buildObject(newManifest);
        return newManifestContent;
    }
    getXml(stringContent) {
        return __awaiter(this, void 0, void 0, function* () {
            const promise = new Promise((resolve, reject) => xml2js_1.parseString(stringContent, (err, result) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(result);
                }
            }));
            return promise;
        });
    }
    getIncludeGradleCompileDependenciesScope(includeGradleFileContent) {
        const indexOfDependenciesScope = includeGradleFileContent.indexOf("dependencies");
        const result = [];
        if (indexOfDependenciesScope === -1) {
            return result;
        }
        const indexOfRepositoriesScope = includeGradleFileContent.indexOf("repositories");
        let repositoriesScope = "";
        if (indexOfRepositoriesScope >= 0) {
            repositoriesScope = this.getScope("repositories", includeGradleFileContent);
            result.push(repositoriesScope);
        }
        const dependenciesScope = this.getScope("dependencies", includeGradleFileContent);
        result.push(dependenciesScope);
        return result;
    }
    getScope(scopeName, content) {
        const indexOfScopeName = content.indexOf(scopeName);
        const openingBracket = "{";
        const closingBracket = "}";
        let foundFirstBracket = false;
        let openBrackets = 0;
        let result = "";
        let i = indexOfScopeName;
        while (i !== -1 && i < content.length) {
            const currCharacter = content[i];
            if (currCharacter === openingBracket) {
                if (openBrackets === 0) {
                    foundFirstBracket = true;
                }
                openBrackets++;
            }
            if (currCharacter === closingBracket) {
                openBrackets--;
            }
            result += currCharacter;
            if (openBrackets === 0 && foundFirstBracket) {
                break;
            }
            i++;
        }
        return result;
    }
    buildAar(options) {
        return __awaiter(this, void 0, void 0, function* () {
            this.validateOptions(options);
            const manifestFilePath = this.getManifest(options.platformsAndroidDirPath);
            const androidSourceDirectories = this.getAndroidSourceDirectories(options.platformsAndroidDirPath);
            const shortPluginName = helpers_1.getShortPluginName(options.pluginName);
            const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName);
            const pluginSourceFileHashesInfo = yield this.getSourceFilesHashes(options.platformsAndroidDirPath, shortPluginName);
            const shouldBuildAar = yield this.shouldBuildAar({
                manifestFilePath,
                androidSourceDirectories,
                pluginTempDir,
                pluginSourceDir: options.platformsAndroidDirPath,
                shortPluginName,
                fileHashesInfo: pluginSourceFileHashesInfo
            });
            if (shouldBuildAar) {
                this.cleanPluginDir(pluginTempDir);
                const pluginTempMainSrcDir = path.join(pluginTempDir, "src", "main");
                yield this.updateManifest(manifestFilePath, pluginTempMainSrcDir, shortPluginName);
                this.copySourceSetDirectories(androidSourceDirectories, pluginTempMainSrcDir);
                yield this.setupGradle(pluginTempDir, options.platformsAndroidDirPath, options.projectDir);
                yield this.buildPlugin({ pluginDir: pluginTempDir, pluginName: options.pluginName });
                this.copyAar(shortPluginName, pluginTempDir, options.aarOutputDir);
                this.writePluginHashInfo(pluginSourceFileHashesInfo, pluginTempDir);
            }
            return shouldBuildAar;
        });
    }
    cleanPluginDir(pluginTempDir) {
        this.$fs.deleteDirectory(pluginTempDir);
        this.$fs.ensureDirectoryExists(pluginTempDir);
    }
    getSourceFilesHashes(pluginTempPlatformsAndroidDir, shortPluginName) {
        const pathToAar = path.join(pluginTempPlatformsAndroidDir, `${shortPluginName}.aar`);
        const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginTempPlatformsAndroidDir, (file, stat) => file !== pathToAar);
        return this.$filesHashService.generateHashes(pluginNativeDataFiles);
    }
    writePluginHashInfo(fileHashesInfo, pluginTempDir) {
        const buildDataFile = this.getPathToPluginBuildDataFile(pluginTempDir);
        this.$fs.writeJson(buildDataFile, fileHashesInfo);
    }
    shouldBuildAar(opts) {
        return __awaiter(this, void 0, void 0, function* () {
            let shouldBuildAar = !!opts.manifestFilePath || !!opts.androidSourceDirectories.length;
            if (shouldBuildAar &&
                this.$fs.exists(opts.pluginTempDir) &&
                this.$fs.exists(path.join(opts.pluginSourceDir, `${opts.shortPluginName}.aar`))) {
                const buildDataFile = this.getPathToPluginBuildDataFile(opts.pluginTempDir);
                if (this.$fs.exists(buildDataFile)) {
                    const oldHashes = this.$fs.readJson(buildDataFile);
                    shouldBuildAar = this.$filesHashService.hasChangesInShasums(oldHashes, opts.fileHashesInfo);
                }
            }
            return shouldBuildAar;
        });
    }
    getPathToPluginBuildDataFile(pluginDir) {
        return path.join(pluginDir, constants_1.PLUGIN_BUILD_DATA_FILENAME);
    }
    updateManifest(manifestFilePath, pluginTempMainSrcDir, shortPluginName) {
        return __awaiter(this, void 0, void 0, function* () {
            let updatedManifestContent;
            this.$fs.ensureDirectoryExists(pluginTempMainSrcDir);
            const defaultPackageName = "org.nativescript." + shortPluginName;
            if (manifestFilePath) {
                let androidManifestContent;
                try {
                    androidManifestContent = this.$fs.readText(manifestFilePath);
                }
                catch (err) {
                    this.$errors.failWithoutHelp(`Failed to fs.readFileSync the manifest file located at ${manifestFilePath}. Error is: ${err.toString()}`);
                }
                updatedManifestContent = yield this.updateManifestContent(androidManifestContent, defaultPackageName);
            }
            else {
                updatedManifestContent = this.createManifestContent(defaultPackageName);
            }
            const pathToTempAndroidManifest = path.join(pluginTempMainSrcDir, constants_1.MANIFEST_FILE_NAME);
            try {
                this.$fs.writeFile(pathToTempAndroidManifest, updatedManifestContent);
            }
            catch (e) {
                this.$errors.failWithoutHelp(`Failed to write the updated AndroidManifest in the new location - ${pathToTempAndroidManifest}. Error is: ${e.toString()}`);
            }
        });
    }
    copySourceSetDirectories(androidSourceSetDirectories, pluginTempMainSrcDir) {
        for (const dir of androidSourceSetDirectories) {
            const dirName = path.basename(dir);
            const destination = path.join(pluginTempMainSrcDir, dirName);
            this.$fs.ensureDirectoryExists(destination);
            this.$fs.copyFile(path.join(dir, "*"), destination);
        }
    }
    setupGradle(pluginTempDir, platformsAndroidDirPath, projectDir) {
        return __awaiter(this, void 0, void 0, function* () {
            const gradleTemplatePath = path.resolve(path.join(__dirname, "../../vendor/gradle-plugin"));
            const allGradleTemplateFiles = path.join(gradleTemplatePath, "*");
            const buildGradlePath = path.join(pluginTempDir, "build.gradle");
            this.$fs.copyFile(allGradleTemplateFiles, pluginTempDir);
            this.addCompileDependencies(platformsAndroidDirPath, buildGradlePath);
            const runtimeGradleVersions = yield this.getRuntimeGradleVersions(projectDir);
            this.replaceGradleVersion(pluginTempDir, runtimeGradleVersions.gradleVersion);
            this.replaceGradleAndroidPluginVersion(buildGradlePath, runtimeGradleVersions.gradleAndroidPluginVersion);
        });
    }
    getRuntimeGradleVersions(projectDir) {
        return __awaiter(this, void 0, void 0, function* () {
            let runtimeGradleVersions = null;
            if (projectDir) {
                const projectData = this.$projectDataService.getProjectData(projectDir);
                const platformData = this.$platformsDataService.getPlatformData(this.$devicePlatformsConstants.Android, projectData);
                const projectRuntimeVersion = platformData.platformProjectService.getFrameworkVersion(projectData);
                this.$logger.trace(`Got gradle versions ${JSON.stringify(runtimeGradleVersions)} from runtime v${projectRuntimeVersion}`);
            }
            if (!runtimeGradleVersions) {
                const latestRuntimeVersion = yield this.getLatestRuntimeVersion();
                runtimeGradleVersions = yield this.getGradleVersions(latestRuntimeVersion);
                this.$logger.trace(`Got gradle versions ${JSON.stringify(runtimeGradleVersions)} from the latest runtime v${latestRuntimeVersion}`);
            }
            return runtimeGradleVersions || {};
        });
    }
    getLatestRuntimeVersion() {
        return __awaiter(this, void 0, void 0, function* () {
            let runtimeVersion = null;
            try {
                const result = yield this.$packageManager.view(constants_1.TNS_ANDROID_RUNTIME_NAME, { "dist-tags": true });
                runtimeVersion = result.latest;
            }
            catch (err) {
                this.$logger.trace(`Error while getting latest android runtime version from view command: ${err}`);
                const registryData = yield this.$packageManager.getRegistryPackageData(constants_1.TNS_ANDROID_RUNTIME_NAME);
                runtimeVersion = registryData["dist-tags"].latest;
            }
            return runtimeVersion;
        });
    }
    getGradleVersions(runtimeVersion) {
        return __awaiter(this, void 0, void 0, function* () {
            let runtimeGradleVersions = null;
            try {
                const output = yield this.$packageManager.view(`${constants_1.TNS_ANDROID_RUNTIME_NAME}@${runtimeVersion}`, { gradle: true });
                runtimeGradleVersions = { gradle: output };
            }
            catch (err) {
                this.$logger.trace(`Error while getting gradle data for android runtime from view command: ${err}`);
                const registryData = yield this.$packageManager.getRegistryPackageData(constants_1.TNS_ANDROID_RUNTIME_NAME);
                runtimeGradleVersions = registryData.versions[runtimeVersion];
            }
            const result = this.getGradleVersionsCore(runtimeGradleVersions);
            return result;
        });
    }
    getGradleVersionsCore(packageData) {
        const packageJsonGradle = packageData && packageData.gradle;
        let runtimeVersions = null;
        if (packageJsonGradle && (packageJsonGradle.version || packageJsonGradle.android)) {
            runtimeVersions = {};
            runtimeVersions.gradleVersion = packageJsonGradle.version;
            runtimeVersions.gradleAndroidPluginVersion = packageJsonGradle.android;
        }
        return runtimeVersions;
    }
    replaceGradleVersion(pluginTempDir, version) {
        const gradleVersion = version || constants_1.AndroidBuildDefaults.GradleVersion;
        const gradleVersionPlaceholder = "{{runtimeGradleVersion}}";
        const gradleWrapperPropertiesPath = path.join(pluginTempDir, "gradle", "wrapper", "gradle-wrapper.properties");
        this.replaceFileContent(gradleWrapperPropertiesPath, gradleVersionPlaceholder, gradleVersion);
    }
    replaceGradleAndroidPluginVersion(buildGradlePath, version) {
        const gradleAndroidPluginVersionPlaceholder = "{{runtimeAndroidPluginVersion}}";
        const gradleAndroidPluginVersion = version || constants_1.AndroidBuildDefaults.GradleAndroidPluginVersion;
        this.replaceFileContent(buildGradlePath, gradleAndroidPluginVersionPlaceholder, gradleAndroidPluginVersion);
    }
    replaceFileContent(filePath, content, replacement) {
        const fileContent = this.$fs.readText(filePath);
        const contentRegex = new RegExp(content, "g");
        const replacedFileContent = fileContent.replace(contentRegex, replacement);
        this.$fs.writeFile(filePath, replacedFileContent);
    }
    addCompileDependencies(platformsAndroidDirPath, buildGradlePath) {
        const includeGradlePath = path.join(platformsAndroidDirPath, constants_1.INCLUDE_GRADLE_NAME);
        if (this.$fs.exists(includeGradlePath)) {
            const includeGradleContent = this.$fs.readText(includeGradlePath);
            const compileDependencies = this.getIncludeGradleCompileDependenciesScope(includeGradleContent);
            if (compileDependencies.length) {
                this.$fs.appendFile(buildGradlePath, "\n" + compileDependencies.join("\n"));
            }
        }
    }
    copyAar(shortPluginName, pluginTempDir, aarOutputDir) {
        const finalAarName = `${shortPluginName}-release.aar`;
        const pathToBuiltAar = path.join(pluginTempDir, "build", "outputs", "aar", finalAarName);
        if (this.$fs.exists(pathToBuiltAar)) {
            try {
                if (aarOutputDir) {
                    this.$fs.copyFile(pathToBuiltAar, path.join(aarOutputDir, `${shortPluginName}.aar`));
                }
            }
            catch (e) {
                this.$errors.failWithoutHelp(`Failed to copy built aar to destination. ${e.message}`);
            }
        }
        else {
            this.$errors.failWithoutHelp(`No built aar found at ${pathToBuiltAar}`);
        }
    }
    migrateIncludeGradle(options) {
        this.validatePlatformsAndroidDirPathOption(options);
        const includeGradleFilePath = path.join(options.platformsAndroidDirPath, constants_1.INCLUDE_GRADLE_NAME);
        if (this.$fs.exists(includeGradleFilePath)) {
            let includeGradleFileContent;
            try {
                includeGradleFileContent = this.$fs.readFile(includeGradleFilePath).toString();
            }
            catch (err) {
                this.$errors.failWithoutHelp(`Failed to fs.readFileSync the include.gradle file located at ${includeGradleFilePath}. Error is: ${err.toString()}`);
            }
            const productFlavorsScope = this.getScope("productFlavors", includeGradleFileContent);
            if (productFlavorsScope) {
                try {
                    const newIncludeGradleFileContent = includeGradleFileContent.replace(productFlavorsScope, "");
                    this.$fs.writeFile(includeGradleFilePath, newIncludeGradleFileContent);
                    return true;
                }
                catch (e) {
                    this.$errors.failWithoutHelp(`Failed to write the updated include.gradle ` +
                        `in - ${includeGradleFilePath}. Error is: ${e.toString()}`);
                }
            }
        }
        return false;
    }
    buildPlugin(pluginBuildSettings) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!pluginBuildSettings.androidToolsInfo) {
                this.$androidToolsInfo.validateInfo({ showWarningsAsErrors: true, validateTargetSdk: true });
                pluginBuildSettings.androidToolsInfo = this.$androidToolsInfo.getToolsInfo();
            }
            const gradlew = this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew";
            const localArgs = [
                "-p",
                pluginBuildSettings.pluginDir,
                "assembleRelease",
                `-PcompileSdk=android-${pluginBuildSettings.androidToolsInfo.compileSdkVersion}`,
                `-PbuildToolsVersion=${pluginBuildSettings.androidToolsInfo.buildToolsVersion}`
            ];
            if (this.$logger.getLevel() === "INFO") {
                localArgs.push("--quiet");
            }
            try {
                yield this.$childProcess.spawnFromEvent(gradlew, localArgs, "close", { cwd: pluginBuildSettings.pluginDir, stdio: "inherit" });
            }
            catch (err) {
                this.$errors.failWithoutHelp(`Failed to build plugin ${pluginBuildSettings.pluginName} : \n${err}`);
            }
        });
    }
    validateOptions(options) {
        if (!options) {
            this.$errors.failWithoutHelp("Android plugin cannot be built without passing an 'options' object.");
        }
        if (!options.pluginName) {
            this.$logger.info("No plugin name provided, defaulting to 'myPlugin'.");
        }
        if (!options.aarOutputDir) {
            this.$logger.info("No aarOutputDir provided, defaulting to the build outputs directory of the plugin");
        }
        if (!options.tempPluginDirPath) {
            this.$errors.failWithoutHelp("Android plugin cannot be built without passing the path to a directory where the temporary project should be built.");
        }
        this.validatePlatformsAndroidDirPathOption(options);
    }
    validatePlatformsAndroidDirPathOption(options) {
        if (!options) {
            this.$errors.failWithoutHelp("Android plugin cannot be built without passing an 'options' object.");
        }
        if (!options.platformsAndroidDirPath) {
            this.$errors.failWithoutHelp("Android plugin cannot be built without passing the path to the platforms/android dir.");
        }
    }
}
AndroidPluginBuildService.MANIFEST_ROOT = {
    $: {
        "xmlns:android": "http://schemas.android.com/apk/res/android"
    }
};
__decorate([
    helpers_1.hook("buildAndroidPlugin")
], AndroidPluginBuildService.prototype, "buildPlugin", null);
exports.AndroidPluginBuildService = AndroidPluginBuildService;
$injector.register("androidPluginBuildService", AndroidPluginBuildService);
