/*******************************************************************************
 * Copyright (c) 2002, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     David Carver (STAR) - bug 297005 - Some static constants not made final.
 *******************************************************************************/
package org.eclipse.wst.xml.core.internal.contentmodel.internal.util;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.Vector;
import org.eclipse.wst.xml.core.internal.contentmodel.CMAnyElement;
import org.eclipse.wst.xml.core.internal.contentmodel.CMContent;
import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType;
import org.eclipse.wst.xml.core.internal.contentmodel.CMDocument;
import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration;
import org.eclipse.wst.xml.core.internal.contentmodel.CMGroup;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNode;
import org.eclipse.wst.xml.core.internal.contentmodel.CMNodeList;
import org.eclipse.wst.xml.core.internal.contentmodel.util.CMVisitor;



public class CMValidator
{
  protected static final StringElementContentComparator stringContentComparitor = new StringElementContentComparator();
  protected Hashtable graphNodeTable = new Hashtable();

  /**
   * GraphNode
   */
  protected static class GraphNode
  {
    public String name;
    public boolean isTerminal;
    public Vector arcList = new Vector();

    public GraphNode(String name)
    {
      this.name = name;
    }

    public void addArc(Arc arc)
    {
      arcList.addElement(arc);
    }

    public String toString()
    {
      return "[GraphNode " + name + "]"; //$NON-NLS-1$ //$NON-NLS-2$
    }
  }


  /**
   * Arc
   */
  protected static class Arc
  {
    public static final int ELEMENT  = 1;
    public static final int REPEAT   = 2;
    public static final int OPTIONAL = 3;
    public static final int PREV_IN  = 4;
    public static final int OUT_NEXT = 5;
    public static final int LINK     = 6;

    public int kind;
    public String name;
    public GraphNode node;
    public CMNode cmNode;

    public Arc(int kind, GraphNode node, CMNode cmNode)
    {
      this(kind, "", node, cmNode); //$NON-NLS-1$
    }

    protected Arc(int kind, String name, GraphNode node, CMNode cmNode)
    {
      this.name = name;
      this.kind = kind;
      this.node = node;
      this.cmNode = cmNode;
    }
  }


  /**
   * GraphGenerator
   */
  protected static class GraphGenerator extends CMVisitor
  {
    public int indent;
    public int count;
    public GraphNode startGraphNode;
    public Context context;

    protected static class Context
    {
      GraphNode from;
      GraphNode to;

      Context(GraphNode from, GraphNode to)
      {
        this.from = from;
        this.to = to;
      }

      GraphNode getLastGraphNode()
      {
        return (to != null) ? to : from;
      }
    }


    protected GraphGenerator()
    {
      startGraphNode = new GraphNode(getGraphNodeName());
      context = new Context(startGraphNode, null);
    }


    protected void generateGraph(CMElementDeclaration ed)
    {
      int contentType = ed.getContentType();

      if (contentType == CMElementDeclaration.MIXED ||
          contentType == CMElementDeclaration.ELEMENT)
      {
        visitCMNode(ed.getContent());
      }
      // CMElementDeclaration.PCDATA... no graph required
      // CMElementDeclaration.ANY... no graph required
      context.getLastGraphNode().isTerminal = true;
    }


    protected String getGraphNodeName()
    {
      return "n" + count++; //$NON-NLS-1$
    }


    protected GraphNode getStartGraphNode()
    {
      return startGraphNode;
    }


    /**
     *                repeat
     *             +----#-----+
     *             |          |
     *             v          |
     *  prev --#-> in --'x'-> out --#-> next
     *  |                               ^
     *  |                               |
     *  +----------------#--------------+
     *                optional
     *
     */
    protected void createArcs(GraphNode in, GraphNode out, CMContent cmContent)
    {
      createArcs(in, out, cmContent, false);
    }

