/*
Copyright (c) 2015 Timur Gafarov

Boost Software License - Version 1.0 - August 17th, 2003

Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:

The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/

// dimage is actually stripped out part of dlib - just to support reading PNG and JPEG
module dimage.jpeg;

version = USE_DIMAGE;
version(USE_DIMAGE):


import std.stdio;
import std.algorithm;
import std.string;
import std.traits;

import dimage.huffman;
import dimage.stream;
import dimage.compound;
import dimage.array;
//import dimage.color;
import dimage.image;
import dimage.bitio;
import dimage.memory;
import dimage.idct;
//import dlib.core.memory;
//import dlib.core.stream;
//import dlib.core.compound;
//import dlib.container.array;
//import dlib.filesystem.local;
//import dlib.image.color;
//import dlib.image.image;
//import dlib.image.io.idct;

//import dlib.core.bitio;
//import dlib.coding.huffman;

/*
 * Simple JPEG decoder
 *
 * Limitations:
 *  - Doesn't support progressive JPEG
 *  - Doesn't perform chroma interpolation
 *  - Doesn't read EXIF metadata
 */

// Uncomment this to see debug messages
//version = JPEGDebug;

T readNumeric(T) (InputStream istrm, Endian endian = Endian.Little)
if (is(T == ubyte))
{
    ubyte b;
    istrm.readBytes(&b, 1);
    return b;
}

T readNumeric(T) (InputStream istrm, Endian endian = Endian.Little)
if (is(T == ushort))
{
    union U16
    {
        ubyte[2] asBytes;
        ushort asUshort;
    }
    U16 u16;
    istrm.readBytes(u16.asBytes.ptr, 2);
    version(LittleEndian)
    {
        if (endian == Endian.Big)
            return u16.asUshort.swapEndian16;
        else
            return u16.asUshort;
    }
    else
    {
        if (endian == Endian.Little)
            return u16.asUshort.swapEndian16;
        else
            return u16.asUshort;
    }
}

char[size] readChars(size_t size) (InputStream istrm)
{
    char[size] chars;
    istrm.readBytes(chars.ptr, size);
    return chars;
}

/*
 * JPEG-related Huffman coding
 */

struct HuffmanCode
{
    ushort bits;
    ushort length;

    auto bitString()
    {
        return .bitString(bits, length);
    }
}

struct HuffmanTableEntry
{
    HuffmanCode code;
    ubyte value;
}

DynamicArray!char bitString(T)(T n, uint len = 1) if (isIntegral!T)
{
    DynamicArray!char arr;

    const int size = T.sizeof * 8;

    bool s = 0;
    for (int a = 0; a < size; a++)
    {
        bool bit = n >> (size - 1);
        if (bit)
            s = 1;
        if (s)
        {
            arr.append(bit + '0');
        }
        n <<= 1;
    }

    while (arr.length < len)
        arr.appendLeft('0');

    return arr;
}

HuffmanTreeNode* emptyNode()
{
    return New!HuffmanTreeNode(null, null, cast(ubyte)0, 0, false);
}

HuffmanTreeNode* treeFromTable(DynamicArray!(HuffmanTableEntry) table)
{
    HuffmanTreeNode* root = emptyNode();

    foreach(i, v; table.data)
        treeAddCode(root, v.code, v.value);

    return root;
}

void treeAddCode(HuffmanTreeNode* root, HuffmanCode code, ubyte value)
{
    HuffmanTreeNode* node = root;
    auto bs = code.bitString;
    foreach(bit; bs.data)
    {
        if (bit == '0')
        {
            if (node.left is null)
            {
                node.left = emptyNode();
                node.left.parent = node;
            }

            node = node.left;
        }
        else if (bit == '1')
        {
            if (node.right is null)
            {
                node.right = emptyNode();
                node.right.parent = node;
            }

            node = node.right;
        }
    }
    assert (node !is null);
    node.ch = value;
    bs.free();
}

/*
 * JPEG-related data types
 */

enum JPEGMarkerType
{
    Unknown,
    SOI,
    SOF0,
    SOF1,
    SOF2,
    DHT,
    DQT,
    DRI,
    SOS,
    RSTn,
    APP0,
    APPn,
    COM,
    EOI
}

