/*
 * PSDCodec
 *
 * Copyright (c) 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 Marco Schmidt.
 * All rights reserved.
 */

package net.sourceforge.jiu.codecs;

import java.io.DataInput;
import java.io.IOException;
import net.sourceforge.jiu.codecs.ImageCodec;
import net.sourceforge.jiu.codecs.InvalidFileStructureException;
import net.sourceforge.jiu.codecs.UnsupportedTypeException;
import net.sourceforge.jiu.codecs.WrongFileFormatException;
import net.sourceforge.jiu.data.MemoryGray8Image;
import net.sourceforge.jiu.data.MemoryPaletted8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.Gray8Image;
import net.sourceforge.jiu.data.Palette;
import net.sourceforge.jiu.data.Paletted8Image;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.ops.OperationFailedException;

/**
 * A codec to read images from Photoshop PSD files.
 * PSD was created by Adobe for their
 * <a href="http://www.adobe.com/store/products/photoshop.html">Photoshop</a>
 * image editing software.
 * Note that only a small subset of valid PSD files is supported by this codec.
 * Typical file extension is <code>.psd</code>.
 * @author Marco Schmidt
 */
public class PSDCodec extends ImageCodec
{
	private final static int MAGIC_8BPS = 0x38425053;
	private final static int COLOR_MODE_GRAYSCALE = 1;
	private final static int COLOR_MODE_INDEXED = 2;
	private final static int COLOR_MODE_RGB_TRUECOLOR = 3;
	private final static short COMPRESSION_NONE = 0;
	private final static short COMPRESSION_PACKBITS = 1;
	private int magic;
	private int channels;
	private int height;
	private int width;
	private int depth;
	private int colorMode;
	private short compression;
	private DataInput in;
	private Gray8Image gray8Image;
	private Palette palette;
	private Paletted8Image paletted8Image;
	private RGB24Image rgb24Image;

