/*
 * Copyright (c) 1998-2006 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 java.util.*;
import java.util.logging.*;
import java.io.*;
import javax.servlet.jsp.*;

import org.w3c.dom.*;
import org.xml.sax.*;

import com.caucho.util.*;
import com.caucho.vfs.*;
import com.caucho.log.Log;
import com.caucho.xml.*;
import com.caucho.xpath.*;

/**
 * Writer stream for generating stylesheet output.
 *
 * <p>Because XSL produces an XML tree, XslWriter contains extra
 * methods for constructing the tree.
 *
 * <p>The writer methods, e.g. println, add to the current text node.
 *
 * <p>In addition, stylesheets can access variables through getPwd and
 * getPage.
 */
public class XslWriter extends Writer implements ExtendedLocator {
  static final Logger log = Log.open(XslWriter.class);
  static final L10N L = new L10N(XslWriter.class);

  // This is the value Axis wants
  private final static String XMLNS = "http://www.w3.org/2000/xmlns/";

  private final static XMLWriter ATTR_WRITER = new DOMBuilder();

  private XMLWriter _xmlWriter;
  
  String _systemId;
  String _filename;
  int _line;
  int _tailLine;

  private IntArray flags = new IntArray();
  
  private CharBuffer _text = new CharBuffer();
  private String elementName;

  private String _attributeURL;
  private String _attributePrefix;
  private String _attributeLocalName;
  private String _attributeName;

  private ArrayList depends = new ArrayList();
  private boolean _isCacheable = true;
  private boolean _disableEscaping;

  private boolean generateLocation;

  private Document _document;

  private StylesheetImpl _stylesheet;
  private TransformerImpl _transformer;

  private HashMap<String,String> _cdataElements;
  private boolean isCdata;

  private HashMap<String,String> _namespaces;
  private ArrayList<String> _topNamespaces;
  private ArrayList<StackItem> _elementStack;
  private int _depth;

  private ExtendedLocator _locator = null;

  XslWriter(HashMap env,
            StylesheetImpl stylesheet,
            TransformerImpl transformer)
  {
    _stylesheet = stylesheet;
    _transformer = transformer;

    ArrayList<String> cdata = stylesheet.getOutputFormat().getCdataSectionElements();
    if (cdata != null) {
      _cdataElements = new HashMap<String,String>();
      
      for (int i = 0; i < cdata.size(); i++) {
        String element = cdata.get(i);

        _cdataElements.put(element, element);
      }
    }
  }

  void init(XMLWriter xmlWriter)
  {
    _xmlWriter = xmlWriter;
    _namespaces = new HashMap<String,String>();
    _topNamespaces = new ArrayList<String>();
    _elementStack = new ArrayList<StackItem>();

    _document = null;

    _locator = this;
    xmlWriter.setDocumentLocator(_locator);
  }

  public TransformerImpl getTransformer()
  {
    return _transformer;
  }

  /**
   * Returns true if the generated stylesheet is currently cacheable.
   */
  boolean isCacheable()
  {
    return _isCacheable;
  }

  /**
   * Returns the Path dependency list of the generated stylesheet.
   */
  ArrayList getDepends()
  {
    return depends;
  }

  /**
   * Indicate that the result document is not cacheable.
   */
  public void setNotCacheable()
  {
    _isCacheable = false;
  }

  /**
   * Add a dependency to the result document.  When the result is checked
   * for modification, this path will also be checked.
   */
  public void addCacheDepend(Path path)
  {
    _transformer.addCacheDepend(path);
  }

  /**
   * Implementation function so jsp:decl tags aren't repeated.
   */
  public boolean isFlagFirst(int id)
  {
    while (flags.size() <= id)
      flags.add(0);

    int value = flags.get(id);
    flags.set(id, 1);

    return value == 0;
  }

  /**
   * Adds a byte to the current text node.
   */
  public void write(int ch)
  {
    _text.append((char) ch);
  }
  /**
   * Adds a byte buffer to the current text node.
   */
  public void write(byte []buf, int offset, int length)
  {
    for (int i = 0; i < length; i++)
      write(buf[offset + i]);
  }