struct JPEGImage
{
    struct JFIF
    {
        ubyte versionMajor;
        ubyte versionMinor;
        ubyte units;
        ushort xDensity;
        ushort yDensity;
        ubyte thumbnailWidth;
        ubyte thumbnailHeight;
        ubyte[] thumbnail;

        void free()
        {
            if (thumbnail.length)
                Delete(thumbnail);
        }
    }

    struct DQT
    {
        ubyte precision;
        ubyte tableId;
        ubyte[] table;

        void free()
        {
            if (table.length)
                Delete(table);
        }
    }

    struct SOF0Component
    {
        ubyte hSubsampling;
        ubyte vSubsampling;
        ubyte dqtTableId;
    }

    struct SOF0
    {
        ubyte precision;
        ushort height;
        ushort width;
        ubyte componentsNum;
        SOF0Component[] components;

        void free()
        {
            if (components.length)
                Delete(components);
        }
    }

    struct DHT
    {
        ubyte clas;
        ubyte tableId;
        DynamicArray!HuffmanTableEntry huffmanTable;
        HuffmanTreeNode* huffmanTree;

        void free()
        {
            huffmanTree.free();
            Delete(huffmanTree);
            huffmanTable.free();
        }
    }

    struct SOSComponent
    {
        ubyte tableIdDC;
        ubyte tableIdAC;
    }

    struct SOS
    {
        ubyte componentsNum;
        SOSComponent[] components;
        ubyte spectralSelectionStart;
        ubyte spectralSelectionEnd;
        ubyte successiveApproximationBitHigh;
        ubyte successiveApproximationBitLow;

        void free()
        {
            if (components.length)
                Delete(components);
        }
    }

    JFIF jfif;
    DQT[] dqt;
    SOF0 sof0;
    DHT[] dht;
    SOS sos;

    DQT* addDQT()
    {
        if (dqt.length > 0)
            reallocateArray(dqt, dqt.length+1);
        else
            dqt = New!(DQT[])(1);
        return &dqt[$-1];
    }

    DHT* addDHT()
    {
        if (dht.length > 0)
            reallocateArray(dht, dht.length+1);
        else
            dht = New!(DHT[])(1);
        return &dht[$-1];
    }

    void free()
    {
        jfif.free();
        foreach(ref t; dqt) t.free();
        Delete(dqt);
        sof0.free();
        foreach(ref t; dht) t.free();
        Delete(dht);
        sos.free();
    }

    DQT* getQuantizationTable(ubyte id)
    {
        foreach(ref t; dqt)
            if (t.tableId == id)
                return &t;
        return null;
    }

    DHT* getHuffmanTable(ubyte clas, ubyte id)
    {
        foreach(ref t; dht)
            if (t.clas == clas &&
                t.tableId == id)
                return &t;
        return null;
    }
}

/*
 * Load JPEG from file using local FileSystem.
 * Causes GC allocation
 */
//SuperImage loadJPEG(string filename)
//{
//    InputStream input = openForInput(filename);
//    auto img = loadJPEG(input);
//    input.close();
//    return img;
//}

/*
 * Load JPEG from stream using default image factory.
 * Causes GC allocation
 */
SuperImage loadJPEG(InputStream istrm)
{
    Compound!(SuperImage, string) res =
        loadJPEG(istrm, defaultImageFactory);
    if (res[0] is null)
        throw new Exception(res[1]);
    else
        return res[0];
}

/*
 * Load JPEG from stream using specified image factory.
 * GC-free
 */
Compound!(SuperImage, string) loadJPEG(
    InputStream istrm,
    SuperImageFactory imgFac)
{
    JPEGImage jpg;
    SuperImage img = null;

    while (istrm.readable)
    {
        JPEGMarkerType mt;
        auto res = readMarker(&jpg, istrm, &mt);
        if (res[0])
        {
            // TODO: add progressive JPEG support
            if (mt == JPEGMarkerType.SOF2)
            {
                jpg.free();
                return compound(img, "loadJPEG error: progressive JPEG is not supported");
            }
            else if (mt == JPEGMarkerType.SOS)
                break;
        }
        else
        {
            jpg.free();
            return compound(img, res[1]);
        }
    }
    auto res = decodeScanData(&jpg, istrm, imgFac);
    jpg.free();
    return res;
}

/*
 *  Decode marker from JPEG stream
 */
