"use strict";
const Fiber = require("fibers");
const Future = require("fibers/future");
const iconv = require("iconv-lite");
const os_1 = require("os");
const osenv = require("osenv");
const path = require("path");
const helpers = require("../../helpers");
const net = require("net");
const device_android_debug_bridge_1 = require("./device-android-debug-bridge");
class VirtualMachine {
    constructor(name, identifier) {
        this.name = name;
        this.identifier = identifier;
    }
}
class AndroidEmulatorServices {
    constructor($logger, $emulatorSettingsService, $errors, $childProcess, $fs, $staticConfig, $devicePlatformsConstants, $logcatHelper, $options, $utils, $injector) {
        this.$logger = $logger;
        this.$emulatorSettingsService = $emulatorSettingsService;
        this.$errors = $errors;
        this.$childProcess = $childProcess;
        this.$fs = $fs;
        this.$staticConfig = $staticConfig;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$logcatHelper = $logcatHelper;
        this.$options = $options;
        this.$utils = $utils;
        this.$injector = $injector;
        iconv.extendNodeEncodings();
        this.adbFilePath = this.$staticConfig.getAdbFilePath().wait();
    }
    get pathToEmulatorExecutable() {
        if (!this._pathToEmulatorExecutable) {
            let androidHome = process.env.ANDROID_HOME;
            let emulatorExecutableName = "emulator";
            this._pathToEmulatorExecutable = androidHome ? path.join(androidHome, "tools", emulatorExecutableName) : emulatorExecutableName;
        }
        return this._pathToEmulatorExecutable;
    }
    getEmulatorId() {
        return (() => {
            let image = this.getEmulatorImage().wait();
            if (!image) {
                this.$errors.fail("Could not find an emulator image to run your project.");
            }
            let emulatorId = this.startEmulatorInstance(image).wait();
            return emulatorId;
        }).future()();
    }
    checkDependencies() {
        return (() => {
            this.checkAndroidSDKConfiguration().wait();
            if (this.$options.geny) {
                this.checkGenymotionConfiguration().wait();
            }
        }).future()();
    }
    checkAvailability() {
        return (() => {
            if (!this.getEmulatorImage().wait()) {
                this.$errors.failWithoutHelp("You do not have any Android emulators installed. Please install at least one.");
            }
            let platform = this.$devicePlatformsConstants.Android;
            if (!this.$emulatorSettingsService.canStart(platform).wait()) {
                this.$errors.fail("The current project does not target Android and cannot be run in the Android emulator.");
            }
        }).future()();
    }
    startEmulator() {
        return (() => {
            if (this.$options.avd && this.$options.geny) {
                this.$errors.fail("You cannot specify both --avd and --geny options. Please use only one of them.");
            }
            let emulatorId = null;
            let image = this.getEmulatorImage().wait();
            if (image) {
                emulatorId = this.startEmulatorInstance(image).wait();
                this.waitForEmulatorBootToComplete(emulatorId).wait();
                this.unlockScreen(emulatorId).wait();
            }
            else {
                this.$errors.fail("Could not find an emulator image to run your project.");
            }
            return emulatorId;
        }).future()();
    }
    runApplicationOnEmulator(app, emulatorOptions) {
        return (() => {
            let emulatorId = this.startEmulator().wait();
            this.runApplicationOnEmulatorCore(app, emulatorOptions.appId, emulatorId).wait();
        }).future()();
    }
    checkAndroidSDKConfiguration() {
        return (() => {
            try {
                this.$childProcess.tryExecuteApplication(this.pathToEmulatorExecutable, ['-help'], "exit", AndroidEmulatorServices.MISSING_SDK_MESSAGE).wait();
            }
            catch (err) {
                this.$logger.trace(`Error while checking Android SDK configuration: ${err}`);
                this.$errors.failWithoutHelp("Android SDK is not configured properly. Make sure you have added tools and platform-tools to your PATH environment variable.");
            }
        }).future()();
    }
    getDeviceAndroidDebugBridge(deviceIdentifier) {
        return this.$injector.resolve(device_android_debug_bridge_1.DeviceAndroidDebugBridge, { identifier: deviceIdentifier });
    }
    checkGenymotionConfiguration() {
        return (() => {
            try {
                let condition = (childProcess) => childProcess.stderr && !_.startsWith(childProcess.stderr, "Usage:");
                this.$childProcess.tryExecuteApplication("player", [], "exit", AndroidEmulatorServices.MISSING_GENYMOTION_MESSAGE, condition).wait();
            }
            catch (err) {
                this.$logger.trace(`Error while checking Genymotion configuration: ${err}`);
                this.$errors.failWithoutHelp("Genymotion is not configured properly. Make sure you have added its installation directory to your PATH environment variable.");
            }
        }).future()();
    }
    getEmulatorImage() {
        return (() => {
            let image = this.$options.avd || this.$options.geny || this.getBestFit().wait();
            return image;
        }).future()();
    }
    runApplicationOnEmulatorCore(app, appId, emulatorId) {
        return (() => {
            this.$logger.info("installing %s through adb", app);
            let adb = this.getDeviceAndroidDebugBridge(emulatorId);
            let childProcess = adb.executeCommand(["install", "-r", app], { returnChildProcess: true }).wait();
            this.$fs.futureFromEvent(childProcess, "close").wait();
            this.unlockScreen(emulatorId).wait();
            this.$logger.info("running %s through adb", app);
            let androidDebugBridgeCommandOptions = {
                childProcessOptions: { stdio: "ignore", detached: true },
                returnChildProcess: true
            };
            childProcess = adb.executeShellCommand(["monkey", "-p", appId, "-c", "android.intent.category.LAUNCHER", "1"], androidDebugBridgeCommandOptions).wait();
            this.$fs.futureFromEvent(childProcess, "close").wait();
            if (!this.$options.justlaunch) {
                this.$logcatHelper.start(emulatorId);
            }
        }).future()();
    }
    unlockScreen(emulatorId) {
        let adb = this.getDeviceAndroidDebugBridge(emulatorId);
        let childProcess = adb.executeShellCommand(["input", "keyevent", "82"], { returnChildProcess: true }).wait();
        return this.$fs.futureFromEvent(childProcess, "close");
    }
    sleep(ms) {
        let fiber = Fiber.current;
        setTimeout(() => fiber.run(), ms);
        Fiber.yield();
    }
    getRunningEmulatorId(image) {
        return (() => {
            let runningEmulators = this.getRunningEmulators().wait();
            if (runningEmulators.length === 0) {
                return "";
            }
            let getNameFunction = this.$options.geny ? this.getNameFromGenymotionEmulatorId : this.getNameFromSDKEmulatorId;
            let emulatorId = _(runningEmulators).find(emulator => getNameFunction.apply(this, [emulator]).wait() === image);
            return emulatorId;
        }).future()();
    }
    getNameFromGenymotionEmulatorId(emulatorId) {
        return (() => {
            let modelOutputLines = this.$childProcess.execFile(this.adbFilePath, ["-s", emulatorId, "shell", "getprop", "ro.product.model"]).wait();
            this.$logger.trace(modelOutputLines);
            let model = _.first(modelOutputLines.split(os_1.EOL)).trim();
            return model;
        }).future()();
    }
    getNameFromSDKEmulatorId(emulatorId) {
        let match = emulatorId.match(/^emulator-(\d+)/);
        let portNumber;
        if (match && match[1]) {
            portNumber = match[1];
        }
        else {
            return Future.fromResult("");
        }
        let future = new Future();
        let output = "";
        let client = net.connect(portNumber, () => {
            client.write(`avd name${os_1.EOL}`);
        });
        client.on('data', (data) => {
            output += data.toString();
            let name = this.getEmulatorNameFromClientOutput(output);
            if (name && !future.isResolved()) {
                future.return(name);
            }
            client.end();
        });
        return future;
    }
    getEmulatorNameFromClientOutput(output) {
        let lines = _.map(output.split(os_1.EOL), (line) => line.trim());
        let name;
        let firstIndexOfOk = _.indexOf(lines, "OK");
        if (firstIndexOfOk < 0) {
            return null;
        }
        let secondIndexOfOk = _.indexOf(lines, "OK", firstIndexOfOk + 1);
        if (secondIndexOfOk < 0) {
            return null;
        }
        name = lines[secondIndexOfOk - 1].trim();
        return name;
    }
    startEmulatorInstance(image) {
        return (() => {
            let emulatorId = this.getRunningEmulatorId(image).wait();
            this.endTimeEpoch = helpers.getCurrentEpochTime() + this.$utils.getMilliSecondsTimeout(AndroidEmulatorServices.TIMEOUT_SECONDS);
            if (emulatorId) {
                return emulatorId;
            }
            this.$logger.info("Starting Android emulator with image %s", image);
            if (this.$options.geny) {
                this.$childProcess.spawn("player", ["--vm-name", image], { stdio: "ignore", detached: true }).unref();
            }
            else {
                this.$childProcess.spawn(this.pathToEmulatorExecutable, ['-avd', image], { stdio: "ignore", detached: true }).unref();
            }
            let isInfiniteWait = this.$utils.getMilliSecondsTimeout(AndroidEmulatorServices.TIMEOUT_SECONDS) === 0;
            let hasTimeLeft = helpers.getCurrentEpochTime() < this.endTimeEpoch;
            while (hasTimeLeft || isInfiniteWait) {
                emulatorId = this.getRunningEmulatorId(image).wait();
                if (emulatorId) {
                    return emulatorId;
                }
                this.sleep(10000);
                hasTimeLeft = helpers.getCurrentEpochTime() < this.endTimeEpoch;
            }
            if (!hasTimeLeft && !isInfiniteWait) {
                this.$errors.fail(AndroidEmulatorServices.UNABLE_TO_START_EMULATOR_MESSAGE);
            }
            return emulatorId;
        }).future()();
    }
    getRunningGenymotionEmulators(adbDevicesOutput) {
        return (() => {
            let futures = (_(adbDevicesOutput).filter(r => !r.match(AndroidEmulatorServices.RUNNING_ANDROID_EMULATOR_REGEX))
                .map(row => {
                let match = row.match(/^(.+?)\s+device$/);
                if (match && match[1]) {
                    let emulatorId = match[1];
                    return Future.fromResult(this.isGenymotionEmulator(emulatorId).wait() ? emulatorId : undefined);
                }
                return Future.fromResult(undefined);
            }).value());
            Future.wait(futures);
            return _(futures).filter(future => !!future.get())
                .map(f => f.get().toString())
                .value();
        }).future()();
    }
    getRunningAvdEmulators(adbDevicesOutput) {
        return (() => {
            let emulatorDevices = [];
            _.each(adbDevicesOutput, (device) => {
                let rx = device.match(AndroidEmulatorServices.RUNNING_ANDROID_EMULATOR_REGEX);
                if (rx && rx[1]) {
                    emulatorDevices.push(rx[1]);
                }
            });
            return emulatorDevices;
        }).future()();
    }
    isGenymotionEmulator(emulatorId) {
        return (() => {
            let manufacturer = this.$childProcess.execFile(this.adbFilePath, ["-s", emulatorId, "shell", "getprop", "ro.product.manufacturer"]).wait();
            if (manufacturer.match(/^Genymotion/i)) {
                return true;
            }
            let buildProduct = this.$childProcess.execFile(this.adbFilePath, ["-s", emulatorId, "shell", "getprop", "ro.build.product"]).wait();
            if (buildProduct && _.includes(buildProduct.toLowerCase(), "vbox")) {
                return true;
            }
            return false;
        }).future()();
    }
    getAllRunningEmulators() {
        return (() => {
            let outputRaw = this.$childProcess.execFile(this.adbFilePath, ['devices']).wait().split(os_1.EOL);
            let emulators = this.getRunningAvdEmulators(outputRaw).wait().concat(this.getRunningGenymotionEmulators(outputRaw).wait());
            return emulators;
        }).future()();
    }
    getRunningEmulators() {
        return (() => {
            let outputRaw = this.$childProcess.execFile(this.adbFilePath, ['devices']).wait().split(os_1.EOL);
            if (this.$options.geny) {
                return this.getRunningGenymotionEmulators(outputRaw).wait();
            }
            else {
                return this.getRunningAvdEmulators(outputRaw).wait();
            }
        }).future()();
    }
    getBestFit() {
        return (() => {
            let minVersion = this.$emulatorSettingsService.minVersion;
            let best = _(this.getAvds().wait())
                .map(avd => this.getInfoFromAvd(avd).wait())
                .maxBy(avd => avd.targetNum);
            return (best && best.targetNum >= minVersion) ? best.name : null;
        }).future()();
    }
    getInfoFromAvd(avdName) {
        return (() => {
            let iniFile = path.join(this.avdDir, avdName + ".ini");
            let avdInfo = this.parseAvdFile(avdName, iniFile).wait();
            if (avdInfo.path && this.$fs.exists(avdInfo.path).wait()) {
                iniFile = path.join(avdInfo.path, "config.ini");
                avdInfo = this.parseAvdFile(avdName, iniFile, avdInfo).wait();
            }
            return avdInfo;
        }).future()();
    }
    parseAvdFile(avdName, avdFileName, avdInfo) {
        return (() => {
            if (!this.$fs.exists(avdFileName).wait()) {
                return null;
            }
            let encoding = this.getAvdEncoding(avdFileName).wait();
            let contents = this.$fs.readText(avdFileName, encoding).wait().split("\n");
            avdInfo = _.reduce(contents, (result, line) => {
                let parsedLine = line.split("=");
                let key = parsedLine[0];
                switch (key) {
                    case "target":
                        result.target = parsedLine[1];
                        result.targetNum = this.readTargetNum(result.target);
                        break;
                    case "path":
                        result.path = parsedLine[1];
                        break;
                    case "hw.device.name":
                        result.device = parsedLine[1];
                        break;
                    case "abi.type":
                        result.abi = parsedLine[1];
                        break;
                    case "skin.name":
                        result.skin = parsedLine[1];
                        break;
                    case "sdcard.size":
                        result.sdcard = parsedLine[1];
                        break;
                }
                return result;
            }, avdInfo || Object.create(null));
            avdInfo.name = avdName;
            return avdInfo;
        }).future()();
    }
    readTargetNum(target) {
        let platform = target.replace('android-', '');
        let platformNumber = +platform;
        if (isNaN(platformNumber)) {
            let googlePlatform = target.split(":")[2];
            if (googlePlatform) {
                platformNumber = +googlePlatform;
            }
            else if (platform === "L") {
                platformNumber = 20;
            }
            else if (platform === "MNC") {
                platformNumber = 22;
            }
        }
        return platformNumber;
    }
    getAvdEncoding(avdName) {
        return (() => {
            let encoding = "utf8";
            let contents = this.$fs.readText(avdName, "ascii").wait();
            if (contents.length > 0) {
                contents = contents.split("\n", 1)[0];
                if (contents.length > 0) {
                    let matches = contents.match(AndroidEmulatorServices.ENCODING_MASK);
                    if (matches) {
                        encoding = matches[1];
                    }
                }
            }
            return encoding;
        }).future()();
    }
    get androidHomeDir() {
        return path.join(osenv.home(), AndroidEmulatorServices.ANDROID_DIR_NAME);
    }
    get avdDir() {
        return path.join(this.androidHomeDir, AndroidEmulatorServices.AVD_DIR_NAME);
    }
    getAvds() {
        return (() => {
            let result = [];
            if (this.$fs.exists(this.avdDir).wait()) {
                let entries = this.$fs.readDirectory(this.avdDir).wait();
                result = _.filter(entries, (e) => e.match(AndroidEmulatorServices.INI_FILES_MASK) !== null)
                    .map((e) => e.match(AndroidEmulatorServices.INI_FILES_MASK)[1]);
            }
            return result;
        }).future()();
    }
    waitForEmulatorBootToComplete(emulatorId) {
        return (() => {
            this.$logger.printInfoMessageOnSameLine("Waiting for emulator device initialization...");
            let isInfiniteWait = this.$utils.getMilliSecondsTimeout(AndroidEmulatorServices.TIMEOUT_SECONDS) === 0;
            while (helpers.getCurrentEpochTime() < this.endTimeEpoch || isInfiniteWait) {
                let isEmulatorBootCompleted = this.isEmulatorBootCompleted(emulatorId).wait();
                if (isEmulatorBootCompleted) {
                    this.$logger.printInfoMessageOnSameLine(os_1.EOL);
                    return;
                }
                this.$logger.printInfoMessageOnSameLine(".");
                this.sleep(10000);
            }
            this.$logger.printInfoMessageOnSameLine(os_1.EOL);
            this.$errors.fail(AndroidEmulatorServices.UNABLE_TO_START_EMULATOR_MESSAGE);
        }).future()();
    }
    isEmulatorBootCompleted(emulatorId) {
        return (() => {
            let output = this.$childProcess.execFile(this.adbFilePath, ["-s", emulatorId, "shell", "getprop", "dev.bootcomplete"]).wait();
            let matches = output.match("1");
            return matches && matches.length > 0;
        }).future()();
    }
}
AndroidEmulatorServices.ANDROID_DIR_NAME = ".android";
AndroidEmulatorServices.AVD_DIR_NAME = "avd";
AndroidEmulatorServices.INI_FILES_MASK = /^(.*)\.ini$/i;
AndroidEmulatorServices.ENCODING_MASK = /^avd\.ini\.encoding=(.*)$/;
AndroidEmulatorServices.TIMEOUT_SECONDS = 120;
AndroidEmulatorServices.UNABLE_TO_START_EMULATOR_MESSAGE = "Cannot run your app in the native emulator. Increase the timeout of the operation with the --timeout option or try to restart your adb server with 'adb kill-server' command. Alternatively, run the Android Virtual Device manager and increase the allocated RAM for the virtual device.";
AndroidEmulatorServices.RUNNING_ANDROID_EMULATOR_REGEX = /^(emulator-\d+)\s+device$/;
AndroidEmulatorServices.MISSING_SDK_MESSAGE = "The Android SDK is not configured properly. " +
    "Verify that you have installed the Android SDK and that you have configured it as described in System Requirements.";
AndroidEmulatorServices.MISSING_GENYMOTION_MESSAGE = "Genymotion is not configured properly. " +
    "Verify that you have installed Genymotion and that you have added its installation directory to your PATH environment variable.";
$injector.register("androidEmulatorServices", AndroidEmulatorServices);
