/*
 * Redistribution and use of this software and associated documentation ("Software"), with or
 * without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
 * must also contain a copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
 * without prior written permission of Intalio, Inc. For written permission, please contact
 * info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
 * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
 * Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2001 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id$ Date Author Changes 04/24/2001 Arnaud Blandin Rewrited from scratch 04/22/2001 Arnaud
 * Blandin Clean-up and support of comments 04/04/2001 Arnaud Blandin Created
 */
package org.exolab.castor.types;

import java.io.StringWriter;
import java.util.Stack;

import org.castor.xml.BackwardCompatibilityContext;
import org.exolab.castor.xml.Serializer;
import org.exolab.castor.xml.util.AnyNode2SAX;

/**
 * A class used to represent an XML node. This is an alternative to DOM which is too heavy for our
 * purpose (mainly handle XML Fragment when {@literal <any>} is used in an XML schema). The model is
 * based on XPath Node. An AnyNode can be a:
 * <ul>
 * <li>ELEMENT</li>
 * <li>ATTRIBUTE</li>
 * <li>NAMESPACE</li>
 * <li>COMMENT</li>
 * <li>TEXT</li>
 * <li>PROCESSING INSTRUCTION</li>
 * </ul>
 *
 * @author <a href="mailto:arkin@intalio.com">Assaf Arkin</a>
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @author <a href="mailto:kvisco@intalio.com">Keith Visco</a>
 * @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
 */
public final class AnyNode implements java.io.Serializable {
  // TODO Processing Instructions
  // TODO Full handling of namespaces

  /** SerialVersionUID */
  private static final long serialVersionUID = -4104117996051705975L;

  /**
   * The prefix for XML namespace
   */
  private final static String XMLNS_PREFIX = "xmlns";

  /**
   * Representation for an element node.
   */
  public static final short ELEMENT = 1;

  /**
   * Representation for an attribute node.
   */
  public static final short ATTRIBUTE = 2;

  /**
   * Representation for a Namespace node.
   */
  public static final short NAMESPACE = 3;

  /**
   * Representation for a processing instruction node.
   */
  public static final short PI = 4;

  /**
   * Representation for a comment node.
   */
  public static final short COMMENT = 5;

  /**
   * Representation for a text node.
   */
  public static final short TEXT = 6;

  /**
   * The type of the current node. ELEMENT is the default value.
   */
  private short _nodeType = ELEMENT;

  /**
   * The next sibling of this AnyNode
   */
  private AnyNode _nextSiblingNode = null;

  /**
   * The first child of this AnyNode
   */
  private AnyNode _firstChildNode = null;


  /**
   * the local name of the current node.
   */
  private String _localName;

  /**
   * the Namespace URI of the current node
   */
  private String _uri;

  /**
   * The prefix of the Namespace
   */
  private String _prefix;

  /**
   * A stack used for avoiding endless loop in toString()
   */
  private static Stack<AnyNode> _elements;

  /**
   * The namespace context used in the toString()
   */

  /**
   * The value of this node defined as follow:
   * <ul>
   * <li>for an element the value is its TEXT NODE value (if any)</li>
   * <li>for an attribute the value is the value of the attribute</li>
   * <li>for a text node it is the character data</li>
   * <li>for a namespace it is the namespace URI that is being bound to the namespace prefix</li>
   * <li>for a comment it is the content of the comment not including the opening &lt;!-- and the
   * closing --&gt;.</li>
   * </ul>
   */
  private String _value;

  /**
   * Default constructor: creates an empty element node
   */
  public AnyNode() {
    this(ELEMENT, null, null, null, null);
  }