Compound!(bool, string) readMarker(
    JPEGImage* jpg,
    InputStream istrm,
    JPEGMarkerType* mt)
{
    ushort magic = istrm.readNumeric!ushort(Endian.Big);

    switch (magic)
    {
        case 0xFFD8:
            *mt = JPEGMarkerType.SOI;
            version(JPEGDebug) writeln("SOI");
            break;

        case 0xFFE0:
            *mt = JPEGMarkerType.APP0;
            return readJFIF(jpg, istrm);

        case 0xFFE1:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 1);

        case 0xFFE2:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 2);

        case 0xFFE3:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 3);

        case 0xFFE4:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 4);

        case 0xFFE5:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 5);

        case 0xFFE6:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 6);

        case 0xFFE7:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 7);

        case 0xFFE8:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 8);

         case 0xFFE9:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 9);

         case 0xFFEA:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 10);

         case 0xFFEB:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 11);

         case 0xFFEC:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 12);

         case 0xFFED:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 13);

         case 0xFFEE:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 14);

         case 0xFFEF:
            *mt = JPEGMarkerType.APPn;
            return readAPPn(jpg, istrm, 15);

        case 0xFFDB:
            *mt = JPEGMarkerType.DQT;
            return readDQT(jpg, istrm);

        case 0xFFC0:
            *mt = JPEGMarkerType.SOF0;
            return readSOF0(jpg, istrm);

        case 0xFFC2:
            *mt = JPEGMarkerType.SOF2;
            break;

        case 0xFFC4:
            *mt = JPEGMarkerType.DHT;
            return readDHT(jpg, istrm);

        case 0xFFDA:
            *mt = JPEGMarkerType.SOS;
            return readSOS(jpg, istrm);

        case 0xFFFE:
            *mt = JPEGMarkerType.COM;
            return readCOM(jpg, istrm);

        default:
            *mt = JPEGMarkerType.Unknown;
            break;
    }

    return compound(true, "");
}

Compound!(bool, string) readJFIF(JPEGImage* jpg, InputStream istrm)
{
    ushort jfif_length = istrm.readNumeric!ushort(Endian.Big);

    char[5] jfif_id = istrm.readChars!5;
    if (jfif_id != "JFIF\0")
        return compound(false, "loadJPEG error: illegal JFIF header");

    jpg.jfif.versionMajor = istrm.readNumeric!ubyte;
    jpg.jfif.versionMinor = istrm.readNumeric!ubyte;
    jpg.jfif.units = istrm.readNumeric!ubyte;
    jpg.jfif.xDensity = istrm.readNumeric!ushort(Endian.Big);
    jpg.jfif.yDensity = istrm.readNumeric!ushort(Endian.Big);
    jpg.jfif.thumbnailWidth = istrm.readNumeric!ubyte;
    jpg.jfif.thumbnailHeight = istrm.readNumeric!ubyte;

    uint jfif_thumb_length = jpg.jfif.thumbnailWidth * jpg.jfif.thumbnailHeight * 3;
    if (jfif_thumb_length > 0)
    {
        jpg.jfif.thumbnail = New!(ubyte[])(jfif_thumb_length);
        istrm.readBytes(jpg.jfif.thumbnail.ptr, jfif_thumb_length);
    }

    version(JPEGDebug)
    {
        writefln("APP0/JFIF length: %s", jfif_length);
        writefln("APP0/JFIF identifier: %s", jfif_id);
        writefln("APP0/JFIF version major: %s", jpg.jfif.versionMajor);
        writefln("APP0/JFIF version minor: %s", jpg.jfif.versionMinor);
        writefln("APP0/JFIF units: %s", jpg.jfif.units);
        writefln("APP0/JFIF xdensity: %s", jpg.jfif.xDensity);
        writefln("APP0/JFIF ydensity: %s", jpg.jfif.yDensity);
        writefln("APP0/JFIF xthumbnail: %s", jpg.jfif.thumbnailWidth);
        writefln("APP0/JFIF ythumbnail: %s", jpg.jfif.thumbnailHeight);
    }

    return compound(true, "");
}

