var Thrift = require('thrift');
var Type = Thrift.Type;
var Promise = require('bluebird');

// NastyHaxx. JavaScript forces hex constants to be
// positive, converting this into a long. If we hardcode the int value
// instead it'll stay in 32 bit-land.

var VERSION_MASK = -65536, // 0xffff0000
    VERSION_1 = -2147418112, // 0x80010000
    TYPE_MASK = 0x000000ff;

function AsyncBinaryProtocol(trans, strictRead, strictWrite) {
    this.transport = this.trans = trans;
    this.strictRead = (strictRead !== undefined ? strictRead : false);
    this.strictWrite = (strictWrite !== undefined ? strictWrite : true);
}

AsyncBinaryProtocol.prototype.flush = function (callback) {
    var wrapTransport;

    if (callback) {
        wrapTransport = function (err, transport) {
            var protocol;
            if (transport) protocol = new AsyncBinaryProtocol(transport);
            return callback(err, protocol);
        };
    }

    return this.trans.flush(wrapTransport);
};

AsyncBinaryProtocol.prototype.writeMessageBegin = function (name, type, seqid) {
    if (this.strictWrite) {
        this.writeI32(VERSION_1 | type);
        this.writeString(name);
        this.writeI32(seqid);
    } else {
        this.writeString(name);
        this.writeByte(type);
        this.writeI32(seqid);
    }
};

AsyncBinaryProtocol.prototype.writeMessageEnd = function () {
};

AsyncBinaryProtocol.prototype.writeStructBegin = function (name) {
};

AsyncBinaryProtocol.prototype.writeStructEnd = function () {
};

AsyncBinaryProtocol.prototype.writeFieldBegin = function (name, type, id) {
    this.writeByte(type);
    this.writeI16(id);
};

AsyncBinaryProtocol.prototype.writeFieldEnd = function () {
};

AsyncBinaryProtocol.prototype.writeFieldStop = function () {
    this.writeByte(Type.STOP);
};

AsyncBinaryProtocol.prototype.writeMapBegin = function (ktype, vtype, size) {
    this.writeByte(ktype);
    this.writeByte(vtype);
    this.writeI32(size);
};

AsyncBinaryProtocol.prototype.writeMapEnd = function () {
};

AsyncBinaryProtocol.prototype.writeListBegin = function (etype, size) {
    this.writeByte(etype);
    this.writeI32(size);
};

AsyncBinaryProtocol.prototype.writeListEnd = function () {
};

AsyncBinaryProtocol.prototype.writeSetBegin = function (etype, size) {
    this.writeByte(etype);
    this.writeI32(size);
};

AsyncBinaryProtocol.prototype.writeSetEnd = function () {
};

AsyncBinaryProtocol.prototype.writeBool = function (bool) {
    if (bool) {
        this.writeByte(1);
    } else {
        this.writeByte(0);
    }
};

AsyncBinaryProtocol.prototype.writeByte = function (b) {
    this.trans.write(BinaryParser.fromByte(b));
};

AsyncBinaryProtocol.prototype.writeBinary = function (bytes) {
    if(typeof bytes === "string") {
        bytes = BinaryParser.fromString(bytes);
    } else if ((bytes instanceof Buffer) == false && bytes.length != null && bytes.byteLength != null && bytes.byteOffset != null) {
		// Assume UInt8Array
		bytes = new Buffer(bytes);
	}
	
    if (bytes.length != null) {
        this.writeI32(bytes.length);
    } else {
        throw Error("Cannot read length of binary data");
    }
    this.trans.write(bytes);
}; 

AsyncBinaryProtocol.prototype.writeI16 = function (i16) {
    this.trans.write(BinaryParser.fromShort(i16));
};

AsyncBinaryProtocol.prototype.writeI32 = function (i32) {
    this.trans.write(BinaryParser.fromInt(i32));
};

AsyncBinaryProtocol.prototype.writeI64 = function (i64) {
    this.trans.write(BinaryParser.fromLong(i64));
};

AsyncBinaryProtocol.prototype.writeDouble = function (dub) {
    this.trans.write(BinaryParser.fromDouble(dub));
};

