var _ = require("./lib");

// new Buffer() is deprecated in recent node. This ensures
// we always use the correct method for the current node.
function newBuffer(size) {
    if (Buffer.alloc) {
        return Buffer.alloc(size);
    } else {
        return new Buffer(size);
    }
}

function bufferFrom(content, encoding) {
    if (Buffer.from) {
        return Buffer.from(content, encoding);
    } else {
        return new Buffer(content, encoding);
    }
}

var entry = _.struct([
    _.char('filename',8),
    _.char('extension',3),
    _.struct('flags', [
        _.bool('readonly'),
        _.bool('hidden'),
        _.bool('system'),
        _.bool('volume'),
        _.bool('directory'),
        _.bool('archive'),
        _.ubit('reserved', 2)
    ].reverse()),
    _.byte('reserved', 4, 2),
    _.struct([
        _.char('reserved1'),
        _.ubit('reserved2', 8)
    ]),
    _.struct('time', [
        _.ubit('hour',5),
        _.ubit('minutes',6),
        _.ubit('seconds',5)
    ]),
    _.struct('date', [
        _.ubit('year',7),
        _.ubit('month',4),
        _.ubit('day',5)
    ]),
    _.uint16le('cluster'),
    _.uint32le('filesize')
]);

var obj0 = {filename:"autoexec", extension:"batch", flags:{reserved:0x82,archive:true}},
    _buf = entry.bytesFromValue(obj0),
    obj1 = entry.valueFromBytes(_buf);
console.log('',obj0, "\n==>\n", obj1);
//console.log(_buf);

console.log("\nRunning tests.");
function assert(b,msg) { if (!b) throw Error("Assertion failure. "+msg); else console.log(msg); }

console.log("  = API check =  ");
assert('fields' in entry, "Entry has fields property.");
assert('reserved' in entry.fields, "Entry fields contain 'reserved' bytefield.");
assert('reserved1' in entry.fields, "Entry fields contain hoisted 'reserved1' bytefield.");
assert('reserved2' in entry.fields, "Entry fields contain hoisted 'reserved2' bytefield.");
assert('time' in entry.fields, "Entry fields contain 'time' struct.");
assert(entry.fields.time.offset === 22, "Offset is correct for 'time' struct.");
assert('field' in entry.fields.reserved, "Reserved array allows access to underlying field");
assert(!('offset' in entry.fields.reserved.field), "Reserved array's underlying field does not have an offset…");
assert(entry.fields.reserved.offset === 12, "…but reserved array field itself does, and said offset is correct.");
assert(entry.fields.reserved2.offset.bytes === 21, "Hoisted field 'reserved2' has correct offset.");

console.log("  = Write check =  ");
var _bufKnown = bufferFrom("6175746f65786563626174a00000000000000000000000000000000000000000", 'hex');
assert(_bufKnown.length === 32, "Runtime parsed known buffer as 'hex'.");
assert(_bufKnown[0] === 0x61 && _bufKnown[7] === 0x63 && _bufKnown[31] === 0, "Known buffer parse passes spot check.");
assert(_buf.length === _bufKnown.length, "Buffer size matches");
for (var i = 0, len = _buf.length; i < len; ++i) assert(_buf[i] === _bufKnown[i], "Buffer contents match at "+i);

console.log("  = Read checks =  ");
assert(obj1.filename === obj0.filename, "Filename field matches");
assert(obj1.extension === obj0.extension.slice(0,3), "(Truncated) extension matches");
assert(obj1.flags.reserved === (obj0.flags.reserved & 0x03), "(Expected bits) of reserved flags match");
assert(obj1.flags.archive === true, "Archive bit set");
assert(obj1.flags.system === false, "System bit not set");
assert(Array.isArray(obj1.reserved), "Reserved array is an array");
assert(Buffer.isBuffer(obj1.reserved[0]), "Reserved array contains a buffer");
assert(obj1.reserved[1][0] === 0, "Reserved array buffer passes content sniff test");
assert(obj1.time.hour === 0, "Hour value as expected");
assert(obj1.cluster === 0, "Cluster value as expected");
assert(obj1.filesize === 0, "Filesize value as expected");

