/*
    Реализация спецификаций CLDC версии 1.1 (JSR-139), MIDP версии 2.1 (JSR-118)
    и других спецификаций для функционирования компактных приложений на языке
    Java (мидлетов) в среде программного обеспечения Малик Эмулятор.

    Copyright © 2016–2017, 2019–2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять ее и/или изменять
    ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она будет полезной,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <https://www.gnu.org/licenses/>.
*/

package malik.emulator.fileformats.graphics.jpeg;

import java.io.*;
import malik.emulator.fileformats.*;
import malik.emulator.fileformats.graphics.*;

public class JPEGDecoder extends Object implements ImageDecoder
{
    public static final long SIGNATURE = 0xffd8L;

    static final int DC = 0;
    static final int AC = 1;
    static final int MIN   = 0xff00;
    static final int SOF0  = 0xffc0;
    static final int SOF1  = 0xffc1;
    static final int SOF2  = 0xffc2;
    static final int SOF3  = 0xffc3;
    static final int DHT   = 0xffc4;
    static final int SOF5  = 0xffc5;
    static final int SOF6  = 0xffc6;
    static final int SOF7  = 0xffc7;
    static final int JPG   = 0xffc8;
    static final int SOF9  = 0xffc9;
    static final int SOF10 = 0xffca;
    static final int SOF11 = 0xffcb;
    static final int DAC   = 0xffcc;
    static final int SOF13 = 0xffcd;
    static final int SOF14 = 0xffce;
    static final int SOF15 = 0xffcf;
    static final int RST0  = 0xffd0;
    static final int RST1  = 0xffd1;
    static final int RST2  = 0xffd2;
    static final int RST3  = 0xffd3;
    static final int RST4  = 0xffd4;
    static final int RST5  = 0xffd5;
    static final int RST6  = 0xffd6;
    static final int RST7  = 0xffd7;
    static final int EOI   = 0xffd9;
    static final int SOS   = 0xffda;
    static final int DQT   = 0xffdb;
    static final int DNL   = 0xffdc;
    static final int DRI   = 0xffdd;
    static final int DHP   = 0xffde;
    static final int EXP   = 0xffdf;
    static final int APP0  = 0xffe0;
    static final int APP1  = 0xffe1;
    static final int APP2  = 0xffe2;
    static final int APP3  = 0xffe3;
    static final int APP4  = 0xffe4;
    static final int APP5  = 0xffe5;
    static final int APP6  = 0xffe6;
    static final int APP7  = 0xffe7;
    static final int APP8  = 0xffe8;
    static final int APP9  = 0xffe9;
    static final int APP10 = 0xffea;
    static final int APP11 = 0xffeb;
    static final int APP12 = 0xffec;
    static final int APP13 = 0xffed;
    static final int APP14 = 0xffee;
    static final int APP15 = 0xffef;
    static final int COM   = 0xfffe;
    static final int MAX   = 0xffff;

    static final int MAX_COMPONENTS = 3;

    static final byte[] ORDER_ZIGZAG_INVERTED;

    private static final int ONE_SQRT_2  = 5793;
    private static final int COS_1_16_PI = 4017; /* cos(1/16π) */
    private static final int SIN_6_16_PI = 3784; /* cos(2/16π) */
    private static final int COS_3_16_PI = 3406; /* cos(3/16π) */
    private static final int HALF_SQRT_2 = 2896; /* cos(4/16π) */
    private static final int SIN_3_16_PI = 2276; /* cos(5/16π) */
    private static final int COS_6_16_PI = 1567; /* cos(6/16π) */
    private static final int SIN_1_16_PI =  799; /* cos(7/16π) */

    static {
        ORDER_ZIGZAG_INVERTED = new byte[] {
                 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
        };
    }

    private static void translateY(ImageHeaderChunk header, Component[] components, int width, int height, int[] pixels) {
        int horzSizeY;
        int[] blocksY;
        Component componentY = components[0];
        horzSizeY = componentY.horzSize;
        blocksY = componentY.blocks;
        for(int index = 0, iy = 0; iy < height; iy++) for(int ix = 0; ix < width; ix++)
        {
            int col = ix >> 3;
            int row = iy >> 3;
            int hrz = ix & 0x07;
            int vrt = iy & 0x07;
            int y;
            if((y = blocksY[col + row * horzSizeY << 6 | vrt << 3 | hrz]) < 0x00)
            {
                y = 0x00;
            }
            else if(y > 0xff)
            {
                y = 0xff;
            }
            pixels[index++] = y << 16 | y << 8 | y;
        }
    }

