/*
 * $Id: ParentNode.java,v 1.4 2001/04/30 21:47:57 edwingo Exp $
 *
 * The Apache Software License, Version 1.1
 *
 *
 * Copyright (c) 2000 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 "Crimson" 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, Sun Microsystems, Inc., 
 * http://www.sun.com.  For more information on the Apache Software 
 * Foundation, please see <http://www.apache.org/>.
 */

package org.apache.crimson.tree;


import java.io.IOException;
import java.io.Writer;

import org.xml.sax.SAXException;

import org.w3c.dom.*;


/**
 * This adds an implementation of "parent of" relationships to the NodeBase
 * class.  It implements operations for maintaining a set of children,
 * providing indexed access to them, and writing them them out as text.
 *
 * <P> The NodeList it implements to describe its children is "live", as
 * required by DOM.  That means that indexed accesses by applications must
 * handle cases associated with unstable indices and lengths.  Indices
 * should not be stored if they can be invalidated by changes, including
 * changes made by other threads.
 *
 * @author David Brownell
 * @version $Revision: 1.4 $
 */
// not public ... javadoc looks a bit odd (hidden base class)
// but it's only subclassable within this package anyway
abstract class ParentNode extends NodeBase
{
    private NodeBase		children [];
    private int			length;

    /**
     * Builds a ParentNode, which can have children that are
     * subclasses of NodeBase.
     */
    // package private
    ParentNode () { }

    /**
     * Called to minimize space utilization.  Affects only
     * this node; children must be individually trimmed.
     */
    public void trimToSize ()
    {
	if (length == 0)
	    children = null;
        else if (children.length != length) {
	    NodeBase	temp [] = new NodeBase [length];

            System.arraycopy (children, 0, temp, 0, length);
	    children = temp;
	}
    }

    // package private
    void reduceWaste ()
    {
	if (children == null)
	    return;

	//
	// Arbitrary -- rather than paying trimToSize() costs
	// on most elements, we routinely accept some waste but
	// do try to reduce egregious waste.  Interacts with
	// the array allocation done in appendChild.
	//
	if ((children.length - length) > 6)
            trimToSize ();
    }


    /**
     * Writes each of the child nodes.  For element nodes, this adds
     * whitespace to indent non-text children (it prettyprints) unless
     * the <em>xml:space='preserve'</em> attribute applies, or the
     * write context disables prettyprinting.
     *
     * @param context describes how the children should be printed
     */
    public void writeChildrenXml (XmlWriteContext context) throws IOException
    {
	if (children == null)
	    return;

	int	oldIndent = 0;
	boolean	preserve = true;
	boolean	pureText = true;

	if (getNodeType () == ELEMENT_NODE) {
	    preserve = "preserve".equals (
		    getInheritedAttribute ("xml:space"));
	    oldIndent = context.getIndentLevel ();
	}

	try {
	    if (!preserve)
		context.setIndentLevel (oldIndent + 2);
	    for (int i = 0; i < length; i++) {
		if (!preserve && children [i].getNodeType () != TEXT_NODE) {
		    context.printIndent ();
		    pureText = false;
		}
        
        // BEGIN ADDITIONS.
        // If we're not preserving whitespace, and the current node 
        // is a TEXT node containing whitespace and nothing else, skip it.
        // if ( !preserve && (children [i].getNodeType() == TEXT_NODE) && context.isIndent(children [i].getNodeValue()) )
        if ( !preserve && (children [i].getNodeType() == TEXT_NODE) && isWhitespace(children [i].getNodeValue()) )
        {
            // Normalize whitespace to one space character?
            Writer	out = context.getWriter ();
            out.write( ' ' );
            continue;
        }
        // END ADDITIONS.

        children [i].writeXml (context);
	    }
	} finally {
	    if (!preserve) {
		context.setIndentLevel (oldIndent);
		if (!pureText)
		    context.printIndent ();		// for ETag
	    }
	}
    }


