/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dom.client;

import com.google.gwt.core.client.JavaScriptObject;

/**
 * The Node interface is the primary datatype for the entire Document Object
 * Model. It represents a single node in the document tree. While all objects
 * implementing the Node interface expose methods for dealing with children, not
 * all objects implementing the Node interface may have children.
 */
public class Node extends JavaScriptObject {

  /**
   * The node is an {@link Element}.
   */
  public static final short ELEMENT_NODE = 1;

  /**
   * The node is a {@link Text} node.
   */
  public static final short TEXT_NODE = 3;

  /**
   * The node is a {@link Document}.
   */
  public static final short DOCUMENT_NODE = 9;

  /**
   * Assert that the given {@link JavaScriptObject} is a DOM node and
   * automatically typecast it.
   */
  public static Node as(JavaScriptObject o) {
    assert is(o);
    return (Node) o;
  }

  /**
   * Determines whether the given {@link JavaScriptObject} is a DOM node. A
   * <code>null</code> object will cause this method to return
   * <code>false</code>.
   * The try catch is needed for the firefox permission error:
   * "Permission denied to access property 'nodeType'"
   */
  public static native boolean is(JavaScriptObject o) /*-{
    try {
      return (!!o) && (!!o.nodeType);
    } catch (e) {
      return false;
    }
  }-*/;

  protected Node() {
  }

  /**
   * Adds the node newChild to the end of the list of children of this node. If
   * the newChild is already in the tree, it is first removed.
   * 
   * @param newChild The node to add
   * @return The node added
   */
  public final native <T extends Node> T appendChild(T newChild) /*-{
    return this.appendChild(newChild);
  }-*/;

  /**
   * Returns a duplicate of this node, i.e., serves as a generic copy
   * constructor for nodes. The duplicate node has no parent; (parentNode is
   * null.).
   * 
   * Cloning an Element copies all attributes and their values, including those
   * generated by the XML processor to represent defaulted attributes, but this
   * method does not copy any text it contains unless it is a deep clone, since
   * the text is contained in a child Text node. Cloning an Attribute directly,
   * as opposed to be cloned as part of an Element cloning operation, returns a
   * specified attribute (specified is true). Cloning any other type of node
   * simply returns a copy of this node.
   * 
   * @param deep If true, recursively clone the subtree under the specified
   *          node; if false, clone only the node itself (and its attributes, if
   *          it is an {@link Element})
   * @return The duplicate node
   */
  public final native Node cloneNode(boolean deep) /*-{
    return this.cloneNode(deep);
  }-*/;

  /**
   * Gets the child node at the given index.
   * 
   * @param index the index of the node to be retrieved
   * @return the child node at the given index
   */
  public final Node getChild(int index) {
    assert (index >= 0) && (index < getChildCount()) : "Child index out of bounds";

    return getChildNodes().getItem(index);
  }

  /**
   * Gets the number of child nodes contained within this node.
   * 
   * @return the number of child nodes
   */
  public final int getChildCount() {
    return getChildNodes().getLength();
  }

  /**
   * A NodeList that contains all children of this node. If there are no
   * children, this is a NodeList containing no nodes.
   */
  public final native NodeList<Node> getChildNodes() /*-{
    return this.childNodes;
  }-*/;

  /**
   * The first child of this node. If there is no such node, this returns null.
   */
  public final native Node getFirstChild() /*-{
    return this.firstChild;
  }-*/;

  /**
   * The last child of this node. If there is no such node, this returns null.
   */
  public final native Node getLastChild() /*-{
    return this.lastChild;
  }-*/;

  /**
   * The node immediately following this node. If there is no such node, this
   * returns null.
   */
  public final native Node getNextSibling() /*-{
    return this.nextSibling;
  }-*/;

  /**
   * The name of this node, depending on its type; see the table above.
   */
  public final native String getNodeName() /*-{
    return this.nodeName;
  }-*/;

  /**
   * A code representing the type of the underlying object, as defined above.
   */
  public final native short getNodeType() /*-{
    return this.nodeType;
  }-*/;

  /**
   * The value of this node, depending on its type; see the table above. When it
   * is defined to be null, setting it has no effect.
   */
  public final native String getNodeValue() /*-{
    return this.nodeValue;
  }-*/;

