/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-2006, GeoTools Project Managment Committee (PMC)
 *
 *    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;
 *    version 2.1 of the License.
 *
 *    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.
 */
package org.geotools.gce.geotiff.IIOMetadataAdpaters;

import java.awt.geom.AffineTransform;

import org.geotools.gce.geotiff.IIOMetadataAdpaters.utils.GeoTiffConstants;
import org.geotools.util.KeySortedList;
import org.jdom.Element;

import com.sun.media.imageio.plugins.tiff.BaselineTIFFTagSet;
import com.sun.media.imageio.plugins.tiff.GeoTIFFTagSet;
import com.sun.media.imageio.plugins.tiff.TIFFTag;

/**
 * This class is responsible for encoding the geotiff tags into suitable
 * metadata for the ImageIO library.
 * 
 * <p>
 * Basically it is and encoder/adapter that collects all the different tags,
 * order it accordingly to the spec and then organize then into a dom tree ready
 * to be used by the ImageIO metadata mechanism.
 * 
 * 
 * 
 * @author Simone Giannecchini
 * @since 2.3
 * 
 */
public final class GeoTiffIIOMetadataEncoder {

	private int numModelTiePoints;

	private TiePoint[] modelTiePoints;

	private PixelScale modelPixelScale;

	private double[] modelTransformation;

	private int numGeoTiffEntries;

	private KeySortedList geoTiffEntries;

	private int numGeoTiffDoubleParams;

	private double[] geoTiffDoubleParams;

	private int numGeoTiffAsciiParams;

	private StringBuffer geoTiffAsciiParams;

	public GeoTiffIIOMetadataEncoder() {
		this(GeoTiffConstants.DEFAULT_GEOTIFF_VERSION,
				GeoTiffConstants.DEFAULT_KEY_REVISION_MAJOR,
				GeoTiffConstants.DEFAULT_KEY_REVISION_MINOR);
	}

	public GeoTiffIIOMetadataEncoder(final int geoTIFFVersion,
			final int keyRevisionMajor, final int keyRevisionMinor) {
		geoTiffEntries = new KeySortedList();
		geoTiffDoubleParams = new double[GeoTiffConstants.ARRAY_ELEM_INCREMENT];
		geoTiffAsciiParams = new StringBuffer();
		modelTiePoints = new TiePoint[GeoTiffConstants.ARRAY_ELEM_INCREMENT];
		modelPixelScale = new PixelScale();
		modelTransformation = new double[16];
		addGeoKeyEntry(geoTIFFVersion, keyRevisionMajor, 1, keyRevisionMinor);
	}

	public static boolean isTiffUShort(final int value) {
		return (value >= GeoTiffConstants.USHORT_MIN)
				&& (value <= GeoTiffConstants.USHORT_MAX);
	}

	public int getGeoTIFFVersion() {
		return getGeoKeyEntryAt(0).getKeyID();
	}

	public void setGeoTIFFVersion(int version) {
		getGeoKeyEntryAt(0).setKeyID(version);
	}

	public int getKeyRevisionMajor() {
		return getGeoKeyEntryAt(0).getTiffTagLocation();
	}

	public int getKeyRevisionMinor() {
		return getGeoKeyEntryAt(0).getCount();
	}

	public void setKeyRevision(int major, int minor) {
		getGeoKeyEntryAt(0).setTiffTagLocation(major);
		getGeoKeyEntryAt(0).setCount(minor);
	}

	public double getModelPixelScaleX() {
		return modelPixelScale.getScaleX();
	}

	public double getModelPixelScaleY() {
		return modelPixelScale.getScaleY();
	}

	public double getModelPixelScaleZ() {
		return modelPixelScale.getScaleZ();
	}

	public void setModelPixelScale(double x, double y) {
		setModelPixelScale(x, y, 0.0);
	}

	public void setModelPixelScale(double x, double y, double z) {
		if (isModelTransformationSet())
			throw new IllegalStateException(
					"ModelTransformationTag already set. It is not possible to set the ModelPixelScale.");
		modelPixelScale.setScaleX(x);
		modelPixelScale.setScaleY(y);
		modelPixelScale.setScaleZ(z);
	}

	public int getNumModelTiePoints() {
		return numModelTiePoints;
	}

	public TiePoint getModelTiePoint() {
		return getModelTiePointAt(0);
	}

