// https://github.com/dfellis/node-udp-rpc

'use strict';
var dgram   = require('dgram');
var crypto  = Require('os/crypto.rand'); // var crypto  = require('crypto');
var util    = require('util');
var events  = require('events');
var async   = Require('rpc/async');
var version = "1.1.3";

// The header flag for udp-rpc packet reconstruction
var flags = {
    // Reserved: 1, 2, 4, 8, 16, 32, 64
    lastPacket: 128
};

// Helper function to generate Xor bytes
function bufXor(buf) {
    var out = 0;
    for (var i = 0; i < buf.length; i++) {
        out = out ^ buf.readUInt8(i, true);
    }
    return out;
}

// Helper function for generating a unique byte for the flag/id byte
function uniqId(usedList) {
    var id = 0;
    do {
        id = (Math.random() * (Math.pow(2, 8) - 1)) & 127; // Get a random byte and trim the top bit off
    } while (typeof usedList[id] != 'undefined')
    return id;
}

// Called when the client wants to call a remote rpc method
function execRpc(method, address) {
    if (!method || !address) {
        throw new Error("Did not receive a valid RPC request!");
    }
    // Construct the payload to send to the remote server
    var rpcParams = Array.prototype.slice.call(arguments, 2);
    var callback = rpcParams.length > 1 ? rpcParams.pop() : undefined;
    var addrArray = address.split(":");
    var messageId = this.genId();
    // var payload = new Buffer(([method].concat(messageId, rpcParams)).join(','));
    var payload = new Buffer(JSON.stringify([method,
        messageId,
    ].concat(rpcParams)));
    this.emit('execRpc', method, address, rpcParams, callback);
    var self = this;

    // we have to register callback handler in advance; maybe reply arrives earlier than
    // sendMessage callback is executed !?
    this.messages[messageId] = {
        callTs: new Date(),
        callback: callback,
        method: method,
        ip: addrArray[0],
        port: addrArray[1],
        rpcParams: rpcParams // This is for debugging purposes only
    };
    sendMessage.call(this, address, payload, function allSent(err) {
        self.emit('sentRpc', method, address, rpcParams, callback, err);
        if (err instanceof Error) {
          // delete callback handler
          delete self.messages[messageId];
          callback(err)
        } else {
          
          // nothing to do!?
        }
    });
}

function sendMessage(address, payload, callback) {
    var addrArray = address.split(":");

    // Construct the 6 byte header elements of the udp-rpc protocol for message verification/splitting
    if (!this.packets[address]) {
        this.packets[address] = {};
    }
    var currFlags = uniqId(this.packets[address]);
    var totXor = bufXor(payload);

    // Construct an array of payload fragments, each no larger than 494 bytes
    var payloads = [];
    if (payload.length > 494) {
        var i = 0;
        for (i = 0; i < payload.length; i += 494) {
            var newPayload = new Buffer(494);
            payload.copy(newPayload, 0, i, i + 493);
            payloads.push(newPayload);
        }
        if (i < payload.length) {
            var newPayload = new Buffer(payload.length - i);
            payload.copy(newPayload, 0, i, payload.length - 1);
            payloads.push(newPayload);
        }
    } else {
        payloads.push(payload);
    }
    var self = this;
    // For each payload fragment, generate the payload header, construct the output message, and send it
    async.forEach(payloads, function sendPayload(payload, callback) {
        var message = new Buffer(payload.length + 6);
        var packetNumber = payloads.indexOf(payload);
        currFlags &= 127;
        message.writeUInt32BE(packetNumber, 1, true);
        var curXor = bufXor(payload);
        if (packetNumber == payloads.length - 1) {
            currFlags |= flags.lastPacket;
            message.writeUInt8(totXor, 5, true);
        } else {
            message.writeUInt8(curXor, 5, true);
        }
        message.writeUInt8(currFlags, 0, true);
        payload.copy(message, 6);
        self.stats.send++;
        self.stats.sendData += message.length;
        self.dgram.send(message, 0, message.length, addrArray[1], addrArray[0], function(err) {
            if (err instanceof Error) {
                return callback(err);
            }
            return callback();
        });
        // When all are sent, or an error occurred, execute the callback
    }, callback);
}

// Called when a remote message is received. This could be an rpc request or response
function receiveRpc(message, info) {
    this.emit('receivedPacket', message, info);
    // Extract the header and body of the message
    var header = {
        flagid: message.readUInt8(0, true),
        packetNumber: message.readUInt32BE(1, true),
        xor: message.readUInt8(5, true)
    };
    var body = message.slice(6);
    this.stats.recv++;
    this.stats.recvData += message.length;
    var source = info.address + ":" + info.port;
    if (!this.packets[source]) {
        this.packets[source] = {};
    }
    if (!this.packets[source][header.flagid]) {
        this.packets[source][header.flagid] = [];
    }
    this.packets[source][header.flagid].push({
        header: header,
        body: body
    });
    attemptProcessing.call(this, source, header.flagid);
}

