/*
 * $Id: Element.java,v 1.49 2010/09/29 20:04:51 agoubard Exp $
 *
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.common.xml;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.xins.common.Log;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.collections.ProtectedList;
import org.xins.common.text.ParseException;

/**
 * XML Element.
 *
 * <p>Note that this class is not thread-safe. It should not be used from
 * different threads at the same time. This applies even to read operations.
 *
 * <p>Note that the namespace URIs and local names are not checked for
 * validity in this class.
 *
 * <p>Instances of this class cannot be created directly, using a constructor.
 * Instead, use {@link ElementBuilder} to build an XML element, or
 * {@link ElementParser} to parse an XML string.
 *
 * @version $Revision: 1.49 $ $Date: 2010/09/29 20:04:51 $
 * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
 * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
 *
 * @since XINS 1.1.0
 * @see org.w3c.dom.Element
 * @deprecated since XINS 3.0. Use {@link org.w3c.dom.Element}
 */
@Deprecated
public class Element implements Cloneable {

   /**
    * Fully-qualified name of this class.
    */
   private static final String CLASSNAME = Element.class.getName();

   /**
    * The secret key to use to add child elements.
    */
   private static final Object SECRET_KEY = new Object();

   /**
    * The namespace prefix. This field can be <code>null</code>, but it can never
    * be an empty string.
    */
   private String _namespacePrefix;

   /**
    * The namespace URI. This field can be <code>null</code>, but it can never
    * be an empty string.
    */
   private String _namespaceURI;

   /**
    * The local name. This field is never <code>null</code>.
    */
   private String _localName;

   /**
    * The child elements. This field is lazily initialized is initially
    * <code>null</code>.
    */
   private ArrayList<Object> _children;

   /**
    * The attributes. This field is lazily initialized and is initially
    * <code>null</code>.
    */
   private LinkedHashMap<QualifiedName,String> _attributes;

   /**
    * The character content for this element. Can be <code>null</code>.
    */
   private String _text;

   /**
    * Creates a new <code>Element</code> with no namespace.
    *
    * @param localName
    *    the local name of the element, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public Element(String localName)
   throws IllegalArgumentException {
      this(null, null, localName);
   }

   /**
    * Creates a new <code>Element</code>.
    *
    * @param namespaceURI
    *    the namespace URI for the element, can be <code>null</code>; an empty
    *    string is equivalent to <code>null</code>.
    *
    * @param localName
    *    the local name of the element, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public Element(String namespaceURI, String localName)
   throws IllegalArgumentException {
      this(null, namespaceURI, localName);
   }

   /**
    * Creates a new <code>Element</code>.
    *
    * @param namespacePrefix
    *    the namespace prefix for the element, can be <code>null</code>; an empty
    *    string is equivalent to <code>null</code>.
    *
    * @param namespaceURI
    *    the namespace URI for the element, can be <code>null</code>; an empty
    *    string is equivalent to <code>null</code>.
    *
    * @param localName
    *    the local name of the element, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.1
    */
   public Element(String namespacePrefix, String namespaceURI, String localName)
   throws IllegalArgumentException {

      // Check preconditions
      MandatoryArgumentChecker.check("localName", localName);

      // An empty namespace prefix is equivalent to null
      if (namespacePrefix != null && namespacePrefix.length() < 1) {
         _namespacePrefix = null;
      } else {
         _namespacePrefix = namespacePrefix;
      }

      // An empty namespace URI is equivalent to null
      if (namespaceURI != null && namespaceURI.length() < 1) {
         _namespaceURI = null;
      } else {
         _namespaceURI = namespaceURI;
      }

      _localName = localName;
   }

   /**
    * Sets the namespace prefix.
    *
    * @param namespacePrefix
    *    the namespace prefix of this element, can be <code>null</code>.
    *    If the value if <code>null</code> or an empty string, the element is
    *    consider to have no namespace prefix.
    *
    * @since XINS 2.1.
    */
   public void setNamespacePrefix(String namespacePrefix) {
      if (namespacePrefix != null && namespacePrefix.length() == 0) {
         _namespacePrefix = null;
      } else {
         _namespacePrefix = namespacePrefix;
      }
   }

   /**
    * Gets the namespace prefix.
    *
    * @return
    *    the namespace prefix for this element, or <code>null</code> if there is
    *    none, but never an empty string.
    *
    * @since XINS 2.1
    */
   public String getNamespacePrefix() {
      return _namespacePrefix;
   }