	public TiePoint getModelTiePointAt(int index) {
		return modelTiePoints[index];
	}

	public void setModelTiePoint(double i, double j, double x, double y) {
		setModelTiePoint(i, j, 0.0, x, y, 0.0);
	}

	public void setModelTiePoint(double i, double j, double k, double x,
			double y, double z) {
		if (isModelTransformationSet())
			throw new IllegalStateException(
					"ModelTransformationTag already set. It is not possible to set the ModelTiePoint.");
		if (getNumModelTiePoints() > 0) {
			getModelTiePointAt(0).set(i, j, k, x, y, z);
		} else {
			addModelTiePoint(i, j, k, x, y, z);
		}
	}

	public void addModelTiePoint(double i, double j, double x, double y) {
		addModelTiePoint(i, j, 0.0, x, y, 0.0);
	}

	public void addModelTiePoint(double i, double j, double k, double x,
			double y, double z) {
		final int numTiePoints = numModelTiePoints;

		if (numTiePoints >= (modelTiePoints.length - 1)) {
			final TiePoint[] tiePoints = new TiePoint[numTiePoints
					+ GeoTiffConstants.ARRAY_ELEM_INCREMENT];
			System.arraycopy(modelTiePoints, 0, tiePoints, 0, numTiePoints);
			modelTiePoints = tiePoints;
		}

		modelTiePoints[numTiePoints] = new TiePoint(i, j, k, x, y, z);
		numModelTiePoints++;
	}

	public int getNumGeoKeyEntries() {
		return numGeoTiffEntries;
	}

	public GeoKeyEntry getGeoKeyEntryAt(int index) {
		// got to retrieve the eleme at a certain index
		final Object it = this.geoTiffEntries.get(index);
		if (it != null)
			return (GeoKeyEntry) (it);
		return null;
	}

	public GeoKeyEntry getGeoKeyEntry(int keyID) {
		GeoKeyEntry retVal = null;
		final Object o = geoTiffEntries.first(new Integer(keyID));
		if (o != null)
			retVal = (GeoKeyEntry) o;

		return retVal;
	}

	public boolean hasGeoKeyEntry(int keyID) {
		return getGeoKeyEntry(keyID) != null;
	}

	public int getGeoShortParam(int keyID) {
		final GeoKeyEntry entry = getNonNullKeyEntry(keyID);
		final int tag = entry.getTiffTagLocation();
		final int value = entry.getValueOffset();
		checkParamTag(tag, 0);

		return value;
	}

	public double getGeoDoubleParam(int keyID) {
		final GeoKeyEntry entry = getNonNullKeyEntry(keyID);
		final int tag = entry.getTiffTagLocation();
		final int offset = entry.getValueOffset();
		checkParamTag(tag, getGeoDoubleParamsTag().getNumber());

		return geoTiffDoubleParams[offset];
	}

	public double[] getGeoDoubleParams(int keyID) {
		return getGeoDoubleParams(keyID, null);
	}

	public double[] getGeoDoubleParams(int keyID, double[] values) {
		final GeoKeyEntry entry = getNonNullKeyEntry(keyID);
		final int tag = entry.getTiffTagLocation();
		final int count = entry.getCount();
		final int offset = entry.getValueOffset();
		checkParamTag(tag, getGeoDoubleParamsTag().getNumber());

		if (values == null) {
			values = new double[count];
		}

		System.arraycopy(geoTiffDoubleParams, offset, values, 0, count);

		return values;
	}

	public String getGeoAsciiParam(int keyID) {
		final GeoKeyEntry entry = getNonNullKeyEntry(keyID);
		final int tag = entry.getTiffTagLocation();
		final int count = entry.getCount();
		final int offset = entry.getValueOffset();
		checkParamTag(tag, getGeoAsciiParamsTag().getNumber());

		return geoTiffAsciiParams.substring(offset, (offset + count) - 1);
	}

	public void addGeoShortParam(int keyID, int value) {
		addGeoKeyEntry(keyID, 0, 1, value);
	}

	public void addGeoDoubleParam(int keyID, double value) {
		addGeoDoubleParamsRef(keyID, 1);
		addDoubleParam(value);
	}

	public void addGeoDoubleParams(int keyID, double[] values) {
		addGeoDoubleParamsRef(keyID, values.length);
		final int length = values.length;
		for (int i = 0; i < length; i++) {
			addDoubleParam(values[i]);
		}
	}

