/*
 * Copyright (c) 1998-2008 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.xml2;

import com.caucho.vfs.Depend;
import com.caucho.vfs.Path;

import org.w3c.dom.*;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * Implements the top-level document for the XML tree.
 */
public class QDocument extends QDocumentFragment implements CauchoDocument {
  QDOMImplementation _implementation;
  QDocumentType _dtd;
  QElement _element; // top
  HashMap<String,String> _attributes;
  String _encoding = "UTF-8";
  String _version;

  private String _systemId;

  private HashMap<String,String> _namespaces;

  private transient HashMap<NameKey,QName> _nameCache = new HashMap<NameKey,QName>();
  private transient NameKey _nameKey = new NameKey();
  private transient ArrayList<Path> _depends;
  private transient ArrayList<Depend> _dependList;

  int _changeCount;

  // possibly different from the systemId if the DOCTYPE doesn't match
  // the actual file location
  String _rootFilename;
  private boolean _standalone;

  public QDocument()
  {
    _implementation = new QDOMImplementation();
    _owner = this;
  }

  public QDocument(DocumentType docType)
  {
    _owner = this;
    setDoctype(docType);
  }

  public QDocument(QDOMImplementation impl)
  {
    _implementation = impl;
    _owner = this;
  }

  void setAttribute(String name, String value)
  {
    if (name.equals("version"))
      _version = value;
    else if (name.equals("encoding"))
      _encoding = value;
    else {
      if (_attributes == null)
        _attributes = new HashMap<String,String>();
      _attributes.put(name, value);
    }
  }

  public String getRootFilename()
  {
    return _rootFilename;
  }

  public void setRootFilename(String filename)
  {
    _rootFilename = filename;
  }

  public void setSystemId(String systemId)
  {
    _systemId = systemId;
  }

  public String getSystemId()
  {
    return _systemId;
  }

  /**
   * Returns the base URI of the node.
   */
  public String getBaseURI()
  {
    return getSystemId();
  }

  public Document getOwnerDocument()
  {
    return null;
  }

  public DOMConfiguration getDomConfig()
  {
    return null;
  }

  public boolean isSupported(String feature, String version)
  {
    return _owner.getImplementation().hasFeature(feature, version);
  }

  /**
   * The node name for the document is #document.
   */
  public String getNodeName()
  {
    return "#document";
  }

  public short getNodeType()
  {
    return DOCUMENT_NODE;
  }

  protected Node copyNode(QDocument newNode, boolean deep)
  {
    newNode._dtd = _dtd;
    newNode._element = _element;

    return newNode;
  }

  /**
   * Returns a clone of the document.
   *
   * @param deep if true, recursively copy the document.
   */
  public Node cloneNode(boolean deep)
  {
    QDocument newDoc = new QDocument();

    newDoc._implementation = _implementation;
    newDoc._dtd = _dtd;
    if (_attributes != null)
      newDoc._attributes = (HashMap) _attributes.clone();
    newDoc._encoding = _encoding;
    newDoc._version = _version;

    if (_namespaces != null)
      newDoc._namespaces = (HashMap) _namespaces.clone();

    if (deep) {
      for (Node node = getFirstChild();
           node != null;
           node = node.getNextSibling()) {
        newDoc.appendChild(newDoc.importNode(node, true));
      }
    }

    return newDoc;
  }

  Node importNode(QDocument doc, boolean deep)
  {
    return null;
  }

  /**
   * Imports a copy of a node into the current document.
   *
   * @param node the node to import/copy
   * @param deep if true, recursively copy the children.
   *
   * @return the new imported node.
   */
  public Node importNode(Node node, boolean deep)
  {
    if (node == null)
      return null;

    QName name;

    switch (node.getNodeType()) {
    case ELEMENT_NODE:
      return importElement((Element) node, deep);

    case ATTRIBUTE_NODE:
      Attr attr = (Attr) node;
      name = createName(attr.getNamespaceURI(), attr.getNodeName());
      QAttr newAttr = new QAttr(name, attr.getNodeValue());
      newAttr._owner = this;
      return newAttr;

    case TEXT_NODE:
      QText newText = new QText(node.getNodeValue());
      newText._owner = this;
      return newText;

    case CDATA_SECTION_NODE:
      QCdata newCData = new QCdata(node.getNodeValue());
      newCData._owner = this;
      return newCData;

    case ENTITY_REFERENCE_NODE:
      QEntityReference newER = new QEntityReference(node.getNodeName());
      newER._owner = this;
      return newER;

    case ENTITY_NODE:
      Entity oldEntity = (Entity) node;
      QEntity newEntity = new QEntity(oldEntity.getNodeName(),
                                      oldEntity.getNodeValue(),
                                      oldEntity.getPublicId(),
                                      oldEntity.getSystemId());
      newEntity._owner = this;
      return newEntity;

    case PROCESSING_INSTRUCTION_NODE:
      QProcessingInstruction newPI;
      newPI = new QProcessingInstruction(node.getNodeName(),
                                         node.getNodeValue());

      newPI._owner = this;
      return newPI;

    case COMMENT_NODE:
      QComment newComment = new QComment(node.getNodeValue());
      newComment._owner = this;
      return newComment;

    case DOCUMENT_FRAGMENT_NODE:
      return importFragment((DocumentFragment) node, deep);

    default:
      throw new UnsupportedOperationException(String.valueOf(node));
    }
  }

