/*
    Реализация спецификаций 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.png;

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

public final class PNGDecoder extends Object implements ImageDecoder
{
    public static final long SIGNATURE = 0x89504e470d0a1a0aL;

    private static final int IHDR = 0x49484452; /* IHDR */
    private static final int GAMA = 0x67414d41; /* gAMA */
    private static final int PLTE = 0x504c5445; /* PLTE */
    private static final int TRNS = 0x74524e53; /* tRNS */
    private static final int IDAT = 0x49444154; /* IDAT */
    private static final int IEND = 0x49454e44; /* IEND */

    static int decodeNormal(byte[] stream, int offset, int pixelType, int bitDepth, int components, byte[] imageData, int imageWidth, int imageHeight, UnwrapMethod method) {
        int k = getLineSize(pixelType, bitDepth, components, imageWidth);
        int f = getFilterSize(pixelType, bitDepth, components);
        int total = imageWidth * components;
        int lineSize = k++;
        for(int ofs = 0, i = 0, j, lim; i < imageHeight; offset += lineSize, ofs += total, i++)
        {
            switch(stream[offset++])
            {
            default:
                break;
            case 1:
                for(lim = offset + lineSize, j = offset + f; j < lim; j++) stream[j] = (byte) (stream[j] + stream[j - f]);
                break;
            case 2:
                if(i > 0) for(lim = offset + lineSize, j = offset; j < lim; j++) stream[j] = (byte) (stream[j] + stream[j - k]);
                break;
            case 3:
                if(i > 0)
                {
                    for(lim = offset + f, j = offset; j < lim; j++) stream[j] = (byte) (stream[j] + ((stream[j - k] & 0xff) >> 1));
                    for(lim = offset + lineSize; j < lim; j++) stream[j] = (byte) (stream[j] + ((stream[j - k] & 0xff) + (stream[j - f] & 0xff) >> 1));
                } else
                {
                    for(lim = offset + lineSize, j = offset + f; j < lim; j++) stream[j] = (byte) (stream[j] + ((stream[j - f] & 0xff) >> 1));
                }
                break;
            case 4:
                if(i > 0)
                {
                    for(lim = offset + f, j = offset; j < lim; j++) stream[j] = (byte) (stream[j] + paethPredictor(0, stream[j - k] & 0xff, 0));
                    for(lim = offset + lineSize; j < lim; j++) stream[j] = (byte) (stream[j] + paethPredictor(stream[j - f] & 0xff, stream[j - k] & 0xff, stream[j - f - k] & 0xff));
                } else
                {
                    for(lim = offset + lineSize, j = offset + f; j < lim; j++) stream[j] = (byte) (stream[j] + paethPredictor(stream[j - f] & 0xff, 0, 0));
                }
                break;
            }
            method.unwrap(stream, offset, imageData, ofs, total);
        }
        return offset;
    }

    static int decodeAdam7(byte[] stream, int offset, int pixelType, int bitDepth, int components, byte[] imageData, int imageWidth, int imageHeight, UnwrapMethod method) {
        int[] interlacing;
        for(int i = (interlacing = new int[] { 0x0112, 0x1022, 0x0224, 0x2044, 0x0448, 0x4088, 0x0088 }).length; i-- > 0; )
        {
            int current;
            int xstart = (current = interlacing[i]) >> 12;
            int ystart = (current >> 8) & 0x0f;
            int xinc = (current >> 4) & 0x0f;
            int yinc = current & 0x0f;
            int width = (imageWidth - xstart + xinc - 1) / xinc;
            int height = (imageHeight - ystart + yinc - 1) / yinc;
            byte[] data = new byte[width * height * components];
            offset = decodeNormal(stream, offset, pixelType, bitDepth, components, data, width, height, method);
            for(int y0 = 0, y = ystart; y < imageHeight; y0++, y += yinc) for(int x0 = 0, x = xstart; x < imageWidth; x0++, x += xinc)
            {
                Array.copy(data, (y0 * width + x0) * components, imageData, (y * imageWidth + x) * components, components);
            }
        }
        return offset;
    }

    private static void copy(InputStream input, OutputStream output, long count) throws IOException {
        int remainder = (int) count & 0xffff;
        long fragments = count >> 16;
        byte[] buffer = new byte[0x00010000];
        for(int i = 0; i < fragments; i++) output.write(buffer, 0, input.read(buffer));
        if(remainder > 0) output.write(buffer, 0, input.read(buffer, 0, remainder));
    }

    private static double power(double base, double exponent) {
        return base > 0.d ? Math.pow2(exponent * Math.log2(base)) : 0.d;
    }

    private static int paethPredictor(int a, int b, int c) {
        int p = a + b - c;
        int pa = Math.abs(p - a);
        int pb = Math.abs(p - b);
        int pc = Math.abs(p - c);
        return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
    }

    private static int readPixel(byte[] data, int offset, int[] gamma, int[] palette, boolean useAlpha, boolean usePalette, boolean useGrayscale) {
        int r;
        int g;
        int b;
        int a;
        if(usePalette)
        {
            int p;
            a = (p = palette[data[offset] & 0xff]) >> 24;
            r = p >> 16;
            g = p >> 8;
            b = p;
        }
        else if(useAlpha)
        {
            if(useGrayscale)
            {
                r = g = b = data[offset];
                a = data[offset + 1];
            } else
            {
                r = data[offset];
                g = data[offset + 1];
                b = data[offset + 2];
                a = data[offset + 3];
            }
        }
        else
        {
            if(useGrayscale)
            {
                r = g = b = data[offset];
            } else
            {
                r = data[offset];
                g = data[offset + 1];
                b = data[offset + 2];
            }
            a = 0xff;
        }
        return a << 24 | gamma[r & 0xff] << 16 | gamma[g & 0xff] << 8 | gamma[b & 0xff];
    }

    private static int getLineSize(int pixelType, int bitDepth, int components, int imageWidth) {
        return (pixelType == 0 || pixelType == 3 ? bitDepth * imageWidth + 7 : bitDepth * imageWidth * components) >> 3;
    }

    private static int getFilterSize(int pixelType, int bitDepth, int components) {
        return (pixelType == 0 || pixelType == 3 ? bitDepth < 16 ? 8 : 16 : bitDepth * components) >> 3;
    }

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

    public PNGDecoder() {
    }

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

    public void loadFromDataStream(ExtendedDataInputStream stream) throws IOException {
        boolean useAlpha = false;
        boolean usePalette = false;
        boolean useGrayscale = false;
        boolean dataHandled = false;
        boolean gammaHandled = false;
        boolean headerHandled = false;
        boolean paletteHandled = false;
        boolean transparencyHandled = false;
        int chunkName;
        int bitDepth = 0;
        int pixelType = 0;
        int components = 0;
        int imageWidth = 0;
        int imageHeight = 0;
        byte[] imageData = null;
        int[] imageGamma = new int[0x0100];
        int[] imagePixels = null;
        int[] imagePalette = null;
        DecodeMethod dmethod = null;
        UnwrapMethod umethod = null;
        ByteArrayOutputStream compressedData = null;
        for(int i = 0x0100; i-- > 0; imageGamma[i] = i);
        /* чтение входного потока данных */
        do
        {
            int chunkSize = stream.readInt();
            switch(chunkName = stream.readInt())
            {
            default:
                stream.skip((long) chunkSize & 0x00000000ffffffffL);
                break;
            case IHDR:
            {
                int w;
                int h;
                int bd;
                int pt;
                int cm;
                int fm;
                int mm;
                long area;
                if(headerHandled || chunkSize != 0x0d)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                w = stream.readInt(); /* ширина */
                h = stream.readInt(); /* высота */
                if((area = ((long) w & 0xffffffffL) * ((long) h & 0xffffffffL)) <= 0L || area > 0x00100000L)
                {
                    throw new UnsupportedDataException("PNGDecoder.loadFromInputStream: неподдерживаемые данные.");
                }
                bd = stream.readUnsignedByte(); /* глубина цвета */
                pt = stream.readUnsignedByte(); /* тип пикселов (0=G, 2=RGB, 3=P, 4=GA, 6=RGBA) */
                if((pt != 0 || bd != 1 && bd != 2 && bd != 4 && bd != 8 && bd != 16) && (pt != 3 || bd != 1 && bd != 2 && bd != 4 && bd != 8) && (pt != 2 && pt != 4 && pt != 6 || bd != 8 && bd != 16))
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                cm = stream.readUnsignedByte(); /* алгоритм сжатия */
                fm = stream.readUnsignedByte(); /* алгоритм фильтрации */
                mm = stream.readUnsignedByte(); /* алгоритм отображения */
                if(cm != 0 || fm != 0 || mm < 0 || mm > 1)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                useAlpha = pt == 4 || pt == 6;
                usePalette = pt == 3;
                useGrayscale = pt == 0 || pt == 4;
                bitDepth = bd;
                pixelType = pt;
                components = pt == 6 ? 4 : pt == 2 ? 3 : pt == 4 ? 2 : 1;
                imageWidth = w;
                imageHeight = h;
                imagePixels = new int[w * h];
                imagePalette = usePalette ? new int[0x0100] : null;
                dmethod = mm == 0 ? (DecodeMethod) (new DecodeNormal()) : (DecodeMethod) (new DecodeAdam7());
                if(usePalette)
                {
                    switch(bitDepth)
                    {
                    default:
                        break;
                    case 1:
                        umethod = new Unwrap1bitPP();
                        break;
                    case 2:
                        umethod = new Unwrap2bitPP();
                        break;
                    case 4:
                        umethod = new Unwrap4bitPP();
                        break;
                    case 8:
                        umethod = new Unwrap8bitPC();
                        break;
                    }
                } else
                {
                    switch(bitDepth)
                    {
                    default:
                        break;
                    case 1:
                        umethod = new Unwrap1bitPC();
                        break;
                    case 2:
                        umethod = new Unwrap2bitPC();
                        break;
                    case 4:
                        umethod = new Unwrap4bitPC();
                        break;
                    case 8:
                        umethod = new Unwrap8bitPC();
                        break;
                    case 16:
                        umethod = new Unwrap16bitPC();
                        break;
                    }
                }
                headerHandled = true;
                break;
            }
            case GAMA:
            {
                double exponent;
                if(!headerHandled || gammaHandled || chunkSize != 0x04)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                exponent = 1.e+005d / (2.2d * (double) stream.readInt());
                for(int i = 0x0100; i-- > 0; ) imageGamma[i] = (int) Math.round(255.d * power((double) i / 255.d, exponent));
                gammaHandled = true;
                break;
            }
            case PLTE:
                if(!headerHandled || paletteHandled || chunkSize < 0 || chunkSize > 0x0300 || chunkSize % 3 != 0 || !usePalette)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                for(int len = chunkSize / 3, i = 0; i < len; i++)
                {
                    int r = stream.readUnsignedByte();
                    int g = stream.readUnsignedByte();
                    int b = stream.readUnsignedByte();
                    imagePalette[i] = 0xff000000 | r << 16 | g << 8 | b;
                }
                paletteHandled = true;
                break;
            case TRNS:
                if(!headerHandled || transparencyHandled || chunkSize < 0 || chunkSize > 0x0100)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                if(usePalette)
                {
                    for(int i = 0; i < chunkSize; i++) imagePalette[i] = imagePalette[i] & 0x00ffffff | stream.readUnsignedByte() << 24;
                } else
                {
                    /* прозрачность по цвету не поддерживается */
                    stream.skip((long) chunkSize & 0x00000000ffffffffL);
                }
                transparencyHandled = true;
                break;
            case IDAT:
                if(!headerHandled || chunkSize < 0)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                copy(stream, compressedData == null ? (compressedData = new ByteArrayOutputStream()) : compressedData, (long) chunkSize);
                dataHandled = true;
                break;
            case IEND:
                int length;
                byte[] rawData;
                if(!headerHandled || !dataHandled || chunkSize != 0 || compressedData == null)
                {
                    throw new InvalidDataFormatException("PNGDecoder.loadFromInputStream: неправильный формат данных.");
                }
                length = imageWidth * imageHeight * components;
                rawData = Zlib.decompress(compressedData.toByteArray());
                dmethod.decode(rawData, 0, pixelType, bitDepth, components, imageData = new byte[length], imageWidth, imageHeight, umethod);
                break;
            }
            stream.skip(4L); /* игнорируем контрольную сумму куска */
        } while(chunkName != IEND);
        /* построение изображения */
        for(int len = imagePixels.length, offset = 0, i = 0; i < len; offset += components, i++)
        {
            imagePixels[i] = readPixel(imageData, offset, imageGamma, imagePalette, useAlpha, usePalette, useGrayscale);
        }
        /* вывод результата */
        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 true;
    }

    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;
    }
}

