"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;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AndroidProcessService = void 0;
const os_1 = require("os");
const device_android_debug_bridge_1 = require("../android/device-android-debug-bridge");
const constants_1 = require("../../constants");
const decorators_1 = require("../../decorators");
const yok_1 = require("../../yok");
const _ = require("lodash");
class AndroidProcessService {
    constructor($errors, $cleanupService, $injector, $net, $staticConfig) {
        this.$errors = $errors;
        this.$cleanupService = $cleanupService;
        this.$injector = $injector;
        this.$net = $net;
        this.$staticConfig = $staticConfig;
        this._devicesAdbs = {};
        this._forwardedLocalPorts = {};
    }
    async forwardFreeTcpToAbstractPort(portForwardInputData) {
        const adb = await this.setupForPortForwarding(portForwardInputData);
        return this.forwardPort(portForwardInputData, adb);
    }
    async mapAbstractToTcpPort(deviceIdentifier, appIdentifier, framework) {
        const adb = await this.setupForPortForwarding({
            deviceIdentifier,
            appIdentifier,
        });
        const processId = (await this.getProcessIds(adb, [appIdentifier]))[appIdentifier];
        const applicationNotStartedErrorMessage = `The application is not started on the device with identifier ${deviceIdentifier}.`;
        if (!processId) {
            this.$errors.fail(applicationNotStartedErrorMessage);
        }
        const abstractPortsInformation = await this.getAbstractPortsInformation(adb);
        const abstractPort = await this.getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation, framework);
        if (!abstractPort) {
            this.$errors.fail(applicationNotStartedErrorMessage);
        }
        const forwardedTcpPort = await this.forwardPort({
            deviceIdentifier,
            appIdentifier,
            abstractPort: `localabstract:${abstractPort}`,
        }, adb);
        return forwardedTcpPort && forwardedTcpPort.toString();
    }
    async getMappedAbstractToTcpPorts(deviceIdentifier, appIdentifiers, framework) {
        const adb = this.getAdb(deviceIdentifier), abstractPortsInformation = await this.getAbstractPortsInformation(adb), processIds = await this.getProcessIds(adb, appIdentifiers), adbForwardList = await adb.executeCommand(["forward", "--list"]), localPorts = {};
        await Promise.all(_.map(appIdentifiers, async (appIdentifier) => {
            localPorts[appIdentifier] = null;
            const processId = processIds[appIdentifier];
            if (!processId) {
                return;
            }
            const abstractPort = await this.getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation, framework);
            if (!abstractPort) {
                return;
            }
            const localPort = await this.getAlreadyMappedPort(adb, deviceIdentifier, abstractPort, adbForwardList);
            if (localPort) {
                localPorts[appIdentifier] = localPort;
            }
        }));
        return localPorts;
    }
    async getDebuggableApps(deviceIdentifier) {
        const adb = this.getAdb(deviceIdentifier);
        const androidWebViewPortInformation = (await this.getAbstractPortsInformation(adb)).split(os_1.EOL);
        const portInformation = await Promise.all(_.map(androidWebViewPortInformation, async (line) => (await this.getApplicationInfoFromWebViewPortInformation(adb, deviceIdentifier, line)) ||
            (await this.getNativeScriptApplicationInformation(adb, deviceIdentifier, line))));
        return _(portInformation)
            .filter((deviceAppInfo) => !!deviceAppInfo)
            .groupBy((element) => element.framework)
            .map((group) => _.uniqBy(group, (g) => g.appIdentifier))
            .flatten()
            .value();
    }
    async getAppProcessId(deviceIdentifier, appIdentifier) {
        const adb = this.getAdb(deviceIdentifier);
        const processId = (await this.getProcessIds(adb, [appIdentifier]))[appIdentifier];
        return processId ? processId.toString() : null;
    }
    async forwardPort(portForwardInputData, adb) {
        let localPort = await this.getAlreadyMappedPort(adb, portForwardInputData.deviceIdentifier, portForwardInputData.abstractPort);
        if (!localPort) {
            localPort = await this.$net.getFreePort();
            await adb.executeCommand(["forward", `tcp:${localPort}`, portForwardInputData.abstractPort], { deviceIdentifier: portForwardInputData.deviceIdentifier });
        }
        this._forwardedLocalPorts[portForwardInputData.deviceIdentifier] = localPort;
        await this.$cleanupService.addCleanupCommand({
            command: await this.$staticConfig.getAdbFilePath(),
            args: [
                "-s",
                portForwardInputData.deviceIdentifier,
                "forward",
                "--remove",
                `tcp:${localPort}`,
            ],
        });
        return localPort && +localPort;
    }
    async setupForPortForwarding(portForwardInputData) {
        const adb = this.getAdb(portForwardInputData.deviceIdentifier);
        return adb;
    }
    async getApplicationInfoFromWebViewPortInformation(adb, deviceIdentifier, information) {
        const processIdRegExp = /@webview_devtools_remote_(.+)/g;
        const processIdMatches = processIdRegExp.exec(information);
        let cordovaAppIdentifier;
        if (processIdMatches) {
            const processId = processIdMatches[1];
            cordovaAppIdentifier = await this.getApplicationIdentifierFromPid(adb, processId);
        }
        else {
            const chromeAppIdentifierRegExp = /@(.+)_devtools_remote\s?/g;
            const chromeAppIdentifierMatches = chromeAppIdentifierRegExp.exec(information);
            if (chromeAppIdentifierMatches && chromeAppIdentifierMatches.length > 0) {
                cordovaAppIdentifier = chromeAppIdentifierMatches[1];
            }
        }
        if (cordovaAppIdentifier) {
            return {
                deviceIdentifier: deviceIdentifier,
                appIdentifier: cordovaAppIdentifier,
                framework: constants_1.TARGET_FRAMEWORK_IDENTIFIERS.Cordova,
            };
        }
        return null;
    }
    async getNativeScriptApplicationInformation(adb, deviceIdentifier, information) {
        const nativeScriptAppIdentifierRegExp = /@(.+)-(debug|inspectorServer)/g;
        const nativeScriptAppIdentifierMatches = nativeScriptAppIdentifierRegExp.exec(information);
        if (nativeScriptAppIdentifierMatches &&
            nativeScriptAppIdentifierMatches.length > 0) {
            const appIdentifier = nativeScriptAppIdentifierMatches[1];
            return {
                deviceIdentifier: deviceIdentifier,
                appIdentifier: appIdentifier,
                framework: constants_1.TARGET_FRAMEWORK_IDENTIFIERS.NativeScript,
            };
        }
        return null;
    }
    async getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation, framework) {
        framework = framework || "";
        switch (framework.toLowerCase()) {
            case constants_1.TARGET_FRAMEWORK_IDENTIFIERS.Cordova.toLowerCase():
                return this.getCordovaPortInformation(abstractPortsInformation, appIdentifier, processId);
            case constants_1.TARGET_FRAMEWORK_IDENTIFIERS.NativeScript.toLowerCase():
                return this.getNativeScriptPortInformation(abstractPortsInformation, appIdentifier);
            default:
                return (this.getCordovaPortInformation(abstractPortsInformation, appIdentifier, processId) ||
                    this.getNativeScriptPortInformation(abstractPortsInformation, appIdentifier));
        }
    }
    getCordovaPortInformation(abstractPortsInformation, appIdentifier, processId) {
        return (this.getPortInformation(abstractPortsInformation, `${appIdentifier}_devtools_remote`) || this.getPortInformation(abstractPortsInformation, processId));
    }
    getNativeScriptPortInformation(abstractPortsInformation, appIdentifier) {
        return this.getPortInformation(abstractPortsInformation, `${appIdentifier}-debug`);
    }
    async getAbstractPortsInformation(adb) {
        return adb.executeShellCommand(["cat", "/proc/net/unix"]);
    }
    getPortInformation(abstractPortsInformation, searchedInfo) {
        const processRegExp = new RegExp(`\\w+:\\s+(?:\\w+\\s+){1,6}@(.*?${searchedInfo})$`, "gm");
        const match = processRegExp.exec(abstractPortsInformation);
        return match && match[1];
    }
    async getProcessIds(adb, appIdentifiers) {
        const result = {};
        const processIdInformation = await adb.executeShellCommand(["ps"]);
        _.each(appIdentifiers, (appIdentifier) => {
            const processIdRegExp = new RegExp(`^\\w*\\s*(\\d+).*?${appIdentifier}$`);
            result[appIdentifier] = this.getFirstMatchingGroupFromMultilineResult(processIdInformation, processIdRegExp);
        });
        return result;
    }
    async getAlreadyMappedPort(adb, deviceIdentifier, abstractPort, adbForwardList) {
        const allForwardedPorts = adbForwardList || (await adb.executeCommand(["forward", "--list"])) || "";
        const regex = new RegExp(`${deviceIdentifier}\\s+?tcp:(\\d+?)\\s+?.*?${abstractPort}$`);
        return this.getFirstMatchingGroupFromMultilineResult(allForwardedPorts, regex);
    }
    getAdb(deviceIdentifier) {
        if (!this._devicesAdbs[deviceIdentifier]) {
            this._devicesAdbs[deviceIdentifier] = this.$injector.resolve(device_android_debug_bridge_1.DeviceAndroidDebugBridge, {
                identifier: deviceIdentifier,
            });
        }
        return this._devicesAdbs[deviceIdentifier];
    }
    async getApplicationIdentifierFromPid(adb, pid, psData) {
        psData = psData || (await adb.executeShellCommand(["ps"]));
        return this.getFirstMatchingGroupFromMultilineResult(psData, new RegExp(`\\s+${pid}(?:\\s+\\d+){3}\\s+.*\\s+(.*?)$`));
    }
    getFirstMatchingGroupFromMultilineResult(input, regex) {
        let result;
        _((input || "").split("\n"))
            .map((line) => line.trim())
            .filter((line) => !!line)
            .each((line) => {
            const matches = line.match(regex);
            if (matches && matches[1]) {
                result = matches[1];
                return false;
            }
        });
        return result;
    }
}
exports.AndroidProcessService = AndroidProcessService;
__decorate([
    (0, decorators_1.exported)("androidProcessService")
], AndroidProcessService.prototype, "getAppProcessId", null);
yok_1.injector.register("androidProcessService", AndroidProcessService);
