/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *   Free SoftwareFoundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.xsl;

import com.caucho.java.JavaWriter;
import com.caucho.java.LineMap;
import com.caucho.util.CharBuffer;
import com.caucho.util.CharScanner;
import com.caucho.util.IntArray;
import com.caucho.util.IntMap;
import com.caucho.util.L10N;
import com.caucho.util.StringCharCursor;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.xml.CauchoDocument;
import com.caucho.xml.QAbstractNode;
import com.caucho.xml.QElement;
import com.caucho.xml.QName;
import com.caucho.xml.Xml;
import com.caucho.xml.XmlChar;
import com.caucho.xpath.Expr;
import com.caucho.xpath.NamespaceContext;
import com.caucho.xpath.XPath;
import com.caucho.xpath.pattern.AbstractPattern;
import com.caucho.xpath.pattern.UnionPattern;
import com.caucho.xsl.fun.FormatNumberFun;
import com.caucho.xsl.fun.KeyFun;
import com.caucho.xsl.java.XslAttributeSet;
import com.caucho.xsl.java.XslNode;
import com.caucho.xsl.java.XslStylesheet;
import com.caucho.xsl.java.XslTemplate;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Base class for generating code from an XSL tree.  JavaGenerator and
 * JavaScriptGenerator extend this class for language-specific code
 * generation.
 */
abstract class Generator {
  private static final Logger log
    = Logger.getLogger(Generator.class.getName());
  protected static final L10N L = new L10N(Generator.class);

  public static final String XSLNS = "http://www.w3.org/1999/XSL/Transform";
  public static final String XTPNS = "http://www.caucho.com/XTP/1.0";

  private static final int STYLESHEET = 0;
  private static final int OUTPUT = STYLESHEET + 1;
  private static final int IMPORT = OUTPUT + 1;
  private static final int INCLUDE = IMPORT + 1;
  private static final int TEMPLATE = INCLUDE + 1;
  private static final int STRIP_SPACE = TEMPLATE + 1;
  private static final int PRESERVE_SPACE = STRIP_SPACE + 1;
  private static final int KEY = PRESERVE_SPACE + 1;
  private static final int LOCALE = KEY + 1;
  private static final int ATTRIBUTE_SET = LOCALE + 1;
  private static final int NAMESPACE_ALIAS = ATTRIBUTE_SET + 1;

  private static final int APPLY_TEMPLATES = NAMESPACE_ALIAS + 1;
  private static final int APPLY_IMPORTS = APPLY_TEMPLATES + 1;
  private static final int CALL_TEMPLATE = APPLY_IMPORTS + 1;
  private static final int PARAM = CALL_TEMPLATE + 1;
  private static final int VARIABLE = PARAM + 1;
  private static final int VALUE_OF = VARIABLE + 1;
  private static final int COPY_OF = VALUE_OF + 1;
  private static final int FOR_EACH = COPY_OF + 1;
  private static final int IF = FOR_EACH + 1;
  private static final int CHOOSE = IF + 1;

  private static final int TEXT = CHOOSE + 1;
  private static final int XSL_TEXT = TEXT + 1;
  private static final int NUMBER = XSL_TEXT + 1;
  private static final int COPY = NUMBER + 1;
  private static final int COPY_ELEMENT = COPY + 1;
  private static final int ELEMENT = COPY_ELEMENT + 1;
  private static final int ATTRIBUTE = ELEMENT + 1;
  private static final int PI = ATTRIBUTE + 1;
  private static final int COMMENT = PI + 1;

  private static final int MESSAGE = COMMENT + 1;

  private static final int EXPRESSION = MESSAGE + 1;
  private static final int SCRIPTLET = EXPRESSION + 1;
  private static final int DECLARATION = SCRIPTLET + 1;
  private static final int DIRECTIVE_CACHE = DECLARATION + 1;
  private static final int DIRECTIVE_PAGE = DIRECTIVE_CACHE + 1;
  private static final int WHILE = DIRECTIVE_PAGE + 1;

  private static final int ASSIGN = WHILE + 1;
  private static final int IGNORE = ASSIGN + 1;

  // xslt 2.0
  private static final int RESULT_DOCUMENT = IGNORE + 1;

  private static IntMap _tags;
  private static IntMap _xtpTags;
  private String _version = "1.0";

  String _xslName;
  // the root context
  Path _topContext;
  // the pwd for the file
  Path _baseURL;
  Path _context;
  CharBuffer _text;
  HashMap<String,String> _names = new HashMap<String,String>();
  int _loopDepth;
  Path _workPath;
  int _uniqueId;

  protected HashMap<String,String> _preserve = new HashMap<String,String>();
  protected HashMap<String,String> _strip = new HashMap<String,String>();

  HashMap<String,XslAttributeSet> _attributeSets =
    new HashMap<String,XslAttributeSet>();

  protected HashMap<String,String[]> _namespaceAliases =
    new HashMap<String,String[]>();
  protected HashMap<String,String> _excludedNamespaces =
    new HashMap<String,String>();
  protected KeyFun _keyFun;
  protected FormatNumberFun _formatNumberFun;

  protected NamespaceContext _namespace;
  protected ArrayList _globalActions = new ArrayList();
  protected ArrayList<String> _globalParameters =
    new ArrayList<String>();

  protected Document _doc;
  protected CauchoDocument _qDoc;

  protected Path _path;

  boolean _lineContent;
  int _lineWs;
  String _systemId;
  String _filename;
  int _line;
  protected LineMap _lineMap;
  private ArrayList _frags;
  protected int _destLine = 1;
  boolean _defaultCacheable = true;
  boolean _isCacheable;
  protected String _encoding;

  HashMap<String,ArrayList<Template>> _templates = new HashMap<String,ArrayList<Template>>();
  int _minImportance; // for included files, the minimum importance
  int _importance;
  int _templateCount;
  private IntArray _vars = new IntArray();
  private ArrayList<XslNode> _inits = new ArrayList<XslNode>();
  protected ArrayList<Path> _depends = new ArrayList<Path>();
  protected ArrayList<String> _cacheDepends = new ArrayList<String>();

  private boolean _isCauchoXsl;
  protected boolean _isRawText;
  protected String _errorPage;
  boolean _hasSession;
  protected AbstractPattern _nodeListContext;
  private boolean _isTop;
  private ClassLoader _loader;

  protected boolean _isSpecial;
  protected boolean _isStyleScript;

  HashMap<String,String> _outputAttributes = new HashMap<String,String>();
  HashMap<String,String> _macros;
  HashMap<String,Document> _files;
  
  protected AbstractStylesheetFactory _xslGenerator;

  protected ArrayList<String> _imports = new ArrayList<String>();

  Generator(AbstractStylesheetFactory xslGenerator)
  {
    _xslGenerator = xslGenerator;
    
    _workPath = xslGenerator.getWorkPath();

    Path path = xslGenerator.getStylePath();

    _context = path;
    _topContext = _context;
    _loader = xslGenerator.getClassLoader();
    if (_loader == null)
      _loader = Thread.currentThread().getContextClassLoader();
    
    _text = new CharBuffer();
    _frags = new ArrayList();
    _macros = new HashMap<String,String>();

    _keyFun = new KeyFun();
    _formatNumberFun = new FormatNumberFun();
  }

  void init(String filename)
  {
    _lineMap = new LineMap(filename);
  }
  
  public void setErrorPage(String errorPage)
  {
    _errorPage = errorPage;
  }

  public void setStyleScript(boolean stylescript)
  {
    _isStyleScript = stylescript;
  }

  /**
   * Adds a Java import to the generated stylesheet.
   */
  public void addImport(String pkg)
  {
    if (! _imports.contains(pkg))
      _imports.add(pkg);
  }
  
  public void setContentType(String type)
  {
    // contentType = type;
  }

  void setPath(Path path)
  {
    _path = path;
    _context = path;
  }

  void setWorkPath(Path path)
  {
    _workPath = path;
  }

  public int getMinImportance()
  {
    return _minImportance;
  }

  public int getMaxImportance()
  {
    return _importance;
  }

  public NamespaceContext getNamespace()
  {
    return _namespace;
  }

  public AbstractPattern getNodeListContext()
  {
    return _nodeListContext;
  }

  public void addLocale(String name, DecimalFormatSymbols format)
  {
    _formatNumberFun.addLocale(name, format);
  }

