/*
 * Copyright (c) 2015 Adobe Systems.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global require, exports, Promise:true */

"use strict";

var USBMUXHelper      = require("./USBMUXHelper"),
    USBMUXDevice      = require("./USBMUXDevice"),
    USBMUXConstants   = require("./USBMUXConstants"),
    _                 = require("lodash"),
    util              = require("util"),
    Logger            = require("./Logging"),
    Promise           = require("bluebird");

var logger = Logger.createLogger({name: "USBMUXConnection", level: "debug"});

/**
 * Create a new USBMUXConnection
 * @param {USBMUXSocket} usbmuxSocket  instance of USBMUXSocket
 * @param {(USBMUXBinaryProtocol|USBMUXPlistProtocol)} usbmuxProtocolVersion specifies which protocol should be used to connect to usbmuxd
 * @constructor
 */
function USBMUXConnection(usbmuxSocket, usbmuxProtocolVersion) {
    if (!(usbmuxSocket && usbmuxProtocolVersion)) {
        throw new Error("Please provide the usbmuxSocket and the usbmuProtocolVersion");
    }

    this.usbmuxSocket = usbmuxSocket;
    this.packetLabel = 1;
    this.devices = [];

    this.usbmuxProtocol = new USBMUXHelper.USBMUXProtocolFactory(usbmuxProtocolVersion, usbmuxSocket);

    logger.debug("created");
}

/**
 * Returns a string representation of this instance.
 * @returns {string}
 */
USBMUXConnection.prototype.toString = function () {
    return "[USBMUXConnection: (" + this.usbmuxSocket + ")]";
};

/**
 * Return the encapsulated socket
 * @readonly
 */
Object.defineProperty(USBMUXConnection.prototype, "socket", {
    get: function () { logger.debug("get socket"); return this.usbmuxSocket; }
});

/**
 * Handle the data on the control socket connected to the usbmuxd.
 * @param {Buffer} data data received on the control socket.
 */
USBMUXConnection.prototype.handleControlSocket = function(data) {
    var result = this.usbmuxProtocol.decodePacket(data);

    logger.debug("received data on '" + this.usbmuxSocket.name + "'");
    logger.log("payload", "Data: " + util.inspect(result));

    switch (result.messageType) {
        case this.usbmuxProtocol.TYPE_DEVICE_ADD:
        {
            logger.debug("Device Attached");

            var payloadJSON = result.packetData;

            var deviceAttachedPayload = USBMUXDevice.create(payloadJSON.DeviceID, payloadJSON.Properties.LocationID, payloadJSON.Properties.SerialNumber, payloadJSON.Properties.ProductID);

            this.devices.push(deviceAttachedPayload);

            logger.debug("Connected devices: " + this.devices.length);

            this.callback({type: "DEVICE_ADDED", device: deviceAttachedPayload});
        }
        break;

        case this.usbmuxProtocol.TYPE_DEVICE_REMOVE:
        {
            logger.debug("Device Removed", result.packetData);

            _.remove(this.devices, function (device) { return device.deviceID === result.packetData.DeviceID; });
            logger.debug("Connected devices: " + this.devices.length);

            this.callback({type: "DEVICE_REMOVED", deviceId: result.packetData.DeviceID});
        }
        break;

        case this.usbmuxProtocol.TYPE_DEVICE_PAIRED:
        {
            logger.debug("Device paired with machine.");
        }
        break;

        default:
        {
            throw new Error("There is something wrong. We hit the default branch. Invalid message type " + result.messageType);
        }
    }
};

/**
 * Connect to the USBMUXSocket
 * @returns {Promise} resolved when connection has been established
 */
USBMUXConnection.prototype.connect = function () {
    logger.debug("connect");

    return this.usbmuxProtocol.usbmuxSocket.connect();
};

/**
 * Send packet to the control socket to establish connection to usbmuxd.
 * @param request   the type of request
 * @param {string=} payload payload to send with the packet
 */
USBMUXConnection.prototype.handshake = function (request, payload) {
    logger.debug("handshake");

    var packetLabel = this.packetLabel;
    this.packetLabel++;

    this.usbmuxProtocol.sendPacket(request, packetLabel, payload);
};

/**
 * Register for a listening client to usbmuxd events.
 *
 * @callback callback
 * @param {Function=} callback handles DEVICE_ADDED and DEVICE_REMOVED events
 * @returns {Promise} resolved when the connection has been established
 */
USBMUXConnection.prototype.listen = function (callback) {
    logger.debug("listen");

    this.callback = callback;

    return this.connect().then(function () {
        return new Promise(function(resolve, reject) {
            // listen for our response
            var onDataListener = function (data) {
                var result = this.usbmuxProtocol.decodePacket(data);

                if (result.messageType !== this.usbmuxProtocol.TYPE_RESULT) {
                    reject(new Error("We failed to listen " + result.packetData.Number));
                } else {
                    this.usbmuxProtocol.usbmuxSocket.on("data", this.handleControlSocket.bind(this));

                    logger.debug("Listen for control traffic on '" + this.usbmuxProtocol.usbmuxSocket.name + "'.");
                    resolve({status: result.Number});
                }
            }.bind(this);

            this.usbmuxProtocol.usbmuxSocket.once("data", onDataListener);

            this.handshake(this.usbmuxProtocol.TYPE_LISTEN);
        }.bind(this));
    }.bind(this));
};

/**
 * Setup a connection to a port on the mobile device.
 * @param {!USBMUXDevice} device    connect to this USBMUXDevice instance
 * @param {!Number} port    port on the mobile device that we want to connect
 * @returns {Promise}   resolved with socket to port on the device, when the connection has been established
 */
USBMUXConnection.prototype.connectToDevice = function (device, port) {
    logger.debug("connectToDevice");

    if (!(device && device.deviceID && port)) {
        throw new Error("Please provide device with id and port on device");
    }

    return this.connect().then(function () {
        return new Promise(function(resolve, reject) {
            logger.debug("DEVICE:", device);

            var onDataListener = function (data) {
                if (!this.usbmuxProtocol.connected) {
                    logger.log("payload", "Handle Connect Response " + data.toString());
                    logger.debug("Using " + this.usbmuxProtocol.usbmuxSocket.name);

                    var result = this.usbmuxProtocol.decodePacket(data);

                    if (result.messageType !== this.usbmuxProtocol.TYPE_RESULT || (result.messageType === this.usbmuxProtocol.TYPE_RESULT && result.packetData.Number !== 0)) {
                        if (result.packetData.Number === USBMUXConstants.RESULT_LOCKSCREEN_OR_APP_NOT_RUNNING) {
                            this.usbmuxProtocol.usbmuxSocket.end();
                            reject(new Error("Can't connect to device app! Lockscreen is visible or app not running"));
                        } else {
                            reject(new Error("Failed to connect with response code " + result.packetData.Number));
                        }
                    } else {
                        this.usbmuxProtocol.connected = true;
                        resolve(this.usbmuxProtocol.usbmuxSocket);
                    }
                }
            }.bind(this);

            this.usbmuxProtocol.usbmuxSocket.on("data", onDataListener);

            this.handshake(this.usbmuxProtocol.TYPE_DEVICE_CONNECT, {"DeviceID": device.deviceID, "PortNumber": ((port << 8) & 0xFF00) | (port >> 8)});
        }.bind(this));
    }.bind(this));
};

// API
exports.USBMUXConnection = USBMUXConnection;