    private static void translateYCbCr(ImageHeaderChunk header, Component[] components, int width, int height, int[] pixels) {
        int horzScaleY;
        int horzScaleCb;
        int horzScaleCr;
        int horzScaleMax;
        int vertScaleY;
        int vertScaleCb;
        int vertScaleCr;
        int vertScaleMax;
        int horzSizeY;
        int horzSizeCb;
        int horzSizeCr;
        int[] blocksY;
        int[] blocksCb;
        int[] blocksCr;
        Component componentY = components[0];
        Component componentCb = components[1];
        Component componentCr = components[2];
        horzScaleY = componentY.horzFactor;
        horzScaleCb = componentCb.horzFactor;
        horzScaleCr = componentCr.horzFactor;
        horzScaleMax = header.maxHorzFactor;
        vertScaleY = componentY.vertFactor;
        vertScaleCb = componentCb.vertFactor;
        vertScaleCr = componentCr.vertFactor;
        vertScaleMax = header.maxVertFactor;
        horzSizeY = componentY.horzSize;
        horzSizeCb = componentCb.horzSize;
        horzSizeCr = componentCr.horzSize;
        blocksY = componentY.blocks;
        blocksCb = componentCb.blocks;
        blocksCr = componentCr.blocks;
        for(int index = 0, iy = 0; iy < height; iy++) for(int ix = 0; ix < width; ix++)
        {
            int sx;
            int sy;
            int col;
            int row;
            int hrz;
            int vrt;
            int y;
            int cb;
            int cr;
            int r;
            int g;
            int b;
            col = (sx = ix * horzScaleY / horzScaleMax) >> 3;
            row = (sy = iy * vertScaleY / vertScaleMax) >> 3;
            hrz = sx & 0x07;
            vrt = sy & 0x07;
            y = blocksY[col + row * horzSizeY << 6 | vrt << 3 | hrz] << 12;
            col = (sx = ix * horzScaleCb / horzScaleMax) >> 3;
            row = (sy = iy * vertScaleCb / vertScaleMax) >> 3;
            hrz = sx & 0x07;
            vrt = sy & 0x07;
            cb = blocksCb[col + row * horzSizeCb << 6 | vrt << 3 | hrz] - 0x80;
            col = (sx = ix * horzScaleCr / horzScaleMax) >> 3;
            row = (sy = iy * vertScaleCr / vertScaleMax) >> 3;
            hrz = sx & 0x07;
            vrt = sy & 0x07;
            cr = blocksCr[col + row * horzSizeCr << 6 | vrt << 3 | hrz] - 0x80;
            r = y + cr * 0x166f >> 12;
            g = y - cb * 0x0582 - cr * 0x0b6d >> 12;
            b = y + cb * 0x1c5a >> 12;
            pixels[index++] = (r < 0 ? 0 : r > 0xff ? 0xff : r) << 16 | (g < 0 ? 0 : g > 0xff ? 0xff : g) << 8 | (b < 0 ? 0 : b > 0xff ? 0xff : b);
        }
    }