	public void addGeoAscii(int keyID, String value) {
		addGeoAsciiParamsRef(keyID, value.length() + 1);
		// +1 for the '|' character to be appended
		addAsciiParam(value);
	}

	private void addGeoKeyEntry(int keyID, int tag, int count, int offset) {
		if (!isTiffUShort(keyID)) {
			throw new IllegalArgumentException("keyID is not a TIFF USHORT");
		}

		if (!isTiffUShort(tag)) {
			throw new IllegalArgumentException("tag is not a TIFF USHORT");
		}

		if (!isTiffUShort(count)) {
			throw new IllegalArgumentException("count is not a TIFF USHORT");
		}

		if (!isTiffUShort(offset)) {
			throw new IllegalArgumentException("offset is not a TIFF USHORT");
		}

		final int numKeyEntries = numGeoTiffEntries;
		geoTiffEntries.add(new Integer(keyID), new GeoKeyEntry(keyID, tag,
				offset, count));
		getGeoKeyEntryAt(0).setCount(numKeyEntries);
		numGeoTiffEntries++;
	}

	public void assignTo(Element element) {
		if (!element.getName().equals(
				GeoTiffConstants.GEOTIFF_IIO_ROOT_ELEMENT_NAME)) {
			throw new IllegalArgumentException("root not found: "
					+ GeoTiffConstants.GEOTIFF_IIO_ROOT_ELEMENT_NAME);
		}

		final Element ifd1 = element.getChild(GeoTiffConstants.GEOTIFF_IFD_TAG);

		if (ifd1 == null) {
			throw new IllegalArgumentException("Unable to find child "
					+ GeoTiffConstants.GEOTIFF_IFD_TAG);
		}

		final Element ifd2 = createIFD();
		ifd1.setAttribute(GeoTiffConstants.GEOTIFF_TAGSETS_ATT_NAME, ifd2
				.getAttributeValue(GeoTiffConstants.GEOTIFF_TAGSETS_ATT_NAME));

		final Element[] childElems = (Element[]) ifd2.getChildren().toArray(
				new Element[0]);
		final int length = childElems.length;
		Element child;
		for (int i = 0; i < length; i++) {
			child = childElems[i];
			ifd2.removeContent(child);
			ifd1.addContent(child);
		}
	}

	public Element createRootTree() {
		final Element rootElement = new Element(
				GeoTiffConstants.GEOTIFF_IIO_ROOT_ELEMENT_NAME);
		rootElement.addContent(createIFD());

		return rootElement;
	}

	protected static TIFFTag getGeoKeyDirectoryTag() {
		return GeoTIFFTagSet.getInstance().getTag(
				GeoTIFFTagSet.TAG_GEO_KEY_DIRECTORY);
	}

	protected static TIFFTag getGeoDoubleParamsTag() {
		return GeoTIFFTagSet.getInstance().getTag(
				GeoTIFFTagSet.TAG_GEO_DOUBLE_PARAMS);
	}

	protected static TIFFTag getGeoAsciiParamsTag() {
		return GeoTIFFTagSet.getInstance().getTag(
				GeoTIFFTagSet.TAG_GEO_ASCII_PARAMS);
	}

	protected static TIFFTag getModelPixelScaleTag() {
		return GeoTIFFTagSet.getInstance().getTag(
				GeoTIFFTagSet.TAG_MODEL_PIXEL_SCALE);
	}

	protected static TIFFTag getModelTiePointTag() {
		return GeoTIFFTagSet.getInstance().getTag(
				GeoTIFFTagSet.TAG_MODEL_TIE_POINT);
	}

	protected static TIFFTag getModelTransformationTag() {
		return GeoTIFFTagSet.getInstance().getTag(
				GeoTIFFTagSet.TAG_MODEL_TRANSFORMATION);
	}

	private GeoKeyEntry getNonNullKeyEntry(int keyID) {
		final GeoKeyEntry entry = getGeoKeyEntry(keyID);

		if (entry == null) {
			throw new IllegalArgumentException(
					"Unable to find an entry for the provided geo key " + keyID);
		}

		return entry;
	}

