/*
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2000-2002 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 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 end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Xerces" and "Apache Software Foundation" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    nor may "Apache" appear in their name, without prior written
 *    permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``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 THE APACHE SOFTWARE FOUNDATION 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation and was
 * originally based on software copyright (c) 1999, International
 * Business Machines, Inc., http://www.apache.org.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.apache.xerces.dom;

import java.util.Vector;

import org.w3c.dom.DOMException;
import org.w3c.dom.Node;


/**
 * AttributeMap inherits from NamedNodeMapImpl and extends it to deal with the
 * specifics of storing attributes. These are:
 * <ul>
 *  <li>managing ownership of attribute nodes
 *  <li>managing default attributes       
 *  <li>firing mutation events
 * </ul>
 * <p>
 * This class doesn't directly support mutation events, however, it notifies
 * the document when mutations are performed so that the document class do so.
 *
 * @version $Id: AttributeMap.java,v 1.28 2004/02/16 23:09:35 mrglavas Exp $
 */
public class AttributeMap extends NamedNodeMapImpl {
    
    /** Serialization version. */
    static final long serialVersionUID = 8872606282138665383L;

    //
    // Constructors
    //

    /** Constructs a named node map. */
    protected AttributeMap(ElementImpl ownerNode, NamedNodeMapImpl defaults) {
        super(ownerNode);
        if (defaults != null) {
            // initialize map with the defaults
            cloneContent(defaults);
            if (nodes != null) {
                hasDefaults(true);
            }
        }
    }