  /**
   * Generates a uniqueId
   */
  public int uniqueId()
  {
    return _uniqueId++;
  }

  /**
   * Starts the generation from the top of the document.
   *
   * @param xsl the stylesheet document.
   */
  public StylesheetImpl generate(Node node) throws Exception
  {
    Document xsl = node.getOwnerDocument();
    if (xsl == null)
      xsl = (Document) node;

    DocumentType dtd = xsl.getDoctype();

    if (dtd != null && dtd.getSystemId() != null) {
      _context = _path.lookup(dtd.getSystemId());
      _topContext = _context;
    }
    
    Element top = (Element) xsl.getDocumentElement();

    if (top == null)
      throw error(xsl, L.l("xsl:stylesheet must be top element."));

    _doc = xsl;
    if (_doc instanceof CauchoDocument)
      _qDoc = (CauchoDocument) _doc;

    QElement qTop = null;
    if (top instanceof QElement)
      qTop = (QElement) top;

    /*
    if (qTop != null && qTop.getFilename() != null)
      context = topContext.lookup(qTop.getFilename()).getParent();
    */
    
    _isTop = true;
    _files = new HashMap<String,Document>();
    scanFiles(top);

    if (_qDoc != null) {
      ArrayList depends = (ArrayList) _qDoc.getProperty(CauchoDocument.DEPENDS);
      for (int i = 0; depends != null && i < depends.size(); i++) {
        Path path = (Path) depends.get(i);
        addDepend(path);
      }
    }
    else {
      addDepend(_path);
    }

    if ("stylesheet".equals(getXslLocal(top)) ||
        "transform".equals(getXslLocal(top))) {
      generateStylesheet(top, true);
    }
    else {
      // literal result element
      printHeader();
      boolean oldCacheable = _isCacheable;
      boolean oldDefaultCacheable = _defaultCacheable;
      _isCacheable = true;
    
      XslNode literal = createChild(top);

      XslTemplate template = new XslTemplate();
      template.setGenerator((JavaGenerator) this);
      template.addAttribute(new QName("match"), "/");
      template.addChild(literal);

      template.generateDeclaration(getOut());
    
      template.generate(getOut());

      // printTemplate(top, null, "/", null, 0.0/0.0);
      
      _isCacheable = oldCacheable;
      _defaultCacheable = oldDefaultCacheable;
    }

    addNamespace(top);
    StylesheetImpl stylesheet = completeGenerate(_inits, _globalActions);

    return stylesheet;
  }

  private static CharScanner commaDelimScanner = new CharScanner(" \t\n\r,");

  /**
   * Scan the stylesheet for imported packages and files.  The imported
   * stylesheets will be read and stored in the 'files' HashMap for when
   * they're actually needed.
   */
  private void scanFiles(Element top)
    throws XslParseException, IOException
  {
    _isCauchoXsl = ! top.getAttribute("xsl-caucho").equals("");

    Iterator iter;
    try {
      iter = XPath.select("//xtp:directive.page/@*", top);
    } catch (Exception e) {
      throw new XslParseException(e);
    }
    while (iter.hasNext()) {
      Attr attr = (Attr) iter.next();
      String name = attr.getNodeName();
      String value = attr.getNodeValue();

      if (name.equals("import")) {
        StringCharCursor cursor = new StringCharCursor(value);
        CharBuffer cb = new CharBuffer();
        while (cursor.current() != cursor.DONE) {
          char ch;
          commaDelimScanner.skip(cursor);

          cb.clear();
          ch = commaDelimScanner.scan(cursor, cb);

          if (cb.length() != 0) {
            addImport(cb.toString());
          }
          else if (ch != cursor.DONE)
            throw new IOException(L.l("illegal `import' directive"));
        }
      }
    }

    try {
      iter = XPath.select("//xsl:import|xsl:include", top);
    } catch (Exception e) {
      throw new XslParseException(e);
    }
    while (iter.hasNext()) {
      Element elt = (Element) iter.next();
      String href = elt.getAttribute("href");

      ReadStream rs;

      try {
        rs = _xslGenerator.openPath(href, _context.getURL());
      } catch (Exception e) {
        throw new XslParseException(e);
      }

      Path path = rs.getPath();
      
      Document xsl = readXsl(rs);
      Element subtop = xsl.getDocumentElement();

      if (subtop == null)
        throw error(elt, L.l("xsl:import file {0} is empty", path.getFullPath()));
      
      Path oldContext = _context;

      Path virtualPath = _context.getParent().lookup(href);
      _context = virtualPath;

      _files.put(virtualPath.getPath(), xsl);
      
      scanFiles(subtop);
      _context = oldContext;
    }
  }

  public void addImportList(String value)
    throws XslParseException
  {
    StringCharCursor cursor = new StringCharCursor(value);
    CharBuffer cb = new CharBuffer();
    while (cursor.current() != cursor.DONE) {
      char ch;
      commaDelimScanner.skip(cursor);

      cb.clear();
      ch = commaDelimScanner.scan(cursor, cb);

      if (cb.length() != 0) {
        addImport(cb.toString());
      }
      else if (ch != cursor.DONE)
        throw error(L.l("illegal `import' directive"));
    }
  }

  /**
   * Read in an imported or included XSL file.
   *
   * @param path Path to the include files.
   *
   * @return XML tree describing the XSL.
   */
  Document readXsl(Path path)
    throws IOException, XslParseException
  {
    return readXsl(path.openRead());
  }

  /**
   * Read in an imported or included XSL file.
   *
   * @param path Path to the include files.
   *
   * @return XML tree describing the XSL.
   */
  Document readXsl(ReadStream file)
    throws IOException, XslParseException
  {
    try {
      addDepend(file.getPath());
      
      if (_isStyleScript) {
        XslParser parser = new XslParser();

        return parser.parse(file);
      }
      else
        return new Xml().parseDocument(file);
    } catch (org.xml.sax.SAXException e) {
      throw new XslParseException(e);
    } finally {
      file.close();
    }
  }

  /**
   * Start the top-level stylesheet generation.
   */
  private void generateStylesheet(Element elt, boolean isTop)
    throws Exception
  {
    QElement element = (QElement) elt;
    _isCauchoXsl = ! element.getAttribute("xsl-caucho").equals("");

    String systemId = element.getBaseURI();
    Path oldContext = _context;

    if (systemId != null)
      _context = _context.lookup(systemId);
    
    XslNode stylesheet = createChild(element);

    addNamespace(element);
    
    if (isTop)
      printHeader();

    stylesheet.generateDeclaration(getOut());
    
    stylesheet.generate(getOut());

    _context = oldContext;

    /*
    _version = element.getAttribute("version");
    if (_version.equals(""))
      _version = "1.0";
    
    // generateAttributeSets(element);

    excludeNamespaces(element);

    String xslSpace = element.getAttribute("xsl-space");
    ArrayList<XslNode> children = new ArrayList<XslNode>();
    for (Node child = element.getFirstChild();
         child != null;
         child = child.getNextSibling()) {
      if (! (child instanceof Element))
        continue;
      
      int code = -1;
      String name = getXslLocal(child);
      if (name != null)
        code = _tags.get(name);
      else if ((name = getXtpLocal(child)) != null)
        code = _xtpTags.get(name);
      else {
        String childName = child.getNodeName();
        
        if (childName.startsWith("jsp:directive.") ||
            childName.equals("jsp:declaration") ||
            childName.equals("jsp:scriptlet"))
          addGlobalAction((Element) child);
        continue;
      }

      NamespaceContext oldNamespace = addNamespace((Element) child);
      // generateTopLevelNode(code, (Element) child);
      XslNode node = createChild(child);

      children.add(node);
      _namespace = oldNamespace;
    }

    for (int i = 0; i < children.size(); i++) {
      XslNode node = children.get(i);
      
      node.generate(getOut());
    }
    */
  }

  abstract protected JavaWriter getOut();

  abstract protected XslNode createChild(Node child)
    throws Exception;
  
  abstract protected XslNode createChild(XslNode parent, Node child)
    throws Exception;

  private void addGlobalAction(Element elt)
  {
    _globalActions.add(elt);
  }