  /**
   * Creates a node given all the necessary information: type, localName, prefix, uri and value.
   * This constructor is not user-friendly and launched RunTime exception is you try to instantiate
   * non-valid node.
   * 
   * @param type the node type.
   * @param localName the name of the node.
   * @param prefix the prefix if any of the namespace.
   * @param uri the namespace uri of this node.
   * @param value the value of this node.
   */
  public AnyNode(short type, String localName, String prefix, String uri, String value) {
    if ((type > 6) && (type < 1)) {
      throw new IllegalArgumentException("Illegal node type");
    }
    _nodeType = type;

    // comment and text nodes don't have name
    if ((type > PI) && (localName != null)) {
      String err = "This node can not have a local name";
      throw new IllegalArgumentException(err);
    }
    _localName = localName;

    // for comment, pi or text we should have no namespaces
    if ((type > NAMESPACE) && ((uri != null) || (prefix != null))) {
      String err = "This node can not handle namespace";
      throw new IllegalArgumentException(err);
    }
    _uri = uri;
    _prefix = prefix;

    // attributes can not be namespaces
    if (type == AnyNode.ATTRIBUTE)
      if (localName.startsWith(XMLNS_PREFIX)) {
        String err = "Namespaces can't be used as attributes.";
        throw new IllegalArgumentException(err);
      }

    // you can't set value for element
    if ((type == ELEMENT) && (value != null)) {
      String err = "You can't set a value for this node type";
      throw new IllegalArgumentException(err);
    }
    _value = value;
  }

  /**
   * Adds an AnyNode to the current node
   * 
   * @param node the node to append
   */
  public void addAnyNode(AnyNode node) {
    if (node == null) {
      throw new IllegalArgumentException("null argument in addAnyNode");
    }

    switch (node.getNodeType()) {
      case ATTRIBUTE:
        addAttribute(node);
        break;
      case NAMESPACE:
        addNamespace(node);
        break;
      default:
        addChild(node);
        break;
    }
  }

  /**
   * <p>
   * Adds a child AnyNode to this node. A 'child' can be either an ELEMENT node, a COMMENT node, a
   * TEXT node or a PROCESSING INSTRUCTION. If the current node already has a child then the node to
   * add will be append as a sibling.
   * <p>
   * Note: you cannot add a child to a TEXT node.
   *
   * @param node the node to add.
   */
  public void addChild(AnyNode node) {
    if (node == null) {
      throw new IllegalArgumentException("null argument in appendChild");
    }

    if (node.getNodeType() == ATTRIBUTE || node.getNodeType() == NAMESPACE) {
      throw new IllegalArgumentException("An Attribute or an Namespace can't be added as a child");
    }

    if (this.getNodeType() == TEXT) {
      throw new IllegalArgumentException("a TEXT node can't have children.");
    }

    if (_firstChildNode == null) {
      _firstChildNode = node;
    } else if (_firstChildNode.getNodeType() == ATTRIBUTE
        || _firstChildNode.getNodeType() == NAMESPACE) {
      _firstChildNode.addChild(node);
    } else {
      _firstChildNode.appendSibling(node);
    }
  }

  /**
   * Adds an attribute to the current node.
   * 
   * @param node the attribute to add.
   */
  public void addAttribute(AnyNode node) {
    if (node == null) {
      throw new IllegalArgumentException("null argument in addAttribute");
    }

    if (node.getNodeType() != ATTRIBUTE) {
      throw new IllegalArgumentException("Only attribute can be added as an attribute");
    }

    if (_firstChildNode == null) {
      _firstChildNode = node;
    } else {
      if (_firstChildNode.getNodeType() == ATTRIBUTE) {
        // if we reach an attribute then we add the node as its sibling
        _firstChildNode.appendSibling(node);
      } else if (_firstChildNode.getNodeType() == NAMESPACE) {
        // if we reach an namespace the attributre should be added to
        // the first child of the namespace
        _firstChildNode.addAttribute(node);
      } else {
        // unplug the current firstNode to add a new one
        node.addChild(_firstChildNode);
        _firstChildNode = node;
      }
    }
  } // addAttribute

  /**
   * Appends an namespace to the current node.
   * 
   * @param node the attribute to add.
   */
  public void addNamespace(AnyNode node) {
    if (node == null) {
      throw new IllegalArgumentException("null argument in addNamespace");
    }

    if (node.getNodeType() != NAMESPACE) {
      throw new IllegalArgumentException("Only namespace can be added as an namespace");
    }

    if (_firstChildNode == null) {
      _firstChildNode = node;
    } else {
      if (_firstChildNode.getNodeType() == NAMESPACE) {
        // if we reach an namepace then we add the node as its sibling
        _firstChildNode.appendSibling(node);
      } else if (_firstChildNode.getNodeType() == ATTRIBUTE) {
        // if we reach an attribute the attributre should be added to
        // the first child of the attribute
        _firstChildNode.addNamespace(node);
      } else {
        // unplug the current firstNode to add a new one
        node.addChild(_firstChildNode);
        _firstChildNode = node;
      }
    }
  } // addNamespace