  /**
   * Adds a char buffer to the current text node.
   */
  public void write(char []buf, int offset, int length)
  {
    _text.append(buf, offset, length);
  }

  /**
   * Adds a string to the current text node.
   */
  public void print(String string)
  {
    if (string == null) {
      _text.append("null");
      return;
    }

    _text.append(string);
  }

  /**
   * Adds a boolean to the current text node.
   */
  public void print(boolean b)
  {
    _text.append(b);
  }

  /**
   * Adds a character to the current text node.
   */
  public void print(char ch)
  {
    _text.append(ch);
  }

  /**
   * Adds an integer to the current text node.
   */
  public void print(int i)
  {
    _text.append(i);
  }

  /**
   * Adds an integer to the current text node.
   */
  public void print(long l)
  {
    _text.append(l);
  }

  /**
   * Adds a float to the current text node.
   */
  public void print(float f)
  {
    _text.append(f);
  }

  /**
   * Adds a double to the current text node.
   */
  public void print(double d)
  {
    _text.append(d);
  }

  /**
   * Adds an object to the current text node, converted by
   * String.valueOf.
   */
  public void print(Object o)
  {
    _text.append(o);
  }
  /**
   * Adds a newline to the current text node.
   */
  public void println()
  {
    _text.append('\n');
    _tailLine++;
  }

  /**
   * Adds a boolean to the current text node.
   */
  public void println(boolean b)
  {
    _text.append(b);
    println();
  }

  /**
   * Adds a string to the current text node.
   */
  public void println(String s)
  {
    print(s);
    println();
  }

  /**
   * Adds a character to the current text node.
   */
  public void println(char ch)
  {
    print(ch);
    println();
  }

  /**
   * Adds an integer to the current text node.
   */
  public void println(int i)
  {
    _text.append(i);
    println();
  }

  /**
   * Adds a long to the current text node.
   */
  public void println(long l)
  {
    _text.append(l);
    println();
  }

  /**
   * Adds a double to the current text node.
   */
  public void println(double d)
  {
    _text.append(d);
    println();
  }

  /**
   * Adds a float to the current text node.
   */
  public void println(float f)
  {
    _text.append(f);
    println();
  }

  /**
   * Adds an object to the current text node, converted by String.valueOf.
   */
  public void println(Object o)
  {
    _text.append(o);
    println();
  }

  /**
   * flush is meaningless for XslWriter.  It's only added to conform to Writer.
   */
  public void flush()
  {
  }

  public void close()
    throws IOException
  {
    try {
      for (int i = 0; i < _topNamespaces.size(); i++) {
        String topPrefix = _topNamespaces.get(i);
        String topUrl = _namespaces.get(topPrefix);

        if (topPrefix.equals(""))
	  _xmlWriter.endPrefixMapping(null);
        else
	  _xmlWriter.endPrefixMapping(topPrefix);
      }

      popText();
      _xmlWriter.endDocument();
    } catch (SAXException e) {
      throw new IOException(e.toString());
    }
  }

  public boolean getDisableEscaping()
  {
    return _disableEscaping;
  }
  
  public boolean disableEscaping(boolean disable)
    throws IOException, SAXException
  {
    if (disable != _disableEscaping) {
      popText();
      _xmlWriter.setEscapeText(! disable);
    }

    boolean old = _disableEscaping;
    _disableEscaping = disable;

    return old;
  }

  public void setLocation(String systemId, String filename, int line)
    throws IOException, SAXException
  {
    // Don't need to pop the text if the line # matches
    if (filename == null || ! filename.equals(_filename) ||
        line != _tailLine)
      popText();

    _systemId = systemId;
    _filename = filename;
    _line = line;
    _tailLine = line;
  }

  /**
   * Adds a new element to the current node, making the new element the
   * current node.
   *
   * <p>Each pushElement should be matched by a popElement.
   *
   * @param name name of the element
   */
  public void pushElement(String name)
   throws IOException, SAXException
  {
    popText();

    String local;
    int p = name.lastIndexOf(':');
    if (p > 0)
      local = name.substring(p + 1);
    else
      local = name;
    
    startElement(null, null, local, name);
  }