    // BEGIN ADDITIONS.
    private boolean isWhitespace ( String value )
    {
        if ( (value == null) || (value.length() == 0) )
            return false;

        int len = value.length( );

	    for (int i = 0; i < len; i++) 
        {
            // Character.isSpaceChar() doesn't work here.
            // Should the following check - taken from method removeWhiteSpaces() - be used instead?
            // if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
            if ( !Character.isWhitespace( value.charAt(i) ) )
                return false;
        }
        
        return true;
    }
    // END ADDITIONS.


    // package private -- overridden in implementation classes
    abstract void checkChildType (int type)
    throws DOMException;

    // DOM support


    /**
     * <b>DOM:</b>  Returns true if there are children to this node.
     */
    final public boolean hasChildNodes ()
    {
	return length > 0;
    }

    /**
     * <b>DOM:</b>  Returns the first child of this node, else null if there
     * are no children.
     */
    final public Node getFirstChild ()
    {
	if (length == 0)
	    return null;
	return children [0];
    }
    
    /**
     * <b>DOM:</b>  Returns the last child of this node, else null if there
     * are no children.
     */
    final public Node getLastChild ()
    {
	if (length == 0)
	    return null;
	return children [length - 1];
    }

    /** <b>DOM:</b>  Returns the number of children */
    final public int getLength ()
    {
	return length;
    }

    /** <b>DOM:</b>  Returns the Nth child, or null */
    final public Node item (int i)
    {
	if (length == 0 || i >= length)
	    return null;
	try {
	    return children [i];
	} catch (ArrayIndexOutOfBoundsException e) {
	    return null;
	}
    }

    // groups all the "wrong document/implementation" checks
    private NodeBase checkDocument (Node newChild)
    throws DOMException
    {
	if (newChild == null)
	    throw new DomEx (DomEx.HIERARCHY_REQUEST_ERR);

	// check for wrong implementation
	if (!(newChild instanceof NodeBase))
	    throw new DomEx (DomEx.WRONG_DOCUMENT_ERR);

	Document	owner = newChild.getOwnerDocument ();
	XmlDocument	myOwner = ownerDocument;
	NodeBase	child = (NodeBase) newChild;

	// bizarre DOM special case for document
	if (myOwner == null && this instanceof XmlDocument)
	    myOwner = (XmlDocument) this;

	// check for wrong document
	if (owner != null && owner != myOwner)
	    throw new DomEx (DomEx.WRONG_DOCUMENT_ERR);
	
	// permit "unowned" NodeBase children to be added,
	// e.g. if someone constructs an ElementNode directly
	if (owner == null) {
	    child.setOwnerDocument (myOwner);
	}

        if (child.hasChildNodes ()) {
    	    for (int i = 0; true; i++) {
	        Node	node = child.item (i);
	        if (node == null)
		    break;
	        if (node.getOwnerDocument () == null)
	    	    ((NodeBase)node).setOwnerDocument (myOwner);
	        else if (node.getOwnerDocument () != myOwner)
		    throw new DomEx (DomEx.WRONG_DOCUMENT_ERR);
	    }
        }

	return child;
    }

    // makes sure that child isn't an ancestor of this
    private void checkNotAncestor (Node newChild) throws DOMException
    {
	// text, etc ...
	if (!newChild.hasChildNodes ())
	    return;

	Node	ancestor = this;

	while (ancestor != null) {
	    if (newChild == ancestor)
		throw new DomEx (DomEx.HIERARCHY_REQUEST_ERR);
	    ancestor = ancestor.getParentNode ();
	}
    }

    // update mutation count 
    private void mutated ()
    {
	XmlDocument doc = ownerDocument;

	if (doc == null && this instanceof XmlDocument)
	    doc = (XmlDocument) this;
	if (doc != null)
	    doc.mutationCount++;
    }

    //
    // When fragments are appended/inserted/replaced, their entire
    // contents get moved and the fragment becomes empty.
    //
    private void consumeFragment (Node fragment, Node before)
    throws DOMException
    {
	ParentNode	frag = (ParentNode) fragment;
	Node		temp;

	// don't start insertions we can't complete
	for (int i = 0; (temp = frag.item (i)) != null; i++) {
	    checkNotAncestor (temp);
	    checkChildType (temp.getNodeType ());
	}

	while ((temp = frag.item (0)) != null) 
	    insertBefore (temp, before);
    }