console.log("  = Unicode check = ");
var str = "\ud83c\udf91",
    ucs = _.char16le(8),
    b16 = ucs.bytesFromValue(str);
assert(b16[0] === 0x3c, "UTF-16 byte 0 as expected");
assert(b16[1] === 0xd8, "UTF-16 byte 1 as expected");
assert(b16[2] === 0x91, "UTF-16 byte 2 as expected");
assert(b16[3] === 0xdf, "UTF-16 byte 3 as expected");
assert(b16[4] === 0, "UTF-16 byte 4 as expected");
assert(b16[5] === 0, "UTF-16 byte 5 as expected");
assert(b16[6] === 0, "UTF-16 byte 6 as expected");
assert(b16[7] === 0, "UTF-16 byte 7 as expected");
assert(ucs.valueFromBytes(b16) === str, "UTF-16LE converted back correctly.");
var str = "\ud83c\udf91",
    ucs = _.char16be(8),
    b16 = ucs.bytesFromValue(str);
assert(b16[0] === 0xd8, "UTF-16BE byte 0 as expected");
assert(b16[1] === 0x3c, "UTF-16BE byte 1 as expected");
assert(b16[2] === 0xdf, "UTF-16BE byte 2 as expected");
assert(b16[3] === 0x91, "UTF-16BE byte 3 as expected");
assert(b16[4] === 0, "UTF-16BE byte 4 as expected");
assert(b16[5] === 0, "UTF-16BE byte 5 as expected");
assert(b16[6] === 0, "UTF-16BE byte 6 as expected");
assert(b16[7] === 0, "UTF-16BE byte 7 as expected");
assert(ucs.valueFromBytes(b16) === str, "UTF-16BE converted back correctly.");
var utf = _.char(4),
    b_8 = utf.bytesFromValue(str);
//console.log(b_8);
assert(b_8[0] === 0xF0, "UTF-8 byte 0 as expected");
assert(b_8[1] === 0x9F, "UTF-8 byte 1 as expected");
assert(b_8[2] === 0x8E, "UTF-8 byte 2 as expected");
assert(b_8[3] === 0x91, "UTF-8 byte 3 as expected");
assert(utf.valueFromBytes(b_8) === str, "UTF-8 converted back correctly.");


console.log("  = Bitfield check = ");
var bitle = _.struct([
    _.ubitLE('n', 8)
]), bufle = bitle.bytesFromValue({n:0x02});
assert(bufle.length === 1, "ubitLE buffer has correct size.");
assert(bufle[0] === 0x40, "ubitLE buffer has correct value.");
assert(bitle.valueFromBytes(bufle).n === 0x02, "ubitLE conversion back has original value.");

var bitzz = _.struct([
    _.bool('a'),
    _.ubit('b', 3),
    _.ubitLE('c', 3),
    _.sbit('d', 9)
]), bufzz = bitzz.bytesFromValue({a:true, b:1, c:1, d:-2}), backzz = bitzz.valueFromBytes(bufzz);
assert(bufzz.length === 2, "Bitfield buffer has correct size.");
//console.log((0x100+bufzz[0]).toString(2).slice(1), (0x100+bufzz[1]).toString(2).slice(1));
assert((bufzz[0] & 0x80) >>> 7 === 1, "Bitfield bool stored correctly.");
assert((bufzz[0] & 0x70) >>> 4 === 1, "Bitfield ubit stored correctly.");
assert((bufzz[0] & 0x0E) >>> 1 === 4, "Bitfield ubitLE stored correctly.");
assert((bufzz[0] & 0x01) >>> 0 === 1, "Bitfield sbit sign stored correctly.");
assert((bufzz[1] & 0xFF) >>> 0 === 2, "Bitfield sbit value stored correctly.");
assert(backzz.a === true, "Bitfield bool read back correctly.");
assert(backzz.b === 1, "Bitfield ubit read back correctly.");
assert(backzz.c === 1, "Bitfield ubitLE read back correctly.");
assert(backzz.d === -2, "Bitfield sbit read back correctly.");