  /**
   * Adds a new element to the current node, making the new element the
   * current node.
   *
   * <p>Each pushElement should be matched by a popElement.
   *
   * @param name name of the element
   * @param namespace namespace context
   */
  public void pushElement(String name, NamespaceContext namespace)
   throws IOException, SAXException
  {
    popText();

    // Look up the proper namespace for the element.
    int p = name.indexOf(':');
    if (p <= 0) {
      startElement(null, null, name, name);
      return;
    }

    String prefix = name.substring(0, p);
    String url = namespace.find(namespace, prefix);
    
    if (url != null)
      startElement(url, prefix, name.substring(p + 1), name);
    else
      startElement(null, null, name, name);
  }

  /**
   * Adds a new element to the current node, making the new element the
   * current node.
   *
   * <p>Each pushElement should be matched by a popElement.
   *
   * @param name name of the element
   * @param url namespace url
   */
  public void pushElementNs(String name, String url)
   throws IOException, SAXException
  {
    popText();
    
    // Look up the proper namespace for the element.
    int p = name.indexOf(':');
    if (p <= 0) {
      startElement(url, "", name, name);
      return;
    }

    String prefix = name.substring(0, p);
    String local = name.substring(p + 1);

    startElement(url, prefix, local, name);
  }

  /**
   * Adds a namespace-aware element to the current node, making the
   * new element the current node.
   *
   * <p>Each pushElement should be matched by a popElement.
   *
   * @param prefix the prefix of the element name, e.g. xsl
   * @param local the local part of the element name, e.g. template
   * @param url the namespace url, e.g. http://www.xml.org/...
   */
  public void pushElement(String url, String prefix, String local, String name)
   throws IOException, SAXException
  {
    popText();

    /*
    if (url != null && url.startsWith("quote:"))
      url = url.substring(6);
    */
    
    startElement(url, prefix, local, name);
  }

  /**
   * Adds a new attribute with the given name to the current node, making
   * the attribute the current node.
   */
  public XMLWriter pushAttribute(String name)
    throws IOException, SAXException
  {
    popText();

    XMLWriter oldWriter = _xmlWriter;
    _xmlWriter = ATTR_WRITER;
    
    _attributeURL = null;
    _attributePrefix = null;
    _attributeLocalName = null;
    _attributeName = name;

    return oldWriter;
  }

  /**
   * Adds a new attribute with the given name to the current node, making
   * the attribute the current node.
   */
  public XMLWriter pushAttribute(String name, NamespaceContext namespace)
   throws IOException, SAXException
  {
    popText();

    XMLWriter oldWriter = _xmlWriter;
    _xmlWriter = ATTR_WRITER;
    
    // Look up the proper namespace for the element.
    int p = name.indexOf(':');
    String prefix = null;
    if (p > 0)
      prefix = name.substring(0, p);
    String url = namespace.find(namespace, prefix);
    Attr attr;
    
    if (url != null) {
      _attributeURL = url;
      _attributePrefix = prefix;
      _attributeLocalName = name.substring(p + 1);
      _attributeName = name;
    }
    else {
      _attributeURL = null;
      _attributePrefix = null;
      _attributeLocalName = null;
      _attributeName = name;
    }

    return oldWriter;
  }

  /**
   * Adds a new attribute to the current node, making the new attribute the
   * current node.
   *
   * <p>Each pushAttributeNs should be matched by a popAttribute.
   *
   * @param name name of the element
   * @param url namespace url
   */
  public XMLWriter pushAttributeNs(String name, String url)
   throws IOException, SAXException
  {
    popText();

    XMLWriter oldWriter = _xmlWriter;
    _xmlWriter = ATTR_WRITER;
    
    Attr attr;

    // Look up the proper namespace for the element.
    int p = name.indexOf(':');
    String prefix = null;
    String local = name;
    if (p > 0) {
      prefix = name.substring(0, p);
      local = name.substring(p + 1);
    }

    _attributeURL = url;
    _attributePrefix = prefix;
    _attributeLocalName = local;
    _attributeName = name;

    return oldWriter;
  }

