"use strict";
const net = require("net");
const ref = require("ref");
const path = require("path");
const util = require("util");
const ios_core_1 = require("./ios-core");
const iOSProxyServices = require("./ios-proxy-services");
const applicationManagerPath = require("./ios-application-manager");
const fileSystemPath = require("./ios-device-file-system");
const constants = require("../../../constants");
class IOSDevice {
    constructor(devicePointer, $coreFoundation, $errors, $fs, $injector, $logger, $mobileDevice, $devicePlatformsConstants, $hostInfo, $options, $iOSDeviceProductNameMapper, $processService, $xcodeSelectService) {
        this.devicePointer = devicePointer;
        this.$coreFoundation = $coreFoundation;
        this.$errors = $errors;
        this.$fs = $fs;
        this.$injector = $injector;
        this.$logger = $logger;
        this.$mobileDevice = $mobileDevice;
        this.$devicePlatformsConstants = $devicePlatformsConstants;
        this.$hostInfo = $hostInfo;
        this.$options = $options;
        this.$iOSDeviceProductNameMapper = $iOSDeviceProductNameMapper;
        this.$processService = $processService;
        this.$xcodeSelectService = $xcodeSelectService;
        this.mountImageCallbackPtr = null;
        this.mountImageCallbackPtr = ios_core_1.CoreTypes.am_device_mount_image_callback.toPointer(IOSDevice.mountImageCallback);
        this.applicationManager = this.$injector.resolve(applicationManagerPath.IOSApplicationManager, { device: this, devicePointer: this.devicePointer });
        this.fileSystem = this.$injector.resolve(fileSystemPath.IOSDeviceFileSystem, { device: this, devicePointer: this.devicePointer });
        this.deviceInfo = {
            identifier: this.$coreFoundation.convertCFStringToCString(this.$mobileDevice.deviceCopyDeviceIdentifier(this.devicePointer)),
            vendor: "Apple",
            platform: this.$devicePlatformsConstants.iOS,
            status: constants.CONNECTED_STATUS,
            errorHelp: null,
            type: "Device"
        };
        let productType = this.getValue("ProductType");
        let deviceName = this.getValue("DeviceName") || productType;
        let displayName = this.$iOSDeviceProductNameMapper.resolveProductName(deviceName) || deviceName;
        this.deviceInfo.displayName = displayName;
        this.deviceInfo.model = this.$iOSDeviceProductNameMapper.resolveProductName(productType);
        this.deviceInfo.version = this.getValue("ProductVersion");
        this.deviceInfo.color = this.getValue("DeviceColor");
        this.deviceInfo.isTablet = productType && productType.toLowerCase().indexOf("ipad") !== -1;
        this.deviceInfo.activeArchitecture = this.getActiveArchitecture(productType);
        if (this.deviceInfo.errorHelp) {
            this.deviceInfo.status = constants.UNREACHABLE_STATUS;
        }
    }
    getActiveArchitecture(productType) {
        let activeArchitecture = "";
        if (productType) {
            productType = productType.toLowerCase().trim();
            let majorVersionAsString = productType.match(/.*?(\d+)\,(\d+)/)[1];
            let majorVersion = parseInt(majorVersionAsString);
            let isArm64Architecture = false;
            if (_.startsWith(productType, "iphone")) {
                isArm64Architecture = majorVersion >= 6;
            }
            else if (_.startsWith(productType, "ipad")) {
                isArm64Architecture = majorVersion >= 4;
            }
            else if (_.startsWith(productType, "ipod")) {
                isArm64Architecture = majorVersion >= 7;
            }
            activeArchitecture = isArm64Architecture ? "arm64" : "armv7";
        }
        return activeArchitecture;
    }
    get isEmulator() {
        return false;
    }
    getApplicationInfo(applicationIdentifier) {
        return this.applicationManager.getApplicationInfo(applicationIdentifier);
    }
    static mountImageCallback(dictionary, user) {
        let coreFoundation = $injector.resolve("coreFoundation");
        let logger = $injector.resolve("logger");
        let jsDictionary = coreFoundation.cfTypeTo(dictionary);
        logger.info("[Mounting] %s", jsDictionary["Status"]);
    }
    getValue(value) {
        try {
            this.connect();
            this.startSession();
            let cfValue = this.$coreFoundation.createCFString(value);
            return this.$coreFoundation.convertCFStringToCString(this.$mobileDevice.deviceCopyValue(this.devicePointer, null, cfValue));
        }
        catch (err) {
            this.deviceInfo.errorHelp = this.deviceInfo.errorHelp || err.message.replace(/ Result code is: \d+$/, "");
            this.$logger.trace(`Error while trying to get ${value} for iOS device. Error is: `, err);
        }
        finally {
            this.stopSession();
            this.disconnect();
        }
        return null;
    }
    validateResult(result, error) {
        if (result !== 0) {
            this.$errors.fail({ formatStr: `${error}. Result code is: ${result}`, errorCode: result });
        }
        else {
            this.deviceInfo.status = constants.CONNECTED_STATUS;
        }
    }
    isPaired() {
        return this.$mobileDevice.deviceIsPaired(this.devicePointer) !== 0;
    }
    pair() {
        let result = this.$mobileDevice.devicePair(this.devicePointer);
        this.validateResult(result, "Make sure you have trusted the computer from your device. If your phone is locked with a passcode, unlock then reconnect it");
        return result;
    }
    validatePairing() {
        let result = this.$mobileDevice.deviceValidatePairing(this.devicePointer);
        this.validateResult(result, "Unable to validate pairing");
        return result;
    }
    connect() {
        let result = this.$mobileDevice.deviceConnect(this.devicePointer);
        this.validateResult(result, "Unable to connect to device");
        if (!this.isPaired()) {
            this.pair();
        }
        return this.validatePairing();
    }
    disconnect() {
        let result = this.$mobileDevice.deviceDisconnect(this.devicePointer);
        if (result > 0) {
            this.$logger.warn(`Unable to disconnect. Result is: ${result}`);
        }
    }
    startSession() {
        let result = this.$mobileDevice.deviceStartSession(this.devicePointer);
        this.validateResult(result, "Unable to start session");
    }
    stopSession() {
        let result = this.$mobileDevice.deviceStopSession(this.devicePointer);
        if (result > 0) {
            this.$logger.warn(`Unable to stop session. Result is: ${result}`);
        }
    }
    getDeviceValue(value) {
        let deviceCopyValue = this.$mobileDevice.deviceCopyValue(this.devicePointer, null, this.$coreFoundation.createCFString(value));
        return this.$coreFoundation.convertCFStringToCString(deviceCopyValue);
    }
    tryExecuteFunction(func) {
        this.connect();
        try {
            this.startSession();
            try {
                return func.apply(this, []);
            }
            finally {
                this.stopSession();
            }
        }
        finally {
            this.disconnect();
        }
    }
    findDeveloperDiskImageDirectoryPath() {
        return (() => {
            let developerDirectory = this.$xcodeSelectService.getDeveloperDirectoryPath().wait();
            let buildVersion = this.getDeviceValue("BuildVersion");
            let productVersion = this.getDeviceValue("ProductVersion");
            let productVersionParts = productVersion.split(".");
            let productMajorVersion = productVersionParts[0];
            let productMinorVersion = productVersionParts[1];
            let developerDiskImagePath = path.join(developerDirectory, "Platforms", "iPhoneOS.platform", "DeviceSupport");
            let supportPaths = this.$fs.readDirectory(developerDiskImagePath).wait();
            let supportPath = null;
            _.each(supportPaths, (sp) => {
                let parts = sp.split(' ');
                let version = parts[0];
                let versionParts = version.split(".");
                let supportPathData = {
                    version: version,
                    majorVersion: versionParts[0],
                    minorVersion: versionParts[1],
                    build: parts.length > 1 ? parts[1].replace(/[()]/, () => "") : null,
                    path: path.join(developerDiskImagePath, sp)
                };
                if (supportPathData.majorVersion === productMajorVersion) {
                    if (!supportPath) {
                        supportPath = supportPathData;
                    }
                    else {
                        if (supportPathData.minorVersion === productMinorVersion) {
                            if (supportPathData.build === buildVersion) {
                                supportPath = supportPathData;
                            }
                            else {
                                if (supportPath.build !== supportPathData.build || supportPath.build === null) {
                                    supportPath = supportPathData;
                                }
                            }
                        }
                    }
                }
            });
            if (!supportPath) {
                this.$errors.fail("Unable to find device support path. Verify that you have installed sdk compatible with your device version.");
            }
            return supportPath.path;
        }).future()();
    }
    mountImage() {
        return (() => {
            let imagePath = this.$options.ddi;
            if (this.$hostInfo.isWindows) {
                if (!imagePath) {
                    this.$errors.fail("On windows operating system you must specify the path to developer disk image using --ddi option");
                }
                let imageSignature = this.$fs.readFile(util.format("%s.signature", imagePath)).wait();
                let imageSize = this.$fs.getFsStats(imagePath).wait().size;
                let imageMounterService = this.startService(iOSProxyServices.MobileServices.MOBILE_IMAGE_MOUNTER);
                let plistService = this.$injector.resolve(ios_core_1.PlistService, { service: imageMounterService, format: ios_core_1.CoreTypes.kCFPropertyListXMLFormat_v1_0 });
                let result = plistService.exchange({
                    Command: "ReceiveBytes",
                    ImageSize: imageSize,
                    ImageType: "Developer",
                    ImageSignature: imageSignature
                }).wait();
                if (result.Status === "ReceiveBytesAck") {
                    let fileData = this.$fs.readFile(imagePath).wait();
                    plistService.sendAll(fileData);
                }
                else {
                    let afcService = this.startService(iOSProxyServices.MobileServices.APPLE_FILE_CONNECTION);
                    let afcClient = this.$injector.resolve(iOSProxyServices.AfcClient, { service: afcService });
                    afcClient.transfer(imagePath, "PublicStaging/staging.dimage").wait();
                }
                try {
                    result = plistService.exchange({
                        Command: "MountImage",
                        ImageType: "Developer",
                        ImageSignature: imageSignature,
                        ImagePath: "/let/mobile/Media/PublicStaging/staging.dimage"
                    }).wait();
                    if (result.Error) {
                        this.$errors.fail("Unable to mount image. %s", result.Error);
                    }
                    if (result.Status) {
                        this.$logger.info("Mount image: %s", result.Status);
                    }
                }
                finally {
                    plistService.close();
                }
            }
            else {
                let func = () => {
                    let developerDiskImageDirectoryPath = this.findDeveloperDiskImageDirectoryPath().wait();
                    imagePath = path.join(developerDiskImageDirectoryPath, "DeveloperDiskImage.dmg");
                    this.$logger.info("Mounting %s", imagePath);
                    let signature = this.$fs.readFile(util.format("%s.signature", imagePath)).wait();
                    let cfImagePath = this.$coreFoundation.createCFString(imagePath);
                    let cfOptions = this.$coreFoundation.cfTypeFrom({
                        ImageType: "Developer",
                        ImageSignature: signature
                    });
                    let result = this.$mobileDevice.deviceMountImage(this.devicePointer, cfImagePath, cfOptions, this.mountImageCallbackPtr);
                    if (result !== 0 && result !== IOSDevice.IMAGE_ALREADY_MOUNTED_ERROR_CODE) {
                        if (result === IOSDevice.INCOMPATIBLE_IMAGE_SIGNATURE_ERROR_CODE) {
                            this.$logger.warn("Unable to mount image %s on device %s.", imagePath, this.deviceInfo.identifier);
                        }
                        else {
                            this.$errors.fail("Unable to mount image on device.");
                        }
                    }
                };
                this.tryExecuteFunction(func);
            }
        }).future()();
    }
    getInterfaceType() {
        return this.$mobileDevice.deviceGetInterfaceType(this.devicePointer);
    }
    startService(serviceName) {
        let func = () => {
            let socket = ref.alloc("int");
            let result = this.$mobileDevice.deviceStartService(this.devicePointer, this.$coreFoundation.createCFString(serviceName), socket);
            this.validateResult(result, `Unable to start service ${serviceName}`);
            return ref.deref(socket);
        };
        return this.tryExecuteFunction(func);
    }
    openDeviceLogStream() {
        if (this.deviceInfo.status !== constants.UNREACHABLE_STATUS) {
            let iOSSystemLog = this.$injector.resolve(iOSProxyServices.IOSSyslog, { device: this });
            iOSSystemLog.read();
        }
    }
    connectToPort(port) {
        let interfaceType = this.getInterfaceType();
        if (interfaceType === IOSDevice.INTERFACE_USB) {
            let connectionId = this.$mobileDevice.deviceGetConnectionId(this.devicePointer);
            let socketRef = ref.alloc(ios_core_1.CoreTypes.intType);
            this.$mobileDevice.uSBMuxConnectByPort(connectionId, this.htons(port), socketRef);
            let socketValue = socketRef.deref();
            let socket;
            if (socketValue < 0) {
                socket = new net.Socket();
                process.nextTick(() => socket.emit("error", new Error("USBMuxConnectByPort returned bad file descriptor")));
            }
            else {
                socket = new net.Socket({ fd: socketValue });
                process.nextTick(() => socket.emit("connect"));
            }
            this._socket = socket;
            this.$processService.attachToProcessExitSignals(this, this.destroySocket);
            return socket;
        }
        return null;
    }
    destroySocket() {
        if (this._socket) {
            this._socket.destroy();
            this._socket = null;
        }
    }
    htons(port) {
        let result = (port & 0xff00) >> 8 | (port & 0x00ff) << 8;
        return result;
    }
}
IOSDevice.IMAGE_ALREADY_MOUNTED_ERROR_CODE = 3892314230;
IOSDevice.INCOMPATIBLE_IMAGE_SIGNATURE_ERROR_CODE = 3892314163;
IOSDevice.INTERFACE_USB = 1;
exports.IOSDevice = IOSDevice;
$injector.register("iOSDevice", IOSDevice);