AsyncBinaryProtocol.prototype.writeString = function (str) {
    var buffer = BinaryParser.fromString(str);
    this.writeI32(buffer.length);
    this.trans.write(buffer);
};

AsyncBinaryProtocol.prototype.writeType = function(type, value) {
    switch (type) {
        case Type.BOOL:
            return this.writeBool(value);
        case Type.BYTE:
            return this.writeByte(value);
        case Type.I16:
            return this.writeI16(value);
        case Type.I32:
            return this.writeI32(value);
        case Type.I64:
            return this.writeI64(value);
        case Type.DOUBLE:
            return this.writeDouble(value);
        case Type.STRING:
            return this.writeString(value);
        case Type.BINARY:
            return this.writeBinary(value);
//            case Type.STRUCT:
//            case Type.MAP:
//            case Type.SET:
//            case Type.LIST:
        default:
            throw Error("Invalid type: " + type);
    }
};

AsyncBinaryProtocol.prototype.readMessageBegin = function () {
    var me = this;

    return me.readI32().then(function (size) {
        var signaturePromise;
        var signature = {
            type: null,
            name: null,
            seqid: null
        };

        if (size < 0) {
            // size written at server: -2147418110 == 0x80010002
            var version = size & VERSION_MASK;
            if (version != VERSION_1) {
                console.log("BAD: " + version);
                throw Error("Bad version in readMessageBegin: " + size);
            }
            signature.type = size & TYPE_MASK;

            signaturePromise = me.readString().then(function (name) {
                signature.name = name;
                return me.readI32();
            }).then(function (seqid) {
                signature.seqid = seqid;
            });

        } else {
            if (me.strictRead) {
                throw Error("No protocol version header");
            }

            signaturePromise = me.trans.read(size).then(function (name) {
                signature.name = name;
                return me.readByte();
            }).then(function (type) {
                signature.type = type;
                return me.readI32();
            }).then(function (seqid) {
                signature.seqid = seqid;
            });
        }

        return signaturePromise.then(function () {
            return {
                fname: signature.name, 
                mtype: signature.type, 
                seqid: signature.seqid
            };
        });
    });
};

AsyncBinaryProtocol.prototype.readMessageEnd = function () {
    if (this.trans.emit) this.trans.emit('readMessageEnd');
    return Promise.resolve();
};

AsyncBinaryProtocol.prototype.readStructBegin = function () {
    return Promise.resolve({fname: ''}); // Where is this return value used? Can it be removed?
};

AsyncBinaryProtocol.prototype.readStructEnd = function () {
    return Promise.resolve();
};

AsyncBinaryProtocol.prototype.readFieldBegin = function () {
    var me = this;

    return this.readByte().then(function (type) {
        if (type == Type.STOP) {
            return {fname: null, ftype: type, fid: 0};
        }
        return me.readI16().then(function (id) {
            return {fname: null, ftype: type, fid: id};
        });
    });
};

AsyncBinaryProtocol.prototype.readFieldEnd = function () {
    return Promise.resolve();
};

AsyncBinaryProtocol.prototype.readMapBegin = function () {
    // Add variables required by thrift generated js code but not needed for BinaryHttpTransport
    var me = this;
    var result = {
        ktype: null, 
        vtype: null, 
        size: null
    };

    return this.readByte().then(function (ktype) {
        result.ktype = ktype;
        return me.readByte();
    }).then(function (vtype) {
        result.vtype = vtype;
        return me.readI32();
    }).then(function (size) {
        result.size = size;
        return result;
    });
};

AsyncBinaryProtocol.prototype.readMapEnd = function () {
    return Promise.resolve();
};

AsyncBinaryProtocol.prototype.readListBegin = function () {
    var me = this;
    var result = {
        etype: null,
        size: null
    };
    return this.readByte().then(function (etype) {
        result.etype = etype;
        return me.readI32();
    }).then(function (size) {
        result.size = size;
        return result;
    });
};

AsyncBinaryProtocol.prototype.readListEnd = function () {
    return Promise.resolve();
};

