/* AbstractGroup.java


	Purpose:
	Description:
	History:
	2001/10/21 16:31:30, Create, Tom M. Yeh.

Copyright (C) 2001 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.idom.impl;

import static org.zkoss.idom.DOMException.HIERARCHY_REQUEST_ERR;
import static org.zkoss.idom.DOMException.NOT_FOUND_ERR;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.zkoss.idom.Binary;
import org.zkoss.idom.CData;
import org.zkoss.idom.Comment;
import org.zkoss.idom.DOMException;
import org.zkoss.idom.Element;
import org.zkoss.idom.EntityReference;
import org.zkoss.idom.Group;
import org.zkoss.idom.Item;
import org.zkoss.idom.ProcessingInstruction;
import org.zkoss.idom.Text;
import org.zkoss.idom.Textual;
import org.zkoss.util.NotableLinkedList;

/**
 * A semi-implemented item for group. A group is a item that has child items.
 *
 * <p>The default implementation of newChildren is for the semantic of
 * Element. A deriving class has to re-implement it, if it is not applicable.
 * Example, Document.
 *
 * @author tomyeh
 * @see Item
 */
public abstract class AbstractGroup extends AbstractItem implements Group {
	/** The list of the children. Never null.
	 */
	protected List<Item> _children;
	
	/** A helper map to enhance the searching speed with tag name.
	 * If any deriving class don't contain this helper map, they should
	 * apply the basic sequential search.
	 */
	private transient ElementMap _elemMap;

	/** Constructor.
	 */
	protected AbstractGroup() {
		_children = newChildren();
	}

	//-- deriving to override --//
	/** Creates a list to hold child vertices.
	 * Note: the list must be able to protect itself from adding
	 * unexpected child -- read-only, wrong type, undetached...
	 *
	 * <p>The default implementation obeys the semantic of Element,
	 * i.e., it doesn't allow any child that cannot be a child of Element.
	 *
	 * <p>For performance issue, we introduced a map to improve the search 
	 * speed for Element node associated with a tag name.
	 */
	protected List<Item> newChildren() {
		return new ChildArray();
	}

	//-- Group --//
	public final List<Item> getChildren() {
		return _children;
	}
	public final List<Item> detachChildren() {
		List<Item> list = new ArrayList<>(_children); //make a copy first

		for (Iterator<Item> it = _children.iterator(); it.hasNext();) {
			it.next();
			it.remove(); //and detach
		}

		return list;
	}
	public final boolean anyElement() {
		if (_elemMap != null)
			return _elemMap.any();

		for (final Item o : _children) {
			if (o instanceof Element)
				return true;
		}
		return false;
	}
	public final Set<String> getElementNames() {
		if (_elemMap != null)
			return _elemMap.names();

		final Set<String> set = new LinkedHashSet<>();
		for (final Iterator<Item> it = _children.iterator(); it.hasNext();) {
			final Object o = it.next();
			if (o instanceof Element)
				set.add(((Element)o).getName());
		}
		return set;
	}
	public final List<Element> getElements() {
		final List<Element> lst = new LinkedList<>();
		for (final Iterator<Item> it = _children.iterator(); it.hasNext();) {
			final Object o = it.next();
			if (o instanceof Element)
				lst.add((Element)o);
		}
		return lst;
	}

	public final int getElementIndex
	(int indexFrom, String namespace, String name, int mode) {
		if (indexFrom < 0 || indexFrom >= _children.size())
			return -1;

		final Pattern ptn =
			(mode & FIND_BY_REGEX) != 0 ? Pattern.compile(name): null;

		final Iterator<Item> it = _children.listIterator(indexFrom);
		for (int j = indexFrom; it.hasNext(); ++j) {
			final Object o = it.next();
			if ((o instanceof Element)
			&& match((Element)o, namespace, name, ptn, mode))
				return j;
		}
		return -1;
	}
	public final int getElementIndex(int indexFrom, String tname) {
		return getElementIndex(indexFrom, null, tname, FIND_BY_TAGNAME);
	}

	public final Element getElement(String namespace, String name, int mode) {
		if (_elemMap != null && namespace == null && mode == FIND_BY_TAGNAME)
			return getElement(name); //use the speed version

		int j = getElementIndex(0, namespace, name, mode);
		if (j >= 0)
			return (Element)_children.get(j);

		if ((mode & FIND_RECURSIVE) != 0) {
			for (Item o : _children) {
				if (o instanceof Group) {
					Element elem = ((Group) o).getElement(namespace, name,
							mode);
					if (elem != null)
						return elem;
				}
			}
		}
		return null;
	}
	public final Element getElement(String tname) {
		if (_elemMap != null)
			return _elemMap.get(tname);

		int j = getElementIndex(0, tname);
		return j >= 0 ? (Element)_children.get(j): null;
	}