   /**
    * Sets the namespace URI.
    *
    * @param namespaceURI
    *    the namespace URI of this element, can be <code>null</code>.
    *    If the value if <code>null</code> or an empty string, the element is
    *    consider to have no namespace URI.
    *
    * @since XINS 2.1.
    */
   public void setNamespaceURI(String namespaceURI) {
      if (namespaceURI != null && namespaceURI.length() == 0) {
         _namespaceURI = null;
      } else {
         _namespaceURI = namespaceURI;
      }
   }

   /**
    * Gets the namespace URI.
    *
    * @return
    *    the namespace URI for this element, or <code>null</code> if there is
    *    none, but never an empty string.
    */
   public String getNamespaceURI() {
      return _namespaceURI;
   }

   /**
    * Gets the local name.
    *
    * @return
    *    the local name of this element, cannot be <code>null</code>.
    */
   public String getLocalName() {
      return _localName;
   }

   /**
    * Sets the local name.
    *
    * @param localName
    *    the local name of this element, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public void setLocalName(String localName) throws IllegalArgumentException {
      MandatoryArgumentChecker.check("localName", localName);
      _localName = localName;
   }

   /**
    * Sets the specified attribute. If the value for the specified
    * attribute is already set, then the previous value is replaced.
    *
    * @param localName
    *    the local name for the attribute, cannot be <code>null</code>.
    *
    * @param value
    *    the value for the attribute, can be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public void setAttribute(String localName, String value)
   throws IllegalArgumentException {
      setAttribute(null, null, localName, value);
   }

   /**
    * Sets the specified attribute. If the value for the specified
    * attribute is already set, then the previous value is replaced.
    *
    * @param namespaceURI
    *    the namespace URI for the attribute, can be <code>null</code>; an
    *    empty string is equivalent to <code>null</code>.
    *
    * @param localName
    *    the local name for the attribute, cannot be <code>null</code>.
    *
    * @param value
    *    the value for the attribute, can be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public void setAttribute(String namespaceURI, String localName, String value)
   throws IllegalArgumentException {
      setAttribute(null, namespaceURI, localName, value);
   }

   /**
    * Sets the specified attribute. If the value for the specified
    * attribute is already set, then the previous value is replaced.
    *
    * @param namespacePrefix
    *    the namespace prefix for the attribute, can be <code>null</code>; an
    *    empty string is equivalent to <code>null</code>.
    *
    * @param namespaceURI
    *    the namespace URI for the attribute, can be <code>null</code>; an
    *    empty string is equivalent to <code>null</code>.
    *
    * @param localName
    *    the local name for the attribute, cannot be <code>null</code>.
    *
    * @param value
    *    the value for the attribute, can be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.1
    */
   public void setAttribute(String namespacePrefix, String namespaceURI, String localName, String value)
   throws IllegalArgumentException {

      // Construct a QualifiedName object. This will check the preconditions.
      QualifiedName qn = new QualifiedName(namespacePrefix, namespaceURI, localName);

      if (_attributes == null) {
         if (value == null) {
            return;
         }

         // Lazily initialize
         _attributes = new LinkedHashMap<QualifiedName,String>();
      }

      // Set or reset the attribute
      if (value == null) {
         _attributes.remove(qn);
      } else {
         _attributes.put(qn, value);
      }
   }

   /**
    * Removes the specified attribute. If no attribute with the specified name
    * exists, nothing happens.
    *
    * @param localName
    *    the local name for the attribute, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public void removeAttribute(String localName)
   throws IllegalArgumentException {
      removeAttribute(null, localName);
   }

   /**
    * Removes the specified attribute. If no attribute with the specified name
    * exists, nothing happens.
    *
    * @param namespaceURI
    *    the namespace URI for the attribute, can be <code>null</code>; an
    *    empty string is equivalent to <code>null</code>.
    *
    * @param localName
    *    the local name for the attribute, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    *
    * @since XINS 2.0
    */
   public void removeAttribute(String namespaceURI, String localName)
   throws IllegalArgumentException {

      // Construct a QualifiedName object. This will check the preconditions.
      QualifiedName qn = new QualifiedName(namespaceURI, localName);

      if (_attributes != null) {
         _attributes.remove(qn);
      }
   }

