/*
 * Copyright 2011 Specto Technologies Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.spectotechnologies.imageio.plugins.eps;

import com.spectotechnologies.imageio.plugins.eps.util.ASCII85Decoder;
import com.spectotechnologies.util.BlankRemover;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;

/**
 *
 * @author Specto Technologies Inc., Alexandre Lavoie
 */
public class EPSImageReader extends ImageReader
{
	ImageInputStream m_oDataStream = null;
	int m_nWidth = -1, m_nHeight = -1;
	int m_nMode;
	// Constants enumerating the values of colorType
	boolean gotHeader = false;
	private EPSMetaData metadata;
	private int m_nPaddingBandsCount					= -1;
	private int m_nDataType								= -1;

	public static final int DEPTH_1						= 1;
	public static final int DEPTH_8						= 8;
	
	public static final int MODE_BITMAP					= 1;
	public static final int MODE_LAB					= 2;
	public static final int MODE_RGB					= 3;
	public static final int MODE_CMYK					= 4;

	public static final int DATATYPE_BINARY				= 1;
	public static final int DATATYPE_HEXASCII			= 2;
	public static final int DATATYPE_ASCII85_JPEG_CMYK	= 5;
	public static final int DATATYPE_ASCII85_JPEG_RGB	= 6;
	
	private long m_lImageLength;

	public EPSImageReader(ImageReaderSpi originatingProvider)
	{
		super(originatingProvider);
	}

