/*
 * GnomeNode.java
 * Copyright (C) 2004 The Free Software Foundation
 * 
 * This file is part of GNU JAXP, a library.
 * 
 * GNU JAXP 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.
 * 
 * GNU JAXP 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.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Linking this library statically or dynamically with other modules is
 * making a combined work based on this library.  Thus, the terms and
 * conditions of the GNU General Public License cover the whole
 * combination.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obliged to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */
package gnu.xml.libxmlj.dom;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.DOMException;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.w3c.dom.UserDataHandler;

import gnu.xml.libxmlj.util.StandaloneDocumentType;

/**
 * A DOM node implemented in libxml2.
 *
 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
 */
class GnomeNode
  implements Node, Comparable
{

  /**
   * Maps document pointers to a map of node pointers to node instances.
   */
  static Map instances;

  /**
   * Retrieves the node instance for the specified node pointer.
   * This creates a new instance and adds it to the cache if required.
   * @param doc the document pointer
   * @param node the node pointer
   * @param type the node type
   */
  static GnomeNode newInstance(final Object doc, final Object node,
                               final int type)
  {
    if (doc == null)
      {
        throw new NullPointerException("doc");
      }
    if (node == null)
      {
        throw new NullPointerException("node");
      }
    if (instances == null)
      {
        instances = new HashMap();
      }
    Map docNodes = (Map) instances.get(doc);
    if (docNodes == null)
      {
        docNodes = new HashMap(1024); // TODO review optimal initial capacity
        instances.put(doc, docNodes);
      }
    GnomeNode nodeInstance = (GnomeNode) docNodes.get(node);
    if (nodeInstance != null)
      {
        return nodeInstance; // Return cached version
      }
    switch (type)
      {
      case ELEMENT_NODE:
        nodeInstance = new GnomeElement(node);
        break;
      case ATTRIBUTE_NODE:
        nodeInstance = new GnomeAttr(node);
        break;
      case TEXT_NODE:
        nodeInstance = new GnomeText(node);
        break;
      case CDATA_SECTION_NODE:
        nodeInstance = new GnomeCDATASection(node);
        break;
      case ENTITY_REFERENCE_NODE:
        nodeInstance = new GnomeEntityReference(node);
        break;
      case ENTITY_NODE:
        nodeInstance = new GnomeEntity(node);
        break;
      case PROCESSING_INSTRUCTION_NODE:
        nodeInstance = new GnomeProcessingInstruction(node);
        break;
      case COMMENT_NODE:
        nodeInstance = new GnomeComment(node);
        break;
      case DOCUMENT_NODE:
        nodeInstance = new GnomeDocument(node);
        break;
      case DOCUMENT_TYPE_NODE:
        nodeInstance = new GnomeDocumentType(node);
        break;
      case DOCUMENT_FRAGMENT_NODE:
        nodeInstance = new GnomeDocumentFragment(node);
        break;
      case NOTATION_NODE:
        nodeInstance = new GnomeNotation(node);
        break;
      default:
        throw new IllegalArgumentException("Unknown node type: " + type);
      }
    docNodes.put(node, nodeInstance);
    return nodeInstance;
  }
  
  /**
   * Frees the specified document.
   * This removes all its nodes from the cache.
   */
  static void freeDocument(final Object doc)
  {
    if (instances == null || doc == null)
      {
        return;
      }
    instances.remove(doc);
    //System.out.println("Freed "+instances.remove(doc));
  }
  
  /**
   * xmlNodePtr
   */
  final Object id;

  Map userData;
  Map userDataHandlers;

  GnomeNode(final Object id)
  {
    this.id = id;
  }

  public native String getNodeName();

  public native String getNodeValue()
    throws DOMException;

  public native void setNodeValue(String nodeValue)
    throws DOMException;

  public native short getNodeType();

  public native Node getParentNode();

  public NodeList getChildNodes()
  {
    return new GnomeNodeList(id);
  }

  public native Node getFirstChild();

  public native Node getLastChild();

  public native Node getPreviousSibling();

  public native Node getNextSibling();

  public NamedNodeMap getAttributes()
  {
    return new GnomeNamedNodeMap(id, 0);
  }

  public native Document getOwnerDocument();

  public Node insertBefore(Node newChild, Node refChild)
    throws DOMException
  {
    if (newChild instanceof StandaloneDocumentType)
      {
        DocumentType dt = (DocumentType) newChild;
        newChild = ((GnomeDocument) getOwnerDocument())
          .createDocumentType(dt.getName(), dt.getPublicId(),
                              dt.getSystemId());
      }
    if (newChild == null)
      {
        throw new GnomeDOMException(DOMException.NOT_FOUND_ERR, null);
      }
    if (!(newChild instanceof GnomeNode))
      {
        throw new GnomeDOMException(DOMException.WRONG_DOCUMENT_ERR, null);
      }
    if (refChild == null || !(refChild instanceof GnomeNode))
      {
        throw new GnomeDOMException(DOMException.NOT_FOUND_ERR, null);
      }
    return xmljInsertBefore(newChild, refChild);
  }

  private native Node xmljInsertBefore(Node newChild, Node refChild)
    throws DOMException;

  public Node replaceChild(Node newChild, Node oldChild)
    throws DOMException
  {
    if (newChild instanceof StandaloneDocumentType)
      {
        DocumentType dt = (DocumentType) newChild;
        newChild = ((GnomeDocument) getOwnerDocument())
          .createDocumentType(dt.getName(), dt.getPublicId(),
                              dt.getSystemId());
      }
    if (newChild == null)
      {
        throw new GnomeDOMException(DOMException.NOT_FOUND_ERR, null);
      }
    if (!(newChild instanceof GnomeNode))
      {
        throw new GnomeDOMException(DOMException.WRONG_DOCUMENT_ERR, newChild.toString());
      }
    if (oldChild == null || !(oldChild instanceof GnomeNode))
      {
        throw new GnomeDOMException(DOMException.NOT_FOUND_ERR, null);
      }
    return xmljReplaceChild(newChild, oldChild);
  }

  private native Node xmljReplaceChild(Node newChild, Node oldChild)
    throws DOMException;

  public Node removeChild(Node oldChild)
    throws DOMException
  {
    if (!(oldChild instanceof GnomeNode))
      {
        throw new GnomeDOMException(DOMException.WRONG_DOCUMENT_ERR, null);
      }
    return xmljRemoveChild(oldChild);
  }

  private native Node xmljRemoveChild(Node oldChild)
    throws DOMException;

  public Node appendChild(Node newChild)
    throws DOMException
  {
    if (newChild instanceof StandaloneDocumentType)
      {
        DocumentType dt = (DocumentType) newChild;
        newChild = ((GnomeDocument) getOwnerDocument())
          .createDocumentType(dt.getName(), dt.getPublicId(),
                              dt.getSystemId());
      }
    if (!(newChild instanceof GnomeNode))
      {
        throw new GnomeDOMException(DOMException.WRONG_DOCUMENT_ERR, null);
      }
    return xmljAppendChild(newChild);
  }

  private native Node xmljAppendChild(Node newChild)
    throws DOMException;

  public native boolean hasChildNodes();

  public Node cloneNode(boolean deep)
  {
    Node ret = xmljCloneNode(deep);
    notifyUserDataHandlers(UserDataHandler.NODE_CLONED, this, ret);
    return ret;
  }
  
  private native Node xmljCloneNode(boolean deep);

  public native void normalize();

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

  public native String getNamespaceURI();

  public native String getPrefix();

  public native void setPrefix(String prefix)
    throws DOMException;

  public native String getLocalName();

  public native boolean hasAttributes();

  public int hashCode()
  {
    return id.hashCode();
  }

  public boolean equals(Object other)
  {
    if (other == this)
      {
        return true;
      }
    return (other instanceof GnomeNode &&
            ((GnomeNode) other).id == id);
  }

  // DOM Level 3 methods

  public native String getBaseURI();

  public short compareDocumentPosition(Node other)
    throws DOMException
  {
    return (short) compareTo(other);
  }

  public final int compareTo(Object other)
  {
    if (other instanceof GnomeNode)
      {
        return xmljCompareTo(other);
      }
    return 0;
  }

  private native int xmljCompareTo(Object other);
  
  public String getTextContent()
    throws DOMException
  {
    switch (getNodeType())
      {
      case ELEMENT_NODE:
      case ATTRIBUTE_NODE:
      case ENTITY_NODE:
      case ENTITY_REFERENCE_NODE:
      case DOCUMENT_FRAGMENT_NODE:
        StringBuffer buffer = new StringBuffer();
        NodeList children = getChildNodes();
        int len = children.getLength();
        for (int i = 0; i < len; i++)
          {
            Node child = children.item(i);
            String textContent = child.getTextContent();
            if (textContent != null)
              {
                buffer.append(textContent);
              }
          }
        return buffer.toString();
      case TEXT_NODE:
      case CDATA_SECTION_NODE:
      case COMMENT_NODE:
      case PROCESSING_INSTRUCTION_NODE:
        return getNodeValue();
      default:
        return null;
      }
  }
  
  public void setTextContent(String textContent)
    throws DOMException
  {
    switch (getNodeType())
      {
      case ENTITY_REFERENCE_NODE:
        // entity references are read only
        throw new GnomeDOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
                                    null);
      case ELEMENT_NODE:
      case ATTRIBUTE_NODE:
      case ENTITY_NODE:
      case DOCUMENT_FRAGMENT_NODE:
        NodeList children = getChildNodes();
        int len = children.getLength();
        for (int i = 0; i < len; i++)
          {
            Node child = children.item(i);
            removeChild(child);
          }
        if (textContent != null)
          {
            Text text = getOwnerDocument().createTextNode(textContent);
            appendChild(text);
          }
        break;
      case TEXT_NODE:
      case CDATA_SECTION_NODE:
      case COMMENT_NODE:
      case PROCESSING_INSTRUCTION_NODE:
        setNodeValue(textContent);
        break;
      }
  }
  
  public boolean isSameNode(Node other)
  {
    return equals(other);
  }
  
  public native String lookupPrefix(String namespaceURI);
  
  public native boolean isDefaultNamespace(String namespaceURI);
  
  public native String lookupNamespaceURI(String prefix);
  
  public native boolean isEqualNode(Node arg);
  
  public Object getFeature(String feature, String version)
  {
    return getOwnerDocument().getImplementation()
      .getFeature(feature, version);
  }

  public Object setUserData(String key, Object data, UserDataHandler handler)
  {
    // TODO handler
    if (userData == null)
      {
        userData = new HashMap();
      }
    if (handler != null)
      {
        if (userDataHandlers == null)
          {
            userDataHandlers = new HashMap();
          }
        userDataHandlers.put(key, handler);
      }
    return userData.put(key, data);
  }

  public Object getUserData(String key)
  {
    if (userData == null)
      {
        return null;
      }
    return userData.get(key);
  }

  void notifyUserDataHandlers(short op, Node src, Node dst)
  {
    if (userDataHandlers != null)
      {
        for (Iterator i = userDataHandlers.entrySet().iterator(); i.hasNext(); )
          {
            Map.Entry entry = (Map.Entry) i.next();
            String key = (String) entry.getKey();
            UserDataHandler handler = (UserDataHandler) entry.getValue();
            Object data = userData.get(key);
            handler.handle(op, key, data, src, dst);
          }
      }
  }

  public String toString()
  {
    StringBuffer buffer = new StringBuffer(getClass().getName());
    buffer.append("[nodeName=");
    buffer.append(getNodeName());
    buffer.append("]");
    return buffer.toString();
  }

}
