/*
 * @(#)IIOMetadataNode.java	1.35 01/12/03
 *
 * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package javax.imageio.metadata;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.DOMException;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

class IIODOMException extends DOMException {

    public IIODOMException(short code, String message) {
        super(code, message);
    }
}

class IIONamedNodeMap implements NamedNodeMap {

    List nodes;

    public IIONamedNodeMap(List nodes) {
        this.nodes = nodes;
    }

    public int getLength() {
        return nodes.size();
    }

    public Node getNamedItem(String name) {
        Iterator iter = nodes.iterator();
        while (iter.hasNext()) {
            Node node = (Node)iter.next();
            if (name.equals(node.getNodeName())) {
                return node;
            }
        }

        return null;
    }

    public Node item(int index) {
        Node node = (Node)nodes.get(index);
        return node;
    }

    public Node removeNamedItem(java.lang.String name) {
        throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
                               "This NamedNodeMap is read-only!");
    }

    public Node setNamedItem(Node arg) {
        throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
                               "This NamedNodeMap is read-only!");
    }

    /**
     * Equivalent to <code>getNamedItem(localName)</code>.
     */
    public Node getNamedItemNS(String namespaceURI, String localName) {
        return getNamedItem(localName);
    }

    /**
     * Equivalent to <code>setNamedItem(arg)</code>.
     */
    public Node setNamedItemNS(Node arg) {
        return setNamedItem(arg);
    }

    /**
     * Equivalent to <code>removeNamedItem(localName)</code>.
     */
    public Node removeNamedItemNS(String namespaceURI, String localName) {
        return removeNamedItem(localName);
    }
}

class IIONodeList implements NodeList {
    
    List nodes;

    public IIONodeList(List nodes) {
        this.nodes = nodes;
    }

    public int getLength() {
        return nodes.size();
    }
    
    public Node item(int index) {
        if (index < 0 || index > nodes.size()) {
            return null;
        }
        return (Node)nodes.get(index);
    }
}

class IIOAttr extends IIOMetadataNode implements Attr {

    boolean specified = true;

    Element owner;
    String name;
    String value;

    public IIOAttr(Element owner, String name, String value) {
        this.owner = owner;
        this.name = name;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public String getNodeName() {
        return name;
    }
    
    public short getNodeType() {
        return ATTRIBUTE_NODE;
    }

    public boolean getSpecified() {
        return specified;
    }

    public String getValue() {
        return value;
    }

    public String getNodeValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public void setNodeValue(String value) {
        this.value = value;
    }

    public Element getOwnerElement() {
        return owner;
    }
}

/**
 * A class representing a node in a meta-data tree, which implements
 * the <a
 * href="http://java.sun.com/xml/docs/api/org/w3c/dom/Element.html">
 * <code>org.w3c.dom.Element</code></a> interface and additionally allows
 * for the storage of non-textual objects via the
 * <code>getUserObject</code> and <code>setUserObject</code> methods.
 *
 * <p> This class is not intended to be used for general XML
 * processing. In particular, <code>Element</code> nodes created
 * within the Image I/O API are not compatible with those created by
 * Sun's standard implementation of the <code>org.w3.dom</code> API.
 * In particular, the implementation is tuned for simple uses and may
 * not perform well for intensive processing.
 *
 * <p> Namespaces are ignored in this implementation.  The terms "tag
 * name" and "node name" are always considered to be synonymous.
 *
 * @see IIOMetadata#getAsTree
 * @see IIOMetadata#setFromTree
 * @see IIOMetadata#mergeTree
 *
 * @version 0.5
 */
public class IIOMetadataNode implements Element, NodeList {

    /**
     * The name of the node as a <code>String</code>.
     */
    private String nodeName = null;

    /**
     * The value of the node as a <code>String</code>.  The Image I/O
     * API typically does not make use of the node value.
     */
    private String nodeValue = null;

    /**
     * The <code>Object</code> value associated with this node.
     */
    private Object userObject = null;

    /**
     * The parent node of this node, or <code>null</code> if this node
     * forms the root of its own tree.
     */
    private IIOMetadataNode parent = null;