    protected void createArcs(GraphNode in, GraphNode out, CMContent cmContent, boolean isAllGroup)
    {
      //println("+createArcs() " + ed.getDescription() + " " + ed.getMinOccur());
      GraphNode prev = context.from;
      GraphNode next = new GraphNode(getGraphNodeName());

      prev.addArc(new Arc(Arc.PREV_IN, in, cmContent));
      out.addArc(new Arc(Arc.OUT_NEXT, next, cmContent));

      if (context.to != null)
      {
        next.addArc(new Arc(Arc.LINK, context.to, cmContent));
      }
      else
      {
        context.from = next;
      }

      if (cmContent.getMinOccur() == 0)
      {
        // todo... should we see if an optional arc has already been added?
        prev.addArc(new Arc(Arc.OPTIONAL, next, cmContent));
      }

      if (cmContent.getMaxOccur() == -1 || cmContent.getMaxOccur() > 1 || isAllGroup)
      {
        out.addArc(new Arc(Arc.REPEAT, in, cmContent));
      }
    }


    public void visitCMGroup(CMGroup group)
    {
      Context prevContext = context;
      GraphNode in = new GraphNode("(" + getGraphNodeName()); //$NON-NLS-1$
      GraphNode out = new GraphNode(")" + getGraphNodeName()); //$NON-NLS-1$

      int groupOperator = group.getOperator();
      if (groupOperator == CMGroup.SEQUENCE)
      {
        context = new Context(in, null);
        super.visitCMGroup(group);
        context.from.addArc(new Arc(Arc.LINK, out, group));
      }
      else if (groupOperator == CMGroup.CHOICE ||
               groupOperator == CMGroup.ALL)
      {
        context = new Context(in, out);
        super.visitCMGroup(group);
      }

      context = prevContext;
      createArcs(in, out, group, groupOperator == CMGroup.ALL);
    }


    public void visitCMElementDeclaration(CMElementDeclaration ed)
    {
      GraphNode in = new GraphNode(getGraphNodeName());
      GraphNode out = new GraphNode(getGraphNodeName());
      createArcs(in, out, ed);
      in.addArc(new Arc(Arc.ELEMENT, ed.getElementName(), out, ed));
    }
                            

    public void visitCMAnyElement(CMAnyElement anyElement)
    {
      GraphNode in = new GraphNode(getGraphNodeName());
      GraphNode out = new GraphNode(getGraphNodeName());
      createArcs(in, out, anyElement);
      in.addArc(new Arc(Arc.ELEMENT, "any", out, anyElement)); //$NON-NLS-1$
    }
  }

  // todo.. implement cache strategy hook, handle name spaces, locals etc.
  //
  public GraphNode lookupOrCreateGraph(CMElementDeclaration element)
  {
    Object key = element;
    GraphNode node = (GraphNode)graphNodeTable.get(key);
    if (node == null)
    {
      node = createGraph(element);
      graphNodeTable.put(key, node);
    }
    return node;
  }

  public GraphNode createGraph(CMElementDeclaration element)
  {
    GraphGenerator generator = new GraphGenerator();
    generator.generateGraph(element);
    return generator.getStartGraphNode();
  }


  public void printGraph(GraphNode node, Vector namedArcList, Vector unamedArcList, int indent)
  {
    //String decoration = node.isTerminal ? " *" : "";
    //printlnIndented(indent, "GraphNode:" + node.name + decoration);

    indent += 2;
    for (Enumeration e = node.arcList.elements() ; e.hasMoreElements() ;)
    {
      Arc arc = (Arc)e.nextElement();
      //boolean visit = false;
      //printlnIndented(indent, "Arc:" + arc.name + " (" + arc.kind + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
      if (arc.kind == Arc.ELEMENT)
      {
        //table.add(currentGrammarObject, arc.grammarObject);
        if (!namedArcList.contains(arc))
        {
          namedArcList.add(arc);
          unamedArcList = new Vector();
          printGraph(arc.node, namedArcList, unamedArcList, indent + 2);
        }
      }
      else
      {
        if (!unamedArcList.contains(arc))
        {
          unamedArcList.add(arc);
          printGraph(arc.node, namedArcList, unamedArcList, indent + 2);
        }
      }
    }
  }

  public void printGraph(GraphNode node)
  {
    printGraph(node, new Vector(), new Vector(), 0);
  }