/*
 * APP1 - EXIF, XMP, ExtendedXMP, QVCI, FLIR
 * APP2 - ICC, FPXR, MPF, PreviewImage
 * APP3 - Kodak Meta, Stim, PreviewImage
 * APP4 - Scalado, FPXR, PreviewImage
 * APP5 - RMETA, PreviewImage
 * APP6 - EPPIM, NITF, HP TDHD
 * APP7 - Pentax, Qualcomm
 * APP8 - SPIFF
 * APP9 - MediaJukebox
 * APP10 - PhotoStudio comment
 * APP11 - JPEG-HDR
 * APP12 - PictureInfo, Ducky
 * APP13 - Photoshop, Adobe CM
 * APP14 - Adobe
 * APP15 - GraphicConverter
 */
Compound!(bool, string) readAPPn(JPEGImage* jpg, InputStream istrm, uint n)
{
    ushort app_length = istrm.readNumeric!ushort(Endian.Big);
    ubyte[] app = New!(ubyte[])(app_length-2);
    istrm.readBytes(app.ptr, app_length-2);

    // TODO: interpret APP data (EXIF etc.) and save it somewhere.
    // Maybe add a generic ImageInfo object for this?
    Delete(app);

    version(JPEGDebug)
    {
        writefln("APP%s length: %s", n, app_length);
    }

    return compound(true, "");
}

Compound!(bool, string) readCOM(JPEGImage* jpg, InputStream istrm)
{
    ushort com_length = istrm.readNumeric!ushort(Endian.Big);
    ubyte[] com = New!(ubyte[])(com_length-2);
    istrm.readBytes(com.ptr, com_length-2);

    version(JPEGDebug)
    {
        writefln("COM string: \"%s\"", cast(string)com);
        writefln("COM length: %s", com_length);
    }

    // TODO: save COM data somewhere.
    // Maybe add a generic ImageInfo object for this?
    Delete(com);

    return compound(true, "");
}

Compound!(bool, string) readDQT(JPEGImage* jpg, InputStream istrm)
{
    ushort dqt_length = istrm.readNumeric!ushort(Endian.Big);
    version(JPEGDebug)
    {
        writefln("DQT length: %s", dqt_length);
    }

    dqt_length -= 2;

    while(dqt_length)
    {
        JPEGImage.DQT* dqt = jpg.addDQT();

        ubyte bite = istrm.readNumeric!ubyte;
        dqt.precision = bite.hiNibble;
        dqt.tableId = bite.loNibble;

        dqt_length--;

        if (dqt.precision == 0)
        {
            dqt.table = New!(ubyte[])(64);
            dqt_length -= 64;
        }
        else if (dqt.precision == 1)
        {
            dqt.table = New!(ubyte[])(128);
            dqt_length -= 128;
        }

        istrm.readBytes(dqt.table.ptr, dqt.table.length);

        version(JPEGDebug)
        {
            writefln("DQT precision: %s", dqt.precision);
            writefln("DQT table id: %s", dqt.tableId);
            writefln("DQT table: %s", dqt.table);
        }
    }

    return compound(true, "");
}

Compound!(bool, string) readSOF0(JPEGImage* jpg, InputStream istrm)
{
    ushort sof0_length = istrm.readNumeric!ushort(Endian.Big);
    jpg.sof0.precision = istrm.readNumeric!ubyte;
    jpg.sof0.height = istrm.readNumeric!ushort(Endian.Big);
    jpg.sof0.width = istrm.readNumeric!ushort(Endian.Big);
    jpg.sof0.componentsNum = istrm.readNumeric!ubyte;

    version(JPEGDebug)
    {
        writefln("SOF0 length: %s", sof0_length);
        writefln("SOF0 precision: %s", jpg.sof0.precision);
        writefln("SOF0 height: %s", jpg.sof0.height);
        writefln("SOF0 width: %s", jpg.sof0.width);
        writefln("SOF0 components: %s", jpg.sof0.componentsNum);
    }

    jpg.sof0.components = New!(JPEGImage.SOF0Component[])(jpg.sof0.componentsNum);

    foreach(ref c; jpg.sof0.components)
    {
        ubyte c_id = istrm.readNumeric!ubyte;
        ubyte bite = istrm.readNumeric!ubyte;
        c.hSubsampling = bite.hiNibble;
        c.vSubsampling = bite.loNibble;
        c.dqtTableId = istrm.readNumeric!ubyte;
        version(JPEGDebug)
        {
            writefln("SOF0 component id: %s", c_id);
            writefln("SOF0 component %s hsubsampling: %s", c_id, c.hSubsampling);
            writefln("SOF0 component %s vsubsampling: %s", c_id, c.vSubsampling);
            writefln("SOF0 component %s table id: %s", c_id, c.dqtTableId);
        }
    }

    return compound(true, "");
}