    /**
     * The number of child nodes.
     */
    private int numChildren = 0;

    /**
     * The first (leftmost) child node of this node, or
     * <code>null</code> if this node is a leaf node.
     */
    private IIOMetadataNode firstChild = null;

    /**
     * The last (rightmost) child node of this node, or
     * <code>null</code> if this node is a leaf node.
     */
    private IIOMetadataNode lastChild = null;
    
    /**
     * The next (right) sibling node of this node, or
     * <code>null</code> if this node is its parent's last child node.
     */
    private IIOMetadataNode nextSibling = null;

    /**
     * The previous (left) sibling node of this node, or
     * <code>null</code> if this node is its parent's first child node.
     */
    private IIOMetadataNode previousSibling = null;

    /**
     * A <code>List</code> of <code>IIOAttr</code> nodes representing
     * attributes.
     */
    private List attributes = new ArrayList();

    /**
     * Constructs an empty <code>IIOMetadataNode</code>.
     */
    public IIOMetadataNode() {}

    /**
     * Constructs an <code>IIOMetadataNode</code> with a given node
     * name.
     *
     * @param nodeName the name of the node, as a <code>String</code>.
     */
    public IIOMetadataNode(String nodeName) {
        this.nodeName = nodeName;
    }

    /**
     * Check that the node is either <code>null</code> or an
     * <code>IIOMetadataNode</code>.
     */
    private void checkNode(Node node) throws DOMException {
        if (node == null) {
            return;
        }
        if (!(node instanceof IIOMetadataNode)) {
            throw new IIODOMException(DOMException.WRONG_DOCUMENT_ERR,
                                      "Node not an IIOMetadataNode!");
        }
    }

    // Methods from Node

    /**
     * Returns the node name associated with this node.
     *
     * @return the node name, as a <code>String</code>.
     */
    public String getNodeName() {
        return nodeName;
    }

    public String getNodeValue() throws DOMException {
        return nodeValue;
    }

    public void setNodeValue(String nodeValue) throws DOMException {
        this.nodeValue = nodeValue;
    }

    /**
     * Returns the node type, which is always
     * <code>ELEMENT_NODE</code>.
     *
     * @return the <code>short</code> value <code>ELEMENT_NODE</code>.
     */
    public short getNodeType() {
        return ELEMENT_NODE;
    }
    
    /**
     * Returns the parent of this node.  A <code>null</code> value
     * indicates that the node is the root of its own tree.  To add a
     * node to an existing tree, use one of the
     * <code>insertBefore</code>, <code>replaceChild</code>, or
     * <code>appendChild</code> methods.
     * 
     * @return the parent, as a <code>Node</code>.
     *
     * @see #insertBefore
     * @see #replaceChild
     * @see #appendChild
     */
    public Node getParentNode() {
        return parent;
    }

    public NodeList getChildNodes() {
        return this;
    }

    /**
     * Returns the first child of this node, or <code>null</code> if
     * the node has no children.
     *
     * @return the first child, as a <code>Node</code>, or
     * <code>null</code>
     */
    public Node getFirstChild() {
        return firstChild;
    }

    /**
     * Returns the last child of this node, or <code>null</code> if
     * the node has no children.
     *
     * @return the last child, as a <code>Node</code>, or
     * <code>null</code>.
     */
    public Node getLastChild() {
        return lastChild;
    }

    /**
     * Returns the previous sibling of this node, or <code>null</code>
     * if this node has no previous sibling.
     *
     * @return the previous sibling, as a <code>Node</code>, or
     * <code>null</code>.
     */
    public Node getPreviousSibling() {
        return previousSibling;
    }

    /**
     * Returns the next sibling of this node, or <code>null</code> if
     * the node has no next sibling.
     *
     * @return the next sibling, as a <code>Node</code>, or
     * <code>null</code>.
     */
    public Node getNextSibling() {
        return nextSibling;
    }

    public NamedNodeMap getAttributes() {
        return new IIONamedNodeMap(attributes);
    }