  /**
   * Imports an element.
   */
  private Element importElement(Element elt, boolean deep)
  {
    QElement newElt = new QElement(createName(elt.getNamespaceURI(),
                                              elt.getNodeName()));
    QElement oldElt = null;

    if (elt instanceof QElement)
      oldElt = (QElement) elt;

    newElt._owner = this;

    if (oldElt != null) {
      newElt._filename = oldElt._filename;
      newElt._line = oldElt._line;
    }

    NamedNodeMap attrs = elt.getAttributes();

    int len = attrs.getLength();
    for (int i = 0; i < len; i++) {
      Attr attr = (Attr) attrs.item(i);

      newElt.setAttributeNode((Attr) importNode(attr, deep));
    }

    if (! deep)
      return newElt;

    for (Node node = elt.getFirstChild();
         node != null;
         node = node.getNextSibling()) {
      newElt.appendChild(importNode(node, true));
    }

    return newElt;
  }

  /**
   * Imports an element.
   */
  private DocumentFragment importFragment(DocumentFragment elt, boolean deep)
  {
    QDocumentFragment newFrag = new QDocumentFragment();

    newFrag._owner = this;

    if (! deep)
      return newFrag;

    for (Node node = elt.getFirstChild();
         node != null;
         node = node.getNextSibling()) {
      newFrag.appendChild(importNode(node, true));
    }

    return newFrag;
  }

  public DocumentType getDoctype() { return _dtd; }

  public void setDoctype(DocumentType dtd)
  {
    QDocumentType qdtd = (QDocumentType) dtd;

    _dtd = qdtd;
    if (qdtd != null)
      qdtd._owner = this;
  }

  public String getEncoding()
  {
    if (_encoding == null)
      return null;
    else
      return _encoding;
  }

  public DOMImplementation getImplementation()
  {
    return _implementation;
  }

  public Element getDocumentElement()
  {
    return _element;
  }

  public void setDocumentElement(Element elt)
  {
    _element = (QElement) elt;
  }

  /**
   * Creates a new element
   */
  public Element createElement(String tagName)
    throws DOMException
  {
    if (! isNameValid(tagName))
      throw new QDOMException(DOMException.INVALID_CHARACTER_ERR,
                              "illegal tag `" + tagName + "'");

    QElement elt = new QElement(createName(null, tagName));
    elt._owner = this;

    return elt;
  }

  /**
   * Creates a new namespace-aware element
   */
  public Element createElementNS(String namespaceURI, String name)
    throws DOMException
  {
    QName qname = createName(namespaceURI, name);

    validateName(qname);
    addNamespace(qname);

    QElement elt = new QElement(qname);
    elt._owner = this;

    return elt;
  }

  public void validateName(QName qname)
    throws DOMException
  {
    String prefix = qname.getPrefix();
    String namespaceURI = qname.getNamespaceURI();

    if (qname.getPrefix() == "") {
    }
    else if (prefix == "xml" &&
             namespaceURI != "http://www.w3.org/XML/1998/namespace")
      throw new DOMException(DOMException.NAMESPACE_ERR,
                             L.l("`xml' prefix expects namespace uri 'http://www.w3.org/XML/1998/namespace'"));
    else if (prefix != "" && prefix != null && namespaceURI == null)
      throw new DOMException(DOMException.NAMESPACE_ERR,
                             L.l("`{0}' prefix expects a namespace uri",
                                 prefix));

  }

  /**
   * Creates a new namespace-aware element
   */
  public Element createElement(String prefix, String local, String url)
    throws DOMException
  {
    QName name = new QName(prefix, local, url);
    addNamespace(name);

    QElement elt = new QElement(name);
    elt._owner = this;

    return elt;
  }