  public void validateElementList(ElementList initialList, GraphNode initialGraphNode, ElementContentComparator comparator, Result result, boolean initialLoopFlag) {
	  Stack arcStack = new Stack();
	  arcStack.push(new ArcStackItem(null, false));
	  boolean loopFlag = initialLoopFlag;
	  ElementList elementList = initialList;
	  GraphNode graphNode = initialGraphNode;
	  while(!arcStack.isEmpty() && !result.isValid) {
		  ArcStackItem stackElement = (ArcStackItem) arcStack.peek();
		  if(stackElement.isVisited) {
			  arcStack.pop();
			  if(stackElement.arc != null) {
				  result.pop(stackElement.arc);
				  continue;
			  }
		  } else {
			  stackElement.isVisited = true;
			  result.push(stackElement.arc);
			  graphNode = stackElement.arc.node;
			  loopFlag = stackElement.loopFlag;
		  } 
		  if(elementList == null && graphNode.isTerminal) {
			  result.isValid = true;
		  } else {
			  for(Iterator arcIterator = graphNode.arcList.iterator(); arcIterator.hasNext();) {
				  Arc arc = (Arc)arcIterator.next();
				  boolean traverseArc = false;
				  if (arc.kind == Arc.ELEMENT) {
					  if(elementList != null && comparator.matches(elementList.head, arc.cmNode)) {
						  loopFlag = false;
						  traverseArc = true;
						  elementList = elementList.tail; // increment our position in the list
					  }
				  } else if(arc.kind == Arc.REPEAT) {
					  if(!loopFlag) {
						  traverseArc = true;
					  }
					  loopFlag = true;
				  } else {
					  traverseArc = true;
				  }
				  if(traverseArc) {
					  if (result.canPush(arc)) { // test to see if we can push this arc due to facet constraints  
						  arcStack.push(new ArcStackItem(arc, loopFlag));
					  }
				  }
			  }
		  }
	  }
  }
  
  
  private class ArcStackItem {
	  
	  Arc arc;
	  boolean loopFlag;
	  boolean isVisited;
	  
	  public ArcStackItem(Arc arc, boolean loopflag) {
		  this.arc = arc;
		  this.loopFlag = loopflag;
		  this.isVisited = arc == null;
	  }

  }


  /**
   *
   */
  protected static ElementList createElementList(int contentType, List v, ElementContentComparator comparator, Result result)
  {
    ElementList first = null;
    ElementList prev = null;

    int size = v.size();
    for (int i = 0; i < size; i++)
    {
      Object o = v.get(i);
      if (o != null && !comparator.isIgnorable(o))
      {
        if (comparator.isElement(o))
        {
          ElementList list = new ElementList();
          list.head = o;

          if (prev != null)
          {
            prev.tail = list;
          }
          else
          {
            first = list;
          }
          prev = list;
        }
        else if (contentType == CMElementDeclaration.ELEMENT)
        {
          result.isValid = false;
          result.errorIndex = i;
          result.errorMessage = "Element can not include PCDATA content"; //$NON-NLS-1$
        }
      }
    }
    return first;
  }

  /**
   *
   */
  public void validate(CMElementDeclaration ed, List elementContent, ElementContentComparator comparator, Result result)
  {
    int contentType = ed.getContentType();

    if (contentType == CMElementDeclaration.MIXED ||
        contentType == CMElementDeclaration.ELEMENT)
    {
      ElementList elementList = createElementList(contentType, elementContent, comparator, result);
      if (result.isValid == true)
      {  
          boolean isGraphValidationNeeded = !(elementList == null && contentType == CMElementDeclaration.MIXED);
                      
          // explicitly handle 'All' groups
          //
          CMContent content = ed.getContent();
          if (content != null && content.getNodeType() == CMNode.GROUP)
          {
            CMGroup group = (CMGroup)content;
            if (group.getOperator() == CMGroup.ALL)
            {
              isGraphValidationNeeded = false;
              validatAllGroupContent(elementContent, comparator, group, result);                               
            }              
          }  
          
          if (isGraphValidationNeeded)
          {
            // validate the elementList using a graph
            //
            result.isValid = false;
            GraphNode node = lookupOrCreateGraph(ed);
            validateElementList(elementList, node, comparator, result, false);
          }
      }
    }
    else if (contentType == CMElementDeclaration.PCDATA)
    {
      int size = elementContent.size();
      for (int i = 0; i < size; i++)
      {
        Object o = elementContent.get(i);
        if (comparator.isElement(o))
        {
          result.isValid = false;
          result.errorIndex = i;
          result.errorMessage = "Element may only include PCDATA content"; //$NON-NLS-1$
          break;
        }
      }
    }
    else if (contentType == CMElementDeclaration.EMPTY)
    {
      int size = elementContent.size();
      for (int i = 0; i < size; i++)
      {
        Object o = elementContent.get(i);
        if (!comparator.isIgnorable(o))
        {
          result.isValid = false;
          result.errorIndex = i;
          result.errorMessage = "Element may not contain PCDATA or Element content"; //$NON-NLS-1$
          break;
        }
      }
    }
    //else if (contentType == CMElementDeclaration.ANY)
    // {
    //   assume elementContent will always be valid for this content type
    // }
  }
    