console.log (" = Padding check = ");

var things = _.struct([
    _.bool('thing1'),
    _.padTo(7),
    _.uint8('thing2')
]);
assert(things.size === 8, "Padded structure has correct size.");
assert(things.fields.thing2.offset === 7, "Field after padding is at correct offset.");
var thingOut = things.bytesFromValue({thing2:0x99}, bufferFrom([0,1,2,3,4,5,6,7,8]), {bytes:1});
for (var i = 0; i < 8; ++i) assert(thingOut[i] === i, "Padded output has original value at index "+i);
assert(thingOut[i] === 0x99, "Padded output has correct value at index "+i);

var threw = false;
try {
    _.struct([
        _.int32('thing1'),
        _.int32('thing2'),
        _.padTo(7)
    ]);
} catch (e) {
  threw = e;
} finally {
    console.log("THREW", threw);
    assert(threw, "Invalid padding detected.");
}

console.log (" = Repetition checks = ");

assert(_.byte(0,0).size === 0, "Size of zero-length and zero-count field is zero.");
assert(_.byte(0,9).size === 0, "Size of zero-length and multi-count field is still zero.");
assert(_.byte(9,0).size === 0, "Size of zero-count of a field with length is still zero.");

var multiStruct = _.struct([_.uint8('n')], 2),
    msBuf = newBuffer(multiStruct.size),
    msArr = [];
msBuf.fill(0xFF);
msArr.push({n:0x42});
multiStruct.bytesFromValue(msArr, msBuf);
assert(msBuf[0] === 0x42, "First value set.");
assert(msBuf[1] === 0xFF, "Next value left.");
msArr.length = 2;
multiStruct.bytesFromValue(msArr, msBuf);
assert(msBuf[0] === 0x42, "First value still set.");
assert(msBuf[1] === 0x00, "Next value is cleared.");
msArr[1] = msArr[0];
multiStruct.bytesFromValue(msArr, msBuf);
assert(msBuf[1] === msArr[0].n, "Values as expected.");

var afterMulti = _.struct([_.uint8('nn', 2), _.uint8('n')]),
    amBuf = newBuffer(afterMulti.size);
amBuf.fill(0x01);
afterMulti.bytesFromValue({nn:[0x00], n:0x02}, amBuf);
assert(amBuf[0] === 0, "Array value correct.");
assert(amBuf[2] === 2, "After array in expected position.");
assert(amBuf[1] === 1, "Array missing correctly.");

var halfArray = _.struct([_.bool('nibble', 4), _.padTo('2')]),
    halfBuf = bufferFrom([0,0]);
halfArray.pack({nibble:[1,1,1,1, 1,1,1,1, 1,1,1,1]}, halfBuf);
assert(halfBuf[0] === 0xF0, "First byte set as expected when providing overlong array");
assert(halfBuf[1] === 0x00, "Second byte set as expected when providing overlong array");

console.log (" = New pack/unpack API = ");

var newAPI = _.struct([_.uint8('nn', 2), _.uint8('n')], 2),
    newBuf = newAPI.pack([{nn:[0xF0], n:0xF2}, {nn:[0xF1], n:0xF3}]),
    newArr = newAPI.unpack(newBuf);
assert(Buffer.isBuffer(newBuf), "New API still returns buffer");
assert(newBuf.length === 6, "New API buffer is correct length");
assert(Array.isArray(newArr), "New API unpacks expected object type");
assert(newArr.length === 2, "New API unpacked array is correct length");
assert(newArr[1].nn[0] === 0xF1, "…and contains expected value.");

console.log (" = Derived type = ");

