/*
 * 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 EventEmitter  = require("events").EventEmitter,
    util          = require("util"),
    _             = require("lodash");

var events = new EventEmitter();

// Set this to true to log all of the traffic sent through SessionManager
// Be sure to reset this to false before checking the code in
var DEBUG = false;

var sessions = {
};

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

/**
 * Flushes the buffer
 */
MessageBuffer.prototype.flush = function () {
    this.buffered = null;
};

function _findCarriageReturn(buf, startPos) {
    for (var i = startPos; i < buf.length; i++) {
        if (buf[i] === 13) {
            return i;
        }
    }
    return -1;
}

/**
 * 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) {
        // Check for L: at the beginning
        if (data[pos] !== 76 || data[pos + 1] !== 58) {
            var slice = data.slice(pos, pos + 2);
            console.error("[error:preview.common] Missing length header in received data: " + slice);
            throw new Error("Missing length header in received data: " + slice);
        }

        var endOfLengthHeader = _findCarriageReturn(data, pos + 2);
        if (endOfLengthHeader === -1) {
            break;
        }

        // We know there's a carriage return. Check for the required line feed.
        if (data.length === endOfLengthHeader) {
            break;
        }
        if (data[endOfLengthHeader + 1] !== 10) {
            throw new Error("Length header must end with carriage return and line feed");
        }

        var messageLengthStr = data.toString("utf8", pos + 2, endOfLengthHeader);

        var messageLength = parseInt(messageLengthStr, 10),
            // L:(messageLengthStr.length)\r\n(messageLength bytes)
            headerLength = 4 + messageLengthStr.length,
            packetLength = headerLength + messageLength;

        if (pos + packetLength <= total) {
            messages.push(data.slice(pos + headerLength, pos + packetLength));
            pos += packetLength;
            continue;
        }
        break;
    }
    if (total - pos > 0) {
        this.buffered = data.slice(pos, total);
    }

    return messages.map(function (buf) {
        return JSON.parse(buf.toString("utf8"));
    });
};

function TransportHandler(socket) {
    this.messageBuffer = new MessageBuffer();
    this.socket = socket;
    this.closed = false;
    socket.on("data", this._handleData.bind(this));
    socket.on("end", this._handleEnd.bind(this));
    socket.on("close", this._handleClosed.bind(this));
    socket.on("error", this._handleClosed.bind(this));
    this._processMessage = this._processMessage.bind(this);
}

util.inherits(TransportHandler, EventEmitter);

TransportHandler.prototype.sendMessage = function (message) {
    if (this.closed) {
        return;
    }
    var payload = JSON.stringify(message.payload);

    if (message.payload.attachmentLength && !message.attachment) {
        console.error("[error:preview.common] Missing attachment");
        throw new Error("Missing attachment in message");
    }

    var length = Buffer.byteLength(payload, "utf8");
    this.socket.write("L:" + length + "\r\n" + payload);
    if (message.attachment) {
        this.socket.write(message.attachment);
    }
};

TransportHandler.prototype._handleData = function (raw) {
    var data = new Buffer(raw, "utf8"),
        messages;
    try {
        messages = this.messageBuffer.getMessages(data);
    } catch (e) {
        // Malformed input! abort!
        console.error("[error:preview.common] Malformed message: " + e);
        this.close();
        return; // make sure we don't go ahead and do the forEach
    }

    messages.forEach(this._processMessage);
};

TransportHandler.prototype._processMessage = function (message) {
    // If we ever need to handle incoming attachments on the JS side,
    // it would go in here.
    this.emit("message", message);
};

TransportHandler.prototype.close = function () {
    if (this.closed) {
        return;
    }
    this.socket.end();
    this._handleClosed();
};

TransportHandler.prototype._handleEnd = function () {
    if (!this.closed) {
        this.close();
    }
};

TransportHandler.prototype._handleClosed = function () {
    if (!this.closed) {
        this.closed = true;
        this.emit("close");
    }
};

function Session(socket, deviceId) {
    this.transport = new TransportHandler(socket);
    this._messageHandler = this._handleMessage.bind(this);
    this.close = this.close.bind(this);
    this.transport.on("message", this._messageHandler);
    this.transport.on("close", this.close);
    this.deviceId = deviceId;
    this.closed = false;
}

util.inherits(Session, EventEmitter);

Session.prototype._handleMessage = function (message) {
    if (DEBUG) {
        console.log("[info:preview.common] Received from ", this.deviceId, JSON.stringify(message, null, 2));
    }
    this.emit("message", {
        sender: this.deviceId,
        payload: message
    }, this);
};

Session.prototype.sendMessage = function (message) {
    if (DEBUG) {
        console.log("[info:preview.common] Sending to ", message.recipient, JSON.stringify(message.payload, null, 2));
    }
    this._checkClosed();
    return this.transport.sendMessage(message);
};

Session.prototype._checkClosed = function () {
    if (this.closed) {
        throw new Error("Attempt to use a closed session");
    }
};

Session.prototype.close = function () {
    this.closed = true;
    this.transport.removeAllListeners();
    this.emit("close", this);
    this.removeAllListeners();
    this.transport.close();
};

function emitGlobalMessage(messageBody) {
    events.emit("message", messageBody);
}

function open(deviceId, socket) {
    if (!deviceId || !socket) {
        throw new Error("must specify a socket and device id");
    }
    if (sessions[deviceId]) {
        throw new Error("must close the session first");
    }
    var session = new Session(socket, deviceId);
    session.on("message", emitGlobalMessage);

    // The session can be closed by the socket closing, so we handle that case
    // by deleting it here.
    session.on("close", function () {
        delete sessions[deviceId];
    });
    sessions[deviceId] = session;
    return session;
}

function close(deviceId) {
    var session = sessions[deviceId];
    if (session) {
        session.close();
        delete sessions[deviceId];
    } else {
        console.warn("[warning:preview.common] NO session found for " + deviceId);
    }
}

function sendMessage(message) {
    if (!message || !message.recipient) {
        throw new Error("must specify message recipient");
    }

    var session = sessions[message.recipient];
    if (!session) {
        throw new Error("no session for device " + message.recipient.deviceId);
    }

    return session.sendMessage(message);
}

function getDeviceIds() {
    return _.keys(sessions);
}


// Public API

exports._MessageBuffer = MessageBuffer;
exports.Session = Session;
exports.open = open;
exports.sendMessage = sendMessage;
exports.close = close;
exports.events = events;
exports.getDeviceIds = getDeviceIds;
