package com.one;

import com.classpath.zip.CRC32;
import com.classpath.zip.Deflater;
import com.classpath.zip.DeflaterOutputStream;
import com.vmx.ProgressCallback;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.microedition.lcdui.Image;

public class PNGFile
{
	private static final long SIGNATURE = 0x89504E470D0A1A0AL;

	private static final int CHUNK_IHDR = 0x49484452;
	private static final int CHUNK_IDAT = 0x49444154;
	private static final int CHUNK_IEND = 0x49454E44;
	private static final int CHUNK_PLTE = 0x504C5445;
	private static final int CHUNK_TRNS = 0x74524E53;

	private static final int MAX_CHUNK_SIZE = 0x10000;

	public static final int GREYSCALE = 0;
	public static final int TRUECOLOR = 2;
	public static final int INDEXED = 3;

	public static final int GREYSCALE_ALPHA = 4;
	public static final int TRUECOLOR_ALPHA = 6;
	public static final int INDEXED_ALPHA = 7;

	private static final int BIT_DEPTH = 8;

	private static final int COMPRESSION_METHOD = 0;
	private static final int FILTER_METHOD = 0;
	private static final int INTERLACE_METHOD = 0;

	public static final int FILTER_AUTO = -1;
	public static final int FILTER_NONE = 0;
	public static final int FILTER_SUB = 1;
	public static final int FILTER_UP = 2;
	public static final int FILTER_AVERAGE = 3;
	public static final int FILTER_PAETH = 4;
	
	private static CRC32 crc = new CRC32();
	
