"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());
    });
};
const ref = require("ref");
const path = require("path");
const iOSCore = require("./ios-core");
const helpers = require("../../../helpers");
const plistlib = require("plistlib");
class 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;
exports.MobileServices = MobileServices;
class AfcBase {
    constructor($logger) {
        this.$logger = $logger;
    }
    tryExecuteAfcAction(action) {
        let result;
        for (let 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;
exports.AfcBase = AfcBase;
class AfcFile extends AfcBase {
    constructor(path, mode, afcConnection, $mobileDevice, $errors, $logger) {
        super($logger);
        this.afcConnection = afcConnection;
        this.$mobileDevice = $mobileDevice;
        this.$errors = $errors;
        this.$logger = $logger;
        this.open = false;
        let modeValue = 0;
        if (mode.indexOf("r") > -1) {
            modeValue = 0x1;
        }
        if (mode.indexOf("w") > -1) {
            modeValue = 0x2;
        }
        let afcFileRef = ref.alloc(ref.types.uint64);
        this.open = false;
        let result = this.tryExecuteAfcAction(() => 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;
    }
    read(len) {
        let readLengthRef = ref.alloc(iOSCore.CoreTypes.uintType, len);
        let data = new Buffer(len * iOSCore.CoreTypes.pointerSize);
        let result = this.tryExecuteAfcAction(() => {
            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);
        }
        let readLength = readLengthRef.deref();
        return data.slice(0, readLength);
    }
    write(buffer, byteLength) {
        let result = this.tryExecuteAfcAction(() => 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;
    }
    close() {
        if (this.open) {
            let result = this.tryExecuteAfcAction(() => 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;
        }
    }
    get writable() {
        return true;
    }
}
exports.AfcFile = AfcFile;
class AfcClient extends AfcBase {
    constructor(service, $mobileDevice, $fs, $errors, $injector, $logger) {
        super($logger);
        this.service = service;
        this.$mobileDevice = $mobileDevice;
        this.$fs = $fs;
        this.$errors = $errors;
        this.$injector = $injector;
        this.$logger = $logger;
        this.afcConnection = null;
        let afcConnection = ref.alloc(ref.refType(ref.types.void));
        let 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);
    }
    open(path, mode) {
        return this.$injector.resolve(AfcFile, { path: path, mode: mode, afcConnection: this.afcConnection });
    }
    mkdir(path) {
        let result = this.tryExecuteAfcAction(() => this.$mobileDevice.afcDirectoryCreate(this.afcConnection, path));
        if (result !== 0) {
            this.$errors.fail(`Unable to make directory: ${path}. Result is ${result}.`);
        }
    }
    listDir(path) {
        let afcDirectoryRef = ref.alloc(ref.refType(ref.types.void));
        let result = this.tryExecuteAfcAction(() => this.$mobileDevice.afcDirectoryOpen(this.afcConnection, path, afcDirectoryRef));
        if (result !== 0) {
            this.$errors.fail("Unable to open AFC directory: '%s' %s ", path, result);
        }
        let afcDirectoryValue = ref.deref(afcDirectoryRef);
        let name = ref.alloc(ref.refType(ref.types.char));
        let entries = [];
        while (this.$mobileDevice.afcDirectoryRead(this.afcConnection, afcDirectoryValue, name) === 0) {
            let value = ref.deref(name);
            if (ref.address(value) === 0) {
                break;
            }
            let filePath = ref.readCString(value, 0);
            if (filePath !== "." && filePath !== "..") {
                entries.push(filePath);
            }
        }
        this.$mobileDevice.afcDirectoryClose(this.afcConnection, afcDirectoryValue);
        return entries;
    }
    close() {
        let result = this.tryExecuteAfcAction(() => this.$mobileDevice.afcConnectionClose(this.afcConnection));
        if (result !== 0) {
            this.$errors.failWithoutHelp(`Unable to close apple file connection: ${result}`);
        }
    }
    transferPackage(localFilePath, devicePath) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.transfer(localFilePath, devicePath);
        });
    }
    deleteFile(devicePath) {
        let removeResult = this.$mobileDevice.afcRemovePath(this.afcConnection, devicePath);
        this.$logger.trace("Removing device file '%s', result: %s", devicePath, removeResult.toString());
    }
    transfer(localFilePath, devicePath) {
        return new Promise((resolve, reject) => {
            let isResolved = false;
            try {
                this.ensureDevicePathExist(path.dirname(devicePath));
                let reader = this.$fs.createReadStream(localFilePath, { bufferSize: 1024 * 1024 * 15, highWaterMark: 1024 * 1024 * 15 });
                devicePath = helpers.fromWindowsRelativePathToUnix(devicePath);
                this.deleteFile(devicePath);
                let target = this.open(devicePath, "w");
                let localFilePathSize = this.$fs.getFileSize(localFilePath), futureThrow = (err) => {
                    if (!isResolved) {
                        isResolved = true;
                        reject(err);
                    }
                };
                reader.on("data", (data) => {
                    try {
                        target.write(data, data.length);
                        this.$logger.trace("transfer-> localFilePath: '%s', devicePath: '%s', localFilePathSize: '%s', transferred bytes: '%s'", localFilePath, devicePath, localFilePathSize.toString(), data.length.toString());
                    }
                    catch (err) {
                        if (err.message.indexOf("Result is: '21'") !== -1) {
                            this.$logger.warn(err.message);
                        }
                        else {
                            futureThrow(err);
                        }
                    }
                });
                reader.on("error", (error) => {
                    futureThrow(error);
                });
                reader.on("end", () => target.close());
                reader.on("close", () => {
                    if (!isResolved) {
                        isResolved = true;
                        resolve();
                    }
                });
            }
            catch (err) {
                this.$logger.trace("Error while transferring files. Error is: ", err);
                if (!isResolved) {
                    isResolved = true;
                    reject(err);
                }
            }
        });
    }
    ensureDevicePathExist(deviceDirPath) {
        let filePathParts = deviceDirPath.split(path.sep);
        let currentDevicePath = "";
        filePathParts.forEach((filePathPart) => {
            if (filePathPart !== "") {
                currentDevicePath = helpers.fromWindowsRelativePathToUnix(path.join(currentDevicePath, filePathPart));
                this.mkdir(currentDevicePath);
            }
        });
    }
}
exports.AfcClient = AfcClient;
class InstallationProxyClient {
    constructor(device, $logger, $injector, $errors) {
        this.device = device;
        this.$logger = $logger;
        this.$injector = $injector;
        this.$errors = $errors;
        this.plistService = null;
    }
    deployApplication(packageFile) {
        return __awaiter(this, void 0, void 0, function* () {
            let service = this.device.startService(MobileServices.APPLE_FILE_CONNECTION);
            let afcClient = this.$injector.resolve(AfcClient, { service: service });
            let devicePath = path.join("PublicStaging", path.basename(packageFile));
            yield afcClient.transferPackage(packageFile, devicePath);
            this.plistService = this.getPlistService();
            this.plistService.sendMessage({
                Command: "Install",
                PackagePath: helpers.fromWindowsRelativePathToUnix(devicePath)
            });
            yield this.plistService.receiveMessage();
        });
    }
    sendMessage(message) {
        return __awaiter(this, void 0, void 0, function* () {
            this.plistService = this.getPlistService();
            this.plistService.sendMessage(message);
            let response = yield this.plistService.receiveMessage();
            if (response.Error) {
                this.$errors.failWithoutHelp(response.Error);
            }
            return response;
        });
    }
    closeSocket() {
        if (this.plistService) {
            return this.plistService.close();
        }
    }
    getPlistService() {
        let service = this.getInstallationService();
        return this.$injector.resolve(iOSCore.PlistService, { service: service, format: iOSCore.CoreTypes.kCFPropertyListBinaryFormat_v1_0 });
    }
    getInstallationService() {
        let 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;
    }
}
exports.InstallationProxyClient = InstallationProxyClient;
$injector.register("installationProxyClient", InstallationProxyClient);
class NotificationProxyClient {
    constructor(device, $injector) {
        this.device = device;
        this.$injector = $injector;
        this.plistService = null;
        this.observers = {};
        this.buffer = "";
    }
    postNotification(notificationName) {
        this.plistService = this.$injector.resolve(iOSCore.PlistService, { service: this.device.startService(MobileServices.NOTIFICATION_PROXY), format: iOSCore.CoreTypes.kCFPropertyListBinaryFormat_v1_0 });
        this.postNotificationCore(notificationName);
    }
    postNotificationAndAttachForData(notificationName) {
        this.openSocket();
        this.postNotificationCore(notificationName);
    }
    addObserver(name, callback) {
        this.openSocket();
        let result = this.plistService.sendMessage({
            "Command": "ObserveNotification",
            "Name": name
        });
        let array = this.observers[name];
        if (!array) {
            array = new Array();
            this.observers[name] = array;
        }
        array.push(callback);
        return result;
    }
    removeObserver(name, callback) {
        let array = this.observers[name];
        if (array) {
            let index = array.indexOf(callback);
            if (index !== -1) {
                array.splice(index, 1);
            }
        }
    }
    openSocket() {
        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));
            }
        }
    }
    handleData(data) {
        this.buffer += data.toString();
        let PLIST_HEAD = "<plist";
        let PLIST_TAIL = "</plist>";
        let start = this.buffer.indexOf(PLIST_HEAD);
        let end = this.buffer.indexOf(PLIST_TAIL);
        while (start >= 0 && end >= 0) {
            let plist = this.buffer.substr(start, end + PLIST_TAIL.length);
            this.buffer = this.buffer.substr(end + PLIST_TAIL.length);
            plistlib.loadString(plist, (err, propertyList) => {
                if (!err && propertyList) {
                    this.handlePlistNotification(propertyList);
                }
            });
            start = this.buffer.indexOf("<plist");
            end = this.buffer.indexOf("</plist>");
        }
    }
    postNotificationCore(notificationName) {
        this.plistService.sendMessage({
            "Command": "PostNotification",
            "Name": notificationName,
            "ClientOptions": ""
        });
    }
    closeSocket() {
        this.plistService.close();
    }
    handlePlistNotification(plist) {
        if (plist.type !== "dict") {
            return;
        }
        let value = plist.value;
        if (!value) {
            return;
        }
        let command = value["Command"];
        let name = value["Name"];
        if (command.type !== "string" || command.value !== "RelayNotification" || name.type !== "string") {
            return;
        }
        let notification = name.value;
        let observers = this.observers[notification];
        if (!observers) {
            return;
        }
        observers.forEach(observer => observer(notification));
    }
}
exports.NotificationProxyClient = NotificationProxyClient;
class HouseArrestClient {
    constructor(device, $injector, $errors) {
        this.device = device;
        this.$injector = $injector;
        this.$errors = $errors;
        this.plistService = null;
    }
    getAfcClientCore(command, applicationIdentifier) {
        return __awaiter(this, void 0, void 0, function* () {
            let 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
            });
            let response = yield this.plistService.receiveMessage();
            if (response.Error) {
                this.$errors.failWithoutHelp(HouseArrestClient.PREDEFINED_ERRORS[response.Error] || response.Error);
            }
            return this.$injector.resolve(AfcClient, { service: service });
        });
    }
    getAfcClientForAppContainer(applicationIdentifier) {
        return this.getAfcClientCore("VendContainer", applicationIdentifier);
    }
    getAfcClientForAppDocuments(applicationIdentifier) {
        return this.getAfcClientCore("VendDocuments", applicationIdentifier);
    }
    closeSocket() {
        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."
};
exports.HouseArrestClient = HouseArrestClient;
class IOSSyslog {
    constructor(device, $injector, $deviceLogProvider, $devicePlatformsConstants) {
        this.device = device;
        this.$injector = $injector;
        this.$deviceLogProvider = $deviceLogProvider;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.plistService = this.$injector.resolve(iOSCore.PlistService, { service: this.device.startService(MobileServices.SYSLOG), format: undefined });
    }
    read() {
        let printData = (data) => {
            this.$deviceLogProvider.logData(data, this.$devicePlatformsConstants.iOS, this.device.deviceInfo.identifier);
        };
        this.plistService.readSystemLog(printData);
    }
}
exports.IOSSyslog = IOSSyslog;
