var min = require('osc-min');
var dgram = require('dgram');
var util = require('util');
var events = require('events');
var jspack = require('jspack').jspack;

////////////////////
// OSC Message
////////////////////


function Message(address) {
    this.oscType = "message";
    this.address = address;
    this.args = [];
    
    for (var i = 1; i < arguments.length; i++) {
        this.append(arguments[i]);
    }
}

Message.prototype = {
    append: function (arg) {
        switch (typeof arg) {
        case 'object':
            if (arg.type) {
                this.args.push(arg);
            } else {
                throw new Error("don't know how to encode object " + arg)
            }
            break;
        case 'number':
            if (Math.floor(arg) == arg) {
                var argOut = new Argument('integer', arg);
                this.args.push(argOut);
            } else {
                var argOut = new Argument('float', arg);
                this.args.push(argOut);
            }
            break;
        case 'string':
            var argOut = new Argument('string', arg);
            this.args.push(argOut);
            break;
        default:
            throw new Error("don't know how to encode " + arg);
        }   
    }
}

exports.Message = Message;

function Argument(type, value){
    this.type = type;
    this.value = value;
}

////////////////////
// OSC Client
////////////////////

var Client = function (host, port) {
    this.host = host;
    this.port = port;
    this._sock = dgram.createSocket('udp4');
}

Client.prototype = {
    send: function (message) {
        switch (typeof message) {
            case 'object':
                var buf = min.toBuffer(message);
                this._sock.send(buf, 0, buf.length, this.port, this.host);
                break;
            case 'string':
                mes = new Message(arguments[0]);
                for (var i = 1; i < arguments.length; i++) {
                    mes.append(arguments[i]);
                }
                var buf = min.toBuffer(mes);
                this._sock.send(buf, 0, buf.length, this.port, this.host);
                break;
            default:
                throw new Error("That Message Just Doesn't Seem Right");
        }
    }                                                  
}

exports.Client = Client;

////////////////////
// OSC Message encoding and decoding functions
////////////////////

function ShortBuffer(type, buf, requiredLength)
{
    this.type = "ShortBuffer";
    var message = "buffer [";
    for (var i = 0; i < buf.length; i++) {
        if (i) {
            message += ", ";
        }
        message += buf.charCodeAt(i);
    }
    message += "] too short for " + type + ", " + requiredLength + " bytes required";
    this.message = message;
}

function TString (value) { this.value = value; }
TString.prototype = {
    typetag: 's',
    decode: function (data) {
        var end = 0;
        while (data[end] && end < data.length) {
            end++;
        }
        if (end == data.length) {
            throw Error("OSC string not null terminated");
        }
        this.value = data.toString('ascii', 0, end);
        var nextData = parseInt(Math.ceil((end + 1) / 4.0) * 4);
        return data.slice(nextData);
    },
    encode: function (buf, pos) {
        var len = Math.ceil((this.value.length + 1) / 4.0) * 4;
        return jspack.PackTo('>' + len + 's', buf, pos, [ this.value ]);
    }
}
exports.TString = TString;

function TInt (value) { this.value = value; }
TInt.prototype = {
    typetag: 'i',
    decode: function (data) {
        if (data.length < 4) {
            throw new ShortBuffer('int', data, 4);
        }

        this.value = jspack.Unpack('>i', data.slice(0, 4))[0];
        return data.slice(4);
    },
    encode: function (buf, pos) {
        return jspack.PackTo('>i', buf, pos, [ this.value ]);
    }
}
exports.TInt = TInt;

function TTime (value) { this.value = value; }
TTime.prototype = {
    typetag: 't',
    decode: function (data) {
        if (data.length < 8) {
            throw new ShortBuffer('time', data, 8);
        }
        var raw = jspack.Unpack('>LL', data.slice(0, 8));
        var secs = raw[0];
        var fracs = raw[1];
        this.value = secs + fracs / 4294967296;
        return data.slice(8);
    },
    encode: function (buf, pos) {
        return jspack.PackTo('>LL', buf, pos, this.value);
    }
}
exports.TTime = TTime;

