// =================================================================================================
// ADOBE SYSTEMS INCORPORATED
// Copyright 2006 Adobe Systems Incorporated
// All Rights Reserved
//
// NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
// of the Adobe license agreement accompanying it.
// =================================================================================================

package com.adobe.xmp.impl;

import java.util.Calendar;
import java.util.Iterator;

import com.adobe.xmp.XMPConst;
import com.adobe.xmp.XMPDateTime;
import com.adobe.xmp.XMPError;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPIterator;
import com.adobe.xmp.XMPMeta;
import com.adobe.xmp.XMPPathFactory;
import com.adobe.xmp.XMPUtils;
import com.adobe.xmp.impl.xpath.XMPPath;
import com.adobe.xmp.impl.xpath.XMPPathParser;
import com.adobe.xmp.options.IteratorOptions;
import com.adobe.xmp.options.ParseOptions;
import com.adobe.xmp.options.PropertyOptions;
import com.adobe.xmp.properties.XMPProperty;


/**
 * Implementation for {@link XMPMeta}.
 * 
 * @since 17.02.2006
 */
public class XMPMetaImpl implements XMPMeta, XMPConst
{
	/** Property values are Strings by default */
	private static final int VALUE_STRING = 0;
	/** */
	private static final int VALUE_BOOLEAN = 1;
	/** */
	private static final int VALUE_INTEGER = 2;
	/** */
	private static final int VALUE_LONG = 3;
	/** */
	private static final int VALUE_DOUBLE = 4;
	/** */
	private static final int VALUE_DATE = 5;
	/** */
	private static final int VALUE_CALENDAR = 6;
	/** */
	private static final int VALUE_BASE64 = 7;

	/** root of the metadata tree */
	private XMPNode tree;
	/** the xpacket processing instructions content */ 
	private String packetHeader = null;
	

	/**
	 * Constructor for an empty metadata object.
	 */
	public XMPMetaImpl()
	{
		// create root node
		tree = new XMPNode(null, null, null);
	}


	/**
	 * Constructor for a cloned metadata tree.
	 * 
	 * @param tree
	 *            an prefilled metadata tree which fulfills all
	 *            <code>XMPNode</code> contracts.
	 */
	public XMPMetaImpl(XMPNode tree)
	{
		this.tree = tree;
	}