    private static int[] buildImage(ImageHeaderChunk header, int[][] quantizationTables, int width, int height) throws InvalidDataFormatException {
        int componentsCount;
        int[] p = new int[0x40];
        int[] result = new int[width * height];
        Component[] components = new Component[componentsCount = header.getComponentsCount()];
        /* деквантование, обратное дискретное косинусное преобразование */
        for(int componentIndex = componentsCount; componentIndex-- > 0; )
        {
            int[] q;
            int[] b;
            Component component = components[componentIndex] = header.getComponent(header.getComponentId(componentIndex));
            if((q = quantizationTables[component.quantizationTableID]) == null)
            {
                throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
            }
            for(int blen = (b = component.blocks).length, offset = 0; offset < blen; offset += 0x40)
            {
                int t;
                int v0;
                int v1;
                int v2;
                int v3;
                int v4;
                int v5;
                int v6;
                int v7;
                /* деквантование */
                for(int index = 0; index < 0x40; index++) p[index] = b[offset + index] * q[index];
                /* обратное дискретное косинусное преобразование */
                for(int r = 0; r < 8 * 8; r += 8)
                {
                    if((p[r + 1] | p[r + 2] | p[r + 3] | p[r + 4] | p[r + 5] | p[r + 6] | p[r + 7]) == 0)
                    {
                        Array.fill(p, r, 8, ONE_SQRT_2 * p[r] + 0x0200 >> 10);
                        continue;
                    }
                    /* стадия 4 */
                    v0 = ONE_SQRT_2 * p[r] + 0x80 >> 8;
                    v1 = ONE_SQRT_2 * p[r + 4] + 0x80 >> 8;
                    v2 = p[r + 2];
                    v3 = p[r + 6];
                    v4 = HALF_SQRT_2 * ((v5 = p[r + 1]) - (v6 = p[r + 7])) + 0x80 >> 8;
                    v7 = HALF_SQRT_2 * (v5 + v6) + 0x80 >> 8;
                    v5 = p[r + 3] << 4;
                    v6 = p[r + 5] << 4;
                    /* стадия 3 */
                    t = v0 - v1 + 1 >> 1;
                    v0 = v0 + v1 + 1 >> 1;
                    v1 = t;
                    t = v2 * SIN_6_16_PI + v3 * COS_6_16_PI + 0x80 >> 8;
                    v2 = v2 * COS_6_16_PI - v3 * SIN_6_16_PI + 0x80 >> 8;
                    v3 = t;
                    t = v4 - v6 + 1 >> 1;
                    v4 = v4 + v6 + 1 >> 1;
                    v6 = t;
                    t = v7 + v5 + 1 >> 1;
                    v5 = v7 - v5 + 1 >> 1;
                    v7 = t;
                    /* стадия 2 */
                    t = v0 - v3 + 1 >> 1;
                    v0 = v0 + v3 + 1 >> 1;
                    v3 = t;
                    t = v1 - v2 + 1 >> 1;
                    v1 = v1 + v2 + 1 >> 1;
                    v2 = t;
                    t = v4 * SIN_3_16_PI + v7 * COS_3_16_PI + 0x0800 >> 12;
                    v4 = v4 * COS_3_16_PI - v7 * SIN_3_16_PI + 0x0800 >> 12;
                    v7 = t;
                    t = v5 * SIN_1_16_PI + v6 * COS_1_16_PI + 0x0800 >> 12;
                    v5 = v5 * COS_1_16_PI - v6 * SIN_1_16_PI + 0x0800 >> 12;
                    v6 = t;
                    /* стадия 1 */
                    p[r] = v0 + v7;
                    p[r + 7] = v0 - v7;
                    p[r + 1] = v1 + v6;
                    p[r + 6] = v1 - v6;
                    p[r + 2] = v2 + v5;
                    p[r + 5] = v2 - v5;
                    p[r + 3] = v3 + v4;
                    p[r + 4] = v3 - v4;
                }
                for(int c = 0; c < 8; c++)
                {
                    if((p[c + 1 * 8] | p[c + 2 * 8] | p[c + 3 * 8] | p[c + 4 * 8] | p[c + 5 * 8] | p[c + 6 * 8] | p[c + 7 * 8]) == 0)
                    {
                        t = ONE_SQRT_2 * p[c] + 0x2000 >> 14;
                        p[c] = t;
                        p[c + 1 * 8] = t;
                        p[c + 2 * 8] = t;
                        p[c + 3 * 8] = t;
                        p[c + 4 * 8] = t;
                        p[c + 5 * 8] = t;
                        p[c + 6 * 8] = t;
                        p[c + 7 * 8] = t;
                        continue;
                    }
                    /* стадия 4 */
                    v0 = ONE_SQRT_2 * p[c] + 0x0800 >> 12;
                    v1 = ONE_SQRT_2 * p[c + 4 * 8] + 0x0800 >> 12;
                    v2 = p[c + 2 * 8];
                    v3 = p[c + 6 * 8];
                    v4 = HALF_SQRT_2 * ((v5 = p[c + 1 * 8]) - (v6 = p[c + 7 * 8])) + 0x0800 >> 12;
                    v7 = HALF_SQRT_2 * (v5 + v6) + 0x0800 >> 12;
                    v5 = p[c + 3 * 8];
                    v6 = p[c + 5 * 8];
                    /* стадия 3 */
                    t = v0 - v1 + 1 >> 1;
                    v0 = v0 + v1 + 1 >> 1;
                    v1 = t;
                    t = v2 * SIN_6_16_PI + v3 * COS_6_16_PI + 0x0800 >> 12;
                    v2 = v2 * COS_6_16_PI - v3 * SIN_6_16_PI + 0x0800 >> 12;
                    v3 = t;
                    t = v4 - v6 + 1 >> 1;
                    v4 = v4 + v6 + 1 >> 1;
                    v6 = t;
                    t = v7 + v5 + 1 >> 1;
                    v5 = v7 - v5 + 1 >> 1;
                    v7 = t;
                    /* стадия 2 */
                    t = v0 - v3 + 1 >> 1;
                    v0 = v0 + v3 + 1 >> 1;
                    v3 = t;
                    t = v1 - v2 + 1 >> 1;
                    v1 = v1 + v2 + 1 >> 1;
                    v2 = t;
                    t = v4 * SIN_3_16_PI + v7 * COS_3_16_PI + 0x0800 >> 12;
                    v4 = v4 * COS_3_16_PI - v7 * SIN_3_16_PI + 0x0800 >> 12;
                    v7 = t;
                    t = v5 * SIN_1_16_PI + v6 * COS_1_16_PI + 0x0800 >> 12;
                    v5 = v5 * COS_1_16_PI - v6 * SIN_1_16_PI + 0x0800 >> 12;
                    v6 = t;
                    /* стадия 1 */
                    p[c] = v0 + v7;
                    p[c + 7 * 8] = v0 - v7;
                    p[c + 1 * 8] = v1 + v6;
                    p[c + 6 * 8] = v1 - v6;
                    p[c + 2 * 8] = v2 + v5;
                    p[c + 5 * 8] = v2 - v5;
                    p[c + 3 * 8] = v3 + v4;
                    p[c + 4 * 8] = v3 - v4;
                }
                for(int index = 0; index < 0x40; index++) b[offset + index] = (p[index] + 8 >> 4) + 0x80;
            }
        }
        /* преобразование в формат пиксела 0x00rrggbb */
        switch(componentsCount)
        {
        case 1:
        case 2:
            translateY(header, components, width, height, result);
            break;
        case 3:
            translateYCbCr(header, components, width, height, result);
            break;
        }
        return result;
    }

    private int width;
    private int height;
    private int[] pixels;

    public JPEGDecoder() {
    }

