/**
* ******************************************************
*                                                       *
*   Copyright (C) Microsoft. All rights reserved.       *
*                                                       *
*******************************************************
*/
/// <reference path="../../typings/node.d.ts" />
/// <reference path="../../typings/Q.d.ts" />
/// <reference path="../../typings/vcremote-utils.d.ts" />
/// <reference path="../../typings/express.d.ts" />
/// <reference path="../../typings/express-extensions.d.ts" />
/// <reference path="../../typings/plist-with-patches.d.ts" />
"use strict";
var path = require('path');
var fs = require('fs');
var child_process = require('child_process');
var net = require('net');
var tls = require('tls');
var Q = require('q');

var pl = require('plist-with-patches');

var utils = require('vcremote-utils');

var Debug;
(function (Debug) {
    var serverInstance = null;
    var proxyInstance = null;
    var debugserverInstance = null;

    function init() {
        process.env['PATH'] = path.resolve(__dirname, "bin") + ":" + process.env["PATH"];
        child_process.exec('which lldb-mi', function (err, stdout, stderr) {
            if (err != null) {
                console.warn("Warning: lldb-mi not installed");
            }
        });
    }
    Debug.init = init;

    function startDebugListenerServer(conf, req, res) {
        var secure = utils.UtilHelper.argToBool(conf.serverConf().get("secure"));
        var idevicedebugserverproxyport = conf.serverConf().get("nativeDebugProxyPort") || 3001;
        var debugListenerPort = conf.serverConf().get("debugListenerPort") || 3002;

        var isDevice = req.param("target", "device").toLowerCase().localeCompare("device") == 0;
        var udid = req.param("deviceUdid", "");

        checkLldb().then(function () {
            return startDebugServer(idevicedebugserverproxyport, isDevice, udid);
        }).then(function () {
            if (serverInstance !== null) {
                return null;
            }
            return createServer(secure, conf.certs());
        }).then(function (server) {
            return startServer(server, debugListenerPort);
        }).then(function (server) {
            if (server !== null) {
                serverInstance = server;
                console.log("Debug Listener listening on port " + debugListenerPort);
            } else {
                console.log("Debug Listener already listening");
            }
            res.json(200, { idevicedebugserverproxyport: idevicedebugserverproxyport, debugListenerPort: debugListenerPort });
        }).fail(function (reason) {
            console.log(reason.message);
            console.log(reason.stack);
            terminate();
            res.send(404, reason);
        }).done();
    }
    Debug.startDebugListenerServer = startDebugListenerServer;

    function getAppRemotePath(req, res) {
        var packageId = req.param("package");
        var udid = req.param("deviceUdid", "");

        var command = "ideviceinstaller -l -o xml";
        if (udid.length > 0) {
            command = command + " -u " + udid;
        }

        Q.nfcall(child_process.exec, command + "> /tmp/$$.ideviceinstaller && echo /tmp/$$.ideviceinstaller").then(function (args) {
            // First find the path of the app on the device
            var filename = args[0].trim();
            if (!/^\/tmp\/[0-9]+\.ideviceinstaller$/.test(filename)) {
                throw new Error("WrongInstalledAppsFile");
            }
            var list = pl.parseFileSync(filename);
            fs.unlink(filename);
            for (var i = 0; i < list.length; ++i) {
                if (list[i].CFBundleIdentifier == packageId) {
                    var path = list[i].Path;
                    return path;
                }
            }
            throw new Error("PackageNotInstalled");
        }).then(function (remotePath) {
            res.send(200, remotePath);
        }).fail(function (reason) {
            res.json(404, reason);
        }).done();
    }
    Debug.getAppRemotePath = getAppRemotePath;

    function terminate() {
        closeListenerServer();
        killIDeviceDebugServerProxy();
    }
    Debug.terminate = terminate;

    function closeListenerServer() {
        if (serverInstance !== null) {
            serverInstance.close();
            serverInstance = null;
        }
    }

    function killIDeviceDebugServerProxy() {
        if (proxyInstance !== null) {
            proxyInstance.kill('SIGHUP'); // idevicedebugserverproxy does not exit from sigterm
            proxyInstance = null;
        }
    }

    function killDebugServer() {
        if (debugserverInstance !== null) {
            debugserverInstance.kill();
            debugserverInstance = null;
        }
    }

    function checkLldb() {
        var deferred = Q.defer();
        var lldb = child_process.exec("lldb-mi --version", function (error, stdout, stderr) {
            if (error) {
                deferred.reject("lldb-mi is not installed.");
            } else {
                deferred.resolve(null);
            }
        });
        return deferred.promise;
    }

    function startDebugServer(port, isDevice, udid) {
        if (isDevice) {
            return startIDeviceDebugServerProxy(port, udid);
        } else {
            killIDeviceDebugServerProxy();
            killDebugServer();
            var configString = fs.readFileSync(path.join(__dirname, "debug-config.json"), "utf8");
            var config = JSON.parse(configString.trim());
            var debugServerArgs = ["--native-regs", "localhost:" + port.toString()];
            debugserverInstance = child_process.spawn(config.debugServerPath, debugServerArgs);
            return debugserverInstance;
        }
    }

    function startIDeviceDebugServerProxy(port, udid) {
        killIDeviceDebugServerProxy();

        var args = [];

        if (udid.length > 0) {
            args.push("-u");
            args.push(udid);
        }
        args.push(port.toString());

        proxyInstance = child_process.spawn("idevicedebugserverproxy", args);
        proxyInstance.stderr.on("data", function (data) {
            console.error("idevicedebugproxyserver err: " + data);
        });
        proxyInstance.stdout.on("data", function (data) {
            console.log("idevicedebugproxyserver out: " + data);
        });
        return proxyInstance;
    }

    function createServer(secure, certs) {
        if (secure) {
            if (certs) {
                return createSecureTcpServer(certs);
            } else {
                throw new Error("Cannot start secure server without certs.");
            }
        } else {
            return createUnsecureTcpServer();
        }
    }

    function createUnsecureTcpServer() {
        var server = net.createServer(function (socket) {
            onConnection(socket);
        });
        return server;
    }

    function createSecureTcpServer(certs) {
        var options = {
            key: certs.getKey(),
            cert: certs.getCert(),
            ca: certs.getCA(),
            requestCert: true,
            rejectUnauthorized: true
        };
        var server = tls.createServer(options, function (cleartextStream) {
            if (!cleartextStream.authorized) {
                console.error(cleartextStream.authorizationError.message);
                cleartextStream.end();
            }

            onConnection(cleartextStream);
        });
        return server;
    }

    function startServer(server, port) {
        var deferred = Q.defer();

        if (server === null) {
            deferred.resolve(null);
        } else {
            server.on("error", function (error) {
                console.error("Debug Listener server shut down unexpectedly.");
                console.log("Error name: " + error.name);
                console.log("Error message: " + error.message);
                closeListenerServer();
            });

            server.listen(port, function () {
                deferred.resolve(server);
            });
        }

        return deferred.promise;
    }

    function onConnection(stream) {
        var lldb = child_process.spawn("lldb-mi", ["--interpreter"]);

        lldb.stdout.on("data", function (data) {
            stream.write(data);
        });

        lldb.stderr.on("data", function (data) {
            stream.write(data);
        });

        lldb.on("error", function (error) {
            console.error("LLDB Error");
            console.error("Error name: " + error.name);
            console.error("Error message: " + error.message);
        });

        lldb.on("exit", function (code, signal) {
            console.log("LLDB Exited. Closing Debug Listener connection.");
            stream.end();
        });

        stream.on("data", function (data) {
            if (lldb.stdin.writable) {
                lldb.stdin.write(data);
            }
        });

        stream.on("error", function (err) {
            console.error("Debug Listener connection error");
            lldb.kill();
        });

        stream.on("close", function (had_error) {
            console.log("Debug Listener connection closed");
            lldb.kill();
        });
    }
})(Debug || (Debug = {}));

module.exports = Debug;
