/*
 * 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 */
/*jshint node:true */
/*jshint -W079 */
/*global require, exports, Promise:true */
// "strict": [2, "global"]
"use strict";

var net         = require("net"),
    events      = require("events"),
    util        = require("util"),
    bufferpack  = require("bufferpack"),
    Logger      = require("./Logging"),
    Promise     = require("bluebird");

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

/**
 * Buffers incoming data and splits it into individual USBMUX messages.
 *
 * @constructor
 */
function MessageBuffer() {
    this.buffered = null;
}

/**
 * Gathers the set of complete messages and saves the remainder.
 *
 * @param {Buffer} data New data to inspect
 * @return {Array.<Buffer>} Collection of complete messages.
 */
MessageBuffer.prototype.getMessages = function(data) {
    if (this.buffered) {
        data = Buffer.concat([this.buffered, data]);
        this.buffered = null;
    }
    var messages = [],
        pos = 0,
        total = data.length;
    while (total - pos > 4) {
        var messageLength = bufferpack.unpack("<I", data, pos)[0];
        if (pos + messageLength <= total) {
            messages.push(data.slice(pos, pos + messageLength));
            pos += messageLength;
            continue;
        }
        break;
    }
    if (total - pos > 0) {
        this.buffered = data.slice(pos, total);
    }
    return messages;
};

/**
 * Create an new instance
 *
 * @param {Object} socketOptions    provides the information about the path/port to connect to running usbmuxd service
 * @param {string=} name    optional name to distinguish different open USBMUXSockets
 * @param {MessageBuffer=} messageBuffer optional message buffer to use to emit data in chunks.
 *
 * @constructor
 */
function USBMUXSocket(socketOptions, name, messageBuffer) {
    events.EventEmitter.call(this);

    if (!(socketOptions && socketOptions.address)) {
        throw new Error("Please specify the socket address/path");
    }

    this.socketOptions = socketOptions;
    this.name = name || "control";
    this.messageBuffer = messageBuffer;

    logger.debug("created USBMUXSocket", JSON.stringify(socketOptions), ", name: " + this.name);
}

// Make this object an EventEmitter
util.inherits(USBMUXSocket, events.EventEmitter);

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

/**
 * Setup all the event listener on the socket. This is to monitor and better resolve issues with the connection.
 *
 * Register listener for these events
 * - data
 * - end
 * - close
 *
 * All these event will be emitted once they occurred.
 */
USBMUXSocket.prototype.setupListener = function() {
    this.socket.on("data", function (buffer) {
        if (this.messageBuffer) {
            this.messageBuffer.getMessages(buffer).forEach(function (message) {
                this.emit("data", message);
            }.bind(this));
        } else {
            this.emit("data", buffer);
        }
    }.bind(this));

    this.socket.on("end", function () {
        logger.debug("Ending", arguments);

        this.emit("end", arguments);
    }.bind(this));

    this.socket.on("close", function (arg) {
        logger.debug("closing socket '" + this.name + "'." + arg);

        this.emit("close", arg);
    }.bind(this));
};

/**
 * Connect the socket to the given unix domain socket or ip address.
 *
 * @returns {Promise}   resolves with this, after the connection has been successfully established
 */
USBMUXSocket.prototype.connect = function () {
    logger.debug("connect");

    return new Promise(function (resolve, reject) {
        this.socket = new net.Socket();

        this.socket.on("error", function (error) {
            logger.debug("Something went wrong", error);

            reject(error);
        });

        this.socket.connect(this.socketOptions.address, function () {
            logger.debug("connection established for '" + this.name + "' socket");
            this.setupListener();

            resolve(this);
        }.bind(this));
    }.bind(this));
};

/**
 * Write data to the socket
 *
 * @param {(Buffer|string)} data  the payload to be written to the socket
 *
 * @returns {boolean}   true, payload was written successfully, false otherwise
 */
USBMUXSocket.prototype.write = function (data) {
    logger.debug("send data to '" + this.name + "' socket");

    return this.socket.write(data);
};

/**
 * If there is a MessageBuffer in use for this socket, this will remove the buffer
 * and emit a data event for any pending data. If there is no MessageBuffer, this
 * will do nothing.
 */
USBMUXSocket.prototype.stopBuffering = function () {
    if (this.messageBuffer) {
        var buffer = this.messageBuffer;
        this.messageBuffer = undefined;
        if (buffer.buffered) {
            this.emit("data", buffer.buffered);
        }
    }
};

/**
 * Closes the connection
 */
USBMUXSocket.prototype.end = function () {
    logger.debug("end socket " + this.name);

    this.socket.end();
};

/**
 * Create a new instance of USBMUXSocket. Primary API entrypoint.
 *
 * @param {Object} socketOptions    provides the information about the path/port to connect to running usbmuxd service
 * @param {string=} name    optional name to distinguish different open USBMUXSockets
 * @param {MessageBuffer=} messageBuffer optional message buffer to use to emit data in chunks.
 *
 * @see {@link USBMUXSocket}
 * @returns {USBMUXSocket}  new instance of {@link USBMUXSocket}
 */
function create(socketOptions, name, messageBuffer) {
    return new USBMUXSocket(socketOptions, name, messageBuffer);
}


// API
exports.MessageBuffer = MessageBuffer;
exports.create = create;