  /**
   * Adds a namespace-aware attribute to the current node, making the
   * new attribute the current node.
   *
   * <p>Each pushAttribute should be matched by a popAttribute.
   *
   * @param prefix the prefix of the element name, e.g. xsl
   * @param local the local part of the element name, e.g. template
   * @param url the namespace url, e.g. http://www.xml.org/...
   */
  public XMLWriter pushAttribute(String prefix, String local, String url)
    throws IOException, SAXException
  {
    popText();

    XMLWriter oldWriter = _xmlWriter;
    _xmlWriter = ATTR_WRITER;
    
    /*
    if (url != null && url.startsWith("quote:"))
      url = url.substring(6);
    */
    
    _attributeURL = url;
    _attributePrefix = prefix;
    _attributeLocalName = local;
    
    if (prefix != null && ! prefix.equals(""))
      _attributeName = prefix + ":" + local;
    else
      _attributeName = local;

    return oldWriter;
  }

  /**
   * Adds a namespace-aware attribute to the current node, making the
   * new attribute the current node.
   *
   * <p>Each pushAttribute should be matched by a popAttribute.
   *
   * @param prefix the prefix of the element name, e.g. xsl
   * @param local the local part of the element name, e.g. template
   * @param url the namespace url, e.g. http://www.xml.org/...
   */
  public void setAttribute(String prefix, String local, String url,
			   String value)
    throws IOException, SAXException
  {
    popText();

    /*
    if (url != null && url.startsWith("quote:"))
      url = url.substring(6);
    */

    String attributeName;
    if (prefix != null && ! prefix.equals(""))
      attributeName = prefix + ":" + local;
    else
      attributeName = local;
    
    attribute(url, prefix, local, attributeName, value);
  }

  /**
   * Adds a new attribute with the given name to the current node, making
   * the attribute the current node.
   */
  public void setAttribute(String name, NamespaceContext namespace, String value)
   throws IOException, SAXException
  {
    popText();
    
    // Look up the proper namespace for the element.
    int p = name.indexOf(':');
    String prefix = null;
    if (p > 0)
      prefix = name.substring(0, p);
    String url = namespace.find(namespace, prefix);
    Attr attr;
    
    if (url != null) {
      attribute(url, prefix, name.substring(p + 1), name, value);
    }
    else {
      attribute(null, null, null, name, value);
    }
  }

  /**
   * Sets the attribute value to the current text, and sets the current node
   * to the parent.
   */
  public void popAttribute(XMLWriter writer)
   throws IOException, SAXException
  {
    _xmlWriter = writer;
    
    attribute(_attributeURL, _attributePrefix,
              _attributeLocalName, _attributeName,
              _text.toString());
    
    _text.clear();
    _attributeName = null;
  }

  /**
   * Directly sets an attribute with a value.
   */
  public void setAttribute(String name, String value)
    throws IOException, SAXException
  {
    attribute(null, null, name, name, value);
  }

  /**
   * Copies the node without attributes or children.
   */
  public void pushCopy(Node copyNode)
    throws IOException, SAXException
  {
    popText();

    switch (copyNode.getNodeType()) {
    case Node.ATTRIBUTE_NODE:
      QAbstractNode oldNode = (QAbstractNode) copyNode;
      attribute(oldNode.getNamespaceURI(),
                oldNode.getPrefix(),
                oldNode.getLocalName(),
                oldNode.getNodeName(),
                oldNode.getNodeValue());
      break;

    case Node.DOCUMENT_NODE:
      return;

    case Node.ELEMENT_NODE:
      QElement oldElt = (QElement) copyNode;

      /*
      String oldSystemId = _systemId;
      String oldFilename = _filename;
      int oldLine = _line;

      _systemId = oldElt.getBaseURI();
      _filename = oldElt.getFilename();
      _line = oldElt.getLine();

      if (generateLocation)
        _xmlWriter.setLocation(.getFilename(), oldElt.getLine(), 0);
      */

      startElement(oldElt.getNamespaceURI(),
                   oldElt.getPrefix(),
                   oldElt.getLocalName(),
                   oldElt.getNodeName());

      /*
      _systemId = oldSystemId;
      _filename = oldFilename;
      _line = oldLine;
      */
      break;

    case Node.COMMENT_NODE:
      _xmlWriter.comment(((Comment) copyNode).getData());
      break;

    case Node.TEXT_NODE:
      /*
      if (generateLocation)
        _xmlWriter.setLocation(((QAbstractNode) copyNode).getFilename(),
                              ((QAbstractNode) copyNode).getLine(),
                              0);
      */
      _text.append(((Text) copyNode).getData());
      break;

    case Node.PROCESSING_INSTRUCTION_NODE:
      ProcessingInstruction oldPi = (ProcessingInstruction) copyNode;

      _xmlWriter.processingInstruction(oldPi.getNodeName(), oldPi.getNodeValue());
      break;
    }
  }

