/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sanselan.formats.tiff;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.sanselan.FormatCompliance;
import org.apache.sanselan.ImageReadException;
import org.apache.sanselan.common.BinaryFileParser;
import org.apache.sanselan.common.byteSources.ByteSource;
import org.apache.sanselan.formats.tiff.TiffDirectory.ImageDataElement;
import org.apache.sanselan.formats.tiff.constants.TiffConstants;
import org.apache.sanselan.util.Debug;

public class TiffReader extends BinaryFileParser implements TiffConstants
{

	private final boolean strict;

	public TiffReader(boolean strict)
	{
		this.strict = strict;
	}

	private TiffHeader readTiffHeader(ByteSource byteSource,
			FormatCompliance formatCompliance) throws ImageReadException,
			IOException
	{
		InputStream is = null;
		try
		{
			is = byteSource.getInputStream();
			return readTiffHeader(is, formatCompliance);
		} finally
		{
			try
			{
				if (is != null)
					is.close();
			} catch (Exception e)
			{
				Debug.debug(e);
			}
		}
	}

	private TiffHeader readTiffHeader(InputStream is,
			FormatCompliance formatCompliance) throws ImageReadException,
			IOException
	{
		int BYTE_ORDER_1 = readByte("BYTE_ORDER_1", is, "Not a Valid TIFF File");
		int BYTE_ORDER_2 = readByte("BYTE_ORDER_2", is, "Not a Valid TIFF File");
		setByteOrder(BYTE_ORDER_1, BYTE_ORDER_2);

		int tiffVersion = read2Bytes("tiffVersion", is, "Not a Valid TIFF File");
		if (tiffVersion != 42)
			throw new ImageReadException("Unknown Tiff Version: " + tiffVersion);

		int offsetToFirstIFD = read4Bytes("offsetToFirstIFD", is,
				"Not a Valid TIFF File");

		skipBytes(is, offsetToFirstIFD - 8,
				"Not a Valid TIFF File: couldn't find IFDs");

		if (debug)
			System.out.println("");

		return new TiffHeader(BYTE_ORDER_1, tiffVersion, offsetToFirstIFD);
	}

	private void readDirectories(ByteSource byteSource,
			FormatCompliance formatCompliance, Listener listener)
			throws ImageReadException, IOException
	{
		TiffHeader tiffHeader = readTiffHeader(byteSource, formatCompliance);
		if (!listener.setTiffHeader(tiffHeader))
			return;

		int offset = tiffHeader.offsetToFirstIFD;
		int dirType = TiffDirectory.DIRECTORY_TYPE_ROOT;

		List visited = new ArrayList();
		readDirectory(byteSource, offset, dirType, formatCompliance, listener,
				visited);
	}

	private boolean readDirectory(ByteSource byteSource, int offset,
			int dirType, FormatCompliance formatCompliance, Listener listener,
			List visited) throws ImageReadException, IOException
	{
		boolean ignoreNextDirectory = false;
		return readDirectory(byteSource, offset, dirType, formatCompliance,
				listener, ignoreNextDirectory, visited);
	}

