/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * 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; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * 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.
 * 
 * $Id$
 */

package net.noderunner.exml;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;

/**
 * This class is useful for validating the contents of elements within XML
 * documents. When element or attribute validation is taking place, various
 * conditions can be checked by passing in state objects. Internally contains a
 * tree of <code>ElementReq</code> objects and a list of
 * <code>AttributeRule</code> objects.
 * <p>
 * 
 * @author Elias Ross
 * @version 1.0
 * @see Dtd
 */
public class ElementRule {
	private ElementReq rootReq;

	private List<AttributeRule> attributeRules;

	private boolean normalized;

	/**
	 * A handle for maintaining state when verifying an element tree.
	 */
	public static final class ElementRuleState {
		private int index;

		private boolean repeat; // current element

		private boolean matched; // a choice was matched

		private boolean done;

		private ElementReq req;

		/**
		 * Constructs a new <code>ElementRuleState</code> instance.
		 */
		public ElementRuleState() {
			clear();
		}

		/**
		 * Recyles this object, so it may be used with other elements.
		 */
		public void clear() {
			index = 0;
			req = null;
			repeat = false;
			done = false;
			matched = false;
		}

		private void incIndex() throws ElementRuleException {
			if (isDone())
				return;
			index++;
			if (req.isChoice() && matched && !req.isStar() && !req.isPlus()) {
				goUp();
				incIndex();
				return;
			}
			if (getIndex() >= req.size()) {
				if (req.isChoice() && !matched)
					throw new ElementRuleException("Choice did not match element", req);
				if (!req.isStar() && !req.isPlus()) {
					goUp();
					incIndex();
				} else {
					// boolean wasChoice = req.isChoice();
					goUp();
					// if (wasChoice && matched)
					// incIndex();
				}
			} else {
				if (req.isSequence()) {
					matched = false;
				}
			}
			repeat = false;
		}

		private int getIndex() {
			return index;
		}

		private boolean isRepeat() {
			return repeat;
		}

		private boolean isMatched() {
			return matched;
		}

		private void setRepeat() {
			repeat = true;
		}

		private void setMatch() throws ElementRuleException {
			matched = true;
			if (!isRepeat() && req.isChoice()) {
				if (!req.isStar() && !req.isPlus()) {
					goUp();
					incIndex();
				} else {
					goUp();
				}
			}
		}

		private void setDone(boolean done) {
			this.done = done;
		}

		private ElementReq getReq() {
			return req;
		}

		private void setReq(ElementReq req) {
			this.req = req;
		}

		private void setIndex(int index) {
			this.index = index;
		}

		/**
		 * Returns a debug string.
		 */
		@Override
		public String toString() {
			return "i=" + getIndex() + " req=" + req + " rp=" + isRepeat()
					+ " m=" + matched + " d=" + isDone();
		}

		private void goUp() throws ElementRuleException {
			ElementReq parent = req.getParent();
			if (parent == null) {
				setDone(true);
			} else {
				setIndex(req.getParentIndex());
				setReq(parent);
				setRepeat();
				setMatch();
			}
		}

		/**
		 * Returns <code>true</code> if the pattern was successfully
		 * completed.
		 */
		private boolean isDone() {
			return done;
		}
	}

	/**
	 * If a bit is set true, that attribute is required.
	 */
	// private BitSet requiredAttributes = new BitSet(128);
	/**
	 * Returns -1 if not found, otherwise index in attributeRules list.
	 */
	private int indexOf(Attribute a) {
		for (int i = 0; i < attributeRules.size(); i++) {
			AttributeRule r = attributeRules.get(i);
			if (r.matches(a))
				return i;
		}
		return -1;
	}

	/**
	 * A handle for maintaining state when verifying an attribute tree.
	 */
	public static final class AttributeRuleState {

		/**
		 * Set of visited attributes. Bits correspond to indicies in the
		 * attributes list.
		 */
		private BitSet visited = new BitSet(32);

		private static final BitSet EMPTY_SET = new BitSet(0);

		/**
		 * Constructs a new <code>AttributeRuleState</code> instance.
		 */
		public AttributeRuleState() {
			clear();
		}

		/**
		 * If this object is to be recycled, call this method to clear its
		 * fields.
		 */
		public void clear() {
			visited.and(EMPTY_SET);
		}