  /**
   * Converts the exclude-result-prefixes into the "excludedNamespaces"
   * hashMap for later use.
   */
  private void excludeNamespaces(Element element)
    throws Exception
  {
    if (! (element instanceof QElement))
      return;
    
    QElement elt = (QElement) element;
    String excludeNamespace;
    excludeNamespace = element.getAttribute("exclude-result-prefixes");
    if (! excludeNamespace.equals("")) {
      for (String prefix : excludeNamespace.split("[,\\s]+")) {

        String ns = elt.getNamespace(prefix);
        if (ns == null)
          throw error(elt, L.l("`{0}' must be a namespace prefix",
                               prefix));
        _excludedNamespaces.put(ns, "");
      }
    }
  }

  public void addExcludedNamespace(String ns)
  {
    _excludedNamespaces.put(ns, "");
  }

  public void addInit(XslNode node)
  {
    _inits.add(node);
  }

  public void addGlobalParameter(String param)
  {
    _globalParameters.add(param);
  }

  /**
   * Adds a file dependency for cacheable references to the output of
   * the stylesheet.
   */
  private void addCacheDepends(String attr)
  {
    if (attr.equals(""))
      return;

    int i = 0;
    int ch = 0;
    int len = attr.length();
    for (;
         i < len && XmlChar.isWhitespace((ch = attr.charAt(i))) || ch == ',';
         i++) {
    }
    CharBuffer cb = new CharBuffer();
    while (i < len) {
      cb.clear();
      for (;
           i < len && ! XmlChar.isWhitespace((ch = attr.charAt(i))) &&
             ch != ',';
           i++) {
        cb.append((char) ch);
      }

      _cacheDepends.add(cb.toString());

      for (;
           i < len && XmlChar.isWhitespace((ch = attr.charAt(i))) || ch == ',';
           i++) {
      }
    }
  }

  private void generateCacheDepends(String attr)
    throws Exception
  {
    if (attr.equals(""))
      return;

    int i = 0;
    int ch = 0;
    int len = attr.length();
    for (;
         i < len && XmlChar.isWhitespace((ch = attr.charAt(i))) || ch == ',';
         i++) {
    }
    CharBuffer cb = new CharBuffer();
    while (i < len) {
      cb.clear();
      for (;
           i < len && ! XmlChar.isWhitespace((ch = attr.charAt(i))) &&
             ch != ',';
           i++) {
        cb.append((char) ch);
      }

      printCacheDepends(cb.toString());

      for (;
           i < len && XmlChar.isWhitespace((ch = attr.charAt(i))) || ch == ',';
           i++) {
      }
    }
  }

  /**
   * Generate code for xsl:template
   */
  void generateTemplate(Element element)
    throws Exception
  {
    String name = element.getAttribute("name");
    String patternString = element.getAttribute("match");
    String mode = element.getAttribute("mode");
    String priority = element.getAttribute("priority");
    double dPriority = 0.0/0.0;

    if (! name.equals(""))
      _macros.put(name, name);

    if (name.equals("") && patternString.equals(""))
      throw error("xsl:template expects a `name' or a `match' attribute.");

    if (! priority.equals("")) {
      try {
        dPriority = Double.valueOf(priority).doubleValue();
      } catch (Exception e) {
        throw error("xsl:template expects `priority' must be a double.");
      }
    }

    boolean oldCacheable = _isCacheable;
    boolean oldDefaultCacheable = _defaultCacheable;
    AbstractPattern oldNodeListContext = _nodeListContext;
    if (! patternString.equals(""))
      _nodeListContext = parseMatch(patternString);

    _isCacheable = true;
    printTemplate(element, name, patternString, mode, dPriority);
    _nodeListContext = oldNodeListContext;
    _isCacheable = oldCacheable;
    _defaultCacheable = oldDefaultCacheable;
  }

  public XslNode generateImport(String href)
    throws Exception
  {
    Path path = lookupPath(href);

    if (_files.get(path.getPath()) != null)
      return null;

    Document xsl = readFile(href, path);
    
    if (xsl == null)
      throw new FileNotFoundException(href);

    QElement top = (QElement) xsl.getDocumentElement();
    if (top == null ||
        ! "stylesheet".equals(getXslLocal(top)) &&
        ! "transform".equals(getXslLocal(top))) {
      throw error("imported stylesheet `" + href + 
                  "' missing xsl:stylesheet.");
    }

    int oldMinImportance = _minImportance;
    Path oldContext = _context;
    Path virtualPath = _context.getParent().lookup(href);
    _context = virtualPath;
    _minImportance = _importance;
    boolean oldTop = _isTop;
    boolean oldRaw = _isRawText;
    _isTop = false;
    _isRawText = false;
    
    String systemId = top.getBaseURI();

    if (systemId != null)
      _context = _context.lookup(systemId);
    
    XslStylesheet stylesheet = (XslStylesheet) createChild(top);

    _isRawText = oldRaw;
    _isTop = oldTop;
    _minImportance = oldMinImportance;
    _context = oldContext;

    incrementImportance();

    return stylesheet;
  }

  /**
   * Generates code for xsl:include.  The included file has the same
   * importance as the containing file.
   */
  void generateInclude(Element element)
    throws Exception
  {
    String href = element.getAttribute("href");
    if (href.equals(""))
      throw error("xsl:include expects `href' attribute.");
    
    if (element.getFirstChild() != null)
      throw error("xsl:include must be empty");
  }

  public void generateInclude(XslNode parent, String href)
    throws Exception
  {
    Path path = lookupPath(href);

    if (_files.get(path.getPath()) != null)
      return;
    
    Document xsl = readFile(href, path);
    
    Element top = (Element) xsl.getDocumentElement();
    if (top == null ||
        ! "stylesheet".equals(getXslLocal(top)) &&
        ! "transform".equals(getXslLocal(top))) {
      throw error("imported stylesheet `" + href + 
                  "' missing xsl:stylesheet.");
    }

    Path oldContext = _context;
    _context = path;

    for (Node node = top.getFirstChild();
         node != null;
         node = node.getNextSibling()) {
      XslNode child = createChild(parent, node);

      if (child != null)
        parent.addChild(child);
    }
    
    // generateStylesheet(top, false);
    
    _context = oldContext;
  }

  /**
   * Returns the actual path for the relative href.
   */
  private Path lookupPath(String href)
  {
    return _context.getParent().lookup(href);
  }

  private Document readFile(String href, Path virtualPath)
    throws Exception
  {
    Document xsl = (Document) _files.get(virtualPath.getPath());

    if (xsl != null)
      throw new IllegalStateException(L.l("'{0}' is a duplicated path",
                                          virtualPath.getPath()));
    
    ReadStream rs;

    try {
      rs = _xslGenerator.openPath(href, _context.getURL());
    } catch (Exception e) {
      throw new XslParseException(e);
    }

    Path path = rs.getPath();
      
    xsl = readXsl(rs);
    Element subtop = xsl.getDocumentElement();

    if (subtop == null)
      throw error(L.l("xsl:import file {0} is empty", path.getFullPath()));
      
    Path oldContext = _context;

    _context = virtualPath;

    _files.put(virtualPath.getPath(), xsl);
      
    _context = oldContext;

    return xsl;
  }

  void generateKey(Element element)
    throws Exception
  {
    String name = element.getAttribute("name");
    if (name.equals(""))
      throw error("xsl:key expects `name' attribute.");
    String match = element.getAttribute("match");
    if (match.equals(""))
      throw error("xsl:key expects `match' attribute.");
    String use = element.getAttribute("use");
    if (use.equals(""))
      throw error("xsl:key expects `use' attribute.");
    
    if (element.getFirstChild() != null)
      throw error("xsl:key must be empty");

    _keyFun.add(name, parseMatch(match), parseExpr(use));
  }

  public void addKey(String name, AbstractPattern match, Expr use)
  {
    _keyFun.add(name, match, use);
  }

  void generateLocale(Element element)
    throws Exception
  {
    String name = element.getAttribute("name");
    if (name.equals(""))
      name = "*";

    DecimalFormatSymbols format = new DecimalFormatSymbols();

    String value = element.getAttribute("decimal-separator");
    if (value.length() > 0) 
      format.setDecimalSeparator(value.charAt(0));

    value = element.getAttribute("grouping-separator");
    if (value.length() > 0) 
      format.setGroupingSeparator(value.charAt(0));

    value = element.getAttribute("infinity");
    if (! value.equals(""))
      format.setInfinity(value);

    value = element.getAttribute("minus-sign");
    if (value.length() > 0)
      format.setMinusSign(value.charAt(0));

    value = element.getAttribute("NaN");
    if (! value.equals(""))
      format.setNaN(value);

    value = element.getAttribute("percent");
    if (value.length() > 0)
      format.setPercent(value.charAt(0));

    value = element.getAttribute("per-mille");
    if (value.length() > 0)
      format.setPerMill(value.charAt(0));

    value = element.getAttribute("zero-digit");
    if (value.length() > 0)
      format.setZeroDigit(value.charAt(0));

    value = element.getAttribute("digit");
    if (value.length() > 0)
      format.setDigit(value.charAt(0));

    value = element.getAttribute("pattern-separator");
    if (value.length() > 0)
      format.setPatternSeparator(value.charAt(0));

    _formatNumberFun.addLocale(name, format);
  }

