var Thrift = require('./thrift-node');
var Type = Thrift.Type;
var ArrayBuffer = require('./typedarrays').ArrayBuffer;
var DataView = require('./typedarrays').DataView;
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 BinaryProtocol(trans, strictRead, strictWrite) {
    this.transport = this.trans = trans;
    this.strictRead = (strictRead !== undefined ? strictRead : false);
    this.strictWrite = (strictWrite !== undefined ? strictWrite : true);
}

BinaryProtocol.prototype.flush = function (onsuccess, onerror) {
    var wrapTransport;

    if (onsuccess) {
        wrapTransport = function (transport, abort) {
            onsuccess(new BinaryProtocol(transport), abort);
        };
    }

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

BinaryProtocol.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);
    }
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

BinaryProtocol.prototype.writeBinary = function (bytes) {
  var buffer;
  if(typeof bytes === "string") {
    bytes = BinaryParser.fromString(bytes);
  }
  if (bytes.length != null) {
    this.writeI32(bytes.length);
    buffer = new Buffer(bytes);
  // } else if (bytes.byteLength) {
  //   this.writeI32(bytes.byteLength);
  //   buffer = new Buffer(bytes);
  } else {
    throw Error("Cannot read length of binary data");
  }
  this.trans.write(buffer);
}; 

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

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

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

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

BinaryProtocol.prototype.writeString = function (str) {
    var bytes = BinaryParser.fromString(str);
    this.writeI32(bytes.byteLength);
    this.trans.write(bytes);
};

BinaryProtocol.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);
    }
};

BinaryProtocol.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
            };
        });
    });
};

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

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

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

BinaryProtocol.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};
        });
    });
};

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

BinaryProtocol.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;
    });
};

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

BinaryProtocol.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;
    });
};

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

BinaryProtocol.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;
    });
};

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

BinaryProtocol.prototype.readBool = function () {
    return this.readByte().then(function (byte) {
        return (b == 0);
    });
};

// ThriftJS expects values to be wrapped in an object with a prop named "value"
BinaryProtocol.prototype.readByte = function () {
    return this.trans.read(1).then(function (dataview) {
        return BinaryParser.toByte(dataview);
    });
};

BinaryProtocol.prototype.readI16 = function () {
    return this.trans.read(2).then(function (dataview) {
        return BinaryParser.toShort(dataview);
    });
};

BinaryProtocol.prototype.readI32 = function () {
    return this.trans.read(4).then(function (dataview) {
        return BinaryParser.toInt(dataview);
    });
};

BinaryProtocol.prototype.readI64 = function () {
    return this.trans.read(8).then(function (dataview) {
        return BinaryParser.toLong(dataview);
    });
};

BinaryProtocol.prototype.readDouble = function () {
    return this.trans.read(8).then(function (dataview) {
        return BinaryParser.toDouble(dataview);
    });
};

BinaryProtocol.prototype.readBinary = function () {
    var me = this;
    return this.readI32().then(function (len) {
        return me.trans.read(len);
    }).then(function (dataview) {
        return BinaryParser.toBytes(dataview);
    });
};

BinaryProtocol.prototype.readString = function () {
    var me = this;
    return this.readI32().then(function (len) {
        return me.trans.read(len);
    }).then(function (dataview) {
        return BinaryParser.toString(dataview);
    });
};

BinaryProtocol.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));
    }
};

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

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

BinaryProtocol.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();
        });
    });
};

BinaryProtocol.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();
    });
};

BinaryProtocol.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();
    });
};

BinaryProtocol.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();
    });
};

BinaryProtocol.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 ArrayBuffer(1);
    new DataView(buffer).setInt8(0, b);
    return buffer;
};

BinaryParser.fromShort = function (i16) {
    i16 = parseInt(i16);
    var buffer = new ArrayBuffer(2);
    new DataView(buffer).setInt16(0, i16);
    return buffer;
};

BinaryParser.fromInt = function (i32) {
    i32 = parseInt(i32);
    var buffer = new ArrayBuffer(4);
    new DataView(buffer).setInt32(0, i32);
    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 ArrayBuffer(8);
    var dataview = new DataView(buffer);
    for (var i = 0; i < 8; i++) {
        var uint8 = parseInt(bits.substr(8 * i, 8), 2);
        dataview.setUint8(i, uint8);
    }

    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 ArrayBuffer(8);
    new DataView(buffer).setFloat64(0, d);
    return buffer;
};

BinaryParser.fromString = function (s) {
    var utf8 = unescape(encodeURIComponent(s));
    return new ArrayBuffer(new Buffer(utf8));
};

BinaryParser.toByte = function (dataview) {
    return dataview.getUint8(0);
};

BinaryParser.toBytes = function (dataview) {
    var len = dataview.byteLength;
    var array = new global.window.Uint8Array(len);
    var i;
    for (i = 0; i < len; i++) {
        array[i] = dataview.getUint8(i);
    }
    return array;
};

BinaryParser.toShort = function (dataview) {
    return dataview.getInt16(0);
};

BinaryParser.toInt = function (dataview) {
    return dataview.getInt32(0);
};

BinaryParser.toLong = function (dataview) {
    // 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') + dataview.getUint8(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;
};

BinaryParser.toDouble = function (dataview) {
    return dataview.getFloat64(0);
};

BinaryParser.toString = function (dataview) {
    var s = '';
    var i;
    var len = dataview.byteLength;
    var hex;

    for (i = 0; i < len; i++) {
        hex = dataview.getUint8(i).toString(16);
        if (hex.length == 1) hex = '0' + hex;
        s += '%' + hex;
    }

    s = decodeURIComponent(s);
    return s;
};

exports.Protocol = BinaryProtocol;