	@Override
	public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata)
	{
		super.setInput(input, seekForwardOnly, ignoreMetadata);

		// Clear all local values based on the previous stream contents.
		resetLocal();

		if(input != null)
		{
			if(!(input instanceof ImageInputStream))
			{
				throw new IllegalArgumentException("input not an ImageInputStream!");
			}
			this.m_oDataStream = (ImageInputStream)input;
		}
		else
		{
			this.m_oDataStream = null;
		}
	}

	protected void resetLocal()
	{
        m_oDataStream = null;
        gotHeader = false;
        //imageReadParam = getDefaultReadParam();
        //streamMetadata = null;
        //currIndex = -1;
        //imageMetadata = null;
        //imageStartPosition = new ArrayList();
        //numImages = -1;
        //imageTypeMap = new HashMap();
        m_nWidth = -1;
        m_nHeight = -1;
        m_nPaddingBandsCount = -1;
        //tileOrStripWidth = -1;
        //tileOrStripHeight = -1;
        //planarConfiguration = BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY;
        //rowsDone = 0;
    }

	@Override
	public int getNumImages(boolean allowSearch) throws IIOException
	{
		return 1; // format can only encode a single image
	}

	private void checkIndex(int imageIndex)
	{
		if(imageIndex != 0)
		{
			throw new IndexOutOfBoundsException("bad index");
		}
	}

	@Override
	public int getWidth(int imageIndex) throws IIOException
	{
		checkIndex(imageIndex); // must throw an exception if != 0
		readHeader();
		return m_nWidth;
	}
	@Override
	public int getHeight(int imageIndex) throws IIOException
	{
		checkIndex(imageIndex);
		readHeader();
		return m_nHeight;
	}

	@Override
	public Iterator getImageTypes(int imageIndex) throws IOException, IIOException
	{
		checkIndex(imageIndex);
		readHeader();
		ImageTypeSpecifier imageType = null;
		int datatype = DataBuffer.TYPE_BYTE;
		java.util.List l = new ArrayList();

		ColorSpace cs;
		int[] bandOffsets;

		// @TODO : Check if the file has an embedded color profile
		switch(m_nMode)
		{
			case MODE_BITMAP:
				cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
				bandOffsets = new int[1];
				bandOffsets[0] = 0;
				imageType = ImageTypeSpecifier.createInterleaved(cs,bandOffsets,datatype,false,false);
				break;
			case MODE_RGB:
				cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
				bandOffsets = new int[3];
				bandOffsets[0] = 0;
				bandOffsets[1] = 1;
				bandOffsets[2] = 2;
				imageType = ImageTypeSpecifier.createInterleaved(cs,bandOffsets,datatype,false,false);
				break;
			case MODE_CMYK:
				// Use the standard Adobe Photoshop ICC
				ICC_Profile ip = ICC_Profile.getInstance("USSheetfedCoated.icc");
				cs = new ICC_ColorSpace(ip);
				bandOffsets = new int[4];
				bandOffsets[0] = 0;
				bandOffsets[1] = 1;
				bandOffsets[2] = 2;
				bandOffsets[3] = 3;
				imageType = ImageTypeSpecifier.createInterleaved(cs,bandOffsets,datatype,false,false);
				break;
			default:
				throw new IIOException("Can't read mode " + m_nMode);
		}

		l.add(imageType);

		return l.iterator();
	}

	public void readHeader() throws IIOException
	{
		byte[] aStart = new byte[4];
		String sLine;
		String[] aParameters;
		int nParameter, nDepth, nBlockSize, nChannels;
		String sDataStart = "";
		
		if(gotHeader)
		{
			return;
		}

		m_nWidth = 0;
		m_nHeight = 0;
		m_nMode = 0;

		gotHeader = true;
		
		if(m_oDataStream == null)
		{
			throw new IllegalStateException("No input stream");
		}

		String sSignature;
		try
		{
			m_oDataStream.read(aStart);
			while(aStart[0] != '%' || aStart[1] != '!' || aStart[2] != 'P' || aStart[3] != 'S')
			{
				aStart[0] = aStart[1];
				aStart[1] = aStart[2];
				aStart[2] = aStart[3];
				aStart[3] = m_oDataStream.readByte();
			}
			sSignature = "%!PS" + m_oDataStream.readLine();
		}
		catch(IOException e)
		{
			throw new IIOException("Error reading signature",e);
		}

		// Test the signature

		//%!PS-Adobe-3.0 EPSF-3.0 %!PS-Adobe-3.1 EPSF-3.0
		if(sSignature.matches("%!PS-Adobe-3\\.[0-1] EPSF-3\\.[0-1]\\n"))
		{
			throw new IIOException("Bad file signature: " + sSignature);
		}

		/*
		%!PS-Adobe-3.0 EPSF-3.0
		%%Creator: Adobe Photoshop Version 10.0x20070321 [20070321.m.1480 2007/03/21:16:39:00 cutoff; m branch]
		%%Title: 235-821.eps
		%%CreationDate: 9/16/08 1:43 PM
		%%BoundingBox: 0 0 360 360
		%%HiResBoundingBox: 0 0 360 360
		%%SuppressDotGainCompensation
		%%DocumentProcessColors: Cyan Magenta Yellow Black
		 */
		try
		{
			while((sLine = m_oDataStream.readLine()) != null)
			{
				// Find the image data
				//%ImageData: <columns> <rows> <depth> <mode> <pad channels> <block size> <binary/hex> "<data start>"
				if(sLine.startsWith("%ImageData:"))
				{
					aParameters = sLine.split(" ");
					nParameter = 1;

					m_nWidth		= Integer.valueOf(aParameters[nParameter++]);
					m_nHeight		= Integer.valueOf(aParameters[nParameter++]);
					nDepth			= Integer.valueOf(aParameters[nParameter++]);
					m_nMode			= Integer.valueOf(aParameters[nParameter++]);
					m_nPaddingBandsCount	= Integer.valueOf(aParameters[nParameter++]);
					nBlockSize		= Integer.valueOf(aParameters[nParameter++]);
					m_nDataType		= Integer.valueOf(aParameters[nParameter++]);
					sDataStart		= aParameters[nParameter++];
					sDataStart		= sDataStart.substring(1,sDataStart.length() - 1);
				}
				else if(sLine.startsWith("%%BeginBinary:"))
				{
					m_lImageLength = Long.parseLong(BlankRemover.ltrim(sLine.substring(sLine.indexOf(':') + 1)));
				}
				else if(sLine.equalsIgnoreCase(sDataStart))
				{
					// We got the right position for image data
					break;
				}
			}
		}
		catch(Exception e)
		{
			throw new IIOException("Error reading header",e);
		}

		switch(m_nDataType)
		{
			case DATATYPE_BINARY:
				break;
			case DATATYPE_ASCII85_JPEG_CMYK:
				break;
			case DATATYPE_ASCII85_JPEG_RGB:
				break;
			default:
				throw new IIOException("Data type " + m_nDataType + " not supported");
		}

		// Read width, height, color type, newline
		if(m_nWidth == 0 || m_nHeight == 0 || m_nMode == 0)
		{
			throw new IIOException("Error reading header");
		}
	}

	/**
	 * @TODO
	 *
	 * @throws IIOException
	 */
	public void readMetadata() throws IIOException
	{
		if(metadata != null)
		{
			return;
		}
		readHeader();

		this.metadata = new EPSMetaData();

		//try
		//{
			//while(true)
			//{
			//	String keyword = stream.readUTF();
			//	stream.readUnsignedByte();
			//	if(keyword.equals("END"))
			//	{
			//		break;
			//	}

			//	String value = stream.readUTF();
			//	stream.readUnsignedByte();
			//	metadata.keywords.add(keyword);
			//	metadata.values.add(value);
			//}
		//}
		//catch(IOException e)
		//{
		//	throw new IIOException("Exception reading metadata",e);
		//}
	}

	@Override
	public BufferedImage read(int imageIndex, ImageReadParam param) throws IIOException, IOException
	{
		readMetadata();

		// Enure band settings from param are compatible with images
		int nInputBands = 0;

		switch(m_nMode)
		{
			case MODE_BITMAP:
				nInputBands = 1;
				break;
			case MODE_LAB:
				break;
			case MODE_CMYK:
				nInputBands = 4;
				break;
			case MODE_RGB:
				nInputBands = 3;
				break;
		}

		// Compute initial source region, clip against destination later
		Rectangle sourceRegion = getSourceRegion(param,m_nWidth,m_nHeight);
		
		// Set everything to default values
		int sourceXSubsampling = 0;
		int sourceYSubsampling = 0;
		int[] aSourceBands = null;
		int[] aResultBands = null;
		Point destinationOffset = new Point(0,0);

		// Get values from the ImageReadParam, if any
		if(param != null)
		{
			sourceXSubsampling = param.getSourceXSubsampling();
			sourceYSubsampling = param.getSourceYSubsampling();
			aSourceBands = param.getSourceBands();
			aResultBands = param.getDestinationBands();
			destinationOffset = param.getDestinationOffset();
		}

		// Get the specified detination image or create a new one
		BufferedImage dst = getDestination(param,getImageTypes(0),m_nWidth,m_nHeight);

		checkReadParamBandSettings(param,nInputBands,dst.getSampleModel().getNumBands());

		int[] bankIndices = new int[nInputBands];
		int[] bandOffsets = new int[nInputBands];
		for(int i = 0;i < nInputBands;i++)
		{
			bandOffsets[i] = i * m_nWidth;
			bankIndices[i] = 0;
		}

		int bytesPerRow = m_nWidth * (nInputBands + m_nPaddingBandsCount);
		DataBufferByte rowDB = new DataBufferByte(bytesPerRow);
		WritableRaster oRasterRow = Raster.createBandedRaster(rowDB,m_nWidth,1,m_nWidth,bankIndices,bandOffsets,new Point(0,0));

		byte[] aRowBuffer = rowDB.getData();
		
		// Create an array that can handle a single pixel
		int[] pixel = oRasterRow.getPixel(0,0,(int[])null);

		WritableRaster oRasterImage = dst.getWritableTile(0,0);
		int dstMinX = oRasterImage.getMinX();
		int dstMaxX = dstMinX + oRasterImage.getWidth();
		int dstMinY = oRasterImage.getMinY();
		int dstMaxY = dstMinY + oRasterImage.getHeight();
		
		// Create a child raster exposing only the desired source bands
		if(aSourceBands != null)
		{
			oRasterRow = oRasterRow.createWritableChild(0,0,m_nWidth,1,0,0,aSourceBands);
		}

		// Create a child raster exposing only the desired dest bands
		if(aResultBands != null)
		{
			oRasterImage = oRasterImage.createWritableChild(0,0,oRasterImage.getWidth(),oRasterImage.getHeight(),0,0,aResultBands);
		}

		//

		ByteBuffer oBuffer = null;
		byte[] aData;

		switch(m_nDataType)
		{
			case DATATYPE_BINARY:
				for(int nSourceY = 0;nSourceY < m_nHeight;nSourceY++)
				{
					// Read the row
					try
					{
						m_oDataStream.readFully(aRowBuffer);

						// Skip paddingbands
						//if(paddingBands > 0)
						//{
						//	m_oDataStream.skipBytes(m_nWidth * paddingBands);
						//}
					}
					catch(IOException e)
					{
						throw new IIOException("Error reading line " + nSourceY,e);
					}

					// Reject rows that lie outside the source region,
					// or which aren’t part of the subsampling
					if((nSourceY < sourceRegion.y) || (nSourceY >= sourceRegion.y + sourceRegion.height) || (((nSourceY - sourceRegion.y) % sourceYSubsampling) != 0))
					{
						continue;
					}

					// Determine where the row will go in the destination
					int dstY = destinationOffset.y + (nSourceY - sourceRegion.y) / sourceYSubsampling;

					if(dstY < dstMinY)
					{
						continue; // The row is above imRas
					}

					if(dstY > dstMaxY)
					{
						break; // We’re done with the image
					}

					// Copy each (subsampled) source pixel into imRas
					for(int srcX = sourceRegion.x;srcX < sourceRegion.x + sourceRegion.width;srcX++)
					{
						if (((srcX - sourceRegion.x) % sourceXSubsampling) != 0)
						{
							continue;
						}

						int dstX = destinationOffset.x + (srcX - sourceRegion.x) / sourceXSubsampling;
						if(dstX < dstMinX)
						{
							continue; // The pixel is to the left of imRas
						}

						if(dstX > dstMaxX)
						{
							break; // We’re done with the row
						}

						// Copy the pixel, sub-banding is done automatically
						oRasterRow.getPixel(srcX, 0, pixel);
						oRasterImage.setPixel(dstX, dstY, pixel);
					}
				}
				break;
			case DATATYPE_ASCII85_JPEG_CMYK:
			case DATATYPE_ASCII85_JPEG_RGB:
				aData = new byte[(int)m_lImageLength];
				m_oDataStream.read(aData,0,(int)m_lImageLength);
				oBuffer = ByteBuffer.wrap(aData);
				oBuffer = ASCII85Decoder.decode(oBuffer);

				dst = ImageIO.read(new ByteArrayInputStream(oBuffer.array()));
				break;
			default:
				throw new IIOException("Data type " + m_nDataType + " not supported");
		}

		return dst;
	}

	@Override
	public IIOMetadata getStreamMetadata() throws IOException
	{
		throw new UnsupportedOperationException("Not supported yet.");
	}

	@Override
	public IIOMetadata getImageMetadata(int imageIndex) throws IOException
	{
		throw new UnsupportedOperationException("Not supported yet.");
	}
}