  void generateNamespaceAlias(Element element)
    throws Exception
  {
    if (! (element instanceof QElement))
      return;
    
    QElement elt = (QElement) element;
    String stylesheetPrefix;
    String resultPrefix;

    stylesheetPrefix = (String) element.getAttribute("stylesheet-prefix");
    resultPrefix = (String) element.getAttribute("result-prefix");

    if (stylesheetPrefix.equals(""))
      throw error(element, "xsl:namespace-alias needs `stylesheet-prefix'");
    if (resultPrefix.equals(""))
      throw error(element, "xsl:namespace-alias needs `result-prefix'");
  }

  public void addNamespaceAlias(String stylesheetPrefix,
                                String resultPrefix)
  {
    /*
    String stylesheetNs = getNamespace(stylesheetPrefix);
    if (stylesheetPrefix.equals("#default"))
      stylesheetNs = "";
    else if (stylesheetNs.equals(""))
      throw error("`" + stylesheetPrefix + "' is not a valid namespace prefix");
    String resultNs =  getNamespace(resultPrefix);
    if (resultPrefix.equals("#default")) {
      resultPrefix = "";
      resultNs = "";
    }
    else if (resultNs.equals(""))
      throw error("`" + resultPrefix + "' is not a valid namespace prefix");
    
    String result[] = new String[] { resultPrefix, resultNs };
    _namespaceAliases.put(stylesheetNs, result);
    */
  }

  public void addNamespaceAlias(String namespace, String []result)
  {
    _namespaceAliases.put(namespace, result);
  }

  public String []getNamespaceAlias(String namespace)
  {
    return _namespaceAliases.get(namespace);
  }

  /**
   * Scans through the stylesheet, grabbing the attribute set
   * definitions.
   *
   * @param element the current nost
   */
  /*
  void generateAttributeSets(Element element)
    throws Exception
  {
    Node child = element.getFirstChild();

    for (; child != null; child = child.getNextSibling()) {
      if (! "attribute-set".equals(getXslLocal(child)))
        continue;

      QElement elt = (QElement) child;
      String name = elt.getAttribute("name");
      if (name.equals(""))
        throw error(L.l("xsl:attribute-set expects `name' attribute."));

      generateAttributeSet(element, name);
    }
  }
  */

  /**
   * Scans through the stylesheet, grabbing the attribute set
   * definitions.
   *
   * @param element the current node
   */
  /*
  HashMap<String,String> generateAttributeSet(Element element, String setName)
    throws Exception
  {
    Node child = element.getFirstChild();

    for (; child != null; child = child.getNextSibling()) {
      if (! "attribute-set".equals(getXslLocal(child)))
        continue;

      QElement elt = (QElement) child;
      String name = elt.getAttribute("name");
      if (name.equals(""))
        throw error(L.l("xsl:attribute-set expects `name' attribute."));

      if (! name.equals(setName))
        continue;

      HashMap<String,String> set = _attributeSets.get(name);
      if (set != null)
        return set;

      set = new HashMap<String,String>();
      _attributeSets.put(name, set);

      // add any attributes from use-attribute-sets
      for (Node attr = elt.getFirstAttribute();
           attr != null;
           attr = attr.getNextSibling()) {
        if (attr.getNodeName().equals("use-attribute-sets")) {
          HashMap<String,String> subset = generateAttributeSet(element, attr.getNodeValue());
          
          if (subset == null)
            throw error(elt, L.l("Unknown attribute-set `{0}'.  Each use-attribute-sets needs a matching xsl:attribute-set.", attr.getNodeValue()));
          Iterator<String> iter = subset.keySet().iterator();
          while (iter.hasNext()) {
            String key = iter.next();
            set.put(key, subset.get(key));
          }
        }
      }

      for (Node attr = elt.getFirstChild();
           attr != null;
           attr = attr.getNextSibling()) {
        if (! "attribute".equals(getXslLocal(attr)))
          continue;
        Element attrElt = (Element) attr;
        String attrName = attrElt.getAttribute("name");
        if (attrName.equals(""))
          throw error(L.l("xsl:attribute expects `name' attribute."));

        set.put(attrName, ((QElement) attr).getTextValue());
      }

      for (Attr attr = ((QElement) elt).getFirstAttribute();
           attr != null;
           attr = (Attr) attr.getNextSibling()) {
        if (attr.getNodeName().equals("name") ||
            attr.getNodeName().equals("use-attribute-sets"))
          continue;

        set.put(attr.getNodeName(), attr.getNodeValue());
      }

      return set;
    }

    return null;
  }
  */

  public void addAttributeSet(String name, XslAttributeSet attributeSet)
  {
    _attributeSets.put(name, attributeSet);
  }

  /*
  public XslAttributeSet getAttributeSet(String name)
  {
    return _attributeSets.get(name);
  }
  */

  public void setDisableOutputEscaping(boolean disable)
  {
    _isRawText = disable;
  }

  public boolean getDisableOutputEscaping()
  {
    return _isRawText;
  }
  
  private void generateOutput(Element element)
    throws Exception
  {
    Node attr = ((QElement) element).getFirstAttribute();

    if (element.getFirstChild() != null)
      throw error("xsl:output must be empty");

    String disableEscaping;
    disableEscaping = element.getAttribute("resin:disable-output-escaping");
    if (disableEscaping.equals(""))
      disableEscaping = element.getAttribute("disable-output-escaping");
    if (disableEscaping.equals("no") ||
        disableEscaping.equals("false"))
      _isRawText = false;
    else if (! disableEscaping.equals(""))
      _isRawText = true;

    // Only top-level xsl:output matters (XXX: spec?)
    if (! _isTop)
      return;

    if (_outputAttributes == null)
      _outputAttributes = new HashMap<String,String>();

    for (; attr != null; attr = attr.getNextSibling()) {
      _outputAttributes.put(attr.getNodeName(), attr.getNodeValue());
    }
  }

  public void setOutputAttribute(String name, String value)
  {
    _outputAttributes.put(name, value);
  }

  private void generatePreserveSpace(Element element)
    throws Exception
  {
    String elements = element.getAttribute("elements");
    if (elements.equals(""))
      throw error("xsl:preserve-space expects `elements' attribute.");
    
    if (element.getFirstChild() != null)
      throw error("xsl:preserve-space must be empty");
    
    int i = 0;
    int len = elements.length();
    for (; i < len && XmlChar.isWhitespace(elements.charAt(i)); i++) {
    }
    CharBuffer cb = new CharBuffer();
    while (i < len) {
      cb.clear();

      for (; i < len && ! XmlChar.isWhitespace(elements.charAt(i)); i++)
        cb.append(elements.charAt(i));

      _preserve.put(cb.toString(), "true");

      for (; i < len && XmlChar.isWhitespace(elements.charAt(i)); i++) {
      }
    }
  }

  private void generateStripSpace(Element element)
    throws Exception
  {
    throw new UnsupportedOperationException();
    /*
    String elements = element.getAttribute("elements");
    if (elements.equals(""))
      throw error("xsl:strip-space expects `elements' attribute.");
    
    if (element.getFirstChild() != null)
      throw error("xsl:strip-space must be empty");
    */
  }

  public void addStripSpace(String elements)
  {
    int i = 0;
    int len = elements.length();
    for (; i < len && XmlChar.isWhitespace(elements.charAt(i)); i++) {
    }
    CharBuffer cb = new CharBuffer();
    while (i < len) {
      cb.clear();

      for (; i < len && ! XmlChar.isWhitespace(elements.charAt(i)); i++) {
        cb.append(elements.charAt(i));
      }

      _strip.put(cb.toString(), "true");

      for (; i < len && XmlChar.isWhitespace(elements.charAt(i)); i++) {
      }
    }
  }