abstract class UnwrapMethod extends Object
{
    public UnwrapMethod() {
    }

    public abstract void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length);
}

final class Unwrap1bitPC extends UnwrapMethod
{
    public Unwrap1bitPC() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = (byte) (((src[srcOffset + (i >>> 3)] & 0xff) >> ((i & 0x07) ^ 0x07)) << 7);
    }
}

final class Unwrap1bitPP extends UnwrapMethod
{
    public Unwrap1bitPP() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = (byte) (((src[srcOffset + (i >>> 3)] & 0xff) >> ((i & 0x07) ^ 0x07)) & 0x01);
    }
}

final class Unwrap2bitPC extends UnwrapMethod
{
    public Unwrap2bitPC() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = (byte) (((src[srcOffset + (i >>> 2)] & 0xff) >> (((i & 0x03) ^ 0x03) << 1)) << 6);
    }
}

final class Unwrap2bitPP extends UnwrapMethod
{
    public Unwrap2bitPP() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = (byte) (((src[srcOffset + (i >>> 2)] & 0xff) >> (((i & 0x03) ^ 0x03) << 1)) & 0x03);
    }
}

final class Unwrap4bitPC extends UnwrapMethod
{
    public Unwrap4bitPC() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = (byte) (((src[srcOffset + (i >>> 1)] & 0xff) >> (((i & 0x01) ^ 0x01) << 2)) << 4);
    }
}