    /**
     * Returns <code>null</code>, since <code>IIOMetadataNode</code>s
     * do not belong to any <code>Document</code>.
     *
     * @return <code>null</code>.
     */
    public Document getOwnerDocument() {
        return null;
    }

    /**
     * Inserts the node <code>newChild</code> before the existing
     * child node <code>refChild</code>. If <code>refChild</code> is
     * <code>null</code>, insert <code>newChild</code> at the end of
     * the list of children.
     *
     * @param newChild the <code>Node</code> to insert.
     * @param refChild the reference <code>Node</code>.
     *
     * @return the node being inserted.
     *
     * @exception IllegalArgumentException if <code>newChild</code> is
     * <code>null</code>.
     */
    public Node insertBefore(Node newChild, 
                             Node refChild) {
        if (newChild == null) {
            throw new IllegalArgumentException("newChild == null!");
        }

        checkNode(newChild);
        checkNode(refChild);

        IIOMetadataNode newChildNode = (IIOMetadataNode)newChild;
        IIOMetadataNode refChildNode = (IIOMetadataNode)refChild;

        // Siblings, can be null.
        IIOMetadataNode previous = null;
        IIOMetadataNode next = null;

        if (refChild == null) {
            previous = this.lastChild;
            next = null;
            this.lastChild = newChildNode;
        } else {
            previous = refChildNode.previousSibling;
            next = refChildNode;
        }

        if (previous != null) {
            previous.nextSibling = newChildNode;
        }
        if (next != null) {
            next.previousSibling = newChildNode;
        }

        newChildNode.parent = this;
        newChildNode.previousSibling = previous;
        newChildNode.nextSibling = next;
        
        // N.B.: O.K. if refChild == null
        if (this.firstChild == refChildNode) {
            this.firstChild = newChildNode;
        }

        ++numChildren;
        return newChildNode;
    }

    /**
     * Replaces the child node <code>oldChild</code> with
     * <code>newChild</code> in the list of children, and returns the
     * <code>oldChild</code> node.
     *
     * @param newChild the <code>Node</code> to insert.
     * @param oldChild the <code>Node</code> to be replaced.
     *
     * @return the node replaced.
     *
     * @exception IllegalArgumentException if <code>newChild</code> is
     * <code>null</code>.
     */
    public Node replaceChild(Node newChild, 
                             Node oldChild) {
        if (newChild == null) {
            throw new IllegalArgumentException("newChild == null!");
        }
        
        checkNode(newChild);
        checkNode(oldChild);

        IIOMetadataNode newChildNode = (IIOMetadataNode)newChild;
        IIOMetadataNode oldChildNode = (IIOMetadataNode)oldChild;

        IIOMetadataNode previous = oldChildNode.previousSibling;
        IIOMetadataNode next = oldChildNode.nextSibling;

        if (previous != null) {
            previous.nextSibling = newChildNode;
        }
        if (next != null) {
            next.previousSibling = newChildNode;
        }

        newChildNode.parent = this;
        newChildNode.previousSibling = previous;
        newChildNode.nextSibling = next;

        if (firstChild == oldChildNode) {
            firstChild = newChildNode;
        }
        if (lastChild == oldChildNode) {
            lastChild = newChildNode;
        }

        oldChildNode.parent = null;
        oldChildNode.previousSibling = null;
        oldChildNode.nextSibling = null;

        return oldChildNode;
    }

    /**
     * Removes the child node indicated by <code>oldChild</code> from
     * the list of children, and returns it.
     *
     * @param oldChild the <code>Node</code> to be removed.
     *
     * @return the node removed.
     *
     * @exception IllegalArgumentException if <code>oldChild</code> is
     * <code>null</code>.
     */
    public Node removeChild(Node oldChild) {
        if (oldChild == null) {
            throw new IllegalArgumentException("oldChild == null!");
        }
        checkNode(oldChild);

        IIOMetadataNode oldChildNode = (IIOMetadataNode)oldChild;

        IIOMetadataNode previous = oldChildNode.previousSibling;
        IIOMetadataNode next = oldChildNode.nextSibling;

        if (previous != null) {
            previous.nextSibling = next;
        }
        if (next != null) {
            next.previousSibling = previous;
        }

        if (this.firstChild == oldChildNode) {
            this.firstChild = next;
        }
        if (this.lastChild == oldChildNode) {
            this.lastChild = previous;
        }

        oldChildNode.parent = null;
        oldChildNode.previousSibling = null;
        oldChildNode.nextSibling = null;

        --numChildren;
        return oldChildNode;
    }