  public void addPreserveSpace(String elements)
  {
    int i = 0;
    int len = elements.length();
    for (; i < len && XmlChar.isWhitespace(elements.charAt(i)); i++) {
    }
    CharBuffer cb = new CharBuffer();
    while (i < len) {
      cb.clear();

      for (; i < len && ! XmlChar.isWhitespace(elements.charAt(i)); i++) {
        cb.append(elements.charAt(i));
      }

      _preserve.put(cb.toString(), "true");

      for (; i < len && XmlChar.isWhitespace(elements.charAt(i)); i++) {
      }
    }
  }

  protected void generateChildren(Node node)
    throws Exception
  {
    _vars.add(0);
    for (Node child = node.getFirstChild();
         child != null;
         child = child.getNextSibling()) {
      generateChild(child);
    }
    int count = _vars.pop();
    if (count > 0 && _vars.size() > 0)
      printPopScope(count);
  }
  
  protected void generateChild(Node child)
    throws Exception
  {
    generateChildImpl(child);
  }

  public void generateChildImpl(Node child)
    throws Exception
  {
    String nodeName = getXslLocal(child);
    int code = -1;
    if (nodeName != null)
      code = _tags.get(nodeName);
    else if ((nodeName = getXtpLocal(child)) != null)
      code = _xtpTags.get(nodeName);
    /* XXX: xsl/04al
    else if (macros.get(child.getNodeName()) != null) {
      generateMacro((Element) child);
      return;
    }
    */

    if (nodeName == null) {
      if (child.getNodeType() == child.TEXT_NODE)
        generateText(child);
      else if (child.getNodeType() == child.ELEMENT_NODE) {
        NamespaceContext oldNamespace = addNamespace((Element) child);
        printElement((Element) child);
        _namespace = oldNamespace;
      }
      return;
    }

    if (child instanceof QElement) {
      NamespaceContext oldNamespace = addNamespace((QElement) child);
      generateChild(child, code);
      _namespace = oldNamespace;
    }
    else
      generateChild(child, code);
  }
  
  public void generateChild(Node child, int code)
    throws Exception
  {
    if (child instanceof QAbstractNode) {
      QAbstractNode qChild = (QAbstractNode) child;
      setLocation(qChild.getBaseURI(), qChild.getFilename(), qChild.getLine());
    }

    switch (code) {
    case TEXT:
      generateText(child);
      break;

    case XSL_TEXT:
      generateXslText((Element) child);
      break;

    case APPLY_TEMPLATES:
      generateApplyTemplates((Element) child);
      break;

    case APPLY_IMPORTS:
      generateApplyImports((Element) child);
      break;

    case CALL_TEMPLATE:
      generateCallTemplate((Element) child);
      break;

    case PARAM:
      generateParamVariable((Element) child);
      break;

    case VARIABLE:
      generateVariable((Element) child);
      break;

    case VALUE_OF:
      generateValueOf((Element) child);
      break;

    case COPY_OF:
      generateCopyOf((Element) child);
      break;

    case FOR_EACH:
      generateForEach((Element) child);
      break;

    case IF:
      generateIf((Element) child);
      break;

    case CHOOSE:
      generateChoose((Element) child);
      break;

    case NUMBER:
      generateNumber((Element) child);
      break;

    case COPY:
      printCopy((Element) child);
      break;

    case COPY_ELEMENT:
      printCopyElement((Element) child);
      break;

    case ELEMENT:
      generateElement((Element) child);
      break;

    case ATTRIBUTE:
      generateAttribute((Element) child);
      break;

    case PI:
      printPi((Element) child);
      break;

    case COMMENT:
      printComment((Element) child);
      break;

    case MESSAGE:
      printMessage((Element) child);
      break;

    case EXPRESSION:
      if (! _defaultCacheable)
        _isCacheable = false;
      printExpression((Element) child);
      break;

    case SCRIPTLET:
      if (! _defaultCacheable)
        _isCacheable = false;
      printScriptlet((Element) child);
      break;

    case DIRECTIVE_CACHE:
      generateCacheDepends(((Element) child).getAttribute("file"));
      if (! ((Element) child).getAttribute("no-cache").equals("")) {
        _isCacheable = false;
        _defaultCacheable = false;
      }
      else
        _defaultCacheable = true;
      break;

    case WHILE:
      generateWhile((Element) child);
      break;

    case ASSIGN:
      generateAssign((Element) child);
      break;

    case RESULT_DOCUMENT:
      generateResultDocument((Element) child);
      break;

    case IGNORE:
      break;

    default:
      if (child instanceof QElement &&
          XSLNS.equals(((QElement) child).getNamespaceURI()) &&
          _version != null && _version.equals("1.0"))
        throw error(child, "unknown XSL element `" +
                    child.getNodeName() + "'");
      else {
        Node subchild = child.getFirstChild();
        boolean hasFallback = false;
        for (; subchild != null; subchild = subchild.getNextSibling()) {
          String local = getXslLocal(subchild);
          if (local != null && local.equals("fallback")) {
            hasFallback = true;
            generateChildren(subchild);
          }
        }
        if (! hasFallback) // && child.getNamespace().equals(XSLNS))
          printError(L.l("expected xsl tag at `{0}'",
                         child.getNodeName()));
      }
      break;
    }
  }

  private void generateText(Node node)
    throws Exception
  {
    String data = node.getNodeValue();
    int length = data.length();

    if (length == 0)
      return;

    int i = 0;
    for (; i < length && XmlChar.isWhitespace(data.charAt(i)); i++) {
    }

    if (i == length && stripNode(node))
      return;

    if (data != null && data.length() > 0 && node instanceof QAbstractNode) {
      setLocation(node);
      writeText(data);
    }
  }

  private boolean stripNode(Node node)
  {
    for (node = node.getParentNode();
         node != null;
         node = node.getParentNode()) {
      if (node instanceof Element) {
        Element elt = (Element) node;
        String space = elt.getAttribute("xml:space");
        if (! space.equals(""))
          return ! space.equals("preserve");
      }
    }

    return true;
  }

  void generateXslText(Element element)
    throws Exception
  {
    _text.clear();
    for (Node child = element.getFirstChild();
         child != null;
         child = child.getNextSibling()) {
      if (! (child instanceof Text))
        continue;

      String data = child.getNodeValue();
      int length = data.length();
      
      _text.append(data);
    }

    String disableEscaping = element.getAttribute("disable-output-escaping");
    if (disableEscaping.equals(""))
      disableEscaping = "no";

    if (_text.length() <= 0) {
    }
    else if (! disableEscaping.equals("yes") &&
             ! disableEscaping.equals("true"))
      writeText(_text.toString());
    else {
      startDisableEscaping();
      writeText(_text.toString());
      endDisableEscaping();
    }
  }

  private void generateApplyTemplates(Node node)
    throws Exception
  {
    QElement element = (QElement) node;
    String select = element.getAttribute("select");
    String mode = element.getAttribute("mode");
    AbstractPattern selectPattern = null;
    if (! select.equals(""))
      selectPattern = parseSelect(select, node);

    Sort []sort = generateSort(node);

    if (sort != null && selectPattern == null)
      selectPattern = parseSelect("*", node);

    pushCall();
    generateArgs(element);
    printApplyTemplates(selectPattern, mode, sort);
    popCall();
  }

  private void generateApplyImports(Node node)
    throws Exception
  {
    QElement element = (QElement) node;
    String mode = element.getAttribute("mode");

    if (element.getFirstChild() != null)
      throw error(L.l("xsl:apply-imports must be empty"));

    pushCall();
    generateArgs(element);
    printApplyImports(mode, _minImportance, _importance);
    popCall();
  }

  private void generateCallTemplate(Element element)
    throws Exception
  {
    String name = element.getAttribute("name");
    String mode = element.getAttribute("mode");

    if (name.equals(""))
      throw error(L.l("{0} expects `{1}' attribute",
                      "xsl:call-template", "name"));

    if (findMacro(name) == null)
      throw error(element, L.l("`{0}' is an unknown macro for xsl:call-template.  All macros must be defined in an <xsl:template name='...'> element.", name));

    pushCall();
    generateArgs(element);
    printCallTemplate(name, mode);
    popCall();
  }