    public void loadFromInputStream(InputStream stream) throws IOException {
        loadFromDataStream(new ExtendedDataInputStream(stream));
    }

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        int chunkName;
        int restartInterval = 0;
        int imageWidth = 0;
        int imageHeight = 0;
        int[] imagePixels = null;
        int[][] quantizationTables = new int[0x10][];
        ChaffmanTable[] chaffmanTables = new ChaffmanTable[0x20];
        StartOfScanChunk scan = new StartOfScanChunk();
        ImageHeaderChunk header = null;
        ScanDecoder decoder = null;
        /* чтение входного потока данных */
        do
        {
            switch(chunkName = stream.readUnsignedShort())
            {
            default:
                if(chunkName < MIN || chunkName > MAX)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                throw new UnsupportedDataException("JPEGDecoder.loadFromInputStream: неподдерживаемые данные.");
            case SOF0:
            case SOF1:
            case SOF2:
                if(header != null)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                ((Chunk) (header = new ImageHeaderChunk(chunkName))).loadFromDataStream(stream);
                imageWidth = header.width;
                imageHeight = header.height;
                break;
            case DHT:
            case DQT:
            {
                Object[] tables;
                TableSetChunk chunk;
                if(chunkName == DQT)
                {
                    tables = quantizationTables;
                    chunk = new QuantizationTableSetChunk();
                } else
                {
                    tables = chaffmanTables;
                    chunk = new ChaffmanTableSetChunk();
                }
                ((Chunk) chunk).loadFromDataStream(stream);
                for(int i = chunk.getTablesCount(); i-- > 0; )
                {
                    int id = chunk.getTableId(i);
                    tables[id] = chunk.getTable(i);
                }
                break;
            }
            case EOI:
                if(header == null)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                break;
            case SOS:
                if(header == null)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                if(decoder == null) decoder = new ScanDecoder(stream.getSourceStream(), header);
                ((Chunk) scan).loadFromDataStream(stream);
                for(int i = scan.getComponentsCount(); i-- > 0; )
                {
                    ChaffmanTable ac;
                    ChaffmanTable dc;
                    Component component = header.getComponent(scan.getComponentId(i));
                    component.chaffmanTableAC = ac = chaffmanTables[scan.getComponentChaffmanTableId(i, AC)];
                    component.chaffmanTableDC = dc = chaffmanTables[scan.getComponentChaffmanTableId(i, DC)];
                    if(ac == null || dc == null)
                    {
                        throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                    }
                }
                decoder.decodeScan(restartInterval, scan);
                break;
            case DRI:
                restartInterval = stream.readInt() & 0xffff;
                break;
            case APP0:
            case APP1:
            case APP2:
            case APP3:
            case APP4:
            case APP5:
            case APP6:
            case APP7:
            case APP8:
            case APP9:
            case APP10:
            case APP11:
            case APP12:
            case APP13:
            case APP14:
            case APP15:
            case COM:
                int len;
                if((len = stream.readUnsignedShort() - 2) < 0 || stream.skipBytes(len) != len)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                break;
            }
        } while(chunkName != EOI);
        /* построение изображения */
        imagePixels = buildImage(header, quantizationTables, imageWidth, imageHeight);
        /* вывод результата */
        width = imageWidth;
        height = imageHeight;
        pixels = imagePixels;
    }

    public void clear() {
        width = 0;
        height = 0;
        pixels = null;
    }

    public boolean isEmpty() {
        return (width | height) == 0 && pixels == null;
    }

    public boolean alphaSupported() {
        return false;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public int[] getPixels() {
        int len;
        int[] result;
        if((result = pixels) == null) return null;
        Array.copy(result, 0, result = new int[len = result.length], 0, len);
        return result;
    }
}

final class JPEGBitInputStream extends Object
{
    private int readedByte;
    private int lastBitIndex;
    private final InputStream stream;

    public JPEGBitInputStream(InputStream stream) {
        this.stream = stream;
    }

    public void reset() {
        lastBitIndex = 0;
    }

    public int readBit() throws IOException {
        int i = lastBitIndex;
        int r = readedByte;
        if(i > 0)
        {
            lastBitIndex = --i;
        } else
        {
            InputStream source = stream;
            lastBitIndex = i = 7;
            if((readedByte = r = source.read()) < 0)
            {
                throw new EOFException("DataInputStream.readUnsignedByte: достигнут конец потока данных.");
            }
            if(r == 0xff)
            {
                int byte0;
                if((byte0 = source.read()) < 0 || byte0 > 0xff)
                {
                    throw new EOFException("DataInputStream.readUnsignedByte: достигнут конец потока данных.");
                }
                switch(JPEGDecoder.MIN | byte0)
                {
                case JPEGDecoder.MIN:
                    break;
                case JPEGDecoder.DNL:
                    if(source.skip(4L) != 4L)
                    {
                        throw new EOFException("DataInputStream.readUnsignedByte: достигнут конец потока данных.");
                    }
                    return lastBitIndex = 0;
                default:
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
            }
        }
        return r >> i & 0x01;
    }

    public int readBits(int quantity) throws IOException {
        int result;
        for(result = 0; quantity-- > 0; result = result << 1 | readBit());
        return result;
    }

    public int readAndExtend(int quantity) throws IOException {
        int bits;
        return quantity <= 0 ? 0 : (bits = readBits(quantity)) < (1 << (quantity - 1)) ? bits + (-1 << quantity) + 1 : bits;
    }

    public int refineAC(int ac, int shift) throws IOException {
        if(ac > 0)
        {
            if(readBit() != 0) ac += 1 << shift;
        }
        else if(ac < 0)
        {
            if(readBit() != 0) ac += -1 << shift;
        }
        return ac;
    }
}

final class Component extends Object
{
    public final int quantizationTableID;
    public final int horzFactor;
    public final int vertFactor;
    public final int horzSize;
    public final int[] blocks;
    public ChaffmanTable chaffmanTableDC;
    public ChaffmanTable chaffmanTableAC;