	private void checkParamTag(final int tag, final int expectedTag) {
		if (tag != expectedTag) {
			if (expectedTag == 0) {
				throw new IllegalArgumentException(
						"invalid key access, not a GeoTIFF SHORT parameter");
			} else if (expectedTag == getGeoDoubleParamsTag().getNumber()) {
				throw new IllegalArgumentException(
						"invalid key access, not a GeoTIFF DOUBLE parameter");
			} else if (expectedTag == getGeoAsciiParamsTag().getNumber()) {
				throw new IllegalArgumentException(
						"invalid key access, not a GeoTIFF ASCII parameter");
			} else {
				throw new IllegalStateException();
			}
		}
	}

	private void addDoubleParam(double param) {
		final int numDoubleParams = numGeoTiffDoubleParams;

		if (numDoubleParams >= (geoTiffDoubleParams.length - 1)) {
			final double[] doubleParams = new double[numDoubleParams
					+ GeoTiffConstants.ARRAY_ELEM_INCREMENT];
			System.arraycopy(geoTiffDoubleParams, 0, doubleParams, 0,
					numDoubleParams);
			geoTiffDoubleParams = doubleParams;
		}

		geoTiffDoubleParams[numDoubleParams] = param;
		numGeoTiffDoubleParams++;
	}

	private void addAsciiParam(String param) {
		geoTiffAsciiParams.append(param);
		geoTiffAsciiParams.append('|');
		numGeoTiffAsciiParams++;
	}

	private void addGeoDoubleParamsRef(int keyID, int count) {
		addGeoKeyEntry(keyID, getGeoDoubleParamsTag().getNumber(), count,
				getCurrentGeoDoublesOffset());
	}

	private void addGeoAsciiParamsRef(int keyID, int length) {
		addGeoKeyEntry(keyID, getGeoAsciiParamsTag().getNumber(), length,
				getCurrentGeoAsciisOffset());
	}

	private int getCurrentGeoDoublesOffset() {
		return numGeoTiffDoubleParams;
	}

	private int getCurrentGeoAsciisOffset() {
		return geoTiffAsciiParams.length();
	}

	private Element createIFD() {
		Element ifd = new Element(GeoTiffConstants.GEOTIFF_IFD_TAG);
		ifd.setAttribute(GeoTiffConstants.GEOTIFF_TAGSETS_ATT_NAME,
				BaselineTIFFTagSet.class.getName() + ","
						+ GeoTIFFTagSet.class.getName());

		if (modelPixelScale.isSet()) {
			ifd.addContent(createModelPixelScaleElement());
		}

		if (isModelTiePointsSet()) {
			ifd.addContent(createModelTiePointsElement());
		} else if (isModelTransformationSet()) {
			ifd.addContent(createModelTransformationElement());
		}

		if (getNumGeoKeyEntries() > 1) {
			ifd.addContent(createGeoKeyDirectoryElement());
		}

		if (numGeoTiffDoubleParams > 0) {
			ifd.addContent(createGeoDoubleParamsElement());
		}

		if (numGeoTiffAsciiParams > 0) {
			ifd.addContent(createGeoAsciiParamsElement());
		}

		return ifd;
	}

	private boolean isModelTiePointsSet() {
		return numModelTiePoints > 0;
	}

	private boolean isModelTransformationSet() {
		final int length = modelTransformation.length;
		for (int i = 0; i < length; i++) {
			if (modelTransformation[i] != 0.0) {
				return true;
			}
		}

		return false;
	}

	public void setModelTransformation(final AffineTransform rasterToModel) {
		if (modelPixelScale!=null&&modelPixelScale.isSet())
			throw new IllegalStateException(
					"ModelPixelScaleTag already set. It is not possible to set the ModelTransformation.");
		if (isModelTiePointsSet())
			throw new IllegalStateException(
					"ModelTiePointsTag already set. It is not possible to set the ModelTransformation.");

		// //
		//
		// See pag 28 of the spec for an explanation
		//
		// //
		// a
		modelTransformation[0] = rasterToModel.getScaleX();
		// b
		modelTransformation[1] = rasterToModel.getShearX();
		// c
		modelTransformation[2] = 0;
		// d
		modelTransformation[3] = rasterToModel.getTranslateX();
		// e
		modelTransformation[4] = rasterToModel.getShearY();
		// f
		modelTransformation[5] = rasterToModel.getScaleY();
		// g
		modelTransformation[6] = 0;
		// h
		modelTransformation[7] = rasterToModel.getTranslateY();
		// i
		modelTransformation[8] = 0;
		// j
		modelTransformation[9] = 0;
		// k
		modelTransformation[10] = 0;
		// l
		modelTransformation[11] = 0;
		// m
		modelTransformation[12] = 0;
		// n
		modelTransformation[13] = 0;
		// o
		modelTransformation[14] = 0;
		// p
		modelTransformation[15] = 1;

	}