  static class ItemCount
  {
    int count = 0;    
  }
  
  private void validatAllGroupContent(List elementContent, ElementContentComparator comparator, CMGroup allGroup, Result result) 
  {
    boolean isValid = true;
    boolean isPartiallyValid = true;
    HashMap map = new HashMap();
    CMNodeList list = allGroup.getChildNodes();
    for (int j = list.getLength() - 1; j >= 0; j--)
    {
      CMNode node = list.item(j);      
      if (map.get(node) == null)
      {  
        map.put(node, new ItemCount());
      }  
    }    
    int validitionCount = 0;
    for (Iterator i = elementContent.iterator(); i.hasNext(); validitionCount++)
    {
      Object o = i.next();        
      if (comparator.isElement(o))
      {              
        // test to see if the element is listed in the all group
        //
        CMNode matchingCMNode = null;
        for (int j = list.getLength() - 1; j >= 0; j--)
        {
          CMNode node = list.item(j);
          if (comparator.matches(o, node))
          {
            matchingCMNode = node;
            break;
          }             
        }                              
        if (matchingCMNode == null)
        {     
          isPartiallyValid = false;
          isValid = false;
          break;
        }
        else
        {  
          // test to see that the element occurs only once
          //
          ItemCount itemCount = (ItemCount)map.get(matchingCMNode);
          if (itemCount != null)
          {  
            if (itemCount.count > 0)
            {
              // we don't want to allow too many elements!
              // we consider 'not enough' to be partially valid... but not 'too many'
              isPartiallyValid = false;
              break;
            }  
            else
            {
              itemCount.count++;
            }  
          }
        }  
      }  
    }
    if (isValid)
    {  
      for (int j = list.getLength() - 1; j >= 0; j--)
      {
        CMNode node = list.item(j);      
        if (node.getNodeType() == CMNode.ELEMENT_DECLARATION)
        {  
          CMContent content = (CMContent)node;
          ItemCount itemCount = (ItemCount)map.get(node);
//          System.out.print("content " + content.getNodeName() + " " + content.getMinOccur());
          if (itemCount.count < content.getMinOccur())
          {  
            isValid = false;
            break;
          }  
        }  
      }
    }
    if (result instanceof ElementPathRecordingResult && isPartiallyValid)
    {
      ((ElementPathRecordingResult)result).setPartialValidationCount(validitionCount);
    }  
    result.isValid = isValid;
  }  
  
  
  public void getOriginArray(CMElementDeclaration ed, List elementContent, ElementContentComparator comparator, ElementPathRecordingResult result)
  {
    CMNode[] cmNodeArray = null;
    validate(ed, elementContent, comparator, result);
    if (result.isValid)
    {
      CMDataType dataType = ed.getDataType();
      int size = elementContent.size();
      cmNodeArray = new CMNode[size];
      Vector originList = result.getElementOriginList();
      int originListSize = originList.size(); 
      int originListIndex = 0;
      for (int i = 0; i < size; i++)
      {
        Object o = elementContent.get(i);
        if (comparator.isElement(o))
        {     
          if (originListIndex < originListSize)
          {
            cmNodeArray[i] = (CMNode)originList.get(originListIndex);
            originListIndex++;
          }
        }
        else if (comparator.isPCData(o))
        {
          cmNodeArray[i] = dataType;
        }
        // else the CMNode at this index is null
      }
      result.setOriginArray(cmNodeArray);
    }
  }
  