   /**
    * Gets the attributes of this element.
    *
    * @return
    *    a {@link Map} (never <code>null</code>) which contains the attributes;
    *    each key in the <code>Map</code> is a {@link QualifiedName} instance
    *    (not <code>null</code>) and each value in it is a <code>String</code>
    *    instance (not <code>null</code>).
    */
   public Map getAttributeMap() {
      if (_attributes == null) {
         _attributes = new LinkedHashMap<QualifiedName,String>();
      }
      return _attributes;
   }

   /**
    * Gets the value of the attribute with the qualified name. If the
    * qualified name does not specify a namespace, then only an attribute that
    * does not have a namespace will match.
    *
    * @param qn
    *    a combination of an optional namespace and a mandatory local name, or
    *    <code>null</code>.
    *
    * @return
    *    the value of the attribute that matches the specified namespace and
    *    local name, or <code>null</code> if such an attribute is either not
    *    set or set to <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>qn == null</code>.
    */
   public String getAttribute(QualifiedName qn)
   throws IllegalArgumentException {

      // Check preconditions
      MandatoryArgumentChecker.check("qn", qn);

      if (_attributes == null) {
         return null;
      } else {
         return (String) _attributes.get(qn);
      }
   }

   /**
    * Gets the value of the attribute with the specified namespace and local
    * name. The namespace is optional. If the namespace is not given, then only
    * an attribute that does not have a namespace will match.
    *
    * @param namespaceURI
    *    the namespace URI for the attribute, can be <code>null</code>; an
    *    empty string is equivalent to <code>null</code>; if specified this
    *    string must be a valid namespace URI.
    *
    * @param localName
    *    the local name of the attribute, cannot be <code>null</code>.
    *
    * @return
    *    the value of the attribute that matches the specified namespace and
    *    local name, or <code>null</code> if such an attribute is either not
    *    set or set to <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    */
   public String getAttribute(String namespaceURI, String localName)
   throws IllegalArgumentException {
      QualifiedName qn = new QualifiedName(namespaceURI, localName);
      return getAttribute(qn);
   }

   /**
    * Gets the value of an attribute that has no namespace.
    *
    * @param localName
    *    the local name of the attribute, cannot be <code>null</code>.
    *
    * @return
    *    the value of the attribute that matches the specified local name and
    *    has no namespace defined, or <code>null</code> if the attribute is
    *    either not set or set to <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>localName == null</code>.
    */
   public String getAttribute(String localName)
   throws IllegalArgumentException {
      return getAttribute(null, localName);
   }

   /**
    * Adds a new child element.
    *
    * @param child
    *    the new child to add to this element, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>child == null || child == <em>this</em></code>.
    *
    * @since XINS 2.0.
    */
   public void addChild(Element child) throws IllegalArgumentException {

      final String METHODNAME = "addChild(Element)";

      // Check preconditions
      MandatoryArgumentChecker.check("child", child);
      if (child == this) {
         String message = "child == this";
         Log.log_1050(CLASSNAME, METHODNAME, Utils.getCallingClass(), Utils.getCallingMethod(), message);
         throw new IllegalArgumentException(message);
      }

      // Lazily initialize
      if (_children == null) {
         _children = new ArrayList<Object>();
      }

      _children.add(child);
   }


   /**
    * Removes a child element. If the child is not found, nothing is removed.
    *
    * @param child
    *    the child to be removed to this element, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>child == null || child == <em>this</em></code>.
    *
    * @since XINS 2.0
    */
   public void removeChild(Element child) throws IllegalArgumentException {

      final String METHODNAME = "removeChild(Element)";

      // Check preconditions
      MandatoryArgumentChecker.check("child", child);
      if (child == this) {
         String message = "child == this";
         Log.log_1050(CLASSNAME, METHODNAME, Utils.getCallingClass(), Utils.getCallingMethod(), message);
         throw new IllegalArgumentException(message);
      }

      // Lazily initialize
      if (_children == null) {
         return;
      }

      _children.remove(child);
   }

   /**
    * Gets the list of all child elements.
    *
    * @return
    *    the {@link List} containing all child elements; each
    *    element in the list is another <code>Element</code> instance;
    *    never <code>null</code>.
    */
   public List getChildElements() {

      if (_children == null) {
         _children = new ArrayList();
      }

      return _children;
   }