  /**
   * Pops the copy.
   */
  public void popCopy(Node copyNode)
   throws IOException, SAXException
  {
    if (copyNode.getNodeType() == Node.ELEMENT_NODE) {
      popText();
      popElement();
    }
  }

  public void pushPi()
    throws IOException, SAXException
  {
    popText();
  }
  /**
   * Sets the PI data to the current text, and sets the current node
   * to the parent.
   */
  public void popPi(String name)
   throws IOException, SAXException
  {
    _xmlWriter.processingInstruction(name, _text.toString());
    _text.clear();
  }

  /**
   * Adds an empty comment to the current node, making
   * the attribute the current node.
   */
  public void pushComment()
   throws IOException, SAXException
  {
    popText();
  }

  /**
   * Sets the comment data to the current text, and sets the current
   * to the the parent.
   */
  public void popComment()
   throws IOException, SAXException
  {
    _xmlWriter.comment(_text.toString());
    
    _text.clear();
  }

  /**
   * Starts a fragment.  The fragment becomes the current node.
   */
  public XMLWriter pushFragment()
   throws IOException, SAXException
  {
    popText();

    DOMBuilder domBuilder = new DOMBuilder();

    if (_document == null)
      _document = Xml.createDocument();
    domBuilder.init(_document.createDocumentFragment());
    domBuilder.setDocumentLocator(_locator);

    XMLWriter oldWriter = _xmlWriter;
    _xmlWriter = domBuilder;

    return oldWriter;
  }

  /**
   * Returns the generated fragment. The current node does not contain
   * the new fragment.
   *
   * @return the generated fragment.
   */
  public Node popFragment(XMLWriter oldWriter)
   throws IOException, SAXException
  {
    popText();

    DOMBuilder domBuilder = (DOMBuilder) _xmlWriter;

    _xmlWriter = oldWriter;

    domBuilder.endDocument();
    Node node = domBuilder.getNode();

    return node;
  }

  /**
   * Adds a the contents of the node to the current node.
   *
   * @param node node to print
   */
  public void valueOf(Object node)
   throws IOException, SAXException
  {
    if (node == null)
      return;
    else if (node instanceof Element || node instanceof DocumentFragment) {
      Node elt = (Node) node;
      for (Node child = elt.getFirstChild();
	   child != null;
	   child = child.getNextSibling()) {
	elementValueOf(child);
      }
    }
    else if (node instanceof Text) {
      String data = ((Text) node).getNodeValue();
      for (int i = 0; i < data.length(); i++) {
	if (! XmlChar.isWhitespace(data.charAt(i))) {
	  print(data);
	  return;
	}
      }
      /*
      if (! _stylesheet.stripSpaces(((Node) node).getParentNode()))
      */
      print(data);
    }
    else if (node instanceof Node) {
      print(((QAbstractNode) node).getNodeValue());
    }
    else if (node instanceof NodeList) {
      NodeList list = (NodeList) node;
      Node value = list.item(0);

      if (value != null)
        valueOf(value);
    }
    else if (node instanceof ArrayList) {
      ArrayList list = (ArrayList) node;
      if (list.size() > 0)
        valueOf(list.get(0));
    }
    else if (node instanceof Iterator) {
      Iterator list = (Iterator) node;
      valueOf(list.next());
    }
    else if (node instanceof Double) {
      Double d = (Double) node;
      double dValue = d.doubleValue();

      if ((int) dValue == dValue)
	print((int) dValue);
      else
	print(dValue);
    }
    else
      print(node);
  }