Compound!(bool, string) readDHT(JPEGImage* jpg, InputStream istrm)
{
    ushort dht_length = istrm.readNumeric!ushort(Endian.Big);
    version(JPEGDebug)
    {
        writefln("DHT length: %s", dht_length);
    }

    dht_length -= 2;

    while(dht_length > 0)
    {
        JPEGImage.DHT* dht = jpg.addDHT();

        ubyte bite = istrm.readNumeric!ubyte;
        dht_length--;
        dht.clas = bite.hiNibble;
        dht.tableId = bite.loNibble;

        ubyte[16] dht_code_lengths;
        istrm.readBytes(dht_code_lengths.ptr, 16);
        dht_length -= 16;

        version(JPEGDebug)
        {
            writefln("DHT class: %s (%s)",
                dht.clas,
                dht.clas? "AC":"DC");
            writefln("DHT tableId: %s", dht.tableId);
            writefln("DHT Huffman code lengths: %s", dht_code_lengths);
        }

        // Read Huffman table
        int totalCodes = reduce!("a + b")(0, dht_code_lengths);
        int storedCodes = 0;
        ubyte treeLevel = 0;
        ushort bits = 0;

        while (storedCodes != totalCodes)
        {
            while (treeLevel < 15 &&
                dht_code_lengths[treeLevel] == 0)
            {
                treeLevel++;
                bits *= 2;
            }

            if (treeLevel < 16)
            {
                uint bitsNum = treeLevel + 1;
                HuffmanCode code = HuffmanCode(bits, cast(ushort)bitsNum);

                auto entry = HuffmanTableEntry(code, istrm.readNumeric!ubyte);
                dht.huffmanTable.append(entry);

                dht_length--;

                storedCodes++;
                bits++;
                dht_code_lengths[treeLevel]--;
            }
        }

        dht.huffmanTree = treeFromTable(dht.huffmanTable);
    }

    return compound(true, "");
}

Compound!(bool, string) readSOS(JPEGImage* jpg, InputStream istrm)
{
    ushort sos_length = istrm.readNumeric!ushort(Endian.Big);
    jpg.sos.componentsNum = istrm.readNumeric!ubyte;

    version(JPEGDebug)
    {
        writefln("SOS length: %s", sos_length);
        writefln("SOS components: %s", jpg.sos.componentsNum);
    }

    jpg.sos.components = New!(JPEGImage.SOSComponent[])(jpg.sos.componentsNum);

    foreach(ref c; jpg.sos.components)
    {
        ubyte c_id = istrm.readNumeric!ubyte;
        ubyte bite = istrm.readNumeric!ubyte;
        c.tableIdDC = bite.hiNibble;
        c.tableIdAC = bite.loNibble;
        version(JPEGDebug)
        {
            writefln("SOS component id: %s", c_id);
            writefln("SOS component %s DC table id: %s", c_id, c.tableIdDC);
            writefln("SOS component %s AC table id: %s", c_id, c.tableIdAC);
        }
    }

    jpg.sos.spectralSelectionStart = istrm.readNumeric!ubyte;
    jpg.sos.spectralSelectionEnd = istrm.readNumeric!ubyte;
    ubyte bite = istrm.readNumeric!ubyte;
    jpg.sos.successiveApproximationBitHigh = bite.hiNibble;
    jpg.sos.successiveApproximationBitLow = bite.loNibble;

    version(JPEGDebug)
    {
        writefln("SOS spectral selection start: %s", jpg.sos.spectralSelectionStart);
        writefln("SOS spectral selection end: %s", jpg.sos.spectralSelectionEnd);
        writefln("SOS successive approximation bit: %s", jpg.sos.successiveApproximationBitHigh);
        writefln("SOS successive approximation bit low: %s", jpg.sos.successiveApproximationBitLow);
    }

    return compound(true, "");
}

struct ScanBitStream
{
    InputStream istrm;

    bool endMarkerFound = false;
    uint bytesRead = 0;
    ubyte prevByte = 0x00;
    ubyte curByte = 0x00;