  private void collectNamedArcs(GraphNode node, List namedArcList, int indent)
  {
    //printlnIndented(indent, "GraphNode:" + node.name + decoration);
    indent += 2;
    for (Iterator i = node.arcList.iterator(); i.hasNext() ;)
    {
      Arc arc = (Arc)i.next();
      //printlnIndented(indent, "Arc:" + arc.name + " (" + arc.kind + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
      if (arc.kind == Arc.ELEMENT)
      { 
        if (!namedArcList.contains(arc))
        {
          namedArcList.add(arc);
          collectNamedArcs(arc.node, namedArcList, indent + 2);
        }
      }
      else if (arc.kind != Arc.REPEAT && arc.kind != Arc.OPTIONAL)
      {
        collectNamedArcs(arc.node, namedArcList, indent + 2);
      }
    }
  }
  
  
  private List getMatchingArcs(CMElementDeclaration ed, String elementName)
  {
    List arcList = new ArrayList();
    GraphNode graphNode = lookupOrCreateGraph(ed);
    if (elementName == null)
    {
      // here we add the 'root' arc
      for (Iterator i = graphNode.arcList.iterator(); i.hasNext() ;)
      {
        Arc arc = (Arc)i.next();
        if (arc.kind == Arc.PREV_IN)
        {
          arcList.add(arc);
          break;
        }  
      }
    }
    else
    { 
      List namedArcs = new ArrayList();
      collectNamedArcs(graphNode, namedArcs, 0);
      for (Iterator i = namedArcs.iterator(); i.hasNext(); )
      {
        Arc arc = (Arc)i.next();
        if (arc.cmNode != null && elementName.equals(arc.cmNode.getNodeName()))
        {  
          arcList.add(arc);
        }  
      }  
    }      
    return arcList;
  }

  
  private void collectNextSiblings(GraphNode node, List nextSiblingList, List namedArcList, List unamedArcList, int indent)
  {
    //printlnIndented(indent, "GraphNode:" + node.name + decoration);
    indent += 2;
    for (Iterator i = node.arcList.iterator(); i.hasNext(); )
    {
      Arc arc = (Arc)i.next();
      if (arc.kind == Arc.ELEMENT)
      {       
        if (!namedArcList.contains(arc))
        {
          if (arc.cmNode != null)
          {  
            nextSiblingList.add(arc.cmNode);
            if (arc.cmNode.getNodeType() == CMNode.ELEMENT_DECLARATION ||
                arc.cmNode.getNodeType() == CMNode.ANY_ELEMENT)
            {              
              namedArcList.add(arc);
              CMContent cmNode = (CMContent)arc.cmNode; 
              if (cmNode.getMinOccur() == 0)
              {
                unamedArcList = new ArrayList();
                collectNextSiblings(arc.node, nextSiblingList, namedArcList, unamedArcList, indent + 2);
              }
            }            
          }
        }
      }  
      else
      {
        if (!unamedArcList.contains(arc))
        {
          unamedArcList.add(arc);
          collectNextSiblings(arc.node, nextSiblingList, namedArcList, unamedArcList, indent + 2);
        }
      }
    }  
  }
    
  public CMNode[] getNextSiblings(CMElementDeclaration ed, String elementName)
  {
    List arcList = getMatchingArcs(ed, elementName);
    List nextSiblingList = new ArrayList();
    for (Iterator i = arcList.iterator(); i.hasNext(); )
    {
      Arc arc = (Arc)i.next();
      collectNextSiblings(arc.node, nextSiblingList, new ArrayList(), new ArrayList(), 0);      
    }  
    CMNode[] result = new CMNode[nextSiblingList.size()];
    nextSiblingList.toArray(result);    
    //System.out.print("getNextSibling(" +elementName + ")");
    //for (int i = 0; i < result.length; i++)
    //{
    //  System.out.print("[" + result[i].getNodeName() + "]");
    //}  
    //System.out.println();
    return result;
  }

  /**
   *
   */
  public static class Result
  {
    public boolean isValid = true;
    public int errorIndex = -1;
    public String errorMessage;
    public boolean isRepeatTraversed; // detects if a repeat has been traversed

    public boolean canPush(Arc arc)
    {
      return true;
    }
    
    public void push(Arc arc)
    {
      // overide this method to record traversed nodes
    }
    public void pop(Arc arc)
    {
      // overide this method to record traversed nodes
    }
    public CMNode[] getOriginArray()
    {
      return null;
    }
  }