    /**
     * <b>DOM:</b>  Appends the child to the set of this node's children.
     * The new child must belong to this document.  
     * 
     * @param newChild the new child to be appended
     */
    public Node appendChild (Node newChild)
    throws DOMException
    {
	NodeBase	child;

	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	child = checkDocument (newChild);

	if (newChild.getNodeType () == DOCUMENT_FRAGMENT_NODE) {
	    consumeFragment (newChild, null);
	    return newChild;
	}

	checkNotAncestor (newChild);
	checkChildType (child.getNodeType ());

	// this is the only place this vector needs allocating,
	// though it may also need to be grown in insertBefore.
	// most elements have very few children
	if (children == null)
	    children = new NodeBase [3];
	else if (children.length == length) {
	    NodeBase temp [] = new NodeBase [length * 2];
            System.arraycopy (children, 0, temp, 0, length);
	    children = temp;
	}

	child.setParentNode (this, length);
	children [length++] = child;
	mutated ();
	return child;
    }


    /**
     * <b>DOM:</b> Inserts the new child before the specified child, which
     * if null indicates appending the new child to the current set of
     * children.  The new child must belong to this particular document.
     * If the newChild is already in the tree, it is first removed.
     *
     * @param newChild the new child to be inserted
     * @param refChild node before which newChild is to be inserted
     */
    public Node insertBefore (Node newChild, Node refChild)
    throws DOMException
    {
	NodeBase	child;

	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	if (refChild == null) 
	    return appendChild (newChild);
	if (length == 0)
	    throw new DomEx (DomEx.NOT_FOUND_ERR);

	child = checkDocument (newChild);

	if (newChild.getNodeType () == DOCUMENT_FRAGMENT_NODE) {
	    consumeFragment (newChild, refChild);
	    return newChild;
	}

	checkNotAncestor (newChild);
	checkChildType (newChild.getNodeType ());

        // If the newChild is already in the tree, it is first removed
        for (int i = 0; i < length; i++) {
            if (children[i] == newChild) {
                removeChild(newChild);
                break;
            }
        }

	// grow array if needed
	if (children.length == length) {
	    NodeBase temp [] = new NodeBase [length * 2];
            System.arraycopy (children, 0, temp, 0, length);
	    children = temp;
	}

	for (int i = 0; i < length; i++) {
	    if (children [i] != refChild)
		continue;
	    child.setParentNode (this, i);
            System.arraycopy (children, i, children, i + 1, length - i);
	    children [i] = child;
	    length++;
	    mutated ();
	    return newChild;
	}

	throw new DomEx (DomEx.NOT_FOUND_ERR);
    }

    /**
     * <b>DOM:</b> Replaces the specified child with the new node,
     * returning the original child or throwing an exception.  The new
     * child must belong to this particular document.  If the newChild is
     * already in the tree, it is first removed.
     *
     * @param newChild the new child to be inserted
     * @param refChild node which is to be replaced
     */
    public Node replaceChild (Node newChild, Node refChild)
    throws DOMException
    {
	NodeBase	child;

	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	if (newChild == null || refChild == null)
	    throw new DomEx (DomEx.HIERARCHY_REQUEST_ERR);
	if (children == null)
	    throw new DomEx (DomEx.NOT_FOUND_ERR);

	child = checkDocument (newChild);

	if (newChild.getNodeType () == DOCUMENT_FRAGMENT_NODE) {
	    consumeFragment (newChild, refChild);
	    return removeChild (refChild);
	}

	checkNotAncestor (newChild);
	checkChildType (newChild.getNodeType ());

        // If the newChild is already in the tree, it is first removed
	for (int i = 0; i < length; i++) {
            if (children[i] == newChild) {
                removeChild(newChild);
                break;
            }
        }

	for (int i = 0; i < length; i++) {
	    if (children [i] != refChild)
		continue;
	    child.setParentNode (this, i);
	    children [i] = child;
	    ((NodeBase) refChild).setParentNode (null, -1);
	    mutated ();
	    return refChild;
	}
	throw new DomEx (DomEx.NOT_FOUND_ERR);
    }


