/*
    Реализация спецификаций 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 PNGEncoder extends Object implements ImageEncoder
{
    public static final long SIGNATURE = 0x89504e470d0a1a0aL;

    private static final int IHDR = 0x49484452; /* IHDR */
    private static final int PLTE = 0x504c5445; /* PLTE */
    private static final int TRNS = 0x74524e53; /* tRNS */
    private static final int TEXT = 0x74455874; /* tEXt */
    private static final int IDAT = 0x49444154; /* IDAT */
    private static final int IEND = 0x49454e44; /* IEND */

    private static void writeChunk(DataOutputStream stream, ByteArrayOutputStream chunk, Checksum32 checksum) throws IOException {
        int chunkBytesLength;
        byte[] chunkBytes;
        chunkBytesLength = (chunkBytes = chunk.toByteArray()).length;
        checksum.reset();
        checksum.update(chunkBytes, 0, chunkBytesLength);
        stream.writeInt(chunkBytesLength - 4);
        stream.write(chunkBytes);
        stream.writeInt(checksum.value());
    }

    private static void writeTag(DataOutputStream stream, ByteArrayOutputStream chunkStream, DataOutputStream chunkData, Checksum32 checksum, String tag, String value) throws IOException {
        chunkStream.reset();
        chunkData.writeInt(TEXT);
        chunkData.write(tag.getBytes());
        chunkData.write(0);
        chunkData.write(value.getBytes());
        writeChunk(stream, chunkStream, checksum);
    }

    private boolean alpha;
    private int level;
    private int width;
    private int height;
    private int[] pixels;

    public PNGEncoder() {
        this.level = 7;
    }

    public PNGEncoder(int compressionLevel) {
        this.level = compressionLevel < 0 ? 0 : compressionLevel > 9 ? 9 : compressionLevel;
    }

    public void saveToOutputStream(OutputStream stream) throws IOException {
        saveToDataStream(new ExtendedDataOutputStream(stream));
    }

    public void saveToDataStream(ExtendedDataOutputStream stream) throws IOException {
        boolean useAlpha = alpha;
        boolean usePalette;
        int compressionLevel = level;
        int lineSize;
        int bitDepth;
        int pixelType;
        int components;
        int paletteLength;
        int imagePixelsLength;
        int imageWidth = width;
        int imageHeight = height;
        byte[] rawPixels;
        int[] palette;
        int[] imagePixels = pixels;
        Checksum32 checksum;
        DataOutputStream chunkData;
        ByteArrayOutputStream chunkStream;
        if(imagePixels == null)
        {
            throw new EmptyAdapterException("PNGEncoder.saveToOutputStream: кодер не содержит данные.");
        }

        /* палитра */
        imagePixelsLength = imagePixels.length;
        paletteLength = 0;
        palette = new int[0x0100];
        usePalette = true;
        for(int i = 0; i < imagePixelsLength; i++)
        {
            int pixel = useAlpha ? imagePixels[i] : (imagePixels[i] &= 0x00ffffff);
            if(paletteLength <= 0 || Array.findb(palette, paletteLength - 1, pixel) < 0)
            {
                if(paletteLength >= 0x0100)
                {
                    paletteLength = 0;
                    palette = null;
                    usePalette = false;
                    break;
                }
                palette[paletteLength++] = pixel;
            }
        }

        /* тип пикселов (2=RGB, 3=P, 6=RGBA) */
        if(usePalette)
        {
            bitDepth = paletteLength <= 2 ? 1 : paletteLength <= 4 ? 2 : paletteLength <= 16 ? 4 : 8;
            pixelType = 3;
            components = 1;
            if(useAlpha)
            {
                useAlpha = false;
                for(int i = paletteLength; i-- > 0; )
                {
                    if((palette[i] >>> 24) < 0xff)
                    {
                        useAlpha = true;
                        break;
                    }
                }
            }
        } else
        {
            bitDepth = 8;
            if(!useAlpha)
            {
                pixelType = 2;
                components = 3;
            } else
            {
                useAlpha = false;
                for(int i = imagePixelsLength; i-- > 0; )
                {
                    if((imagePixels[i] >>> 24) < 0xff)
                    {
                        useAlpha = true;
                        break;
                    }
                }
                if(useAlpha)
                {
                    pixelType = 6;
                    components = 4;
                } else
                {
                    pixelType = 2;
                    components = 3;
                }
            }
        }

        /* кодирование */
        lineSize = bitDepth < 8 ? (imageWidth * bitDepth + 7) >> 3 : imageWidth * components;
        rawPixels = new byte[imageHeight * (1 + lineSize)];
        for(int imageOffset = 0, rawOffset = 0, y = 0; y < imageHeight; y++)
        {
            rawPixels[rawOffset++] = (byte) 1;
            for(int subOffset = rawOffset, x = 0; x < imageWidth; x++)
            {
                int shift;
                int pixel = imagePixels[imageOffset++];
                switch(pixelType)
                {
                default:
                    break;
                case 2:
                    rawPixels[subOffset++] = (byte) (pixel >> 16);
                    rawPixels[subOffset++] = (byte) (pixel >> 8);
                    rawPixels[subOffset++] = (byte) pixel;
                    break;
                case 3:
                    pixel = Array.findf(palette, 0, pixel);
                    switch(bitDepth)
                    {
                    case 1:
                        shift = (x & 7) ^ 7;
                        rawPixels[subOffset] = (byte) (rawPixels[subOffset] & ~(0x01 << shift) | pixel << shift);
                        if(shift == 0) subOffset++;
                        break;
                    case 2:
                        shift = ((x & 3) ^ 3) << 1;
                        rawPixels[subOffset] = (byte) (rawPixels[subOffset] & ~(0x03 << shift) | pixel << shift);
                        if(shift == 0) subOffset++;
                        break;
                    case 4:
                        shift = ((x & 1) ^ 1) << 2;
                        rawPixels[subOffset] = (byte) (rawPixels[subOffset] & ~(0x0f << shift) | pixel << shift);
                        if(shift == 0) subOffset++;
                        break;
                    case 8:
                        rawPixels[subOffset++] = (byte) pixel;
                        break;
                    }
                    break;
                case 6:
                    rawPixels[subOffset++] = (byte) (pixel >> 16);
                    rawPixels[subOffset++] = (byte) (pixel >> 8);
                    rawPixels[subOffset++] = (byte) pixel;
                    rawPixels[subOffset++] = (byte) (pixel >> 24);
                    break;
                }
            }
            for(int lim = rawOffset + components, j = (rawOffset += lineSize); j-- > lim; ) rawPixels[j] = (byte) (rawPixels[j] - rawPixels[j - components]);
        }
        rawPixels = Zlib.compress(rawPixels, compressionLevel);

        /* запись данных */
        checksum = new CRC32();
        chunkData = new DataOutputStream(chunkStream = new ByteArrayOutputStream(16));
        stream.writeLong(SIGNATURE);

        /* кусок – заголовок */
        chunkStream.reset();
        chunkData.writeInt(IHDR);
        chunkData.writeInt(imageWidth); /* ширина */
        chunkData.writeInt(imageHeight); /* высота */
        chunkData.writeByte(bitDepth); /* глубина цвета */
        chunkData.writeByte(pixelType); /* тип пикселов */
        chunkData.writeByte(0); /* алгоритм сжатия */
        chunkData.writeByte(0); /* алгоритм фильтрации */
        chunkData.writeByte(0); /* алгоритм отображения */
        writeChunk(stream, chunkStream, checksum);

        if(usePalette)
        {
            /* кусок – палитра */
            chunkStream.reset();
            chunkData.writeInt(PLTE);
            for(int i = 0; i < paletteLength; i++)
            {
                int colour = palette[i];
                chunkData.writeByte(colour >> 16);
                chunkData.writeByte(colour >> 8);
                chunkData.writeByte(colour);
            }
            writeChunk(stream, chunkStream, checksum);

            if(useAlpha)
            {
                int len = paletteLength;
                for(int i = paletteLength; i-- > 0; )
                {
                    if((palette[i] >>> 24) < 0xff)
                    {
                        len = i + 1;
                        break;
                    }
                }

                /* кусок – палитра, альфа-канал */
                chunkStream.reset();
                chunkData.writeInt(TRNS);
                for(int i = 0; i < len; i++) chunkData.writeByte(palette[i] >> 24);
                writeChunk(stream, chunkStream, checksum);
            }
        }

        /* кусок – таг «Программное обеспечение» */
        writeTag(stream, chunkStream, chunkData, checksum, "Software", "Malik Emulator https://malik-elaborarer.ru/emulator/");

        {
            int rawLength = rawPixels.length;
            int chunksCount = (rawLength + (-rawLength & 0xffff)) >> 16;
            int lim = (rawLength & 0xffff) <= 0 ? chunksCount : chunksCount - 1;
            for(int i = 0; i < chunksCount; i++)
            {
                /* кусок – данные */
                chunkStream.reset();
                chunkData.writeInt(IDAT);
                chunkData.write(rawPixels, i << 16, i < lim ? 0x00010000 : rawLength & 0xffff);
                writeChunk(stream, chunkStream, checksum);
            }
        }

        /* кусок – конец */
        chunkStream.reset();
        chunkData.writeInt(IEND);
        writeChunk(stream, chunkStream, checksum);
    }

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

    public void setPixels(boolean alpha, int width, int height, int[] pixels) throws InvalidDataFormatException, UnsupportedDataException {
        int iarea;
        long area;
        if(pixels == null)
        {
            throw new NullPointerException("PNGEncoder.setPixels: аргумент pixels равен нулевой ссылке.");
        }
        if(width < 1 || height < 1)
        {
            throw new InvalidDataFormatException("PNGEncoder.setPixels: размеры могут быть только положительными.");
        }
        if((long) pixels.length < (area = (long) width * (long) height))
        {
            throw new InvalidDataFormatException("PNGEncoder.setPixels: длина аргумента pixels не может быть меньше произведения размеров.");
        }
        if(area > 0x00100000L)
        {
            throw new UnsupportedDataException("PNGEncoder.setPixels: размеры слишком велики.");
        }
        Array.copy(pixels, 0, pixels = new int[iarea = (int) area], 0, iarea);
        this.alpha = alpha;
        this.width = width;
        this.height = height;
        this.pixels = pixels;
    }

    public boolean alphaSupported() {
        return true;
    }

    public boolean isEmpty() {
        return pixels == null;
    }

    public void setCompressionLevel(int compressionLevel) {
        this.level = compressionLevel < 0 ? 0 : compressionLevel > 9 ? 9 : compressionLevel;
    }

    public int getCompressionLevel() {
        return level;
    }
}