    public Component(int horzFactor, int vertFactor, int quantizationTableID, int horzSize, int vertSize) {
        this.quantizationTableID = quantizationTableID;
        this.horzFactor = horzFactor;
        this.vertFactor = vertFactor;
        this.horzSize = horzSize;
        this.blocks = new int[horzSize * vertSize << 6];
    }

    public int getBlocksOffset(int col, int row) {
        return col + row * horzSize << 6;
    }
}

final class ChaffmanTable extends Object
{
    private final byte[] valueSet;
    private final short[] valueIndices;
    private final int[] minCodes;
    private final int[] maxCodes;

    public ChaffmanTable(byte[] valueSet, short[] valueIndices, int[] minCodes, int[] maxCodes) {
        this.valueSet = valueSet;
        this.valueIndices = valueIndices;
        this.minCodes = minCodes;
        this.maxCodes = maxCodes;
    }

    public int readValue(JPEGBitInputStream stream) throws IOException {
        int i;
        int code = stream.readBit();
        int[] maxCodes = this.maxCodes;
        for(i = 0; code > maxCodes[i]; i++) code = code << 1 | stream.readBit();
        return valueSet[valueIndices[i] + code - minCodes[i]] & 0xff;
    }
}

abstract class Chunk extends Object
{
    public static int readSize(DataInputStream stream) throws IOException {
        int result;
        if((result = stream.readUnsignedShort() - 2) < 0)
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        return result;
    }

    public final int chunkName;

    public Chunk(int chunkName) {
        this.chunkName = chunkName;
    }

    public abstract void loadFromDataStream(ExtendedDataInputStream stream) throws IOException;
}

final class ImageHeaderChunk extends Chunk
{
    public static final int BASELINE = 0;
    public static final int LOSSLESS = 3;
    public static final int SEQUENTIAL = 1;
    public static final int PROGRESSIVE = 2;

    public int width;
    public int height;
    public int horzBlocks;
    public int vertBlocks;
    public int maxHorzFactor;
    public int maxVertFactor;
    private byte[] componentsIds;
    private final Component[] components;

    public ImageHeaderChunk(int chunkName) {
        super(chunkName);
        this.components = new Component[0x0100];
    }

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        int iw;
        int ih;
        int bw;
        int bh;
        int hb;
        int vb;
        int mhf;
        int mvf;
        int chunkLen;
        byte[] componentData;
        Component[] componentProps;
        if((chunkLen = readSize(stream)) < 9)
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        stream.readByte(); /* пропускаем точность */
        if((ih = stream.readUnsignedShort()) < 1 || (iw = stream.readUnsignedShort()) < 1)
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        if((long) iw * (long) ih > 0x00100000L)
        {
            throw new UnsupportedDataException("JPEGDecoder.loadFromInputStream: неподдерживаемые данные.");
        }
        if(stream.readUnsignedByte() * 3 != (chunkLen -= 6))
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        if(chunkLen > JPEGDecoder.MAX_COMPONENTS * 3)
        {
            throw new UnsupportedDataException("JPEGDecoder.loadFromInputStream: неподдерживаемые данные.");
        }
        stream.readFully(componentData = new byte[chunkLen]);
        componentProps = components;
        mhf = mvf = 0;
        for(int i = 1; i < chunkLen; i += 3)
        {
            int byte0;
            int ihf = (byte0 = componentData[i]) >> 4 & 0x0f;
            int ivf = byte0 & 0x0f;
            if(ihf * ivf == 0)
            {
                throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
            }
            if(mhf < ihf) mhf = ihf;
            if(mvf < ivf) mvf = ivf;
        }
        bw = mhf << 3;
        bh = mvf << 3;
        hb = (iw + bw - 1) / bw;
        vb = (ih + bh - 1) / bh;
        for(int i = 0; i < chunkLen; )
        {
            int byte0;
            int index = componentData[i++] & 0xff;
            int ihf = (byte0 = componentData[i++]) >> 4 & 0x0f;
            int ivf = byte0 & 0x0f;
            int iqt = componentData[i++] & 0x0f;
            componentProps[index] = new Component(ihf, ivf, iqt, hb * ihf, vb * ivf);
        }
        width = iw;
        height = ih;
        horzBlocks = hb;
        vertBlocks = vb;
        maxHorzFactor = mhf;
        maxVertFactor = mvf;
        componentsIds = componentData;
    }

    public boolean isDifferentialCoding() {
        return (chunkName & 0x04) != 0;
    }

    public boolean isArithmeticCoding() {
        return chunkName >= JPEGDecoder.SOF9;
    }

    public int getCodingType() {
        return chunkName & 0x03;
    }

    public int getComponentsCount() {
        return componentsIds.length / 3;
    }

    public int getComponentId(int index) {
        return componentsIds[index * 3] & 0xff;
    }

    public Component getComponent(int componentId) {
        return components[componentId];
    }
}

abstract class TableSetChunk extends Chunk
{
    public TableSetChunk(int chunkName) {
        super(chunkName);
    }

    public abstract void loadFromDataStream(ExtendedDataInputStream stream) throws IOException;

    public abstract int getTablesCount();

    public abstract int getTableId(int index);

    public abstract Object getTable(int index);
}