  /**
   * The Document object associated with this node. This is also the
   * {@link Document} object used to create new nodes.
   */
  public final native Document getOwnerDocument() /*-{
    return this.ownerDocument;
  }-*/;

  /**
   * Gets the parent element of this node.
   * 
   * @return this node's parent element, or <code>null</code> if none exists
   */
  public final Element getParentElement() {
    return DOMImpl.impl.getParentElement(this);
  }

  /**
   * The parent of this node. All nodes except Document may have a parent.
   * However, if a node has just been created and not yet added to the tree, or
   * if it has been removed from the tree, this is null.
   */
  public final native Node getParentNode() /*-{
    return this.parentNode;
  }-*/;

  /**
   * The node immediately preceding this node. If there is no such node, this
   * returns null.
   */
  public final native Node getPreviousSibling() /*-{
    return this.previousSibling;
  }-*/;

  /**
   * Returns whether this node has any children.
   */
  public final native boolean hasChildNodes() /*-{
    return this.hasChildNodes();
  }-*/;

  /**
   * Determines whether this node has a parent element.
   * 
   * @return true if the node has a parent element
   */
  public final boolean hasParentElement() {
    return getParentElement() != null;
  }

  /**
   * Inserts the node newChild after the existing child node refChild. If
   * refChild is <code>null</code>, insert newChild at the end of the list of children.
   * 
   * @param newChild The node to insert
   * @param refChild The reference node (that is, the node after which the new
   *          node must be inserted), or <code>null</code>
   * @return The node being inserted
   */
  public final Node insertAfter(Node newChild, Node refChild) {
    assert (newChild != null) : "Cannot add a null child node";

    Node next = (refChild == null) ? null : refChild.getNextSibling();
    if (next == null) {
      return appendChild(newChild);
    } else {
      return insertBefore(newChild, next);
    }
  }

  /**
   * Inserts the node newChild before the existing child node refChild. If
   * refChild is <code>null</code>, insert newChild at the end of the list of children.
   * 
   * @param newChild The node to insert
   * @param refChild The reference node (that is, the node before which the new
   *          node must be inserted), or <code>null</code> 
   * @return The node being inserted
   */
  public final native Node insertBefore(Node newChild, Node refChild) /*-{
    return this.insertBefore(newChild, refChild);
  }-*/;

  /**
   * Inserts the given child as the first child of this node.
   * 
   * @param child the child to be inserted
   * @return The node being inserted
   */
  public final Node insertFirst(Node child) {
    assert (child != null) : "Cannot add a null child node";

    return insertBefore(child, getFirstChild());
  }

  /**
   * Determine whether a node is equal to, or the child of, this node.
   * 
   * @param child the potential child element
   * @return <code>true</code> if the relationship holds
   */
  public final boolean isOrHasChild(Node child) {
    assert (child != null) : "Child cannot be null";

    return DOMImpl.impl.isOrHasChild(this, child);
  }

  /**
   * Removes the child node indicated by oldChild from the list of children, and
   * returns it.
   * 
   * @param oldChild The node being removed
   * @return The node removed
   */
  public final native Node removeChild(Node oldChild) /*-{
    return this.removeChild(oldChild);
  }-*/;

  /**
   * Remove all children of the node.
   */
  public final native Node removeAllChildren() /*-{
    while (this.lastChild) {
      this.removeChild(this.lastChild);
    }
  }-*/;

  /**
   * Removes this node from its parent node if it is attached to one.
   */
  public final void removeFromParent() {
    Element parent = getParentElement();
    if (parent != null) {
      parent.removeChild(this);
    }
  }

  /**
   * Replaces the child node oldChild with newChild in the list of children, and
   * returns the oldChild node.
   * 
   * @param newChild The new node to put in the child list
   * @param oldChild The node being replaced in the list
   * @return The node replaced
   */
  public final native Node replaceChild(Node newChild, Node oldChild) /*-{
    return this.replaceChild(newChild, oldChild);
  }-*/;

  /**
   * The value of this node, depending on its type; see the table above. When it
   * is defined to be null, setting it has no effect.
   */
  public final native void setNodeValue(String nodeValue) /*-{
    this.nodeValue = nodeValue;
  }-*/;
}