    ubyte readNextByte()
    {
        ubyte b = istrm.readNumeric!ubyte;
        bytesRead++;
        endMarkerFound = (prevByte == 0xFF && b == 0xD9);
        assert(!endMarkerFound);
        if (!endMarkerFound)
        {
            prevByte = b;
            curByte = b;
            return b;
        }
        else
        {
            curByte = 0;
        }
        return curByte;
    }

    bool readable()
    {
        return !istrm.readable || endMarkerFound;
    }

    uint bitPos = 0;

    // Huffman decode a byte
    Compound!(bool, string) decodeByte(HuffmanTreeNode* node, ubyte* result)
    {
        while(!node.isLeaf)
        {
            ubyte b = curByte;

            bool bit = getBit(b, 7-bitPos);
            bitPos++;
            if (bitPos == 8)
            {
                bitPos = 0;
                readNextByte();

                if (b == 0xFF)
                {
                    b = curByte;
                    if (b == 0x00)
                    {
                        readNextByte();
                    }
                }
            }

            if (bit)
                node = node.right;
            else
                node = node.left;

            if (node is null)
                return compound(false, "loadJPEG error: no Huffman code found");
        }

        *result = node.ch;
        return compound(true, "");
    }

    // Read len bits from stream to buffer
    uint readBits(ubyte len)
    {
        uint buffer = 0;
        uint i = 0;
        uint by = 0;
        uint bi = 0;

        while (i < len)
        {
            ubyte b = curByte;

            bool bit = getBit(b, 7-bitPos);
            buffer = setBit(buffer, (by * 8 + bi), bit);

            bi++;
            if (bi == 8)
            {
                bi = 0;
                by++;
            }

            i++;

            bitPos++;
            if (bitPos == 8)
            {
                bitPos = 0;
                readNextByte();

                if (b == 0xFF)
                {
                    b = curByte;
                    if (b == 0x00)
                        readNextByte();
                }
            }
        }

        return buffer;
    }
}

/*
 *  Decodes compressed data and creates RGB image from it
 */
