package com.jclark.xsl.tr;

import com.jclark.xsl.om.*;
import com.jclark.xsl.expr.Pattern;
import com.jclark.xsl.expr.ExprParser;
import com.jclark.xsl.expr.StringExpr;
import com.jclark.xsl.expr.NodeSetExpr;
import com.jclark.xsl.expr.BooleanExpr;
import com.jclark.xsl.expr.VariantExpr;
import com.jclark.xsl.expr.Variant;
import com.jclark.xsl.expr.StringVariant;
import com.jclark.xsl.expr.NumberVariant;
import com.jclark.xsl.expr.VariableSet;
import com.jclark.xsl.expr.EmptyVariableSet;
import com.jclark.xsl.expr.ExtensionContext;
import com.jclark.xsl.expr.CloneableNodeIterator;
import com.jclark.xsl.expr.CloneableNodeIteratorImpl;
import com.jclark.xsl.util.Comparator;
import com.jclark.xsl.util.BilevelComparator;
import com.jclark.xsl.util.TextComparator;
import com.jclark.xsl.util.NumberComparator;
import com.jclark.xsl.util.ReverseComparator;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.StringTokenizer;
import java.util.Locale;
import java.text.Collator;

class SheetImpl implements Sheet, LoadContext {
  private
  interface TopLevelParser {
    void parse(Node node) throws XSLException, IOException;
  }

  private
  class IncludeParser implements TopLevelParser {
    public void parse(Node ruleNode) throws XSLException, IOException {
      Node sheetNode = parser.load(new URL(ruleNode.getURL(),
					   getRequiredAttribute(ruleNode,
								HREF)),
				   sheetLoadContext,
				   nameTable).getChildren().next();
      if (XSL_NAMESPACE.equals(sheetNode.getName().getNamespace())) {
	if (!XSL_STYLESHEET.equals(sheetNode.getName())
	    && !XSL_TRANSFORM.equals(sheetNode.getName()))
	  throw new XSLException("bad document element for stylesheet",
				 sheetNode);
	parseTopLevel(sheetNode);
      }
      else
	parseRootTemplate(sheetNode);
    }
  }

  private
  class ImportParser extends IncludeParser {
    public void parse(Node ruleNode) throws XSLException, IOException {
      Importance oldFirstImportImportance = firstImportImportance;
      firstImportImportance = currentImportance;
      super.parse(ruleNode);
      currentImportance = currentImportance.createHigher();
      firstImportImportance = oldFirstImportImportance;
    }
  }

  private
  class TemplateParser implements TopLevelParser {
    public void parse(Node defNode) throws XSLException {
      String name = defNode.getAttributeValue(NAME);
      Action contents = parseActions(defNode, emptyAction);
      if (name != null)
        namedTemplateTable.put(expandSourceElementTypeName(name, defNode), contents);
      String pattern = defNode.getAttributeValue(MATCH);
      if (pattern == null)
	return;
      String modeString = defNode.getAttributeValue(MODE);
      TemplateRuleSet ruleSet;
      if (modeString != null)
        ruleSet = getModeTemplateRuleSet(expandSourceElementTypeName(modeString, defNode));
      else
        ruleSet = templateRules;
      try {
        ruleSet.add(ExprParser.parsePattern(defNode, pattern),
	            currentImportance,
		    firstImportImportance,
		    Priority.create(defNode.getAttributeValue(PRIORITY)),
		    contents);
      }
      catch (NumberFormatException e) {
        throw new XSLException("invalid priority", defNode);
      }
    }
  }

  private
  class AttributeSetParser implements TopLevelParser {
    public void parse(Node defNode) throws XSLException {
      // FIXME
    }
  }

  private
  class KeyParser implements TopLevelParser {
    public void parse(Node defNode) throws XSLException {
      // FIXME
    }
  }

  private
  class LocaleParser implements TopLevelParser {
    public void parse(Node defNode) throws XSLException {
      // FIXME
    }
  }

  private
  class VariableTopLevelParser implements TopLevelParser {
    public void parse(Node defNode) throws XSLException {
      variableExprTable
	.put(expandSourceElementTypeName(getRequiredAttribute(defNode, NAME), defNode),
	     getVariantExpr(defNode));
    }
  }

  private
  class StripSpaceParser implements TopLevelParser {
    public void parse(Node node) throws XSLException {
      parse(node, Boolean.TRUE);
    }

    void parse(Node node, Boolean strip) throws XSLException {
      if (stripSourceTable == null)
	stripSourceTable = new Hashtable();
      StringTokenizer iter
	  = new StringTokenizer(getRequiredAttribute(node, ELEMENTS));
      while (iter.hasMoreElements())
	stripSourceTable.put(expandSourceElementTypeName((String)iter.nextElement(), node),
			     strip);
    }
  }

  private
  class PreserveSpaceParser extends StripSpaceParser {
    public void parse(Node node) throws XSLException {
      parse(node, Boolean.FALSE);
    }
  }

  private
  interface ActionParser {
    Action parse(Node node) throws XSLException;
  }