	private boolean readDirectory(ByteSource byteSource, int offset,
			int dirType, FormatCompliance formatCompliance, Listener listener,
			boolean ignoreNextDirectory, List visited)
			throws ImageReadException, IOException
	{
		Number key = new Integer(offset);

		// Debug.debug();
		// Debug.debug("dir offset", offset + " (0x" +
		// Integer.toHexString(offset)
		// + ")");
		// Debug.debug("dir key", key);
		// Debug.debug("dir visited", visited);
		// Debug.debug("dirType", dirType);
		// Debug.debug();

		if (visited.contains(key))
			return false;
		visited.add(key);

		InputStream is = null;
		try
		{
			is = byteSource.getInputStream();
			if (offset > 0)
				is.skip(offset);

			ArrayList fields = new ArrayList();

			if (offset >= byteSource.getLength())
			{
				// Debug.debug("skipping invalid directory!");
				return true;
			}

			int entryCount;
			try
			{
				entryCount = read2Bytes("DirectoryEntryCount", is,
						"Not a Valid TIFF File");
			} catch (IOException e)
			{
				if (strict)
					throw e;
				else
					return true;
			}

			// Debug.debug("entryCount", entryCount);

			for (int i = 0; i < entryCount; i++)
			{
				int tag = read2Bytes("Tag", is, "Not a Valid TIFF File");
				int type = read2Bytes("Type", is, "Not a Valid TIFF File");
				int length = read4Bytes("Length", is, "Not a Valid TIFF File");

				// Debug.debug("tag*", tag + " (0x" + Integer.toHexString(tag)
				// + ")");

				byte valueOffsetBytes[] = readByteArray("ValueOffset", 4, is,
						"Not a Valid TIFF File");
				int valueOffset = convertByteArrayToInt("ValueOffset",
						valueOffsetBytes);

				if (tag == 0)
				{
					// skip invalid fields.
					// These are seen very rarely, but can have invalid value
					// lengths,
					// which can cause OOM problems.
					continue;
				}

				// if (keepField(tag, tags))
				// {
				TiffField field = new TiffField(tag, dirType, type, length,
						valueOffset, valueOffsetBytes, getByteOrder());
				field.setSortHint(i);

				// Debug.debug("tagInfo", field.tagInfo);

				field.fillInValue(byteSource);

				// Debug.debug("\t" + "value", field.getValueDescription());

				fields.add(field);

				if (!listener.addField(field))
					return true;
			}

			int nextDirectoryOffset = read4Bytes("nextDirectoryOffset", is,
					"Not a Valid TIFF File");
			// Debug.debug("nextDirectoryOffset", nextDirectoryOffset);

			TiffDirectory directory = new TiffDirectory(dirType, fields,
					offset, nextDirectoryOffset);

			if (listener.readImageData())
			{
				if (directory.hasTiffImageData())
				{
					TiffImageData rawImageData = getTiffRawImageData(
							byteSource, directory);
					directory.setTiffImageData(rawImageData);
				}
				if (directory.hasJpegImageData())
				{
					JpegImageData rawJpegImageData = getJpegRawImageData(
							byteSource, directory);
					directory.setJpegImageData(rawJpegImageData);
				}
			}

			if (!listener.addDirectory(directory))
				return true;

			if (listener.readOffsetDirectories())
			{
				List fieldsToRemove = new ArrayList();
				for (int j = 0; j < fields.size(); j++)
				{
					TiffField entry = (TiffField) fields.get(j);

					if (entry.tag == TiffConstants.EXIF_TAG_EXIF_OFFSET.tag
							|| entry.tag == TiffConstants.EXIF_TAG_GPSINFO.tag
							|| entry.tag == TiffConstants.EXIF_TAG_INTEROP_OFFSET.tag)
						;
					else
						continue;

					int subDirectoryOffset = ((Number) entry.getValue())
							.intValue();
					int subDirectoryType;
					if (entry.tag == TiffConstants.EXIF_TAG_EXIF_OFFSET.tag)
						subDirectoryType = TiffDirectory.DIRECTORY_TYPE_EXIF;
					else if (entry.tag == TiffConstants.EXIF_TAG_GPSINFO.tag)
						subDirectoryType = TiffDirectory.DIRECTORY_TYPE_GPS;
					else if (entry.tag == TiffConstants.EXIF_TAG_INTEROP_OFFSET.tag)
						subDirectoryType = TiffDirectory.DIRECTORY_TYPE_INTEROPERABILITY;
					else
						throw new ImageReadException(
								"Unknown subdirectory type.");

					// Debug.debug("sub dir", subDirectoryOffset);
					boolean subDirectoryRead = readDirectory(byteSource,
							subDirectoryOffset, subDirectoryType,
							formatCompliance, listener, true, visited);

					if (!subDirectoryRead)
					{
						// Offset field pointed to invalid location.
						// This is a bug in certain cameras. Ignore offset
						// field.
						fieldsToRemove.add(entry);
					}

				}
				fields.removeAll(fieldsToRemove);
			}

			if (!ignoreNextDirectory && directory.nextDirectoryOffset > 0)
			{
				// Debug.debug("next dir", directory.nextDirectoryOffset );
				readDirectory(byteSource, directory.nextDirectoryOffset,
						dirType + 1, formatCompliance, listener, visited);
			}

			return true;
		} finally
		{
			try
			{
				if (is != null)
					is.close();
			} catch (Exception e)
			{
				Debug.debug(e);
			}
		}
	}

	public static interface Listener
	{
		public boolean setTiffHeader(TiffHeader tiffHeader);

		public boolean addDirectory(TiffDirectory directory);

		public boolean addField(TiffField field);

		public boolean readImageData();

		public boolean readOffsetDirectories();
	}

	private static class Collector implements Listener
	{
		private TiffHeader tiffHeader = null;
		private ArrayList directories = new ArrayList();
		private ArrayList fields = new ArrayList();
		private final boolean readThumbnails;

		public Collector()
		{
			this(null);
		}

		public Collector(Map params)
		{
			boolean readThumbnails = true;
			if (params != null && params.containsKey(PARAM_KEY_READ_THUMBNAILS))
				readThumbnails = Boolean.TRUE.equals(params
						.get(PARAM_KEY_READ_THUMBNAILS));
			this.readThumbnails = readThumbnails;
		}

		public boolean setTiffHeader(TiffHeader tiffHeader)
		{
			this.tiffHeader = tiffHeader;
			return true;
		}