  public Element createElementByName(QName name)
    throws DOMException
  {
    QElement elt = new QElement(name);
    elt._owner = this;

    return elt;
  }

  /**
   * Creates a new document fragment.
   */
  public DocumentFragment createDocumentFragment()
  {
    QDocumentFragment frag = new QDocumentFragment();
    frag._owner = this;

    return frag;
  }

  /**
   * Creates a new text node in this document.
   */
  public Text createTextNode(String data)
  {
    if (data == null)
      data = "";

    QText text = new QText(data);
    text._owner = this;

    return text;
  }

  public Text createUnescapedTextNode(String data)
  {
    if (data == null)
      data = "";

    QText text = new QUnescapedText(data);
    text._owner = this;

    return text;
  }

  public Comment createComment(String data)
  {
    if (data == null)
      data = "";

    QComment comment = new QComment(data);
    comment._owner = this;

    return comment;
  }

  public CDATASection createCDATASection(String data)
  {
    if (data == null)
      data = "";

    QCdata cdata = new QCdata(data);
    cdata._owner = this;

    return cdata;
  }

  public ProcessingInstruction createProcessingInstruction(String target,
                                                           String data)
    throws DOMException
  {
    if (target == null || target.length() == 0)
      throw new QDOMException(DOMException.INVALID_CHARACTER_ERR,
                              L.l("Empty processing instruction name.  The processing instruction syntax is: <?name ... ?>"));

    if (! isNameValid(target))
      throw new QDOMException(DOMException.INVALID_CHARACTER_ERR,
                              L.l("`{0}' is an invalid processing instruction name.  The processing instruction syntax is: <?name ... ?>", target));

    if (data == null)
      data = "";

    QProcessingInstruction pi = new QProcessingInstruction(target, data);
    pi._owner = this;

    return pi;
  }

  public Attr createAttribute(String name, String value)
    throws DOMException
  {
    if (! isNameValid(name))
      throw new QDOMException(DOMException.INVALID_CHARACTER_ERR,
                              "illegal attribute `" + name + "'");

    if (value == null)
      value = "";

    QAttr attr = new QAttr(new QName(null, name, null), value);
    attr._owner = this;

    return attr;
  }

  public Attr createAttribute(String name)
    throws DOMException
  {
    return createAttribute(name, null);
  }

  /**
   * Creates a new namespace-aware attribute
   */
  public Attr createAttribute(String prefix, String local, String url)
    throws DOMException
  {
    QName name = new QName(prefix, local, url);
    if (url != null && ! url.equals(""))
      addNamespace(prefix, url);

    QAttr attr = new QAttr(name, null);
    attr._owner = this;

    return attr;
  }

  /**
   * Creates a new namespace-aware attribute
   */
  public Attr createAttributeNS(String namespaceURI, String qualifiedName)
    throws DOMException
  {
    QName qname = createName(namespaceURI, qualifiedName);

    validateName(qname);
    addNamespace(qname);

    /* xml/0213
    else if (name.getNamespace() == "")
      throw new DOMException(DOMException.NAMESPACE_ERR,
                             L.l("`{0}' prefix expects a namespace uri",
                                 name.getPrefix()));
    */

    QAttr attr = new QAttr(qname, null);
    attr._owner = this;

    return attr;
  }

  public QName createName(String uri, String name)
  {
    _nameKey.init(name, uri);
    QName qName = _nameCache.get(_nameKey);

    if (qName != null)
      return qName;

    if (uri == null) {
      qName = new QName(null, name, null);
    }
    else {
      int p = name.indexOf(':');
      String prefix;
      String local;
      if (p < 0) {
        prefix = null;
        local = name;
      }
      else {
        prefix = name.substring(0, p);
        local = name.substring(p + 1);
      }

      qName = new QName(prefix, local, uri);
    }

    _nameCache.put(new NameKey(name, uri), qName);

    return qName;
  }

  /**
   * Creates a new namespace-aware attribute
   */
  public Attr createAttribute(QName name, String value)
    throws DOMException
  {
    String url = name.getNamespaceURI();

    if (url != null && url != "") {
      addNamespace(name.getPrefix(), url);
    }

    QAttr attr = new QAttr(name, value);
    attr._owner = this;

    return attr;
  }

  public EntityReference createEntityReference(String name)
    throws DOMException
  {
    if (! isNameValid(name))
      throw new QDOMException(DOMException.INVALID_CHARACTER_ERR,
                              "illegal entityReference `" + name + "'");

    QEntityReference er = new QEntityReference(name);
    er._owner = this;

    return er;
  }