var hexType = _.derive(_.uint32(2), function pack(hex) {
    return [ parseInt(hex.slice(0,8), 16), parseInt(hex.slice(8,16), 16) ]
}, function unpack(arr) {
    function _hex(n,ff) { return (n+ff+1).toString(16).slice(1).toUpperCase(); }
    return _hex(arr[0], 0xFFFFFFFF) + _hex(arr[1], 0xFFFFFFFF)
});

var bignums = ["01234567ABCDEF90", "8BADF00DC00010FF"],
    dervStruct = _.struct([_.char('header',8), hexType('bignums',2), _.bool('footer'), _.bool('pad',7)]),
    dervBuf = dervStruct.pack({header:"Hello.", bignums:bignums, footer:true}),
    dervObj = dervStruct.unpack(dervBuf);

assert(dervStruct.fields.bignums.offset === 8, "Derived field is at correct offset.");
assert(dervStruct.fields.footer.offset.bytes === 24, "Field following derived field is at correct byte offset.");
assert(dervStruct.fields.footer.offset.bits === 0, "Field following derived field is at correct bit offset.");
assert(dervBuf[8] === 0x01 && dervBuf[15] === 0x90, "Spot check of first packed value looks alright.");
assert(dervBuf[17] === 0xAD && dervBuf[22] === 0x10, "Spot check of second packed value looks alright.");
assert(dervBuf[24] === 0x80, "Following field packed as expected.");
assert(dervObj.header === "Hello.", "Preceding field is correct, yawn…");
assert(dervObj.bignums[0] === bignums[0], "First array item unpacked via derived field is correct.");
assert(dervObj.bignums[1] === bignums[1], "Second array item unpacked via derived field is correct.");
assert(dervObj.footer, "Following field value correct.");

var derInvert = _.derive(_.bool(), function (v) { return !v }, function (v) { return !v }),
    derString = _.derive(derInvert(), function (s) { return s === 'true' }, function (x) { return ''+x }),
    derBoolArray = _.struct([
      derString('vals', 3),
      _.padTo(2)
    ]),
    derBuf = bufferFrom("0000", 'hex'),
    __ = derBoolArray.pack({vals:['true', 'false', 'true', "extra"]}, derBuf),
    derObj = derBoolArray.unpack(derBuf);

assert(derBoolArray.size === 2, "Struct with doubly-derived field is correct size.");
assert((derBuf[0] & 0b10000000) === 0b00000000, "First bit in doubly-derived output is correct.");
assert((derBuf[0] & 0b01000000) === 0b01000000, "Second bit in doubly-derived output is correct.");
assert((derBuf[0] & 0b00100000) === 0b00000000, "Third bit in doubly-derived output is correct.");
assert(derBuf[0] === 0x40, "First byte packed fully as expected.");
assert(derBuf[1] === 0x00, "Padding after derived field packed as expected.");
assert(derObj.vals.length === 3, "Doubly-derived rountripped array has correct length.");
assert(derObj.vals[0] === 'true', "Doubly-derived first value round-tripped correctly.");
assert(derObj.vals[1] === 'false', "Doubly-derived second value round-tripped correctly.");
assert(derObj.vals[0] === 'true', "Doubly-derived third value round-tripped correctly.");

console.log (" = Out of bounds check = ");

var shortStruct = _.struct([
    _.ubit('a', 2),
    _.padTo(1),
    _.ubit('b', 2),
    _.padTo(2),
    _.ubit('c', 2),
    _.padTo(3)
]);

var packedShortStruct = shortStruct.pack({ a: 1, b: 2, c: 3 });
assert(packedShortStruct.length === 3, "Struct less than 32 bits packs successfully");

var unpackedShortStruct = shortStruct.unpack(packedShortStruct);
assert(unpackedShortStruct.a === 1, "First bit in unpacked < 32 bit structs is unpacked correctly");
assert(unpackedShortStruct.b === 2, "Second bit in unpacked < 32 bit structs is unpacked correctly");
assert(unpackedShortStruct.c === 3, "Third bit in unpacked < 32 bit structs is unpacked correctly");


console.log("\nAll tests passed!");