    /**
     * Adds an attribute using its nodeName attribute.
     * @see org.w3c.dom.NamedNodeMap#setNamedItem
     * @return If the new Node replaces an existing node the replaced Node is
     *      returned, otherwise null is returned. 
     * @param arg 
     *      An Attr node to store in this map.
     * @exception org.w3c.dom.DOMException The exception description.
     */
    public Node setNamedItem(Node arg)
        throws DOMException {

        if (isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
        }
        if(arg.getOwnerDocument() != ownerNode.ownerDocument()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "WRONG_DOCUMENT_ERR", null);
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, msg);
        }
        if (arg.getNodeType() != Node.ATTRIBUTE_NODE) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "HIERARCHY_REQUEST_ERR", null);
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, msg);
        }

        AttrImpl argn = (AttrImpl)arg;

        if (argn.isOwned()){
            if (argn.getOwnerElement() != ownerNode) {
                   String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INUSE_ATTRIBUTE_ERR", null);
                   throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR, msg);
            } 
            // replacing an Attribute with itself does nothing
            return arg;
        }

        // set owner
        argn.ownerNode = ownerNode;
        argn.isOwned(true);

    int i = findNamePoint(arg.getNodeName(),0);
        AttrImpl previous = null;
        if (i >= 0) {
            previous = (AttrImpl) nodes.elementAt(i);
            nodes.setElementAt(arg,i);
            previous.ownerNode = ownerNode.ownerDocument();
            previous.isOwned(false);
            // make sure it won't be mistaken with defaults in case it's reused
            previous.isSpecified(true);
        } else {
            i = -1 - i; // Insert point (may be end of list)
            if (null == nodes) {
                nodes = new Vector(5, 10);
            }
            nodes.insertElementAt(arg, i);
        }

        // notify document
        ownerNode.ownerDocument().setAttrNode(argn, previous);

        // If the new attribute is not normalized,
        // the owning element is inherently not normalized.
        if (!argn.isNormalized()) {
            ownerNode.isNormalized(false);
        }
        return previous;

    } // setNamedItem(Node):Node

    /**
     * Adds an attribute using its namespaceURI and localName.
     * @see org.w3c.dom.NamedNodeMap#setNamedItem
     * @return If the new Node replaces an existing node the replaced Node is
     *      returned, otherwise null is returned. 
     * @param arg A node to store in a named node map.
     */
    public Node setNamedItemNS(Node arg)
        throws DOMException {

        if (isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);            
        }
    
        if(arg.getOwnerDocument() != ownerNode.ownerDocument()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "WRONG_DOCUMENT_ERR", null);
            throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, msg);
        }

        if (arg.getNodeType() != Node.ATTRIBUTE_NODE) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "HIERARCHY_REQUEST_ERR", null);
            throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, msg);
        }
        AttrImpl argn = (AttrImpl)arg;

        if (argn.isOwned()){
            if (argn.getOwnerElement() != ownerNode) {
                String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "INUSE_ATTRIBUTE_ERR", null);
                throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR, msg);
            } 
            // replacing an Attribute with itself does nothing
            return arg;
        }

        // set owner
        argn.ownerNode = ownerNode;
        argn.isOwned(true);

        int i = findNamePoint(argn.getNamespaceURI(), argn.getLocalName());
        AttrImpl previous = null;
        if (i >= 0) {
            previous = (AttrImpl) nodes.elementAt(i);
            nodes.setElementAt(arg,i);
            previous.ownerNode = ownerNode.ownerDocument();
            previous.isOwned(false);
            // make sure it won't be mistaken with defaults in case it's reused
            previous.isSpecified(true);
        } else {
            // If we can't find by namespaceURI, localName, then we find by
            // nodeName so we know where to insert.
            i = findNamePoint(arg.getNodeName(),0);
            if (i >=0) {
                previous = (AttrImpl) nodes.elementAt(i);
                nodes.insertElementAt(arg,i);
            } else {
                i = -1 - i; // Insert point (may be end of list)
                if (null == nodes) {
                    nodes = new Vector(5, 10);
                }
                nodes.insertElementAt(arg, i);
            }
        }
        //    	changed(true);

        // notify document
        ownerNode.ownerDocument().setAttrNode(argn, previous);

        // If the new attribute is not normalized,
        // the owning element is inherently not normalized.
        if (!argn.isNormalized()) {
            ownerNode.isNormalized(false);
        }
        return previous;

    } // setNamedItemNS(Node):Node
   
    /**
     * Removes an attribute specified by name.
     * @param name
     *      The name of a node to remove. If the
     *      removed attribute is known to have a default value, an
     *      attribute immediately appears containing the default value
     *      as well as the corresponding namespace URI, local name,
     *      and prefix when applicable.
     * @return The node removed from the map if a node with such a name exists.
     * @throws              NOT_FOUND_ERR: Raised if there is no node named
     *                      name in the map.
     */
    /***/
    public Node removeNamedItem(String name)
        throws DOMException {
        return internalRemoveNamedItem(name, true);
    }

    /**
     * Same as removeNamedItem except that it simply returns null if the
     * specified name is not found.
     */
    Node safeRemoveNamedItem(String name) {
        return internalRemoveNamedItem(name, false);
    }


    /**
     * NON-DOM: Remove the node object
     * 
     * NOTE: Specifically removes THIS NODE -- not the node with this
     * name, nor the node with these contents. If node does not belong to
     * this named node map, we throw a DOMException.
     * 
     * @param item       The node to remove
     * @param addDefault true -- magically add default attribute
     * @return Removed node
     * @exception DOMException
     */
    protected Node removeItem(Node item, boolean addDefault)
        throws DOMException {

        int index = -1;
        if (nodes != null) {
            for (int i = 0; i < nodes.size(); i++) {
                if (nodes.elementAt(i) == item) {
                    index = i;
                    break;
                }
            }
        }
        if (index < 0) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NOT_FOUND_ERR", null);
            throw new DOMException(DOMException.NOT_FOUND_ERR, msg);
        }
        
        return remove((AttrImpl)item, index, addDefault);
    }

    /**
     * Internal removeNamedItem method allowing to specify whether an exception
     * must be thrown if the specified name is not found.
     */
    final protected Node internalRemoveNamedItem(String name, boolean raiseEx){
        if (isReadOnly()) {            
                String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
                throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);            
        }
        int i = findNamePoint(name,0);
        if (i < 0) {
            if (raiseEx) {
                String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NOT_FOUND_ERR", null);
                throw new DOMException(DOMException.NOT_FOUND_ERR, msg);
            } else {
                return null;
            }
        }

        return remove((AttrImpl)nodes.elementAt(i), i, true);

    } // internalRemoveNamedItem(String,boolean):Node

    private final Node remove(AttrImpl attr, int index,
                              boolean addDefault) {

        CoreDocumentImpl ownerDocument = ownerNode.ownerDocument();
        String name = attr.getNodeName();
        if (attr.isIdAttribute()) {
            ownerDocument.removeIdentifier(attr.getValue());
        }

        if (hasDefaults() && addDefault) {
            // If there's a default, add it instead
            NamedNodeMapImpl defaults =
                ((ElementImpl) ownerNode).getDefaultAttributes();

            Node d;
            if (defaults != null &&
                (d = defaults.getNamedItem(name)) != null &&
                findNamePoint(name, index+1) < 0) {
                    NodeImpl clone = (NodeImpl)d.cloneNode(true);
                    if (d.getLocalName() !=null){
                            // we must rely on the name to find a default attribute
                            // ("test:attr"), but while copying it from the DOCTYPE
                            // we should not loose namespace URI that was assigned
                            // to the attribute in the instance document.
                            ((AttrNSImpl)clone).namespaceURI = attr.getNamespaceURI();
                    } 
                    clone.ownerNode = ownerNode;
                    clone.isOwned(true);
                    clone.isSpecified(false);
                
                    nodes.setElementAt(clone, index);
                    if (attr.isIdAttribute()) {
                        ownerDocument.putIdentifier(clone.getNodeValue(),
                                                (ElementImpl)ownerNode);
                    }
            } else {
                nodes.removeElementAt(index);
            }
        } else {
            nodes.removeElementAt(index);
        }

        //        changed(true);

        // remove reference to owner
        attr.ownerNode = ownerDocument;
        attr.isOwned(false);

        // make sure it won't be mistaken with defaults in case it's
        // reused
        attr.isSpecified(true);
        attr.isIdAttribute(false);

        // notify document
        ownerDocument.removedAttrNode(attr, ownerNode, name);

        return attr;
    }
    
    /**
     * Introduced in DOM Level 2. <p>
     * Removes an attribute specified by local name and namespace URI.
     * @param namespaceURI
     *                      The namespace URI of the node to remove.
     *                      When it is null or an empty string, this
     *                      method behaves like removeNamedItem.
     * @param               The local name of the node to remove. If the
     *                      removed attribute is known to have a default
     *                      value, an attribute immediately appears
     *                      containing the default value.
     * @return Node         The node removed from the map if a node with such
     *                      a local name and namespace URI exists.
     * @throws              NOT_FOUND_ERR: Raised if there is no node named
     *                      name in the map.
     */
    public Node removeNamedItemNS(String namespaceURI, String name)
        throws DOMException {
        return internalRemoveNamedItemNS(namespaceURI, name, true);
    }

    /**
     * Same as removeNamedItem except that it simply returns null if the
     * specified local name and namespace URI is not found.
     */
    Node safeRemoveNamedItemNS(String namespaceURI, String name) {
        return internalRemoveNamedItemNS(namespaceURI, name, false);
    }

    /**
     * Internal removeNamedItemNS method allowing to specify whether an
     * exception must be thrown if the specified local name and namespace URI
     * is not found.
     */
    final protected Node internalRemoveNamedItemNS(String namespaceURI,
                                                   String name,
                                                   boolean raiseEx) {
        if (isReadOnly()) {
            String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
            throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);            
        }
        int i = findNamePoint(namespaceURI, name);
        if (i < 0) {
            if (raiseEx) {
                String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NOT_FOUND_ERR", null);
                throw new DOMException(DOMException.NOT_FOUND_ERR, msg);
            } else {
                return null;
            }
        }

        AttrImpl n = (AttrImpl)nodes.elementAt(i);
        CoreDocumentImpl ownerDocument = ownerNode.ownerDocument();

        if (n.isIdAttribute()) {
                ownerDocument.removeIdentifier(n.getValue());
        }
        // If there's a default, add it instead
        String nodeName = n.getNodeName();
        if (hasDefaults()) {
            NamedNodeMapImpl defaults = ((ElementImpl) ownerNode).getDefaultAttributes();
            Node d;
            if (defaults != null
                && (d = defaults.getNamedItem(nodeName)) != null)
                {
                    int j = findNamePoint(nodeName,0);
                    if (j>=0 && findNamePoint(nodeName, j+1) < 0) {
                        NodeImpl clone = (NodeImpl)d.cloneNode(true);
                        clone.ownerNode = ownerNode;
                        if (d.getLocalName() != null) {
                            // we must rely on the name to find a default attribute
                            // ("test:attr"), but while copying it from the DOCTYPE
                            // we should not loose namespace URI that was assigned
                            // to the attribute in the instance document.
                            ((AttrNSImpl)clone).namespaceURI = namespaceURI;
                        }
                        clone.isOwned(true);
                        clone.isSpecified(false);
                        nodes.setElementAt(clone, i);
                        if (clone.isIdAttribute()) {
                                ownerDocument.putIdentifier(clone.getNodeValue(), 
                                                            (ElementImpl)ownerNode);
                        }
                    } else {
                        nodes.removeElementAt(i);
                    }
                } else {
                    nodes.removeElementAt(i);
                }
        } else {
            nodes.removeElementAt(i);
        }

        //        changed(true);

        // remove reference to owner
        n.ownerNode = ownerDocument;
        n.isOwned(false);
        // make sure it won't be mistaken with defaults in case it's
        // reused
        n.isSpecified(true);
        // update id table if needed
        n.isIdAttribute(false);

        // notify document
        ownerDocument.removedAttrNode(n, ownerNode, name);

        return n;

    } // internalRemoveNamedItemNS(String,String,boolean):Node

    //
    // Public methods
    //

    /**
     * Cloning a NamedNodeMap is a DEEP OPERATION; it always clones
     * all the nodes contained in the map.
     */
     
    public NamedNodeMapImpl cloneMap(NodeImpl ownerNode) {
        AttributeMap newmap =
            new AttributeMap((ElementImpl) ownerNode, null);
        newmap.hasDefaults(hasDefaults());
        newmap.cloneContent(this);
        return newmap;
    } // cloneMap():AttributeMap

    /**
     * Override parent's method to set the ownerNode correctly
     */
    protected void cloneContent(NamedNodeMapImpl srcmap) {
        Vector srcnodes = srcmap.nodes;
        if (srcnodes != null) {
            int size = srcnodes.size();
            if (size != 0) {
                if (nodes == null) {
                    nodes = new Vector(size);
                }
                nodes.setSize(size);
                for (int i = 0; i < size; ++i) {
                    NodeImpl n = (NodeImpl) srcnodes.elementAt(i);
                    NodeImpl clone = (NodeImpl) n.cloneNode(true);
                    clone.isSpecified(n.isSpecified());
                    nodes.setElementAt(clone, i);
                    clone.ownerNode = ownerNode;
                    clone.isOwned(true);
                }
            }
        }
    } // cloneContent():AttributeMap


    /**
     * Move specified attributes from the given map to this one
     */
    void moveSpecifiedAttributes(AttributeMap srcmap) {
        int nsize = (srcmap.nodes != null) ? srcmap.nodes.size() : 0;
        for (int i = nsize - 1; i >= 0; i--) {
            AttrImpl attr = (AttrImpl) srcmap.nodes.elementAt(i);
            if (attr.isSpecified()) {
                srcmap.remove(attr, i, false);
                if (attr.getLocalName() != null) {
                    setNamedItem(attr);
                }
                else {
                    setNamedItemNS(attr);
                }
            }
        }
    } // moveSpecifiedAttributes(AttributeMap):void


    /**
     * Get this AttributeMap in sync with the given "defaults" map.
     * @param defaults The default attributes map to sync with.
     */
    protected void reconcileDefaults(NamedNodeMapImpl defaults) {

        // remove any existing default
        int nsize = (nodes != null) ? nodes.size() : 0;
        for (int i = nsize - 1; i >= 0; i--) {
            AttrImpl attr = (AttrImpl) nodes.elementAt(i);
            if (!attr.isSpecified()) {
                remove(attr, i, false);
            }
        }
        // add the new defaults
        if (defaults == null) {
            return;
        }
        if (nodes == null || nodes.size() == 0) {
            cloneContent(defaults);
        }
        else {
            int dsize = defaults.nodes.size();
            for (int n = 0; n < dsize; n++) {
                AttrImpl d = (AttrImpl) defaults.nodes.elementAt(n);
                int i = findNamePoint(d.getNodeName(), 0); 
                if (i < 0) {
            		i = -1 - i; 
                    NodeImpl clone = (NodeImpl) d.cloneNode(true);
                    clone.ownerNode = ownerNode;
                    clone.isOwned(true);
                    clone.isSpecified(false);
            		nodes.insertElementAt(clone, i);
                }
            }
        }

    } // reconcileDefaults()

} // class AttributeMap