  /**
   * Returns a list of elements, filtered by the tag name.
   */
  public NodeList getElementsByTagName(String name)
  {
    if (_element == null)
      return new QDeepNodeList(null, null, null);
    else
      return new QDeepNodeList(_element, _element, new QElement.TagPredicate(name));
  }

  public NodeList getElementsByTagNameNS(String uri, String name)
  {
    if (_element == null)
      return new QDeepNodeList(null, null, null);
    else
      return new QDeepNodeList(_element, _element, new QElement.NSTagPredicate(uri, name));
  }

  public Element getElementById(String name)
  {
    Node node = _element;

    for (; node != null; node = XmlUtil.getNext(node)) {
      if (node instanceof Element) {
        Element elt = (Element) node;

        String id = elt.getAttribute("id");

        if (name.equals(id))
          return elt;
      }
    }

    return null;
  }

  static public Document create()
  {
    QDocument doc = new QDocument();
    doc._masterDoc = doc;

    return doc;
  }

  void setAttributes(HashMap<String,String> attributes)
  {
    _attributes = attributes;
  }

  public Node appendChild(Node newChild) throws DOMException
  {
    if (newChild instanceof Element) {
      _element = (QElement) newChild;

      // xml/0201
      if (false && _namespaces != null) {
        Iterator<String> iter = _namespaces.keySet().iterator();

        while (iter.hasNext()) {
          String prefix = iter.next();
          String ns = _namespaces.get(prefix);

          String xmlns;

          if (prefix.equals(""))
            xmlns = "xmlns";
          else
            xmlns = "xmlns:" + prefix;

          if (_element.getAttribute(xmlns).equals("")) {
            QName qName = new QName(xmlns, XmlParser.XMLNS);
            _element.setAttributeNode(createAttribute(qName, ns));
          }
        }
      }
    }

    return super.appendChild(newChild);
  }

  public Node removeChild(Node oldChild) throws DOMException
  {
    Node value = super.removeChild(oldChild);
    if (oldChild == _element)
      _element = null;
    return value;
  }

  // non-DOM

  public void addNamespace(QName qname)
  {
    addNamespace(qname.getPrefix(), qname.getNamespaceURI());
  }

  /**
   * Add a namespace declaration to a document.  If the declaration
   * prefix already has a namespace, the old one wins.
   */
  public void addNamespace(String prefix, String url)
  {
    if (url == null
        || url.length() == 0
        || XmlParser.XMLNS.equals(url)
        || XmlParser.XML.equals(url))
    {
      return;
    }

    if (prefix == null)
      prefix = "";

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

    String old = _namespaces.get(prefix);
    if (old == null)
      _namespaces.put(prefix, url.intern());
  }

  public HashMap<String,String> getNamespaces()
  {
    return _namespaces;
  }

  /**
   * Returns the namespace url for a given prefix.
   */
  public String getNamespace(String prefix)
  {
    if (_namespaces == null)
      return null;
    else
      return _namespaces.get(prefix);
  }

  /**
   * Returns an iterator of top-level namespace prefixes.
   */
  public Iterator<String> getNamespaceKeys()
  {
    if (_namespaces == null)
      return null;

    return _namespaces.keySet().iterator();
  }

  public Object getProperty(String name)
  {
    if (name.equals(DEPENDS))
      return _depends;
    else
      return null;
  }

  public ArrayList<Path> getDependList()
  {
    return _depends;
  }

  public ArrayList<Depend> getDependencyList()
  {
    return _dependList;
  }

  public void setProperty(String name, Object value)
  {
    if (name.equals(DEPENDS))
      _depends = (ArrayList) value;
  }

  // DOM LEVEL 3
  public String getActualEncoding()
  {
    throw new UnsupportedOperationException();
  }

  public void setActualEncoding(String actualEncoding)
  {
    throw new UnsupportedOperationException();
  }
/*  
  public String getEncoding()
  {
    throw new UnsupportedOperationException();
  }
*/

  public void setEncoding(String encoding)
  {
    throw new UnsupportedOperationException();
  }

  public boolean getStandalone()
  {
    return _standalone;
  }

  public void setStandalone(boolean standalone)
  {
    _standalone = true;
  }

  public String getXmlVersion()
  {
    return _version;
  }

  public void setXmlVersion(String version)
    throws DOMException
  {
    _version = version;
  }

  public void setXmlStandalone(boolean value)
    throws DOMException
  {
  }

