"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 events_1 = require("events");
const constants_1 = require("../../constants");
const net = require("net");
const ws = require("ws");
const temp = require("temp");
const ios_device_lib_1 = require("ios-device-lib");
class AppDebugSocketProxyFactory extends events_1.EventEmitter {
    constructor($logger, $errors, $options, $net) {
        super();
        this.$logger = $logger;
        this.$errors = $errors;
        this.$options = $options;
        this.$net = $net;
        this.deviceWebServers = {};
        this.deviceTcpServers = {};
    }
    getTCPSocketProxy(deviceIdentifier, appId) {
        return this.deviceTcpServers[`${deviceIdentifier}-${appId}`];
    }
    addTCPSocketProxy(device, appId) {
        return __awaiter(this, void 0, void 0, function* () {
            const cacheKey = `${device.deviceInfo.identifier}-${appId}`;
            const existingServer = this.deviceTcpServers[cacheKey];
            if (existingServer) {
                this.$errors.failWithoutHelp(`TCP socket proxy is already running for device '${device.deviceInfo.identifier}' and app '${appId}'`);
            }
            this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
            const server = net.createServer({
                allowHalfOpen: true
            });
            this.deviceTcpServers[cacheKey] = server;
            server.on("connection", (frontendSocket) => __awaiter(this, void 0, void 0, function* () {
                this.$logger.info("Frontend client connected.");
                frontendSocket.on("end", () => {
                    this.$logger.info('Frontend socket closed!');
                    if (!this.$options.watch) {
                        process.exit(0);
                    }
                });
                const appDebugSocket = yield device.getDebugSocket(appId);
                this.$logger.info("Backend socket created.");
                appDebugSocket.on("end", () => {
                    this.$logger.info("Backend socket closed!");
                    if (!this.$options.watch) {
                        process.exit(0);
                    }
                });
                frontendSocket.on("close", () => {
                    this.$logger.info("Frontend socket closed");
                    device.destroyDebugSocket(appId);
                });
                appDebugSocket.on("close", () => {
                    this.$logger.info("Backend socket closed");
                    frontendSocket.destroy();
                    server.close();
                    delete this.deviceTcpServers[cacheKey];
                });
                appDebugSocket.pipe(frontendSocket);
                frontendSocket.pipe(appDebugSocket);
                frontendSocket.resume();
            }));
            const socketFileLocation = temp.path({ suffix: ".sock" });
            server.listen(socketFileLocation);
            if (!this.$options.client) {
                this.$logger.info("socket-file-location: " + socketFileLocation);
            }
            return server;
        });
    }
    ensureWebSocketProxy(device, appId) {
        return __awaiter(this, void 0, void 0, function* () {
            const existingWebProxy = this.deviceWebServers[`${device.deviceInfo.identifier}-${appId}`];
            const result = existingWebProxy || (yield this.addWebSocketProxy(device, appId));
            this.$logger.info("Opened localhost " + result.options.port);
            return result;
        });
    }
    addWebSocketProxy(device, appId) {
        return __awaiter(this, void 0, void 0, function* () {
            const cacheKey = `${device.deviceInfo.identifier}-${appId}`;
            const existingServer = this.deviceWebServers[cacheKey];
            if (existingServer) {
                this.$errors.failWithoutHelp(`Web socket proxy is already running for device '${device.deviceInfo.identifier}' and app '${appId}'`);
            }
            const localPort = yield this.$net.getAvailablePortInRange(41000);
            this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
            const server = new ws.Server({
                port: localPort,
                host: "localhost",
                verifyClient: (info, callback) => __awaiter(this, void 0, void 0, function* () {
                    let acceptHandshake = true;
                    this.$logger.info("Frontend client connected.");
                    let appDebugSocket;
                    try {
                        appDebugSocket = yield device.getDebugSocket(appId);
                        this.$logger.info("Backend socket created.");
                        info.req["__deviceSocket"] = appDebugSocket;
                    }
                    catch (err) {
                        err.deviceIdentifier = device.deviceInfo.identifier;
                        this.$logger.trace(err);
                        this.emit(constants_1.CONNECTION_ERROR_EVENT_NAME, err);
                        acceptHandshake = false;
                        this.$logger.warn(`Cannot connect to device socket. The error message is '${err.message}'.`);
                    }
                    callback(acceptHandshake);
                })
            });
            this.deviceWebServers[cacheKey] = server;
            server.on("connection", (webSocket, req) => {
                const encoding = "utf16le";
                const appDebugSocket = req["__deviceSocket"];
                const packets = new ios_device_lib_1.MessageUnpackStream();
                appDebugSocket.pipe(packets);
                packets.on("data", (buffer) => {
                    const message = buffer.toString(encoding);
                    if (webSocket.readyState === webSocket.OPEN) {
                        webSocket.send(message);
                    }
                    else {
                        this.$logger.trace(`Received message ${message}, but unable to send it to webSocket as its state is: ${webSocket.readyState}`);
                    }
                });
                webSocket.on("error", err => {
                    this.$logger.trace("Error on debugger websocket", err);
                });
                appDebugSocket.on("error", err => {
                    this.$logger.trace("Error on debugger deviceSocket", err);
                });
                webSocket.on("message", (message) => {
                    const length = Buffer.byteLength(message, encoding);
                    const payload = Buffer.allocUnsafe(length + 4);
                    payload.writeInt32BE(length, 0);
                    payload.write(message, 4, length, encoding);
                    appDebugSocket.write(payload);
                });
                appDebugSocket.on("close", () => {
                    this.$logger.info("Backend socket closed!");
                    webSocket.close();
                });
                webSocket.on("close", () => {
                    this.$logger.info('Frontend socket closed!');
                    appDebugSocket.unpipe(packets);
                    packets.destroy();
                    device.destroyDebugSocket(appId);
                    if (!this.$options.watch) {
                        process.exit(0);
                    }
                });
            });
            return server;
        });
    }
    removeAllProxies() {
        let deviceId;
        for (deviceId in this.deviceWebServers) {
            this.deviceWebServers[deviceId].close();
        }
        for (deviceId in this.deviceTcpServers) {
            this.deviceTcpServers[deviceId].close();
        }
        this.deviceWebServers = {};
        this.deviceTcpServers = {};
    }
}
exports.AppDebugSocketProxyFactory = AppDebugSocketProxyFactory;
$injector.register("appDebugSocketProxyFactory", AppDebugSocketProxyFactory);