	/**
	 * @see XMPMeta#appendArrayItem(String, String, PropertyOptions, String,
	 *      PropertyOptions)
	 */
	public void appendArrayItem(String schemaNS, String arrayName, PropertyOptions arrayOptions,
			String itemValue, PropertyOptions itemOptions) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);

		if (arrayOptions == null)
		{
			arrayOptions = new PropertyOptions();
		}
		if (!arrayOptions.isOnlyArrayOptions())
		{
			throw new XMPException("Only array form flags allowed for arrayOptions",
					XMPError.BADOPTIONS);
		}

		// Check if array options are set correctly.
		arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null);


		// Locate or create the array. If it already exists, make sure the array
		// form from the options
		// parameter is compatible with the current state.
		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);


		// Just lookup, don't try to create.
		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);

		if (arrayNode != null)
		{
			// The array exists, make sure the form is compatible. Zero
			// arrayForm means take what exists.
			if (!arrayNode.getOptions().isArray())
			{
				throw new XMPException("The named property is not an array", XMPError.BADXPATH);
			}
			// if (arrayOptions != null && !arrayOptions.equalArrayTypes(arrayNode.getOptions()))
			// {
			// throw new XMPException("Mismatch of existing and specified array form", BADOPTIONS);
			// }
		}
		else
		{
			// The array does not exist, try to create it.
			if (arrayOptions.isArray())
			{
				arrayNode = XMPNodeUtils.findNode(tree, arrayPath, true, arrayOptions);
				if (arrayNode == null)
				{
					throw new XMPException("Failure creating array node", XMPError.BADXPATH);
				}
			}
			else
			{
				// array options missing
				throw new XMPException("Explicit arrayOptions required to create new array",
						XMPError.BADOPTIONS);
			}
		}

		doSetArrayItem(arrayNode, ARRAY_LAST_ITEM, itemValue, itemOptions, true);
	}

	
	/**
	 * @see XMPMeta#appendArrayItem(String, String, String)
	 */
	public void appendArrayItem(String schemaNS, String arrayName, String itemValue)
			throws XMPException
	{
		appendArrayItem(schemaNS, arrayName, null, itemValue, null);
	}


	/**
	 * @throws XMPException
	 * @see XMPMeta#countArrayItems(String, String)
	 */
	public int countArrayItems(String schemaNS, String arrayName) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);

		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);

		if (arrayNode == null)
		{
			return 0;
		}

		if (arrayNode.getOptions().isArray())
		{
			return arrayNode.getChildrenLength();
		}
		else
		{
			throw new XMPException("The named property is not an array", XMPError.BADXPATH);
		}
	}


	/**
	 * @see XMPMeta#deleteArrayItem(String, String, int)
	 */
	public void deleteArrayItem(String schemaNS, String arrayName, int itemIndex)
	{
		try
		{
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertArrayName(arrayName);

			String itemPath = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex);
			deleteProperty(schemaNS, itemPath);
		}
		catch (XMPException e)
		{
			// EMPTY, exceptions are ignored within delete
		}
	}


	/**
	 * @see XMPMeta#deleteProperty(String, String)
	 */
	public void deleteProperty(String schemaNS, String propName)
	{
		try
		{
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertPropName(propName);

			XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);

			XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);
			if (propNode != null)
			{
				XMPNodeUtils.deleteNode(propNode);
			}
		}
		catch (XMPException e)
		{
			// EMPTY, exceptions are ignored within delete
		}
	}


	/**
	 * @see XMPMeta#deleteQualifier(String, String, String, String)
	 */
	public void deleteQualifier(String schemaNS, String propName, String qualNS, String qualName)
	{
		try
		{
			// Note: qualNS and qualName are checked inside composeQualfierPath
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertPropName(propName);

			String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName);
			deleteProperty(schemaNS, qualPath);
		}
		catch (XMPException e)
		{
			// EMPTY, exceptions within delete are ignored
		}
	}


	/**
	 * @see XMPMeta#deleteStructField(String, String, String, String)
	 */
	public void deleteStructField(String schemaNS, String structName, String fieldNS,
			String fieldName)
	{
		try
		{
			// fieldNS and fieldName are checked inside composeStructFieldPath
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertStructName(structName);

			String fieldPath = structName
					+ XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
			deleteProperty(schemaNS, fieldPath);
		}
		catch (XMPException e)
		{
			// EMPTY, exceptions within delete are ignored
		}
	}


	/**
	 * @see XMPMeta#doesPropertyExist(String, String)
	 */
	public boolean doesPropertyExist(String schemaNS, String propName)
	{
		try
		{
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertPropName(propName);

			XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
			final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);
			return propNode != null;
		}
		catch (XMPException e)
		{
			return false;
		}
	}


	/**
	 * @see XMPMeta#doesArrayItemExist(String, String, int)
	 */
	public boolean doesArrayItemExist(String schemaNS, String arrayName, int itemIndex)
	{
		try
		{
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertArrayName(arrayName);

			String path = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex);
			return doesPropertyExist(schemaNS, path);
		}
		catch (XMPException e)
		{
			return false;
		}
	}


	/**
	 * @see XMPMeta#doesStructFieldExist(String, String, String, String)
	 */
	public boolean doesStructFieldExist(String schemaNS, String structName, String fieldNS,
			String fieldName)
	{
		try
		{
			// fieldNS and fieldName are checked inside composeStructFieldPath()
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertStructName(structName);

			String path = XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
			return doesPropertyExist(schemaNS, structName + path);
		}
		catch (XMPException e)
		{
			return false;
		}
	}


	/**
	 * @see XMPMeta#doesQualifierExist(String, String, String, String)
	 */
	public boolean doesQualifierExist(String schemaNS, String propName, String qualNS,
			String qualName)
	{
		try
		{
			// qualNS and qualName are checked inside composeQualifierPath()
			ParameterAsserts.assertSchemaNS(schemaNS);
			ParameterAsserts.assertPropName(propName);

			String path = XMPPathFactory.composeQualifierPath(qualNS, qualName);
			return doesPropertyExist(schemaNS, propName + path);
		}
		catch (XMPException e)
		{
			return false;
		}
	}


	/**
	 * @see XMPMeta#getArrayItem(String, String, int)
	 */
	public XMPProperty getArrayItem(String schemaNS, String arrayName, int itemIndex)
			throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);

		String itemPath = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex);
		return getProperty(schemaNS, itemPath);
	}


	/**
	 * @throws XMPException
	 * @see XMPMeta#getLocalizedText(String, String, String, String)
	 */
	public XMPProperty getLocalizedText(String schemaNS, String altTextName, String genericLang,
			String specificLang) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(altTextName);
		ParameterAsserts.assertSpecificLang(specificLang);

		genericLang = genericLang != null ? Utils.normalizeLangValue(genericLang) : null;
		specificLang = Utils.normalizeLangValue(specificLang);

		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, altTextName);
		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);
		if (arrayNode == null)
		{
			return null;
		}

		Object[] result = XMPNodeUtils.chooseLocalizedText(arrayNode, genericLang, specificLang);
		int match = ((Integer) result[0]).intValue();
		final XMPNode itemNode = (XMPNode) result[1];

		if (match != XMPNodeUtils.CLT_NO_VALUES)
		{
			return new XMPProperty()
			{
				public String getValue()
				{
					return itemNode.getValue();
				}


				public PropertyOptions getOptions()
				{
					return itemNode.getOptions();
				}


				public String getLanguage()
				{
					return itemNode.getQualifier(1).getValue();
				}


				public String toString()
				{
					return itemNode.getValue().toString();
				}
			};
		}
		else
		{
			return null;
		}
	}


	/**
	 * @see XMPMeta#setLocalizedText(String, String, String, String, String,
	 *      PropertyOptions)
	 */
	public void setLocalizedText(String schemaNS, String altTextName, String genericLang,
			String specificLang, String itemValue, PropertyOptions options) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(altTextName);
		ParameterAsserts.assertSpecificLang(specificLang);

		genericLang = genericLang != null ? Utils.normalizeLangValue(genericLang) : null;
		specificLang = Utils.normalizeLangValue(specificLang);

		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, altTextName);

		// Find the array node and set the options if it was just created.
		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, true, new PropertyOptions(
				PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED
						| PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT));

		if (arrayNode == null)
		{
			throw new XMPException("Failed to find or create array node", XMPError.BADXPATH);
		}
		else if (!arrayNode.getOptions().isArrayAltText())
		{
			if (!arrayNode.hasChildren() && arrayNode.getOptions().isArrayAlternate())
			{
				arrayNode.getOptions().setArrayAltText(true);
			}
			else
			{
				throw new XMPException(
					"Specified property is no alt-text array", XMPError.BADXPATH);
			}
		}

		// Make sure the x-default item, if any, is first.
		boolean haveXDefault = false;
		XMPNode xdItem = null;

		for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
		{
			XMPNode currItem = (XMPNode) it.next();
			if (!currItem.hasQualifier()
					|| !XMPConst.XML_LANG.equals(currItem.getQualifier(1).getName()))
			{
				throw new XMPException("Language qualifier must be first", XMPError.BADXPATH);
			}
			else if (XMPConst.X_DEFAULT.equals(currItem.getQualifier(1).getValue()))
			{
				xdItem = currItem;
				haveXDefault = true;
				break;
			}
		}

		// Moves x-default to the beginning of the array
		if (xdItem != null  &&  arrayNode.getChildrenLength() > 1)
		{
			arrayNode.removeChild(xdItem);
			arrayNode.addChild(1, xdItem);
		}

		// Find the appropriate item.
		// chooseLocalizedText will make sure the array is a language
		// alternative.
		Object[] result = XMPNodeUtils.chooseLocalizedText(arrayNode, genericLang, specificLang);
		int match = ((Integer) result[0]).intValue();
		XMPNode itemNode = (XMPNode) result[1];

		boolean specificXDefault = XMPConst.X_DEFAULT.equals(specificLang);

		switch (match)
		{
		case XMPNodeUtils.CLT_NO_VALUES:

			// Create the array items for the specificLang and x-default, with
			// x-default first.
			XMPNodeUtils.appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue);
			haveXDefault = true;
			if (!specificXDefault)
			{
				XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
			}
			break;

		case XMPNodeUtils.CLT_SPECIFIC_MATCH:

			if (!specificXDefault)
			{
				// Update the specific item, update x-default if it matches the
				// old value.
				if (haveXDefault && xdItem != itemNode && xdItem != null
						&& xdItem.getValue().equals(itemNode.getValue()))
				{
					xdItem.setValue(itemValue);
				}
				// ! Do this after the x-default check!
				itemNode.setValue(itemValue);
			}
			else
			{
				// Update all items whose values match the old x-default value.
				assert  haveXDefault  &&  xdItem == itemNode;
				for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
				{
					XMPNode currItem = (XMPNode) it.next();
					if (currItem == xdItem
							|| !currItem.getValue().equals(
									xdItem != null ? xdItem.getValue() : null))
					{
						continue;
					}
					currItem.setValue(itemValue);
				}
				// And finally do the x-default item.
				if (xdItem != null)
				{	
					xdItem.setValue(itemValue);
				}	
			}
			break;

		case XMPNodeUtils.CLT_SINGLE_GENERIC:

			// Update the generic item, update x-default if it matches the old
			// value.
			if (haveXDefault && xdItem != itemNode && xdItem != null
					&& xdItem.getValue().equals(itemNode.getValue()))
			{
				xdItem.setValue(itemValue);
			}
			itemNode.setValue(itemValue); // ! Do this after
			// the x-default
			// check!
			break;

		case XMPNodeUtils.CLT_MULTIPLE_GENERIC:

			// Create the specific language, ignore x-default.
			XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
			if (specificXDefault)
			{
				haveXDefault = true;
			}
			break;

		case XMPNodeUtils.CLT_XDEFAULT:

			// Create the specific language, update x-default if it was the only
			// item.
			if (xdItem != null  &&  arrayNode.getChildrenLength() == 1)
			{
				xdItem.setValue(itemValue);
			}
			XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
			break;

		case XMPNodeUtils.CLT_FIRST_ITEM:

			// Create the specific language, don't add an x-default item.
			XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
			if (specificXDefault)
			{
				haveXDefault = true;
			}
			break;

		default:
			// does not happen under normal circumstances
			throw new XMPException("Unexpected result from ChooseLocalizedText",
					XMPError.INTERNALFAILURE);

		}

		// Add an x-default at the front if needed.
		if (!haveXDefault && arrayNode.getChildrenLength() == 1)
		{
			XMPNodeUtils.appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue);
		}
	}

	
	/**
	 * @see XMPMeta#setLocalizedText(String, String, String, String, String)
	 */
	public void setLocalizedText(String schemaNS, String altTextName, String genericLang,
			String specificLang, String itemValue) throws XMPException
	{
		setLocalizedText(schemaNS, altTextName, genericLang, specificLang, itemValue, null);
	}
	

	/**
	 * @throws XMPException
	 * @see XMPMeta#getProperty(String, String)
	 */
	public XMPProperty getProperty(String schemaNS, String propName) throws XMPException
	{
		return getProperty(schemaNS, propName, VALUE_STRING);
	}


	/**
	 * Returns a property, but the result value can be requested. It can be one
	 * of {@link XMPMetaImpl#VALUE_STRING}, {@link XMPMetaImpl#VALUE_BOOLEAN},
	 * {@link XMPMetaImpl#VALUE_INTEGER}, {@link XMPMetaImpl#VALUE_LONG},
	 * {@link XMPMetaImpl#VALUE_DOUBLE}, {@link XMPMetaImpl#VALUE_DATE},
	 * {@link XMPMetaImpl#VALUE_CALENDAR}, {@link XMPMetaImpl#VALUE_BASE64}.
	 * 
	 * @see XMPMeta#getProperty(String, String)
	 * @param schemaNS
	 *            a schema namespace
	 * @param propName
	 *            a property name or path
	 * @param valueType
	 *            the type of the value, see VALUE_...
	 * @return Returns an <code>XMPProperty</code>
	 * @throws XMPException
	 *             Collects any exception that occurs.
	 */
	protected XMPProperty getProperty(String schemaNS, String propName, int valueType)
			throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertPropName(propName);

		final XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
		final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);

		if (propNode != null)
		{
			if (valueType != VALUE_STRING && propNode.getOptions().isCompositeProperty())
			{
				throw new XMPException("Property must be simple when a value type is requested",
						XMPError.BADXPATH);
			}

			final Object value = evaluateNodeValue(valueType, propNode);

			return new XMPProperty()
			{
				public String getValue()
				{
					return value != null ? value.toString() : null;
				}


				public PropertyOptions getOptions()
				{
					return propNode.getOptions();
				}


				public String getLanguage()
				{
					return null;
				}


				public String toString()
				{
					return value.toString();
				}
			};
		}
		else
		{
			return null;
		}
	}


	/**
	 * Returns a property, but the result value can be requested.
	 * 
	 * @see XMPMeta#getProperty(String, String)
	 * @param schemaNS
	 *            a schema namespace
	 * @param propName
	 *            a property name or path
	 * @param valueType
	 *            the type of the value, see VALUE_...
	 * @return Returns the node value as an object according to the
	 *         <code>valueType</code>.
	 * @throws XMPException
	 *             Collects any exception that occurs.
	 */
	protected Object getPropertyObject(String schemaNS, String propName, int valueType)
			throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertPropName(propName);

		final XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
		final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);

		if (propNode != null)
		{
			if (valueType != VALUE_STRING && propNode.getOptions().isCompositeProperty())
			{
				throw new XMPException("Property must be simple when a value type is requested",
						XMPError.BADXPATH);
			}

			return evaluateNodeValue(valueType, propNode);
		}
		else
		{
			return null;
		}
	}


	/**
	 * @see XMPMeta#getPropertyBoolean(String, String)
	 */
	public Boolean getPropertyBoolean(String schemaNS, String propName) throws XMPException
	{
		return (Boolean) getPropertyObject(schemaNS, propName, VALUE_BOOLEAN);
	}


	/**
	 * @throws XMPException
	 * @see XMPMeta#setPropertyBoolean(String, String, boolean, PropertyOptions)
	 */
	public void setPropertyBoolean(String schemaNS, String propName, boolean propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, propValue ? TRUESTR : FALSESTR, options);
	}

	
	/**
	 * @see XMPMeta#setPropertyBoolean(String, String, boolean)
	 */
	public void setPropertyBoolean(String schemaNS, String propName, boolean propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, propValue ? TRUESTR : FALSESTR, null);
	}
	

	/**
	 * @see XMPMeta#getPropertyInteger(String, String)
	 */
	public Integer getPropertyInteger(String schemaNS, String propName) throws XMPException
	{
		return (Integer) getPropertyObject(schemaNS, propName, VALUE_INTEGER);
	}


	/**
	 * @see XMPMeta#setPropertyInteger(String, String, int, PropertyOptions)
	 */
	public void setPropertyInteger(String schemaNS, String propName, int propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, new Integer(propValue), options);
	}

	
	/**
	 * @see XMPMeta#setPropertyInteger(String, String, int)
	 */
	public void setPropertyInteger(String schemaNS, String propName, int propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, new Integer(propValue), null);
	}
	

	/**
	 * @see XMPMeta#getPropertyLong(String, String)
	 */
	public Long getPropertyLong(String schemaNS, String propName) throws XMPException
	{
		return (Long) getPropertyObject(schemaNS, propName, VALUE_LONG);
	}


	/**
	 * @see XMPMeta#setPropertyLong(String, String, long, PropertyOptions)
	 */
	public void setPropertyLong(String schemaNS, String propName, long propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, new Long(propValue), options);
	}


	/**
	 * @see XMPMeta#setPropertyLong(String, String, long)
	 */
	public void setPropertyLong(String schemaNS, String propName, long propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, new Long(propValue), null);
	}
	

	/**
	 * @see XMPMeta#getPropertyDouble(String, String)
	 */
	public Double getPropertyDouble(String schemaNS, String propName) throws XMPException
	{
		return (Double) getPropertyObject(schemaNS, propName, VALUE_DOUBLE);
	}


	/**
	 * @see XMPMeta#setPropertyDouble(String, String, double, PropertyOptions)
	 */
	public void setPropertyDouble(String schemaNS, String propName, double propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, new Double(propValue), options);
	}

	
	/**
	 * @see XMPMeta#setPropertyDouble(String, String, double)
	 */
	public void setPropertyDouble(String schemaNS, String propName, double propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, new Double(propValue), null);
	}
	

	/**
	 * @see XMPMeta#getPropertyDate(String, String)
	 */
	public XMPDateTime getPropertyDate(String schemaNS, String propName) throws XMPException
	{
		return (XMPDateTime) getPropertyObject(schemaNS, propName, VALUE_DATE);
	}


	/**
	 * @see XMPMeta#setPropertyDate(String, String, XMPDateTime,
	 *      PropertyOptions)
	 */
	public void setPropertyDate(String schemaNS, String propName, XMPDateTime propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, propValue, options);
	}

	
	/**
	 * @see XMPMeta#setPropertyDate(String, String, XMPDateTime)
	 */
	public void setPropertyDate(String schemaNS, String propName, XMPDateTime propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, propValue, null);
	}
	

	/**
	 * @see XMPMeta#getPropertyCalendar(String, String)
	 */
	public Calendar getPropertyCalendar(String schemaNS, String propName) throws XMPException
	{
		return (Calendar) getPropertyObject(schemaNS, propName, VALUE_CALENDAR);
	}


	/**
	 * @see XMPMeta#setPropertyCalendar(String, String, Calendar,
	 *      PropertyOptions)
	 */
	public void setPropertyCalendar(String schemaNS, String propName, Calendar propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, propValue, options);
	}

	
	/**
	 * @see XMPMeta#setPropertyCalendar(String, String, Calendar)
	 */
	public void setPropertyCalendar(String schemaNS, String propName, Calendar propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, propValue, null);
	}
	

	/**
	 * @see XMPMeta#getPropertyBase64(String, String)
	 */
	public byte[] getPropertyBase64(String schemaNS, String propName) throws XMPException
	{
		return (byte[]) getPropertyObject(schemaNS, propName, VALUE_BASE64);
	}


	/**
	 * @see XMPMeta#getPropertyString(String, String)
	 */
	public String getPropertyString(String schemaNS, String propName) throws XMPException
	{
		return (String) getPropertyObject(schemaNS, propName, VALUE_STRING);
	}


	/**
	 * @see XMPMeta#setPropertyBase64(String, String, byte[], PropertyOptions)
	 */
	public void setPropertyBase64(String schemaNS, String propName, byte[] propValue,
			PropertyOptions options) throws XMPException
	{
		setProperty(schemaNS, propName, propValue, options);
	}

	
	/**
	 * @see XMPMeta#setPropertyBase64(String, String, byte[])
	 */
	public void setPropertyBase64(String schemaNS, String propName, byte[] propValue)
			throws XMPException
	{
		setProperty(schemaNS, propName, propValue, null);
	}
	

	/**
	 * @throws XMPException
	 * @see XMPMeta#getQualifier(String, String, String, String)
	 */
	public XMPProperty getQualifier(String schemaNS, String propName, String qualNS, 
		String qualName) throws XMPException
	{
		// qualNS and qualName are checked inside composeQualfierPath
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertPropName(propName);

		String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName);
		return getProperty(schemaNS, qualPath);
	}


	/**
	 * @see XMPMeta#getStructField(String, String, String, String)
	 */
	public XMPProperty getStructField(String schemaNS, String structName, String fieldNS,
			String fieldName) throws XMPException
	{
		// fieldNS and fieldName are checked inside composeStructFieldPath
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertStructName(structName);

		String fieldPath = structName + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
		return getProperty(schemaNS, fieldPath);
	}


	/**
	 * @throws XMPException
	 * @see XMPMeta#iterator()
	 */
	public XMPIterator iterator() throws XMPException
	{
		return iterator(null, null, null);
	}


	/**
	 * @see XMPMeta#iterator(IteratorOptions)
	 */
	public XMPIterator iterator(IteratorOptions options) throws XMPException
	{
		return iterator(null, null, options);
	}


	/**
	 * @see XMPMeta#iterator(String, String, IteratorOptions)
	 */
	public XMPIterator iterator(String schemaNS, String propName, IteratorOptions options)
			throws XMPException
	{
		return new XMPIteratorImpl(this, schemaNS, propName, options);
	}


	/**
	 * @throws XMPException
	 * @see XMPMeta#setArrayItem(String, String, int, String, PropertyOptions)
	 */
	public void setArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue,
			PropertyOptions options) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);

		// Just lookup, don't try to create.
		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);

		if (arrayNode != null)
		{
			doSetArrayItem(arrayNode, itemIndex, itemValue, options, false);
		}
		else
		{
			throw new XMPException("Specified array does not exist", XMPError.BADXPATH);
		}
	}

	
	/**
	 * @see XMPMeta#setArrayItem(String, String, int, String)
	 */
	public void setArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue)
			throws XMPException
	{
		setArrayItem(schemaNS, arrayName, itemIndex, itemValue, null);
	}
	

	/**
	 * @throws XMPException
	 * @see XMPMeta#insertArrayItem(String, String, int, String,
	 *      PropertyOptions)
	 */
	public void insertArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue,
			PropertyOptions options) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);

		// Just lookup, don't try to create.
		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);

		if (arrayNode != null)
		{
			doSetArrayItem(arrayNode, itemIndex, itemValue, options, true);
		}
		else
		{
			throw new XMPException("Specified array does not exist", XMPError.BADXPATH);
		}
	}

	
	/**
	 * @see XMPMeta#insertArrayItem(String, String, int, String)
	 */
	public void insertArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue)
			throws XMPException
	{
		insertArrayItem(schemaNS, arrayName, itemIndex, itemValue, null);
	}
	

	/**
	 * @throws XMPException
	 * @see XMPMeta#setProperty(String, String, Object, PropertyOptions)
	 */
	public void setProperty(String schemaNS, String propName, Object propValue,
			PropertyOptions options) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertPropName(propName);

		options = XMPNodeUtils.verifySetOptions(options, propValue);

		XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);

		XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, true, options);
		if (propNode != null)
		{
			setNode(propNode, propValue, options, false);
		}
		else
		{
			throw new XMPException("Specified property does not exist", XMPError.BADXPATH);
		}
	}

	
	/**
	 * @see XMPMeta#setProperty(String, String, Object)
	 */
	public void setProperty(String schemaNS, String propName, Object propValue) throws XMPException
	{
		setProperty(schemaNS, propName, propValue, null);
	}
	

	/**
	 * @throws XMPException
	 * @see XMPMeta#setQualifier(String, String, String, String, String,
	 *      PropertyOptions)
	 */
	public void setQualifier(String schemaNS, String propName, String qualNS, String qualName,
			String qualValue, PropertyOptions options) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertPropName(propName);

		if (!doesPropertyExist(schemaNS, propName))
		{
			throw new XMPException("Specified property does not exist!", XMPError.BADXPATH);
		}

		String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName);
		setProperty(schemaNS, qualPath, qualValue, options);
	}

	
	/**
	 * @see XMPMeta#setQualifier(String, String, String, String, String)
	 */
	public void setQualifier(String schemaNS, String propName, String qualNS, String qualName,
			String qualValue) throws XMPException
	{
		setQualifier(schemaNS, propName, qualNS, qualName, qualValue, null);
		
	}
	

	/**
	 * @see XMPMeta#setStructField(String, String, String, String, String,
	 *      PropertyOptions)
	 */
	public void setStructField(String schemaNS, String structName, String fieldNS,
			String fieldName, String fieldValue, PropertyOptions options) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertStructName(structName);

		String fieldPath = structName + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
		setProperty(schemaNS, fieldPath, fieldValue, options);
	}

	
	/**
	 * @see XMPMeta#setStructField(String, String, String, String, String)
	 */
	public void setStructField(String schemaNS, String structName, String fieldNS,
			String fieldName, String fieldValue) throws XMPException
	{
		setStructField(schemaNS, structName, fieldNS, fieldName, fieldValue, null);
	}
	

	/**
	 * @see XMPMeta#getObjectName()
	 */
	public String getObjectName()
	{
		return tree.getName() != null ? tree.getName() : "";
	}

	
	/**
	 * @see XMPMeta#setObjectName(String)
	 */
	public void setObjectName(String name)
	{
		tree.setName(name);
	}

	
	/**
	 * @see XMPMeta#getPacketHeader()
	 */
	public String getPacketHeader()
	{
		return packetHeader;
	}

	
	/**
	 * Sets the packetHeader attributes, only used by the parser.
	 * @param packetHeader the processing instruction content
	 */
	public void setPacketHeader(String packetHeader)
	{
		this.packetHeader = packetHeader;
	}
	
	
	/**
	 * Performs a deep clone of the XMPMeta-object
	 * 
	 * @see java.lang.Object#clone()
	 */
	public Object clone()
	{
		XMPNode clonedTree = (XMPNode) tree.clone();
		return new XMPMetaImpl(clonedTree);
	}


	/**
	 * @see XMPMeta#dumpObject()
	 */
	public String dumpObject()
	{
		// renders tree recursively
		return getRoot().dumpNode(true);
	}

	
	/**
	 * @see XMPMeta#sort()
	 */
	public void sort()
	{
		this.tree.sort();
	}


	/**
	 * @see XMPMeta#normalize(ParseOptions)
	 */
	public void normalize(ParseOptions options) throws XMPException
	{
		if (options == null)
		{
			options = new ParseOptions();
		}
		XMPNormalizer.process(this, options);		
	}

	
	/**
	 * @return Returns the root node of the XMP tree.
	 */
	public XMPNode getRoot()
	{
		return tree;
	}



	// -------------------------------------------------------------------------------------
	// private


	/**
	 * Locate or create the item node and set the value. Note the index
	 * parameter is one-based! The index can be in the range [1..size + 1] or
	 * "last()", normalize it and check the insert flags. The order of the
	 * normalization checks is important. If the array is empty we end up with
	 * an index and location to set item size + 1.
	 * 
	 * @param arrayNode an array node
	 * @param itemIndex the index where to insert the item
	 * @param itemValue the item value
	 * @param itemOptions the options for the new item
	 * @param insert insert oder overwrite at index position?
	 * @throws XMPException
	 */
	private void doSetArrayItem(XMPNode arrayNode, int itemIndex, String itemValue,
			PropertyOptions itemOptions, boolean insert) throws XMPException
	{
		XMPNode itemNode = new XMPNode(ARRAY_ITEM_NAME, null);
		itemOptions = XMPNodeUtils.verifySetOptions(itemOptions, itemValue);

		// in insert mode the index after the last is allowed,
		// even ARRAY_LAST_ITEM points to the index *after* the last.
		int maxIndex = insert ? arrayNode.getChildrenLength() + 1 : arrayNode.getChildrenLength();
		if (itemIndex == ARRAY_LAST_ITEM)
		{
			itemIndex = maxIndex;
		}

		if (1 <= itemIndex && itemIndex <= maxIndex)
		{
			if (!insert)
			{
				arrayNode.removeChild(itemIndex);
			}
			arrayNode.addChild(itemIndex, itemNode);
			setNode(itemNode, itemValue, itemOptions, false);
		}
		else
		{
			throw new XMPException("Array index out of bounds", XMPError.BADINDEX);
		}
	}


	/**
	 * The internals for setProperty() and related calls, used after the node is
	 * found or created.
	 * 
	 * @param node
	 *            the newly created node
	 * @param value
	 *            the node value, can be <code>null</code>
	 * @param newOptions
	 *            options for the new node, must not be <code>null</code>.
	 * @param deleteExisting flag if the existing value is to be overwritten 
	 * @throws XMPException thrown if options and value do not correspond
	 */
	void setNode(XMPNode node, Object value, PropertyOptions newOptions, boolean deleteExisting)
			throws XMPException
	{
		if (deleteExisting)
		{
			node.clear();
		}

		// its checked by setOptions(), if the merged result is a valid options set
		node.getOptions().mergeWith(newOptions);

		if (!node.getOptions().isCompositeProperty())
		{
			// This is setting the value of a leaf node.
			XMPNodeUtils.setNodeValue(node, value);
		}
		else
		{
			if (value != null && value.toString().length() > 0)
			{
				throw new XMPException("Composite nodes can't have values", XMPError.BADXPATH);
			}

			node.removeChildren();
		}

	}


	/**
	 * Evaluates a raw node value to the given value type, apply special
	 * conversions for defined types in XMP.
	 * 
	 * @param valueType
	 *            an int indicating the value type
	 * @param propNode
	 *            the node containing the value
	 * @return Returns a literal value for the node.
	 * @throws XMPException
	 */
	private Object evaluateNodeValue(int valueType, final XMPNode propNode) throws XMPException
	{
		final Object value;
		String rawValue = propNode.getValue();
		switch (valueType)
		{
		case VALUE_BOOLEAN:
			value = new Boolean(XMPUtils.convertToBoolean(rawValue));
			break;
		case VALUE_INTEGER:
			value = new Integer(XMPUtils.convertToInteger(rawValue));
			break;
		case VALUE_LONG:
			value = new Long(XMPUtils.convertToLong(rawValue));
			break;
		case VALUE_DOUBLE:
			value = new Double(XMPUtils.convertToDouble(rawValue));
			break;
		case VALUE_DATE:
			value = XMPUtils.convertToDate(rawValue);
			break;
		case VALUE_CALENDAR:
			XMPDateTime dt = XMPUtils.convertToDate(rawValue);
			value = dt.getCalendar();
			break;
		case VALUE_BASE64:
			value = XMPUtils.decodeBase64(rawValue);
			break;
		case VALUE_STRING:
		default:
			// leaf values return empty string instead of null
			// for the other cases the converter methods provides a "null"
			// value.
			// a default value can only occur if this method is made public.
			value = rawValue != null || propNode.getOptions().isCompositeProperty() ? rawValue : "";
			break;
		}
		return value;
	}
}