  public TypeInfo getSchemaTypeInfo()
  {
    return null;
  }

  public String getXmlEncoding()
  {
    return null;
  }

  public String getInputEncoding()
  {
    return null;
  }

  public boolean getXmlStandalone()
    throws DOMException
  {
    return false;
  }

  public boolean getStrictErrorChecking()
  {
    throw new UnsupportedOperationException();
  }

  public void setStrictErrorChecking(boolean strictErrorChecking)
  {
    throw new UnsupportedOperationException();
  }

  public DOMErrorHandler getErrorHandler()
  {
    throw new UnsupportedOperationException();
  }

  public void setErrorHandler(DOMErrorHandler errorHandler)
  {
    throw new UnsupportedOperationException();
  }

  public String getDocumentURI()
  {
    throw new UnsupportedOperationException();
  }

  public void setDocumentURI(String documentURI)
  {
    throw new UnsupportedOperationException();
  }

  public Node adoptNode(Node source)
    throws DOMException
  {
    throw new UnsupportedOperationException();
  }

  public void normalizeDocument()
  {
    throw new UnsupportedOperationException();
  }

  public boolean canSetNormalizationFeature(String name,
                                            boolean state)
  {
    throw new UnsupportedOperationException();
  }

  public void setNormalizationFeature(String name,
                                      boolean state)
    throws DOMException
  {
    throw new UnsupportedOperationException();
  }

  public boolean getNormalizationFeature(String name)
    throws DOMException
  {
    throw new UnsupportedOperationException();
  }

  public Node renameNode(Node n,
                         String namespaceURI,
                         String name)
    throws DOMException
  {
      throw new UnsupportedOperationException();
  }

  // CAUCHO

  public void addDepend(Path path)
  {
    if (path == null)
      return;

    if (_depends == null)
      _depends = new ArrayList<Path>();

    if (! _depends.contains(path)) {
      _depends.add(path);

      if (_dependList == null)
        _dependList = new ArrayList<Depend>();

      _dependList.add(new Depend(path));
    }
  }

  public boolean isModified()
  {
    if (_dependList == null)
      return false;

    for (int i = 0; i < _dependList.size(); i++) {
      Depend depend = _dependList.get(i);

      if (depend.isModified())
        return true;
    }

    return false;
  }

  void print(XmlPrinter os) throws IOException
  {
    os.startDocument(this);

    if (_namespaces != null) {
      Iterator<String> iter = _namespaces.keySet().iterator();
      while (iter.hasNext()) {
        String prefix = iter.next();
        String url = _namespaces.get(prefix);

        if (prefix.equals(""))
          os.attribute(null, prefix, "xmlns", url);
        else
          os.attribute(null, prefix, "xmlns:" + prefix, url);
      }
    }

    if (getFirstChild() == null)
      os.printHeader(null);

    for (Node node = getFirstChild();
         node != null;
         node = node.getNextSibling()) {
      ((QAbstractNode) node).print(os);
      if (os.isPretty())
        os.println();
    }

    os.endDocument();
  }

  public String toString()
  {
    String topElt = _element == null ? "XXX:top" : _element.getNodeName();

    if (_dtd == null)
      return "Document[" + topElt + "]";

    if (_dtd.getPublicId() != null && _dtd.getSystemId() != null)
      return ("Document[" + topElt + " PUBLIC '" + _dtd.getPublicId() + "' '" +
              _dtd.getSystemId() + "']");
    else if (_dtd._publicId != null)
      return "Document[" + topElt + " PUBLIC '" + _dtd.getPublicId() + "']";
    else if (_dtd.getSystemId() != null)
      return "Document[" + topElt + " SYSTEM '" + _dtd.getSystemId() + "']";
    else
      return "Document[" + topElt + "]";
  }

  static class NameKey {
    String _qName;
    String _url;

    NameKey()
    {
    }

    NameKey(String qName, String url)
    {
      init(qName, url);
    }

    void init(String qName, String url)
    {
      if (qName == null)
        throw new NullPointerException();

      if (url == null)
        url = "";

      _qName = qName;
      _url = url;
    }

    public int hashCode()
    {
      return 65521 * _url.hashCode() + _qName.hashCode();
    }

    public boolean equals(Object b)
    {
      if (! (b instanceof NameKey))
        return false;

      NameKey key = (NameKey) b;

      return _qName.equals(key._qName) && _url.equals(key._url);
    }
  }

  private Object writeReplace()
  {
    return new SerializedXml(this);
  }
}