	public final List<Element> getElements(String namespace, String name, int mode) {
		if (_elemMap != null && namespace == null && mode == FIND_BY_TAGNAME)
			return getElements(name); //use the speed version

		final Pattern ptn =
			(mode & FIND_BY_REGEX) != 0 ? Pattern.compile(name): null;

		final List<Element> list = new LinkedList<>();
		for (Item item: _children) {
			if (item instanceof Element) {
				final Element e = (Element)item;
				if (match(e, namespace, name, ptn, mode))
					list.add(e);
			}
		}

		if ((mode & FIND_RECURSIVE) != 0) {
			for (Item o : _children) {
				if (o instanceof Group)
					list.addAll(((Group) o).getElements(namespace, name, mode));
			}
		}
		return list;
	}
	public final List<Element> getElements(String tname) {
		if (_elemMap != null)
			return _elemMap.getAll(tname);

		return getElements(null, tname, FIND_BY_TAGNAME);
	}

	public final String getElementValue
	(String namespace, String name, int mode, boolean trim) {
		Element child = getElement(namespace, name, mode);
		return child != null ? child.getText(trim): null;
	}
	public final String getElementValue(String tname, boolean trim) {
		final Element child = getElement(tname);
		return child != null ? child.getText(trim): null;
	}

	public final int coalesce(boolean recursive) {
		int count = 0;
		Item found = null;
		StringBuilder sb = new StringBuilder();
		for (final Iterator<Item> it = _children.iterator(); it.hasNext();) {
			Item o = it.next();
			Item newFound =
				(o instanceof Textual) && ((Textual)o).isCoalesceable() ?
						o : null;

			if (newFound != null && found != null
			&& found.getClass().equals(o.getClass())) {
				if (sb.length() == 0)
					sb.append(found.getText());
				sb.append(o.getText()); //coalesce text
				it.remove(); //remove this node
				++count; //# being coalesced and removed
			} else {
				if (sb.length() > 0) { //coalesced before?
					if (found != null) {
						found.setText(sb.toString());
					}
					sb.setLength(0);
				}
				found = newFound;
			}
		}
		if (sb.length() > 0 && found != null)
			found.setText(sb.toString());

		if (recursive) {
			for (final Item o : _children) {
				if (o instanceof Group)
					count += ((Group) o).coalesce(recursive);
			}
		}
		return count;
	}

	//-- Node --//
	@Override
	public final NodeList getChildNodes() {
		return new FacadeNodeList(_children);
	}

	@Override
	public final Node getFirstChild() {
		return _children.isEmpty() ? null: (Node)_children.get(0);
	}

	@Override
	public final Node getLastChild() {
		int sz = _children.size();
		return sz == 0 ? null: (Node)_children.get(sz - 1);
	}

	@Override
	public final boolean hasChildNodes() {
		return !_children.isEmpty();
	}

	@Override
	public final Node insertBefore(Node newChild, Node refChild) {
		if (refChild == null)
			return appendChild(newChild);

		int j = _children.indexOf((Item) refChild);
		if (j < 0)
			throw new DOMException(NOT_FOUND_ERR, getLocator());
		_children.add(j, (Item)newChild);
		return newChild;
	}

	@Override
	public final Node replaceChild(Node newChild, Node oldChild) {
		int j = _children.indexOf((Item) oldChild);
		if (j < 0)
			throw new DOMException(NOT_FOUND_ERR, getLocator());
		return (Node)_children.set(j, (Item)newChild);
	}

	@Override
	public final Node removeChild(Node oldChild) {
		int j = _children.indexOf((Item) oldChild);
		if (j < 0)
			throw new DOMException(NOT_FOUND_ERR, getLocator());
		return (Node)_children.remove(j);
	}

	@Override
	public final Node appendChild(Node newChild) {
		_children.add((Item)newChild);
		return newChild;
	}

	//Cloneable//
	@Override
	public Object clone() {
		AbstractGroup group = (AbstractGroup)super.clone();

		group._children = group.newChildren();
		for (Item src: _children)
			group._children.add((Item)src.clone());

		if (group._children instanceof ChildArray)
			((ChildArray)group._children).afterClone();
		return group;
	}

	//-- Serializable --//
	//NOTE: they must be declared as private
	private void readObject(java.io.ObjectInputStream s)
	throws java.io.IOException, ClassNotFoundException {
		s.defaultReadObject();

		if (_children instanceof ChildArray)
			((ChildArray)_children).afterUnmarshal();
	}

	//-- ElementMap
	/** Stores a 'cached' map of child elements to speed up the access.
	 */
	protected static class ElementMap {
		/** the map of (String elemName, List of Elements). */
		private final Map<String, List<Element>> _map = new LinkedHashMap<>();
		