    /**
     * Adds the node <code>newChild</code> to the end of the list of
     * children of this node.
     *
     * @param newChild the <code>Node</code> to insert.
     *
     * @return the node added.
     *
     * @exception IllegalArgumentException if <code>newChild</code> is
     * <code>null</code>.
     */
    public Node appendChild(Node newChild) {
        if (newChild == null) {
            throw new IllegalArgumentException("newChild == null!");
        }
        checkNode(newChild);

        // insertBefore will increment numChildren
        return insertBefore(newChild, null);
    }

    /**
     * Returns <code>true</code> if this node has child nodes.
     *
     * @return <code>true</code> if this node has children.
     */
    public boolean hasChildNodes() {
        return numChildren > 0;
    }

    /**
     * Returns a duplicate of this node.  The duplicate node has no
     * parent (<code>getParentNode</code> returns <code>null</code>).
     * If a shallow clone is being performed (<code>deep</code> is
     * <code>false</code>), the new node will not have any children or
     * siblings.  If a deep clone is being performed, the new node
     * will form the root of a complete cloned subtree.
     *
     * @param deep if <code>true</code>, recursively clone the subtree
     * under the specified node; if <code>false</code>, clone only the
     * node itself.
     *
     * @return the duplicate node.
     */
    public Node cloneNode(boolean deep) {
        IIOMetadataNode newNode = new IIOMetadataNode(this.nodeName);
        newNode.setUserObject(getUserObject());
        // Attributes

        if (deep) {
            for (IIOMetadataNode child = firstChild;
                 child != null;
                 child = child.nextSibling) {
                newNode.appendChild(child.cloneNode(true));
            }
        }
        
        return newNode;
    }

    /**
     * Does nothing, since <code>IIOMetadataNode</code>s do not
     * contain <code>Text</code> children.
     */
    public void normalize() {
    }

    /**
     * Returns <code>false</code> since DOM features are not
     * supported.
     *
     * @return <code>false</code>.
     *
     * @param feature a <code>String</code>, which is ignored.
     * @param version a <code>String</code>, which is ignored.
     */
    public boolean isSupported(String feature, String version) {
        return false;
    }

    /**
     * Returns <code>null</code>, since namespaces are not supported.
     */
    public String getNamespaceURI() throws DOMException {
        return null;
    }

    /**
     * Returns <code>null</code>, since namespaces are not supported.
     *
     * @return <code>null</code>.
     *
     * @see #setPrefix
     */
    public String getPrefix() {
        return null;
    }

    /** 
     * Does nothing, since namespaces are not supported.
     *
     * @param prefix a <code>String</code>, which is ignored.
     *
     * @see #getPrefix
     */
    public void setPrefix(String prefix) {
    }

    /**
     * Equivalent to <code>getNodeName</code>.
     *
     * @return the node name, as a <code>String</code>.
     */
    public String getLocalName() {
        return nodeName;
    }

    // Methods from Element

    public String getTagName() {
        return nodeName;
    }
    
    public String getAttribute(String name) {
        Attr attr = getAttributeNode(name);
        if (attr == null) {
            return "";
        }
        return attr.getValue();
    }

    /**
     * Equivalent to <code>getAttribute(localName)</code>.
     *
     * @see #setAttributeNS
     */
    public String getAttributeNS(String namespaceURI, String localName) {
        return getAttribute(localName);
    }

    public void setAttribute(String name, String value) {
        // Note minor dependency on Crimson package
        // Steal the code if Crimson ever goes away
        if (!org.apache.crimson.util.XmlNames.isName(name)) {
            throw new IIODOMException(DOMException.INVALID_CHARACTER_ERR,
                                      "Attribute name is illegal!");
        }
        removeAttribute(name, false);
        attributes.add(new IIOAttr(this, name, value));
    }