  /**
   * Returns the first attribute of the current ELEMENT node or null. The next attribute,if any,is
   * the sibling of the returned node.
   */
  public AnyNode getFirstAttribute() {
    if (this.getNodeType() != ELEMENT) {
      String err = "This node type can not contain attributes";
      throw new UnsupportedOperationException(err);
    }

    boolean found = false;
    AnyNode tempNode = this.getFirstChildNode();
    while (tempNode != null && !found) {
      short type = tempNode.getNodeType();
      // if the child is not an attribute or a namespace
      // this element does not have any attribute
      if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
        tempNode = null;
      } else if (type == NAMESPACE) {
        tempNode = tempNode.getFirstChildNode();
      } else {
        found = true;
      }
    }
    return tempNode;
  }

  /**
   * Returns the first namespace of the current ELEMENT node or null. The next attribute if any is
   * the sibling of the returned node.
   * 
   * @return the first namespace of the current ELEMENT node or null.
   */
  public AnyNode getFirstNamespace() {
    if (this.getNodeType() != ELEMENT) {
      String err = "This node type can not contain namespaces";
      throw new UnsupportedOperationException(err);
    }

    AnyNode tempNode = this.getFirstChildNode();
    boolean found = false;
    while (tempNode != null && !found) {
      short type = tempNode.getNodeType();
      // if the child is not an attribute or a namespace
      // this element does not have any namespace
      if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
        tempNode = null;
      } else if (type == ATTRIBUTE) {
        tempNode = tempNode.getFirstChildNode();
      } else {
        found = true;
      }
    }
    return tempNode;
  }

  /**
   * Returns the first Child node of this node. A 'child' can be either an ELEMENT node, a COMMENT
   * node, a TEXT node or a PROCESSING INSTRUCTION.
   *
   * @return the first child of this node
   */
  public AnyNode getFirstChild() {
    // an ATTRIBUTE or a NAMESPACE can not
    // have children
    if (this.getNodeType() == ATTRIBUTE || this.getNodeType() == NAMESPACE) {
      return null;
    }

    // loop througth the first two (in the worst case) nodes
    // and then return the firstChild if any
    AnyNode tempNode = this.getFirstChildNode();
    boolean found = false;
    while (tempNode != null && !found) {
      short type = tempNode.getNodeType();
      if (type == ELEMENT || type == COMMENT || type == TEXT || type == PI) {
        found = true;
      } else if (type == ATTRIBUTE || type == NAMESPACE) {
        tempNode = tempNode.getFirstChildNode();
      }
    }
    return tempNode;
  }

  /**
   * Returns the next sibling of the current node. When the AnyNode is an ATTRIBUTE, it will return
   * the next ATTRIBUTE node. When the AnyNode is a NAMESPACE, it will return the next NAMESPACE
   * node.
   *
   * @return the next sibling of the current node
   */
  public AnyNode getNextSibling() {
    return _nextSiblingNode;
  }

  /**
   * Returns the type of this node.
   * 
   * @return The type of this node
   */
  public short 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 of 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 _uri;
  }

  /**
   * 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 String getStringValue() {
    switch (_nodeType) {
      case ATTRIBUTE:
      case TEXT:
        return _value;
      case NAMESPACE:
        return _uri;
      // not yet supported
      case PI:
        return "";
      case COMMENT:
        return _value;
      case ELEMENT:
        StringBuffer result = new StringBuffer(4096);
        AnyNode tempNode = this.getNextSibling();
        while (tempNode != null && tempNode.getNodeType() == TEXT) {
          result.append(tempNode.getStringValue());
          tempNode = tempNode.getNextSibling();
        }

        tempNode = this.getFirstChild();
        while (tempNode != null) {
          result.append(tempNode.getStringValue());
          tempNode = tempNode.getNextSibling();
        }
        tempNode = null;
        return result.toString();
      default:
        return null;
    }
  }

  /**
   * Returns the namespace prefix associated with the namespace URI of this node. Returns null if no
   * prefix. is defined for this namespace URI. Returns an empty string if the default prefix is
   * associated with this namespace URI.
   *
   * @return The namespace prefix, or null
   */
  public String getNamespacePrefix() {
    return _prefix;
  }

  /**
   * Returns the String representation of this AnyNode. The String representation is a xml
   * well-formed fragment corresponding to the representation of this node.
   * 
   * @return the String representation of this AnyNode.
   */
  public String toString() {
    Serializer serializer = new BackwardCompatibilityContext().getSerializer();
    if (serializer == null) {
      throw new RuntimeException("Unable to obtain serializer");
    }

    StringWriter writer = new StringWriter();

    serializer.setOutputCharStream(writer);

    try {
      AnyNode2SAX.fireEvents(this, serializer.asDocumentHandler());
    } catch (java.io.IOException ioe) {
      return privateToString();
    } catch (org.xml.sax.SAXException saxe) {
      throw new RuntimeException(saxe.getMessage());
    }

    return writer.toString();
  }

  private String privateToString() {
    StringBuilder sb = new StringBuilder(4096);

    if (_elements == null) {
      _elements = new Stack<>();
    }

    // check the Stack too see if we have
    // already proceed the node
    if (_elements.search(this) == -1) {
      _elements.push(this);

      if (this.getNodeType() == ELEMENT) {
        // open the tag
        sb.append('<');
        String prefix = getNamespacePrefix();
        if (prefix != null) {
          sb.append(prefix).append(':');
        }
        prefix = null;
        sb.append(getLocalName());

        // append the attributes
        AnyNode tempNode = this.getFirstAttribute();
        while (tempNode != null) {
          sb.append(' ').append(tempNode.getLocalName()).append("='")
              .append(tempNode.getStringValue()).append('\'');
          tempNode = tempNode.getNextSibling();
        }

        // append the namespaces
        tempNode = this.getFirstNamespace();
        while (tempNode != null) {
          sb.append(' ').append(XMLNS_PREFIX);
          prefix = tempNode.getNamespacePrefix();
          if (prefix != null && prefix.length() != 0) {
            sb.append(':').append(prefix);
          }
          sb.append("='").append(tempNode.getNamespaceURI()).append('\'');
          tempNode = tempNode.getNextSibling();
        } // namespaceNode

        tempNode = this.getFirstChild();
        if (tempNode != null) {
          sb.append('>');
          while (tempNode != null) {
            sb.append(tempNode.privateToString());
            tempNode = tempNode.getNextSibling();
          }
          // close the tag
          sb.append("</").append(getLocalName()).append('>');
        } else {
          sb.append("/>");
        }
      } else {
        sb.append(this.getStringValue());
      }
    }
    return sb.toString();
  }// toString()

  /**
   * Appends a sibling AnyNode to the current node. The node to append will be added at the end of
   * the sibling branch.
   *
   * @param node the node to add
   */
  protected void appendSibling(AnyNode node) {
    if (node == null) {
      throw new IllegalArgumentException();
    }

    if (((node.getNodeType() == ATTRIBUTE) || (node.getNodeType() == NAMESPACE))
        && (this.getNodeType() != node.getNodeType())) {
      String err =
          "a NAMESPACE or an ATTRIBUTE can only be add as a sibling to a node of the same type";
      throw new UnsupportedOperationException(err);
    }

    if (_nextSiblingNode == null) {
      // if we already have a TEXT node -> merge
      if ((node.getNodeType() == TEXT) && (this.getNodeType() == TEXT)) {
        mergeTextNode(this, node);
      } else {
        _nextSiblingNode = node;
      }
    } else {
      _nextSiblingNode.appendSibling(node);
    }
  }

  /**
   * Returns the first child node in the tree.
   * 
   * @return the first child node in the tree.
   */
  protected AnyNode getFirstChildNode() {
    return _firstChildNode;
  }

  /**
   * Adds the text value of a TEXT node to another TEXT node.
   * 
   * @param node1 the AnyNode that receives the text value
   * @param node2 the AnyNode that needs to be merges with node1.
   */
  private void mergeTextNode(AnyNode node1, AnyNode node2) {
    if (node1.getNodeType() != node2.getNodeType()) {
      return;
    }
    if (node1.getNodeType() != AnyNode.TEXT) {
      return;
    }

    node1._value = node1.getStringValue() + node2.getStringValue();
    node2 = null;
  }

}