		public boolean addDirectory(TiffDirectory directory)
		{
			directories.add(directory);
			return true;
		}

		public boolean addField(TiffField field)
		{
			fields.add(field);
			return true;
		}

		public boolean readImageData()
		{
			return readThumbnails;
		}

		public boolean readOffsetDirectories()
		{
			return true;
		}

		public TiffContents getContents()
		{
			return new TiffContents(tiffHeader, directories);
		}
	}

	private static class FirstDirectoryCollector extends Collector
	{
		private final boolean readImageData;

		public FirstDirectoryCollector(final boolean readImageData)
		{
			this.readImageData = readImageData;
		}

		public boolean addDirectory(TiffDirectory directory)
		{
			super.addDirectory(directory);
			return false;
		}

		public boolean readImageData()
		{
			return readImageData;
		}
	}

	private static class DirectoryCollector extends Collector
	{
		private final boolean readImageData;

		public DirectoryCollector(final boolean readImageData)
		{
			this.readImageData = readImageData;
		}

		public boolean addDirectory(TiffDirectory directory)
		{
			super.addDirectory(directory);
			return false;
		}

		public boolean readImageData()
		{
			return readImageData;
		}
	}

	public TiffContents readFirstDirectory(ByteSource byteSource, Map params,
			boolean readImageData, FormatCompliance formatCompliance)
			throws ImageReadException, IOException
	{
		Collector collector = new FirstDirectoryCollector(readImageData);
		read(byteSource, params, formatCompliance, collector);
		TiffContents contents = collector.getContents();
		if (contents.directories.size() < 1)
			throw new ImageReadException(
					"Image did not contain any directories.");
		return contents;
	}

	public TiffContents readDirectories(ByteSource byteSource,
			boolean readImageData, FormatCompliance formatCompliance)
			throws ImageReadException, IOException
	{
		Collector collector = new FirstDirectoryCollector(readImageData);
		readDirectories(byteSource, formatCompliance, collector);
		TiffContents contents = collector.getContents();
		if (contents.directories.size() < 1)
			throw new ImageReadException(
					"Image did not contain any directories.");
		return contents;
	}

	public TiffContents readContents(ByteSource byteSource, Map params,
			FormatCompliance formatCompliance) throws ImageReadException,
			IOException
	{

		Collector collector = new Collector(params);
		read(byteSource, params, formatCompliance, collector);
		TiffContents contents = collector.getContents();
		return contents;
	}

	public void read(ByteSource byteSource, Map params,
			FormatCompliance formatCompliance, Listener listener)
			throws ImageReadException, IOException
	{
		// TiffContents contents =
		readDirectories(byteSource, formatCompliance, listener);
	}

	private TiffImageData getTiffRawImageData(ByteSource byteSource,
			TiffDirectory directory) throws ImageReadException, IOException
	{

		ArrayList elements = directory.getTiffRawImageDataElements();
		TiffImageData.Data data[] = new TiffImageData.Data[elements.size()];
		for (int i = 0; i < elements.size(); i++)
		{
			TiffDirectory.ImageDataElement element = (TiffDirectory.ImageDataElement) elements
					.get(i);
			byte bytes[] = byteSource.getBlock(element.offset, element.length);
			data[i] = new TiffImageData.Data(element.offset, element.length,
					bytes);
		}

		if (directory.imageDataInStrips())
		{
			TiffField rowsPerStripField = directory
					.findField(TIFF_TAG_ROWS_PER_STRIP);
			if (null == rowsPerStripField)
				throw new ImageReadException("Can't find rows per strip field.");
			int rowsPerStrip = rowsPerStripField.getIntValue();

			return new TiffImageData.Strips(data, rowsPerStrip);
		} else
		{
			TiffField tileWidthField = directory.findField(TIFF_TAG_TILE_WIDTH);
			if (null == tileWidthField)
				throw new ImageReadException("Can't find tile width field.");
			int tileWidth = tileWidthField.getIntValue();

			TiffField tileLengthField = directory
					.findField(TIFF_TAG_TILE_LENGTH);
			if (null == tileLengthField)
				throw new ImageReadException("Can't find tile length field.");
			int tileLength = tileLengthField.getIntValue();

			return new TiffImageData.Tiles(data, tileWidth, tileLength);
		}
	}

	private JpegImageData getJpegRawImageData(ByteSource byteSource,
			TiffDirectory directory) throws ImageReadException, IOException
	{
		ImageDataElement element = directory.getJpegRawImageDataElement();
		int offset = element.offset;
		int length = element.length;
		// Sony DCR-PC110 has an off-by-one error.
		if (offset + length == byteSource.getLength() + 1)
			length--;
		byte data[] = byteSource.getBlock(offset, length);
		return new JpegImageData(offset, length, data);
	}

}