   /**
    * Gets the list of child elements that match the specified name.
    *
    * @param name
    *    the name for the child elements to match, cannot be
    *    <code>null</code>.
    *
    * @return
    *    a {@link List} containing each child element that matches the
    *    specified name as another <code>Element</code> instance;
    *    never <code>null</code>. The list cannot be modified.
    *
    * @throws IllegalArgumentException
    *    if <code>name == null</code>.
    */
   public List getChildElements(String name)
   throws IllegalArgumentException {

      // TODO: Support namespaces

      // Check preconditions
      MandatoryArgumentChecker.check("name", name);

      // If there are no children, then return null
      if (_children == null || _children.size() == 0) {
         return Collections.EMPTY_LIST;

      // There are children, find all matching ones
      } else {
         ProtectedList matches = new ProtectedList(SECRET_KEY);
         Iterator it = _children.iterator();
         while (it.hasNext()) {
            Element child = (Element) it.next();
            if (name.equals(child.getLocalName())) {
               matches.add(SECRET_KEY, child);
            }
         }

         // If there are no matching children, then return null
         if (matches.size() == 0) {
            return Collections.EMPTY_LIST;

         // Otherwise return an immutable list with all matches
         } else {
            return matches;
         }
      }
   }

   /**
    * Sets the character content. The existing character content, if any, is
    * replaced
    *
    * @param text
    *    the character content for this element, or <code>null</code>.
    *
    * @since XINS 2.0.
    */
   public void setText(String text) {
      _text = text;
   }

   /**
    * Gets the character content, if any.
    *
    * @return
    *    the character content of this element, or <code>null</code> if no
    *    text has been specified for this element.
    */
   public String getText() {
      return _text;
   }

   /**
    * Gets the unique child of this element with the specified name.
    *
    * @param elementName
    *    the name of the child element to get, or <code>null</code> if the
    *    element name is irrelevant.
    *
    * @return
    *    the sub-element of this element, never <code>null</code>.
    *
    * @throws ParseException
    *    if no child with the specified name was found,
    *    or if more than one child with the specified name was found.
    *
    * @since XINS 1.4.0
    */
   public Element getUniqueChildElement(String elementName)
   throws ParseException {

      List childList;
      if (elementName == null) {
         childList = getChildElements();
      } else {
         childList = getChildElements(elementName);
      }

      if (childList.size() == 0) {
         throw new ParseException("No \"" + elementName +
               "\" children found in the \"" + getLocalName() +
               "\" element.");
      } else if (childList.size() > 1) {
         throw new ParseException("More than one \"" + elementName +
               "\" children found in the \"" + getLocalName() +
               "\" element.");
      }
      return (Element) childList.get(0);
   }

   /**
    * Gets the unique child of this element.
    *
    * @return
    *    the sub-element of this element, never <code>null</code>.
    *
    * @throws ParseException
    *    if no child was found or more than one child was found.
    *
    * @since XINS 2.2
    */
   public Element getUniqueChildElement() throws ParseException {
      return getUniqueChildElement(null);
   }

   public int hashCode() {
      int hashCode = _localName.hashCode();
      if (_namespaceURI != null) {
         hashCode += _namespaceURI.hashCode();
      }
      if (_attributes != null) {
         hashCode += _attributes.hashCode();
      }
      if (_children != null) {
         hashCode += _children.hashCode();
      }
      if (_text != null) {
         hashCode += _text.hashCode();
      }
      return hashCode;
   }

   public boolean equals(Object obj) {
      if (obj == null || !(obj instanceof Element)) {
         return false;
      }
      Element other = (Element) obj;
      if (!_localName.equals(other.getLocalName()) ||
            (_namespaceURI != null && !_namespaceURI.equals(other._namespaceURI)) ||
            (_namespaceURI == null && other._namespaceURI != null)) {
         return false;
      }
      if ((_attributes != null && !_attributes.equals(other._attributes)) ||
            (_attributes == null && other._attributes != null)) {
         return false;
      }
      // XXX This should be changed as the order of the children should no matter
      if ((_children != null && !_children.equals(other._children)) ||
            (_children == null && other._children != null)) {
         return false;
      }
      if ((_text != null && !_text.equals(other._text)) ||
            (_text == null && other._text != null)) {
         return false;
      }
      return true;
   }

   /**
    * Clones this object. The clone will have the same namespace URI and local
    * name and equivalent attributes, children and character content.
    *
    * @return
    *    a new clone of this object, never <code>null</code>.
    */
   public Object clone() {

      // Construct a new Element, copy all field values (shallow copy)
      Element clone;
      try {
         clone = (Element) super.clone();
      } catch (CloneNotSupportedException exception) {
         throw Utils.logProgrammingError(exception);
      }

      // Deep copy the children
      if (_children != null) {
         clone._children = (ArrayList<Object>) _children.clone();
      }

      // Deep copy the attributes
      if (_attributes != null) {
         clone._attributes = (LinkedHashMap<QualifiedName,String>) _attributes.clone();
      }

      return clone;
   }