final class Unwrap4bitPP extends UnwrapMethod
{
    public Unwrap4bitPP() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = (byte) (((src[srcOffset + (i >>> 1)] & 0xff) >> (((i & 0x01) ^ 0x01) << 2)) & 0x0f);
    }
}

final class Unwrap8bitPC extends UnwrapMethod
{
    public Unwrap8bitPC() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        Array.copy(src, srcOffset, dst, dstOffset, length);
    }
}

final class Unwrap16bitPC extends UnwrapMethod
{
    public Unwrap16bitPC() {
    }

    public void unwrap(byte[] src, int srcOffset, byte[] dst, int dstOffset, int length) {
        for(int i = 0; i < length; i++) dst[dstOffset++] = src[srcOffset + (i << 1)];
    }
}

abstract class DecodeMethod extends Object
{
    public DecodeMethod() {
    }

    public abstract int decode(byte[] stream, int offset, int pixelType, int bitDepth, int components, byte[] imageData, int imageWidth, int imageHeight, UnwrapMethod method);
}

final class DecodeNormal extends DecodeMethod
{
    public DecodeNormal() {
    }

    public int decode(byte[] stream, int offset, int pixelType, int bitDepth, int components, byte[] imageData, int imageWidth, int imageHeight, UnwrapMethod method) {
        return PNGDecoder.decodeNormal(stream, offset, pixelType, bitDepth, components, imageData, imageWidth, imageHeight, method);
    }
}

final class DecodeAdam7 extends DecodeMethod
{
    public DecodeAdam7() {
    }

    public int decode(byte[] stream, int offset, int pixelType, int bitDepth, int components, byte[] imageData, int imageWidth, int imageHeight, UnwrapMethod method) {
        return PNGDecoder.decodeAdam7(stream, offset, pixelType, bitDepth, components, imageData, imageWidth, imageHeight, method);
    }
}