    /**
     * <b>DOM:</b>  removes child if present, returning argument.
     *
     * @param oldChild the node which is to be removed
     */
    public Node removeChild (Node oldChild)
    throws DOMException
    {
	NodeBase	child;

	if (readonly)
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
	if (!(oldChild instanceof NodeBase))
	    throw new DomEx (DomEx.NOT_FOUND_ERR);
	child = (NodeBase) oldChild;
	for (int i = 0; i < length; i++) {
	    if (children [i] != child)
		continue;
	    if ((i + 1) != length)
		System.arraycopy (children, i + 1, children, i,
		    (length - 1) - i);
	    length--;
	    children [length] = null;
	    child.setParentNode (null, -1);
	    mutated ();
	    return oldChild;
	}
	throw new DomEx (DomEx.NOT_FOUND_ERR);
    }


    /**
     * <b>DOM:</b>  Returns a "live" list view of the elements below this
     * one which have the specified tag name.  Because this is "live", this
     * API is dangerous -- indices are not stable in the face of most tree
     * updates.  Use a TreeWalker instead.
     *
     * @param tagname the tag name to show; or "*" for all elements.
     * @return list of such elements
     */
    public NodeList getElementsByTagName (String tagname)
    {
	if ("*".equals (tagname))
	    tagname = null;
	return new TagList (tagname); 
    }

    /**
     * @since DOM Level 2
     */
    public NodeList getElementsByTagNameNS(String namespaceURI,
                                           String localName) {
	if ("*".equals(namespaceURI)) {
	    namespaceURI = null;
        }
	if ("*".equals(localName)) {
	    localName = null;
        }
	return new TagListNS(namespaceURI, localName); 
    }

    //
    // Slightly optimized to track document mutation count.  For now
    // we assume that a 32 bit counter won't wrap around, and that
    // there's no point in caching list length.
    //
    class TagList implements NodeList {
	protected String        tag;

	protected int		lastMutationCount;
	protected int		lastIndex;
	protected TreeWalker	lastWalker;

	protected int getLastMutationCount() {
	    XmlDocument doc = (XmlDocument) getOwnerDocument ();
	    return (doc == null) ? 0 : doc.mutationCount;
	}

	TagList (String tag) { this.tag = tag; }

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

	    int temp = getLastMutationCount ();

	    // Can we try to reuse the last walker?
	    if (lastWalker != null) {
		if (i < lastIndex || temp != lastMutationCount)
		    lastWalker = null;
	    }

	    // if not, get a new one ...
	    if (lastWalker == null) {
		lastWalker = new TreeWalker (ParentNode.this);
		lastIndex = -1;
		lastMutationCount = temp;
	    }

	    if (i == lastIndex)
		return lastWalker.getCurrent ();

	    Node	node = null;

	    while (i > lastIndex
                   && (node = lastWalker.getNextElement (tag)) != null) {
		lastIndex++;
            }

            // If we walk off the end of the list, throw away lastWalker
            if (node == null) {
                lastWalker = null;
            }
	    return node;
	}