Compound!(SuperImage, string) decodeScanData(
    JPEGImage* jpg,
    InputStream istrm,
    SuperImageFactory imgFac)
{
    SuperImage img = imgFac.createImage(jpg.sof0.width, jpg.sof0.height, 3, 8);

    MCU mcu;
    foreach(ci, ref c; jpg.sof0.components)
    {
        if (ci == 0)
            mcu.createYBlocks(c.hSubsampling, c.vSubsampling);
        else if (ci == 1)
            mcu.createCbBlocks(c.hSubsampling, c.vSubsampling);
        else if (ci == 2)
            mcu.createCrBlocks(c.hSubsampling, c.vSubsampling);
    }

    Compound!(SuperImage, string) error(string errorMsg)
    {
        mcu.free();
        if (img)
        {
            img.free();
            img = null;
        }
        return compound(img, errorMsg);
    }

    // Decode DCT coefficient from bit buffer
    int decodeCoef(uint buffer, ubyte numBits)
    {
        bool positive = getBit(buffer, 0);

        int value = 0;
        foreach(j; 0..numBits)
        {
            bool bit = getBit(buffer, numBits-1-j);
            value = setBit(value, j, bit);
        }

        if (positive)
            return value;
        else
            return value - 2^^numBits + 1;
    }

    static const ubyte[64] dezigzag =
    [
         0,  1,  8, 16,  9,  2,  3, 10,
        17, 24, 32, 25, 18, 11,  4,  5,
        12, 19, 26, 33, 40, 48, 41, 34,
        27, 20, 13,  6,  7, 14, 21, 28,
        35, 42, 49, 56, 57, 50, 43, 36,
        29, 22, 15, 23, 30, 37, 44, 51,
        58, 59, 52, 45, 38, 31, 39, 46,
        53, 60, 61, 54, 47, 55, 62, 63
    ];

    if (jpg.sos.componentsNum != 3)
    {
        return error(format(
                "loadJPEG error: unsupported number of components: %s",
                jpg.sos.componentsNum));
    }

    // Store previous DC coefficients
    int[3] dcCoefPrev;

    if (jpg.dqt.length == 0)
        return error("loadJPEG error: no DQTs found");

    ScanBitStream sbs;
    sbs.endMarkerFound = false;
    sbs.bytesRead = 0;
    sbs.prevByte = 0x00;
    sbs.curByte = 0x00;
    sbs.istrm = istrm;
    sbs.readNextByte();

    uint numMCUsH = jpg.sof0.width / mcu.width + ((jpg.sof0.width % mcu.width) > 0);
    uint numMCUsV = jpg.sof0.height / mcu.height + ((jpg.sof0.height % mcu.height) > 0);

    // Read MCUs
    foreach(mcuY; 0..numMCUsV)
    foreach(mcuX; 0..numMCUsH)
    {
        // Read MCU for each channel
        foreach(ci, ref c; jpg.sos.components)
        {
            auto tableDC = jpg.getHuffmanTable(0, c.tableIdDC);
            auto tableAC = jpg.getHuffmanTable(1, c.tableIdAC);

            if (tableDC is null)
                return error("loadJPEG error: illegal DC table index in MCU component");
            if (tableAC is null)
                return error("loadJPEG error: illegal AC table index in MCU component");

            auto component = jpg.sof0.components[ci];
            auto hblocks = component.hSubsampling;
            auto vblocks = component.vSubsampling;
            auto dqtTableId = component.dqtTableId;
            if (dqtTableId >= jpg.dqt.length)
                return error("loadJPEG error: illegal DQT table index in MCU component");

            // Read 8x8 blocks
            foreach(by; 0..vblocks)
            foreach(bx; 0..hblocks)
            {
                int[8*8] block;

                // Read DC coefficient
                ubyte dcDiffLen;
                auto res = sbs.decodeByte(tableDC.huffmanTree, &dcDiffLen);
                if (!res[0]) return error(res[1]);

                if (dcDiffLen > 0)
                {
                    uint dcBuffer = sbs.readBits(dcDiffLen);
                    dcCoefPrev[ci] += decodeCoef(dcBuffer, dcDiffLen);
                }

                block[0] = dcCoefPrev[ci];

                // Read AC coefficients
                {
                    uint i = 1;
                    bool eob = false;
                    while (!eob && i < 64)
                    {
                        ubyte code;
                        res = sbs.decodeByte(tableAC.huffmanTree, &code);
                        if (!res[0]) return error(res[1]);

                        if (code == 0x00) // EOB, all next values are zero
                            eob = true;
                        else if (code == 0xF0) // ZRL, next 16 values are zero
                        {
                            foreach(j; 0..16)
                            if (i < 64)
                            {
                                block[i] = 0;
                                i++;
                            }
                        }
                        else
                        {
                            ubyte hi = hiNibble(code);
                            ubyte lo = loNibble(code);

                            uint zeroes = hi;
                            foreach(j; 0..zeroes)
                            if (i < 64)
                            {
                                block[i] = 0;
                                i++;
                            }

                            int acCoef = 0;
                            if (lo > 0)
                            {
                                uint acBuffer = sbs.readBits(lo);
                                acCoef = decodeCoef(acBuffer, lo);
                            }

                            if (i < 64)
                                block[i] = acCoef;

                            i++;
                        }
                    }
                }

                // Multiply block by quantization matrix
                foreach(i, ref v; block)
                    v *= jpg.dqt[dqtTableId].table[i];

                // Convert matrix from zig-zag order to normal order
                int[8*8] dctMatrix;

                foreach(i, v; block)
                    dctMatrix[dezigzag[i]] = v;

                idct64(dctMatrix.ptr);

                // Copy the matrix into corresponding channel
                int* outMatrixPtr;
                if (ci == 0)
                    outMatrixPtr = mcu.yBlocks[by * hblocks + bx].ptr;
                else if (ci == 1)
                    outMatrixPtr = mcu.cbBlocks[by * hblocks + bx].ptr;
                else if (ci == 2)
                    outMatrixPtr = mcu.crBlocks[by * hblocks + bx].ptr;
                else
                    return error("loadJPEG error: illegal component index");

                for(uint i = 0; i < 64; i++)
                    outMatrixPtr[i] = dctMatrix[i];
            }
        }

        // Convert MCU from YCbCr to RGB
        foreach(y; 0..mcu.height) // Pixel coordinates in MCU
        foreach(x; 0..mcu.width)
        {
            //Color4f col = mcu.getPixel(x, y);
            uint col = mcu.getPixel(x, y);

            // Pixel coordinates in image
            uint ix = mcuX * mcu.width + x;
            uint iy = mcuY * mcu.height + y;

            if (ix < img.width && iy < img.height)
                img[ix, iy] = col;
        }
    }

    version(JPEGDebug)
    {
        writefln("Bytes read: %s", sbs.bytesRead);
    }

    mcu.free();

    return compound(img, "");
}