  /**
   * Adds a the contents of the node to the current node.
   *
   * @param node node to print
   */
  private void elementValueOf(Node node)
   throws IOException, SAXException
  {
    if (node == null)
      return;
    else if (node instanceof Element) {
      Element elt = (Element) node;
      for (Node child = elt.getFirstChild();
	   child != null;
	   child = child.getNextSibling()) {
	elementValueOf(child);
      }
    }
    else if (node instanceof Text) {
      String data = ((Text) node).getNodeValue();
      for (int i = 0; i < data.length(); i++) {
	if (! XmlChar.isWhitespace(data.charAt(i))) {
	  print(data);
	  return;
	}
      }
      /*
      if (! _stylesheet.stripSpaces(node.getParentNode()))
      */
      print(data);
    }
  }

  /**
   * Adds a deep copy of the node to the current node.
   *
   * @param XPath node to be copied to the destination.
   */
  public void copyOf(Object value)
    throws IOException, SAXException, XPathException
  {
    popText();

    if (value instanceof NodeList) {
      NodeList list = (NodeList) value;

      int length = list.getLength();
      for (int i = 0; i < length; i++) {
	Node child = list.item(i);

	copyOf(child);
      }
    }
    else if (value instanceof ArrayList) {
      ArrayList list = (ArrayList) value;

      for (int i = 0; i < list.size(); i++) {
	Node child = (Node) list.get(i);

	copyOf(child);
      }
    }
    else if (value instanceof Iterator) {
      Iterator iter = (Iterator) value;

      while (iter.hasNext()) {
	Node child = (Node) iter.next();

	copyOf(child);
      }
    }
    else if (value instanceof Attr) {
      Attr child = (Attr) value;

      attribute(child.getNamespaceURI(),
                child.getPrefix(),
                child.getLocalName(),
                child.getNodeName(),
                child.getNodeValue());
    }
    else if (value instanceof QElement) {
      QElement child = (QElement) value;

      String oldSystemId = _systemId;
      String oldFilename = _filename;
      int oldLine = _line;

      _systemId = child.getBaseURI();
      _filename = child.getFilename();
      _line = child.getLine();

      startElement(child.getNamespaceURI(),
                   child.getPrefix(),
                   child.getLocalName(),
                   child.getNodeName());
      Node subNode = child.getFirstAttribute();
      for (; subNode != null; subNode = subNode.getNextSibling()) {
        QAttr attr = (QAttr) subNode;
        
        attribute(attr.getNamespaceURI(),
                  attr.getPrefix(),
                  attr.getLocalName(),
                  attr.getNodeName(),
                  attr.getNodeValue());
      }
      
      for (subNode = child.getFirstChild();
           subNode != null;
           subNode = subNode.getNextSibling()) {
        copyOf(subNode);
      }

      popElement();

      _systemId = oldSystemId;
      _filename = oldFilename;
      _line = oldLine;
    }
    else if (value instanceof DocumentFragment) {
      for (Node subNode = ((Node) value).getFirstChild();
           subNode != null;
           subNode = subNode.getNextSibling()) {
        copyOf(subNode);
      }
    }
    else if (value instanceof Text) {
      Text child = (Text) value;

      _text.append(child.getNodeValue());
    }
    else if (value instanceof Comment) {
      Comment child = (Comment) value;

      _xmlWriter.comment(child.getNodeValue());
    }
    else if (value instanceof ProcessingInstruction) {
      ProcessingInstruction pi = (ProcessingInstruction) value;
      
      _xmlWriter.processingInstruction(pi.getNodeName(),
				       pi.getNodeValue());
    }
    else if (value instanceof EntityReference) {
      EntityReference child = (EntityReference) value;

      _text.append("&" + child.getNodeName() + ";");
    }
    else if (value instanceof Node) {
      Node child = (Node) value;

      _text.append(child.getNodeValue());
    }
    else {
      print(Expr.toString(value));
    }
  }

  public void addNamespace(String prefix, String url)
  {
    /*
    if (url.startsWith("quote:"))
      url = url.substring(6);
    */
    if (! url.equals("")) {
      _namespaces.put(prefix, url);

      _topNamespaces.add(prefix);
    }
  }

