/*
Copyright (C) 2014 Ishraq Ibne Ashraf <ishraq@tinkerforge.com>
Copyright (C) 2014 Matthias Bolte <matthias@tinkerforge.com>
Copyright (C) 2014 Olaf Lüke <olaf@tinkerforge.com>

Redistribution and use in source and binary forms of this file,
with or without modification, are permitted. See the Creative
Commons Zero (CC0 1.0) License for more details.
*/

var Device = require('./Device');

IPConnection.FUNCTION_ENUMERATE = 254;
IPConnection.FUNCTION_DISCONNECT_PROBE = 128;
IPConnection.CALLBACK_ENUMERATE = 253;
IPConnection.CALLBACK_CONNECTED = 0;
IPConnection.CALLBACK_DISCONNECTED = 1;
IPConnection.BROADCAST_UID = 0;
// Enumeration type parameter to the enumerate callback
IPConnection.ENUMERATION_TYPE_AVAILABLE = 0;
IPConnection.ENUMERATION_TYPE_CONNECTED = 1;
IPConnection.ENUMERATION_TYPE_DISCONNECTED = 2;
// Connect reason parameter to the connected callback
IPConnection.CONNECT_REASON_REQUEST = 0;
IPConnection.CONNECT_REASON_AUTO_RECONNECT = 1;
// Disconnect reason parameter to the disconnected callback
IPConnection.DISCONNECT_REASON_REQUEST = 0;
IPConnection.DISCONNECT_REASON_ERROR = 1;
IPConnection.DISCONNECT_REASON_SHUTDOWN = 2;
// Returned by getConnectionState()
IPConnection.CONNECTION_STATE_DISCONNECTED = 0;
IPConnection.CONNECTION_STATE_CONNECTED = 1;
IPConnection.CONNECTION_STATE_PENDING = 2; //auto-reconnect in process
IPConnection.DISCONNECT_PROBE_INTERVAL = 5000;
IPConnection.RETRY_CONNECTION_INTERVAL = 2000;
// Error codes
IPConnection.ERROR_ALREADY_CONNECTED = 11;
IPConnection.ERROR_NOT_CONNECTED = 12;
IPConnection.ERROR_CONNECT_FAILED = 13;
IPConnection.ERROR_INVALID_FUNCTION_ID = 21;
IPConnection.ERROR_TIMEOUT = 31;
IPConnection.ERROR_INVALID_PARAMETER = 41;
IPConnection.ERROR_FUNCTION_NOT_SUPPORTED = 42;
IPConnection.ERROR_UNKNOWN_ERROR = 43;

IPConnection.TASK_KIND_CONNECT = 0;
IPConnection.TASK_KIND_DISCONNECT = 1;
IPConnection.TASK_KIND_AUTO_RECONNECT = 2;
IPConnection.TASK_KIND_AUTHENTICATE = 3;

// Socket implementation for Node.js and Websocket.
// The API resembles the Node.js API.
function TFSocket(PORT, HOST, ipcon) {
    this.port = PORT;
    this.host = HOST;
    this.socket = null;

    if (process.browser) {
        var webSocketURL = "ws://" + this.host + ":" + this.port + "/";
        if (typeof MozWebSocket != "undefined") {
            this.socket = new MozWebSocket(webSocketURL, "tfp");
        }
        else {
            this.socket = new WebSocket(webSocketURL, "tfp");
        }
        this.socket.binaryType = 'arraybuffer';
    }
    else {
        var net = require('net');
        this.socket = new net.Socket();
    }
    this.on = function (str, func) {
        if (process.browser) {
            switch (str) {
            case "connect":
                this.socket.onopen = func;
                break;
            case "data":
                // Websockets in browsers return a MessageEvent. We just
                // expose the data from the event as a Buffer as in Node.js.
                this.socket.onmessage = function (messageEvent) {
                    var data = new Buffer(new Uint8Array(messageEvent.data));
                    func(data);
                };
                break;
            case "error":
                // There is no easy way to get errno for error in browser websockets.
                // We assume error['errno'] === 'ECONNRESET'
                this.socket.onerror = function () {
                    var error = {"errno": "ECONNRESET"};
                    func(error);
                };
                break;
            case "close":
                this.socket.onclose = func;
                break;
            }
        }
        else {
            this.socket.on(str, func);
        }
    };
    this.connect = function () {
        if (process.browser) {
            // In the browser we already connected by creating a WebSocket object
        }
        else {
            this.socket.connect(this.port, this.host, null);
        }
    };
    this.setNoDelay = function (value) {
        if (process.browser) {
            // Currently no API available in browsers
            // But Nagle algorithm seems te be turned off in most browsers by default anyway
        }
        else {
            this.socket.setNoDelay(value);
        }
    };
    this.write = function (data) {
        if (process.browser) {
            // Some browers can't send a nodejs Buffer through a websocket,
            // we copy it into an ArrayBuffer
            var arrayBuffer = new Uint8Array(data).buffer;
            this.socket.send(arrayBuffer);
            ipcon.resetDisconnectProbe();
        }
        else {
            this.socket.write(data, ipcon.resetDisconnectProbe());
        }
    };
    this.end = function () {
        if (process.browser) {
            this.socket.close();
        }
        else {
            this.socket.end();
        }
    };
    this.destroy = function () {
        if (process.browser) {
            // There is no end/destroy in browser socket, so we close in end
            // and do nothing in destroy
        }
        else {
            this.socket.destroy();
        }
    };
}