final class QuantizationTableSetChunk extends TableSetChunk
{
    private int count;
    private byte[] ids;
    private Object[] tables;

    public QuantizationTableSetChunk() {
        super(JPEGDecoder.DQT);
    }

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        int chunkLen;
        int dataCount;
        byte[] order;
        byte[] dataIds;
        int[][] dataTables;
        if((chunkLen = readSize(stream)) < 0x41)
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        dataCount = 0;
        dataIds = new byte[1];
        dataTables = new int[1][];
        order = JPEGDecoder.ORDER_ZIGZAG_INVERTED;
        do
        {
            int byte0;
            int valueLen;
            int[] table;
            switch(valueLen = (byte0 = stream.readUnsignedByte()) >> 4)
            {
            case 0:
                if(chunkLen < 0x41)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                chunkLen -= 0x41;
                break;
            case 1:
                if(chunkLen < 0x81)
                {
                    throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                chunkLen -= 0x81;
                break;
            default:
                throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
            }
            table = new int[0x40];
            for(int i = 0; i < 0x40; i++) table[order[i]] = valueLen == 1 ? stream.readUnsignedShort() : stream.readUnsignedByte();
            if(dataIds.length == dataCount)
            {
                int newLen = (dataCount << 1) + 1;
                Array.copy(dataIds, 0, dataIds = new byte[newLen], 0, dataCount);
                Array.copy(dataTables, 0, dataTables = new int[newLen][], 0, dataCount);
            }
            dataTables[dataCount] = table;
            dataIds[dataCount] = (byte) (byte0 & 0x0f);
            dataCount++;
        } while(chunkLen > 0);
        count = dataCount;
        ids = dataIds;
        tables = dataTables;
    }

    public int getTablesCount() {
        return count;
    }

    public int getTableId(int index) {
        return ids[index];
    }

    public Object getTable(int index) {
        return tables[index];
    }
}

final class ChaffmanTableSetChunk extends TableSetChunk
{
    private int count;
    private byte[] ids;
    private Object[] tables;

    public ChaffmanTableSetChunk() {
        super(JPEGDecoder.DHT);
    }

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        int chunkLen;
        int dataCount;
        byte[] dataIds;
        int[] bits;
        ChaffmanTable[] dataTables;
        if((chunkLen = readSize(stream)) < 0x11)
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        dataCount = 0;
        dataIds = new byte[1];
        dataTables = new ChaffmanTable[1];
        bits = new int[0x10];
        do
        {
            int id;
            int count;
            byte[] valueSet;
            short[] valueIndices;
            int[] minCodes;
            int[] maxCodes;
            int[] chaCodes;
            int[] chaCodesLens;
            if(chunkLen < 0x11)
            {
                throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
            }
            chunkLen -= 0x11;
            id = stream.readByte();
            count = 0;
            for(int i = 0; i < 0x10; i++) count += (bits[i] = stream.readUnsignedByte());
            if(chunkLen < count)
            {
                throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
            }
            chunkLen -= count;
            stream.readFully(valueSet = new byte[count]);
            chaCodesLens = new int[count];
            for(int index = 0, i = 1; i <= 0x10; i++) for(int len = bits[i - 1], j = 0; j < len; j++) chaCodesLens[index++] = i;
            chaCodes = new int[count];
            for(int index = 0, code = 0, si = count <= 0 ? 0 : chaCodesLens[0], p = 0; p < count; code <<= 1, si++) for(; p < count && chaCodesLens[p] == si; p++) chaCodes[index++] = code++;
            valueIndices = new short[0x10];
            minCodes = new int[0x10];
            maxCodes = new int[0x10];
            for(int k = 0, i = 0; i < 0x10; i++)
            {
                int bsize;
                if((bsize = bits[i]) <= 0)
                {
                    maxCodes[i] = -1;
                    continue;
                }
                valueIndices[i] = (short) k;
                minCodes[i] = chaCodes[k];
                maxCodes[i] = chaCodes[(k += bsize) - 1];
            }
            if(dataIds.length == dataCount)
            {
                int newLen = (dataCount << 1) + 1;
                Array.copy(dataIds, 0, dataIds = new byte[newLen], 0, dataCount);
                Array.copy(dataTables, 0, dataTables = new ChaffmanTable[newLen], 0, dataCount);
            }
            dataTables[dataCount] = new ChaffmanTable(valueSet, valueIndices, minCodes, maxCodes);
            dataIds[dataCount] = (byte) (id & 0x1f);
            dataCount++;
        } while(chunkLen > 0);
        count = dataCount;
        ids = dataIds;
        tables = dataTables;
    }

    public int getTablesCount() {
        return count;
    }

    public int getTableId(int index) {
        return ids[index];
    }

    public Object getTable(int index) {
        return tables[index];
    }
}

final class StartOfScanChunk extends Chunk
{
    public int spectralSelectionBegin;
    public int spectralSelectionEnd;
    public int approximateBitPositionHigh;
    public int approximateBitPositionLow;
    private byte[] componentsIds;

