/*
 * TIFFDecoder
 * 
 * Copyright (c) 2002, 2003 Marco Schmidt.
 * All rights reserved.
 */

package net.sourceforge.jiu.codecs.tiff;

import java.io.DataInput;
import java.io.IOException;
import java.io.RandomAccessFile;
import net.sourceforge.jiu.codecs.InvalidFileStructureException;
import net.sourceforge.jiu.color.conversion.CMYKConversion;
import net.sourceforge.jiu.color.conversion.LogLuvConversion;
import net.sourceforge.jiu.data.BilevelImage;
import net.sourceforge.jiu.data.ByteChannelImage;
import net.sourceforge.jiu.data.RGBIndex;
import net.sourceforge.jiu.data.ShortChannelImage;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.util.ArrayConverter;

/**
 * The abstract base class for a TIFF decoder, a class that decompresses one tile or
 * strip of image data and understands one or more compression types.
 * Each child class implements the decoding of a particular TIFF compression type
 * in its {@link #decode} method.
 * <p>
 * This class does all the work of storing decompressed data (given as a byte array)
 * in the image object.
 * Given the many variants (sample order, color depth, color space etc.) this is 
 * a larger portion of code. 
 * @author Marco Schmidt
 * @since 0.7.0
 */
public abstract class TIFFDecoder
{
	private TIFFCodec codec;
	private TIFFImageFileDirectory ifd;
	private int currentRow;
	private int leftColumn;
	private int rightColumn;
	private int topRow;
	private int bottomRow;
	private byte[] rowBuffer;
	private int bufferIndex;
	private int tileIndex;
	private int processedTileRows;
	private int totalTileRows;

	public TIFFDecoder()
	{
		tileIndex = -1;
	}

	/**
	 * Decode data from input and write the decompressed pixel data to
	 * the image associated with this decoder.
	 * Child classes must override this method to implement the decoding
	 * for a particular compression type.
	 */
	public abstract void decode() throws 
		InvalidFileStructureException,
		IOException;

	/**
	 * Returns the number of bytes per row for the strip or tile
	 * that this decoder deals with.
	 * So with a tiled TIFF and an image width of 500 and a tile width of 100,
	 * for an eight bit grayscale image this would return 100 (not 500).
	 * @return number of bytes per row
	 */
	public int getBytesPerRow()
	{
		return ifd.getBytesPerRow();
	}

	/**
	 * Returns the codec from which this decoder is used.
	 * @return TIFFCodec object using this decoder
	 */
	public TIFFCodec getCodec()
	{
		return codec;
	}

	/**
	 * Returns an array with Integer values of all compression types supported by
	 * this decoder (see the COMPRESSION_xyz constants in {@link TIFFConstants}.
	 * Normally, this is only one value, but some compression types got assigned more than one constant
	 * (e.g. deflated).
	 * Also, a decoder could be capable of dealing with more than one type of compression
	 * if the compression types are similar enough to justify that.
	 * However, typically a decoder can only deal with one type of compression.
	 * @return array with Integer objects of all TIFF compression constants supported by this decoder
	 */
	public abstract Integer[] getCompressionTypes();

	/**
	 * Returns the IFD for the image this decoder is supposed to uncompress
	 * (partially).
	 * @return IFD object
	 */
	public TIFFImageFileDirectory getImageFileDirectory()
	{
		return ifd;
	}

	/**
	 * Returns the input stream from which this decoder is supposed 
	 * to read data.
	 */
	public DataInput getInput()
	{
		return codec.getRandomAccessFile();
	}

	/**
	 * Returns the zero-based index of the tile or strip this decoder
	 * is supposed to be decompressing.
	 * @return tile index
	 */
	public int getTileIndex()
	{
		return tileIndex;
	}

	/**
	 * Returns the leftmost column of the image strip / tile to be read 
	 * by this decoder.
	 */
	public int getX1()
	{
		return leftColumn;
	}

	/**
	 * Returns the rightmost column of the image strip / tile to be read 
	 * by this decoder.
	 */
	public int getX2()
	{
		return rightColumn;
	}