BrickDaemon.FUNCTION_GET_AUTHENTICATION_NONCE = 1;
BrickDaemon.FUNCTION_AUTHENTICATE = 2;

function BrickDaemon(uid, ipcon) {
	Device.call(this, this, uid, ipcon);
	BrickDaemon.prototype = Object.create(Device);
	this.responseExpected = {};
	this.callbackFormats = {};
	this.APIVersion = [2, 0, 0];
	this.responseExpected[BrickDaemon.FUNCTION_GET_AUTHENTICATION_NONCE] = Device.RESPONSE_EXPECTED_ALWAYS_TRUE;
	this.responseExpected[BrickDaemon.FUNCTION_AUTHENTICATE] = Device.RESPONSE_EXPECTED_TRUE;

	this.getAuthenticationNonce = function(returnCallback, errorCallback) {
		this.ipcon.sendRequest(this, BrickDaemon.FUNCTION_GET_AUTHENTICATION_NONCE, [], '', 'B4', returnCallback, errorCallback);
	};
	this.authenticate = function(clientNonce, digest, returnCallback, errorCallback) {
		this.ipcon.sendRequest(this, BrickDaemon.FUNCTION_AUTHENTICATE, [clientNonce, digest], 'B4 B20', '', returnCallback, errorCallback);
	};
}