  /**
   *
   */
  public static class ElementPathRecordingResult extends Result
  {  
    protected List activeItemCountList = new ArrayList();
    protected List inactiveItemCountList = new ArrayList();    
    protected Vector elementOriginStack = new Vector();
    protected CMNode[] originArray = null;
    protected int partialValidationCount = 0;

    
    // this method is used to support facet counts
    //
    public boolean canPush(Arc arc)
    {     
      boolean result = true;        
      try
      {        
        if (arc.kind == Arc.REPEAT)
        {          
          if (arc.cmNode instanceof CMContent)
          {
            CMContent content = (CMContent)arc.cmNode;
            
            // we only need to do 'special' facet checking if the maxOccurs is > 1
            // values of '0' and '-1' (unbounded) work 'for free' without any special checking
            //
            if (content.getMaxOccur() > 1)
            {  
              ItemCount itemCount = (ItemCount)activeItemCountList.get(activeItemCountList.size() - 1);
              
              // here we need to compute if we can do another repeat
              // if we increase the repeat count by '1' will this violate the maxOccurs
              //
              if (itemCount.count + 1 >= content.getMaxOccur())
              {
                result = false;
              }
            }
            //System.out.println("canPush REPEAT (" + itemCount.count + ")" + content.getNodeName() + " result= " + result);            
          } 
        }       
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
      //System.out.flush();
      return result;
    }
    
    public void push(Arc arc)
    {
      if (arc.kind == Arc.ELEMENT)
      {
        //System.out.println("[X]push(" + arc.kind + ")" + arc.cmNode.getNodeName());
        elementOriginStack.add(arc.cmNode);
        partialValidationCount = Math.max(elementOriginStack.size(), partialValidationCount);
      }
      else if (arc.kind == Arc.PREV_IN)
      {
        //System.out.println("[X]push(" + arc.kind + ")" + arc.cmNode.getNodeName());
        activeItemCountList.add(new ItemCount());   
      }
      else if (arc.kind == Arc.OUT_NEXT)
      {        
        //System.out.println("[X]push(" + arc.kind + ")" + arc.cmNode.getNodeName() + "[" + arc + "]");
        int size = activeItemCountList.size();
        ItemCount itemCount = (ItemCount)activeItemCountList.get(size - 1);
        activeItemCountList.remove(size - 1);
        inactiveItemCountList.add(itemCount); 
      }      
      else if (arc.kind == Arc.REPEAT)
      {
        //System.out.println("[X]push(" + arc.kind + ")" + arc.cmNode.getNodeName());
        ItemCount itemCount = (ItemCount)activeItemCountList.get(activeItemCountList.size() - 1);
        itemCount.count++;
        //System.out.println("repeat(" + itemCount.count + ")" + arc.cmNode.getNodeName());
      }        
    }

    public void pop(Arc arc)
    {
      if (arc.kind == Arc.ELEMENT)
      {
        //System.out.println("[X]pop(" + arc.kind + ")" + arc.cmNode.getNodeName());
        int size = elementOriginStack.size();
        elementOriginStack.remove(size - 1);
      }
      else if (arc.kind == Arc.PREV_IN)
      {
        //System.out.println("[X]pop(" + arc.kind + ")" + arc.cmNode.getNodeName());
        activeItemCountList.remove(activeItemCountList.size() - 1);        
      }
      else if (arc.kind == Arc.OUT_NEXT)
      {
        //System.out.println("[X]pop(" + arc.kind + ")" + arc.cmNode.getNodeName());
        int size = inactiveItemCountList.size();
        ItemCount itemCount = (ItemCount)inactiveItemCountList.get(size - 1);
        inactiveItemCountList.remove(size - 1);
        activeItemCountList.add(itemCount);     
      }  
      else if (arc.kind == Arc.REPEAT)
      {
        //System.out.println("[X]pop(" + arc.kind + ")" + arc.cmNode.getNodeName());
        ItemCount itemCount = (ItemCount)activeItemCountList.get(activeItemCountList.size() - 1);
        itemCount.count--;
      }     
    }

    public Vector getElementOriginList()
    {
      return elementOriginStack;
    }

    public CMNode[] getOriginArray()
    {
      return originArray;
    }

    public void setOriginArray(CMNode[] originArray)
    {
      this.originArray = originArray;
    }
    
    public int getPartialValidationCount()
    {
      return partialValidationCount;
    }

    public void setPartialValidationCount(int partialValidationCount)
    {
      this.partialValidationCount = partialValidationCount;
    }
  }  

  /**
   *
   */
  public static class PathRecordingResult extends Result
  {
    protected Vector arcList = new Vector();

