// =================================================================================================
// 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.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;

import com.adobe.xmp.XMPError;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPIterator;
import com.adobe.xmp.XMPMetaFactory;
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.PropertyOptions;
import com.adobe.xmp.properties.XMPPropertyInfo;


/**
 * The <code>XMPIterator</code> implementation.
 * Iterates the XMP Tree according to a set of options.
 * During the iteration the XMPMeta-object must not be changed.
 * Calls to <code>skipSubtree()</code> / <code>skipSiblings()</code> will affect the iteration.
 *  
 * @since   29.06.2006
 */
public class XMPIteratorImpl implements XMPIterator
{
	/** stores the iterator options */
	private IteratorOptions options;
	/** the base namespace of the property path, will be changed during the iteration */ 
	private String baseNS = null;
	/** flag to indicate that skipSiblings() has been called. */
	protected boolean skipSiblings = false;
	/** flag to indicate that skipSiblings() has been called. */
	protected boolean skipSubtree = false;
	/** the node iterator doing the work */
	private Iterator nodeIterator = null;
	
	
	/**
	 * Constructor with optionsl initial values. If <code>propName</code> is provided, 
	 * <code>schemaNS</code> has also be provided.
	 * @param xmp the iterated metadata object.
	 * @param schemaNS the iteration is reduced to this schema (optional) 
	 * @param propPath the iteration is redurce to this property within the <code>schemaNS</code>
	 * @param options advanced iteration options, see {@link IteratorOptions}
	 * @throws XMPException If the node defined by the paramters is not existing. 
	 */
	public XMPIteratorImpl(XMPMetaImpl xmp, String schemaNS, String propPath,
			IteratorOptions options) throws XMPException
	{
		// make sure that options is defined at least with defaults
		this.options = options != null ? options : new IteratorOptions();
		
		// the start node of the iteration depending on the schema and property filter
		XMPNode startNode = null;
		String initialPath = null;
		boolean baseSchema = schemaNS != null  &&  schemaNS.length() > 0; 
		boolean baseProperty = propPath != null  &&  propPath.length() > 0; 
		
		if (!baseSchema  &&  !baseProperty)
		{
			// complete tree will be iterated
			startNode = xmp.getRoot();
		}
		else if (baseSchema  &&  baseProperty)
		{
			// Schema and property node provided
			XMPPath path = XMPPathParser.expandXPath(schemaNS, propPath);
			
			// base path is the prop path without the property leaf
			XMPPath basePath = new XMPPath();
			for (int i = 0; i < path.size() - 1; i++)
			{
				basePath.add(path.getSegment(i));
			}
			
			startNode = XMPNodeUtils.findNode(xmp.getRoot(), path, false, null);
			baseNS = schemaNS;
			initialPath = basePath.toString();
		}
		else if (baseSchema  &&  !baseProperty)
		{
			// Only Schema provided
			startNode = XMPNodeUtils.findSchemaNode(xmp.getRoot(), schemaNS, false);
		}
		else // !baseSchema  &&  baseProperty
		{
			// No schema but property provided -> error
			throw new XMPException("Schema namespace URI is required", XMPError.BADSCHEMA);
		}			

		
		// create iterator
		if (startNode != null)
		{
			if (!this.options.isJustChildren())
			{	
				nodeIterator = new NodeIterator(startNode, initialPath, 1);
			}
			else
			{
				nodeIterator = new NodeIteratorChildren(startNode, initialPath);
			}
		}
		else
		{
			// create null iterator
			nodeIterator = Collections.EMPTY_LIST.iterator();
		}
	}


	/**
	 * @see XMPIterator#skipSubtree()
	 */
	public void skipSubtree()
	{
		this.skipSubtree = true;
	}

	
	/**
	 * @see XMPIterator#skipSiblings()
	 */
	public void skipSiblings()
	{
		skipSubtree();
		this.skipSiblings = true;
	}
	

	/**
	 * @see java.util.Iterator#hasNext()
	 */
	public boolean hasNext()
	{
		return nodeIterator.hasNext();
	}

	
	/**
	 * @see java.util.Iterator#next()
	 */
	public Object next()
	{
		return nodeIterator.next();
	}

	
	/**
	 * @see java.util.Iterator#remove()
	 */
	public void remove()
	{
		throw new UnsupportedOperationException("The XMPIterator does not support remove().");
	}
	
	
	/**
	 * @return Exposes the options for inner class.
	 */
	protected IteratorOptions getOptions()
	{
		return options;
	}

	
	/**
	 * @return Exposes the options for inner class.
	 */
	protected String getBaseNS()
	{
		return baseNS;
	}

	
	/**
	 * @param baseNS sets the baseNS from the inner class.
	 */
	protected void setBaseNS(String baseNS)
	{
		this.baseNS = baseNS;
	}
	
	
	
	
	
	
	/**
	 * The <code>XMPIterator</code> implementation.
	 * It first returns the node itself, then recursivly the children and qualifier of the node.
	 * 
	 * @since   29.06.2006
	 */
	private class NodeIterator implements Iterator
	{
		/** iteration state */
		protected static final int ITERATE_NODE = 0;
		/** iteration state */
		protected static final int ITERATE_CHILDREN = 1;
		/** iteration state */
		protected static final int ITERATE_QUALIFIER = 2;
		