	/**
	 * Returns the top row of the image strip / tile to be read 
	 * by this decoder.
	 */
	public int getY1()
	{
		return topRow;
	}

	/**
	 * Returns the bottom row of the image strip / tile to be read 
	 * by this decoder.
	 */
	public int getY2()
	{
		return bottomRow;
	}

	/**
	 * Check if all necessary parameters have been given to this decoder
	 * and initialize several internal fields from them.
	 * Required parameters are a TIFFCodec object, a TIFFImageFileDirectory object and
	 * a tile index.
	 */
	public void initialize() throws 
		IOException, 
		MissingParameterException
	{
		if (tileIndex < 0)
		{
			throw new MissingParameterException("Tile index was not initialized.");
		}
		if (codec == null)
		{
			throw new MissingParameterException("No TIFFCodec object was given to this decoder.");
		}
		if (ifd == null)
		{
			throw new MissingParameterException("No TIFFImageFileDirectory object was given to this decoder.");
		}

		RandomAccessFile raf = codec.getRandomAccessFile();
		long offset = ifd.getTileOffset(tileIndex) & 0x00000000ffffffffL;
		raf.seek(offset);

		leftColumn = ifd.getTileX1(tileIndex);
		rightColumn = ifd.getTileX2(tileIndex);
		topRow = ifd.getTileY1(tileIndex);
		bottomRow = ifd.getTileY2(tileIndex);
		currentRow = topRow;
		processedTileRows = tileIndex * ifd.getTileHeight();
		totalTileRows = ifd.getTileHeight() * ifd.getNumTiles();
		rowBuffer = new byte[ifd.getBytesPerRow()];
	}

	/**
	 * Adds a number of bytes to the internal row buffer.
	 * If the row buffer gets full (a complete line is available)
	 * that data will be copied to the image.
	 * Note that more than one line, exactly one line or only part
	 * of a line can be stored in the <code>number</code> bytes
	 * in <code>data</code>.
	 * @param data byte array with image data that has been decoded
	 * @param offset int index into data where the first byte to be stored is situated
	 * @param number int number of bytes to be stored
	 */
	public void putBytes(byte[] data, int offset, int number)
	{
		// assert(bufferIndex < rowBuffer.length);
		while (number > 0)
		{
			int remaining = rowBuffer.length - bufferIndex;
			int numCopy;
			if (number > remaining)
			{
				numCopy = remaining;
			}
			else
			{
				numCopy = number;
			}
			System.arraycopy(data, offset, rowBuffer, bufferIndex, numCopy);
			number -= numCopy;
			offset += numCopy;
			bufferIndex += numCopy;
			if (bufferIndex == getBytesPerRow())
			{
				storeRow(rowBuffer, 0);
				bufferIndex = 0;
			}
		}
	}

	/**
	 * Specify the codec to be used with this decoder.
	 * This is a mandatory parameter - without it, {@link #initialize}
	 * will throw an exception.
	 * @param tiffCodec TIFFCodec object to be used by this decoder
	 * @see #getCodec
	 */
	public void setCodec(TIFFCodec tiffCodec)
	{
		codec = tiffCodec;
	}

	/**
	 * Specify the IFD to be used with this decoder.
	 * This is a mandatory parameter - without it, {@link #initialize}
	 * will throw an exception.
	 * @param tiffIfd object to be used by this decoder
	 * @see #getImageFileDirectory
	 */
	public void setImageFileDirectory(TIFFImageFileDirectory tiffIfd)
	{
		ifd = tiffIfd;
	}

	/**
	 * Specify the zero-based tile index for the tile or strip to be decompressed
	 * by this decoder.
	 * This is a mandatory parameter - without it, {@link #initialize}
	 * will throw an exception.
	 * @param index zero-based tile / strip index
	 * @see #getTileIndex
	 */
	public void setTileIndex(int index)
	{
		if (index < 0)
		{
			throw new IllegalArgumentException("Tile index must be 0 or larger.");
		}
		tileIndex = index;
	}