		private boolean encountered(int index) {
			return visited.get(index);
		}

		/**
		 * Checks for duplicate attribute declarations.
		 */
		private void encounterAttribute(int index)
				throws AttributeRuleException {
			if (visited.get(index))
				throw new AttributeRuleException(
						"Duplicate attribute declaration");
			visited.set(index);
		}

		/**
		 * Returns debug information.
		 */
		@Override
		public String toString() {
			return "AttributeRuleState visited=" + visited;
		}
	}

	/**
	 * Constructs a plain <code>ElementRule</code>. Call
	 * <code>allowElement<code/> and <code>allowAttribute</code> with
	 * elements and attributes to allow, otherwise this element will accept
	 * any element or attribute, as well as PCDATA.
	 *
	 * @see #allowElement
	 * @see #allowAttribute
	 * @see ElementReq#setANY
	 */
	public ElementRule() {
		this(new ElementReq(), null);
		rootReq.setANY();
	}

	/**
	 * Constructs an "undeclared" element rule, with attribute rules. This is
	 * useful when scanning DTD's with attribute rules that proceed an element
	 * declaration.
	 */
	public ElementRule(List<AttributeRule> attributeRules) {
		this.rootReq = null;
		this.attributeRules = attributeRules;
		this.normalized = false;
	}

	/**
	 * Constructs an ElementRule.
	 * 
	 * @param req
	 *            an element requirements tree (non-null)
	 * @param attributeRules
	 *            a list of AttributeRule objects; null implying any attribute
	 *            is allowed
	 * @see Element
	 */
	public ElementRule(ElementReq rootReq, List<AttributeRule> attributeRules) {
		if (rootReq == null)
			throw new IllegalArgumentException("Must specify root element");
		this.rootReq = rootReq;
		this.attributeRules = attributeRules;
		this.normalized = false;
	}

	/**
	 * Constructs an ElementRule. As a convience, optionally allows PCDATA
	 * within this element.
	 * 
	 * @see ElementReq
	 */
	public ElementRule(ElementReq rootReq, List<AttributeRule> attributeRules, boolean pcdata) {
		this(rootReq, attributeRules);
		setAllowPCData(pcdata);
	}
	
	/**
	 * Parses a String, such as <code>"(A | B* | C)"</code> and returns a simple
	 * rule.
	 * 
	 * @param s string to parse
	 * @return new rule
	 * @throws XmlException if parsing fails
	 */
	public static ElementRule parse(String s) throws XmlException {
		XmlReader r = new XmlReader(new StringReader(s + " "));
		try {
			return new ElementRule(r.contentspec(), null);
		} catch (IOException e) {
			throw new XmlException(e);
		}
	}

	/**
	 * Returns true if character data is allowed in for this element.
	 * 
	 * @see ElementReq#isPCDATA
	 */
	public boolean isPCDataAllowed() {
		return rootReq.isPCDATA();
	}

	/**
	 * Validates that the given attribute can be added to this element, given a
	 * <code>AttributeRuleState</code>. Returns the rule appropriate for the
	 * given attribute. If no rule was matched, returns null.
	 * 
	 * @throws AttributeRuleException
	 *             if the attribute does not belong in the element or has an
	 *             invalid value
	 */
	public AttributeRule encounterAttribute(Attribute a,
			AttributeRuleState state) throws AttributeRuleException {
		if (attributeRules == null)
			return null;
		int index = indexOf(a);
		if (index == -1)
			throw new AttributeRuleException("Unknown attribute " + a);
		AttributeRule rule = attributeRules.get(index);
		if (!rule.allowedValue(a.getValue()))
			throw new AttributeRuleException(
					"Attribute value not allowed " + a, rule);
		state.encounterAttribute(index);
		return rule;
	}