		/** the state of the iteration */
		private int state = ITERATE_NODE; 
		/** the currently visited node */
		private XMPNode visitedNode;
		/** the recursively accumulated path */
		private String path;
		/** the iterator that goes through the children and qualifier list */
		private Iterator childrenIterator = null;
		/** index of node with parent, only interesting for arrays */
		private int index = 0;
		/** the iterator for each child */
		private Iterator subIterator = Collections.EMPTY_LIST.iterator();
		/** the cached <code>PropertyInfo</code> to return */
		private XMPPropertyInfo returnProperty = null;

		
		/**
		 * Default constructor
		 */
		public NodeIterator()
		{
			// EMPTY
		}
		
		
		/**
		 * Constructor for the node iterator.
		 * @param visitedNode the currently visited node
		 * @param parentPath the accumulated path of the node
		 * @param index the index within the parent node (only for arrays)
		 */
		public NodeIterator(XMPNode visitedNode, String parentPath, int index)
		{
			this.visitedNode = visitedNode;
			this.state = NodeIterator.ITERATE_NODE;
			if (visitedNode.getOptions().isSchemaNode())
			{	
				setBaseNS(visitedNode.getName());
			}

			// for all but the root node and schema nodes
			path = accumulatePath(visitedNode, parentPath, index);
		}

		
		/**
		 * Prepares the next node to return if not already done. 
		 * 
		 * @see Iterator#hasNext()
		 */
		public boolean hasNext()
		{
			if (returnProperty != null)
			{
				// hasNext has been called before
				return true;
			}
			
			// find next node
			if (state == ITERATE_NODE)
			{
				return reportNode();
			}
			else if (state == ITERATE_CHILDREN)
			{
				if (childrenIterator == null)
				{
					childrenIterator = visitedNode.iterateChildren();
				}
				
				boolean hasNext = iterateChildren(childrenIterator);
				
				if (!hasNext  &&  visitedNode.hasQualifier()  &&  !getOptions().isOmitQualifiers()) 
				{
					state = ITERATE_QUALIFIER;
					childrenIterator = null;
					hasNext = hasNext();
				}
				return hasNext;
			}
			else
			{
				if (childrenIterator == null)
				{
					childrenIterator = visitedNode.iterateQualifier();
				}
				
				return iterateChildren(childrenIterator);
			}
		}


		/**
		 * Sets the returnProperty as next item or recurses into <code>hasNext()</code>.
		 * @return Returns if there is a next item to return. 
		 */
		protected boolean reportNode()
		{
			state = ITERATE_CHILDREN;
			if (visitedNode.getParent() != null  &&
				(!getOptions().isJustLeafnodes()  ||  !visitedNode.hasChildren()))
			{	
				returnProperty = createPropertyInfo(visitedNode, getBaseNS(), path);
				return true;
			}
			else
			{
				return hasNext();
			}
		}