function attemptProcessing(source, id) {
    // Get the packet array to work on
    var packetArray = this.packets[source][id];
    // Tag the set of first and last packets in the packet array
    var integerSum = 0;
    var totalMessageLen = 0;
    var lastPacket = false;
    for (var i = 0; i < packetArray.length; i++) {
        integerSum += packetArray[i].header.packetNumber + 1;
        totalMessageLen += packetArray[i].body.length;
        if (!!(packetArray[i].header.flagid & flags.lastPacket)) {
            lastPacket = true;
        }
    }
    // If there is no last packet, processing impossible
    // Also, if the sum of packet numbers isn't n(n+1)/2, there must be a missing packet
    if (!lastPacket || integerSum != (packetArray.length * (packetArray.length + 1) / 2)) {
        return;
    }
    // Sort the array of packets based on packet number, since we can process this data
    packetArray = packetArray.sort(function(a, b) {
        return a.header.packetNumber - b.header.packetNumber;
    });
    // Build the full message buffer from the sorted packets
    var fullMessage = new Buffer(totalMessageLen);
    for (var i = 0, j = 0; i < packetArray.length; j += packetArray[i].body.length, i++) {
        packetArray[i].body.copy(fullMessage, 0, j);
    }
    // Remove the saved packets from the packets object and pass the message to the processMessage
    delete this.packets[source][id];
    processMessage.call(this, fullMessage, source);
}

function processMessage(message, source) {
    var sourceArr = source.split(':');
    // var messageArr = message.toString('utf8').split(',');
    try {
      var messageArr = JSON.parse(message.toString('utf8'));
    } catch (e) { console.log(e) }
    // console.log('processMessage',messageArr[0])
    // Determine type of message: RPC response, request
    if (!this.oneway && typeof this.messages[messageArr[0]] == 'object' &&
        sourceArr[1] == this.messages[messageArr[0]].port) {
        // If a response, the message array consists of the request id and response results
        // Find the appropriate callback
        this.emit('receivedRpcResponse', message, source);
        this.messages[messageArr[0]].callback.apply(this, messageArr.slice(1));
        delete this.messages[messageArr[0]];
    } else if (typeof this.methods[messageArr[0]] == 'function') {
        // If a request, the message array consists of the request method, id, and parameters, in that order
        this.emit('receivedRpcRequest', message, source);
        var messageId = messageArr[1];
        var params = messageArr.slice(2);
        // Parameters sent to the RPC method start with the source string, in case they need to know who is calling (simple state tracking)
        params.unshift(source);
        var self = this;
        // Automatically insert a callback to the params passed to the RPC method to handle the results
        if (!this.oneway) params.push(function rpcCallback() {
            // var payload = new Buffer(([messageId].concat(Array.prototype.slice.call(arguments, 0))).join(','));
            var payload = new Buffer(JSON.stringify([messageId].concat((Array.prototype.slice.call(arguments, 0)))));
            sendMessage.call(self, source, payload, function(err) {
                if (err instanceof Error) {
                    throw err;
                } // RPC server non-functional, this is fatal
            });
        });
        // Execute the requested RPC method
        this.methods[messageArr[0]].apply(this, params);
    } else {
        // If packet is not understood, ignore it
        console.log('receivedUnknownMessage',messageArr)
        this.emit('receivedUnknownMessage', message, source);
    }
}

// UDP-RPC object to provide a nice interface to the protocol and properly initialize/destroy it.
// I can't find a way to execute a function on ``delete`` like you can with get/set, so destroy this
// object with the ``die`` method.
function udpRpc(ipType, srvPort, methods, oneway) {
    var options = {};
    events.EventEmitter.call(this);
    if (typeof oneway == 'object') {
        options = oneway;
        oneway = options.oneway;
    }
    this.methods = {};
    this.messages = {};
    this.packets = {};
    this.state = 'starting';
    this.stats = {
        recv  : 0, // #msgs
        send  : 0,
        recvData : 0,   // #Bytes
        sendData : 0,
    }
    for (var i in methods) {
        Object.defineProperty(this, methods[i].name, {
            enumerable: true,
            configurable: false,
            writable: false,
            value: execRpc.bind(this, methods[i].name)
        });
        Object.defineProperty(this.methods, methods[i].name, {
            enumerable: true,
            configurable: false,
            writable: false,
            value: methods[i]
        });
    }
    this.genId = function() {
        var id = "";
        do {
            try {
                id = crypto.randomBytes(3).toString('base64');
            } catch (e) {
                id = new Buffer(Math.random() * (Math.pow(2, 24) - 1)).toString('base64');
            }
        } while (typeof this.messages[id] != 'undefined')
        return id;
    };
    this.srvPort = srvPort;
    this.address = undefined;
    var self = this;
    process.nextTick(function() {
        self.dgram = dgram.createSocket(ipType);
        self.dgram.on('listening', function udpRpcStart() {
            self.address = self.dgram.address();
            self.state = 'listening';
            if (options.verbose) console.log('udpRpc: listening on', self.address);
            self.emit('init');
        });
        self.dgram.on('message', receiveRpc.bind(self));
        self.dgram.bind(srvPort); // Currently hardwired; will look into alternatives
    });
    this.die = function die() {
        this.dgram.close();
        this.emit('death');
        delete this;
    };
    this.oneway = oneway || false;
    return this;
}
util.inherits(udpRpc, events.EventEmitter);

exports = module.exports = udpRpc;