	private void storeRow(byte[] data, int offset)
	{
		codec.setProgress(processedTileRows++, totalTileRows);
		// get current row number and increase field currentRow by one
		int y = currentRow++;
		// buffer index field is reset to zero so that putBytes will start at the beginning of the buffer next time
		bufferIndex = 0;
		// leave if we don't need that row because of bounds
		if (!codec.isRowRequired(y))
		{
			return;
		}
		// adjust y so that it will be in bounds coordinate space
		y -= codec.getBoundsY1();
		// get leftmost and rightmost pixel index of the current tile
		int x1 = getX1();
		int x2 = getX2();
		// compute number of pixels, adjust for bounds
		int numPixels = x2 - x1 + 1;
		int leftPixels = 0;
		if (getX1() < codec.getBoundsX1())
		{
			leftPixels = codec.getBoundsX1() - getX1();
		}
		int rightPixels = 0;
		if (getX2() > codec.getBoundsX2())
		{
			rightPixels = getX2() - codec.getBoundsX2();
		}
		numPixels -= (rightPixels + leftPixels);
		switch(ifd.getImageType())
		{
			case(TIFFImageFileDirectory.TYPE_BILEVEL_BYTE):
			{
				BilevelImage image = (BilevelImage)codec.getImage();
				int index = offset + leftPixels;
				int x = getX1() - codec.getBoundsX1() + leftPixels;
				while (numPixels-- > 0)
				{
					if (data[index++] == (byte)BilevelImage.BLACK)
					{
						image.putBlack(x++, y);
					}
					else
					{
						image.putWhite(x++, y);
					}
				}
				break;
			}
			case(TIFFImageFileDirectory.TYPE_BILEVEL_PACKED):
			{
				BilevelImage image = (BilevelImage)codec.getImage();
				int x = getX1() - codec.getBoundsX1() + leftPixels;
				image.putPackedBytes(x, y, numPixels, data, offset + (leftPixels / 8), leftPixels % 8);
				break;
			}
			case(TIFFImageFileDirectory.TYPE_GRAY4):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				byte[] dest = new byte[data.length * 2];
				ArrayConverter.decodePacked4Bit(data, 0, dest, 0, data.length);
				for (int i = 0; i < dest.length; i++)
				{
					int value = dest[i] & 15;
					value = (value << 4) | value;
					dest[i] = (byte)value;
				}
				image.putByteSamples(0, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, offset + leftPixels);
				break;
			}
			case(TIFFImageFileDirectory.TYPE_PALETTED4):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				byte[] dest = new byte[data.length * 2];
				ArrayConverter.decodePacked4Bit(data, 0, dest, 0, data.length);
				image.putByteSamples(0, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, offset + leftPixels);
				break;
			}
			case(TIFFImageFileDirectory.TYPE_GRAY8):
			case(TIFFImageFileDirectory.TYPE_PALETTED8):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				image.putByteSamples(0, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, data, offset + leftPixels);
				break;
			}
			case(TIFFImageFileDirectory.TYPE_CMYK32_INTERLEAVED):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				byte[] dest = new byte[data.length];
				int numSamples = ifd.getTileWidth();
				CMYKConversion.convertCMYK32InterleavedToRGB24Planar(
					data, 0,
					dest, 0,
					dest, numSamples,
					dest, numSamples * 2,
					numSamples);
				image.putByteSamples(RGBIndex.INDEX_RED, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, leftPixels);
				image.putByteSamples(RGBIndex.INDEX_GREEN, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, numSamples + leftPixels);
				image.putByteSamples(RGBIndex.INDEX_BLUE, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, 2 * numSamples + leftPixels);
				break;
			}