	private Element createGeoKeyDirectoryElement() {
		Element field = createFieldElement(getGeoKeyDirectoryTag());
		Element data = new Element(GeoTiffConstants.GEOTIFF_SHORTS_TAG);
		field.addContent(data);
		int[] values;
		int lenght;
		Element GeoKeyRecord;
		;
		for (int i = 0; i < numGeoTiffEntries; i++) {
			values = getGeoKeyEntryAt(i).getValues();
			lenght = values.length;
			for (int j = 0; j < lenght; j++) {
				GeoKeyRecord = createShortElement(values[j]);
				data.addContent(GeoKeyRecord);
			}
		}

		return field;
	}

	private Element createGeoDoubleParamsElement() {
		Element field = createFieldElement(getGeoDoubleParamsTag());
		Element data = new Element(GeoTiffConstants.GEOTIFF_DOUBLES_TAG);
		field.addContent(data);
		Element param;
		for (int i = 0; i < numGeoTiffDoubleParams; i++) {
			param = createDoubleElement(geoTiffDoubleParams[i]);
			data.addContent(param);
		}

		return field;
	}

	private Element createGeoAsciiParamsElement() {
		Element field = createFieldElement(getGeoAsciiParamsTag());
		Element data = new Element(GeoTiffConstants.GEOTIFF_ASCIIS_TAG);
		field.addContent(data);
		data.addContent(createAsciiElement(geoTiffAsciiParams.toString()));

		return field;
	}

	private Element createModelPixelScaleElement() {
		Element field = createFieldElement(getModelPixelScaleTag());
		Element data = new Element(GeoTiffConstants.GEOTIFF_DOUBLES_TAG);
		field.addContent(data);
		addDoubleElements(data, modelPixelScale.getValues());

		return field;
	}

	private Element createModelTransformationElement() {
		Element field = createFieldElement(getModelTransformationTag());
		Element data = new Element(GeoTiffConstants.GEOTIFF_DOUBLES_TAG);
		field.addContent(data);
		addDoubleElements(data, modelTransformation);

		return field;
	}

	private Element createModelTiePointsElement() {
		Element field = createFieldElement(getModelTiePointTag());
		Element data = new Element(GeoTiffConstants.GEOTIFF_DOUBLES_TAG);
		field.addContent(data);

		for (int i = 0; i < numModelTiePoints; i++) {
			addDoubleElements(data, modelTiePoints[i].getData());
		}

		return field;
	}

	private Element createFieldElement(final TIFFTag tag) {
		Element field = new Element(GeoTiffConstants.GEOTIFF_FIELD_TAG);
		field.setAttribute(GeoTiffConstants.NUMBER_ATTR, String.valueOf(tag
				.getNumber()));
		field.setAttribute(GeoTiffConstants.NAME_ATTR, tag.getName());

		return field;
	}

	private Element createShortElement(final int value) {
		Element GeoKeyRecord = new Element(GeoTiffConstants.GEOTIFF_SHORT_TAG);
		GeoKeyRecord.setAttribute(GeoTiffConstants.VALUE_ATTR, String
				.valueOf(value));

		return GeoKeyRecord;
	}

	private Element createDoubleElement(final double value) {
		Element param = new Element(GeoTiffConstants.GEOTIFF_DOUBLE_TAG);
		param.setAttribute(GeoTiffConstants.VALUE_ATTR, String.valueOf(value));

		return param;
	}

	private Element createAsciiElement(final String value) {
		Element param = new Element(GeoTiffConstants.GEOTIFF_ASCII_TAG);
		param.setAttribute(GeoTiffConstants.VALUE_ATTR, String.valueOf(value));

		return param;
	}

	private void addDoubleElements(Element data, final double[] values) {
		final int length = values.length;
		Element GeoKeyRecord;
		for (int j = 0; j < length; j++) {
			GeoKeyRecord = createDoubleElement(values[j]);
			data.addContent(GeoKeyRecord);
		}
	}
}