	/**
	 * Returns true when encountered something, false if to call this method
	 * again.
	 */
	private boolean encounter(Element e, ElementRuleState state)
			throws ElementRuleException
	{
		if (rootReq.isANY())
			return true;
		ElementReq req = state.getReq();
		if (rootReq.size() == 0)
			throw new ElementRuleException("No child elements allowed", req);
		if (req.isSole())
			throw new IllegalStateException("Parse state incorrect " + state);
		if (state.isDone()) {
			state.setDone(false);
			if (req.isPlus() || req.isStar())
				state.setIndex(0);
			else
				throw new ElementRuleException("Reached end of pattern, encountered " + e + " state " + state, req);
		}
		ElementReq child = req.getChild(state.getIndex());
		if (!child.isSole()) {
			// going down the tree here
			ElementReq newchild = child.followChoice(e);
			if (newchild != null) {
				// arrived at either a child sequence or choice
				state.clear();
				state.setIndex(newchild.getParentIndex());
				state.setReq(newchild.getParent());
				return false;
			}
		} else if (child.isElement(e)) {
			if (!child.isStar() && !child.isPlus()) {
				if (req.isChoice()) { // we found it
					if (req.isPlus() || req.isStar()) {
						state.setIndex(0);
					}
				} else {
					state.incIndex();
				}
			} else {
				state.setRepeat();
			}
			state.setMatch();
			return true;
		}
		// couldn't find in subtree or as child
		if (req.isSequence()) {
			if (!child.isQuestion() && !child.isStar()
					&& !(child.isPlus() && state.isMatched()))
			{
				throw new ElementRuleException("Unexpected element " + e + " state " + state, req);
			}
		}
		state.incIndex();
		return false;
	}

	/**
	 * Normalizes the ElementReq tree, which is required for content validation
	 * to work.
	 */
	private void normalize() {
		if (!normalized)
			rootReq.normalize();
	}

	/**
	 * Verifies when encountering an child element, that it may be added. Pass
	 * in a non-null instance of <code>ElementRuleState</code> to be modified.
	 * 
	 * @param e
	 *            the element being encountered
	 * @param state
	 *            the last state posted to this method, or if no initial state
	 *            (this is the first child element encountered), clear it first
	 *            using state.clear()
	 */
	public void encounterElement(Element e, ElementRuleState state)
			throws ElementRuleException {
		normalize();
		if (state.getReq() == null)
			state.setReq(rootReq);
		while (!encounter(e, state))
			;
	}

	private boolean findEnd(ElementRuleState state) throws ElementRuleException {
		ElementReq req = state.getReq();
		// System.out.println("findEnd " + req + " " + state);
		ElementReq child = req.getChild(state.getIndex());
		if (child.isQuestion() || child.isStar()
				|| (child.isPlus() && state.isMatched())) {
			state.incIndex();
			return state.isDone();
		}
		throw new ElementRuleException("Pattern not completed", req);
	}

	/**
	 * Verifies that there are no more elements required to be encountered
	 * before closing the element. Otherwise, throws an exception.
	 * 
	 * @throws ElementRuleException
	 *             if more elements must be visited
	 */
	public void encounterEnd(ElementRuleState state)
			throws ElementRuleException {
		if (rootReq.isPCDATA() || rootReq.isANY() || rootReq.isEmpty())
			return;
		normalize();
		if (state.getReq() == null)
			state.setReq(rootReq);
		if (state.isDone())
			return;
		if (rootReq.isStar() || rootReq.isQuestion())
			return;
		while (!findEnd(state))
			;
	}

	/**
	 * Verifies that there are no more attributes are required within the start
	 * tag. Also, appends any default or undeclared attributes to the given
	 * list.
	 * 
	 * @param attributes
	 *            an existing list instance or null if no attributes were yet
	 *            encountered
	 * @return an appended or a newly constructed list
	 * @throws AttributeRuleException
	 *             if more attributes must be visited
	 */
	public List<Attribute> encounterEnd(AttributeRuleState state, List<Attribute> attributes)
			throws AttributeRuleException {
		if (attributeRules == null)
			return attributes;
		// attributes
		for (int i = 0; i < attributeRules.size(); i++) {
			AttributeRule r = attributeRules.get(i);
			boolean enc = state.encountered(i);
			if (!enc && r.isRequired())
				throw new AttributeRuleException("Required attribute missing", r);
			if (!enc && r.isDefault()) {
				if (attributes == null)
					attributes = new ArrayList<Attribute>();
				attributes.add(new Attribute(r.getName(), r.getValue()));
			}
		}
		return attributes;
	}

	/**
	 * Allows an element to be inside this element.
	 * 
	 * @param e
	 *            allow this element to be added
	 */
	public void allowElement(Element e) {
		if (rootReq.isANY()) {
			rootReq = new ElementReq();
			rootReq.isChoice();
			rootReq.isStar();
		}
		rootReq.add(new ElementReq(e));
	}