   /**
    * Overrides the {@link Object#toString()} method to return
    * the element as its XML representation.
    *
    * @return
    *    the XML representation of this element without the XML declaration,
    *    never <code>null</code>.
    */
   @Override
   public String toString() {
      return new ElementSerializer().serialize(this);
   }

   /**
    * Qualified name for an element or attribute. This is a combination of an
    * optional namespace URI and a mandatory local name.
    *
    * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
    *
    * @since XINS 1.1.0
    */
   public static final class QualifiedName {

      /**
       * The hash code for this object.
       */
      private final int _hashCode;

      /**
       * The namespace prefix. Can be <code>null</code>.
       */
      private final String _namespacePrefix;

      /**
       * The namespace URI. Can be <code>null</code>.
       */
      private final String _namespaceURI;

      /**
       * The local name. Cannot be <code>null</code>.
       */
      private final String _localName;

      /**
       * Constructs a new <code>QualifiedName</code> with the specified
       * namespace and local name.
       *
       * @param namespaceURI
       *    the namespace URI for the element, can be <code>null</code>; an
       *    empty string is equivalent to <code>null</code>.
       *
       * @param localName
       *    the local name of the element, cannot be <code>null</code>.
       *
       * @throws IllegalArgumentException
       *    if <code>localName == null</code>.
       */
      public QualifiedName(String namespaceURI, String localName)
      throws IllegalArgumentException {
         this(null, namespaceURI, localName);
      }

      /**
       * Constructs a new <code>QualifiedName</code> with the specified
       * namespace and local name.
       *
       * @param namespacePrefix
       *    the namespace prefix for the element, can be <code>null</code>; an
       *    empty string is equivalent to <code>null</code>.
       *
       * @param namespaceURI
       *    the namespace URI for the element, can be <code>null</code>; an
       *    empty string is equivalent to <code>null</code>.
       *
       * @param localName
       *    the local name of the element, cannot be <code>null</code>.
       *
       * @throws IllegalArgumentException
       *    if <code>localName == null</code>.
       *
       * @since XINS 2.1
       */
      public QualifiedName(String namespacePrefix, String namespaceURI, String localName)
      throws IllegalArgumentException {

         // Check preconditions
         MandatoryArgumentChecker.check("localName", localName);

         // An empty namespace prefix is equivalent to null
         if (namespacePrefix != null && namespacePrefix.length() < 1) {
            _namespacePrefix = null;
         } else {
            _namespacePrefix = namespacePrefix;
         }

         // An empty namespace URI is equivalent to null
         if (namespaceURI != null && namespaceURI.length() < 1) {
            _namespaceURI = null;
         } else {
            _namespaceURI = namespaceURI;
         }

         // Initialize fields
         _hashCode     = localName.hashCode();
         _localName    = localName;
      }

      /**
       * Returns the hash code value for this object.
       *
       * @return
       *    the hash code value.
       */
      public int hashCode() {
         return _hashCode;
      }

      /**
       * Compares this object with the specified object for equality.
       *
       * @param obj
       *    the object to compare with, or <code>null</code>.
       *
       * @return
       *    <code>true</code> if this object and the argument are considered
       *    equal, <code>false</code> otherwise.
       */
      public boolean equals(Object obj) {

         if (! (obj instanceof QualifiedName)) {
            return false;
         }

         QualifiedName qn = (QualifiedName) obj;
         return ((_namespaceURI == null && qn._namespaceURI == null) ||
               (_namespaceURI != null && _namespaceURI.equals(qn._namespaceURI)))
            &&  _localName.equals(qn._localName);
      }

      /**
       * Gets the namespace prefix.
       *
       * @return
       *    the namespace prefix, can be <code>null</code>.
       *
       * @since XINS 2.1
       */
      public String getNamespacePrefix() {
         return _namespacePrefix;
      }

      /**
       * Gets the namespace URI.
       *
       * @return
       *    the namespace URI, can be <code>null</code>.
       */
      public String getNamespaceURI() {
         return _namespaceURI;
      }

      /**
       * Gets the local name.
       *
       * @return
       *    the local name, never <code>null</code>.
       */
      public String getLocalName() {
         return _localName;
      }
   }
}