	public int	getLength ()
	{
	    TreeWalker	walker = new TreeWalker (ParentNode.this);
	    Node	node = null;
	    int		retval;

	    for (retval = 0;
		    (node = walker.getNextElement (tag)) != null;
		    retval++)
		continue;
	    return retval;
	}
    }

    // Namespace version
    // XXX Ugly: much code in common with superclass
    class TagListNS extends TagList {
	private String		namespaceURI;

	TagListNS(String namespaceURI, String localName) {
            // XXX Use the super.tag field as a localName
            super(localName);
            this.namespaceURI = namespaceURI;
        }

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

	    int temp = getLastMutationCount();

	    // Can we try to reuse the last walker?
	    if (lastWalker != null) {
		if (i < lastIndex || temp != lastMutationCount) {
		    lastWalker = null;
                }
	    }

	    // if not, get a new one ...
	    if (lastWalker == null) {
		lastWalker = new TreeWalker(ParentNode.this);
		lastIndex = -1;
		lastMutationCount = temp;
	    }

	    if (i == lastIndex) {
		return lastWalker.getCurrent();
            }

	    Node node = null;

	    while (i > lastIndex
                   && (node = lastWalker.getNextElement(namespaceURI,
                                                        tag)) != null) {
		lastIndex++;
            }

            // If we walk off the end of the list, throw away lastWalker
            if (node == null) {
                lastWalker = null;
            }
	    return node;
	}

	public int getLength() {
	    TreeWalker walker = new TreeWalker(ParentNode.this);
            int count;
	    for (count = 0;
                 walker.getNextElement(namespaceURI, tag) != null;
                 count++) {
                // noop
            }
	    return count;
	}
    }


    /**
     * Returns the index of the node in the list of children, such
     * that <em>item()</em> will return that child.
     *
     * @param maybeChild the node which may be a child of this one
     * @return the index of the node in the set of children, or
     *	else -1 if that node is not a child
     */
    final public int getIndexOf (Node maybeChild)
    {
	for (int i = 0; i < length; i++)
	    if (children [i] == maybeChild)
		return i;
	return -1;
    }


    /**
     * @since DOM Level 2
     * In DOM2, normalize() was generalized and got moved to Node.
     *
     * XXX Comments below are old:
     * <b>DOM2:</b> Merges all adjacent Text nodes in the tree rooted by this
     * element.  Avoid using this on large blocks of text not separated
     * by markup such as elements or processing instructions, since it
     * can require arbitrarily large blocks of contiguous memory.
     *
     * XXX The following extension breaks a DOM conformance test so the
     * code has been modified to not behave as described:
     * <P> As a compatible extension to DOM, this normalizes treatment
     * of whitespace except when the <em>xml:space='preserve'</em>
     * attribute value applies to a node.  All whitespace is normalized
     * to one space.  This ensures that text which is pretty-printed and
     * then reread (and normalized) retains the same content. </P>
     */
    public void normalize() {
	boolean	preserve = false;
	boolean	knowPreserve = false;

	if (readonly) {
	    throw new DomEx (DomEx.NO_MODIFICATION_ALLOWED_ERR);
        }

	for (int i = 0; true; i++) {
            Node node = item(i);
	    if (node == null) {
		break;
            }
	    switch (node.getNodeType()) {
            case ELEMENT_NODE:
		((Element)node).normalize ();
		continue;
                // case CDATA_SECTION_NODE:
            case TEXT_NODE: {
                Node node2 = item(i + 1);
                if (node2 == null || node2.getNodeType() != TEXT_NODE) {
                    if (false) {
                        // The following code breaks DOM conformance so this
                        // feature is turned off.

                    // See if xml:space='preserve' is set...
                    if (!knowPreserve) {
                        preserve = "preserve".equals(
                            getInheritedAttribute("xml:space"));
                        knowPreserve = true;
                    }

                    // ... and if not, normalize whitespace
                    if (!preserve) {
                        char[] buf = ((TextNode)node).data;

                        // XXX this isn't supposed to happen
                        if (buf == null || buf.length == 0) {
                            removeChild(node);
                            i--;
                            continue;
                        }

                        int current = removeWhiteSpaces(buf);

                        // compact if it shrank
                        if (current != buf.length) {
                            char[] tmp = new char[current];
                            System.arraycopy(buf, 0, tmp, 0, current);
                            ((TextNode)node).data = tmp;
                        }
                    }
                    }
                    continue;
                }
                ((TextNode) node).joinNextText();
                i--;
                continue;
            }
            default:
		continue;
	    }
	}
    }

    /*
     * removes white leading, trailing and extra white spaces from the buffer.
     * returns the size of the new buf after the white spaces are removed.
     */
    public int removeWhiteSpaces(char[] buf) {
        int current = 0;
        int j = 0;

        // copy to beginning, normalizing whitespace
        // (including leading, trailing) to one space
        while (j < buf.length) {
            boolean sawSpace = false;
            char c = buf[j++];

            if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
                c = ' ';
                sawSpace = true;
            }
            buf[current++] = c;
            if (sawSpace) {
                while (j < buf.length) {
                    c = buf[j];
                    if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
                        j++;
                        continue;
                    } else {
                        break;
                    }
                }
            }
        }
        return current;
    }
}