    public StartOfScanChunk() {
        super(JPEGDecoder.SOS);
    }

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        int byte0;
        int chunkLen;
        byte[] componentData;
        if((chunkLen = readSize(stream)) < 6 || (stream.readUnsignedByte() << 1) != (chunkLen -= 4))
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        stream.readFully(componentData = new byte[chunkLen]);
        spectralSelectionBegin = stream.readUnsignedByte();
        spectralSelectionEnd = stream.readUnsignedByte();
        approximateBitPositionHigh = (byte0 = stream.readByte()) >> 4 & 0x0f;
        approximateBitPositionLow = byte0 & 0x0f;
        componentsIds = componentData;
    }

    public boolean isVeryProgressive() {
        int begin = spectralSelectionBegin;
        int end = spectralSelectionEnd;
        int high = approximateBitPositionHigh;
        int low = approximateBitPositionLow;
        return (begin <= 0 && end <= 0 || begin <= end && end <= 0x3f) && low <= 0x0d && high <= 0x0d && (high <= 0 || high == low + 1) && (begin <= 0 || getComponentsCount() == 1);
    }

    public boolean isACProgressive() {
        return spectralSelectionBegin != 0 && spectralSelectionEnd != 0;
    }

    public boolean isDCProgressive() {
        return (spectralSelectionBegin | spectralSelectionEnd) == 0;
    }

    public int getComponentsCount() {
        return componentsIds.length >> 1;
    }

    public int getComponentId(int index) {
        return componentsIds[index << 1] & 0xff;
    }

    public int getComponentChaffmanTableId(int index, int table) {
        return componentsIds[(index << 1) + 1] >> ((table ^ 1) << 2) & 0x0f | table << 4;
    }
}

final class ScanDecoder extends Object
{
    private final boolean progressive;
    private int eobrun;
    private final int[] dcs;
    private StartOfScanChunk scan;
    private final InputStream source;
    private final JPEGBitInputStream stream;
    private final ImageHeaderChunk header;

    public ScanDecoder(InputStream stream, ImageHeaderChunk header) {
        this.progressive = header.getCodingType() == ImageHeaderChunk.PROGRESSIVE;
        this.dcs = new int[JPEGDecoder.MAX_COMPONENTS];
        this.source = stream;
        this.stream = new JPEGBitInputStream(stream);
        this.header = header;
    }

    public void decodeScan(int restartInterval, StartOfScanChunk scan) throws IOException {
        boolean first;
        int horzBlocks;
        int vertBlocks;
        int spectralEnd;
        int spectralBegin;
        int restartRemain;
        int approximateBit;
        int componentsCount;
        ImageHeaderChunk header;
        if(progressive && !scan.isVeryProgressive())
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        stream.reset();
        this.scan = scan;
        header = this.header;
        horzBlocks = header.horzBlocks;
        vertBlocks = header.vertBlocks;
        Array.fill(dcs, 0, JPEGDecoder.MAX_COMPONENTS, 0);
        if((componentsCount = scan.getComponentsCount()) == 1)
        {
            int mcuWidth;
            int mcuHeight;
            Component component = header.getComponent(scan.getComponentId(0));
            mcuWidth = (header.maxHorzFactor << 3) / component.horzFactor;
            mcuHeight = (header.maxVertFactor << 3) / component.vertFactor;
            horzBlocks = (header.width + mcuWidth - 1) / mcuWidth;
            vertBlocks = (header.height + mcuHeight - 1) / mcuHeight;
        }
        first = scan.approximateBitPositionHigh == 0;
        spectralBegin = scan.spectralSelectionBegin;
        spectralEnd = scan.spectralSelectionEnd;
        approximateBit = scan.approximateBitPositionLow;
        restartRemain = restartInterval;
        for(int ymcu = 0; ymcu < vertBlocks; ymcu++) for(int xmcu = 0; xmcu < horzBlocks; xmcu++)
        {
            if(restartInterval != 0)
            {
                if(restartRemain == 0)
                {
                    handleRestartInterval();
                    restartRemain = restartInterval;
                }
                restartRemain--;
            }
            decodeMCU(xmcu, ymcu, componentsCount, first, spectralBegin, spectralEnd, approximateBit);
        }
    }

    private void decodeMCU(int xmcu, int ymcu, int componentsCount, boolean first, int begin, int end, int approximateBit) throws IOException {
        boolean progressive = this.progressive;
        ImageHeaderChunk header = this.header;
        StartOfScanChunk scan = this.scan;
        for(int componentIndex = 0; componentIndex < componentsCount; componentIndex++)
        {
            int hf;
            int vf;
            int[] blocks;
            Component component;
            blocks = (component = header.getComponent(scan.getComponentId(componentIndex))).blocks;
            if(componentsCount == 1)
            {
                hf = 1;
                vf = 1;
            } else
            {
                hf = component.horzFactor;
                vf = component.vertFactor;
            }
            for(int v = 0; v < vf; v++) for(int h = 0; h < hf; h++)
            {
                int offset = component.getBlocksOffset(h + hf * xmcu, v + vf * ymcu);
                if(!progressive) Array.fill(blocks, offset, 0x40, 0);
                if(!progressive || scan.isDCProgressive()) decodeDCCoefficient(blocks, offset, component, componentIndex, first, approximateBit);
                if(!progressive)
                {
                    decodeACCoefficients(blocks, offset, component);
                }
                else if(scan.isACProgressive())
                {
                    if(first)
                    {
                        decodeACFirstCoefficients(blocks, offset, component, begin, end, approximateBit);
                    } else
                    {
                        decodeACRefineCoefficients(blocks, offset, component, begin, end, approximateBit);
                    }
                }
            }
        }
    }