/*			case(TIFFImageFileDirectory.TYPE_CMYK32_PLANAR):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				byte[] dest = new byte[data.length];
				int numSamples = ifd.getTileWidth();
				CMYKConversion.convertCMYK32PlanarToRGB24Planar(
					data, 0,
					data, numPixels,
					data, numPixels * 2,
					data, numPixels * 3,
					dest, 0,
					dest, numSamples,
					dest, numSamples * 2,
					numSamples);
				image.putByteSamples(RGBIndex.INDEX_RED, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, leftPixels);
				image.putByteSamples(RGBIndex.INDEX_GREEN, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, numSamples + leftPixels);
				image.putByteSamples(RGBIndex.INDEX_BLUE, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, dest, 2 * numSamples + leftPixels);
				break;
			}*/
			case(TIFFImageFileDirectory.TYPE_RGB24_INTERLEAVED):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				offset += leftPixels * 3;
				for (int i = 0, x = getX1() - codec.getBoundsX1() + leftPixels; i < numPixels; i++, x++)
				{
					image.putByteSample(RGBIndex.INDEX_RED, x, y, data[offset++]);
					image.putByteSample(RGBIndex.INDEX_GREEN, x, y, data[offset++]);
					image.putByteSample(RGBIndex.INDEX_BLUE, x, y, data[offset++]);
				}
				break;
			}
			case(TIFFImageFileDirectory.TYPE_RGB48_INTERLEAVED):
			{
				ShortChannelImage image = (ShortChannelImage)codec.getImage();
				offset += leftPixels * 3;
				short[] triplet = new short[3];
				boolean littleEndian = codec.getByteOrder() == TIFFCodec.BYTE_ORDER_INTEL;
				for (int i = 0, x = getX1() - codec.getBoundsX1() + leftPixels; i < numPixels; i++, x++)
				{
					for (int j = 0; j < 3; j++, offset += 2)
					{
						if (littleEndian)
						{
							triplet[j] = ArrayConverter.getShortLE(data, offset);
						}
						else
						{
							triplet[j] = ArrayConverter.getShortBE(data, offset);
						}
					}
					image.putShortSample(RGBIndex.INDEX_RED, x, y, triplet[0]);
					image.putShortSample(RGBIndex.INDEX_GREEN, x, y, triplet[1]);
					image.putShortSample(RGBIndex.INDEX_BLUE, x, y, triplet[2]);
				}
				break;
			}
			case(TIFFImageFileDirectory.TYPE_LOGLUV32_INTERLEAVED):
			{
				if (getImageFileDirectory().getCompression() == TIFFConstants.COMPRESSION_SGI_LOG_RLE)
				{
					ByteChannelImage image = (ByteChannelImage)codec.getImage();
					int numSamples = ifd.getTileWidth();
					byte[] red = new byte[numSamples];
					byte[] green = new byte[numSamples];
					byte[] blue = new byte[numSamples];
					LogLuvConversion.convertLogLuv32InterleavedtoRGB24Planar(data, red, green, blue, numSamples);
					image.putByteSamples(RGBIndex.INDEX_RED, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, red, leftPixels);
					image.putByteSamples(RGBIndex.INDEX_GREEN, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, green, leftPixels);
					image.putByteSamples(RGBIndex.INDEX_BLUE, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, blue, leftPixels);
				}
				else
				if (getImageFileDirectory().getCompression() == TIFFConstants.COMPRESSION_SGI_LOG_24_PACKED)
				{
					ByteChannelImage image = (ByteChannelImage)codec.getImage();
					int numSamples = ifd.getTileWidth();
					byte[] red = new byte[numSamples];
					byte[] green = new byte[numSamples];
					byte[] blue = new byte[numSamples];
					LogLuvConversion.convertLogLuv24InterleavedtoRGB24Planar(data, red, green, blue, numSamples);
					image.putByteSamples(RGBIndex.INDEX_RED, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, red, leftPixels);
					image.putByteSamples(RGBIndex.INDEX_GREEN, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, green, leftPixels);
					image.putByteSamples(RGBIndex.INDEX_BLUE, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, blue, leftPixels);
				}
				break;
			}
			case(TIFFImageFileDirectory.TYPE_LOGL):
			{
				ByteChannelImage image = (ByteChannelImage)codec.getImage();
				int numSamples = ifd.getTileWidth();
				byte[] gray = new byte[numSamples];
				LogLuvConversion.convertLogL16toGray8(data, gray, numSamples);
				image.putByteSamples(0, getX1() - codec.getBoundsX1() + leftPixels, y, numPixels, 1, gray, leftPixels);
				break;
			}
		}
	}
}