  void startElement(String url, String prefix, String local, String qName)
    throws IOException, SAXException
  {
    if (_attributeName != null)
      throw error(L.l("element `{0}' is not allowed inside attribute `{1}'.  xsl:attribute must contain text only.", qName, _attributeName));
    popText();

    StackItem item = null;
    if (_elementStack.size() <= _depth) {
      item = new StackItem();
      _elementStack.add(item);
    }
    else
      item = _elementStack.get(_depth);

    item.init(url, prefix, local, qName, isCdata);

    if (_cdataElements != null && _cdataElements.get(qName) != null)
      isCdata = true;
    
    _depth++;

    _xmlWriter.startElement(url, local, qName);

    // Initialize top-level namespaces
    if (_depth == 1) {
      for (int i = 0; i < _topNamespaces.size(); i++) {
        String topPrefix = _topNamespaces.get(i);
        String topUrl = _namespaces.get(topPrefix);

        if (topPrefix.equals("")) {
	  _xmlWriter.startPrefixMapping(null, topUrl);
          _xmlWriter.attribute(XMLNS, null, "xmlns", topUrl);
	}
        else {
	  _xmlWriter.startPrefixMapping(topPrefix, topUrl);
          _xmlWriter.attribute(XMLNS, topPrefix, "xmlns:" + topPrefix, topUrl);
	}
      }
    }

    if (url == null)
      return;

    bindNamespace(prefix, url);
  }

  public void popElement()
    throws IOException, SAXException
  {
    popText();
    _depth--;

    StackItem item = _elementStack.get(_depth);
    
    try{
      _xmlWriter.endElement(item.getNamespace(),
                           item.getLocalName(),
                           item.getName());

      // If this element bound namespaces, pop the old values
      for (int i = 0; i < item.nsSize(); i++) {
        String oldPrefix = item.getNSPrefix(i);
        String oldUrl = item.getNSUrl(i);

        if (oldUrl == null)
          _namespaces.remove(oldPrefix);
        else
          _namespaces.put(oldPrefix, oldUrl);

	_xmlWriter.endPrefixMapping(oldPrefix);
      }

      isCdata = item.getCdata();
    } catch (Throwable e) {
      log.log(Level.FINE, e.toString(), e);
    }
  }

  /**
   * Sends the attribute to the output
   *
   * @param url the namespace for the attribute name
   * @param prefix the prefix for the attribute name
   * @param local the local attribute name
   * @param qName the full qualified name
   * @param value the attribute's value
   */
  public void attribute(String url, String prefix, String local,
			String qName, String value)
    throws IOException, SAXException
  {
    if (qName.startsWith("xmlns:"))
      bindNamespace(qName.substring("xmlns:".length()), value);
    else if (qName.equals("xmlns"))
      bindNamespace(null, value);
    else {
      _xmlWriter.attribute(url, local, qName, value);

      // null namespace binding doesn't add binding
      if (url != null && ! url.equals("") && ! prefix.equals(""))
        bindNamespace(prefix, url);
    }
  }

  /**
   * Sends the attribute to the output
   *
   * @param url the namespace for the attribute name
   * @param prefix the prefix for the attribute name
   * @param local the local attribute name
   * @param qName the full qualified name
   * @param value the attribute's value
   */
  public void attribute(String qName, String value)
    throws IOException, SAXException
  {
    _xmlWriter.attribute(null, null, qName, value);
  }

  public void bindNamespace(String prefix, String url)
    throws IOException, SAXException
  {
    String oldUrl = _namespaces.get(prefix);

    // If the namespace matches, return
    if (oldUrl == null && url.equals("") ||
        oldUrl != null && url.equals(oldUrl))
      return;

    // Send the namespace declaration to the writer
    if (prefix != null) {
      _xmlWriter.startPrefixMapping(prefix, url);
      _xmlWriter.attribute(XMLNS, prefix, "xmlns:" + prefix, url);
      _namespaces.put(prefix, url);
    }
    else {
      _xmlWriter.startPrefixMapping(null, url);
      _xmlWriter.attribute(XMLNS, null, "xmlns", url);
      _namespaces.put(null, url);
    }

    StackItem item = _elementStack.get(_depth - 1);
    item.addNamespace(prefix, oldUrl);
  }