	/**
	 * Allows an element to be inside this element.
	 * 
	 * @param name
	 *            allow this named element to be added
	 */
	public void allowElement(String name) {
		if (rootReq.isANY()) {
			rootReq = new ElementReq();
			rootReq.isChoice();
			rootReq.isStar();
		}
		rootReq.add(new ElementReq(name));
	}

	/**
	 * Allows an attribute (CDATA) to belong inside this element.
	 * 
	 * @param a
	 *            the attribute to be allowed in this element
	 * @see AttributeValueType#CDATA
	 */
	public void allowAttribute(Attribute a) {
		if (attributeRules == null)
			attributeRules = new ArrayList<AttributeRule>();
		AttributeRule r = new AttributeRule(AttributeValueType.CDATA);
		r.setName(a.getName());
		attributeRules.add(AttributeRule.cdata(a.getName()));
	}

	/**
	 * Adds an <code>AttributeRule</code> to this element rule, if it does not
	 * already exist.
	 */
	public void addAttributeRule(AttributeRule rule) {
		if (attributeRules == null)
			attributeRules = new ArrayList<AttributeRule>();
		for (int i = 0; i < attributeRules.size(); i++) {
			AttributeRule r = attributeRules.get(i);
			if (r.getName().equals(rule.getName()))
				return;
		}
		attributeRules.add(rule);
	}

	/**
	 * Allows no elements to be inside this element.
	 */
	public void allowNoElements() {
		rootReq = new ElementReq();
	}

	/**
	 * Allows no attributes to be inside this element.
	 */
	public void allowNoAttributes() {
		attributeRules = new ArrayList<AttributeRule>();
	}

	/**
	 * Returns the attribute rules for this element, a list of
	 * <code>AttributeRule</code> instances.
	 * 
	 * @see AttributeRule
	 */
	public List<AttributeRule> getAttributeRules() {
		return attributeRules;
	}

	/**
	 * Sets the root <code>ElementReq</code> for undeclared elements. This
	 * method operates only on previously undeclared elements.
	 * 
	 * @throws ElementRuleException
	 *             if this was constructed with a rootReq
	 */
	public void setRootReq(ElementReq rootReq) throws ElementRuleException {
		if (this.rootReq != null)
			throw new ElementRuleException("Cannot setElementReq", this);
		this.rootReq = rootReq;
	}

	/**
	 * Allows PCDATA.
	 * 
	 * @see ElementReq#setPCDATA
	 */
	public void setAllowPCData(boolean allow) {
		if (allow)
			rootReq.setPCDATA();
	}

	/**
	 * Returns a String representation of this object for debugging.
	 */
	@Override
	public String toString() {
		return "rootReq=" + rootReq + " attr=" + getAttributeRules();
	}

	/**
	 * Formats the <code>ElementReq</code> tree as a DTD-style declaration.
	 * <p>
	 * Example result:
	 * 
	 * <pre>
	 * &lt;!ELEMENT name (a | b+)&gt;
	 * </pre>
	 * 
	 * </p>
	 * 
	 * @param name
	 *            an element name to indicate this rule belonging to
	 */
	public String elementString(String name) {
		return "<!ELEMENT " + name + " " + rootReq + ">";
	}

	/**
	 * Formats the <code>AttributeRule</code> list as a DTD-style declaration.
	 * <p>
	 * Example result:
	 * 
	 * <pre>
	 * &lt;!ATTLIST name att1 #FIXED &quot;value&quot;&gt;
	 * </pre>
	 * 
	 * </p>
	 * 
	 * @param name
	 *            an element name to indicate this rule belonging to
	 */
	public String attlistString(String ename) {
		if (getAttributeRules() == null)
			return "";
		StringBuffer sb = new StringBuffer(64);
		Iterator i = getAttributeRules().iterator();
		while (i.hasNext()) {
			sb.append("<!ATTLIST ").append(ename).append(' ');
			AttributeRule rule = (AttributeRule) i.next();
			sb.append(rule);
			sb.append('>');
			if (i.hasNext())
				sb.append('\n');
		}
		return sb.toString();
	}

	/**
	 * For internal use.
	 */
	ElementReq getRootReq() {
		return rootReq;
	}
}