	private void allocate()
	{
		gray8Image = null;
		paletted8Image = null;
		rgb24Image = null;
		if (depth == 8 && colorMode == COLOR_MODE_RGB_TRUECOLOR)
		{
			rgb24Image = new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight());
			setImage(rgb24Image);
		}
		else
		if (channels == 1 && depth == 8 && colorMode == 2)
		{
			paletted8Image = new MemoryPaletted8Image(width, height, palette);
			setImage(paletted8Image);
		}
		else
		if (channels == 1 && depth == 8 && colorMode == COLOR_MODE_GRAYSCALE)
		{
			gray8Image = new MemoryGray8Image(width, height);
			setImage(gray8Image);
		}
		else
		{
			throw new IllegalArgumentException("Unknown image type in PSD file.");
		}
	}

	private static String getColorTypeName(int colorMode)
	{
		switch(colorMode)
		{
			case(0): return "Black & white";
			case(1): return "Grayscale";
			case(2): return "Indexed";
			case(3): return "RGB truecolor";
			case(4): return "CMYK truecolor";
			case(7): return "Multichannel";
			case(8): return "Duotone";
			case(9): return "Lab";
			default: return "Unknown (" + colorMode + ")";
		}
	}

	public String getFormatName()
	{
		return "Photoshop (PSD)";
	}

	public String[] getMimeTypes()
	{
		return new String[] {"image/psd", "image/x-psd"};
	}

	public boolean isLoadingSupported()
	{
		return true;
	}

	public boolean isSavingSupported()
	{
		return false;
	}

	/**
	 * Attempts to load an Image from argument stream <code>in</code> (which
	 * could, as an example, be a <code>RandomAccessFile</code> instance, it 
	 * implements the <code>DataInput</code> interface).
	 * Checks a magic byte sequence and then reads all chunks as they appear
	 * in the IFF file.
	 * Will return the resulting image or null if no image body chunk was
	 * encountered before end-of-stream.
	 * Will throw an exception if the file is corrupt, information is missing
	 * or there were reading errors.
	 */
	private void load() throws
		InvalidFileStructureException,
		IOException, 
		UnsupportedTypeException,
		WrongFileFormatException
	{
		loadHeader();
		//System.out.println(width + " x " + height + ", color=" + colorMode + ", channels=" + channels + ", depth=" + depth);
		// check values
		if (width < 1 || height < 1)
		{
			throw new InvalidFileStructureException("Cannot load image. " +
				"Invalid pixel resolution in PSD file header (" + width +
				" x " + height + ").");
		}
		if (colorMode != COLOR_MODE_RGB_TRUECOLOR &&
		    colorMode != COLOR_MODE_GRAYSCALE &&
		    colorMode != COLOR_MODE_INDEXED)
		{
			throw new UnsupportedTypeException("Cannot load image. Only RGB" +
				" truecolor and indexed color are supported for PSD files. " +
				"Found: " +getColorTypeName(colorMode));
		}
		if (depth != 8)
		{
			throw new UnsupportedTypeException("Cannot load image. Only a depth of 8 bits " +
				"per channel is supported (found " + depth + 
				" bits).");
		}

		// COLOR MODE DATA
		int colorModeSize = in.readInt();
		//System.out.println("colorModeSize=" + colorModeSize);
		byte[] colorMap = null;
		if (colorMode == COLOR_MODE_INDEXED)
		{
			if (colorModeSize != 768)
			{
				throw new InvalidFileStructureException("Cannot load image." +
					" Color map length was expected to be 768 (found " + 
					colorModeSize + ").");
			}
			colorMap = new byte[colorModeSize];
			in.readFully(colorMap);
			palette = new Palette(256, 255);
			for (int index = 0; index < 256; index++)
			{
				palette.putSample(Palette.INDEX_RED, index, colorMap[index] & 0xff);
				palette.putSample(Palette.INDEX_GREEN, index, colorMap[256 + index] & 0xff);
				palette.putSample(Palette.INDEX_BLUE, index, colorMap[512 + index] & 0xff);
			}
		}
		else
		{
			in.skipBytes(colorModeSize);
		}
		// IMAGE RESOURCES
		int resourceLength = in.readInt();
		in.skipBytes(resourceLength);
		//System.out.println("resourceLength=" + resourceLength);
		// LAYER AND MASK INFORMATION
		int miscLength = in.readInt();
		in.skipBytes(miscLength);
		//System.out.println("miscLength=" + miscLength);
		// IMAGE DATA
		compression = in.readShort();
		if (compression != COMPRESSION_NONE && compression != COMPRESSION_PACKBITS)
		{
			throw new UnsupportedTypeException("Cannot load image. Unsupported PSD " +
				"compression type (" + compression + ")");
		}
		//System.out.println("compression=" + compression);
		loadImageData();
	}

	/**
	 * Reads the PSD header to private members of this class instance.
	 * @throws IOException if there were reading errors
	 */
	private void loadHeader() throws
		IOException,
		WrongFileFormatException
	{
		magic = in.readInt();
		if (magic != MAGIC_8BPS)
		{
			throw new WrongFileFormatException("Not a valid PSD file " +
				"(wrong magic byte sequence).");
		}
		in.readShort(); // skip version short value
		in.skipBytes(6);
		channels = in.readShort();
		height = in.readInt();
		width = in.readInt();
		depth = in.readShort();
		colorMode = in.readShort();
	}

	private void loadPackbitsCompressedData(byte[] data, int offset, int num) throws
		InvalidFileStructureException,
		IOException
	{
		int x = offset;
		int max = offset + num;
		while (x < max)
		{
			byte n = in.readByte();
			boolean compressed = false;
			int count = -1;
			try
			{
				if (n >= 0)
				{
					// copy next n + 1 bytes literally
					in.readFully(data, x, n + 1);
					x += (n + 1);
				}
				else
				{
					// if n == -128, nothing happens (stupid design decision)
					if (n != -128)
					{
						compressed = true;
						// otherwise, compute counter
						count = -((int)n) + 1;
						// read another byte
						byte value = in.readByte();
						// write this byte counter times to output
						while (count-- > 0)
						{
							data[x++] = value;
						}
					}
				}
			}
			catch (ArrayIndexOutOfBoundsException ioobe)
			{
				/* if the encoder did anything wrong, the above code
				   could potentially write beyond array boundaries
				   (e.g. if runs of data exceed line boundaries);
				   this would result in an IndexOutOfBoundsException
				   thrown by the virtual machine;
				   to give a more understandable error message to the 
				   user, this exception is caught here and a
				   corresponding IOException is thrown */
				throw new InvalidFileStructureException("Error: RLE-compressed image " +
					"file seems to be corrupt (x=" + x +
					", count=" + (compressed ? (-((int)n) + 1) : n) + 
					", compressed=" + (compressed ? "y" : "n") + ", array length=" + data.length + ").");
			}
		}
	}

	private void loadImageData() throws 
		InvalidFileStructureException, 
		IOException
	{
		setBoundsIfNecessary(width, height);
		allocate();
		if (compression == COMPRESSION_PACKBITS)
		{
			// skip counters
			in.skipBytes(2 * channels * height);
		}
		byte[] data = new byte[width];
		int totalScanLines = channels * height;
		int currentScanLine = 0;
		for (int c = 0; c < channels; c++)
		{
			for (int y = 0, destY = - getBoundsY1(); y < height; y++, destY++)
			{
				if (compression == COMPRESSION_PACKBITS)
				{
					loadPackbitsCompressedData(data, 0, width);
				}
				else
				{
					if (compression == COMPRESSION_PACKBITS)
					{
						in.readFully(data, 0, width);
					}
				}
				setProgress(currentScanLine++, totalScanLines);
				if (!isRowRequired(y))
				{
					continue;
				}
				if (rgb24Image != null)
				{
					int channelIndex = RGB24Image.INDEX_RED;
					if (c == 1)
					{
						channelIndex = RGB24Image.INDEX_GREEN;
					}
					if (c == 2)
					{
						channelIndex = RGB24Image.INDEX_BLUE;
					}
					rgb24Image.putByteSamples(channelIndex, 0, destY, getBoundsWidth(), 1, data, getBoundsX1());
				}
				if (gray8Image != null)
				{
					gray8Image.putByteSamples(0, 0, destY, getBoundsWidth(), 1, data, getBoundsX1());
				}
				if (paletted8Image != null)
				{
					paletted8Image.putByteSamples(0, 0, destY, getBoundsWidth(), 1, data, getBoundsX1());
				}
			}
		}
	}

	public void process() throws
		OperationFailedException
	{
		initModeFromIOObjects();
		try
		{
			if (getMode() == CodecMode.LOAD)
			{
				in = getInputAsDataInput();
				if (in == null)
				{
					throw new MissingParameterException("Input stream / file missing.");
				}
				load();
			}
			else
			{
				throw new OperationFailedException("Only loading is supported in PSD codec.");
			}
		}
		catch (IOException ioe)
		{
			throw new OperationFailedException("I/O error: " + ioe.toString());
		}
	}
}