  /**
   * Pop the accumulated text to the DOM.
   */
  public void popText()
    throws IOException, SAXException
  {
    if (_xmlWriter == ATTR_WRITER)
      return;
    
    Text textNode = null;
    
    if (_text.length() == 0)
      return;
    
    if (_filename != null)
      _line = _tailLine;

    if (isCdata)
      _xmlWriter.cdata(_text.getBuffer(), 0, _text.getLength());
    else
      _xmlWriter.text(_text.getBuffer(), 0, _text.getLength());
    
    _text.clear();
  }

  /**
   * Returns the attribute with the given name.
   */
  public Object getProperty(String name)
  {
    return _transformer.getProperty(name);
  }

  /**
   * Sets the attribute with the given name.
   */
  public void setProperty(String name, Object value)
  {
    _transformer.setProperty(name, value);
  }

  /**
   * removes the attribute with the given name.
   */
  public void removeProperty(String name)
  {
  }

  /**
   * Lists the names of all the attributes.
   */
  public Iterator getPropertyNames()
  {
    return null;
  }

  public Object getParameter(String name)
  {
    return _transformer.getParameter(name);
  }

  public Path getPwd()
  {
    return (Path) getProperty("caucho.pwd");
  }

  public OutputStream openWrite(ExprEnvironment env, String href)
    throws IOException
  {
    if (_xmlWriter instanceof XmlPrinter) {
      XmlPrinter printer = (XmlPrinter) _xmlWriter;

      Path path = printer.getPath();

      if (path != null) {
	Path dst = path.getParent().lookup(href);
	dst.getParent().mkdirs();
	
        return dst.openWrite();
      }
    }
    
    Path stylesheetPath = env.getStylesheetEnv().getPath();
    
    return stylesheetPath.getParent().lookup(href).openWrite();
  }

  public XslWriter openResultDocument(OutputStream os)
    throws IOException, SAXException
  {
    XMLWriter writer = new XmlPrinter(os);
    XslWriter out = new XslWriter(null, _stylesheet, _transformer);
    out.init(writer);

    writer.startDocument();

    return out;
  }

  /**
   * @deprecated
   */
  public javax.servlet.jsp.PageContext getPage()
  {
    return (javax.servlet.jsp.PageContext) getProperty("caucho.page.context");
  }

  private IOException error(String message)
  {
    if (_filename != null)
      return new IOException(_filename + ":" + _line + ": " + message);
    else
      return new IOException(message);
  }
  
  public String getSystemId()
  {
    if (_systemId != null)
      return _systemId;
    else
      return _filename;
  }
    
  public String getFilename()
  {
    if (_filename != null)
      return _filename;
    else
      return _systemId;
  }
    
  public String getPublicId()
  {
    return null;
  }

  public int getLineNumber()
  {
    return _line;
  }

  public int getColumnNumber()
  {
    return 0;
  }

  static class StackItem {
    String _url;
    String _prefix;
    String _local;
    String _qName;
    boolean _isCdata;

    ArrayList<String> _nsPrefixes;
    ArrayList<String> _nsUrls;

    void clear()
    {
    }

    void init(String url, String prefix, String local, String qName,
              boolean isCdata)
    {
      if (_nsPrefixes != null) {
        _nsPrefixes.clear();
        _nsUrls.clear();
      }

      _url = url;
      _prefix = prefix;
      _local = local;
      _qName = qName;

      _isCdata = isCdata;
    }

    String getNamespace()
    {
      return _url;
    }

    String getPrefix()
    {
      return _prefix;
    }

    String getLocalName()
    {
      return _local;
    }

    String getName()
    {
      return _qName;
    }

    boolean getCdata()
    {
      return _isCdata;
    }

    int nsSize()
    {
      return _nsPrefixes == null ? 0 : _nsPrefixes.size();
    }

    String getNSPrefix(int i)
    {
      return _nsPrefixes.get(i);
    }

    String getNSUrl(int i)
    {
      return _nsUrls.get(i);
    }

    void addNamespace(String prefix, String oldUrl)
    {
      if (_nsPrefixes == null) {
        _nsPrefixes = new ArrayList<String>();
        _nsUrls = new ArrayList<String>();
      }

      _nsPrefixes.add(prefix);
      _nsUrls.add(oldUrl);
    }
  }
}
