/*
 * Copyright (c) 2014 - present Adobe Systems Incorporated. All rights reserved.
 *
 * 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, indent: 4, maxerr: 50, node: true */

(function () {
    "use strict";

    var WebSocketServer = require("ws").Server,
        _ = require("lodash");

    /**
     * @private
     * The WebSocket server we listen for incoming connections on.
     * @type {?WebSocketServer}
     */
    var _wsServer;

    /**
     * @private
     * The Brackets domain manager for registering node extensions.
     * @type {?DomainManager}
     */
    var _domainManager;

    /**
     * @private
     * The ID that should be allocated to the next client that connects to the transport.
     * @type {number}
     */
    var _nextClientId = 1;

    /**
     * @private
     * A map of client IDs to the URL and WebSocket for the given ID.
     * @type {Object.<number, {id: number, url: string, socket: WebSocket}>}
     */
    var _clients = {};

    // This must match the port declared in NodeSocketTransport.js.
    // TODO: randomize this?
    var SOCKET_PORT = 8123;

    /**
     * @private
     * Returns the client info for a given WebSocket, or null if that socket isn't registered.
     * @param {WebSocket} ws
     * @return {?{id: number, url: string, socket: WebSocket}}
     */
    function _clientForSocket(ws) {
        return _.find(_clients, function (client) {
            return (client.socket === ws);
        });
    }

    /**
     * @private
     * Creates the WebSocketServer and handles incoming connections.
     */
    function _createServer() {
        if (!_wsServer) {
            // TODO: make port configurable, or use random port
            _wsServer = new WebSocketServer({port: SOCKET_PORT});
            _wsServer.on("connection", function (ws) {
                ws.on("message", function (msg) {
                    console.log("WebSocketServer - received - " + msg);
                    var msgObj;
                    try {
                        msgObj = JSON.parse(msg);
                    } catch (e) {
                        console.error("nodeSocketTransport: Error parsing message: " + msg);
                        return;
                    }

                    // See the comment in NodeSocketTransportRemote.connect() for why we have an extra
                    // layer of transport-layer message objects surrounding the protocol messaging.

                    if (msgObj.type === "connect") {
                        if (!msgObj.url) {
                            console.error("nodeSocketTransport: Malformed connect message: " + msg);
                            return;
                        }
                        var clientId = _nextClientId++;
                        _clients[clientId] = {
                            id: clientId,
                            url: msgObj.url,
                            socket: ws
                        };
                        console.log("emitting connect event");
                        _domainManager.emitEvent("nodeSocketTransport", "connect", [clientId, msgObj.url]);
                    } else if (msgObj.type === "message") {
                        var client = _clientForSocket(ws);
                        if (client) {
                            _domainManager.emitEvent("nodeSocketTransport", "message", [client.id, msgObj.message]);
                        } else {
                            console.error("nodeSocketTransport: Couldn't locate client for message: " + msg);
                        }
                    } else {
                        console.error("nodeSocketTransport: Got bad socket message type: " + msg);
                    }
                }).on("error", function (e) {
                    // TODO: emit error event
                    var client = _clientForSocket(ws);
                    console.error("nodeSocketTransport: Error on socket for client " + JSON.stringify(client) + ": " + e);
                }).on("close", function () {
                    var client = _clientForSocket(ws);
                    if (client) {
                        _domainManager.emitEvent("nodeSocketTransport", "close", [client.id]);
                        delete _clients[client.id];
                    } else {
                        console.error("nodeSocketTransport: Socket closed, but couldn't locate client");
                    }
                });
            }).on("error", function (e) {
                // TODO: emit error event
                console.error("nodeSocketTransport: Error on live preview server creation: " + e);
            });
        }
    }

    /**
     * Initializes the socket server.
     * @param {string} url
     */
    function _cmdStart(url) {
        _createServer();
    }

    /**
     * Sends a transport-layer message over the socket.
     * @param {number|Array.<number>} idOrArray A client ID or array of client IDs to send the message to.
     * @param {string} msgStr The message to send as a JSON string.
     */
    function _cmdSend(idOrArray, msgStr) {
        if (!Array.isArray(idOrArray)) {
            idOrArray = [idOrArray];
        }
        idOrArray.forEach(function (id) {
            var client = _clients[id];
            if (!client) {
                console.error("nodeSocketTransport: Couldn't find client ID: " + id);
            } else {
                client.socket.send(msgStr);
            }
        });
    }

    /**
     * Closes the connection for a given client ID.
     * @param {number} clientId
     */
    function _cmdClose(clientId) {
        var client = _clients[clientId];
        if (client) {
            client.socket.close();
            delete _clients[clientId];
        }
    }

    /**
     * Initializes the domain and registers commands.
     * @param {DomainManager} domainManager The DomainManager for the server
     */
    function init(domainManager) {
        _domainManager = domainManager;
        if (!domainManager.hasDomain("nodeSocketTransport")) {
            domainManager.registerDomain("nodeSocketTransport", {major: 0, minor: 1});
        }
        domainManager.registerCommand(
            "nodeSocketTransport",      // domain name
            "start",       // command name
            _cmdStart,     // command handler function
            false,          // this command is synchronous in Node
            "Creates the WS server",
            []
        );
        domainManager.registerCommand(
            "nodeSocketTransport",      // domain name
            "send",         // command name
            _cmdSend,       // command handler function
            false,          // this command is synchronous in Node
            "Sends a message to a given client or list of clients",
            [
                {name: "idOrArray", type: "number|Array.<number>", description: "id or array of ids to send the message to"},
                {name: "message", type: "string", description: "JSON message to send"}
            ],
            []
        );
        domainManager.registerCommand(
            "nodeSocketTransport",      // domain name
            "close",         // command name
            _cmdClose,       // command handler function
            false,          // this command is synchronous in Node
            "Closes the connection to a given client",
            [
                {name: "id", type: "number", description: "id of connection to close"}
            ],
            []
        );
        domainManager.registerEvent(
            "nodeSocketTransport",
            "connect",
            [
                {name: "clientID", type: "number", description: "ID of live preview page connecting to live development"},
                {name: "url", type: "string", description: "URL of page that live preview is connecting from"}
            ]
        );
        domainManager.registerEvent(
            "nodeSocketTransport",
            "message",
            [
                {name: "clientID", type: "number", description: "ID of live preview page sending message"},
                {name: "msg", type: "string", description: "JSON message from client page"}
            ]
        );
        domainManager.registerEvent(
            "nodeSocketTransport",
            "close",
            [
                {name: "clientID", type: "number", description: "ID of live preview page being closed"}
            ]
        );
    }

    exports.init = init;

}());