// the IPConnection class and constructor
function IPConnection() {
    // Creates an IP Connection object that can be used to enumerate the available
    // devices. It is also required for the constructor of Bricks and Bricklets.
    this.host = undefined;
    this.port = undefined;
    this.timeout = 2500;
    this.autoReconnect = true;
    this.nextSequenceNumber = 0;
    this.nextAuthenticationNonce = 0;
    this.devices = {};
    this.registeredCallbacks = {};
    this.socket = undefined;
    this.disconnectProbeIID = undefined;
    this.taskQueue = [];
    this.isConnected = false;
    this.connectErrorCallback = undefined;
    this.mergeBuffer = new Buffer(0);
    this.brickd = new BrickDaemon('2', this);

    this.disconnectProbe = function () {
        if (this.socket !== undefined) {
            this.socket.write(this.createPacketHeader(undefined, 8, IPConnection.FUNCTION_DISCONNECT_PROBE), this.resetDisconnectProbe());
        }
    };
    this.pushTask = function (handler, kind) {
        this.taskQueue.push({"handler": handler, "kind": kind});

        if (this.taskQueue.length === 1) {
            this.executeTask();
        }
    };
    this.executeTask = function () {
        var task = this.taskQueue[0];

        if (task !== undefined) {
            task.handler();
        }
    };
    this.popTask = function () {
        this.taskQueue.splice(0, 1);
        this.executeTask();
    };
    this.removeNextTask = function () {
        this.taskQueue.splice(1, 1);
    };
    this.getCurrentTaskKind = function () {
        var task = this.taskQueue[0];

        if (task !== undefined) {
            return task.kind;
        }

        return undefined;
    };
    this.getNextTaskKind = function () {
        var task = this.taskQueue[1];

        if (task !== undefined) {
            return task.kind;
        }

        return undefined;
    };
    this.disconnect = function (errorCallback) {
        this.pushTask(this.disconnectInternal.bind(this, errorCallback), IPConnection.TASK_KIND_DISCONNECT);
    };
    this.disconnectInternal = function (errorCallback) {
        var autoReconnectAborted = false;

        if (this.getNextTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) {
            // Remove auto-reconnect task, to break recursion
            this.removeNextTask();
            autoReconnectAborted = true;
        }

        if (!this.isConnected) {
            if (!autoReconnectAborted && errorCallback !== undefined) {
                // Not using `this.` for the error callback function because
                // we want to call what user provided not the saved one
                errorCallback(IPConnection.ERROR_NOT_CONNECTED);
            }
            this.popTask();
            return;
        }

        this.socket.end();
        this.socket.destroy();
        // no popTask() here, will be done in handleConnectionClose()
        return;
    };
    this.connect = function (host, port, errorCallback) {
        this.pushTask(this.connectInternal.bind(this, host, port, errorCallback), IPConnection.TASK_KIND_CONNECT);
    };
    this.connectInternal = function (host, port, errorCallback) {
        if (this.isConnected) {
            if (errorCallback !== undefined) {
                // Not using `this.` for the error callback function because
                // we want to call what user provided not the saved one
                errorCallback(IPConnection.ERROR_ALREADY_CONNECTED);
            }

            this.popTask();
            return;
        }

        // Saving the user provided error callback function for future use
        this.connectErrorCallback = errorCallback;
        clearInterval(this.disconnectProbeIID);
        this.host = host;
        this.port = port;
        this.socket = new TFSocket(this.port, this.host, this);
        this.socket.setNoDelay(true);
        this.socket.on('connect', this.handleConnect.bind(this));
        this.socket.on('data', this.handleIncomingData.bind(this));
        this.socket.on('error', this.handleConnectionError.bind(this));
        this.socket.on('close', this.handleConnectionClose.bind(this));
        this.socket.connect();
    };
    this.handleConnect = function () {
        var connectReason = IPConnection.CONNECT_REASON_REQUEST;

        if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) {
            connectReason = IPConnection.CONNECT_REASON_AUTO_RECONNECT;
        }

        clearInterval(this.disconnectProbeIID);
        this.isConnected = true;

        // Check and call functions if registered for callback connected
        if (this.registeredCallbacks[IPConnection.CALLBACK_CONNECTED] !== undefined) {
            this.registeredCallbacks[IPConnection.CALLBACK_CONNECTED](connectReason);
        }

        this.disconnectProbeIID = setInterval(this.disconnectProbe.bind(this),
                                              IPConnection.DISCONNECT_PROBE_INTERVAL);

        this.popTask();
    };
    this.handleIncomingData = function (data) {
        this.resetDisconnectProbe();
        if (data.length === 0) {
            return;
        }
        this.mergeBuffer = bufferConcat([this.mergeBuffer, data]);

        if (this.mergeBuffer.length < 8) {
            return;
        }
        if (this.mergeBuffer.length < this.mergeBuffer.readUInt8(4)) {
            return;
        }
        while (this.mergeBuffer.length >= 8) {
            var newPacket = new Buffer(this.mergeBuffer.readUInt8(4));
            this.mergeBuffer.copy(newPacket, 0, 0, this.mergeBuffer.readUInt8(4));
            this.handlePacket(newPacket);
            this.mergeBuffer = this.mergeBuffer.slice(this.mergeBuffer.readUInt8(4));
        }
    };
    this.handleConnectionError = function (error) {
        if (error.errno === 'ECONNRESET') {
            // Check and call functions if registered for callback disconnected
            if (this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED] !== undefined) {
                this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED](IPConnection.DISCONNECT_REASON_SHUTDOWN);
            }
        }
    };
    this.handleAutoReconnectError = function (error) {
        if (!this.isConnected && this.autoReconnect && error !== IPConnection.ERROR_ALREADY_CONNECTED) {
            this.pushTask(this.connectInternal.bind(this, this.host, this.port, this.handleAutoReconnectError), IPConnection.TASK_KIND_AUTO_RECONNECT);
        }
    };
    this.handleConnectionClose = function () {
        if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_DISCONNECT) {
            // This disconnect was requested
            var uid;
            for (uid in this.devices) {
                for (var i=0;i<this.devices[uid].expectedResponses.length;i++) {
                    clearTimeout(this.devices[uid].expectedResponses[i].timeout);

                    if (this.devices[uid].expectedResponses[i].errorCB !== undefined) {
                        this.devices[uid].expectedResponses[i].errorCB(IPConnection.ERROR_TIMEOUT);
                    }
                }

                this.devices[uid].expectedResponses = [];
            }

            this.isConnected = false;
            clearInterval(this.disconnectProbeIID);

            if (this.socket !== undefined) {
                this.socket.end();
                this.socket.destroy();
                this.socket = undefined;
            }

            // Check and call functions if registered for callback disconnected
            if (this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED] !== undefined) {
                this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED](IPConnection.DISCONNECT_REASON_REQUEST);
            }

            this.popTask();
            return;
        }
        // Was connected, disconnected because of error and auto reconnect is enabled
        if (this.isConnected) {
            this.isConnected = false;
            clearInterval(this.disconnectProbeIID);

            if (this.socket !== undefined) {
                this.socket.end();
                this.socket.destroy();
                this.socket = undefined;
            }

            // Check and call functions if registered for callback disconnected
            if (this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED] !== undefined) {
                this.registeredCallbacks[IPConnection.CALLBACK_DISCONNECTED](IPConnection.DISCONNECT_REASON_ERROR);
            }

            if (this.autoReconnect) {
                this.pushTask(this.connectInternal.bind(this, this.host, this.port, this.handleAutoReconnectError), IPConnection.TASK_KIND_AUTO_RECONNECT);
            }

            return;
        }

        // Were not connected. failed at new connection attempt
        if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_CONNECT || this.getCurrentTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) {
            if (this.connectErrorCallback !== undefined) {
                this.connectErrorCallback(IPConnection.ERROR_CONNECT_FAILED);
            }
            this.popTask();
            return;
        }
    };
    this.resetDisconnectProbe = function() {
        if(this.disconnectProbeIID === undefined) {
            return;
        }
        clearInterval(this.disconnectProbeIID);
        this.disconnectProbeIID = setInterval(this.disconnectProbe.bind(this),
                                                   IPConnection.DISCONNECT_PROBE_INTERVAL);
    };
    this.getUIDFromPacket = function (packetUID){
        return packetUID.readUInt32LE(0);
    };
    this.getLengthFromPacket = function (packetLen) {
        return packetLen.readUInt8(4);
    };
    this.getFunctionIDFromPacket = function (packetFID) {
        return packetFID.readUInt8(5);
    };
    this.getSequenceNumberFromPacket = function (packetSeq) {
        return (packetSeq.readUInt8(6) >>> 4) & 0x0F;
    };
    this.getRFromPacket = function (packetR) {
        return (packetR.readUInt8(6) >>> 3) & 0x01;
    };
    this.getEFromPacket = function (packetE) {
        // Getting Error bits(E, 2bits)
        return (packetE.readUInt8(7) >>> 6) & 0x03;
    };
    this.getPayloadFromPacket = function (packetPayload) {
        var payloadReturn = new Buffer(packetPayload.length - 8);
        packetPayload.copy(payloadReturn, 0, 8, packetPayload.length);
        return new Buffer(payloadReturn);
    };
    function pack(data, format) {
        var formatArray = format.split(' ');
        if (formatArray.length <= 0) {
            return new Buffer(0);
        }
        var packedBuffer = new Buffer(0);
        for (var i=0; i<formatArray.length; i++){
            if (formatArray[i].split('').length === 1) {
                if (formatArray[i] === 's') {
                    var tmpPackedBuffer = new Buffer(1);
                    tmpPackedBuffer.fill(0x00);
                    tmpPackedBuffer.writeUInt8(data[i].charCodeAt(0), 0);
                    packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                    continue;
                }
                switch (formatArray[i]) {
                    case 'c':
                        var tmpPackedBuffer = new Buffer(1);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeUInt8(data[i].charCodeAt(0), 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'b':
                        var tmpPackedBuffer = new Buffer(1);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeInt8(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'B':
                        var tmpPackedBuffer = new Buffer(1);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeUInt8(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'h':
                        var tmpPackedBuffer = new Buffer(2);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeInt16LE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'H':
                        var tmpPackedBuffer = new Buffer(2);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeUInt16LE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'i':
                        var tmpPackedBuffer = new Buffer(4);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeInt32LE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'I':
                        var tmpPackedBuffer = new Buffer(4);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeUInt32LE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'q':
                        var tmpPackedBuffer = new Buffer(8);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeDoubleLE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'Q':
                        var tmpPackedBuffer = new Buffer(8);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeDoubleLE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'f':
                        var tmpPackedBuffer = new Buffer(4);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeFloatLE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case 'd':
                        var tmpPackedBuffer = new Buffer(8);
                        tmpPackedBuffer.fill(0x00);
                        tmpPackedBuffer.writeDoubleLE(data[i], 0);
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                    case '?':
                        var tmpPackedBuffer = new Buffer(1);
                        tmpPackedBuffer.fill(0x00);
                        if(data[i] === 0 || data[i] === false || data[i] === undefined ||
                           data[i] === null || data[i] === NaN || data[i] === -0) {
                            tmpPackedBuffer.writeUInt8(0x00, 0);
                        }
                        else {
                            tmpPackedBuffer.writeUInt8(0x01, 0);
                        }
                        packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        continue;
                }
            }
            if(formatArray[i].split('').length > 1) {
                var singleFormatArray = formatArray[i].split('');
                for(var j=0; j<parseInt(formatArray[i].match(/\d/g).join('')); j++) {
                    if(singleFormatArray[0] === 's') {
                        if(!isNaN(data[i].charCodeAt(j))) {
                            var tmpPackedBuffer = new Buffer(1);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeUInt8(data[i].charCodeAt(j), 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        }
                        else {
                            var tmpPackedBuffer = new Buffer(1);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeUInt8(0x00, 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                        }
                        continue;
                    }
                    switch(singleFormatArray[0]) {
                        case 'c':
                            var tmpPackedBuffer = new Buffer(1);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeUInt8(data[i][j].charCodeAt(0), 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'b':
                            var tmpPackedBuffer = new Buffer(1);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeInt8(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'B':
                            var tmpPackedBuffer = new Buffer(1);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeUInt8(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'h':
                            var tmpPackedBuffer = new Buffer(2);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeInt16LE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'H':
                            var tmpPackedBuffer = new Buffer(2);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeUInt16LE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'i':
                            var tmpPackedBuffer = new Buffer(4);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeInt32LE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'I':
                            var tmpPackedBuffer = new Buffer(4);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeUInt32LE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'q':
                            var tmpPackedBuffer = new Buffer(8);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeDoubleLE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'Q':
                            var tmpPackedBuffer = new Buffer(8);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeDoubleLE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'f':
                            var tmpPackedBuffer = new Buffer(4);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeFloatLE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case 'd':
                            var tmpPackedBuffer = new Buffer(8);
                            tmpPackedBuffer.fill(0x00);
                            tmpPackedBuffer.writeDoubleLE(data[i][j], 0);
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                        case '?':
                            var tmpPackedBuffer = new Buffer(1);
                            tmpPackedBuffer.fill(0x00);
                            if(data[i][j] === 0 || data[i][j] === false || data[i][j] === undefined ||
                               data[i][j] === null || data[i][j] === NaN || data[i][j] === -0) {
                                tmpPackedBuffer.writeUInt8(0x00, 0);
                            }
                            else {
                                tmpPackedBuffer.writeUInt8(0x01, 0);
                            }
                            packedBuffer = bufferConcat([packedBuffer,tmpPackedBuffer]);
                            continue;
                    }
                }
            }
        }
        return packedBuffer;
    }
    function unpack(unpackPayload, format) {
        var formatArray = format.split(' ');
        var returnArguments = [];
        var returnSubArray = [];
        var constructedString = '';
        var payloadReadOffset = 0;
        if (formatArray.length <= 0) {
            return returnArguments;
        }
        for (var i=0; i<formatArray.length; i++) {
            if (formatArray[i].split('').length === 1) {
                if (formatArray[i] === 's') {
                    constructedString += String.fromCharCode(unpackPayload.readUInt8(payloadReadOffset));
                    payloadReadOffset++;
                    returnArguments.push(constructedString);
                    constructedString = '';
                    continue;
                }
                switch(formatArray[i]) {
                    case 'c':
                        returnArguments.push(String.fromCharCode(unpackPayload.readUInt8(payloadReadOffset)));
                        payloadReadOffset += 1;
                        continue;
                    case 'b':
                        returnArguments.push(unpackPayload.readInt8(payloadReadOffset));
                        payloadReadOffset += 1;
                        continue;
                    case 'B':
                        returnArguments.push(unpackPayload.readUInt8(payloadReadOffset));
                        payloadReadOffset += 1;
                        continue;
                    case 'h':
                        returnArguments.push(unpackPayload.readInt16LE(payloadReadOffset));
                        payloadReadOffset += 2;
                        continue;
                    case 'H':
                        returnArguments.push(unpackPayload.readUInt16LE(payloadReadOffset));
                        payloadReadOffset += 2;
                        continue;
                    case 'i':
                        returnArguments.push(unpackPayload.readInt32LE(payloadReadOffset));
                        payloadReadOffset += 4;
                        continue;
                    case 'I':
                        returnArguments.push(unpackPayload.readUInt32LE(payloadReadOffset));
                        payloadReadOffset += 4;
                        continue;
                    case 'q':
                        returnArguments.push(unpackPayload.readDoubleLE(payloadReadOffset));
                        payloadReadOffset += 8;
                        continue;
                    case 'Q':
                        returnArguments.push(unpackPayload.readDoubleLE(payloadReadOffset));
                        payloadReadOffset += 8;
                        continue;
                    case 'f':
                        returnArguments.push(unpackPayload.readFloatLE(payloadReadOffset));
                        payloadReadOffset += 4;
                        continue;
                    case 'd':
                        returnArguments.push(unpackPayload.readDoubleLE(payloadReadOffset));
                        payloadReadOffset += 8;
                        continue;
                    case '?':
                        if (unpackPayload.readUInt8(payloadReadOffset) === 0x01) {
                            returnArguments.push(true);
                        }
                        if (unpackPayload.readUInt8(payloadReadOffset) === 0x00) {
                            returnArguments.push(false);
                        }
                        payloadReadOffset++;
                        continue;
                }
            }
            if (formatArray[i].split('').length > 1) {
                var singleFormatArray = formatArray[i].split('');
                if (singleFormatArray[0] === 's') {
                    constructedString = '';
					skip = false;
                    for(var j=0; j<parseInt(formatArray[i].match(/\d/g).join('')); j++) {
						c = String.fromCharCode(unpackPayload.readUInt8(payloadReadOffset));
						if(c === '\0' || skip) {
							skip = true;
						} else {
	                        constructedString += c;
						}
						payloadReadOffset++;
                    }
					skip = false;
                    returnArguments.push(constructedString);
                    constructedString = '';
                    continue;
                }
                returnSubArray = [];
                for (var k=0; k<parseInt(formatArray[i].match(/\d/g).join('')); k++) {
                    switch(singleFormatArray[0]) {
                        case 'c':
                            returnSubArray.push(String.fromCharCode(unpackPayload.readUInt8(payloadReadOffset)));
                            payloadReadOffset += 1;
                            continue;
                        case 'b':
                            returnSubArray.push(unpackPayload.readInt8(payloadReadOffset));
                            payloadReadOffset += 1;
                            continue;
                        case 'B':
                            returnSubArray.push(unpackPayload.readUInt8(payloadReadOffset));
                            payloadReadOffset += 1;
                            continue;
                        case 'h':
                            returnSubArray.push(unpackPayload.readInt16LE(payloadReadOffset));
                            payloadReadOffset += 2;
                            continue;
                        case 'H':
                            returnSubArray.push(unpackPayload.readUInt16LE(payloadReadOffset));
                            payloadReadOffset += 2;
                            continue;
                        case 'i':
                            returnSubArray.push(unpackPayload.readInt32LE(payloadReadOffset));
                            payloadReadOffset += 4;
                            continue;
                        case 'I':
                            returnSubArray.push(unpackPayload.readUInt32LE(payloadReadOffset));
                            payloadReadOffset += 4;
                            continue;
                        case 'q':
                            returnSubArray.push(unpackPayload.readDoubleLE(payloadReadOffset));
                            payloadReadOffset += 8;
                            continue;
                        case 'Q':
                            returnSubArray.push(unpackPayload.readDoubleLE(payloadReadOffset));
                            payloadReadOffset += 8;
                            continue;
                        case 'f':
                            returnSubArray.push(unpackPayload.readFloatLE(payloadReadOffset));
                            payloadReadOffset += 4;
                            continue;
                        case 'd':
                            returnSubArray.push(unpackPayload.readDoubleLE(payloadReadOffset));
                            payloadReadOffset += 8;
                            continue;
                        case '?':
                            if (unpackPayload.readUInt8(payloadReadOffset) === 0x01) {
                                returnSubArray.push(true);
                            }
                            if (unpackPayload.readUInt8(payloadReadOffset) === 0x00) {
                                returnSubArray.push(false);
                            }
                            payloadReadOffset++;
                            continue;
                    }
                }
                if (returnSubArray.length !== 0) {
                    returnArguments.push(returnSubArray);
                    returnSubArray = [];
                    continue;
                }
            }
        }
        return returnArguments;
     }
    this.sendRequest = function (sendRequestDevice, sendRequestFID, sendRequestData,
                                 sendRequestPackFormat, sendRequestUnpackFormat,
                                 sendRequestReturnCB, sendRequestErrorCB) {
        if (this.getConnectionState() !== IPConnection.CONNECTION_STATE_CONNECTED) {
            if (sendRequestErrorCB !== undefined) {
                sendRequestErrorCB(IPConnection.ERROR_NOT_CONNECTED);
            }
            return;
        }
        // Packet creation
        var sendRequestPayload = pack(sendRequestData, sendRequestPackFormat);
        var sendRequestHeader = this.createPacketHeader(sendRequestDevice,
                                                        8+sendRequestPayload.length,
                                                        sendRequestFID, sendRequestErrorCB);
        if (sendRequestHeader === undefined) {
            return;
        }
        var sendRequestPacket = bufferConcat([sendRequestHeader, sendRequestPayload]);
        var sendRequestSEQ = this.getSequenceNumberFromPacket(sendRequestHeader);
        // Sending the created packet
        if (sendRequestDevice.getResponseExpected(sendRequestFID)) {
            // Setting the requesting current device's current request
            var sendRequestDeviceOID = sendRequestDevice.getDeviceOID();
            sendRequestDevice.expectedResponses.push({DeviceOID:sendRequestDeviceOID,
                FID:sendRequestFID,
                SEQ:sendRequestSEQ,
                unpackFormat:sendRequestUnpackFormat,
                timeout:setTimeout(this.sendRequestTimeout.bind
                        (this, sendRequestDevice, sendRequestDeviceOID, sendRequestErrorCB), this.timeout),
                returnCB:sendRequestReturnCB,
                errorCB:sendRequestErrorCB});
        }
        this.socket.write(sendRequestPacket, this.resetDisconnectProbe());
    };
    this.sendRequestTimeout = function (timeoutDevice, timeoutDeviceOID, timeoutErrorCB) {
        for (var i=0; i<timeoutDevice.expectedResponses.length; ++i) {
            if (timeoutDevice.expectedResponses[i].DeviceOID === timeoutDeviceOID) {
                clearTimeout(timeoutDevice.expectedResponses[i].timeout);
                timeoutDevice.expectedResponses.splice(i, 1);
                if (timeoutErrorCB !== undefined){
                    timeoutErrorCB(IPConnection.ERROR_TIMEOUT);
                }
                return;
            }
        }
    };
    this.handleResponse = function (packetResponse) {
        var handleResponseDevice = this.devices[this.getUIDFromPacket(packetResponse)];
        var handleResponseFID = this.getFunctionIDFromPacket(packetResponse);
        var handleResponseSEQ = this.getSequenceNumberFromPacket(packetResponse);
        for (var i=0; i < handleResponseDevice.expectedResponses.length; i++) {
            if (this.devices[this.getUIDFromPacket(packetResponse)].expectedResponses[i].returnCB === undefined) {
                clearTimeout(handleResponseDevice.expectedResponses[i].timeout);
                handleResponseDevice.expectedResponses.splice(i, 1);
                return;
            }
            if (this.devices[this.getUIDFromPacket(packetResponse)].expectedResponses[i].unpackFormat === '') {
                clearTimeout(handleResponseDevice.expectedResponses[i].timeout);
                if (handleResponseDevice.expectedResponses[i].returnCB !== undefined) {
                    eval('handleResponseDevice.expectedResponses[i].returnCB();');
                }
                handleResponseDevice.expectedResponses.splice(i, 1);
                return;
            }
            if (handleResponseDevice.expectedResponses[i].FID === handleResponseFID &&
                    handleResponseDevice.expectedResponses[i].SEQ === handleResponseSEQ) {
            	if (this.getEFromPacket(packetResponse) === 1) {
                    clearTimeout(handleResponseDevice.expectedResponses[i].timeout);
            		if (this.devices[this.getUIDFromPacket(packetResponse)].expectedResponses[i].errorCB !== undefined) {
            			eval('handleResponseDevice.expectedResponses[i].errorCB(IPConnection.ERROR_INVALID_PARAMETER);');
            		}
            		handleResponseDevice.expectedResponses.splice(i, 1);
            		return;
            	}
            	if (this.getEFromPacket(packetResponse) === 2) {
                    clearTimeout(handleResponseDevice.expectedResponses[i].timeout);
            		if (this.devices[this.getUIDFromPacket(packetResponse)].expectedResponses[i].errorCB !== undefined) {
            			eval('handleResponseDevice.expectedResponses[i].errorCB(IPConnection.ERROR_FUNCTION_NOT_SUPPORTED);');
            		}
            		handleResponseDevice.expectedResponses.splice(i, 1);
            		return;
            	}
            	if (this.getEFromPacket(packetResponse) !== 0) {
                    clearTimeout(handleResponseDevice.expectedResponses[i].timeout);
            		if (this.devices[this.getUIDFromPacket(packetResponse)].expectedResponses[i].errorCB !== undefined) {
            			eval('handleResponseDevice.expectedResponses[i].errorCB(IPConnection.ERROR_UNKNOWN_ERROR);');
            		}
            		handleResponseDevice.expectedResponses.splice(i, 1);
            		return;
            	}
                clearTimeout(handleResponseDevice.expectedResponses[i].timeout);
                if (handleResponseDevice.expectedResponses[i].returnCB !== undefined) {
                	var retArgs = unpack(this.getPayloadFromPacket(packetResponse),
                			handleResponseDevice.expectedResponses[i].unpackFormat);
                	var evalStr = 'handleResponseDevice.expectedResponses[i].returnCB(';
                	for (var j=0; j<retArgs.length;j++) {
                		eval('var retSingleArg'+j+'=retArgs['+j+'];');
                		if (j != retArgs.length-1) {
                			evalStr += 'retSingleArg'+j+',';
                		} else {
                			evalStr += 'retSingleArg'+j+');';
                		}
                	}
                	eval(evalStr);
                }
                handleResponseDevice.expectedResponses.splice(i, 1);
                return;
            }
        }
    };
    this.handleCallback = function (packetCallback) {
        if (this.getFunctionIDFromPacket(packetCallback) === IPConnection.CALLBACK_ENUMERATE) {
            if (this.registeredCallbacks[IPConnection.CALLBACK_ENUMERATE] !== undefined) {
                var args = unpack(this.getPayloadFromPacket(packetCallback), 's8 s8 c B3 B3 H B');
                var evalCBString = 'this.registeredCallbacks[IPConnection.CALLBACK_ENUMERATE](';
                for (var i=0; i<args.length;i++) {
                    eval('var cbArg'+i+'=args['+i+'];');
                    if (i != args.length-1) {
                        evalCBString += 'cbArg'+i+',';
                    } else {
                        evalCBString += 'cbArg'+i+');';
                    }
                }
                eval(evalCBString);
                return;
            }
        }
        if (this.devices[this.getUIDFromPacket(packetCallback)] === undefined) {
            return;
        }
        if (this.devices[this.getUIDFromPacket(packetCallback)].
            registeredCallbacks[this.getFunctionIDFromPacket(packetCallback)] === undefined ||
            this.devices[this.getUIDFromPacket(packetCallback)].
            callbackFormats[this.getFunctionIDFromPacket(packetCallback)] === undefined) {
            return;
        }
        var cbFunction = this.devices[this.getUIDFromPacket(packetCallback)].
                         registeredCallbacks[this.getFunctionIDFromPacket(packetCallback)];
        var cbUnpackString = this.devices[this.getUIDFromPacket(packetCallback)].
                             callbackFormats[this.getFunctionIDFromPacket(packetCallback)];
        if (cbFunction == undefined) {
            return;
        }
        if (cbUnpackString == undefined) {
            return;
        }
        if (cbUnpackString === '') {
            eval('this.devices[this.getUIDFromPacket(packetCallback)].\
                    registeredCallbacks[this.getFunctionIDFromPacket(packetCallback)]();');
            return;
        }
        var args = unpack(this.getPayloadFromPacket(packetCallback), cbUnpackString);
        var evalCBString = 'this.devices[this.getUIDFromPacket(packetCallback)].\
                            registeredCallbacks[this.getFunctionIDFromPacket(packetCallback)](';
        if (args.length <= 0) {
            eval(evalCBString+');');
            return;
        }
        for (var i=0; i<args.length;i++) {
            eval('var cbArg'+i+'=args['+i+'];');
            if (i != args.length-1) {
                evalCBString += 'cbArg'+i+',';
            } else {
                evalCBString += 'cbArg'+i+');';
            }
        }
        eval(evalCBString);
        return;
    };
    this.handlePacket = function (packet) {
        if (this.getSequenceNumberFromPacket(packet) === 0) {
            this.handleCallback(packet);
        }
        if (this.getSequenceNumberFromPacket(packet) > 0) {
            this.handleResponse(packet);
        }
    };
    this.getConnectionState = function () {
        if (this.isConnected) {
            return IPConnection.CONNECTION_STATE_CONNECTED;
        }
        if (this.getCurrentTaskKind() === IPConnection.TASK_KIND_AUTO_RECONNECT) {
            return IPConnection.CONNECTION_STATE_PENDING;
        }
        return IPConnection.CONNECTION_STATE_DISCONNECTED;
    };
    this.setAutoReconnect = function (autoReconnect) {
        this.autoReconnect = autoReconnect;
    };
    this.getAutoReconnect = function () {
        return this.autoReconnect;
    };
    this.setTimeout = function (timeout) {
        this.timeout = timeout;
    };
    this.getTimeout = function () {
        return this.timeout;
    };
    this.enumerate = function (errorCallback) {
        if (this.getConnectionState() !== IPConnection.CONNECTION_STATE_CONNECTED) {
            if (errorCallback !== undefined) {
                errorCallback(IPConnection.ERROR_NOT_CONNECTED);
            }
            return;
        }
        this.socket.write(this.createPacketHeader(undefined, 8, IPConnection.FUNCTION_ENUMERATE), this.resetDisconnectProbe());
    };
    this.getRandomUInt32 = function (returnCallback) {
        if (process.browser) {
            if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
                var r = new Uint32Array(1);
                window.crypto.getRandomValues(r);
                returnCallback(r[0]);
            }
            else if (typeof window !== 'undefined' && window.msCrypto && window.msCrypto.getRandomValues) {
                var r = new Uint32Array(1);
                window.msCrypto.getRandomValues(r);
                returnCallback(r[0]);
            }
            else {
                // fallback to non-crypto random numbers
                returnCallback(Math.ceil(Math.random() * 4294967295));
            }
        }
        else {
            var crypto = require('crypto');
            crypto.randomBytes(4, function(error, buffer) {
                if (error) {
                    crypto.pseudoRandomBytes(4, function(error, buffer) {
                        if (error) {
                            returnCallback(Math.ceil(Math.random() * 4294967295));
                        }
                        else {
                            var data = new Buffer(buffer);
                            returnCallback(data.readUInt32LE(0));
                        }
                    });
                }
                else {
                    var data = new Buffer(buffer);
                    returnCallback(data.readUInt32LE(0));
                }
            });
        }
    };
    this.authenticateInternal = function (secret, returnCallback, errorCallback) {
        this.brickd.getAuthenticationNonce(function (serverNonce) {
            var serverNonceBytes = pack([serverNonce], 'B4');
            var clientNonceNumber = this.nextAuthenticationNonce++;
            var clientNonceBytes = pack([clientNonceNumber], 'I');
            var clientNonce = unpack(clientNonceBytes, 'B4')[0];
            var combinedNonceBytes = pack([serverNonce, clientNonce], 'B4 B4');
            var crypto = require('crypto');
            var hmac = crypto.createHmac('sha1', secret);

            hmac.update(combinedNonceBytes);

            var digestBytes = hmac.digest();
            var digest = unpack(digestBytes, 'B20')[0];

            this.brickd.authenticate(clientNonce, digest, function () {
                if (returnCallback !== undefined) {
                    returnCallback();
                }

                this.popTask();
            }.bind(this), function (error) {
                if (errorCallback !== undefined) {
                    errorCallback(error);
                }

                this.popTask();
            }.bind(this));
        }.bind(this), function (error) {
            if (errorCallback !== undefined) {
                errorCallback(error);
            }

            this.popTask();
        }.bind(this));
    };
    this.authenticate = function (secret, returnCallback, errorCallback) {
        // need to do authenticate() as a task because two authenticate() calls
        // are not allowed to overlap, otherwise the correct order of operations
        // in the handshake process cannot be guaranteed
        this.pushTask(function () {
            if (this.nextAuthenticationNonce === 0) {
                this.getRandomUInt32(function (r) {
                    this.nextAuthenticationNonce = r;
                    this.authenticateInternal(secret, returnCallback, errorCallback);
                }.bind(this));
            }
            else {
                this.authenticateInternal(secret, returnCallback, errorCallback);
            }
        }.bind(this), IPConnection.TASK_KIND_AUTHENTICATE);
    };
    this.on = function (FID, CBFunction) {
        this.registeredCallbacks[FID] = CBFunction;
    };
    this.getNextSequenceNumber = function () {
        if (this.nextSequenceNumber >= 15) {
            this.nextSequenceNumber = 0;
        }
        return ++this.nextSequenceNumber;
    };
    this.createPacketHeader = function (headerDevice, headerLength, headerFunctionID, headerErrorCB) {
        var UID = IPConnection.BROADCAST_UID;
        var len = headerLength;
        var FID = headerFunctionID;
        var seq = this.getNextSequenceNumber();
        var responseBits = 0;
        var EFutureUse = 0;
        var returnOnError = false;
        if (headerDevice !== undefined) {
            var responseExpected = headerDevice.getResponseExpected(headerFunctionID,
                function (errorCode) {
                    returnOnError = true;
                    if (headerErrorCB !== undefined) {
                        headerErrorCB(errorCode);
                    }
                }
            );
            if (returnOnError) {
                returnOnError = false;
                return;
            }
            UID = headerDevice.uid;
            if (responseExpected) {
                responseBits = 1;
            }
        }
        var seqResponseOOBits = seq << 4;
        if (responseBits) {
            seqResponseOOBits |= (responseBits << 3);
        }
        var returnHeader = new Buffer(8);
        returnHeader.writeUInt32LE(UID, 0);
        returnHeader.writeUInt8(len, 4);
        returnHeader.writeUInt8(FID, 5);
        returnHeader.writeUInt8(seqResponseOOBits, 6);
        returnHeader.writeUInt8(EFutureUse , 7);
        return returnHeader;
    };
    function bufferConcat(arrayOfBuffers) {
        var newBufferSize = 0;
        var targetStart = 0;
        for (var i = 0; i<arrayOfBuffers.length; i++) {
            newBufferSize += arrayOfBuffers[i].length;
        }
        var returnBufferConcat = new Buffer(newBufferSize);
        for (var j=0; j<arrayOfBuffers.length; j++) {
            arrayOfBuffers[j].copy(returnBufferConcat, targetStart);
            targetStart += arrayOfBuffers[j].length;
        }
        return returnBufferConcat;
    }
}

module.exports = IPConnection;