AsyncBinaryProtocol.prototype.readSetBegin = function () {
    var me = this;
    var result = {
        etype: null,
        size: null
    };
    return this.readByte().then(function (etype) {
        result.etype = etype;
        return me.readI32();
    }).then(function (size) {
        result.size = size;
        return result;
    });
};

AsyncBinaryProtocol.prototype.readSetEnd = function () {
    return Promise.resolve();
};

AsyncBinaryProtocol.prototype.readBool = function () {
    return this.readByte().then(function (byte) {
        return (byte == 1);
    });
};

// ThriftJS expects values to be wrapped in an object with a prop named "value"
AsyncBinaryProtocol.prototype.readByte = function () {
    return this.trans.read(1).then(function (buffer) {
        var result = buffer.readUInt8(0)
        return result;
    });
};

AsyncBinaryProtocol.prototype.readI16 = function () {
    return this.trans.read(2).then(function (buffer) {
        var result = buffer.readInt16BE(0);
        return result;
    });
};

AsyncBinaryProtocol.prototype.readI32 = function () {
    return this.trans.read(4).then(function (buffer) {
        var result = buffer.readInt32BE(0);
        return result;
    });
};

AsyncBinaryProtocol.prototype.readI64 = function () {
    return this.trans.read(8).then(function (buffer) {
        var result = BinaryParser.toLong(buffer);
        return result;
    });
};

AsyncBinaryProtocol.prototype.readDouble = function () {
    return this.trans.read(8).then(function (buffer) {
        var result = buffer.readDoubleBE(0);
        return result;
    });
};

AsyncBinaryProtocol.prototype.readBinary = function () {
    // Returns Uint8Array for node webkit apps instead of Node Buffer
    var me = this;
    return this.readI32().then(function (len) {
        return Promise.all([len, me.trans.read(len)]);
    }).spread(function (len, buffer) {
        var i;
        var array = new global.window.Uint8Array(len);
        for (i = 0; i < len; i++) {
            array[i] = buffer.readUInt8(i);
        }
        return array;
    });
};

AsyncBinaryProtocol.prototype.readString = function () {
    var me = this;
    return this.readI32().then(function (len) {
        return me.trans.read(len);
    }).then(function (buffer) {
        var result = buffer.toString();
        return result;
    });
};

AsyncBinaryProtocol.prototype.readType = function(type) {
    switch (type) {
        case Type.BOOL:
            return this.readBool();
        case Type.BYTE:
            return this.readByte();
        case Type.I16:
            return this.readI16();
        case Type.I32:
            return this.readI32();
        case Type.I64:
            return this.readI64();
        case Type.DOUBLE:
            return this.readDouble();
        case Type.STRING:
            return this.readString();
        case Type.BINARY:
            return this.readBinary();
//            case Type.STRUCT:
//            case Type.MAP:
//            case Type.SET:
//            case Type.LIST:
        default:
            return Promise.reject(new Error("Invalid type: " + type));
    }
};

AsyncBinaryProtocol.prototype.getTransport = function () {
    return this.trans;
};

AsyncBinaryProtocol.prototype.skipStruct = function() {
    var me = this;
    return this.readStructBegin().then(function () {
        return me.skipFields();
    }).then(function () {
        return me.readStructEnd();
    });
};

AsyncBinaryProtocol.prototype.skipFields = function() {
    var me = this;
    return this.readFieldBegin().then(function (r) {
        if (r.ftype === Type.STOP) return;

        return me.skip(r.ftype).then(function () {
            return me.readFieldEnd();
        }).then(function () {
            return me.skipFields();
        });
    });
};

AsyncBinaryProtocol.prototype.skipMap = function() {
    var me = this;
    var i = 0;
    var promises = [];
    return this.readMapBegin().then(function (map) {
        for (i = 0; i < map.size; i++) {
            promises.push(me.skip(map.ktype));
            promises.push(me.skip(map.vtype));
        }
        return Promise.all(promises);
    }).then(function () {
        return this.readMapEnd();
    });
};

