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

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

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

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

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


package malik.emulator.fileformats.graphics.pnglib;

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

public final class PNGDecoder extends Object
		implements InputAdapter, DataHolder, DataDecoder, ImageDecoder
{
	public static final long PNG_SIGNATURE = 0x89504e470d0a1a0aL;
	private static final int HEADER_SIGNATURE = 0x49484452; /* IHDR */
	private static final int GAMMA_SIGNATURE = 0x67414d41; /* gAMA */
	private static final int PALETTE_SIGNATURE = 0x504c5445; /* PLTE */
	private static final int TRANSPARENCY_SIGNATURE = 0x74524e53; /* tRNS */
	private static final int DATA_SIGNATURE = 0x49444154; /* IDAT */
	private static final int END_SIGNATURE = 0x49454e44; /* IEND */

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

	private static double power(double base, double exponent)
	{
		return Double.isNaN(base) || Double.isNaN(exponent) ? Double.NaN :
				(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 final class DecodeNormal extends Object
			implements DecodeMethod
	{
		DecodeNormal()
		{
		}

		public int decode(byte[] stream, int startIndex, int pixelType,
				byte[] imageData, int imageWidth, int imageHeight)
		{
			return (PNGDecoder.this).decodeNormal(stream, startIndex, pixelType,
					imageData, imageWidth, imageHeight);
		}
	}

	private final class DecodeAdam7 extends Object
			implements DecodeMethod
	{
		DecodeAdam7()
		{
		}

		public int decode(byte[] stream, int startIndex, int pixelType,
				byte[] imageData, int imageWidth, int imageHeight)
		{
			return (PNGDecoder.this).decodeAdam7(stream, startIndex, pixelType,
					imageData, imageWidth, imageHeight);
		}
	}

	private boolean useGrayscale;
	private boolean usePalette;
	private boolean useAlpha;
	private int width;
	private int height;
	private int bitDepth;
	private int components;
	private byte[] data;
	private int[] gamma;
	private int[] palette;

	public PNGDecoder()
	{
		clear();
	}

	public void loadFromInputStream(InputStream stream)
			throws IOException, InvalidDataFormatException
	{
		loadFromDataStream(new DataInputStream(stream));
	}

	public void loadFromDataStream(DataInputStream stream)
			throws IOException, InvalidDataFormatException
	{
		boolean headerHandled = false;
		boolean gammaHandled = false;
		boolean paletteHandled = false;
		boolean transparencyHandled = false;
		int chunkLength;
		int chunkStart;
		int pixelType = -1;
		int imgw = 0;
		int imgh = 0;
		int cmpn = 0;
		DecodeMethod d = null;
		ByteArrayOutputStream compressedData = null;
		clear();
		do
		{
			chunkLength = stream.readInt();
			switch(chunkStart = stream.readInt())
			{
			default:
				{
					stream.skip(((long) chunkLength) & 0xffffffffL);
				}
				break;
			case HEADER_SIGNATURE:
				{
					int w;
					int h;
					int bd;
					int pt;
					int cm;
					int fm;
					int im;
					long area;
					if(headerHandled || chunkLength != 0x0d)
					{
						invalidChunkOrder();
					}
					w = stream.readInt(); /* ширина */
					h = stream.readInt(); /* высота */
					bd = stream.readByte(); /* глубина цвета */
					pt = stream.readByte(); /* тип пикселов */
					cm = stream.readByte(); /* алгоритм сжатия */
					fm = stream.readByte(); /* алгоритм фильтрации */
					im = stream.readByte(); /* алгоритм отображения */
					if((area = (((long) w) & 0xffffffffL) * (((long) h) & 0xffffffffL)) <= 0L ||
							area > 1000000L || cm != 0 || fm != 0 || (im != 0 && im != 1) || (
							(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))))
					{
						invalidChunkOrder();
					}
					pixelType = pt;
					useGrayscale = pt == 0 || pt == 4;
					usePalette = pt == 3;
					useAlpha = pt == 4 || pt == 6;
					width = imgw = w;
					height = imgh = h;
					bitDepth = bd;
					switch(pt)
					{
					default:
						break;
					case 0:
					case 3:
						components = cmpn = 1;
						break;
					case 4:
						components = cmpn = 2;
						break;
					case 2:
						components = cmpn = 3;
						break;
					case 6:
						components = cmpn = 4;
						break;
					}
					switch(im)
					{
					default:
						break;
					case 0:
						d = this.new DecodeNormal();
						break;
					case 1:
						d = this.new DecodeAdam7();
						break;
					}
					headerHandled = true;
				}
				break;
			case GAMMA_SIGNATURE:
				{
					double gammaValue;
					double exponent;
					int i;
					int[] gm;
					if((!headerHandled) || gammaHandled || chunkLength != 0x04)
					{
						invalidChunkOrder();
					}
					gm = gamma;
					gammaValue = (double) stream.readInt();
					exponent = 1.e+5d / (2.2d * gammaValue);
					for(i = 0x00; i <= 0xff; i++)
					{
						gm[i] = (int) Math.round(255.d * power(((double) i) / 255.d, exponent));
					}
					gammaHandled = true;
				}
				break;
			case PALETTE_SIGNATURE:
				{
					int i;
					int r;
					int g;
					int b;
					int lim;
					int[] p;
					if((!headerHandled) || paletteHandled ||
							chunkLength < 0x0000 || chunkLength > 0x0300 ||
							(chunkLength % 3) != 0)
					{
						invalidChunkOrder();
					}
					lim = chunkLength / 3;
					p = palette;
					for(i = 0; i < lim; i++)
					{
						r = stream.readUnsignedByte();
						g = stream.readUnsignedByte();
						b = stream.readUnsignedByte();
						p[i] = 0xff000000 | (r << 0x10) | (g << 0x08) | b;
					}
					paletteHandled = true;
				}
				break;
			case TRANSPARENCY_SIGNATURE:
				{
					int i;
					int[] p;
					if((!headerHandled) || transparencyHandled ||
							chunkLength < 0x0000 || chunkLength > 0x0100)
					{
						invalidChunkOrder();
					}
					if(usePalette)
					{
						p = palette;
						for(i = 0; i < chunkLength; i++)
						{
							p[i] = (p[i] & 0x00ffffff) | (stream.readUnsignedByte() << 0x18);
						}
					} else
					{
						/* прозрачность по цвету не поддерживается */
						stream.skipBytes(chunkLength);
					}
					transparencyHandled = true;
				}
				break;
			case DATA_SIGNATURE:
				{
					if((!headerHandled) || chunkLength < 0)
					{
						invalidChunkOrder();
					}
					copyBytes(stream, compressedData == null ?
							(compressedData = new ByteArrayOutputStream()) : compressedData,
							(long) chunkLength);
				}
				break;
			case END_SIGNATURE:
				{
					if((!headerHandled) || chunkLength != 0 || compressedData == null)
					{
						invalidChunkOrder();
					}
					d.decode(Zlib.decompress(compressedData.toByteArray()), 0, pixelType,
							data = new byte[imgw * imgh * cmpn], imgw, imgh);
				}
				break;
			}
			stream.skipBytes(0x04); /* игнорируем контрольную сумму */
		} while(chunkStart != END_SIGNATURE);
	}

	public void clear()
	{
		int i;
		int[] g;
		int[] p;
		useGrayscale = false;
		usePalette = false;
		useAlpha = false;
		width = 0;
		height = 0;
		components = 0;
		bitDepth = 0;
		data = null;
		if((g = gamma) == null)
		{
			g = gamma = new int[256];
		}
		if((p = palette) == null)
		{
			p = palette = new int[256];
		}
		for(i = 0x00; i <= 0xff; i++)
		{
			g[i] = i;
			p[i] = 0;
		}
	}

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

	public boolean isAlphaSupported()
	{
		return true;
	}

	public int getWidth()
	{
		return width;
	}

	public int getHeight()
	{
		return height;
	}

	public int[] getPixels()
	{
		int i;
		int size;
		int step;
		int offset;
		int[] result;
		if(data == null)
		{
			return null;
		}
		offset = 0;
		step = components;
		result = new int[size = width * height];
		for(i = 0; i < size; offset += step, i++)
		{
			result[i] = readPixel(offset);
		}
		return result;
	}

	int decodeNormal(byte[] stream, int startIndex, int pixelType,
			byte[] imageData, int imageWidth, int imageHeight)
	{
		int result = startIndex;
		int f = getFilterOffset(pixelType);
		int i;
		int j;
		int k;
		int lim;
		int ofs = 0;
		int total = imageWidth * components;
		int lineSize = getLineSize(imageWidth, pixelType);
		UnwrapMethod u = null;
		k = lineSize + 1;
		if(usePalette)
		{
			switch(bitDepth)
			{
			default:
				break;
			case 1:
				u = new Unwrap1bitPP();
				break;
			case 2:
				u = new Unwrap2bitPP();
				break;
			case 4:
				u = new Unwrap4bitPP();
				break;
			case 8:
				u = new Unwrap8bitPC();
				break;
			}
		} else
		{
			switch(bitDepth)
			{
			default:
				break;
			case 1:
				u = new Unwrap1bitPC();
				break;
			case 2:
				u = new Unwrap2bitPC();
				break;
			case 4:
				u = new Unwrap4bitPC();
				break;
			case 8:
				u = new Unwrap8bitPC();
				break;
			case 16:
				u = new Unwrap16bitPC();
				break;
			}
		}
		for(i = 0; i < imageHeight; i++)
		{
			switch(stream[result++])
			{
			default:
				break;
			case 1:
				lim = result + lineSize;
				for(j = result + f; j < lim; j++)
				{
					stream[j] = (byte) (stream[j] + stream[j - f]);
				}
				break;
			case 2:
				if(i > 0)
				{
					lim = result + lineSize;
					for(j = result; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + stream[j - k]);
					}
				}
				break;
			case 3:
				if(i > 0)
				{
					lim = result + f;
					for(j = result; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + (
								(stream[j - k] & 0xff) >> 1));
					}
					lim = result + lineSize;
					for(; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + ((
								(stream[j - f] & 0xff) +
								(stream[j - k] & 0xff)) >> 1));
					}
				} else
				{
					lim = result + lineSize;
					for(j = result + f; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + (
								(stream[j - f] & 0xff) >> 1));
					}
				}
				break;
			case 4:
				if(i > 0)
				{
					lim = result + f;
					for(j = result; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + paethPredictor(
								0,
								stream[j - k] & 0xff,
								0));
					}
					lim = result + lineSize;
					for(; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + paethPredictor(
								stream[j - f] & 0xff,
								stream[j - k] & 0xff,
								stream[j - f - k] & 0xff));
					}
				} else
				{
					lim = result + f;
					for(j = result; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + paethPredictor(
								0,
								0,
								0));
					}
					lim = result + lineSize;
					for(; j < lim; j++)
					{
						stream[j] = (byte) (stream[j] + paethPredictor(
								stream[j - f] & 0xff,
								0,
								0));
					}
				}
				break;
			}
			u.unwrap(stream, result, imageData, ofs, total);
			ofs += total;
			result += lineSize;
		}
		return result;
	}

	int decodeAdam7(byte[] stream, int startIndex, int pixelType,
			byte[] imageData, int imageWidth, int imageHeight)
	{
		int result = startIndex;
		int currentPassage;
		int currentInterlacing;
		int width;
		int height;
		int x0;
		int y0;
		int x;
		int y;
		int c = components;
		int xstart;
		int ystart;
		int xinc;
		int yinc;
		int lim;
		byte[] data;
		int[] adam7interlacing;
		lim = (adam7interlacing = new int[] {
				0x0088, 0x4088, 0x0448, 0x2044, 0x0224, 0x1022, 0x0112
		}).length;
		for(currentPassage = 0; currentPassage < lim; currentPassage++)
		{
			xstart = ((currentInterlacing = adam7interlacing[currentPassage]) >> 12) & 0x0f;
			ystart = (currentInterlacing >> 8) & 0x0f;
			xinc = (currentInterlacing >> 4) & 0x0f;
			yinc = currentInterlacing & 0x0f;
			width = (imageWidth - xstart + xinc - 1) / xinc;
			height = (imageHeight - ystart + yinc - 1) / yinc;
			data = new byte[width * height * c];
			result = decodeNormal(stream, result, pixelType, data, width, height);
			for(y0 = 0, y = ystart; y < imageHeight; y0++, y += yinc)
			{
				for(x0 = 0, x = xstart; x < imageWidth; x0++, x += xinc)
				{
					Array.copy(data, (y0 * width + x0) * c,
							imageData, (y * imageWidth + x) * c, c);
				}
			}
		}
		return result;
	}

	private void invalidChunkOrder()
			throws InvalidDataFormatException
	{
		clear();
		throw new InvalidDataFormatException("Декодирование PNG-файлов: " +
				"неправильный формат файла.");
	}

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

	private int getFilterOffset(int pixelType)
	{
		return pixelType == 0 || pixelType == 3 ?
				(bitDepth < 16 ? 1 : 2) : (bitDepth * components) >> 3;
	}

	private int readPixel(int offset)
	{
		int a;
		int r;
		int g;
		int b;
		int p;
		byte[] d = data;
		int[] gm = gamma;
		if(usePalette)
		{
			a = (p = palette[d[offset] & 0xff]) >> 0x18;
			r = (p >> 0x10) & 0xff;
			g = (p >> 0x08) & 0xff;
			b = p & 0xff;
		} else
		{
			if(useAlpha)
			{
				if(useGrayscale)
				{
					a = d[offset + 1];
					r = g = b = d[offset] & 0xff;
				} else
				{
					a = d[offset + 3];
					r = d[offset] & 0xff;
					g = d[offset + 1] & 0xff;
					b = d[offset + 2] & 0xff;
				}
			} else
			{
				a = 0xff;
				if(useGrayscale)
				{
					r = g = b = d[offset] & 0xff;
				} else
				{
					r = d[offset] & 0xff;
					g = d[offset + 1] & 0xff;
					b = d[offset + 2] & 0xff;
				}
			}
		}
		return (a << 0x18) | (gm[r] << 0x10) | (gm[g] << 0x08) | gm[b];
	}
}