function TFloat (value) { this.value = value; }
TFloat.prototype = {
    typetag: 'f',
    decode: function (data) {
        if (data.length < 4) {
            throw new ShortBuffer('float', data, 4);
        }

        this.value = jspack.Unpack('>f', data.slice(0, 4))[0];
        return data.slice(4);
    },
    encode: function (buf, pos) {
        return jspack.PackTo('>f', buf, pos, [ this.value ]);
    }
}
exports.TFloat = TFloat;

function TBlob (value) { this.value = value; }
TBlob.prototype = {
    typetag: 'b',
    decode: function (data) {
        var length = jspack.Unpack('>i', data.slice(0, 4))[0];
        var nextData = parseInt(Math.ceil((length) / 4.0) * 4) + 4;
        this.value = data.slice(4, length + 4);
        return data.slice(nextData);
    },
    encode: function (buf, pos) {
        var len = Math.ceil((this.value.length) / 4.0) * 4;
        return jspack.PackTo('>i' + len + 's', buf, pos, [len, this.value]);
    }
}
exports.TBlob = TBlob;

function TDouble (value) { this.value = value; }
TDouble.prototype = {
    typetag: 'd',
    decode: function (data) {
        if (data.length < 8) {
            throw new ShortBuffer('double', data, 8);
        }
        this.value = jspack.Unpack('>d', data.slice(0, 8))[0];
        return data.slice(8);
    },
    encode: function (buf, pos) {
        return jspack.PackTo('>d', buf, pos, [ this.value ]);
    }
}
exports.TDouble = TDouble;

// for each OSC type tag we use a specific constructor function to decode its respective data
var tagToConstructor = { 'i': function () { return new TInt },
                         'f': function () { return new TFloat },
                         's': function () { return new TString },
                         'b': function () { return new TBlob },
                         'd': function () { return new TDouble } };

function decode (data) {
    // this stores the decoded data as an array
    var message = [];

    // we start getting the <address> and <rest> of OSC msg /<address>\0<rest>\0<typetags>\0<data>
    var address = new TString;
    data = address.decode(data);

    // Checking if we received a bundle (typical for TUIO/OSC)
    if (address.value == '#bundle') {
        var time = new TTime;
        data = time.decode(data);

        message.push('#bundle');
        message.push(time.value);

        var length, part;
        while(data.length > 0) {
            length = new TInt;
            data = length.decode(data);

            part = data.slice(0, length.value);
            //message = message.concat(decode(part));
            message.push(decode(part));

            data = data.slice(length.value, data.length);
        }

    } else if (data.length > 0) {
        message.push(address.value);

        // if we have rest, maybe we have some typetags... let see...

        // now we advance on the old rest, getting <typetags>
        var typetags = new TString;
        data = typetags.decode(data);
        typetags = typetags.value;
        // so we start building our message list
        if (typetags[0] != ',') {
            throw "invalid type tag in incoming OSC message, must start with comma";
        }
        for (var i = 1; i < typetags.length; i++) {
            var constructor = tagToConstructor[typetags[i]];
            if (!constructor) {
                throw "Unsupported OSC type tag " + typetags[i] + " in incoming message";
            }
            var argument = constructor();
            data = argument.decode(data);
            message.push(argument.value);
        }
    }

    return message;
};



////////////////////
// OSC Server
////////////////////

var Server = function(port, host) {
    events.EventEmitter.call(this);
    var _callbacks = [];
    this.port = port;
    this.host = host;
    this._sock = dgram.createSocket('udp4');
    this._sock.bind(port);
    var server = this;
    this._sock.on('message', function (msg, rinfo) {
        try {
            var decoded = decode(msg);
            // [<address>, <typetags>, <values>*] 
            if (decoded) {
                server.emit('message', decoded, rinfo);
                server.emit(decoded[0], decoded, rinfo);
            }
        }
        catch (e) {
            console.log("can't decode incoming message: " + e.message);
        }
    });
}

util.inherits(Server, events.EventEmitter);

exports.Server = Server;