	public static void writePNG(Image image, DataOutputStream out, int colortype, int fixedfilter, ProgressCallback callback) throws IOException
	{
		if(colortype != GREYSCALE &&
		   colortype != TRUECOLOR &&
		   colortype != INDEXED &&
		   colortype != GREYSCALE_ALPHA &&
		   colortype != TRUECOLOR_ALPHA &&
		   colortype != INDEXED_ALPHA)
		{
			throw new IllegalArgumentException(Integer.toString(colortype));
		}

		boolean trns = false;

		if(colortype == INDEXED_ALPHA)
		{
			colortype = INDEXED;
			trns = true;
		}

		if(colortype == INDEXED)
		{
			fixedfilter = FILTER_NONE;
		}
		else if(fixedfilter > FILTER_PAETH)
		{
			fixedfilter = FILTER_AUTO;
		}

		int width = image.getWidth();
		int height = image.getHeight();
		
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		DataOutputStream dos = new DataOutputStream(baos);
		
		out.writeLong(SIGNATURE);
		
		dos.writeInt(CHUNK_IHDR);
		dos.writeInt(width);
		dos.writeInt(height);
		dos.writeByte(BIT_DEPTH);
		dos.writeByte(colortype);
		dos.writeByte(COMPRESSION_METHOD);
		dos.writeByte(FILTER_METHOD);
		dos.writeByte(INTERLACE_METHOD);
		
		writeChunk(out, baos.toByteArray());
		baos.reset();
		
		ColorQuantizer quantizer = null;

		if(colortype == INDEXED)
		{
			quantizer = new ColorQuantizer(image, 256, ColorQuantizer.MAX_NODES_ALPHA * 2, true, trns, callback);
			int[] palette = quantizer.getPalette();

			dos.writeInt(CHUNK_PLTE);

			for(int i = 0; i < palette.length; i++)
			{
				dos.write(palette[i] >> 16);
				dos.write(palette[i] >> 8);
				dos.write(palette[i]);
			}

			writeChunk(out, baos.toByteArray());
			baos.reset();

			if(trns)
			{
				dos.writeInt(CHUNK_TRNS);

				for(int i = 0; i < palette.length; i++)
				{
					dos.write(palette[i] >> 24);
				}

				writeChunk(out, baos.toByteArray());
				baos.reset();
			}
		}

		if(callback != null)
		{
			callback.setProgress(0);
			callback.setMax(height);
		}
		
		dos.writeInt(CHUNK_IDAT);
		DeflaterOutputStream def = new DeflaterOutputStream(baos, new Deflater(Deflater.BEST_COMPRESSION, false));

		int bytesperpixel;

		switch(colortype)
		{
			case GREYSCALE:
			case INDEXED:
				bytesperpixel = 1;
				break;

			case GREYSCALE_ALPHA:
				bytesperpixel = 2;
				break;

			case TRUECOLOR:
				bytesperpixel = 3;
				break;

			default:
			case TRUECOLOR_ALPHA:
				bytesperpixel = 4;
				break;
		}
		
		int scanlength = width * bytesperpixel + 1;
		int index, filtertype;

		int[] curline = new int[scanlength];
		int[] prevline = new int[scanlength];
		int[] pixel = new int[width];

		byte[][] scanline = new byte[5][scanlength];
		int[] scanlinesum = new int[scanline.length];
		int[] filterusage = new int[scanline.length];
		
		for(int y = 0; y < height; y++)
		{
			image.getRGB(pixel, 0, width, 0, y, width, 1);
			index = 0;

			if(colortype == GREYSCALE)
			{
				for(int x = 0; x < width; x++)
				{
					curline[++index] = (((pixel[x] >> 16) & 0xFF) + ((pixel[x] >> 8) & 0xFF) + (pixel[x] & 0xFF)) / 3;
				}
			}
			else if(colortype == TRUECOLOR)
			{
				for(int x = 0; x < width; x++)
				{
					curline[++index] = (pixel[x] >> 16) & 0xFF;
					curline[++index] = (pixel[x] >> 8) & 0xFF;
					curline[++index] = pixel[x] & 0xFF;
				}
			}
			else if(colortype == INDEXED)
			{
				for(int x = 0; x < width; x++)
				{
					curline[++index] = quantizer.getColorIndex(pixel[x]);
				}
			}
			else if(colortype == GREYSCALE_ALPHA)
			{
				for(int x = 0; x < width; x++)
				{
					curline[++index] = (((pixel[x] >> 16) & 0xFF) + ((pixel[x] >> 8) & 0xFF) + (pixel[x] & 0xFF)) / 3;
					curline[++index] = (pixel[x] >> 24) & 0xFF;
				}
			}
			else if(colortype == TRUECOLOR_ALPHA)
			{
				for(int x = 0; x < width; x++)
				{
					curline[++index] = (pixel[x] >> 16) & 0xFF;
					curline[++index] = (pixel[x] >> 8) & 0xFF;
					curline[++index] = pixel[x] & 0xFF;
					curline[++index] = (pixel[x] >> 24) & 0xFF;
				}
			}

			for(filtertype = (fixedfilter >= 0 ? fixedfilter : 0); filtertype < scanline.length; filtertype++)
			{
				scanline[filtertype][0] = (byte)filtertype;
				scanlinesum[filtertype] = 0;

				for(index = 1; index <= bytesperpixel; index++)
				{
					scanline[filtertype][index] = (byte)(curline[index] - filter(filtertype, 0, prevline[index], 0));
					scanlinesum[filtertype] += Math.abs(scanline[filtertype][index]);
				}

				for(index = bytesperpixel + 1; index < scanlength; index++)
				{
					scanline[filtertype][index] = (byte)(curline[index] - filter(filtertype, curline[index - bytesperpixel], prevline[index], prevline[index - bytesperpixel]));
					scanlinesum[filtertype] += Math.abs(scanline[filtertype][index]);
				}

				if(fixedfilter >= 0)
				{
					break;
				}
			}

			System.arraycopy(curline, 0, prevline, 0, scanlength);

			if(fixedfilter >= 0)
			{
				filtertype = fixedfilter;
			}
			else
			{
				filtertype = 0;

				for(index = 1; index < scanline.length; index++)
				{
					if(scanlinesum[index] < scanlinesum[filtertype])
					{
						filtertype = index;
					}
				}
			}

			def.write(scanline[filtertype]);
			filterusage[filtertype]++;

			if(baos.size() >= MAX_CHUNK_SIZE)
			{
				writeChunk(out, baos.toByteArray());
				baos.reset();

				dos.writeInt(CHUNK_IDAT);
			}
			
			if(callback != null)
			{
				callback.progress(1);
			}
		}
		
//		int max = 0;
//
//		for(index = 0; index < filterusage.length; index++)
//		{
//			if(filterusage[index] > max)
//			{
//				max = filterusage[index];
//			}
//		}
//
//		System.out.println();
//
//		for(index = 0; index < filterusage.length; index++)
//		{
//			System.out.print(Integer.toString(index) + ": ");
//
//			for(int i = 0, count = 77 * filterusage[index] / max; i < count; i++)
//			{
//				System.out.print('-');
//			}
//
//			System.out.println();
//		}
		
		def.finish();
		
		writeChunk(out, baos.toByteArray());
		baos.reset();
		
		dos.writeInt(CHUNK_IEND);
		
		writeChunk(out, baos.toByteArray());
		baos.close();
	}
	
	private static int filter(int type, int a, int b, int c)
	{
		if(type == FILTER_SUB)
		{
			return a;
		}
		else if(type == FILTER_UP)
		{
			return b;
		}
		if(type == FILTER_AVERAGE)
		{
			return (a + b) / 2;
		}
		else if(type == FILTER_PAETH)
		{
			int p = a + b - c;

			int pa = Math.abs(p - a);
			int pb = Math.abs(p - b);
			int pc = Math.abs(p - c);

			if(pa <= pb && pa <= pc)
			{
				 return a;
			}
			else if(pb <= pc)
			{
				 return b;
			}
			else
			{
				return c;
			}
		}
		else
		{
			return 0;
		}
	}
	
	private static void writeChunk(DataOutputStream dos, byte[] data) throws IOException
	{
		dos.writeInt(data.length - 4);
		
		dos.write(data);
		
		crc.reset();
		crc.update(data);
		
		dos.writeInt((int)crc.getValue());
	}
}