// requires local modules: ast2100, ast2100idct, ast2100const, ast2100util
/* jshint expr: true */

var assert = chai.assert;
var expect = chai.expect;


// Convert a hex string, optionally containing spaces, into an array of integers representing bytes.
var parseHex = function (s) {
    s = s.replace(/\s/g, '');
    if (s.length % 2 != 0)
        throw 'Hex data has uneven length!';
    for (var bytes = [], i = 0; i < s.length; i += 2)
        bytes.push(parseInt(s.substr(i, 2), 16));
    return bytes;
};


function make_decoder () {
    return new Ast2100Decoder({
        width: 0x400,
        height: 0x300,
        blitCallback: function () {}
    });
}

// Swap u32 byte order.  Mutates input!
function buf_swap32 (data) {
    for (var i = 0; i < data.length; i += 4) {
        var tmp;
    
        tmp = data[i+0];
        data[i+0] = data[i+3];
        data[i+3] = tmp;
        
        tmp = data[i+1];
        data[i+1] = data[i+2];
        data[i+2] = tmp;
    }
    
    return data;
}


describe('ATEN_AST2100 video encoding', function() {
    "use strict";
    
    var dec;
    beforeEach(function () {
        dec = make_decoder();
        dec._mcuLimit = 10;
    });

    describe('BitStream', function() {
        var stream, stream2, stream3;
        var data = buf_swap32(parseHex('f0f0 cccc cccc cccc cccc cccc cccc cccc'));
        var data2 = parseHex('F0F1F2F3 F4F5F6F7 F8F9FAFB FCFDFEFF');
        var data3 = parseHex('0b0b01bc45fbc5e020040101ffff3f20c0ffffff0000240000000000000000005028140a0000000000000000');
        
        beforeEach(function () {
            stream = new BitStream({data: data});
            stream2 = new BitStream({data: data2});
            stream3 = new BitStream({data: data3});
        });

        it('should pass simple sanity checks', function () {
            expect(stream.read(4)).to.equal(0xF);
            expect(stream.read(4)).to.equal(0x0);
            expect(stream.read(4)).to.equal(0xF);
            expect(stream.read(4)).to.equal(0x0);
        });
        
        it('should pass simple sanity checks (part II)', function () {
            var i;
            for (i = 0; i < 4; ++i)
                expect(stream.read(1)).to.equal(1);
            for (i = 0; i < 4; ++i)
                expect(stream.read(1)).to.equal(0);
        });
        
        it('should be able to properly refill the buffer', function () {
            expect(stream.read(4)).to.equal(0xF);
            expect(stream.read(4)).to.equal(0x0);
            expect(stream.read(4)).to.equal(0xF);
            expect(stream.read(8)).to.equal(0x0C);
        });
        
        it('should properly swap byte order', function () {
            expect(stream2.read(8)).to.equal(0xF3);
            expect(stream2.read(8)).to.equal(0xF2);
            expect(stream2.read(8)).to.equal(0xF1);
            expect(stream2.read(8)).to.equal(0xF0);
            expect(stream2.read(8)).to.equal(0xF7);
        });
        
        it('should properly handle skipping the full 32-bit read-buffer (data3)', function () {
            // Must do this in two parts so that bits < 32 each time.
            stream3.skip(16);
            stream3.skip(16);
            expect(stream3.read(4)).to.equal(0xE);
        });
        
        // it('issue repro', function () {
        //     var data = parseHex('f0f0 cccc cccc cccc cccc cccc cccc cccc');
        //     var stream = new BitStream({data: data});
        //     expect(stream.read(31)).to.equal(0xF0F0CCCC >>> 1);
        //     expect(stream.read(8)).to.equal(0x66);
        // });        
    });
             
    describe('quant tables', function() {
        it('quant tables are properly loaded and scaled', function () {
            // This is luma quant table #4.
            var expected = Int32Array.from([
                0x00090000, 0x0008527e, 0x00068866, 0x000a9537, 0x000d0000, 0x00114908, 0x000f274b, 0x0009616d,
                0x0008527e, 0x000b8b14, 0x000caf8f, 0x00104f53, 0x00136b26, 0x0022df8f, 0x0018c594, 0x000b7b02,
                0x0009255c, 0x000caf8f, 0x000f5d2c, 0x0013f8fd, 0x001cbe90, 0x0020d994, 0x001adebc, 0x000b2cc4,
                0x00083b2b, 0x000eadca, 0x00126faf, 0x00161f78, 0x0020ecad, 0x002c58a1, 0x001ca316, 0x000b07c7,
                0x000a0000, 0x0010a4fc, 0x001a219a, 0x002473bf, 0x00260000, 0x002fed69, 0x001ed922, 0x000bdd19,
                0x000a36ca, 0x0014b4bd, 0x001ecbfa, 0x00214279, 0x00235b34, 0x0023cdea, 0x001ac9de, 0x000b0e2f,
                0x000e9cbf, 0x001b0616, 0x001e67d4, 0x001e8bd4, 0x001ed922, 0x001cea24, 0x00139fb4, 0x00085c96,
                0x000b0935, 0x00138450, 0x00131afd, 0x0011d7e1, 0x001161b4, 0x000c23a7, 0x000882d0, 0x00042fc6
            ]);

            dec._loadQuantTable(0, ATEN_QT_LUMA[4]);
            var luma_quant_table = dec.quantTables[0];
            expect(luma_quant_table).to.deep.equal(expected);
        });
    });
    
    describe('IDCT', function () {

        var outputBuf;
        
        beforeEach(function () {
            outputBuf = new Uint8Array(DCTSIZE2);  // equivalent to one of the componentBufs in Ast2100Decoder.
        });

        // TODO: Fix typing: variables should be typed arrays.
        it('test case 0 - DC value only', function () {
            
            // this is the output of the VLC / entropy coding process
            // XXX: @KK: Why is this 16-bit?
            var dataUnit = [  // new Int16Array(
                0xFF9C,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];

            var expected_idct_output = Uint8Array.from([  // new Uint8Array(
                0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,
                0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,
                0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF]);

            dec._loadQuantTable(0, ATEN_QT_LUMA[4]);
            var luma_quant_table = dec.quantTables[0];
            
            AST2100IDCT.idct_fixed_aan(luma_quant_table, dataUnit, outputBuf);

            // console.log(result);

            console.log('expected:');
            console.log(fmt_u8a(expected_idct_output));
            console.log('result:');
            console.log(fmt_u8a(outputBuf));
            
            expect(outputBuf).to.deep.equal(expected_idct_output);
            
        });

        it('test case 1', function () {
            // this is the output of the VLC / entropy coding process
            // XXX: @KK: Why is this 16-bit?
            var dataUnit = new Int16Array([
                0xFFBD,0,0,0,0,0,0,0,
                0xFFC3,0,0,0,0,0,0,0,
                0x26,0,0,0,0,0,0,0,
                0xFFED,0,0,0,0,0,0,
                0,0,0,0,0,0,0,0,0,
                6,0,0,0,0,0,0,0,
                0xFFFC,0,0,0,0,0,0,0,
                2,0,0,0,0,0,0,0]);

            var expected = Uint8Array.from([
                0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,
                0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,
                0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,
                0xE,0xE,0xE,0xE,0xE,0xE,0xE,0xE,
                0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,
                0xF,0xF,0xF,0xF,0xF,0xF,0xF,0xF,
                0x9F,0x9F,0x9F,0x9F,0x9F,0x9F,0x9F,0x9F,
                0xA1,0xA1,0xA1,0xA1,0xA1,0xA1,0xA1,0xA1]);

            dec._loadQuantTable(0, ATEN_QT_LUMA[4]);
            var luma_quant_table = dec.quantTables[0];

            AST2100IDCT.idct_fixed_aan(luma_quant_table, dataUnit, outputBuf);

            // console.log(result);

            console.log('expected:');
            console.log(fmt_u8a(expected));
            console.log('result:');
            console.log(fmt_u8a(outputBuf));

            // XXX: TEMPORARY -- figure out this rounding issue
            // expect(result).to.deep.equal(expected);
            var maxErr = 0;
            for (i = 0; i < 64; ++i)
                maxErr = Math.max(maxErr, Math.abs(outputBuf[i] - expected[i]));
            if (maxErr > 1)
                throw 'Error too high!';
            
        });
        
        it('test case 2', function () {
            // this is the output of the VLC / entropy coding process
            // XXX: @KK: Why is this 16-bit?
            
            var dataUnit = Int16Array.from([
                0xFF9B, 0, 0, 0, 0, 0, 0, 0, 0xFFA4, 0, 0, 0, 0, 0, 0, 0, 0x35, 0, 0, 0, 0, 0, 0, 0,
                0xFFE6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0xFFFA,
                0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]);
            
            var expected = [
                0xE, 0xE, 0xE, 0xE, 0xE, 0xE, 0xE, 0xE, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
                0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
                0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0xD, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14,
                0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0x9C, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1];

             // Between IDCT passes, the workspace should look like this:
            //  [-903, 0, 0, 0, 0, 0, 0, 0, -874, 0, 0, 0, 0, 0, 0, 0, -914, 0, 0, 0, 0, 0, 0, 0, -874, 0, 0, 0, 0, 0, 0, 0,
            //   -915, 0, 0, 0, 0, 0, 0, 0, -868, 0, 0, 0, 0, 0, 0, 0, 230, 0, 0, 0, 0, 0, 0, 0, 266, 0, 0, 0, 0, 0, 0, 0]
            
            
            dec._loadQuantTable(0, ATEN_QT_LUMA[5]);
            var luma_quant_table = dec.quantTables[0];

            AST2100IDCT.idct_fixed_aan(luma_quant_table, dataUnit, outputBuf);

            // console.log(result);

            console.log('expected:');
            console.log(fmt_u8a(expected));
            console.log('result:');
            console.log(fmt_u8a(outputBuf));

            // XXX: TEMPORARY -- figure out this rounding issue
            // expect(result).to.deep.equal(expected);
            var maxErr = 0;
            for (i = 0; i < 64; ++i)
                maxErr = Math.max(maxErr, Math.abs(outputBuf[i] - expected[i]));
            if (maxErr > 1)
                throw 'Error too high!';
        });
        
    });

    describe('VQ', function () {
        
        it('should successfully load a codebook (colors)', function () {
            var data = buf_swap32(parseHex('bc010b0b e0c5fb45 01010420 203fffff ffffffc0 00240000 00000000 00000000 0a142850'));
            data = data.slice(4);

            var stream = new BitStream({data: data});  // Strip off quant table selectors and subsampling mode.
            var controlFlag = stream.read(4);
            expect(controlFlag).to.equal(0xE);

            var xMcuPos = stream.read(8), yMcuPos = stream.read(8);
            expect(xMcuPos).to.equal(0xC);
            expect(yMcuPos).to.equal(0x5F);
            
            dec._stream = stream;
            dec._parseVqBlock(1);  // codewordSize=1
            expect(dec._vqCodewordLookup).to.deep.equal([1, 0, 2, 3]);
            expect(dec._vqCodebook).to.deep.equal([
                [0x10, 0x80, 0x80],
                [0xA2, 0x80, 0x80],
                [0x80, 0x80, 0x80],
                [0xC0, 0x80, 0x80]
            ]);
        });

        it('should correctly handle multiple data blocks', function () {
            // This data is from a memory dump.  It should contain two VQ (0xE-type) blocks and then an 0x9 (end-frame).
            var data = buf_swap32(parseHex('bc010b0b e0c5fb45 01010420 203fffff ffffffc0 00240000 00000000 00000000 0a142850' + '00000000 00000000'));
            console.log('VQ multiple block data:');
            console.log(fmt_u8a(data));
            dec.decode(data);
            
            // TODO: improve asserts/etc. beyond just 'finished without error'
        });
        
    });

    describe('Full JPEG subsampled MCU example 0', function () {
        // Corresponds to whole-mcu-example / main_mcu_test_2()
        var data = parseHex('040701a61bff6280f81fbaa2ff4dbc408dfccf405c15ff1f004800f500000000000000002dd4a462237ced2c9c25dca4cef7e6667d6626f3308063c3c7a1b7335badc24aaf0b5e9daf69590fcb96206b59e6f2ccb2bdd386b17dbb72182080d6288a2a1f2f0608a0fe87ae287f132f1023ff33d057c5ff470012403d0000000000000000ec1fb06cdacd2bdf9d79f3cde7f9f1362b7ce914ba521c79524a5bdc212a5ed47c3cbc6c7e072ae3e3c18384785f6ca6d4e22fb1af0c96449d1847a9c7037e966fa3ac8d4990830339463d277f25fc6f30ffe5f4f5cfec9f4ffff8bf00a0f5d358a2ab6e4e6778d929f686bd58ca9c26b8f7338f15105065dd39c718e219a78e');

        
        it('should decode properly', function () {
            // DOES NOT APPEAR TO correspond to my notes in 'whole-mcu-example.txt'.
            
            dec._blitCallback = function (x, y, width, height, buf) {
                console.log({x:x, y:y, width:width, height:height, buf:buf});
                console.log(fmt_rgb_buf(width, buf));
            };

            dec.decode(data);

        });
        
    });
    
    describe('Full JPEG subsampled MCU example 1', function () {

        /* TODO: this looks VERRRRRY SIMILAR to the above example */
        
        var data = parseHex('040701a61bff6280f81fbaa2ff4dbc408dfccf405c15ff1f004800f500000000000000002dd4a462237ced2c9c25dca4cef7e6667d6626f3308063c3c7a1b7335badc24aaf0b5e9daf69590fcb96206b59e6f2ccb2bdd386b17dbb72182080d6288a2a1f2f0608a0fe87ae287f132f1023ff33d057c5ff470012403d0000000000000000ec1fb06cdacd2bdf9d79f3cde7f9f1362b7ce914ba521c79524a5bdc212a5ed47c3cbc6c7e072ae3e3c18384785f6ca6d4e22fb1af0c96449d1847a9c7037e966fa3ac8d4990830339463d277f25fc6f30ffe5f4f5cfec9f4ffff8bf00a0f5d358a2ab6e4e6778d929f686bd58ca9c26b8f7338f15105065dd39c718');

        /*
        it('should load quant tables and set subsamplingMode', function () {
            dec.decode(data);

            expect(dec._loadedQuantTables[0]).to.equal(4);
            expect(dec._loadedQuantTables[1]).to.equal(7);
            expect(dec.subsamplingMode).to.equal(422);
        });
         */
        
        it('should decode properly', function () {
            // DOES NOT APPEAR TO correspond to my notes in 'whole-mcu-example.txt'.
            
            dec._blitCallback = function (x, y, width, height, buf) {
                console.log({x:x, y:y, width:width, height:height, buf:buf});
                console.log(fmt_rgb_buf(width, buf));
            };

            dec.decode(data);

        });
    });

    describe('Full frame decode test', function () {

        it('should decode properly', function () {

            throw "Won't work without jQuery (or some other way of loading data).";
            
            dec._blitCallback = function (x, y, width, height, buf) {
                if (x == 0 && y == 0) {
                    console.log({x:x, y:y, width:width, height:height, buf:buf});
                    console.log(fmt_rgb_buf(width, buf));
                }
            };

            // TODO: This path won't work for other people, and the test case needs to be made to understand that this
            // is async; plus, I had to manually add jQuery to the generated HTML.
            $.get("/novnc-tests/tests/frame4.hex", function (data) {
                data = parseHex(data);
                dec.decode(data);
            });

        });
            
        
    });

    describe('Frame update decode test', function () {

        it('should decode properly', function () {
            var data = parseHex('050501a69a3f6080008aa2a8000000900000000000000000');
            console.log(data);

            // this data should NOT be solid-white!

            dec._blitCallback = function (x, y, width, height, buf) {
                console.log({x:x, y:y, width:width, height:height, buf:buf});
                console.log(fmt_rgb_buf(width, buf));
            };

            dec.decode(data);
        });

    });
    
});
