/*
 * 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";

/**
 * This module maintains the state for the application.
 * It should not depend on any other application modules.
 */

var EventEmitter  = require("events").EventEmitter,
    util          = require("util"),
    _             = require("lodash");

var _deviceStates = ["NEW", "SOCKET_AVAILABLE", "CONNECTING", "CONNECTED", "DISCONNECTED"],
    _connectionTypes = ["usb", "wifi"],
    DEVICE_STATE = _.object(_deviceStates, _deviceStates),
    CONNECTION_TYPE = _.object(_connectionTypes.map(function (type) {
            return type.toUpperCase();
    }), _connectionTypes),
    EVENT = {
        DEVICE_ADDED: "deviceAdded",
        DEVICE_SOCKET_AVAILABLE: "deviceSocketAvailable",
        DEVICE_REMOVED: "deviceRemoved",
        DOCUMENT_CHANGED: "documentChanged",
        IMAGE_CHANGED: "imageChanged",
        DOCINFO_CHANGED: "docInfoChanged"
    },
    DEVICE_EVENT = {
        STATE_CHANGED: "stateChanged"
    };

/**
 * Hangs on to the data related to a single device.
 */
function Device(deviceId, connectionType) {
    this.id = deviceId;
    this._state = DEVICE_STATE.NEW;
    this.connectionType = connectionType;
    this.info = null;
    this.subscription = null;
    this.usageData = null;
}

util.inherits(Device, EventEmitter);

Object.defineProperty(Device.prototype, "state", {
    get: function () {
        return this._state;
    },

    set: function (newState) {
        var currentState = this._state;
        if (currentState === newState) {
            return;
        }

        this._state = newState;
        this.emit(DEVICE_EVENT.STATE_CHANGED, this, currentState, newState);
    }
});

Object.defineProperty(Device.prototype, "guid", {
    get: function () {
        return this._guid || this.id;
    },

    set: function (guid) {
        this._guid = guid;
    }
});

/*
 * This is basically the information about the device
 * in the form that we receive it from the device
 */
Device.prototype.toJSON = function () {
    return this.info;
};

/**
 * Keeps track of an image rendered for this session.
 *
 * @param {integer} size Number of bytes in the image
 */
Device.prototype.trackImageRendered = function (size) {
    if (this.usageData) {
        this.usageData.imagesRendered++;
        this.usageData.bytesRendered += size;
    }
};

Device.prototype.trackImageFailure = function (errorMessage) {
    if (this.usageData) {
        this.usageData.renderFailures++;
        // This only stores the last error message, but that should be good enough.
        this.usageData.renderedError = errorMessage;
    }
};

/**
 * Maintains the state for all of the known devices.
 */
function DeviceManager() {
    this.devices = {};
    this.handleDeviceStateChange = this.handleDeviceStateChange.bind(this);
}

util.inherits(DeviceManager, EventEmitter);

/**
 * Start tracking a new device (called by DeviceMonitor).
 */
DeviceManager.prototype.add = function (deviceId, connectionType) {
    if (this.devices[deviceId]) {
        throw new Error("Device with ID " + deviceId + " is already registered");
    }
    var device = new Device(deviceId, connectionType);
    device.on(DEVICE_EVENT.STATE_CHANGED, this.handleDeviceStateChange);
    this.devices[deviceId] = device;
    this.emit(EVENT.DEVICE_ADDED, device);
    return device; // chainable
};

/**
 * Remove a device that is no longer connected.
 */
DeviceManager.prototype.remove = function (deviceId) {
    if (!this.devices[deviceId]) {
        throw new Error("Attempt to remove device with ID " + deviceId + ", but that device is unknown");
    }
    var device = this.get(deviceId);
    delete this.devices[deviceId];
    this.emit(EVENT.DEVICE_REMOVED, device);
};

/**
 * Iterate over the device objects.
 */
DeviceManager.prototype.each = function (f) {
    _.each(this.devices, f);
};

/**
 * Filter the collection of devices.
 */
DeviceManager.prototype.filter = function (f) {
    return _.filter(this.devices, f);
};

/**
 * Removes subscriptions for all devices.
 */
DeviceManager.prototype.cancelAllSubscriptions = function () {
    this.each(function (device) {
        device.subscription = null;
    });
};

/**
 * Get an array of devices that are subscribed to the given subscription.
 */
DeviceManager.prototype.getAllForSubscription = function (subscription) {
    return this.filter(function (device) {
        return device.state === DEVICE_STATE.CONNECTED && _.isEqual(device.subscription, subscription);
    });
};

/**
 * Returns a string form of the subscription.
 */
function getSubscriptionKey(subscription) {
    if (subscription.artboardId) {
        return "documentId=" + subscription.documentId + ";artboardId=" + subscription.artboardId;
    }
    return "documentId=" + subscription.documentId;
}

/**
 * Get an array of all of the unique subscriptions.
 */
DeviceManager.prototype.getAllSubscriptions = function () {
    return _(this.devices).values().filter(function (device) {
        return device.state === DEVICE_STATE.CONNECTED;
    }).pluck("subscription").uniq(false, getSubscriptionKey).value();
};

/**
 * Get a device by its ID. Will throw if the device is unknown.
 */
DeviceManager.prototype.get = function (deviceId) {
    if (!this.devices[deviceId]) {
        throw new Error("Unknown device: " + deviceId);
    }
    return this.devices[deviceId];
};

/**
 * Returns true if the device ID is known.
 */
DeviceManager.prototype.contains = function (deviceId) {
    return this.devices[deviceId] !== undefined;
};

/**
 * Notifies when a device has changes state
 */
DeviceManager.prototype.handleDeviceStateChange = function (device, oldState, newState) {
    // just re-emit the device and state changes
    this.emit(DEVICE_EVENT.STATE_CHANGED, device, oldState, newState);
};

function DocInfo() {
    var self = this,
        current = null;

    Object.defineProperty(this, "current", {
        set: function(data) {
            var id          = current && current.id,
                newId       = data && data.id,
                infoChanged = !_.isEqual(current, data);
            current = data;
            if (id !== newId) {
                self.emit(EVENT.DOCUMENT_CHANGED);
            } else {
                self.emit(EVENT.IMAGE_CHANGED);
                if (infoChanged) {
                    self.emit(EVENT.DOCINFO_CHANGED);
                }
            }
        },
        get: function () {
            return current;
        }
    });
}

DeviceManager.prototype.toJSON = function () {
    return _.values(this.devices);
};

DeviceManager.prototype.clear = function () {
    this.devices = {};
};

util.inherits(DocInfo, EventEmitter);

function clear() {
    exports.devices.clear();
}

// For testing
exports._DeviceManager = DeviceManager;
exports._DocInfo = DocInfo;

// Public API
exports.devices = new DeviceManager();
exports.DEVICE_STATE = DEVICE_STATE;
exports.DEVICE_EVENT = DEVICE_EVENT;
exports.CONNECTION_TYPE = CONNECTION_TYPE;
exports.EVENT = EVENT;
exports.docInfo = new DocInfo();
exports.clear = clear;
exports.getSubscriptionKey = getSubscriptionKey;