  private Element findMacro(String name)
    throws Exception
  {
    Element template = findMacroInDocument(_doc, name);
    if (template != null)
      return template;
    
    Iterator iter = _files.values().iterator();
    while (iter.hasNext()) {
      Document doc = (Document) iter.next();

      template = findMacroInDocument(doc, name);

      if (template != null)
        return template;
    }

    return null;
  }

  private Element findMacroInDocument(Document doc, String name)
  {
    Element elt = doc.getDocumentElement();

    for (Node child = elt.getFirstChild();
         child != null;
         child = child.getNextSibling()) {
      if (! child.getNodeName().equals("xsl:template"))
        continue;

      Element template = (Element) child;

      if (template.getAttribute("name").equals(name))
        return template;
    }

    return null;
  }

  private void generateMacro(Element element)
    throws Exception
  {
    QElement elt = (QElement) element;

    String name = element.getNodeName();
    String mode = element.getAttribute("mode");

    pushCall();
    for (Node node = elt.getFirstAttribute();
         node != null;
         node = node.getNextSibling()) {
      String argName = node.getNodeName();
      String argValue = node.getNodeValue();

      printParam(argName, argValue, elt);
    }
    printParam("contents", elt);
    printCallTemplate(name, mode);
    popCall();
  }

  private void generateArgs(Element element)
    throws Exception
  {
    for (Node node = element.getFirstChild(); node != null;
         node = node.getNextSibling()) {
      String localName = getXslLocal(node);

      if ("with-param".equals(localName)) {
        String key = ((Element) node).getAttribute("name");
        String expr = ((Element) node).getAttribute("select");
        if (key.equals(""))
          throw error(L.l("{0} requires `{1}' attribute",
                          "xsl:with-param", "name"));

        if (! expr.equals(""))
          printParam(key, parseExpr(expr));
        else
          printParam(key, node);
      }
    }
  }

  private void generateParamVariable(Element element)
    throws Exception
  {
    int i = _vars.size() - 1;
    _vars.set(i, _vars.get(i) + 1);
    
    String name = element.getAttribute("name");
    String expr = element.getAttribute("select");
    if (name.equals(""))
      throw error(L.l("{0} expects `{1}' attribute",
                      "xsl:param", "name"));

    if (! expr.equals(""))
      printParamVariable(name, parseExpr(expr));
    else
      printParamVariable(name, element);
  }

  private void generateVariable(Element element)
    throws Exception
  {
    int i = _vars.size() - 1;
    _vars.set(i, _vars.get(i) + 1);

    String name = element.getAttribute("name");
    String expr = element.getAttribute("select");
    if (name.equals(""))
      throw error(L.l("{0} expects `{1}' attribute.",
                      "xsl:variable", "name"));

    if (! expr.equals(""))
      printVariable(name, parseExpr(expr));
    else
      printVariable(name, element);
  }

  private void generateAssign(Element element)
    throws Exception
  {
    String name = element.getAttribute("name");
    String expr = element.getAttribute("select");
    if (name.equals(""))
      throw error(L.l("{0} expects `{1}' attribute.",
                      "xtp:assign", "name"));

    if (! expr.equals(""))
      printAssign(name, parseExpr(expr));
    else
      printAssign(name, element);
  }

  private void generateResultDocument(Element element)
    throws Exception
  {
    String href = element.getAttribute("href");
    String format = element.getAttribute("format");
    if (href.equals(""))
      throw error(L.l("{0} expects `{1}' attribute.",
                      "xtp:result-document", "href"));

    printResultDocument(element, href, format);
  }

  private void generateValueOf(Element element)
    throws Exception
  {
    String select = element.getAttribute("select");
    if (select.equals(""))
      throw error(L.l("{0} expects `{1}' attribute.",
                      "xsl:value-of", "select"));
    
    if (element.getFirstChild() != null)
      throw error(L.l("{0} must be empty", "xsl:value-of"));

    String disable = element.getAttribute("disable-output-escaping");
    boolean isDisabled = disable.equals("yes");
    if (isDisabled)
      startDisableEscaping();

    printSelectValue(select, element);

    if (isDisabled)
      endDisableEscaping();
  }

  private void generateCopyOf(Element element)
    throws Exception
  {
    String select = element.getAttribute("select");
    if (select.equals(""))
      throw error(L.l("{0} expects `{1}' attribute",
                      "xsl:copy-of", "select"));

    if (element.getFirstChild() != null)
      throw error(L.l("{0} must be empty", "xsl:copy-of"));
    
    printCopyOf(select, element);
  }

  /**
   * Generates code for the xsl:for-each element
   */
  void generateForEach(Element element)
    throws Exception
  {
    String select = element.getAttribute("select");
    if (select.equals(""))
      throw error(L.l("{0} expects `{1}' attribute",
                      "xsl:for-each", "select"));

    Sort []sort = generateSort(element);

    if (sort != null)
      printForEach(element, select, sort);
    else
      printForEach(element, select);
  }

  private Sort []generateSort(Node node)
    throws XslParseException, IOException
  {
    ArrayList<Sort> sorts = new ArrayList<Sort>();
  sort:
    for (Node child = node.getFirstChild();
         child != null;
         child = child.getNextSibling()) {
      if (child.getNodeType() == child.TEXT_NODE) {
        String data = child.getNodeValue();
        for (int i = 0; i < data.length(); i++) {
          if (! XmlChar.isWhitespace(data.charAt(i)))
            break sort;
        }
        continue;
      }
      else if (child.getNodeType() == child.COMMENT_NODE ||
               child.getNodeType() == child.PROCESSING_INSTRUCTION_NODE)
        continue;

      String name = getXslLocal(child);
      if (! "sort".equals(name))
        break;

      Element elt = (Element) child;
      String select = elt.getAttribute("select");
      if (select.equals(""))
        throw error(L.l("{0} expects attribute `{1}'",
                        "xsl:sort", "select"));

      Expr expr = parseExpr(select);
      String order = elt.getAttribute("order");
      Expr isAscending = null;
      if (order.equals("")) {
        isAscending = parseExpr("true()");
      }
      else if (order.startsWith("{") && order.endsWith("}")) {
        order = order.substring(1, order.length() - 1);
        
        isAscending = parseExpr(order + " = 'ascending'");
      }
      else if (order.equals("ascending"))
        isAscending = parseExpr("true()");
      else
        isAscending = parseExpr("false()");
      
      String dataType = elt.getAttribute("data-type");
      boolean isText = true;
      if (dataType.equals("number"))
        isText = false;

      String lang = elt.getAttribute("lang");
      if (lang.equals("")) {
        sorts.add(Sort.create(expr, isAscending, isText));
      }
      else {
        if (lang.startsWith("{") && lang.endsWith("}"))
          lang = lang.substring(1, lang.length() - 1);
        else
          lang = "'" + lang + "'";

        sorts.add(Sort.create(expr, isAscending, parseExpr(lang)));
        /*
        int p = lang.indexOf('-');
        Locale locale = null;

        if (p < 0) {
          locale = new Locale(lang, "");
        }
        else {
          String language = lang.substring(0, p);
          
          int q = lang.indexOf(p + 1, '-');

          if (q < 0) {
            String country = lang.substring(p + 1);

            locale = new Locale(language, country);
          }
          else {
            String country = lang.substring(p + 1, q);
            String variant = lang.substring(q);

            locale = new Locale(language, country, variant);
          }
        }

        sorts.add(Sort.create(expr, isAscending, lang));
        */
      }
    }

    if (sorts.size() > 0)
      return (Sort []) sorts.toArray(new Sort[sorts.size()]);
    else
      return null;
  }

  void generateIf(Element element)
    throws Exception
  {
    String test = (String) element.getAttribute("test");
    if (test.equals(""))
      throw error(L.l("{0} expects `{1}' attribute", "xsl:if", "test"));

    printIf(element, parseExpr(test));
  }

  void generateWhile(Element element)
    throws Exception
  {
    String test = (String) element.getAttribute("test");
    if (test.equals(""))
      throw error(L.l("{0} expects `{1}' attribute", "xsl:while", "test"));

    printWhile(element, parseExpr(test));
  }