		protected ElementMap() {
		}
		
		/**
		 * Put an element into the map.
		 * If the "following" argument is assocaied the same name, we will
		 * add the element before the "following".
		 */
		public final void put(Element e, Element following) {
			final String name = e.getName();
			List<Element> valueList = _map.computeIfAbsent(name,
					k -> new LinkedList<>());

			if (following != null && name.equals(following.getName())) {
				//add into list before the following
				for (ListIterator<Element> it = valueList.listIterator(); it.hasNext();) {
					if (it.next() == following) { //no need to use equals
						it.previous();
						it.add(e);
						return;
					}
				}
			}
			
			valueList.add(e);	//add into list
		}
		/**
		 * Get the element with name. If you have many values associated with
		 * the same key, it returned the head for you.
		 */
		public final Element get(String name) {
			final List<Element> vals = _map.get(name);
			return vals != null && !vals.isEmpty() ? vals.get(0): null;
		}
		/**
		 * Get a readonly list of all elements with name.
		 */
		public final List<Element> getAll(String name) {
			final List<Element> vals = _map.get(name);
			if (vals != null)
				return Collections.unmodifiableList(vals);
			return Collections.emptyList();
		}
		/**
		 * Remove e from the map.
		 */
		public final void remove(Element e) {
			final List<Element> vals = _map.get(e.getName());
			vals.remove(e);
			if (vals.isEmpty())
				_map.remove(e.getName());
		}
		/** Returns true if any element.
		 */
		public final boolean any() {
			return !_map.isEmpty();
		}
		/** Returns a readonly set of names of all elements.
		 */
		public final Set<String> names() {
			return _map.keySet();
		}
		/** Returns the number of elements.
		 */
		public final int size() {
			int sz = 0;
			for (List<Element> lst: _map.values()) {
				sz += lst.size();
			}
			return sz;
		}
	}
	
	//-- ChildArray --//
	/** The array to hold children.
	 */
	protected class ChildArray extends NotableLinkedList<Item> {
		protected ChildArray() {
			_elemMap = new ElementMap();
		}

		/** Called after unmarshalling back the AbstractGroup instance
		 * that owns this object.
		 */
		private void afterUnmarshal() {
			_elemMap = new ElementMap();

			for (final Object o : this) {
				if (o instanceof Element)
					_elemMap.put((Element) o, null);
			}
		}
		/** Called after cloning the AbstractGroup instance that owns this object.
		 */
		private void afterClone() {
			afterUnmarshal();
		}

		//-- CheckableTreeArray --//
		@Override
		protected void onAdd(Item newElement, Item followingElement) {
			checkAdd(newElement, followingElement, false);
		}

		@Override
		protected void onSet(Item newElement, Item replaced) {
			assert(replaced != null);
			checkAdd(newElement, replaced, true);
		}
		private void checkAdd(Item newVal, Item other, boolean replace) {
			//allowed type?
			if (!(newVal instanceof Element) && !(newVal instanceof Text)
			&& !(newVal instanceof CData) && !(newVal instanceof Comment)
			&& !(newVal instanceof EntityReference) && !(newVal instanceof Binary)
			&& !(newVal instanceof ProcessingInstruction))
				throw new DOMException(HIERARCHY_REQUEST_ERR, "Invalid type", getLocator());

			//to be safe, no auto-detach
			final Item newItem = newVal;
			if (newItem.getParent() != null) {
				throw new DOMException(HIERARCHY_REQUEST_ERR,
					"Item, "+newItem.toString()+", owned by "+newItem.getParent()+" "+newItem.getLocator()+"; detach or clone it", getLocator());
			}

			//test whether a graph will be created?
			if (newItem instanceof Group)
				for (Item p = AbstractGroup.this; p != null; p = p.getParent())
					if (p == newItem)
						throw new DOMException(
								HIERARCHY_REQUEST_ERR, "Add to itself", getLocator());

			if (newItem instanceof Element) { //Element put into map, this must be done before replaced
				//try to find the first Element node on the array
				Element eOther;
				if ((other != null) && !(other instanceof Element)) {
					eOther = null;
					boolean bFirstElemFind = false;
					for (Item node : this) {
						if (bFirstElemFind) {
							if (node instanceof Element) {
								eOther = (Element) node;
								break;
							}
						} else if (node == other) {
							bFirstElemFind = true;
						}
					}
				} else {
					eOther = (Element)other;
				}
				_elemMap.put((Element)newItem, eOther);
			}
			
			if (replace)
				onRemove(other);
			newItem.setParent(AbstractGroup.this);
		}

		@Override
		protected void onRemove(Item item) {
			final Item removeItem = item;
			removeItem.setParent(null);

			if (removeItem instanceof Element) //Element remove from map
				_elemMap.remove((Element)removeItem);
		}
	}
}