    /**
     * Equivalent to <code>setAttribute(qualifiedName, value)</code>.
     *
     * @see #getAttributeNS
     */
    public void setAttributeNS(String namespaceURI,
                               String qualifiedName, String value) {
        setAttribute(qualifiedName, value);
    }

    public void removeAttribute(String name) {
        removeAttribute(name, true);
    }

    private void removeAttribute(String name, boolean checkPresent) {
        int numAttributes = attributes.size();
        for (int i = 0; i < numAttributes; i++) {
            Attr attr = (Attr)attributes.get(i);
            if (name.equals(attr.getName())) {
                attributes.remove(i);
                return;
            }
        }

        // If we get here, the attribute doesn't exist
        if (checkPresent) {
            throw new IIODOMException(DOMException.NOT_FOUND_ERR,
                                      "No such attribute!");
        }
    }
    
    /**
     * Equivalent to <code>removeAttribute(localName)</code>.
     */
    public void removeAttributeNS(String namespaceURI,
                                  String localName) {
        removeAttribute(localName);
    }

    public Attr getAttributeNode(String name) {
        Node node = getAttributes().getNamedItem(name);
        return (Attr)node;
    }

    /**
     * Equivalent to <code>getAttributeNode(localName)</code>.
     *
     * @see #setAttributeNodeNS
     */ 
   public Attr getAttributeNodeNS(String namespaceURI,
                                   String localName) {
        return getAttributeNode(localName);
    }

    public Attr setAttributeNode(Attr newAttr) throws DOMException {
        IIOAttr attr;
        if (newAttr instanceof IIOAttr) {
            attr = (IIOAttr)newAttr;
        } else {
            attr = new IIOAttr(newAttr.getOwnerElement(),
                               newAttr.getName(),
                               newAttr.getValue());
        }
        attributes.add(attr);
        return attr;
    }
    
    /**
     * Equivalent to <code>setAttributeNode(newAttr)</code>.
     *
     * @see #getAttributeNodeNS
     */
    public Attr setAttributeNodeNS(Attr newAttr) {
        return setAttributeNode(newAttr);
    }

    public Attr removeAttributeNode(Attr oldAttr) {
        removeAttribute(oldAttr.getName());
        return oldAttr;
    }
     
    public NodeList getElementsByTagName(String name) {
        List l = new ArrayList();
        getElementsByTagName(name, l);
        return new IIONodeList(l);
    }

    private void getElementsByTagName(String name, List l) {
        if (nodeName.equals(name)) {
            l.add(this);
        }

        Node child = getFirstChild();
        while (child != null) {
            ((IIOMetadataNode)child).getElementsByTagName(name, l);
            child = child.getNextSibling();
        }
    }
    
    /**
     * Equivalent to <code>getElementsByTagName(localName)</code>.
     */
    public NodeList getElementsByTagNameNS(String namespaceURI,
                                           String localName) {
        return getElementsByTagName(localName);
    }

    public boolean hasAttributes() {
	return attributes.size() > 0;
    }

    public boolean hasAttribute(String name) {
        return getAttributeNode(name) != null;
    }

    /**
     * Equivalent to <code>hasAttribute(localName)</code>.
     */
    public boolean hasAttributeNS(String namespaceURI,
                                  String localName) {
        return hasAttribute(localName);
    }

    // Methods from NodeList

    public int getLength() {
        return numChildren;
    }

    public Node item(int index) {
        if (index < 0) {
            return null;
        }

        Node child = getFirstChild();
        while (child != null && index-- > 0) {
            child = child.getNextSibling();
        }
        return child;
    }

    /**
     * Returns the <code>Object</code> value associated with this node.
     *
     * @return the user <code>Object</code>.
     *
     * @see #setUserObject
     */
    public Object getUserObject() {
        return userObject;
    }

    /**
     * Sets the value associated with this node.
     *
     * @param userObject the user <code>Object</code>.
     *
     * @see #getUserObject
     */
    public void setUserObject(Object userObject) {
        this.userObject = userObject;
    }
}
