/*
 * Copyright (c) 2013 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.
 * 
 */

(function () {
    "use strict";
    
    var util = require("util"),
        events = require("events");

    /**
     * @private
     * @constructor
     * A WebSocket connection to a client. This is a private constructor.
     * Callers should use the ConnectionManager.createConnection function
     * instead.
     * 
     * @param {WebSocket} ws The WebSocket representing the client
     * @param {Logger} logger
     */
    function Connection(ws, logger) {
        events.EventEmitter.call(this);

        this._ws = ws;
        this._logger = logger;
        this._connected = true;

        this._ws.on("message", this._receive.bind(this));
        this._ws.once("close", this.close.bind(this));
    }
    util.inherits(Connection, events.EventEmitter);
    
    /**
     * @private
     * @type {boolean}
     * Whether the connection is connected.
     */
    Connection.prototype._connected = false;
    
    /**
     * @private
     * @type {WebSocket}
     * The connection's WebSocket
     */
    Connection.prototype._ws = null;
    
    /**
     * @private
     * @type {Logger}
     */
    Connection.prototype._logger = null;

    /**
     * @private
     * Sends a message over the WebSocket. Called by public sendX commands.
     * @param {string} type Message type. Currently supported types are 
         "event", "commandResponse", "commandError", "error"
     * @param {object} message Message body, must be JSON.stringify-able
     */
    Connection.prototype._send = function (type, message) {
        if (this._ws && this._connected) {
            try {
                this._ws.send(JSON.stringify({type: type, message: message}));
            } catch (e) {
                this._logger.error("Unable to stringify message: " + e.message);
            }
        }
    };
    
    /**
     * @private
     * Sends a binary message over the WebSocket. Implicitly interpreted as a
     * message of type "commandResponse".
     * @param {Buffer} message
     */
    Connection.prototype._sendBinary = function (message) {
        if (this._ws && this._connected) {
            this._ws.send(message, {binary: true, mask: false});
        }
    };

    /**
     * @private
     * Receive event handler for the WebSocket. Responsible for parsing
     * message and handing it off to the appropriate handler.
     * @param {string} message Message received by WebSocket
     */
    Connection.prototype._receive = function (message) {
        try {
            var m = JSON.parse(message);

            if (m.id !== null && m.id !== undefined && m.domain && m.command) {
                // okay if m.parameters is null/undefined
                this.emit("command", m.id, m.domain, m.command, m.parameters);
            } else {
                this.sendError("Malformed message: " + message);
            }
        } catch (parseError) {
            this.sendError("Unable to parse message: " + message);
        }
    };

    /**
     * Closes the connection and does necessary cleanup
     */
    Connection.prototype.close = function () {
        if (this._ws) {
            try {
                this._ws.close();
            } catch (e) { }
        }
        this._connected = false;
        this.emit("close");
    };
    
    /**
     * Sends an Error message
     * @param {object} message Error message. Must be JSON.stringify-able.
     */
    Connection.prototype.sendError = function (message) {
        this._send("error", {message: message});
    };

    /**
     * Sends a response to a command execution
     * @param {number} id unique ID of the command that was executed. ID is
     *    generated by the client when the command is issued.
     * @param {object|Buffer} response Result of the command execution. Must
     *    either be JSON.stringify-able or a raw Buffer. In the latter case,
     *    the result will be sent as a binary response.
     */
    Connection.prototype.sendCommandResponse = function (id, response) {
        if (Buffer.isBuffer(response)) {
            // Assume the id is an unsigned 32-bit integer, which is encoded
            // as a four-byte header
            var header = new Buffer(4);
            
            header.writeUInt32LE(id, 0);

            // Prepend the header to the message
            var message = Buffer.concat([header, response], response.length + 4);

            this._sendBinary(message);
        } else {
            this._send("commandResponse", {id: id, response: response });
        }
    };
    
    /**
     * Sends a progress notification to a command execution
     * @param {number} id unique ID of the command that was executed. ID is
     *    generated by the client when the command is issued.
     * @param {object} progress Result of the command progress. Must be 
     *    JSON.stringify-able.
     */
    Connection.prototype.sendCommandProgress = function (id, progress) {
        this._send("commandProgress", {id: id, progress: progress });
    };

    /**
     * Sends a response indicating that an error occurred during command 
     * execution
     * @param {number} id unique ID of the command that was executed. ID is
     *    generated by the client when the command is issued.
     * @param {string} message Error message
     * @param {?object} stack Call stack from the exception, if possible. Must
     *    be JSON.stringify-able.
     */
    Connection.prototype.sendCommandError = function (id, message, stack) {
        this._send("commandError", {id: id, message: message, stack: stack});
    };

    /**
     * Sends an event message
     * @param {number} id unique ID for the event.
     * @param {string} domain Domain of the event.
     * @param {string} event Name of the event
     * @param {object} parameters Event parameters. Must be JSON.stringify-able.
     */
    Connection.prototype.sendEventMessage = function (id, domain, event, parameters) {
        this._send("event", {
            id: id,
            domain: domain,
            event: event,
            parameters: parameters
        });
    };

    module.exports = Connection;
}());