    public void push(Arc arc)
    {
      arcList.add(arc);
    }

    public void pop(Arc arc)
    {
      int size = arcList.size();
      arcList.remove(size - 1);
    }

    public List getArcList()
    {
      List list = new Vector();
      for (Iterator iterator = arcList.iterator(); iterator.hasNext(); )
      {
        Arc arc = (Arc)iterator.next();
        if (arc.kind == Arc.ELEMENT)
        {
          list.add(arc);
        }
      }
      return list;
    }

    public MatchModelNode getMatchModel()
    {
      MatchModelNodeBuilder builder = new MatchModelNodeBuilder(arcList);
      builder.buildMatchModel();
      return builder.getRoot();
    }
  }

  /**
   *
   */
  public static class MatchModelNode
  {
    public CMNode cmNode;
    public List children = new Vector();
    public Object data;

    public MatchModelNode(MatchModelNode parent, CMNode cmNode)
    {
      this.cmNode = cmNode;
    }

    public void printModel(int indent)
    {
      //String cmNodeName = cmNode != null ? cmNode.getNodeName() : "null";
      //printlnIndented(indent, "MatchModelNode : " + cmNodeName);
      for (Iterator iterator = children.iterator(); iterator.hasNext(); )
      {
        MatchModelNode child = (MatchModelNode)iterator.next();
        child.printModel(indent + 2);
      }
    }
  }

  public static class MatchModelNodeBuilder
  {
    protected List arcList;
    protected List stack = new Vector();
    protected MatchModelNode root;
    protected MatchModelNode current;

    public MatchModelNodeBuilder(List arcList)
    {
      this.arcList = arcList;
      root = new MatchModelNode(null, null);
      push(root);
    }

    protected void push(MatchModelNode node)
    {
      current = node;
      stack.add(node);
    }

    protected void pop()
    {
      int size = stack.size();
      stack.remove(size - 1);
      current = (MatchModelNode)stack.get(size - 2);
    }

    public boolean isCMGroup(CMNode cmNode)
    {
      return cmNode != null && cmNode.getNodeType() == CMNode.GROUP;
    }

    public void buildMatchModel()
    {
      for (Iterator iterator = arcList.iterator(); iterator.hasNext(); )
      {
        Arc arc = (Arc)iterator.next();

        if (arc.kind == Arc.ELEMENT)
        {
          current.children.add(new MatchModelNode(current, arc.cmNode));
        }
        else if (arc.kind == Arc.PREV_IN)
        {
          if (isCMGroup(arc.cmNode))
          {
            MatchModelNode newModelNode = new MatchModelNode(current, arc.cmNode);
            current.children.add(newModelNode);
            push(newModelNode);
          }
        }
        else if (arc.kind == Arc.OUT_NEXT)
        {
          if (isCMGroup(arc.cmNode))
          {
            pop();
          }
        }
        else if (arc.kind == Arc.REPEAT)
        {
          if (isCMGroup(arc.cmNode))
          {
            pop();
            MatchModelNode newModelNode = new MatchModelNode(current, arc.cmNode);
            current.children.add(newModelNode);
            push(newModelNode);
          }
        }
      }
    }

    public MatchModelNode getRoot()
    {
      return root;
    }
  }


  /**
   *
   */
  public interface ElementContentComparator
  {
    public boolean isIgnorable(Object o);
    public boolean isPCData(Object o);
    public boolean isElement(Object o);
    public boolean matches(Object o, CMNode cmNode);
  }

  /**
   * A linked list
   */
  public static class ElementList
  {
    protected Object head;
    protected ElementList tail;

    public static ElementList create(List v)
    {
      ElementList first = null;
      ElementList prev = null;

      for (Iterator iterator = v.iterator(); iterator.hasNext(); )
      {
        Object o = iterator.next();
        if (o != null)
        {
          ElementList list = new ElementList();
          list.head = o;

          if (prev != null)
          {
            prev.tail = list;
          }
          else
          {
            first = list;
          }
          prev = list;
        }
      }
      return first;
    }


    public String toString()
    {
      String string = "[" + head + "],"; //$NON-NLS-1$ //$NON-NLS-2$

      if (tail != null)
      {
        string += tail.toString();
      }

      return string;
    }
  }