/*
 * MCU struct keeps a storage for one Minimal Code Unit
 * and provides a generalized interface for decoding
 * images with different subsampling modes.
 * Decoder should read 8x8 blocks one by one for each channel
 * and fill corresponding arrays in MCU.
 */
struct MCU
{
    uint width;
    uint height;

    alias int[8*8] Block;
    Block[] yBlocks;
    Block[] cbBlocks;
    Block[] crBlocks;

    uint ySamplesH, ySamplesV;
    uint cbSamplesH, cbSamplesV;
    uint crSamplesH, crSamplesV;

    uint yWidth, yHeight;
    uint cbWidth, cbHeight;
    uint crWidth, crHeight;

    void createYBlocks(uint hsubsampling, uint vsubsampling)
    {
        yBlocks = New!(Block[])(hsubsampling * vsubsampling);

        width = hsubsampling * 8;
        height = vsubsampling * 8;

        ySamplesH = hsubsampling;
        ySamplesV = vsubsampling;

        yWidth = width / ySamplesH;
        yHeight = height / ySamplesV;
    }

    void createCbBlocks(uint hsubsampling, uint vsubsampling)
    {
        cbBlocks = New!(Block[])(hsubsampling * vsubsampling);

        cbSamplesH = hsubsampling;
        cbSamplesV = vsubsampling;

        cbWidth = width / cbSamplesH;
        cbHeight = height / cbSamplesV;
    }

    void createCrBlocks(uint hsubsampling, uint vsubsampling)
    {
        crBlocks = New!(Block[])(hsubsampling * vsubsampling);

        crSamplesH = hsubsampling;
        crSamplesV = vsubsampling;

        crWidth = width / crSamplesH;
        crHeight = height / crSamplesV;
    }

    void free()
    {
        if (yBlocks.length) Delete(yBlocks);
        if (cbBlocks.length) Delete(cbBlocks);
        if (crBlocks.length) Delete(crBlocks);
    }

    uint getPixel(uint x, uint y) // coordinates relative to upper-left MCU corner
    {
        // Y block coordinates
        uint ybx = x / yWidth;
        uint yby = y / yHeight;
        uint ybi = yby * ySamplesH + ybx;

        // Pixel coordinates in Y block
        uint ybpx = x - ybx * yWidth;
        uint ybpy = y - yby * yHeight;

        // Cb block coordinates
        uint cbx = x / cbWidth;
        uint cby = y / cbHeight;
        uint cbi = cby * cbSamplesH + cbx;

        // Pixel coordinates in Cb block
        uint cbpx = (x - cbx * cbWidth)  / ySamplesH;
        uint cbpy = (y - cby * cbHeight) / ySamplesV;

        // Cr block coordinates
        uint crx = x / crWidth;
        uint cry = y / crHeight;
        uint cri = cry * crSamplesH + crx;

        // Pixel coordinates in Cr block
        uint crpx = (x - crx * crWidth)  / ySamplesH;
        uint crpy = (y - cry * crHeight) / ySamplesV;

        // Get color components
        float Y  = cast(float)yBlocks [ybi][ybpy * 8 + ybpx] + 128.0f;
        float Cb = cast(float)cbBlocks[cbi][cbpy * 8 + cbpx];
        float Cr = cast(float)crBlocks[cri][crpy * 8 + crpx];

        // Convert from YCbCr to RGB
        //Color4f col;
        uint col_r = clamp(Y + 1.402f * Cr);
        uint col_g = clamp(Y - 0.34414f * Cb - 0.71414f * Cr);
        uint col_b = clamp(Y + 1.772f * Cb);
        //col = col / 255.0f;
        //col.a = 1.0f;

        return 0xFF000000 | (col_r << 16) | (col_g << 8) | (col_b) ;
    }
}

uint clamp(float v) {
    import std.conv;
    if (v < 0)
        return 0;
    uint res = to!uint(v);
    if (v > 255)
        return 255;
    return res;
}