AsyncBinaryProtocol.prototype.skipSet = function() {
    var me = this;
    var i = 0;
    var promises = [];
    return this.readSetBegin().then(function (set) {
        for (i = 0; i < set.size; i++) {
            promises.push(me.skip(set.etype));
        }
        return Promise.all(promises);
    }).then(function () {
        return this.readSetEnd();
    });
};

AsyncBinaryProtocol.prototype.skipList = function() {
    var me = this;
    var i = 0;
    var promises = [];
    return this.readListBegin().then(function (list) {
        for (i = 0; i < list.size; i++) {
            promises.push(me.skip(list.etype));
        }
        return Promise.all(promises);
    }).then(function () {
        return this.readListEnd();
    });
};

AsyncBinaryProtocol.prototype.skip = function(type) {
    // console.log("skip: " + type);
    switch (type) {
        case Type.STOP:
            return;
        case Type.BOOL:
            return this.readBool();
        case Type.BYTE:
            return this.readByte();
        case Type.I16:
            return this.readI16();
        case Type.I32:
            return this.readI32();
        case Type.I64:
            return this.readI64();
        case Type.DOUBLE:
            return this.readDouble();
        case Type.STRING:
            return this.readString();
        case Type.STRUCT:
            return this.skipStruct();
        case Type.MAP:
            return this.skipMap();
        case Type.SET:
            return this.skipSet();
        case Type.LIST:
            return this.skipList();
        case Type.BINARY:
            return this.readBinary();
        default:
            return Promise.reject(Error("Invalid type: " + type));
    }
};


var BinaryParser = {};

BinaryParser.fromByte = function (b) {
    var buffer = new Buffer(1);
    buffer.writeInt8(b, 0);
    return buffer;
};

BinaryParser.fromShort = function (i16) {
    i16 = parseInt(i16);
    var buffer = new Buffer(2);
    buffer.writeInt16BE(i16, 0);
    return buffer;
};

BinaryParser.fromInt = function (i32) {
    i32 = parseInt(i32);
    var buffer = new Buffer(4);
    buffer.writeInt32BE(i32, 0);
    return buffer;
};

BinaryParser.fromLong = function (n) {
    n = parseInt(n);
    if (Math.abs(n) >= Math.pow(2, 53)) {
        throw new Error('Unable to accurately transfer numbers larger than 2^53 - 1 as integers. '
            + 'Number provided was ' + n);
    }

    var bits = (Array(64).join('0') + Math.abs(n).toString(2)).slice(-64);
    if (n < 0) bits = this.twosCompliment(bits);

    var buffer = new Buffer(8);
    for (var i = 0; i < 8; i++) {
        var uint8 = parseInt(bits.substr(8 * i, 8), 2);
        buffer.writeUInt8(uint8, i);
    }
    return buffer;
};

BinaryParser.twosCompliment = function (bits) {
    // Convert to two's compliment using string manipulation because bitwise operator is limited to 32 bit numbers
    var smallestOne = bits.lastIndexOf('1');
    var left = bits.substring(0, smallestOne).
        replace(/1/g, 'x').
        replace(/0/g, '1').
        replace(/x/g, '0');
    bits = left + bits.substring(smallestOne);
    return bits;
};

BinaryParser.fromDouble = function (d) {
    var buffer = new Buffer(8);
    buffer.writeDoubleBE(d, 0);
    return buffer;
};

BinaryParser.fromString = function (s) {
        var len = Buffer.byteLength(s);
        var buffer = new Buffer(len);
        buffer.write(s);
        return buffer;
};

BinaryParser.toLong = function (buffer) {
    // Javascript does not support 64-bit integers. Only decode values up to 2^53 - 1.
    var sign = 1;
    var bits = '';
    for (var i = 0; i < 8; i++) {
        bits += (Array(8).join('0') + buffer.readUInt8(i).toString(2)).slice(-8);
    }

    if (bits[0] === '1') {
        sign = -1;
        bits = this.twosCompliment(bits);
    }
    var largestOne = bits.indexOf('1');
    if (largestOne != -1 && largestOne < 64 - 54) throw new Error('Unable to receive number larger than 2^53 - 1 as an integer');

    return parseInt(bits, 2) * sign;
};

module.exports = AsyncBinaryProtocol;