  void generateChoose(Element element)
    throws Exception
  {
    boolean first = true;
    for (Node child = element.getFirstChild();
         child != null;
         child = child.getNextSibling()) {
      if (! (child instanceof Element))
        continue;
      
      String name = getXslLocal(child);

      if ("when".equals(name)) {
        Element elt = (Element) child;
        String test = elt.getAttribute("test");
        if (test.equals(""))
          throw error(L.l("{0} expects `{1}' attribute", "xsl:when", "test"));

        printChoose(elt, parseExpr(test), first);
        first = false;
      }
      else if ("otherwise".equals(name)) {
        printOtherwise((Element) child, first);
      }
      else
        throw error(L.l("xsl:choose expects `xsl:when' or `xsl:otherwise' at `{0}'",
                        child.getNodeName()));
    }
  }

  void generateElement(Element element)
    throws Exception
  {
    String name = (String) element.getAttribute("name");
    if (name.equals(""))
      throw error(L.l("{0} expects `{1}' attribute.", "xsl:element", "name"));
    Attr nsAttr = element.getAttributeNode("namespace");

    if (nsAttr == null)
      printElement(element, name);
    else
      printElement(element, name, nsAttr.getNodeValue());
  }

  void generateAttribute(Element element)
    throws Exception
  {
    String name = (String) element.getAttribute("name");
    if (name.equals(""))
      throw error(L.l("{0} expects `{1}' attribute",
                      "xsl:attribute", "name"));
    Attr nsAttr = element.getAttributeNode("namespace");

    boolean oldSpecial = _isSpecial;
    _isSpecial = true;
    
    if (nsAttr == null)
      printAttribute(element, name);
    else
      printAttribute(element, name, nsAttr.getNodeValue());

    _isSpecial = oldSpecial;
  }

  void generateNumber(Element element)
    throws Exception
  {
    String value = element.getAttribute("value");
    String count = element.getAttribute("count");
    String from = element.getAttribute("from");
    String level = element.getAttribute("level");
    String format = element.getAttribute("format");
    String letter = element.getAttribute("letter-value");
    String separator = element.getAttribute("grouping-separator");
    String lang = element.getAttribute("lang");
    String size_name = element.getAttribute("grouping-size");

    int size = 0;
    for (int i = 0; i < size_name.length(); i++) {
        char ch = size_name.charAt(i);
        if (ch >= '0' && ch <= '9')
            size = 10 * size + ch - '0';
    }

    boolean isAlphabetic = true;
    if (! letter.equals("alphabetic"))
      isAlphabetic = false;

    AbstractPattern countPattern = null;
    if (! count.equals(""))
      countPattern = parseMatch(count);

    AbstractPattern fromPattern = null;
    if (! from.equals(""))
      fromPattern = parseMatch(from);

    if (level.equals("") || level.equals("single"))
      level = "single";
    else if (level.equals("multiple")) {
    }
    else if (level.equals("any")) {
    }
    else
      throw error(L.l("xsl:number can't understand level=`{0}'",
                      level));

    XslNumberFormat xslFormat;
    xslFormat = new XslNumberFormat(format, lang, isAlphabetic,
                                    separator, size);

    if (! value.equals(""))
      printNumber(parseExpr(value), xslFormat);
    else
      printNumber(level, countPattern, fromPattern, xslFormat);
  }

  void printNumber(Expr expr, XslNumberFormat format)
    throws Exception
  {
    
  }

  void printNumber(String level,
                   AbstractPattern countPattern,
                   AbstractPattern fromPattern,
                   XslNumberFormat format)
    throws Exception
  {
    
  }

  /**
   * Sets location code for the node.
   */
  void setLocation(Node node)
    throws Exception
  {
    if (node instanceof QAbstractNode) {
      setLocation(((QAbstractNode) node).getBaseURI(),
                  ((QAbstractNode) node).getFilename(),
                  ((QAbstractNode) node).getLine());
    }
  }

  public void setLocation(String systemId, String filename, int line)
    throws XslParseException, IOException
  {
    if (filename != null) {
      _systemId = systemId;
      _filename = filename;
      _line = line;
      // _lineMap.add(filename, line, getOut().getDestLine());
    }
  }

  int getTextLength()
  {
    return _text.length();
  }

  protected void printHeader()
    throws XslParseException, IOException
  {
  }
  
  abstract protected void startDisableEscaping()
    throws Exception;
   
  abstract protected void endDisableEscaping()
    throws Exception;

  abstract protected void writeText(String text)
    throws Exception;

  abstract protected void printTemplate(Element node,
                                        String name, String pattern,
                                        String mode, double priority)
    throws Exception;


  /**
   * Prints location code for the node.
   */
  void printLocation(Node node)
    throws Exception
  {
    if (node instanceof QAbstractNode) {
      printLocation(((QAbstractNode) node).getBaseURI(),
                    ((QAbstractNode) node).getFilename(),
                    ((QAbstractNode) node).getLine());
    }
  }

  abstract protected void printLocation(String systemId, String filename, int line)
    throws Exception;
  
  abstract protected void printElement(Node node)
    throws Exception;

  abstract protected void
    printApplyTemplates(AbstractPattern select, String mode, Sort []sort)
    throws Exception;

  abstract protected void
    printApplyImports(String mode, int min, int max)
    throws Exception;
  
  abstract protected void
    printCallTemplate(String name, String mode)
    throws Exception;
  
  abstract protected void pushCall()
    throws Exception;
  
  abstract protected void popCall()
    throws Exception;
  
  abstract protected void printParam(String name, Object value)
    throws Exception;

  abstract protected void printParam(String name, String value, Element elt)
    throws Exception;
    
  abstract protected void printParamVariable(String name, Expr expr)
    throws Exception;
  
  abstract protected void printParamVariable(String name, Element elt)
    throws Exception;
  
  abstract protected void printVariable(String name, Object value)
    throws Exception;
  
  protected void printAssign(String name, Object value)
    throws Exception
  {
    printVariable(name, value);
  }
  
  abstract protected void printPopScope(int count)
    throws Exception;
  
  abstract protected void printCopyOf(String select, Element element)
    throws Exception;

  abstract protected void printSelectValue(String select, Element element)
    throws Exception;

  abstract protected void printForEach(Element element, String select)
    throws Exception;

  abstract protected void printForEach(Element element, String select,
                                       Sort []sort)
    throws Exception;

  protected void printIf(Element element, Expr expr)
    throws Exception
  {
  }

  protected void printChoose(Element element, Expr expr, boolean first)
    throws Exception
  {
  }

  protected void printOtherwise(Element element, boolean first)
    throws Exception
  {
  }

  protected void printCopy(Element element)
    throws Exception
  {
  }

  protected void printCopyElement(Element element)
    throws Exception
  {
  }

  protected void printElement(Element element, String name)
    throws Exception
  {
  }

  protected void printElement(Element element, String name, String namespace)
    throws Exception
  {
  }

  protected void printAttribute(Element node, String name)
    throws Exception
  {
  }

  protected void printAttribute(Element node, String name, String namespace)
    throws Exception
  {
  }

  protected void printPi(Element node)
    throws Exception
  {
  }

  protected void printComment(Element node)
    throws Exception
  {
  }

  protected void printError(String msg)
    throws Exception
  {
  }

  protected void printMessage(Element node)
    throws Exception
  {
  }

  // extension

  protected void printExpression(Element node)
    throws Exception
  {
  }

  protected void printScriptlet(Element node)
    throws Exception
  {
  }

  protected void printDeclaration(Element node)
    throws Exception
  {
  }

  protected void printCacheDepends(String path)
    throws Exception
  {
  }

  protected void printWhile(Element element, Expr expr)
    throws Exception
  {
  }

  protected void printResultDocument(Element element, String href,
                                     String format)
    throws Exception
  {
  }

  public int getImportance()
  {
    return _importance;
  }

  public void setMinImportance(int importance)
  {
    _minImportance = importance;
  }
  
  public void incrementImportance()
  {
    _importance++;
  }

  /**
   * Adds a new template pattern.
   *
   * @param pattern the match pattern.
   * @param mode the template mode.
   * @param priority the template priority.
   * @param function the associated function name.
   * @param funId the function id.
   */
  Template addPattern(AbstractPattern pattern, String mode, double priority,
                      String function, int funId)
  {
    if (pattern instanceof UnionPattern) {
      UnionPattern union = (UnionPattern) pattern;
      addPattern(union.getLeft(), mode, priority, function, funId);
      return addPattern(union.getRight(), mode, priority, function, funId);
    }

    if (Double.isNaN(priority))
      priority = pattern.getPriority();

    if (log.isLoggable(Level.FINER))
      log.finer("add " + pattern.getNodeName() + " " + pattern + " fun:" +
                function + " mode:" + mode + " priority:" + priority);

    Template template = new Template(pattern, mode,
                                     _minImportance, _importance,
                                     priority, _templateCount++,
                                     function, funId);

    addTemplate(pattern.getNodeName(), template);

    return template;
  }
  
