/*
 * ADOBE CONFIDENTIAL
 *
 * Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */

"use strict";

var USBMUXClient        = require("../node-usbmux/lib/USBMUXClient"),
    PreviewService      = require("./PreviewService"),
    SessionManager      = require("preview-common/SessionManager"),
    Model               = require("./Model"),
    headlights          = require("../headlights"),
    Preferences         = require("./Preferences"),
    net                 = require("net"),
    Q                   = require("q"),
    _                   = require("lodash"),
    logger              = require("../logger");

var PORT                = 43256,
    WAIT_FOR_APP_DELAY  = 500,
    WAIT_FOR_CONN_DELAY = 10000;

var usbMuxConnection,
    isMonitoring = false,
    alreadyLoggedWifiError = [];

var wifiConnection = {
    connectToDevice: function (device, ip, port) {
        var result = Q.defer(),
            timer;

        function onConnectionError(err) {
            clearTimeout(timer);
            if (!alreadyLoggedWifiError[ip]) {
                logger.logMessage("wifiConnection to " + ip + ":" + port + " failed, got " + err.toString(), logger.LOGLEVEL_ERROR);
                alreadyLoggedWifiError[ip] = true;
            }

            headlights.logConnectionFailure(device, err.toString());
            result.reject(err);
        }

        function onConnectionSuccess() {
            logger.logMessage("wifiConnection successful");
            alreadyLoggedWifiError[ip] = false;
            clearTimeout(timer);
        }

        logger.logMessage("attempting wifiConnection.connectToDevice");
        var socket = net.connect({
                        host: ip,
                        port: port
                    }, function() {
                socket.removeListener("error", onConnectionError);
                result.resolve(socket);
            });

        socket.on("error", onConnectionError);

        socket.on("connect", onConnectionSuccess);

        timer = setTimeout(function() {
            logger.logMessage("wifiConnection connectToDevice attempt exceeded timeout value");

            socket.destroy();
            onConnectionError("Error: connect ETIMEDOUT");
        }, WAIT_FOR_CONN_DELAY);
        
        return result.promise;
    }
};

function startPreviewSession (deviceId, socket) {
    var device = Model.devices.get(deviceId);                 // get the device first to make sure we still recognize it
    var session = SessionManager.open(deviceId, socket);      // next, open the socket
    device.state = Model.DEVICE_STATE.SOCKET_AVAILABLE;       // this will send out notifications that the socket is ready

    // remember that we've connected successfully at least once.  Always allow immediate connection now.
    if (!Preferences.getPref("watchImmediate")) {
        Preferences.setPref("watchImmediate", true);
    }

    // If there was any buffered data on a USBMUXSocket, we need to release that.
    // This is actually a leaky abstraction that should be handled entirely in node-usbmux.
    // Note that the timing of this is important because we need the data to be released
    // only after the listeners for that data have been set up.
    if (socket.stopBuffering) {
        socket.stopBuffering();
    }

    session.on("close", function () {
        // The device could be gone if it was unplugged rather than having had an app quit
        if (Model.devices.contains(deviceId)) {
            device = Model.devices.get(deviceId);
            device.state = Model.DEVICE_STATE.NEW;
        }
    });

    return session;
}

function establishUSBConnection(usbDevice) {

    var deviceId = usbDevice.deviceID;
    function attemptConnection() {
        // If the device's cable was disconnected, then stop trying to connect.
        if (!Model.devices.contains(deviceId)) {
            logger.logMessage("Device removed");
            return;
        }

        usbMuxConnection.connectToDevice(usbDevice, PORT).then(function (socket) {
            logger.logMessage("Got socket for " + deviceId);
            var session = startPreviewSession(deviceId, socket);
            // If the session closes, that means the app may have been closed. Try to restore the
            // connection. If the device is unplugged, we'll remove the device from the model so
            // we'll stop trying then.
            session.on("close", function () {
                attemptConnection();
            });
        }).catch(function (err) {
            if (err.toString().indexOf("Lockscreen") === -1) {
                logger.logMessage("Error" + err, logger.LOGLEVEL_ERROR);
            }

            // physically connected to device, but waiting for app to connect
            setTimeout(attemptConnection, WAIT_FOR_APP_DELAY);  // retry after a delay
        });
    }

    attemptConnection();
}

