"use strict";
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var ref = require("ref");
var path = require("path");
var iOSCore = require("./ios-core");
var helpers = require("../../../helpers");
var plistlib = require("plistlib");
var Future = require("fibers/future");
var MobileServices = (function () {
    function MobileServices() {
    }
    MobileServices.APPLE_FILE_CONNECTION = "com.apple.afc";
    MobileServices.INSTALLATION_PROXY = "com.apple.mobile.installation_proxy";
    MobileServices.HOUSE_ARREST = "com.apple.mobile.house_arrest";
    MobileServices.NOTIFICATION_PROXY = "com.apple.mobile.notification_proxy";
    MobileServices.SYSLOG = "com.apple.syslog_relay";
    MobileServices.MOBILE_IMAGE_MOUNTER = "com.apple.mobile.mobile_image_mounter";
    MobileServices.DEBUG_SERVER = "com.apple.debugserver";
    MobileServices.NO_WIFI_SYNC_ERROR_CODE = 3892314239;
    return MobileServices;
}());
exports.MobileServices = MobileServices;
var AfcBase = (function () {
    function AfcBase($logger) {
        this.$logger = $logger;
    }
    AfcBase.prototype.tryExecuteAfcAction = function (action) {
        var result;
        for (var currentTry = 0; currentTry < AfcBase.NUMBER_OF_RETRIES && result !== 0; currentTry++) {
            try {
                result = action();
            }
            catch (err) {
                this.$logger.trace("Error #" + currentTry + " while trying to execute action. Error is: ", err);
            }
        }
        return result;
    };
    AfcBase.NUMBER_OF_RETRIES = 5;
    return AfcBase;
}());
exports.AfcBase = AfcBase;
var AfcFile = (function (_super) {
    __extends(AfcFile, _super);
    function AfcFile(path, mode, afcConnection, $mobileDevice, $errors, $logger) {
        var _this = this;
        _super.call(this, $logger);
        this.afcConnection = afcConnection;
        this.$mobileDevice = $mobileDevice;
        this.$errors = $errors;
        this.$logger = $logger;
        this.open = false;
        var modeValue = 0;
        if (mode.indexOf("r") > -1) {
            modeValue = 0x1;
        }
        if (mode.indexOf("w") > -1) {
            modeValue = 0x2;
        }
        var afcFileRef = ref.alloc(ref.types.uint64);
        this.open = false;
        var result = this.tryExecuteAfcAction(function () { return _this.$mobileDevice.afcFileRefOpen(_this.afcConnection, path, modeValue, afcFileRef); });
        if (result !== 0) {
            this.$errors.fail("Unable to open file reference: '%s' with path '%s", result, path);
        }
        this.afcFile = ref.deref(afcFileRef);
        if (this.afcFile === 0) {
            this.$errors.fail("Invalid file reference");
        }
        this.open = true;
    }
    AfcFile.prototype.read = function (len) {
        var _this = this;
        var readLengthRef = ref.alloc(iOSCore.CoreTypes.uintType, len);
        var data = new Buffer(len * iOSCore.CoreTypes.pointerSize);
        var result = this.tryExecuteAfcAction(function () {
            data = new Buffer(len * iOSCore.CoreTypes.pointerSize);
            _this.$mobileDevice.afcFileRefRead(_this.afcConnection, _this.afcFile, data, readLengthRef);
        });
        if (result !== 0) {
            this.$errors.fail("Unable to read data from file '%s'. Result is: '%s'", this.afcFile, result);
        }
        var readLength = readLengthRef.deref();
        return data.slice(0, readLength);
    };
    AfcFile.prototype.write = function (buffer, byteLength) {
        var _this = this;
        var result = this.tryExecuteAfcAction(function () { return _this.$mobileDevice.afcFileRefWrite(_this.afcConnection, _this.afcFile, buffer, byteLength); });
        if (result !== 0) {
            this.$errors.fail("Unable to write to file: '%s'. Result is: '%s'", this.afcFile, result);
        }
        return true;
    };
    AfcFile.prototype.close = function () {
        var _this = this;
        if (this.open) {
            var result = this.tryExecuteAfcAction(function () { return _this.$mobileDevice.afcFileRefClose(_this.afcConnection, _this.afcFile); });
            if (result !== 0) {
                this.$errors.fail("Unable to close afc file connection: '%s'. Result is: '%s'", this.afcFile, result);
            }
            this.open = false;
        }
    };
    Object.defineProperty(AfcFile.prototype, "writable", {
        get: function () {
            return true;
        },
        enumerable: true,
        configurable: true
    });
    return AfcFile;
}(AfcBase));
exports.AfcFile = AfcFile;
var AfcClient = (function (_super) {
    __extends(AfcClient, _super);
    function AfcClient(service, $mobileDevice, $coreFoundation, $fs, $errors, $injector, $logger) {
        _super.call(this, $logger);
        this.service = service;
        this.$mobileDevice = $mobileDevice;
        this.$coreFoundation = $coreFoundation;
        this.$fs = $fs;
        this.$errors = $errors;
        this.$injector = $injector;
        this.$logger = $logger;
        this.afcConnection = null;
        var afcConnection = ref.alloc(ref.refType(ref.types.void));
        var result = $mobileDevice.afcConnectionOpen(this.service, 0, afcConnection);
        if (result !== 0) {
            $errors.fail("Unable to open apple file connection: %s", result);
        }
        this.afcConnection = ref.deref(afcConnection);
    }
    AfcClient.prototype.open = function (path, mode) {
        return this.$injector.resolve(AfcFile, { path: path, mode: mode, afcConnection: this.afcConnection });
    };
    AfcClient.prototype.mkdir = function (path) {
        var _this = this;
        var result = this.tryExecuteAfcAction(function () { return _this.$mobileDevice.afcDirectoryCreate(_this.afcConnection, path); });
        if (result !== 0) {
            this.$errors.fail("Unable to make directory: " + path + ". Result is " + result + ".");
        }
    };
    AfcClient.prototype.listDir = function (path) {
        var _this = this;
        var afcDirectoryRef = ref.alloc(ref.refType(ref.types.void));
        var result = this.tryExecuteAfcAction(function () { return _this.$mobileDevice.afcDirectoryOpen(_this.afcConnection, path, afcDirectoryRef); });
        if (result !== 0) {
            this.$errors.fail("Unable to open AFC directory: '%s' %s ", path, result);
        }
        var afcDirectoryValue = ref.deref(afcDirectoryRef);
        var name = ref.alloc(ref.refType(ref.types.char));
        var entries = [];
        while (this.$mobileDevice.afcDirectoryRead(this.afcConnection, afcDirectoryValue, name) === 0) {
            var value = ref.deref(name);
            if (ref.address(value) === 0) {
                break;
            }
            var filePath = ref.readCString(value, 0);
            if (filePath !== "." && filePath !== "..") {
                entries.push(filePath);
            }
        }
        this.$mobileDevice.afcDirectoryClose(this.afcConnection, afcDirectoryValue);
        return entries;
    };
    AfcClient.prototype.close = function () {
        var _this = this;
        var result = this.tryExecuteAfcAction(function () { return _this.$mobileDevice.afcConnectionClose(_this.afcConnection); });
        if (result !== 0) {
            this.$errors.failWithoutHelp("Unable to close apple file connection: " + result);
        }
    };
    AfcClient.prototype.transferPackage = function (localFilePath, devicePath) {
        var _this = this;
        return (function () {
            _this.transfer(localFilePath, devicePath).wait();
        }).future()();
    };
    AfcClient.prototype.deleteFile = function (devicePath) {
        var removeResult = this.$mobileDevice.afcRemovePath(this.afcConnection, devicePath);
        this.$logger.trace("Removing device file '%s', result: %s", devicePath, removeResult.toString());
    };
    AfcClient.prototype.transfer = function (localFilePath, devicePath) {
        var _this = this;
        return (function () {
            var future = new Future();
            try {
                _this.ensureDevicePathExist(path.dirname(devicePath));
                var reader = _this.$fs.createReadStream(localFilePath, { bufferSize: 1024 * 1024 * 15, highWaterMark: 1024 * 1024 * 15 });
                devicePath = helpers.fromWindowsRelativePathToUnix(devicePath);
                _this.deleteFile(devicePath);
                var target_1 = _this.open(devicePath, "w");
                var localFilePathSize_1 = _this.$fs.getFileSize(localFilePath).wait(), futureThrow_1 = function (err) {
                    if (!future.isResolved()) {
                        future.throw(err);
                    }
                };
                reader.on("data", function (data) {
                    try {
                        target_1.write(data, data.length);
                        _this.$logger.trace("transfer-> localFilePath: '%s', devicePath: '%s', localFilePathSize: '%s', transferred bytes: '%s'", localFilePath, devicePath, localFilePathSize_1.toString(), data.length.toString());
                    }
                    catch (err) {
                        if (err.message.indexOf("Result is: '21'") !== -1) {
                            _this.$logger.warn(err.message);
                        }
                        else {
                            futureThrow_1(err);
                        }
                    }
                });
                reader.on("error", function (error) {
                    futureThrow_1(error);
                });
                reader.on("end", function () { return target_1.close(); });
                reader.on("close", function () {
                    if (!future.isResolved()) {
                        future.return();
                    }
                });
            }
            catch (err) {
                _this.$logger.trace("Error while transferring files. Error is: ", err);
                if (!future.isResolved()) {
                    future.throw(err);
                }
            }
            future.wait();
        }).future()();
    };
    AfcClient.prototype.ensureDevicePathExist = function (deviceDirPath) {
        var _this = this;
        var filePathParts = deviceDirPath.split(path.sep);
        var currentDevicePath = "";
        filePathParts.forEach(function (filePathPart) {
            if (filePathPart !== "") {
                currentDevicePath = helpers.fromWindowsRelativePathToUnix(path.join(currentDevicePath, filePathPart));
                _this.mkdir(currentDevicePath);
            }
        });
    };
    return AfcClient;
}(AfcBase));
exports.AfcClient = AfcClient;
var InstallationProxyClient = (function () {
    function InstallationProxyClient(device, $logger, $injector, $errors) {
        this.device = device;
        this.$logger = $logger;
        this.$injector = $injector;
        this.$errors = $errors;
        this.plistService = null;
    }
    InstallationProxyClient.prototype.deployApplication = function (packageFile) {
        var _this = this;
        return (function () {
            var service = _this.device.startService(MobileServices.APPLE_FILE_CONNECTION);
            var afcClient = _this.$injector.resolve(AfcClient, { service: service });
            var devicePath = path.join("PublicStaging", path.basename(packageFile));
            afcClient.transferPackage(packageFile, devicePath).wait();
            _this.plistService = _this.getPlistService();
            _this.plistService.sendMessage({
                Command: "Install",
                PackagePath: helpers.fromWindowsRelativePathToUnix(devicePath)
            });
            _this.plistService.receiveMessage().wait();
        }).future()();
    };
    InstallationProxyClient.prototype.sendMessage = function (message) {
        var _this = this;
        return (function () {
            _this.plistService = _this.getPlistService();
            _this.plistService.sendMessage(message);
            var response = _this.plistService.receiveMessage().wait();
            if (response.Error) {
                _this.$errors.failWithoutHelp(response.Error);
            }
            return response;
        }).future()();
    };
    InstallationProxyClient.prototype.closeSocket = function () {
        if (this.plistService) {
            return this.plistService.close();
        }
    };
    InstallationProxyClient.prototype.getPlistService = function () {
        var service = this.getInstallationService();
        return this.$injector.resolve(iOSCore.PlistService, { service: service, format: iOSCore.CoreTypes.kCFPropertyListBinaryFormat_v1_0 });
    };
    InstallationProxyClient.prototype.getInstallationService = function () {
        var service;
        try {
            service = this.device.startService(MobileServices.INSTALLATION_PROXY);
        }
        catch (err) {
            if (err.code === MobileServices.NO_WIFI_SYNC_ERROR_CODE) {
                this.$logger.trace("Unable to start " + MobileServices.INSTALLATION_PROXY + ". Looks like the problem is with WIFI sync: " + err.message);
                this.$logger.printMarkdown("Unable to start installation service. Looks like `Sync over Wi-Fi` option in iTunes is enabled. " +
                    "Try disabling it, reconnect the device and execute your command again.");
            }
            this.$errors.failWithoutHelp(err);
        }
        return service;
    };
    return InstallationProxyClient;
}());
exports.InstallationProxyClient = InstallationProxyClient;
$injector.register("installationProxyClient", InstallationProxyClient);
var NotificationProxyClient = (function () {
    function NotificationProxyClient(device, $injector) {
        this.device = device;
        this.$injector = $injector;
        this.plistService = null;
        this.observers = {};
        this.buffer = "";
    }
    NotificationProxyClient.prototype.postNotification = function (notificationName) {
        this.plistService = this.$injector.resolve(iOSCore.PlistService, { service: this.device.startService(MobileServices.NOTIFICATION_PROXY), format: iOSCore.CoreTypes.kCFPropertyListBinaryFormat_v1_0 });
        this.postNotificationCore(notificationName);
    };
    NotificationProxyClient.prototype.postNotificationAndAttachForData = function (notificationName) {
        this.openSocket();
        this.postNotificationCore(notificationName);
    };
    NotificationProxyClient.prototype.addObserver = function (name, callback) {
        this.openSocket();
        var result = this.plistService.sendMessage({
            "Command": "ObserveNotification",
            "Name": name
        });
        var array = this.observers[name];
        if (!array) {
            array = new Array();
            this.observers[name] = array;
        }
        array.push(callback);
        return result;
    };
    NotificationProxyClient.prototype.removeObserver = function (name, callback) {
        var array = this.observers[name];
        if (array) {
            var index = array.indexOf(callback);
            if (index !== -1) {
                array.splice(index, 1);
            }
        }
    };
    NotificationProxyClient.prototype.openSocket = function () {
        if (!this.plistService) {
            this.plistService = this.$injector.resolve(iOSCore.PlistService, { service: this.device.startService(MobileServices.NOTIFICATION_PROXY), format: iOSCore.CoreTypes.kCFPropertyListBinaryFormat_v1_0 });
            if (this.plistService.receiveAll) {
                this.plistService.receiveAll(this.handleData.bind(this));
            }
        }
    };
    NotificationProxyClient.prototype.handleData = function (data) {
        var _this = this;
        this.buffer += data.toString();
        var PLIST_HEAD = "<plist";
        var PLIST_TAIL = "</plist>";
        var start = this.buffer.indexOf(PLIST_HEAD);
        var end = this.buffer.indexOf(PLIST_TAIL);
        while (start >= 0 && end >= 0) {
            var plist = this.buffer.substr(start, end + PLIST_TAIL.length);
            this.buffer = this.buffer.substr(end + PLIST_TAIL.length);
            plistlib.loadString(plist, function (err, plist) {
                if (!err && plist) {
                    _this.handlePlistNotification(plist);
                }
            });
            start = this.buffer.indexOf("<plist");
            end = this.buffer.indexOf("</plist>");
        }
    };
    NotificationProxyClient.prototype.postNotificationCore = function (notificationName) {
        this.plistService.sendMessage({
            "Command": "PostNotification",
            "Name": notificationName,
            "ClientOptions": ""
        });
    };
    NotificationProxyClient.prototype.closeSocket = function () {
        this.plistService.close();
    };
    NotificationProxyClient.prototype.handlePlistNotification = function (plist) {
        if (plist.type !== "dict") {
            return;
        }
        var value = plist.value;
        if (!value) {
            return;
        }
        var command = value["Command"];
        var name = value["Name"];
        if (command.type !== "string" || command.value !== "RelayNotification" || name.type !== "string") {
            return;
        }
        var notification = name.value;
        var observers = this.observers[notification];
        if (!observers) {
            return;
        }
        observers.forEach(function (observer) { return observer(notification); });
    };
    return NotificationProxyClient;
}());
exports.NotificationProxyClient = NotificationProxyClient;
var HouseArrestClient = (function () {
    function HouseArrestClient(device, $injector, $errors) {
        this.device = device;
        this.$injector = $injector;
        this.$errors = $errors;
        this.plistService = null;
    }
    HouseArrestClient.prototype.getAfcClientCore = function (command, applicationIdentifier) {
        var service = this.device.startService(MobileServices.HOUSE_ARREST);
        this.plistService = this.$injector.resolve(iOSCore.PlistService, { service: service, format: iOSCore.CoreTypes.kCFPropertyListXMLFormat_v1_0 });
        this.plistService.sendMessage({
            "Command": command,
            "Identifier": applicationIdentifier
        });
        var response = this.plistService.receiveMessage().wait();
        if (response.Error) {
            this.$errors.failWithoutHelp(HouseArrestClient.PREDEFINED_ERRORS[response.Error] || response.Error);
        }
        return this.$injector.resolve(AfcClient, { service: service });
    };
    HouseArrestClient.prototype.getAfcClientForAppContainer = function (applicationIdentifier) {
        return this.getAfcClientCore("VendContainer", applicationIdentifier);
    };
    HouseArrestClient.prototype.closeSocket = function () {
        this.plistService.close();
    };
    HouseArrestClient.PREDEFINED_ERRORS = {
        ApplicationLookupFailed: "Unable to find the application on a connected device. Ensure that the application is installed and try again."
    };
    return HouseArrestClient;
}());
exports.HouseArrestClient = HouseArrestClient;
var IOSSyslog = (function () {
    function IOSSyslog(device, $logger, $injector, $deviceLogProvider, $devicePlatformsConstants) {
        this.device = device;
        this.$logger = $logger;
        this.$injector = $injector;
        this.$deviceLogProvider = $deviceLogProvider;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.plistService = this.$injector.resolve(iOSCore.PlistService, { service: this.device.startService(MobileServices.SYSLOG), format: undefined });
    }
    IOSSyslog.prototype.read = function () {
        var _this = this;
        var printData = function (data) {
            _this.$deviceLogProvider.logData(data, _this.$devicePlatformsConstants.iOS, _this.device.deviceInfo.identifier);
        };
        this.plistService.readSystemLog(printData);
    };
    return IOSSyslog;
}());
exports.IOSSyslog = IOSSyslog;