  /**
   * StringElementContentComparator
   */
  public static class StringElementContentComparator implements ElementContentComparator
  {
    public boolean isIgnorable(Object o)
    {
      String string = o.toString();
      return string.startsWith("!") || string.startsWith("?"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    public boolean isPCData(Object o)
    {
      String string = o.toString();
      return string.startsWith("'") || string.startsWith("\""); //$NON-NLS-1$ //$NON-NLS-2$
    }

    public boolean isElement(Object o)
    {
      return !isIgnorable(o) && !isPCData(o);
    }

    public boolean matches(Object o, CMNode cmNode)
    {
      boolean result = false;
      if (cmNode.getNodeType() == CMNode.ELEMENT_DECLARATION)
      { 
        CMElementDeclaration element = (CMElementDeclaration)cmNode;
        String name = o.toString();                              
        int index = name.indexOf("]"); //$NON-NLS-1$
        if (index != -1)
        {
          name = name.substring(index + 1);
        }
        result = name.equalsIgnoreCase(element.getElementName());        

        // TODO... here's we consider substitution groups... revisit to see if this should be moved into validator code
        if (!result)
        {
          CMNodeList cmNodeList = (CMNodeList)element.getProperty("SubstitutionGroup");   //$NON-NLS-1$
          if (cmNodeList != null)
          {
            int cmNodeListLength = cmNodeList.getLength();
            if (cmNodeListLength > 1)
            {                        
              for (int i = 0; i < cmNodeListLength; i++)
              {                                                               
                CMElementDeclaration alternativeCMElementDeclaration = (CMElementDeclaration)cmNodeList.item(i);
                String altName = alternativeCMElementDeclaration.getElementName();
                result = name.equalsIgnoreCase(altName);
                if (result)
                {
                  break;
                }
              }
            }
          }
        }         
      }   
      else if (cmNode.getNodeType() == CMNode.ANY_ELEMENT)
      {                                   
        String string = o.toString();
        if (string.equals("*")) //$NON-NLS-1$
        {
          result = true;
        }
        else
        {
          CMAnyElement anyElement = (CMAnyElement)cmNode;
          String anyElementURI = anyElement.getNamespaceURI();    
          if (anyElementURI != null)
          {           
            if (anyElementURI.equals("##any")) //$NON-NLS-1$
            {                               
              result = true;
            }
            else if (anyElementURI.equals("##other")) //$NON-NLS-1$
            {     
              result = true;    
              CMDocument cmDocument = (CMDocument)anyElement.getProperty("CMDocument");   //$NON-NLS-1$
              if (cmDocument != null)
              {
                String excludedURI = (String)cmDocument.getProperty("http://org.eclipse.wst/cm/properties/targetNamespaceURI"); //$NON-NLS-1$
                if (excludedURI != null)
                { 
                  String specifiedURI = getURIForContentSpecification(string);
                  if (specifiedURI != null && excludedURI.equals(specifiedURI))
                  { 
                    result = false;
                  } 
                }
              }
            } 
            else if (anyElementURI.equals("##targetNamespace")) //$NON-NLS-1$
            {
              result = true;
              CMDocument cmDocument = (CMDocument)anyElement.getProperty("CMDocument");   //$NON-NLS-1$
              if (cmDocument != null)
              {     
                String targetNamespaceURI = (String)cmDocument.getProperty("http://org.eclipse.wst/cm/properties/targetNamespaceURI"); //$NON-NLS-1$
                String specifiedURI = getURIForContentSpecification(string);
                if (specifiedURI != null && !targetNamespaceURI.equals(specifiedURI))
                { 
                  result = false;
                } 
              }
            }
            else  
            {        
              result = true;
              String specifiedURI = getURIForContentSpecification(string);
              if (specifiedURI != null && !anyElementURI.equals(specifiedURI))
              { 
                result = false;
              }      
            }
          }  
          else 
          {          
            result = true;
          }        
        }
      }
      return result;
    }     
    

    protected String getURIForContentSpecification(String specification)
    {           
      String result = null;
      int index = specification.indexOf("]"); //$NON-NLS-1$
      if (index != -1)
      {                
        result = specification.substring(1, index);
      } 
      return result;  
    }
  }      

  public static List createStringList(String arg[], int startIndex)
  {
    Vector v = new Vector();
    for (int i = startIndex; i < arg.length; i++)
    {
      v.add(arg[i]);
    }
    return v;
  }
}