  private
  class ApplyTemplatesParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      NodeSetExpr expr = getSortNodeSetExpr(node, getNodeSetExpr(node));
      String modeString = node.getAttributeValue(MODE);
      Name modeName = null;
      if (modeString != null)
	modeName = expandSourceElementTypeName(modeString, node);
      return addParams(new ProcessAction(expr, modeName), node);
    }


    NodeSetExpr getNodeSetExpr(Node node) throws XSLException {
      String select = node.getAttributeValue(SELECT);
      if (select == null)
	return childrenExpr;
      return ExprParser.parseNodeSetExpr(node, select, currentLocalVariables);
    }

  }

  private
  class ForEachParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new ForEachAction(getSortNodeSetExpr(node,
					    ExprParser
					    .parseNodeSetExpr(node, getRequiredAttribute(node, SELECT), currentLocalVariables)),
			       parseActions(node, emptyAction));
    }
  }

  private
  class IfParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new IfAction(makeCondition(node),
			  parseActions(node, emptyAction),
			  emptyAction);
    }

    BooleanExpr makeCondition(Node node) throws XSLException {
      return ExprParser.parseBooleanExpr(node,
					 getRequiredAttribute(node, TEST),
					 currentLocalVariables);
    }
  }

  private
  class CopyParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new CopyAction(parseActions(node, emptyAction));
    }
  }

  private
  class CopyOfParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new CopyOfAction(ExprParser.parseVariantExpr(node,
							  getRequiredAttribute(node, SELECT),
							  currentLocalVariables));
    }
  }

  private
  class CommentParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new CommentAction(parseActions(node, emptyAction));
    }
  }

  private
  class ProcessingInstructionParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new ProcessingInstructionAction(
	ExprParser.parseAttributeValueTemplate(node,
					       getRequiredAttribute(node, NAME),
					       currentLocalVariables),
	parseActions(node, emptyAction));
    }
  }

  private
  class ElementParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new ElementAction(
	ExprParser.parseAttributeValueTemplate(node,
					       getRequiredAttribute(node, NAME),
					       currentLocalVariables),
	getNamespaceExpr(node),
	node.getNamespacePrefixMap(),
	parseActions(node, emptyAction));
    }

    StringExpr getNamespaceExpr(Node node) throws XSLException {
      String namespace = node.getAttributeValue(NAMESPACE);
      if (namespace == null)
	return null;
      return ExprParser.parseAttributeValueTemplate(node, namespace, currentLocalVariables);
    }
  }

  private
  class AttributeParser extends ElementParser {
    public Action parse(Node node) throws XSLException {
      return new AttributeAction(
	ExprParser.parseAttributeValueTemplate(node,
					       getRequiredAttribute(node, NAME),
					       currentLocalVariables),
	getNamespaceExpr(node),
	node.getNamespacePrefixMap(),
	parseActions(node, emptyAction));
    }
  }

  private
  class CallTemplateParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      
      return addParams(new InvokeAction(expandSourceElementTypeName(getRequiredAttribute(node, NAME), node),
					namedTemplateTable),
		       node);
    }
  }

  private
  class ChooseParser extends IfParser {
    public Action parse(Node node) throws XSLException {
      return parseChoices(node.getChildren());
    }
    public Action parseChoices(NodeIterator iter) throws XSLException {
      Node node = iter.next();
      if (node == null)
	return emptyAction;
      Name name = node.getName();
      if (XSL_WHEN.equals(name))
	return new IfAction(makeCondition(node),
			    parseActions(node, emptyAction),
			    parseChoices(iter));
      if (XSL_OTHERWISE.equals(name)) {
	Node next = iter.next();
	if (next != null)
	  throw new XSLException("unexpected element after otherwise",
				 next);
	return parseActions(node, emptyAction);
      }
      throw new XSLException("expected when or otherwise", node);
    }
  }

  private
  class TextParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      Node child = node.getChildren().next();
      if (child != null) {
	String data = child.getData();
	if (data == null || child.getFollowingSiblings().next() != null)
	  throw new XSLException("xsl:text must not contain elements", node);
	return new CharsAction(data);
      }
      else
	return emptyAction;
    }
  }

  private
  class ValueOfParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new ValueOfAction(ExprParser.parseStringExpr(node,
							  getRequiredAttribute(node, SELECT),
							  currentLocalVariables));
    }
  }

  private
  class NumberParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      NumberListFormatTemplate format = getNumberListFormatTemplate(node);
      String value = node.getAttributeValue(VALUE);
      if (value != null)
	return new ExprNumberAction(ExprParser.parseNumberExpr(node, value, currentLocalVariables), format);
      String level = getOptionalAttribute(node, LEVEL, "single");
      String countString = node.getAttributeValue(COUNT);
      Pattern count;
      if (countString == null)
	count = null;
      else
	count = ExprParser.parsePattern(node, countString);
      String fromString = node.getAttributeValue(FROM);
      Pattern from;
      if (fromString == null)
	from = null;
      else
	from = ExprParser.parsePattern(node, fromString);
      if (level.equals("any"))
	return new AnyLevelNumberAction(count, from, format);
      if (level.equals("multiple"))
	return new MultiLevelNumberAction(count, from, format);
      if (level.equals("single"))
	return new SingleLevelNumberAction(count, from, format);
      throw new XSLException("bad level", node);
    }
  }

  private
  class ApplyImportsParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      return new ApplyImportsAction();
    }
  }

  private
  class VariableActionParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      Name name = expandSourceElementTypeName(getRequiredAttribute(node, NAME), node);
      VariantExpr expr = getVariantExpr(node);
      // Must create the VariantExpr before adding to local variables.
      currentLocalVariables = new AddVariableSet(name, currentLocalVariables);
      nCurrentLocalVariables++;
      return makeAction(name, expr);
    }

    Action makeAction(Name name, VariantExpr expr) {
      return new BindLocalVariableAction(name, expr);
    }
  }

  private
  class ParamActionParser extends VariableActionParser {
    Action makeAction(Name name, VariantExpr expr) {
      return new BindLocalParamAction(name, expr);
    }
  }

  private
  class MessageParser implements ActionParser {
    public Action parse(Node node) throws XSLException {
      // FIXME do something more useful
      return new AppendAction();
    }
  }

  static final int OPEN_ACTION_INIT_SIZE = 2;
  static StringVariant emptyStringVariant = new StringVariant("");

  static final class VariableBindings {
    VariableBindings(Name name, Variant value, VariableBindings next) {
      this.name = name;
      this.value = value;
      this.next = next;
    }
    final VariableBindings next;
    final Variant value;
    final Name name;
  }

  class ProcessContextImpl implements ProcessContext {
    Node root;
    Hashtable variableValueTable = new Hashtable();
    Name evalGlobalVariableName = null;
    Name[] actionNames = new Name[OPEN_ACTION_INIT_SIZE];
    Node[] actionNodes = new Node[OPEN_ACTION_INIT_SIZE];
    int[] actionImportLevels = null;
    int[] actionForEachLevels = null;
    int nOpenActions = 0;
    VariableBindings localVariables;
    Name[] currentParamNames = null;
    Variant[] currentParamValues = null;
    int position = 1;
    int lastPosition = 1;
    NodeIterator currentIter = null;
    Hashtable extensionTable = new Hashtable();

    ProcessContextImpl(Node root) {
      this.root = root;
    }

    public void invoke(NodeIterator iter, Action action, Result result) throws XSLException {
      int savePosition = position;
      int saveLastPosition = lastPosition;
      NodeIterator saveCurrentIter = currentIter;
      currentIter = iter;
      position = 0;
      lastPosition = 0;
      if (actionForEachLevels == null)
	actionForEachLevels = new int[nOpenActions];
      else if (nOpenActions > actionForEachLevels.length) {
	int[] oldActionForEachLevels = actionForEachLevels;
	actionForEachLevels = new int[nOpenActions];
	System.arraycopy(oldActionForEachLevels, 0,
			 actionForEachLevels, 0,
			 oldActionForEachLevels.length);
      }
      actionForEachLevels[nOpenActions - 1]++;
      try {
	for (;;) {
	  // getLastPosition() may change the iterator, so use currentIter not iter
	  Node node = currentIter.next();
	  if (node == null)
	    break;
	  ++position;
	  action.invoke(this, node, result);
	}
      }
      finally {
	actionForEachLevels[nOpenActions - 1]--;
	position = savePosition;
	lastPosition = saveLastPosition;
	currentIter = saveCurrentIter;
      }
    }

    public void process(NodeIterator iter, Name modeName,
			Name[] paramNames, Variant[] paramValues,
			Result result)
      throws XSLException {
      int savePosition = position;
      int saveLastPosition = lastPosition;
      NodeIterator saveCurrentIter = currentIter;
      currentIter = iter;
      position = 0;
      lastPosition = 0;
      Name[] saveParamNames = currentParamNames;
      currentParamNames = paramNames;
      Variant[] saveParamValues = currentParamValues;
      currentParamValues = paramValues;
      boolean processSafely = paramValues == null;
      try {
	for (;;) {
	  // getLastPosition() may change the iterator, so use currentIter not iter
	  Node node = currentIter.next();
	  if (node == null)
	    break;
	  ++position;
	  if (processSafely)
	    processSafe(node, modeName, result);
	  else
	    processUnsafe(node, modeName, result);
	}
      }
      finally {
	position = savePosition;
	lastPosition = saveLastPosition;
	currentIter = saveCurrentIter;
	currentParamNames = saveParamNames;
	currentParamValues = saveParamValues;
      }
    }

    private void processUnsafe(Node node, Name name, Result result) throws XSLException {
      getAction(name, node).invoke(this, node, result);
    }

    void processSafe(Node node, Name name, Result result) throws XSLException {
      if (name == null) {
	for (int i = 0; i < nOpenActions; i++)
	  if (actionNames[i] == null && actionNodes[i].equals(node))
	    return; // loop detected
      }
      else {
	for (int i = 0; i < nOpenActions; i++)
	  if (name.equals(actionNames[i]) && actionNodes[i].equals(node))
	    return; // loop detected
      }
      if (nOpenActions == actionNames.length) {
	Name[] oldActionNames = actionNames;
	actionNames = new Name[nOpenActions * 2];
	System.arraycopy(oldActionNames, 0, actionNames, 0, nOpenActions);
	Node[] oldActionNodes = actionNodes;
	actionNodes = new Node[nOpenActions * 2];
	System.arraycopy(oldActionNodes, 0, actionNodes, 0, nOpenActions);
      }
      actionNames[nOpenActions] = name;
      actionNodes[nOpenActions] = node;
      ++nOpenActions;
      getAction(name, node).invoke(this, node, result);
      --nOpenActions;
    }

    private final Action getAction(Name name, Node node) throws XSLException {
      return getModeTemplateRuleSet(name).getAction(node, this);
    }

    public void applyImports(Node node, Result result) throws XSLException {
      if (actionForEachLevels != null
	  && actionForEachLevels[nOpenActions - 1] > 0)
	return;
      if (actionImportLevels == null)
	actionImportLevels = new int[nOpenActions];
      else if (nOpenActions > actionImportLevels.length) {
	int[] oldActionImportLevels = actionImportLevels;
	actionImportLevels = new int[nOpenActions];
	System.arraycopy(oldActionImportLevels, 0,
			 actionImportLevels, 0,
			 oldActionImportLevels.length);
      }
      getModeTemplateRuleSet(actionNames[nOpenActions - 1])
       .getImportAction(node,
			this,
			actionImportLevels[nOpenActions - 1]++)
       .invoke(this, node, result);
      actionImportLevels[nOpenActions - 1]--;
    }
 
    public final boolean hasAttribute(Vector nameList, Node node, String value) {
      int len = nameList.size();
      for (int i = 0; i < len; i++)
	if (value.equals(node.getAttributeValue((Name)nameList.elementAt(i))))
	  return true;
      return false;
    }

    public int getPosition() {
      return position;
    }

    public int getLastPosition() throws XSLException {
      if (lastPosition == 0) {
	lastPosition = position;
	for (NodeIterator iter = cloneCurrentIter(); iter.next() != null;)
	  lastPosition++;
      }
      return lastPosition;
    }
    
    private NodeIterator cloneCurrentIter() {
      if (!(currentIter instanceof CloneableNodeIterator))
	currentIter = new CloneableNodeIteratorImpl(currentIter);
      return (NodeIterator)((CloneableNodeIterator)currentIter).clone();
    }

    public Variant getGlobalVariableValue(Name name) throws XSLException {
      Variant value = (Variant)variableValueTable.get(name);
      if (value != null)
	return value;
      VariantExpr expr = (VariantExpr)variableExprTable.get(name);
      if (expr == null)
	return null;
      // Avoid possibility of infinite loop
      variableValueTable.put(name, emptyStringVariant);
      Name temp = evalGlobalVariableName;
      // set this so we can save it in a memento
      evalGlobalVariableName = name;
      try {
	value = expr.eval(root, this).makePermanent();
      }
      finally {
	evalGlobalVariableName = temp;
      }
      variableValueTable.put(name, value);
      return value;
    }

    public Variant getLocalVariableValue(Name name) {
      for (VariableBindings p = localVariables; p != null; p = p.next) {
	if (p.name.equals(name))
	  return p.value;
      }
      throw new Error("no such local variable");
    }

    public void bindLocalVariable(Name name, Variant value) {
      localVariables = new VariableBindings(name, value.makePermanent(), localVariables);
    }

    public void unbindLocalVariables(int n) {
      for (; n > 0; --n)
	localVariables = localVariables.next;
    }

    public void invokeWithParams(Action action, Name[] paramNames, Variant[] paramValues,
				 Node node, Result result) throws XSLException {
      Name[] saveParamNames = currentParamNames;
      currentParamNames = paramNames;
      Variant[] saveParamValues = currentParamValues;
      currentParamValues = paramValues;
      try {
	action.invoke(this, node, result);
      }
      finally {
	currentParamNames = saveParamNames;
	currentParamValues = saveParamValues;
      }
    }

    public Variant getParam(Name name) {
      if (currentParamNames != null) {
	for (int i = 0; i < currentParamNames.length; i++)
	  if (name.equals(currentParamNames[i]))
	    return currentParamValues[i];
      }
      return null;
    }

    public ProcessContext.Memento createMemento() {
      final VariableBindings rememberLocalVariables = localVariables;
      final int rememberPosition = position;
      final int rememberLastPosition = lastPosition;
      final NodeIterator rememberCurrentIter
	= lastPosition == 0 ? cloneCurrentIter() : null;
      final Name rememberEvalGlobalVariableName = evalGlobalVariableName;
      return new ProcessContext.Memento() {
	public void invoke(Action action, Node node, Result result) throws XSLException {
	  Name[] saveParamNames = currentParamNames;
	  currentParamNames = null;
	  Variant[] saveParamValues = currentParamValues;
	  currentParamValues = null;
	  int savePosition = position;
	  position = rememberPosition;
	  int saveLastPosition = lastPosition;
	  lastPosition = rememberLastPosition;
	  NodeIterator saveCurrentIter = currentIter;
	  currentIter = rememberCurrentIter;
	  VariableBindings saveLocalVariables = localVariables;
	  localVariables = rememberLocalVariables;
	  Object saveGlobalVariableValue = null;
	  if (rememberEvalGlobalVariableName != null) {
	    saveGlobalVariableValue
	      = variableValueTable.get(rememberEvalGlobalVariableName);
	    variableValueTable.put(rememberEvalGlobalVariableName,
				   emptyStringVariant);
	  }
	  try {
	    action.invoke(ProcessContextImpl.this, node, result);
	  }
	  finally {
	    currentParamNames = saveParamNames;
	    currentParamValues = saveParamValues;
	    localVariables = saveLocalVariables;
	    position = savePosition;
	    lastPosition = saveLastPosition;
	    currentIter = saveCurrentIter;
	    if (rememberEvalGlobalVariableName != null)
	      variableValueTable.put(rememberEvalGlobalVariableName,
				     saveGlobalVariableValue);
	  }
	}
      };
    }

    public ExtensionContext getExtensionContext(String namespace) throws XSLException {
      ExtensionContext extension = (ExtensionContext)extensionTable.get(namespace);
      if (extension == null) {
	extension = extensionHandler.createContext(namespace);
	if (extension == null) {
	  extension = new ExtensionContext() {
	    public boolean available(String name) {
	      return false;
	    }
	    public Object call(String name, Node currentNode, Object[] args) throws XSLException {
	      throw new XSLException("implementation of extension namespace not available");
	    }
	  };
	}
	extensionTable.put(namespace, extension);
      }
      return extension;
    }

    public Variant getSystemProperty(Name name) {
      if (name.equals(XSL_VERSION))
	return new NumberVariant(1.0);
      if (name.equals(XSL_VENDOR))
	return new StringVariant("James Clark");
      if (name.equals(XSL_VENDOR_URL))
	return new StringVariant("http://www.jclark.com/");
      return emptyStringVariant;
    }

  }


  private boolean stripSource = false;
  private Hashtable stripSourceTable = null;
  private boolean indentResult = false;
  private String resultNamespace = null;
  private TemplateRuleSet templateRules = new TemplateRuleSet(new BuiltinAction());
  private Hashtable modeTable = new Hashtable();
  private Hashtable namedTemplateTable = new Hashtable();
  private Hashtable variableExprTable = new Hashtable();
  private Vector idAttributes = new Vector();
  private static final Action emptyAction = new EmptyAction();
  private Hashtable topLevelTable = new Hashtable();
  private Hashtable actionTable = new Hashtable();
  Importance currentImportance = Importance.create();
  Importance firstImportImportance = Importance.create();
  static NodeSetExpr childrenExpr = ExprParser.getChildrenExpr();
  VariableSet currentLocalVariables = new EmptyVariableSet();
  int nCurrentLocalVariables = 0;
  XMLProcessor parser;
  Name XSL_WHEN;
  Name XSL_OTHERWISE;
  Name XSL_STYLESHEET;
  Name XSL_TRANSFORM;
  Name XSL_WITH_PARAM;
  Name XSL_SORT;
  Name XSL_FOR_EACH;
  Name XSL_VERSION;
  Name XSL_VENDOR;
  Name XSL_VENDOR_URL;
  Name HREF;
  Name MATCH;
  Name PRIORITY;
  Name SELECT;
  Name TEST;
  Name DEFAULT_SPACE;
  Name INDENT_RESULT;
  Name RESULT_NS;
  Name NAME;
  Name NAMESPACE;
  Name DEFAULT;
  Name VALUE;
  Name ELEMENTS;
  Name ATTRIBUTE;
  Name FROM;
  Name COUNT;
  Name LEVEL;
  Name FORMAT;
  Name LETTER_VALUE;
  Name GROUPING_SIZE;
  Name GROUPING_SEPARATOR;
  Name MODE;
  Name ORDER;
  Name LANG;
  Name CASE_ORDER;
  Name DATA_TYPE;
  LoadContext sheetLoadContext;
  NameTable nameTable;
  ExtensionHandler extensionHandler;
  
  SheetImpl(Node node, XMLProcessor parser, ExtensionHandler extensionHandler,
	    LoadContext sheetLoadContext, NameTable nameTable)
    throws IOException, XSLException {
    this.sheetLoadContext = sheetLoadContext;
    this.nameTable = nameTable;
    this.parser = parser;
    this.extensionHandler = extensionHandler;
    XSL_WHEN = xsl("when");
    XSL_OTHERWISE = xsl("otherwise");
    XSL_STYLESHEET = xsl("stylesheet");
    XSL_TRANSFORM = xsl("transform");
    XSL_WITH_PARAM = xsl("with-param");
    XSL_SORT = xsl("sort");
    XSL_FOR_EACH = xsl("for-each");
    XSL_VERSION = xsl("version");
    XSL_VENDOR = xsl("vendor");
    XSL_VENDOR_URL = xsl("vendor-url");
    HREF = nameTable.createName("href");
    SELECT = nameTable.createName("select");
    TEST = nameTable.createName("test");
    MATCH = nameTable.createName("match");
    PRIORITY = nameTable.createName("priority");
    DEFAULT_SPACE = nameTable.createName("default-space");
    INDENT_RESULT = nameTable.createName("indent-result");
    RESULT_NS = nameTable.createName("result-ns");
    NAME = nameTable.createName("name");
    NAMESPACE = nameTable.createName("namespace");
    DEFAULT = nameTable.createName("default");
    VALUE = nameTable.createName("value");
    ELEMENTS = nameTable.createName("elements");
    ATTRIBUTE = nameTable.createName("attribute");
    COUNT = nameTable.createName("count");
    LEVEL = nameTable.createName("level");
    FROM = nameTable.createName("from");
    FORMAT = nameTable.createName("format");
    LETTER_VALUE = nameTable.createName("letter-value");
    GROUPING_SIZE = nameTable.createName("grouping-size");
    GROUPING_SEPARATOR = nameTable.createName("grouping-separator");
    MODE = nameTable.createName("mode");
    ORDER = nameTable.createName("order");
    LANG = nameTable.createName("lang");
    DATA_TYPE = nameTable.createName("data-type");
    CASE_ORDER = nameTable.createName("case-order");
    
    topLevelTable.put(xsl("include"), new IncludeParser());
    topLevelTable.put(xsl("import"), new ImportParser());
    topLevelTable.put(xsl("template"), new TemplateParser());
    topLevelTable.put(xsl("attribute-set"), new AttributeSetParser());
    topLevelTable.put(xsl("locale"), new LocaleParser());
    topLevelTable.put(xsl("key"), new KeyParser());
    topLevelTable.put(xsl("variable"), new VariableTopLevelParser());
    topLevelTable.put(xsl("param"), new VariableTopLevelParser());
    topLevelTable.put(xsl("strip-space"), new StripSpaceParser());
    topLevelTable.put(xsl("preserve-space"), new PreserveSpaceParser());
    actionTable.put(xsl("text"), new TextParser());
    actionTable.put(xsl("apply-templates"), new ApplyTemplatesParser());
    actionTable.put(xsl("call-template"), new CallTemplateParser());
    actionTable.put(xsl("for-each"), new ForEachParser());
    actionTable.put(xsl("value-of"), new ValueOfParser());
    actionTable.put(xsl("number"), new NumberParser());
    actionTable.put(xsl("if"), new IfParser());
    actionTable.put(xsl("choose"), new ChooseParser());
    actionTable.put(xsl("copy"), new CopyParser());
    actionTable.put(xsl("copy-of"), new CopyOfParser());
    actionTable.put(xsl("comment"), new CommentParser());
    actionTable.put(xsl("processing-instruction"), new ProcessingInstructionParser());
    actionTable.put(xsl("element"), new ElementParser());
    actionTable.put(xsl("attribute"), new AttributeParser());
    actionTable.put(xsl("apply-imports"), new ApplyImportsParser());
    actionTable.put(xsl("variable"), new VariableActionParser());
    actionTable.put(xsl("param"), new ParamActionParser());
    actionTable.put(xsl("message"), new MessageParser());
    parseSheet(node);
  }

  public
  Result process(Node node, Result root) throws XSLException {
    root.start();
    new ProcessContextImpl(node).processSafe(node, null, root);
    root.end();
    return root;
  }

  public
  boolean getIndentResult() {
    return indentResult;
  }

  public
  String getResultNamespace() {
    return resultNamespace;
  }

  private
  void parseSheet(Node rootNode) throws XSLException, IOException {
    Node sheetNode = rootNode.getChildren().next();
    if (XSL_NAMESPACE.equals(sheetNode.getName().getNamespace())) {
      if (!XSL_STYLESHEET.equals(sheetNode.getName())
	  && !XSL_TRANSFORM.equals(sheetNode.getName()))
	throw new XSLException("bad document element for stylesheet",
			       sheetNode);
      if ("strip".equals(sheetNode.getAttributeValue(DEFAULT_SPACE)))
	stripSource = true;
      if ("yes".equals(sheetNode.getAttributeValue(INDENT_RESULT)))
	indentResult = true;
      resultNamespace = sheetNode.getAttributeValue(RESULT_NS);
      if (resultNamespace != null) {
	NamespacePrefixMap map = sheetNode.getNamespacePrefixMap();
	if (resultNamespace.equals(""))
	  resultNamespace = map.getDefaultNamespace();
	else
	  resultNamespace = map.getNamespace(resultNamespace);
      }
      parseTopLevel(sheetNode);
    }
    else
      parseRootTemplate(sheetNode);
    templateRules.compile();
    for (Enumeration iter = modeTable.elements(); iter.hasMoreElements();)
      ((TemplateRuleSet)iter.nextElement()).compile();
  }

  void parseTopLevel(Node sheetNode) throws XSLException, IOException {
    for (NodeIterator iter = sheetNode.getChildren();;) {
      Node node = iter.next();
      if (node == null)
	break;
      TopLevelParser parser = null;
      try {
	Name name = node.getName();
	if (name == null)
	  throw new XSLException("illegal data characters inside xsl:stylesheet", node);
	parser = (TopLevelParser)topLevelTable.get(name);
	if (parser == null && name.getNamespace() == null)
	  throw new XSLException("illegal top-level element", node);
      }
      catch (ClassCastException e) { }
      parser.parse(node);
    }
  }

  void parseRootTemplate(Node defNode) throws XSLException {
    templateRules.add(ExprParser.parsePattern(defNode, "/"),
		      currentImportance,
		      firstImportImportance,
		      null,
		      parseActions(defNode, emptyAction));
  }

  /* Parse the attributes on node as literal attributes and then
     parse the actions. */

  Action parseAttributesAndActions(Node node) throws XSLException {
    AppendAction sequence = null;
    for (NodeIterator iter = node.getAttributes();;) {
      Node att = iter.next();
      if (att == null)
	break;
      if (sequence == null)
	sequence = new AppendAction();
      String value = att.getData();
      Name name = unquoteName(att.getName(), node);
      if (value.indexOf('{') >= 0 || value.indexOf('}') >= 0)
	sequence.add(new TemplateAttributeAction(name,
						 ExprParser.parseAttributeValueTemplate(node, value, currentLocalVariables)));
      else
	sequence.add(new LiteralAttributeAction(name, value));
    }
    return parseActions(node, null, sequence);
  }

  Action parseActions(Node node, Action ifEmpty) throws XSLException {
    return parseActions(node, ifEmpty, null);
  }
  
  Action parseActions(Node node, Action ifEmpty, AppendAction sequence) throws XSLException {
    final VariableSet startLocalVariables = currentLocalVariables;
    final int nStartLocalVariables = nCurrentLocalVariables;
    NodeIterator iter = node.getChildren();
    node = iter.next();
    if (node == null) {
      if (sequence == null)
	return ifEmpty;
      else
	return sequence;
    }
    if (sequence == null)
      sequence = new AppendAction();
    do {
      switch (node.getType()) {
      case Node.TEXT:
	sequence.add(new CharsAction(node.getData()));
	break;
      case Node.ELEMENT:
	{
	  Name name = node.getName();
	  if (!XSL_NAMESPACE.equals(name.getNamespace()))
	    sequence.add(new LiteralElementAction(unquoteName(node.getName(), node),
					          unquoteNamespacePrefixMap(node.getNamespacePrefixMap()),
					          parseAttributesAndActions(node)));
	  else {
	    ActionParser actionParser = null;
	    try {
	      actionParser = (ActionParser)actionTable.get(name);
	    }
	    catch (ClassCastException e) { }
	    if (actionParser == null) {
	      if (name.equals(XSL_SORT)
		  && XSL_FOR_EACH.equals(node.getParent().getName()))
		;
	      else
		throw new XSLException("expected action not " + name, node);
	    }
	    else
	      sequence.add(actionParser.parse(node));
	  }
	}
      }
      node = iter.next();
    } while (node != null);
    // FIXME should use finally here
    if (nStartLocalVariables != nCurrentLocalVariables) {
      sequence.add(new UnbindLocalVariablesAction(nCurrentLocalVariables - nStartLocalVariables));
      nCurrentLocalVariables = nStartLocalVariables;
      currentLocalVariables = startLocalVariables;
    }
    return sequence;
  }

  String getRequiredAttribute(Node node, Name name) throws XSLException {
    String value = node.getAttributeValue(name);
    if (value == null)
      throw new XSLException("missing attribute \"" + name + "\"", node);
    return value;
  }

  String getOptionalAttribute(Node node, Name name, String dflt) {
    String value = node.getAttributeValue(name);
    return value == null ? dflt : value;
  }

  String getData(Node node) throws XSLException {
    node = node.getChildren().next();
    if (node == null)
      return "";
    String data = node.getData();
    if (data == null)
      throw new XSLException("only character data allowed", node);
    return data;
  }

  NumberListFormatTemplate getNumberListFormatTemplate(Node node) throws XSLException {
    NumberListFormatTemplate t = new NumberListFormatTemplate();
    String value = node.getAttributeValue(FORMAT);
    if (value != null) {
      StringExpr expr = ExprParser.parseAttributeValueTemplate(node, value, currentLocalVariables);
      String format = expr.constantValue();
      if (format != null)
  	t.setFormat(format);
      else
	t.setFormat(expr);
    }
    // FIXME should be able to use attribute value templates for these
    value = node.getAttributeValue(LANG);
    if (value != null)
      t.setLang(value);
    value = node.getAttributeValue(LETTER_VALUE);
    if (value != null)
      t.setLetterValue(value);
    value = node.getAttributeValue(GROUPING_SIZE);
    if (value != null) {
      try {
	t.setGroupingSize(Integer.parseInt(value));
      }
      catch (NumberFormatException e) { }
    }
    value = node.getAttributeValue(GROUPING_SEPARATOR);
    if (value != null)
      t.setGroupingSeparator(value);
    return t;
  }

  Action addParams(ParamAction action, Node node) throws XSLException {
    for (NodeIterator iter = node.getChildren();;) {
      node = iter.next();
      if (node == null)
	break;
      if (XSL_WITH_PARAM.equals(node.getName()))
	action.addParam(expandSourceElementTypeName(getRequiredAttribute(node, NAME), node),
			getVariantExpr(node));
    }
    return action;
  }

  NodeSetExpr getSortNodeSetExpr(Node node, NodeSetExpr expr) throws XSLException {
    ComparatorTemplate comparatorTemplate = null;
    for (NodeIterator iter = node.getChildren();;) {
      node = iter.next();
      if (node == null)
	break;
      if (XSL_SORT.equals(node.getName())) {
	Locale locale = Lang.getLocale(node.getAttributeValue(LANG));
	Comparator cmp;
	String dataType = node.getAttributeValue(DATA_TYPE);
	if ("number".equals(dataType))
	  cmp = new NumberComparator(locale);
	else {
	  int caseOrder = 0;
	  String caseOrderString = node.getAttributeValue(CASE_ORDER);
	  if ("upper-first".equals(caseOrderString))
	    caseOrder = TextComparator.UPPER_FIRST;
	  else if ("lower-first".equals(caseOrderString))
	    caseOrder = TextComparator.LOWER_FIRST;
	  cmp = TextComparator.create(locale, caseOrder);
	}
	if ("descending".equals(node.getAttributeValue(ORDER)))
	  cmp = new ReverseComparator(cmp);
	ComparatorTemplate templ 
	  = new NodeComparatorTemplate(cmp,
				       ExprParser.parseStringExpr(node,
								  getOptionalAttribute(node, SELECT, "."), currentLocalVariables));
	if (comparatorTemplate == null)
	  comparatorTemplate = templ;
	else
	  comparatorTemplate = new BilevelComparatorTemplate(comparatorTemplate, templ);
      }
    }
    if (comparatorTemplate == null)
      return expr;
    return new SortNodeSetExpr(expr, comparatorTemplate);
  }

  VariantExpr getVariantExpr(Node defNode) throws XSLException {
    String expr = defNode.getAttributeValue(SELECT);
    if (expr != null)
      return ExprParser.parseVariantExpr(defNode, expr, currentLocalVariables);
    return new ResultFragmentExpr(parseActions(defNode, emptyAction), extensionHandler);
  }

  TemplateRuleSet getModeTemplateRuleSet(Name modeName) {
    if (modeName == null)
      return templateRules;
    TemplateRuleSet ruleSet = (TemplateRuleSet)modeTable.get(modeName);
    if (ruleSet == null) {
      ruleSet = new TemplateRuleSet(new BuiltinAction(modeName));
      modeTable.put(modeName, ruleSet);
    }
    return ruleSet;
  }

  static final String XSL_NAMESPACE = "http://www.w3.org/XSL/Transform/1.0";

  private Name xsl(String name) {
    return nameTable.createName("xsl:" + name, XSL_NAMESPACE);
  }
  
  Name unquoteName(Name name, Node node) {
    String ns = unquoteNamespace(name.getNamespace());
    if (ns == null)
      return name;
    return nameTable.createName(name.toString(), ns);
  }

  static private NamespacePrefixMap unquoteNamespacePrefixMap(NamespacePrefixMap map) {
    String ns = map.getDefaultNamespace();
    if (XSL_NAMESPACE.equals(ns))
      map = map.unbindDefault();
    else {
      ns = unquoteNamespace(ns);
      if (ns != null)
	map = map.bindDefault(ns);
    }
    NamespacePrefixMap newMap = map;
    int size = map.getSize();
    for (int i = 0; i < size; i++) {
      ns = map.getNamespace(i);
      if (XSL_NAMESPACE.equals(ns))
	newMap = newMap.unbind(map.getPrefix(i));
      else {
	ns = unquoteNamespace(ns);
	if (ns != null)
	  newMap = newMap.bind(map.getPrefix(i), ns);
      }
    }
    return newMap;
  }

  /* If the namespace is a quoted namespace, return the unquoted namespace.
     Otherwise return null. */

  private static String unquoteNamespace(String ns) {
    if (ns != null && ns.startsWith("quote:"))
      return ns.substring(6);
    return null;
  }

  static Name expandSourceElementTypeName(String nameString, Node node) throws XSLException {
    return node.getNamespacePrefixMap().expandAttributeName(nameString, node);
  }

  public LoadContext getSourceLoadContext() {
    return this;
  }

  public boolean getStripSource(Name elementTypeName) {
    if (stripSourceTable != null) {
      Boolean b = (Boolean)stripSourceTable.get(elementTypeName);
      if (b != null)
	return b.booleanValue();
    }
    return stripSource;
  }

  public boolean getIncludeComments() {
    return true;
  }

  public boolean getIncludeProcessingInstructions() {
    return true;
  }

}