		/**
		 * Handles the iteration of the children or qualfier
		 * @param iterator an iterator
		 * @return Returns if there are more elements available.
		 */
		private boolean iterateChildren(Iterator iterator)
		{
			if (skipSiblings)
			{
				// setSkipSiblings(false);
				skipSiblings = false;
				subIterator = Collections.EMPTY_LIST.iterator();
			}
			
			// create sub iterator for every child,
			// if its the first child visited or the former child is finished 
			if ((!subIterator.hasNext())  &&  iterator.hasNext())
			{
				XMPNode child = (XMPNode) iterator.next();
				index++;
				subIterator = new NodeIterator(child, path, index);
			}
			
			if (subIterator.hasNext())
			{
				returnProperty = (XMPPropertyInfo) subIterator.next();
				return true;
			}
			else
			{
				return false;
			}
		}
	
		
		/**
		 * Calls hasNext() and returnes the prepared node. Afterwards its set to null.
		 * The existance of returnProperty indicates if there is a next node, otherwise
		 * an exceptio is thrown.
		 * 
		 * @see Iterator#next()
		 */
		public Object next()
		{
			if (hasNext())
			{
				XMPPropertyInfo result = returnProperty; 
				returnProperty = null;
				return result;
			}
			else
			{
				throw new NoSuchElementException("There are no more nodes to return");
			}
		}
	
		
		/**
		 * Not supported.
		 * @see Iterator#remove()
		 */
		public void remove()
		{
			throw new UnsupportedOperationException();
		}
		
		
		/**
		 * @param currNode the node that will be added to the path.
		 * @param parentPath the path up to this node.
		 * @param currentIndex the current array index if an arrey is traversed
		 * @return Returns the updated path.
		 */
		protected String accumulatePath(XMPNode currNode, String parentPath, int currentIndex)
		{
			String separator;
			String segmentName;
			if (currNode.getParent() == null  ||  currNode.getOptions().isSchemaNode())
			{
				return null;
			}
			else if (currNode.getParent().getOptions().isArray())
			{
				separator = "";
				segmentName = "[" + String.valueOf(currentIndex) + "]";
			}
			else
			{	
				separator = "/";
				segmentName = currNode.getName();
			}
			
			
			if (parentPath == null  ||  parentPath.length() == 0)
			{
				return segmentName;
			}
			else if (getOptions().isJustLeafname())
			{
				return !segmentName.startsWith("?") ? 
					segmentName :
					segmentName.substring(1); // qualifier
			}
			else 
			{
				return parentPath + separator + segmentName;
			}
		}
			
		
		/**
		 * Creates a property info object from an <code>XMPNode</code>.
		 * @param node an <code>XMPNode</code>
		 * @param baseNS the base namespace to report
		 * @param path the full property path
		 * @return Returns a <code>XMPProperty</code>-object that serves representation of the node.
		 */
		protected XMPPropertyInfo createPropertyInfo(final XMPNode node, final String baseNS,
				final String path)
		{
			final String value = node.getOptions().isSchemaNode() ? null : node.getValue();
			
			return new XMPPropertyInfo()
			{
				public String getNamespace()
				{
					if (!node.getOptions().isSchemaNode())
					{	
						// determine namespace of leaf node
						QName qname = new QName(node.getName());
						return XMPMetaFactory.getSchemaRegistry().getNamespaceURI(qname.getPrefix());
					}
					else
					{
						return baseNS;
					}	
				}
	
				public String getPath()
				{
					return path;
				}
				
				public String getValue()
				{
					return value;
				}
	
				public PropertyOptions getOptions()
				{
					return node.getOptions();
				}

				public String getLanguage()
				{
					// the language is not reported
					return null;
				}
			};
		}


		/**
		 * @return the childrenIterator
		 */
		protected  Iterator getChildrenIterator()
		{
			return childrenIterator;
		}


		/**
		 * @param childrenIterator the childrenIterator to set
		 */
		protected void setChildrenIterator(Iterator childrenIterator)
		{
			this.childrenIterator = childrenIterator;
		}

		
		/**
		 * @return Returns the returnProperty.
		 */
		protected XMPPropertyInfo getReturnProperty()
		{
			return returnProperty;
		}
		

		/**
		 * @param returnProperty the returnProperty to set
		 */
		protected  void setReturnProperty(XMPPropertyInfo returnProperty)
		{
			this.returnProperty = returnProperty;
		}
	}
	
	
	/**
	 * This iterator is derived from the default <code>NodeIterator</code>,
	 * and is only used for the option {@link IteratorOptions#JUST_CHILDREN}.
	 * 
	 * @since 02.10.2006
	 */
	private class NodeIteratorChildren extends NodeIterator
	{
		/** */
		private String parentPath;
		/** */
		private Iterator childrenIterator;
		/** */
		private int index = 0;
		
		
		/**
		 * Constructor 
		 * @param parentNode the node which children shall be iterated. 
		 * @param parentPath the full path of the former node without the leaf node.
		 */
		public NodeIteratorChildren(XMPNode parentNode, String parentPath)
		{
			if (parentNode.getOptions().isSchemaNode())
			{	
				setBaseNS(parentNode.getName());
			}
			this.parentPath = accumulatePath(parentNode, parentPath, 1);

			childrenIterator = parentNode.iterateChildren();
		}
		
		
		/**
		 * Prepares the next node to return if not already done. 
		 * 
		 * @see Iterator#hasNext()
		 */
		public boolean hasNext()
		{
			if (getReturnProperty() != null)
			{
				// hasNext has been called before
				return true;
			}
			else if (skipSiblings)
			{
				return false;
			}
			else if (childrenIterator.hasNext())
			{
				XMPNode child = (XMPNode) childrenIterator.next();
				index++;
				
				String path = null;
				if (child.getOptions().isSchemaNode())
				{	
					setBaseNS(child.getName());
				}
				else if (child.getParent() != null)
				{
					// for all but the root node and schema nodes
					path = accumulatePath(child, parentPath, index);
				}

				// report next property, skip not-leaf nodes in case options is set
				if (!getOptions().isJustLeafnodes()  ||  !child.hasChildren())
				{	
					setReturnProperty(createPropertyInfo(child, getBaseNS(), path));
					return true;
				}
				else
				{
					return hasNext();
				}
			}
			else
			{
				return false;
			}
		}		
	}
}