/*
 * Copyright 2007 Edward Kuns
 *
 * 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.
 *
 * $Id: Element.java 0000 2007-01-11 00:00:00Z ekuns $
 */
package org.castor.xmlctf.xmldiff.xml.nodes;

import java.util.Iterator;

import org.castor.xmlctf.xmldiff.xml.Location;

/**
 * The base node for all XMLNode types.
 *
 * @author <a href="mailto:edward.kuns@aspect.com">Edward Kuns</a>
 * @version $Revision: 0000 $ $Date: 2007-01-11 00:00:00 -0600 (Thu, 11 Jan 2007) $
 * @since Castor 1.1
 */
public abstract class XMLNode {

  /** Node is a root node. */
  public static final int ROOT = 1;
  /** Node is an element. */
  public static final int ELEMENT = 2;
  /** Node is an attribute. */
  public static final int ATTRIBUTE = 3;
  /** Node is a text node. */
  public static final int TEXT = 4;
  /** Node is a processing instruction. */
  public static final int PROCESSING_INSTRUCTION = 5;

  /** The localname (non-qualified) for this XMLNode. */
  private final String _localName;
  /** The node type being created. */
  private final int _nodeType;

  /** A reference for the parent node. */
  private ParentNode _parent = null;
  /** The namespace to which this XMLNode belongs. */
  private String _namespace = null;

  /**
   * Creates a new XMLNode
   *
   * @param namespace the namespace URI for this node. [May be null]
   * @param localName the local-name of this node. [May be null]
   * @param nodeType the node type being created
   */
  XMLNode(final String namespace, final String localName, final int nodeType) {
    _namespace = namespace;
    _localName = localName;
    _nodeType = nodeType;
  }

  /**
   * Returns the type of this node.
   * 
   * @return The type of this node
   */
  public final int getNodeType() {
    return _nodeType;
  }

  /**
   * Returns the local name of the node. Returns the local name of an element or attribute, the
   * prefix of a namespace node, the target of a processing instruction, or null for all other node
   * types.
   *
   * @return The local name of the node, or null if the node has no name
   */
  public String getLocalName() {
    return _localName;
  }

  /**
   * Returns the namespace URI the node. Returns the namespace URI of an element, attribute or
   * namespace node, or null for all other node types.
   *
   * @return The namespace URI of the node, or null if the node has no namespace URI
   */
  public String getNamespaceURI() {
    return _namespace;
  }

  /**
   * Returns the parent node, or null if the node has no parent. This method is valid on all node
   * types except the root node. Attribute and namespace nodes have the element as their parent
   * node.
   *
   * @return The parent node, or null
   */
  public ParentNode getParentNode() {
    return _parent;
  }

  /**
   * Returns the root node.
   *
   * @return The root node
   */
  public XMLNode getRootNode() {
    return (_parent != null) ? _parent.getRootNode() : null;
  }

  /**
   * Returns the string value of the node. The string value of a text node or an attribute node is
   * its text value. The string value of an element or a root node is the concatenation of the
   * string value of all its child nodes. The string value of a namespace node is its namespace URI.
   * The string value of a processing instruction is the instruction, and the string value of a
   * comment is the comment text.
   *
   * @return The string value of the node
   */
  public abstract String getStringValue();

  /**
   * Returns the namespace URI associated with this namespace prefix, as defined in the context of
   * this node. Returns null if the prefix is undefined. Returns empty if the prefix is defined and
   * associated with no namespace. This method is valid only for element nodes.
   *
   * @param prefix The namespace prefix
   * @return The namespace URI, or null
   */
  public String getNamespaceURI(final String prefix) {
    return (_parent != null) ? _parent.getNamespaceURI(prefix) : null;
  }

  /**
   * Sets the namespace URI for this XMLNode.
   * 
   * @param namespace the Namespace URI
   */
  public void setNamespace(final String namespace) {
    _namespace = namespace;
  }

  /**
   * Sets the parent XMLNode.
   *
   * @param node the XMLNode which is the parent of this XMLNode
   */
  void setParent(final ParentNode node) {
    _parent = node;
  }

  /**
   * Finds and returns the location of this node in its root's tree.
   * 
   * @return the location of this node in its root's tree.
   */
  public String getNodeLocation() {
    int column = -1;
    int line = -1;

    String xpath = "XPATH: " + getXPath();

    if (this instanceof Element) {
      Location loc = ((Element) this).getLocation();
      if (loc != null) {
        line = loc.getLineNumber();
        column = loc.getColumnNumber();
      }
    }

    String location = null;
    if (line >= 0) {
      location = "[" + line + ", " + column + "] " + xpath;
    } else {
      location = xpath;
    }

    return location;
  }

  /**
   * Returns the XPath from the root node to this node.
   * 
   * @return the XPath from the root node to this node.
   */
  protected String getXPath() {
    StringBuffer xpath = new StringBuffer();

    switch (getNodeType()) {
      case XMLNode.ATTRIBUTE:
        xpath.append(getParentNode().getXPath() + "/@" + getLocalName());
        break;
      case XMLNode.ELEMENT:
        String name = getLocalName();
        xpath.append(getParentNode().getXPath() + "/" + name);

        // Do we have elements of the same type before us in our parent's list?
        int position = 1;
        Iterator i = getParentNode().getChildIterator();
        while (i.hasNext()) {
          XMLNode sibling = (XMLNode) i.next();
          if (sibling == this) {
            break;
          }
          if (name.equals(sibling.getLocalName())) {
            ++position;
          }
        }

        boolean usePosition = (position > 1);
        if (!usePosition) {
          // Do we have elements of the same type after us in our parent's list?
          while (i.hasNext()) {
            XMLNode sibling = (XMLNode) i.next();
            if (name.equals(sibling.getLocalName())) {
              usePosition = true;
              break;
            }
          }
        }
        if (usePosition) {
          xpath.append("[" + position + "]");
        }
        break;
      case XMLNode.TEXT:
        xpath.append(getParentNode().getXPath() + "/text()");
        break;
      case XMLNode.ROOT:
        break;
      case XMLNode.PROCESSING_INSTRUCTION:
        xpath.append(getParentNode().getXPath() + "/pi()");
        break;
      default:
        break;
    }

    return xpath.toString();
  }

}