    private void decodeACCoefficients(int[] blocks, int offset, Component component) throws IOException {
        byte[] order = JPEGDecoder.ORDER_ZIGZAG_INVERTED;
        ChaffmanTable table = component.chaffmanTableAC;
        JPEGBitInputStream stream = this.stream;
        for(int k = 1; k < 0x40; )
        {
            int rs = table.readValue(stream);
            int s = rs & 0x0f;
            int r = rs >> 4;
            if(s == 0)
            {
                if(r == 0x0f)
                {
                    k += 0x10;
                } else
                {
                    break;
                }
            } else
            {
                int bits = stream.readAndExtend(s);
                blocks[offset + order[k += r]] = bits;
                k++;
            }
        }
    }

    private void decodeACFirstCoefficients(int[] blocks, int offset, Component component, int begin, int end, int approximateBit) throws IOException {
        int eobrun;
        byte[] order;
        ChaffmanTable table;
        JPEGBitInputStream stream;
        if((eobrun = this.eobrun) > 0)
        {
            this.eobrun = eobrun - 1;
            return;
        }
        order = JPEGDecoder.ORDER_ZIGZAG_INVERTED;
        table = component.chaffmanTableAC;
        stream = this.stream;
        for(int k = begin; k <= end; )
        {
            int rs = table.readValue(stream);
            int s = rs & 0x0f;
            int r = rs >> 4;
            if(s == 0)
            {
                if(r == 0x0f)
                {
                    k += 0x10;
                } else
                {
                    this.eobrun = (1 << r) + stream.readBits(r) - 1;
                    break;
                }
            } else
            {
                int bits = stream.readAndExtend(s);
                blocks[offset + order[k += r]] = bits << approximateBit;
                k++;
            }
        }
    }

    private void decodeACRefineCoefficients(int[] blocks, int offset, Component component, int begin, int end, int approximateBit) throws IOException {
        int eobrun = this.eobrun;
        byte[] order = JPEGDecoder.ORDER_ZIGZAG_INVERTED;
        ChaffmanTable table = component.chaffmanTableAC;
        JPEGBitInputStream stream = this.stream;
        for(int k = begin; k <= end; )
        {
            if(eobrun > 0)
            {
                for(; k <= end; k++)
                {
                    int index = offset + order[k];
                    blocks[index] = stream.refineAC(blocks[index], approximateBit);
                }
                eobrun--;
            } else
            {
                int rs = table.readValue(stream);
                int s = rs & 0x0f;
                int r = rs >> 4;
                if(s == 0)
                {
                    if(r == 0x0f)
                    {
                        for(int zeros = 0; zeros < 0x10 && k <= end; k++)
                        {
                            int index = offset + order[k];
                            if(blocks[index] != 0)
                            {
                                blocks[index] = stream.refineAC(blocks[index], approximateBit);
                            } else
                            {
                                zeros++;
                            }
                        }
                    } else
                    {
                        eobrun = (1 << r) + stream.readBits(r);
                    }
                } else
                {
                    int bits = stream.readBits(s);
                    int index = offset + order[k];
                    for(int zeros = 0; (zeros < r || blocks[index] != 0) && k <= end; index = offset + order[++k])
                    {
                        if(blocks[index] != 0)
                        {
                            blocks[index] = stream.refineAC(blocks[index], approximateBit);
                        } else
                        {
                            zeros++;
                        }
                    }
                    blocks[index] = (bits != 0 ? 1 : -1) << approximateBit;
                    k++;
                }
            }
        }
        this.eobrun = eobrun;
    }

    private void decodeDCCoefficient(int[] blocks, int offset, Component component, int componentIndex, boolean first, int approximateBit) throws IOException {
        boolean progressive = this.progressive;
        int dc = 0;
        ChaffmanTable table = component.chaffmanTableDC;
        if(progressive && !first)
        {
            int bit = stream.readBit();
            dc = blocks[offset] + (bit << approximateBit);
        } else
        {
            int q;
            int[] dcs;
            JPEGBitInputStream stream = this.stream;
            dc = (dcs = this.dcs)[componentIndex];
            if((q = table.readValue(stream)) != 0)
            {
                int diff = stream.readAndExtend(q);
                dcs[componentIndex] = (dc += diff);
            }
            if(!progressive) dc <<= approximateBit;
        }
        blocks[offset] = dc;
    }

    private void handleRestartInterval() throws IOException {
        int byte0;
        int chunkName;
        InputStream source = this.source;
        do
        {
            if((byte0 = source.read()) < 0)
            {
                throw new EOFException("DataInputStream.readUnsignedByte: достигнут конец потока данных.");
            }
        } while(byte0 != 0xff);
        do
        {
            if((byte0 = source.read()) < 0)
            {
                throw new EOFException("DataInputStream.readUnsignedByte: достигнут конец потока данных.");
            }
        } while(byte0 == 0xff);
        if((chunkName = JPEGDecoder.MIN | byte0) < JPEGDecoder.RST0 || chunkName > JPEGDecoder.RST7)
        {
            throw new InvalidDataFormatException("JPEGDecoder.loadFromInputStream: неправильный формат данных.");
        }
        eobrun = 0;
        stream.reset();
        Array.fill(dcs, 0, JPEGDecoder.MAX_COMPONENTS, 0);
    }
}