/**
 * Go through Model.devices, and remove any that have error state and are not now in the clients array.
 */
function removeOldErrors(clients) {
    var devicesToRemove = [];
    Model.devices.each(function (device) {
        if (device.state === Model.DEVICE_STATE.ERROR) {
            if (!(clients.length && _.some(clients, function (client) {
                return (client.deviceid === device.id);
            }))) {
                devicesToRemove.push(device.id);
            }
        }
    });
    if (devicesToRemove.length) {
        devicesToRemove.forEach(function (deviceid) {
            Model.devices.remove(deviceid);        
        });
    }
}

function connectToPreviewServiceClientList(clients) {
    if (!_.isArray(clients)) {
        clients = [clients];
    }

    removeOldErrors(clients);

    clients.forEach(function (client) {
        if (!client || !client.ip || client.ip === "error") {
            return;
        }

        // It's possible for the service to have two records for the same device
        // if the device did not unregister successfully.
        // Also, if our state for the device is ERROR, remove and re-add it, so we can try re-connecting.
        if (Model.devices.contains(client.deviceid)) {
            if (Model.devices.get(client.deviceid).state === Model.DEVICE_STATE.ERROR) {
                Model.devices.remove(client.deviceid);
            } else {
                return;
            }
        }
        var device = Model.devices.add(client.deviceid, Model.CONNECTION_TYPE.WIFI);
        
        headlights.logAction(headlights.actions.CONNECTION_START_WIFI);

        wifiConnection.connectToDevice(device, client.ip, PORT).then(function (socket) {
            var session = startPreviewSession(client.deviceid, socket);
            session.on("close", function() {
                // remove wifi devices on close
                Model.devices.remove(client.deviceid);
            });
        }).catch(function () {
            // Report that there's a device error, but don't remove this device from our list,
            // so that the panel can show it as having an error.
            Model.devices.deviceError(client);
        });
    });
}


// callback invoked when watching for devices connected via USB
function handleUSBMUXDeviceEvents(event) {
    switch(event.type) {
        case "DEVICE_ADDED": {
            // a device has connected
            logger.logMessage("Device " + event.device + " attached");
            Model.devices.add(event.device.deviceID, Model.CONNECTION_TYPE.USB);
            establishUSBConnection(event.device);
        }
        break;

        case "DEVICE_REMOVED": {
            // a device has been disconnected
            var deviceId = event.deviceId;
            if (Model.devices.contains(deviceId)) {
                Model.devices.remove(deviceId);
            }
            SessionManager.close(deviceId);
        }
        break;
    }
}

function disconnectUSBDevices() {
    Model.devices.each(function (device) {
        if (device.connectionType === Model.CONNECTION_TYPE.USB) {
            Model.devices.remove(device.id);
            SessionManager.close(device.id);
        }
    });
}

function disconnectWifiDevices() {
    Model.devices.each(function (device) {
        if (device.connectionType === Model.CONNECTION_TYPE.WIFI) {
            SessionManager.close(device.id);
        }
    });
}

// start watching for devices
function start(socketOptions) {
    // watch for devices connected via USB
    usbMuxConnection = USBMUXClient.create(socketOptions);
    usbMuxConnection.watch(handleUSBMUXDeviceEvents);

    PreviewService.events.on("clientList", connectToPreviewServiceClientList);
    PreviewService.events.on("shutdown", disconnectWifiDevices);

    this.isMonitoring = true;
}

// stop watching for devices
function stop() {
    this.isMonitoring = false;

    // quit watching for devices connected via USB
    if (usbMuxConnection && usbMuxConnection.connection && usbMuxConnection.connection.usbmuxSocket) {
        // just close the socket connection to usbmuxd
        usbMuxConnection.connection.usbmuxSocket.end();
    }

    Model.clear();
}

exports.start        = start;
exports.stop         = stop;
exports.isMonitoring = isMonitoring;
exports.disconnectUSBDevices = disconnectUSBDevices;

// for testing
exports.removeOldErrors = removeOldErrors;