  private void addTemplate(String nodeName, Template template)
  {
    ArrayList<Template> templateList = _templates.get(nodeName);
    
    if (templateList == null) {
      templateList = new ArrayList<Template>();
      _templates.put(nodeName, templateList);
    }

    for (int i = templateList.size() - 1; i >= 0; i--) {
      Template item = templateList.get(i);

      if (template.compareTo(item) <= 0) {
        templateList.add(i + 1, template);
        return;
      }
    }

    templateList.add(0, template);
  }

  public AbstractPattern parseMatch(String pattern)
    throws XslParseException, IOException
  {
    if (true)
      throw new RuntimeException();
    try {
      return XPath.parseMatch(pattern, _namespace).getPattern();
    } catch (Exception e) {
      throw error(L.l("{0} in pattern `{1}'",
                      e.toString(), pattern));
    }
  }

  public AbstractPattern parseSelect(String pattern)
    throws IOException, XslParseException
  {
    if (true)
      throw new RuntimeException();
    
    try {
      return XPath.parseSelect(pattern, _namespace).getPattern();
    } catch (Exception e) {
      throw error(e);
    }
  }

  protected AbstractPattern parseSelect(String pattern, Node node)
    throws IOException, XslParseException
  {
    if (true)
      throw new UnsupportedOperationException();
    try {
      return XPath.parseSelect(pattern, _namespace).getPattern();
    } catch (Exception e) {
      throw error(node, e);
    }
  }

  public Expr parseExpr(String pattern)
    throws XslParseException
  {
    if (true)
      throw new UnsupportedOperationException();
    try {
      return XPath.parseExpr(pattern, _namespace, _nodeListContext);
    } catch (Exception e) {
      throw error(e);
    }
  }

  XslParseException error(Exception e)
  {
    if (e.getMessage() != null)
      return error(e.getMessage());
    else {
      log.log(Level.WARNING, e.toString(), e);
      
      return error(e.toString());
    }
  }

  XslParseException error(Node node, Exception e)
  {
    if (e.getMessage() != null)
      return error(node, e.getMessage());
    else {
      log.log(Level.WARNING, e.toString(), e);

      return error(e.toString());
    }
  }
  
  XslParseException error(String message)
  {
    return new XslParseException(_filename + ":" + _line + ": " + message);
  }

  /**
   * Creates an error message with filename and line number based on
   * the source node.
   *
   * @param node XML node of the source XSL.
   * @param message the error message.
   */
  XslParseException error(Node node, String message)
  {
    if (! (node instanceof QAbstractNode))
      return error(message);
    
    QAbstractNode qnode = (QAbstractNode) node;

    String filename = qnode.getFilename();
    int line = qnode.getLine();
    
    if (filename != null)
      return new XslParseException(filename + ":" +
                                   line + ": " +
                                   message);
    else
      return error(message);
  }

  /**
   * Returns the local name of an XSL element.  Non-XSL elements return
   * null.  So xsl:copy will return "copy", while "foo:bar" returns null.
   *
   * @param node the XSL source node
   * @return the local part of the XSL name.
   */
  protected String getXslLocal(Node node)
  {
    if (! (node instanceof Element))
      return null;

    QElement elt = (QElement) node;

    String ns = elt.getNamespaceURI();
    String prefix = elt.getPrefix();

    if (ns == null || ns.equals("")) {
      return (elt.getNodeName().startsWith("xsl:") ?
              elt.getNodeName().substring(4) :
              null);
    }
    else if (ns.startsWith(XSLNS) &&
             (ns.length() == XSLNS.length() || ns.charAt(XSLNS.length()) == '/'))
      return elt.getLocalName();
    else
      return null;
  }

  protected String getXtpLocal(Node node)
  {
    if (! (node instanceof Element))
      return null;

    QElement elt = (QElement) node;

    String ns = elt.getNamespaceURI();
    String prefix = elt.getPrefix();

    if (ns == null || ns.equals("")) {
      return (elt.getNodeName().startsWith("xtp:") ?
              elt.getNodeName().substring(4) :
              null);
    }
    else if (ns.startsWith(XTPNS))
      return elt.getLocalName();
    else
      return null;
  }

  /**
   * Parses an expression in a context
   */
  private Expr parseExpr(Node node, String expr)
    throws Exception
  {
    try {
      return XPath.parseExpr(expr, _namespace, _nodeListContext);
    } catch (Exception e) {
      throw error(node, e.getMessage());
    }
  }

  /**
   * Adds the namespaces in the element to the current NamespaceContext.
   * The XPath pattern parsing uses NamespaceContext to associate the right
   * context with element patterns.
   *
   * @param elt the XSL element being processed.
   *
   * @return the old namespace context
   */
  protected NamespaceContext addNamespace(Element elt)
  {
    NamespaceContext oldNamespace = _namespace;

    Node attr = ((QElement) elt).getFirstAttribute();
    for (; attr != null; attr = attr.getNextSibling()) {
      String name = attr.getNodeName();

      if (name.startsWith("xmlns:"))
        name = name.substring(6);
      else if (name.equals("xmlns"))
        name = "";
      else
        continue;

      // Note: according to the spec, the default namespace is not used
      
      String url = attr.getNodeValue();
      if (url.equals(XSLNS) || url.equals(XTPNS))
        continue;

      if (url.startsWith("quote:"))
        url = url.substring(6);

      _namespace = new NamespaceContext(_namespace, name, url);
    }

    return oldNamespace;
  }

  void addDepend(Path depend)
  {
    if (depend != null)
      _depends.add(depend);
  }

  abstract protected StylesheetImpl completeGenerate(ArrayList<XslNode> inits,
                                                     ArrayList globals)
    throws Exception;

  /**
   * Close call when an error occurs.
   */
  public void close()
    throws IOException, XslParseException
  {
  }

  static {
    _tags = new IntMap();
    _tags.put("stylesheet", STYLESHEET);
    _tags.put("transform", STYLESHEET);
    _tags.put("output", OUTPUT);
    _tags.put("template", TEMPLATE);
    _tags.put("preserve-space", PRESERVE_SPACE);
    _tags.put("strip-space", STRIP_SPACE);
    _tags.put("import", IMPORT);
    _tags.put("include", INCLUDE);
    _tags.put("key", KEY);
    _tags.put("decimal-format", LOCALE);
    _tags.put("attribute-set", ATTRIBUTE_SET);
    _tags.put("namespace-alias", NAMESPACE_ALIAS);

    _tags.put("apply-templates", APPLY_TEMPLATES);
    _tags.put("apply-imports", APPLY_IMPORTS);
    _tags.put("call-template", CALL_TEMPLATE);
    _tags.put("param", PARAM);
    _tags.put("variable", VARIABLE);
    _tags.put("for-each", FOR_EACH);
    _tags.put("if", IF);
    _tags.put("choose", CHOOSE);

    _tags.put("value-of", VALUE_OF);
    _tags.put("copy-of", COPY_OF);
    _tags.put("text", XSL_TEXT);
    _tags.put("#text", TEXT);
    _tags.put("number", NUMBER);
    _tags.put("copy", COPY);
    _tags.put("element", ELEMENT);
    _tags.put("attribute", ATTRIBUTE);
    _tags.put("pi", PI);
    _tags.put("processing-instruction", PI);
    _tags.put("comment", COMMENT);

    _tags.put("message", MESSAGE);

    _tags.put("sort", IGNORE);
    _tags.put("fallback", IGNORE);
    // xslt 2.0
    _tags.put("result-document", RESULT_DOCUMENT);

    _xtpTags = new IntMap();
    _xtpTags.put("expression", EXPRESSION);
    _xtpTags.put("expr", EXPRESSION);
    _xtpTags.put("eval", EXPRESSION);
    _xtpTags.put("scriptlet", SCRIPTLET);
    _xtpTags.put("script", SCRIPTLET);
    _xtpTags.put("decl", DECLARATION);
    _xtpTags.put("declaration", DECLARATION);
    _xtpTags.put("directive.cache", DIRECTIVE_CACHE);
    _xtpTags.put("while", WHILE);
    _xtpTags.put("assign", ASSIGN);
  }
}
