/*
 * @(#)VariableHeightLayoutCache.java	1.21 04/05/05
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package javax.swing.tree;

import javax.swing.event.TreeModelEvent;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.Vector;

/**
 * NOTE: This will become more open in a future release.
 * <p>
 * <strong>Warning:</strong>
 * Serialized objects of this class will not be compatible with
 * future Swing releases. The current serialization support is
 * appropriate for short term storage or RMI between applications running
 * the same version of Swing.  As of 1.4, support for long term storage
 * of all JavaBeans<sup><font size="-2">TM</font></sup>
 * has been added to the <code>java.beans</code> package.
 * Please see {@link java.beans.XMLEncoder}.
 *
 * @version 1.21 05/05/04
 * @author Rob Davis
 * @author Ray Ryan
 * @author Scott Violet
 */

public class VariableHeightLayoutCache extends AbstractLayoutCache {
    /**
     * The array of nodes that are currently visible, in the order they
     * are displayed.
     */
    private Vector            visibleNodes;

    /**
     * This is set to true if one of the entries has an invalid size.
     */
    private boolean           updateNodeSizes;

    /**
     * The root node of the internal cache of nodes that have been shown.
     * If the treeModel is vending a network rather than a true tree,
     * there may be one cached node for each path to a modeled node.
     */
    private TreeStateNode     root;

    /**
     * Used in getting sizes for nodes to avoid creating a new Rectangle
     * every time a size is needed.
     */
    private Rectangle         boundsBuffer;

    /**
     * Maps from <code>TreePath</code> to a <code>TreeStateNode</code>.
     */
    private Hashtable         treePathMapping;

    /**
     * A stack of stacks.
     */
    private Stack             tempStacks;


    public VariableHeightLayoutCache() {
	super();
	tempStacks = new Stack();
	visibleNodes = new Vector();
	boundsBuffer = new Rectangle();
	treePathMapping = new Hashtable();
    }

    /**
     * Sets the <code>TreeModel</code> that will provide the data.
     *
     * @param newModel the <code>TreeModel</code> that is to provide the data
     * @beaninfo
     *        bound: true
     *  description: The TreeModel that will provide the data.
     */
    public void setModel(TreeModel newModel) {
	super.setModel(newModel);
	rebuild(false);
    }

    /**
     * Determines whether or not the root node from
     * the <code>TreeModel</code> is visible.
     *
     * @param rootVisible true if the root node of the tree is to be displayed
     * @see #rootVisible
     * @beaninfo
     *        bound: true
     *  description: Whether or not the root node
     *               from the TreeModel is visible.
     */
    public void setRootVisible(boolean rootVisible) {
	if(isRootVisible() != rootVisible && root != null) {
	    if(rootVisible) {
		root.updatePreferredSize(0);
		visibleNodes.insertElementAt(root, 0);
	    }
	    else if(visibleNodes.size() > 0) {
		visibleNodes.removeElementAt(0);
		if(treeSelectionModel != null)
		    treeSelectionModel.removeSelectionPath
			(root.getTreePath());
	    }
	    if(treeSelectionModel != null)
		treeSelectionModel.resetRowSelection();
	    if(getRowCount() > 0)
		getNode(0).setYOrigin(0);
	    updateYLocationsFrom(0);
	    visibleNodesChanged();
	}
	super.setRootVisible(rootVisible);
    }

    /**
     * Sets the height of each cell.  If the specified value
     * is less than or equal to zero the current cell renderer is
     * queried for each row's height.
     *
     * @param rowHeight the height of each cell, in pixels
     * @beaninfo
     *        bound: true
     *  description: The height of each cell.
     */
    public void setRowHeight(int rowHeight) {
	if(rowHeight != getRowHeight())	{
	    super.setRowHeight(rowHeight);
	    invalidateSizes();
	    this.visibleNodesChanged();
	}
    }

    /**
     * Sets the renderer that is responsible for drawing nodes in the tree.
     * @param nd the renderer
     */
    public void setNodeDimensions(NodeDimensions nd) {
	super.setNodeDimensions(nd);
	invalidateSizes();
	visibleNodesChanged();
    }

    /**
     * Marks the path <code>path</code> expanded state to
     * <code>isExpanded</code>.
     * @param path the <code>TreePath</code> of interest
     * @param isExpanded true if the path should be expanded, otherwise false
     */
    public void setExpandedState(TreePath path, boolean isExpanded) {
	if(path != null) {
	    if(isExpanded)
		ensurePathIsExpanded(path, true);
	    else {
		TreeStateNode        node = getNodeForPath(path, false, true);

		if(node != null) {
		    node.makeVisible();
		    node.collapse();
		}
	    }
	}
    }

    /**
     * Returns true if the path is expanded, and visible.
     * @return true if the path is expanded and visible, otherwise false
     */
    public boolean getExpandedState(TreePath path) {
	TreeStateNode       node = getNodeForPath(path, true, false);

	return (node != null) ? (node.isVisible() && node.isExpanded()) :
	                         false;
    }

    /**
      * Returns the <code>Rectangle</code> enclosing the label portion
      * into which the item identified by <code>path</code> will be drawn.
      *
      * @param path  the path to be drawn
      * @param placeIn the bounds of the enclosing rectangle
      * @return the bounds of the enclosing rectangle or <code>null</code>
      *    if the node could not be ascertained
      */
    public Rectangle getBounds(TreePath path, Rectangle placeIn) {
	TreeStateNode       node = getNodeForPath(path, true, false);

	if(node != null) {
	    if(updateNodeSizes)
		updateNodeSizes(false);
	    return node.getNodeBounds(placeIn);
	}
	return null;
    }

    /**
      * Returns the path for <code>row</code>.  If <code>row</code>
      * is not visible, <code>null</code> is returned.
      *
      * @param row the location of interest
      * @return the path for <code>row</code>, or <code>null</code>
      * if <code>row</code> is not visible
      */
    public TreePath getPathForRow(int row) {
	if(row >= 0 && row < getRowCount()) {
	    return getNode(row).getTreePath();
	}
	return null;
    }

    /**
      * Returns the row where the last item identified in path is visible.
      * Will return -1 if any of the elements in path are not
      * currently visible.
      * 
      * @param path the <code>TreePath</code> of interest
      * @return the row where the last item in path is visible
      */
    public int getRowForPath(TreePath path) {
	if(path == null)
	    return -1;

	TreeStateNode    visNode = getNodeForPath(path, true, false);

	if(visNode != null)
	    return visNode.getRow();
	return -1;
    }

    /**
     * Returns the number of visible rows.
     * @return the number of visible rows
     */
    public int getRowCount() {
	return visibleNodes.size();
    }

    /**
     * Instructs the <code>LayoutCache</code> that the bounds for
     * <code>path</code> are invalid, and need to be updated.
     *
     * @param path the <code>TreePath</code> which is now invalid
     */
    public void invalidatePathBounds(TreePath path) {
	TreeStateNode       node = getNodeForPath(path, true, false);

	if(node != null) {
	    node.markSizeInvalid();
	    if(node.isVisible())
		updateYLocationsFrom(node.getRow());
	}
    }

    /**
     * Returns the preferred height.
     * @return the preferred height
     */
    public int getPreferredHeight() {
	// Get the height
	int           rowCount = getRowCount();

	if(rowCount > 0) {
	    TreeStateNode  node = getNode(rowCount - 1);

	    return node.getYOrigin() + node.getPreferredHeight();
	}
	return 0;
    }

    /**
     * Returns the preferred width and height for the region in
     * <code>visibleRegion</code>.
     *
     * @param bounds  the region being queried
     */
    public int getPreferredWidth(Rectangle bounds) {
	if(updateNodeSizes)
	    updateNodeSizes(false);

	return getMaxNodeWidth();
    }

    /**
      * Returns the path to the node that is closest to x,y.  If
      * there is nothing currently visible this will return <code>null</code>,
      * otherwise it will always return a valid path. 
      * If you need to test if the
      * returned object is exactly at x, y you should get the bounds for
      * the returned path and test x, y against that.
      *
      * @param x  the x-coordinate 
      * @param y  the y-coordinate 
      * @return the path to the node that is closest to x, y
      */
    public TreePath getPathClosestTo(int x, int y) {
	if(getRowCount() == 0)
	    return null;

	if(updateNodeSizes)
	    updateNodeSizes(false);

	int                row = getRowContainingYLocation(y);

	return getNode(row).getTreePath();
    }

    /**
     * Returns an <code>Enumerator</code> that increments over the visible paths
     * starting at the passed in location. The ordering of the enumeration
     * is based on how the paths are displayed.
     *
     * @param path the location in the <code>TreePath</code> to start
     * @return an <code>Enumerator</code> that increments over the visible
     *     paths
     */
    public Enumeration<TreePath> getVisiblePathsFrom(TreePath path) {
	TreeStateNode       node = getNodeForPath(path, true, false);

	if(node != null) {
	    return new VisibleTreeStateNodeEnumeration(node);
	}
	return null;
    }

    /**
     * Returns the number of visible children for <code>path</code>.
     * @return the number of visible children for <code>path</code>
     */
    public int getVisibleChildCount(TreePath path) {
	TreeStateNode         node = getNodeForPath(path, true, false);

	return (node != null) ? node.getVisibleChildCount() : 0;
    }

    /**
     * Informs the <code>TreeState</code> that it needs to recalculate
     * all the sizes it is referencing.
     */
    public void invalidateSizes() {
	if(root != null)
	    root.deepMarkSizeInvalid();
	if(!isFixedRowHeight() && visibleNodes.size() > 0) {
	    updateNodeSizes(true);
	}
    }

    /**
      * Returns true if the value identified by <code>path</code> is
      * currently expanded.
      * @return true if the value identified by <code>path</code> is
      *    currently expanded
      */
    public boolean isExpanded(TreePath path) {
	if(path != null) {
	    TreeStateNode     lastNode = getNodeForPath(path, true, false);

	    return (lastNode != null && lastNode.isExpanded());
	}
	return false;
    }

    //
    // TreeModelListener methods
    //

    /**
     * Invoked after a node (or a set of siblings) has changed in some
     * way. The node(s) have not changed locations in the tree or
     * altered their children arrays, but other attributes have
     * changed and may affect presentation. Example: the name of a
     * file has changed, but it is in the same location in the file
     * system.
     *
     * <p><code>e.path</code> returns the path the parent of the
     * changed node(s).
     *
     * <p><code>e.childIndices</code> returns the index(es) of the
     * changed node(s).
     *
     * @param e the <code>TreeModelEvent</code> of interest
     */
    public void treeNodesChanged(TreeModelEvent e) {
	if(e != null) {
	    int               changedIndexs[];
	    TreeStateNode     changedNode;

	    changedIndexs = e.getChildIndices();
	    changedNode = getNodeForPath(e.getTreePath(), false, false);
	    if(changedNode != null) {
		Object            changedValue = changedNode.getValue();

		/* Update the size of the changed node, as well as all the
		   child indexs that are passed in. */
		changedNode.updatePreferredSize();
		if(changedNode.hasBeenExpanded() && changedIndexs != null) {
		    int                counter;
		    TreeStateNode      changedChildNode;

		    for(counter = 0; counter < changedIndexs.length;
			counter++) {
			changedChildNode = (TreeStateNode)changedNode
				    .getChildAt(changedIndexs[counter]);
			/* Reset the user object. */
			changedChildNode.setUserObject
				    (treeModel.getChild(changedValue,
						     changedIndexs[counter]));
			changedChildNode.updatePreferredSize();
		    }
		}
		else if (changedNode == root) {
		    // Null indicies for root indicates it changed.
		    changedNode.updatePreferredSize();
		}
		if(!isFixedRowHeight()) {
		    int          aRow = changedNode.getRow();

		    if(aRow != -1)
			this.updateYLocationsFrom(aRow);
		}
		this.visibleNodesChanged();
	    }
	}
    }


    /**
     * Invoked after nodes have been inserted into the tree.
     *
     * <p><code>e.path</code> returns the parent of the new nodes.
     * <p><code>e.childIndices</code> returns the indices of the new nodes in
     * ascending order.
     *
     * @param e the <code>TreeModelEvent</code> of interest
     */
    public void treeNodesInserted(TreeModelEvent e) {
	if(e != null) {
	    int               changedIndexs[];
	    TreeStateNode     changedParentNode;

	    changedIndexs = e.getChildIndices();
	    changedParentNode = getNodeForPath(e.getTreePath(), false, false);
	    /* Only need to update the children if the node has been
	       expanded once. */
	    // PENDING(scott): make sure childIndexs is sorted!
	    if(changedParentNode != null && changedIndexs != null &&
	       changedIndexs.length > 0) {
		if(changedParentNode.hasBeenExpanded()) {
		    boolean            makeVisible;
		    int                counter;
		    Object             changedParent;
		    TreeStateNode      newNode;
		    int                oldChildCount = changedParentNode.
			                  getChildCount();

		    changedParent = changedParentNode.getValue();
		    makeVisible = ((changedParentNode == root &&
				    !rootVisible) ||
				   (changedParentNode.getRow() != -1 &&
				    changedParentNode.isExpanded()));
		    for(counter = 0;counter < changedIndexs.length;counter++)
		    {
			newNode = this.createNodeAt(changedParentNode,
						    changedIndexs[counter]);
		    }
		    if(oldChildCount == 0) {
			// Update the size of the parent.
			changedParentNode.updatePreferredSize();
		    }
		    if(treeSelectionModel != null)
			treeSelectionModel.resetRowSelection();
		    /* Update the y origins from the index of the parent
		       to the end of the visible rows. */
		    if(!isFixedRowHeight() && (makeVisible ||
					       (oldChildCount == 0 &&
					changedParentNode.isVisible()))) {
			if(changedParentNode == root)
			    this.updateYLocationsFrom(0);
			else
			    this.updateYLocationsFrom(changedParentNode.
						      getRow());
			this.visibleNodesChanged();
		    }
		    else if(makeVisible)
			this.visibleNodesChanged();
		}
		else if(treeModel.getChildCount(changedParentNode.getValue())
			- changedIndexs.length == 0) {
		    changedParentNode.updatePreferredSize();
		    if(!isFixedRowHeight() && changedParentNode.isVisible())
			updateYLocationsFrom(changedParentNode.getRow());
		}
	    }
	}
    }

    /**
     * Invoked after nodes have been removed from the tree.  Note that
     * if a subtree is removed from the tree, this method may only be
     * invoked once for the root of the removed subtree, not once for
     * each individual set of siblings removed.
     *
     * <p><code>e.path</code> returns the former parent of the deleted nodes.
     *
     * <p><code>e.childIndices</code> returns the indices the nodes had
     * before they were deleted in ascending order.
     *
     * @param e the <code>TreeModelEvent</code> of interest
     */
    public void treeNodesRemoved(TreeModelEvent e) {
	if(e != null) {
	    int               changedIndexs[];
	    TreeStateNode     changedParentNode;

	    changedIndexs = e.getChildIndices();
	    changedParentNode = getNodeForPath(e.getTreePath(), false, false);
	    // PENDING(scott): make sure that changedIndexs are sorted in
	    // ascending order.
	    if(changedParentNode != null && changedIndexs != null &&
	       changedIndexs.length > 0) {
		if(changedParentNode.hasBeenExpanded()) {
		    boolean            makeInvisible;
		    int                counter;
		    int                removedRow;
		    TreeStateNode      removedNode;

		    makeInvisible = ((changedParentNode == root &&
				      !rootVisible) ||
				     (changedParentNode.getRow() != -1 &&
				      changedParentNode.isExpanded()));
		    for(counter = changedIndexs.length - 1;counter >= 0;
			counter--) {
			removedNode = (TreeStateNode)changedParentNode.
				getChildAt(changedIndexs[counter]);
			if(removedNode.isExpanded()) {
			    removedNode.collapse(false);
			}

			/* Let the selection model now. */
			if(makeInvisible) {
			    removedRow = removedNode.getRow();
			    if(removedRow != -1) {
				visibleNodes.removeElementAt(removedRow);
			    }
			}
			changedParentNode.remove(changedIndexs[counter]);
		    }
		    if(changedParentNode.getChildCount() == 0) {
			// Update the size of the parent.
			changedParentNode.updatePreferredSize();
                        if (changedParentNode.isExpanded() &&
                                   changedParentNode.isLeaf()) {
                            // Node has become a leaf, collapse it.
                            changedParentNode.collapse(false);
                        }
		    }
		    if(treeSelectionModel != null)
			treeSelectionModel.resetRowSelection();
		    /* Update the y origins from the index of the parent
		       to the end of the visible rows. */
		    if(!isFixedRowHeight() && (makeInvisible ||
			       (changedParentNode.getChildCount() == 0 &&
				changedParentNode.isVisible()))) {
			if(changedParentNode == root) {
			    /* It is possible for first row to have been
			       removed if the root isn't visible, in which
			       case ylocations will be off! */
			    if(getRowCount() > 0)
				getNode(0).setYOrigin(0);
			    updateYLocationsFrom(0);
			}
			else
			    updateYLocationsFrom(changedParentNode.getRow());
			this.visibleNodesChanged();
		    }
		    else if(makeInvisible)
			this.visibleNodesChanged();
		}
		else if(treeModel.getChildCount(changedParentNode.getValue())
			== 0) {
		    changedParentNode.updatePreferredSize();
		    if(!isFixedRowHeight() && changedParentNode.isVisible())
			this.updateYLocationsFrom(changedParentNode.getRow());
		}
	    }
	}
    }

    /**
     * Invoked after the tree has drastically changed structure from a
     * given node down.  If the path returned by <code>e.getPath</code>
     * is of length one and the first element does not identify the
     * current root node the first element should become the new root
     * of the tree.
     *
     * <p><code>e.path</code> holds the path to the node.
     * <p><code>e.childIndices</code> returns <code>null</code>.
     *
     * @param e the <code>TreeModelEvent</code> of interest
     */
    public void treeStructureChanged(TreeModelEvent e) {
	if(e != null)
	{
	    TreePath          changedPath = e.getTreePath();
	    TreeStateNode     changedNode;

	    changedNode = getNodeForPath(changedPath, false, false);

	    // Check if root has changed, either to a null root, or
            // to an entirely new root.
	    if(changedNode == root ||
               (changedNode == null &&
                ((changedPath == null && treeModel != null &&
                  treeModel.getRoot() == null) ||
                 (changedPath != null && changedPath.getPathCount() == 1)))) {
                rebuild(true);
            }
	    else if(changedNode != null) {
                int                              nodeIndex, oldRow;
                TreeStateNode                    newNode, parent;
                boolean                          wasExpanded, wasVisible;
                int                              newIndex;

                wasExpanded = changedNode.isExpanded();
                wasVisible = (changedNode.getRow() != -1);
                /* Remove the current node and recreate a new one. */
                parent = (TreeStateNode)changedNode.getParent();
                nodeIndex = parent.getIndex(changedNode);
                if(wasVisible && wasExpanded) {
                    changedNode.collapse(false);
                }
                if(wasVisible)
                    visibleNodes.removeElement(changedNode);
                changedNode.removeFromParent();
                createNodeAt(parent, nodeIndex);
                newNode = (TreeStateNode)parent.getChildAt(nodeIndex);
                if(wasVisible && wasExpanded)
                    newNode.expand(false);
                newIndex = newNode.getRow();
                if(!isFixedRowHeight() && wasVisible) {
                    if(newIndex == 0)
                        updateYLocationsFrom(newIndex);
                    else
                        updateYLocationsFrom(newIndex - 1);
                    this.visibleNodesChanged();
                }
                else if(wasVisible)
                    this.visibleNodesChanged();
	    }
	}
    }


    //
    // Local methods
    //

    private void visibleNodesChanged() {
    }

    /**
     * Adds a mapping for node.
     */
    private void addMapping(TreeStateNode node) {
	treePathMapping.put(node.getTreePath(), node);
    }

    /**
     * Removes the mapping for a previously added node.
     */
    private void removeMapping(TreeStateNode node) {
	treePathMapping.remove(node.getTreePath());
    }

    /**
     * Returns the node previously added for <code>path</code>. This may
     * return null, if you to create a node use getNodeForPath.
     */
    private TreeStateNode getMapping(TreePath path) {
	return (TreeStateNode)treePathMapping.get(path);
    }

    /**
     * Retursn the bounds for row, <code>row</code> by reference in
     * <code>placeIn</code>. If <code>placeIn</code> is null a new
     * Rectangle will be created and returned.
     */
    private Rectangle getBounds(int row, Rectangle placeIn) {
	if(updateNodeSizes)
	    updateNodeSizes(false);

	if(row >= 0 && row < getRowCount()) {
	    return getNode(row).getNodeBounds(placeIn);
	}
	return null;
    }

    /**
     * Completely rebuild the tree, all expanded state, and node caches are
     * removed. All nodes are collapsed, except the root.
     */
    private void rebuild(boolean clearSelection) {
        Object rootObject;

	treePathMapping.clear();
	if(treeModel != null && (rootObject = treeModel.getRoot()) != null) {
	    root = createNodeForValue(rootObject);
	    root.path = new TreePath(rootObject);
	    addMapping(root);
	    root.updatePreferredSize(0);
	    visibleNodes.removeAllElements();
	    if (isRootVisible())
		visibleNodes.addElement(root);
	    if(!root.isExpanded())
		root.expand();
	    else {
		Enumeration cursor = root.children();
		while(cursor.hasMoreElements()) {
		    visibleNodes.addElement(cursor.nextElement());
		}
		if(!isFixedRowHeight())
		    updateYLocationsFrom(0);
	    }
	}
	else {
	    visibleNodes.removeAllElements();
	    root = null;
	}
	if(clearSelection && treeSelectionModel != null) {
	    treeSelectionModel.clearSelection();
	}
	this.visibleNodesChanged();
    }

    /**
      * Creates a new node to represent the node at <I>childIndex</I> in
      * <I>parent</I>s children.  This should be called if the node doesn't
      * already exist and <I>parent</I> has been expanded at least once.
      * The newly created node will be made visible if <I>parent</I> is
      * currently expanded.  This does not update the position of any
      * cells, nor update the selection if it needs to be.  If succesful
      * in creating the new TreeStateNode, it is returned, otherwise
      * null is returned.
      */
    private TreeStateNode createNodeAt(TreeStateNode parent,
					 int childIndex) {
	boolean                isParentRoot;
	Object                 newValue;
	TreeStateNode          newChildNode;

	newValue = treeModel.getChild(parent.getValue(), childIndex);
	newChildNode = createNodeForValue(newValue);
	parent.insert(newChildNode, childIndex);
	newChildNode.updatePreferredSize(-1);
	isParentRoot = (parent == root);
	if(newChildNode != null && parent.isExpanded() &&
	   (parent.getRow() != -1 || isParentRoot)) {
	    int                 newRow;

	    /* Find the new row to insert this newly visible node at. */
	    if(childIndex == 0) {
		if(isParentRoot && !isRootVisible())
		    newRow = 0;
		else
		    newRow = parent.getRow() + 1;
	    }
	    else if(childIndex == parent.getChildCount())
		newRow = parent.getLastVisibleNode().getRow() + 1;
	    else {
		TreeStateNode          previousNode;

		previousNode = (TreeStateNode)parent.
		    getChildAt(childIndex - 1);
		newRow = previousNode.getLastVisibleNode().getRow() + 1;
	    }
	    visibleNodes.insertElementAt(newChildNode, newRow);
	}
	return newChildNode;
    }

    /**
      * Returns the TreeStateNode identified by path.  This mirrors
      * the behavior of getNodeForPath, but tries to take advantage of
      * path if it is an instance of AbstractTreePath.
      */
    private TreeStateNode getNodeForPath(TreePath path,
					   boolean onlyIfVisible,
					   boolean shouldCreate) {
	if(path != null) {
	    TreeStateNode      node;

	    node = getMapping(path);
	    if(node != null) {
		if(onlyIfVisible && !node.isVisible())
		    return null;
		return node;
	    }

	    // Check all the parent paths, until a match is found.
	    Stack                paths;

	    if(tempStacks.size() == 0) {
		paths = new Stack();
	    }
	    else {
		paths = (Stack)tempStacks.pop();
	    }

	    try {
		paths.push(path);
		path = path.getParentPath();
		node = null;
		while(path != null) {
		    node = getMapping(path);
		    if(node != null) {
			// Found a match, create entries for all paths in
			// paths.
			while(node != null && paths.size() > 0) {
			    path = (TreePath)paths.pop();
			    node.getLoadedChildren(shouldCreate);

			    int            childIndex = treeModel.
				      getIndexOfChild(node.getUserObject(),
						  path.getLastPathComponent());

			    if(childIndex == -1 ||
			       childIndex >= node.getChildCount() ||
			       (onlyIfVisible && !node.isVisible())) {
				node = null;
			    }
			    else
				node = (TreeStateNode)node.getChildAt
				               (childIndex);
			}
			return node;
		    }
		    paths.push(path);
		    path = path.getParentPath();
		}
	    }
	    finally {
		paths.removeAllElements();
		tempStacks.push(paths);
	    }
	    // If we get here it means they share a different root!
	    // We could throw an exception...
	}
	return null;
    }

    /**
      * Updates the y locations of all of the visible nodes after
      * location.
      */
    private void updateYLocationsFrom(int location) {
	if(location >= 0 && location < getRowCount()) {
	    int                    counter, maxCounter, newYOrigin;
	    TreeStateNode          aNode;

	    aNode = getNode(location);
	    newYOrigin = aNode.getYOrigin() + aNode.getPreferredHeight();
	    for(counter = location + 1, maxCounter = visibleNodes.size();
		counter < maxCounter;counter++) {
		aNode = (TreeStateNode)visibleNodes.
		    elementAt(counter);
		aNode.setYOrigin(newYOrigin);
		newYOrigin += aNode.getPreferredHeight();
	    }
	}
    }

    /**
      * Resets the y origin of all the visible nodes as well as messaging
      * all the visible nodes to updatePreferredSize().  You should not
      * normally have to call this.  Expanding and contracting the nodes
      * automaticly adjusts the locations.
      * updateAll determines if updatePreferredSize() is call on all nodes
      * or just those that don't have a valid size.
      */
    private void updateNodeSizes(boolean updateAll) {
	int                      aY, counter, maxCounter;
	TreeStateNode            node;

	updateNodeSizes = false;
	for(aY = counter = 0, maxCounter = visibleNodes.size();
	    counter < maxCounter; counter++) {
	    node = (TreeStateNode)visibleNodes.elementAt(counter);
	    node.setYOrigin(aY);
	    if(updateAll || !node.hasValidSize())
		node.updatePreferredSize(counter);
	    aY += node.getPreferredHeight();
	}
    }

    /**
      * Returns the index of the row containing location.  If there
      * are no rows, -1 is returned.  If location is beyond the last
      * row index, the last row index is returned.
      */
    private int getRowContainingYLocation(int location) {
	if(isFixedRowHeight()) {
	    if(getRowCount() == 0)
		return -1;
	    return Math.max(0, Math.min(getRowCount() - 1,
					location / getRowHeight()));
	}

	int                    max, maxY, mid, min, minY;
	TreeStateNode          node;

	if((max = getRowCount()) <= 0)
	    return -1;
	mid = min = 0;
	while(min < max) {
	    mid = (max - min) / 2 + min;
	    node = (TreeStateNode)visibleNodes.elementAt(mid);
	    minY = node.getYOrigin();
	    maxY = minY + node.getPreferredHeight();
	    if(location < minY) {
		max = mid - 1;
	    }
	    else if(location >= maxY) {
		min = mid + 1;
	    }
	    else
		break;
	}
	if(min == max) {
	    mid = min;
	    if(mid >= getRowCount())
		mid = getRowCount() - 1;
	}
	return mid;
    }

    /**
     * Ensures that all the path components in path are expanded, accept
     * for the last component which will only be expanded if expandLast
     * is true.
     * Returns true if succesful in finding the path.
     */
    private void ensurePathIsExpanded(TreePath aPath, boolean expandLast) {
	if(aPath != null) {
	    // Make sure the last entry isn't a leaf.
	    if(treeModel.isLeaf(aPath.getLastPathComponent())) {
		aPath = aPath.getParentPath();
		expandLast = true;
	    }
	    if(aPath != null) {
		TreeStateNode     lastNode = getNodeForPath(aPath, false,
							    true);

		if(lastNode != null) {
		    lastNode.makeVisible();
		    if(expandLast)
			lastNode.expand();
		}
	    }
	}
    }

    /**
     * Returns the AbstractTreeUI.VisibleNode displayed at the given row
     */
    private TreeStateNode getNode(int row) {
	return (TreeStateNode)visibleNodes.elementAt(row);
    }

    /**
      * Returns the maximum node width.
      */
    private int getMaxNodeWidth() {
	int                     maxWidth = 0;
	int                     nodeWidth;
	int                     counter;
	TreeStateNode           node;

	for(counter = getRowCount() - 1;counter >= 0;counter--) {
	    node = this.getNode(counter);
	    nodeWidth = node.getPreferredWidth() + node.getXOrigin();
	    if(nodeWidth > maxWidth)
		maxWidth = nodeWidth;
	}
	return maxWidth;
    }

    /**
      * Responsible for creating a TreeStateNode that will be used
      * to track display information about value.
      */
    private TreeStateNode createNodeForValue(Object value) {
	return new TreeStateNode(value);
    }


    /**
     * TreeStateNode is used to keep track of each of
     * the nodes that have been expanded. This will also cache the preferred
     * size of the value it represents.
     */
    private class TreeStateNode extends DefaultMutableTreeNode {
	/** Preferred size needed to draw the user object. */
	protected int             preferredWidth;
	protected int             preferredHeight;

	/** X location that the user object will be drawn at. */
	protected int             xOrigin;

	/** Y location that the user object will be drawn at. */
	protected int             yOrigin;

	/** Is this node currently expanded? */
	protected boolean         expanded;

	/** Has this node been expanded at least once? */
	protected boolean         hasBeenExpanded;

	/** Path of this node. */
	protected TreePath        path;


	public TreeStateNode(Object value) {
	    super(value);
	}

	//
	// Overriden DefaultMutableTreeNode methods
	//

	/**
	 * Messaged when this node is added somewhere, resets the path
	 * and adds a mapping from path to this node.
	 */
	public void setParent(MutableTreeNode parent) {
	    super.setParent(parent);
	    if(parent != null) {
		path = ((TreeStateNode)parent).getTreePath().
		                       pathByAddingChild(getUserObject());
		addMapping(this);
	    }
	}

	/**
	 * Messaged when this node is removed from its parent, this messages
	 * <code>removedFromMapping</code> to remove all the children.
	 */
	public void remove(int childIndex) {
	    TreeStateNode     node = (TreeStateNode)getChildAt(childIndex);

	    node.removeFromMapping();
	    super.remove(childIndex);
	}

	/**
	 * Messaged to set the user object. This resets the path.
	 */
	public void setUserObject(Object o) {
	    super.setUserObject(o);
	    if(path != null) {
		TreeStateNode      parent = (TreeStateNode)getParent();

		if(parent != null)
		    resetChildrenPaths(parent.getTreePath());
		else
		    resetChildrenPaths(null);
	    }
	}

	/**
	 * Returns the children of the receiver.
	 * If the receiver is not currently expanded, this will return an
	 * empty enumeration.
	 */
	public Enumeration children() {
	    if (!this.isExpanded()) {
		return DefaultMutableTreeNode.EMPTY_ENUMERATION;
	    } else {
		return super.children();
	    }
	}

	/**
	 * Returns true if the receiver is a leaf.
	 */
	public boolean isLeaf() {
	    return getModel().isLeaf(this.getValue());
	}

	//
	// VariableHeightLayoutCache
	//

	/**
	 * Returns the location and size of this node.
	 */
	public Rectangle getNodeBounds(Rectangle placeIn) {
	    if(placeIn == null)
		placeIn = new Rectangle(getXOrigin(), getYOrigin(),
					getPreferredWidth(),
					getPreferredHeight());
	    else {
		placeIn.x = getXOrigin();
		placeIn.y = getYOrigin();
		placeIn.width = getPreferredWidth();
		placeIn.height = getPreferredHeight();
	    }
	    return placeIn;
	}

	/**
	 * @return x location to draw node at.
	 */
	public int getXOrigin() {
	    if(!hasValidSize())
		updatePreferredSize(getRow());
	    return xOrigin;
	}

	/**
	 * Returns the y origin the user object will be drawn at.
	 */
	public int getYOrigin() {
	    if(isFixedRowHeight()) {
		int      aRow = getRow();

		if(aRow == -1)
		    return -1;
		return getRowHeight() * aRow;
	    }
	    return yOrigin;
	}

	/**
	 * Returns the preferred height of the receiver.
	 */
	public int getPreferredHeight() {
	    if(isFixedRowHeight())
		return getRowHeight();
	    else if(!hasValidSize())
		updatePreferredSize(getRow());
	    return preferredHeight;
	}

	/**
	 * Returns the preferred width of the receiver.
	 */
	public int getPreferredWidth() {
	    if(!hasValidSize())
		updatePreferredSize(getRow());
	    return preferredWidth;
	}

	/**
	 * Returns true if this node has a valid size.
	 */
	public boolean hasValidSize() {
	    return (preferredHeight != 0);
	}

	/**
	 * Returns the row of the receiver.
	 */
	public int getRow() {
	    return visibleNodes.indexOf(this);
	}

	/**
	 * Returns true if this node has been expanded at least once.
	 */
	public boolean hasBeenExpanded() {
	    return hasBeenExpanded;
	}

	/**
	 * Returns true if the receiver has been expanded.
	 */
	public boolean isExpanded() {
	    return expanded;
	}

	/**
	 * Returns the last visible node that is a child of this
	 * instance.
	 */
	public TreeStateNode getLastVisibleNode() {
	    TreeStateNode                node = this;

	    while(node.isExpanded() && node.getChildCount() > 0)
		node = (TreeStateNode)node.getLastChild();
	    return node;
	}

	/**
	 * Returns true if the receiver is currently visible.
	 */
	public boolean isVisible() {
	    if(this == root)
		return true;

	    TreeStateNode        parent = (TreeStateNode)getParent();

	    return (parent != null && parent.isExpanded() &&
		    parent.isVisible());
	}

	/**
	 * Returns the number of children this will have. If the children
	 * have not yet been loaded, this messages the model.
	 */
	public int getModelChildCount() {
	    if(hasBeenExpanded)
		return super.getChildCount();
	    return getModel().getChildCount(getValue());
	}

	/**
	 * Returns the number of visible children, that is the number of
	 * children that are expanded, or leafs.
	 */
	public int getVisibleChildCount() {
	    int               childCount = 0;

	    if(isExpanded()) {
		int         maxCounter = getChildCount();

		childCount += maxCounter;
		for(int counter = 0; counter < maxCounter; counter++)
		    childCount += ((TreeStateNode)getChildAt(counter)).
			            getVisibleChildCount();
	    }
	    return childCount;
	}

	/**
	 * Toggles the receiver between expanded and collapsed.
	 */
	public void toggleExpanded() {
	    if (isExpanded()) {
		collapse();
	    } else {
		expand();
	    }
	}

	/**
	 * Makes the receiver visible, but invoking
	 * <code>expandParentAndReceiver</code> on the superclass.
	 */
	public void makeVisible() {
	    TreeStateNode       parent = (TreeStateNode)getParent();

	    if(parent != null)
		parent.expandParentAndReceiver();
	}

	/**
	 * Expands the receiver.
	 */
	public void expand() {
	    expand(true);
	}

	/**
	 * Collapses the receiver.
	 */
	public void collapse() {
	    collapse(true);
	}

	/**
	 * Returns the value the receiver is representing. This is a cover
	 * for getUserObject.
	 */
	public Object getValue() {
	    return getUserObject();
	}

	/**
	 * Returns a TreePath instance for this node.
	 */
	public TreePath getTreePath() {
	    return path;
	}

	//
	// Local methods
	//

	/**
	 * Recreates the receivers path, and all its childrens paths.
	 */
	protected void resetChildrenPaths(TreePath parentPath) {
	    removeMapping(this);
	    if(parentPath == null)
		path = new TreePath(getUserObject());
	    else
		path = parentPath.pathByAddingChild(getUserObject());
	    addMapping(this);
	    for(int counter = getChildCount() - 1; counter >= 0; counter--)
		((TreeStateNode)getChildAt(counter)).resetChildrenPaths(path);
	}

	/**
	 * Sets y origin the user object will be drawn at to
	 * <I>newYOrigin</I>.
	 */
	protected void setYOrigin(int newYOrigin) {
	    yOrigin = newYOrigin;
	}

	/**
	 * Shifts the y origin by <code>offset</code>.
	 */
	protected void shiftYOriginBy(int offset) {
	    yOrigin += offset;
	}

	/**
	 * Updates the receivers preferredSize by invoking
	 * <code>updatePreferredSize</code> with an argument of -1.
	 */
	protected void updatePreferredSize() {
	    updatePreferredSize(getRow());
	}

	/**
	 * Updates the preferred size by asking the current renderer
	 * for the Dimension needed to draw the user object this
	 * instance represents.
	 */
	protected void updatePreferredSize(int index) {
	    Rectangle       bounds = getNodeDimensions(this.getUserObject(),
						       index, getLevel(),
						       isExpanded(),
						       boundsBuffer);

	    if(bounds == null) {
		xOrigin = 0;
		preferredWidth = preferredHeight = 0;
		updateNodeSizes = true;
	    }
	    else if(bounds.height == 0) {
		xOrigin = 0;
		preferredWidth = preferredHeight = 0;
		updateNodeSizes = true;
	    }
	    else {
		xOrigin = bounds.x;
		preferredWidth = bounds.width;
		if(isFixedRowHeight())
		    preferredHeight = getRowHeight();
		else
		    preferredHeight = bounds.height;
	    }
	}

	/**
	 * Marks the receivers size as invalid. Next time the size, location
	 * is asked for it will be obtained.
	 */
	protected void markSizeInvalid() {
	    preferredHeight = 0;
	}

	/**
	 * Marks the receivers size, and all its descendants sizes, as invalid.
	 */
	protected void deepMarkSizeInvalid() {
	    markSizeInvalid();
	    for(int counter = getChildCount() - 1; counter >= 0; counter--)
		((TreeStateNode)getChildAt(counter)).deepMarkSizeInvalid();
	}

	/**
	 * Returns the children of the receiver. If the children haven't
	 * been loaded from the model and
	 * <code>createIfNeeded</code> is true, the children are first
	 * loaded.
	 */
	protected Enumeration getLoadedChildren(boolean createIfNeeded) {
	    if(!createIfNeeded || hasBeenExpanded)
		return super.children();

	    TreeStateNode   newNode;
	    Object          realNode = getValue();
	    TreeModel       treeModel = getModel();
	    int             count = treeModel.getChildCount(realNode);

	    hasBeenExpanded = true;

	    int    childRow = getRow();

	    if(childRow == -1) {
		for (int i = 0; i < count; i++) {
		    newNode = createNodeForValue
			(treeModel.getChild(realNode, i));
		    this.add(newNode);
		    newNode.updatePreferredSize(-1);
		}
	    }
	    else {
		childRow++;
		for (int i = 0; i < count; i++) {
		    newNode = createNodeForValue
			(treeModel.getChild(realNode, i));
		    this.add(newNode);
		    newNode.updatePreferredSize(childRow++);
		}
	    }
	    return super.children();
	}

	/**
	 * Messaged from expand and collapse. This is meant for subclassers
	 * that may wish to do something interesting with this.
	 */
	protected void didAdjustTree() {
	}

	/**
	 * Invokes <code>expandParentAndReceiver</code> on the parent,
	 * and expands the receiver.
	 */
	protected void expandParentAndReceiver() {
	    TreeStateNode       parent = (TreeStateNode)getParent();

	    if(parent != null)
		parent.expandParentAndReceiver();
	    expand();
	}

	/**
	 * Expands this node in the tree.  This will load the children
	 * from the treeModel if this node has not previously been
	 * expanded.  If <I>adjustTree</I> is true the tree and selection
	 * are updated accordingly.
	 */
	protected void expand(boolean adjustTree) {
	    if (!isExpanded() && !isLeaf()) {
		boolean         isFixed = isFixedRowHeight();
		int             startHeight = getPreferredHeight();
		int             originalRow = getRow();

		expanded = true;
		updatePreferredSize(originalRow);

		if (!hasBeenExpanded) {
		    TreeStateNode  newNode;
		    Object         realNode = getValue();
		    TreeModel      treeModel = getModel();
		    int            count = treeModel.getChildCount(realNode);

		    hasBeenExpanded = true;
		    if(originalRow == -1) {
			for (int i = 0; i < count; i++) {
			    newNode = createNodeForValue(treeModel.getChild
							    (realNode, i));
			    this.add(newNode);
			    newNode.updatePreferredSize(-1);
			}
		    }
		    else {
			int offset = originalRow + 1;
			for (int i = 0; i < count; i++) {
			    newNode = createNodeForValue(treeModel.getChild
						       (realNode, i));
			    this.add(newNode);
			    newNode.updatePreferredSize(offset);
			}
		    }
		}

		int i = originalRow;
		Enumeration cursor = preorderEnumeration();
		cursor.nextElement(); // don't add me, I'm already in

		int newYOrigin;

		if(isFixed)
		    newYOrigin = 0;
		else if(this == root && !isRootVisible())
		    newYOrigin = 0;
		else
		    newYOrigin = getYOrigin() + this.getPreferredHeight();
		TreeStateNode   aNode;
		if(!isFixed) {
		    while (cursor.hasMoreElements()) {
			aNode = (TreeStateNode)cursor.nextElement();
			if(!updateNodeSizes && !aNode.hasValidSize())
			    aNode.updatePreferredSize(i + 1);
			aNode.setYOrigin(newYOrigin);
			newYOrigin += aNode.getPreferredHeight();
			visibleNodes.insertElementAt(aNode, ++i);
		    }
		}
		else {
		    while (cursor.hasMoreElements()) {
			aNode = (TreeStateNode)cursor.nextElement();
			visibleNodes.insertElementAt(aNode, ++i);
		    }
		}

		if(adjustTree && (originalRow != i ||
				  getPreferredHeight() != startHeight)) {
		    // Adjust the Y origin of any nodes following this row.
		    if(!isFixed && ++i < getRowCount()) {
			int              counter;
			int              heightDiff = newYOrigin -
			    (getYOrigin() + getPreferredHeight()) +
			    (getPreferredHeight() - startHeight);

			for(counter = visibleNodes.size() - 1;counter >= i;
			    counter--)
			    ((TreeStateNode)visibleNodes.elementAt(counter)).
				shiftYOriginBy(heightDiff);
		    }
		    didAdjustTree();
		    visibleNodesChanged();
		}

		// Update the rows in the selection
		if(treeSelectionModel != null) {
		    treeSelectionModel.resetRowSelection();
		}
	    }
	}

	/**
	 * Collapses this node in the tree.  If <I>adjustTree</I> is
	 * true the tree and selection are updated accordingly.
	 */
	protected void collapse(boolean adjustTree) {
	    if (isExpanded()) {
		Enumeration cursor = preorderEnumeration();
		cursor.nextElement(); // don't remove me, I'm still visible
		int rowsDeleted = 0;
		boolean isFixed = isFixedRowHeight();
		int lastYEnd;
		if(isFixed)
		    lastYEnd = 0;
		else
		    lastYEnd = getPreferredHeight() + getYOrigin();
		int startHeight = getPreferredHeight();
		int startYEnd = lastYEnd;
		int myRow = getRow();

		if(!isFixed) {
		    while(cursor.hasMoreElements()) {
			TreeStateNode node = (TreeStateNode)cursor.
			    nextElement();
			if (node.isVisible()) {
			    rowsDeleted++;
			    //visibleNodes.removeElement(node);
			    lastYEnd = node.getYOrigin() +
				node.getPreferredHeight();
			}
		    }
		}
		else {
		    while(cursor.hasMoreElements()) {
			TreeStateNode node = (TreeStateNode)cursor.
			    nextElement();
			if (node.isVisible()) {
			    rowsDeleted++;
			    //visibleNodes.removeElement(node);
			}
		    }
		}

		// Clean up the visible nodes.
		for (int counter = rowsDeleted + myRow; counter > myRow;
		     counter--) {
		    visibleNodes.removeElementAt(counter);
		}

		expanded = false;

		if(myRow == -1)
		    markSizeInvalid();
		else if (adjustTree)
		    updatePreferredSize(myRow);

		if(myRow != -1 && adjustTree &&
		   (rowsDeleted > 0 || startHeight != getPreferredHeight())) {
		    // Adjust the Y origin of any rows following this one.
		    startYEnd += (getPreferredHeight() - startHeight);
		    if(!isFixed && (myRow + 1) < getRowCount() &&
		       startYEnd != lastYEnd) {
			int                 counter, maxCounter, shiftAmount;

			shiftAmount = startYEnd - lastYEnd;
			for(counter = myRow + 1, maxCounter =
				visibleNodes.size();
			    counter < maxCounter;counter++)
			    ((TreeStateNode)visibleNodes.elementAt(counter))
				.shiftYOriginBy(shiftAmount);
		    }
		    didAdjustTree();
		    visibleNodesChanged();
		}
 		if(treeSelectionModel != null && rowsDeleted > 0 &&
 		   myRow != -1) {
		    treeSelectionModel.resetRowSelection();
		}
	    }
	}

	/**
	 * Removes the receiver, and all its children, from the mapping
	 * table.
	 */
	protected void removeFromMapping() {
	    if(path != null) {
		removeMapping(this);
		for(int counter = getChildCount() - 1; counter >= 0; counter--)
		    ((TreeStateNode)getChildAt(counter)).removeFromMapping();
	    }
	}
    } // End of VariableHeightLayoutCache.TreeStateNode


    /**
     * An enumerator to iterate through visible nodes.
     */
    private class VisibleTreeStateNodeEnumeration implements
	             Enumeration<TreePath> {
	/** Parent thats children are being enumerated. */
	protected TreeStateNode       parent;
	/** Index of next child. An index of -1 signifies parent should be
	 * visibled next. */
	protected int                 nextIndex;
	/** Number of children in parent. */
	protected int                 childCount;

	protected VisibleTreeStateNodeEnumeration(TreeStateNode node) {
	    this(node, -1);
	}

	protected VisibleTreeStateNodeEnumeration(TreeStateNode parent,
						  int startIndex) {
	    this.parent = parent;
	    this.nextIndex = startIndex;
	    this.childCount = this.parent.getChildCount();
	}

	/**
	 * @return true if more visible nodes.
	 */
	public boolean hasMoreElements() {
	    return (parent != null);
	}

	/**
	 * @return next visible TreePath.
	 */
	public TreePath nextElement() {
	    if(!hasMoreElements())
		throw new NoSuchElementException("No more visible paths");

	    TreePath                retObject;

	    if(nextIndex == -1) {
		retObject = parent.getTreePath();
	    }
	    else {
		TreeStateNode   node = (TreeStateNode)parent.
		                        getChildAt(nextIndex);

		retObject = node.getTreePath();
	    }
	    updateNextObject();
	    return retObject;
	}

	/**
	 * Determines the next object by invoking <code>updateNextIndex</code>
	 * and if not succesful <code>findNextValidParent</code>.
	 */
	protected void updateNextObject() {
	    if(!updateNextIndex()) {
		findNextValidParent();
	    }
	}

	/**
	 * Finds the next valid parent, this should be called when nextIndex
	 * is beyond the number of children of the current parent.
	 */
	protected boolean findNextValidParent() {
	    if(parent == root) {
		// mark as invalid!
		parent = null;
		return false;
	    }
	    while(parent != null) {
		TreeStateNode      newParent = (TreeStateNode)parent.
		                                  getParent();

		if(newParent != null) {
		    nextIndex = newParent.getIndex(parent);
		    parent = newParent;
		    childCount = parent.getChildCount();
		    if(updateNextIndex())
			return true;
		}
		else
		    parent = null;
	    }
	    return false;
	}

	/**
	 * Updates <code>nextIndex</code> returning false if it is beyond
	 * the number of children of parent.
	 */
	protected boolean updateNextIndex() {
	    // nextIndex == -1 identifies receiver, make sure is expanded
	    // before descend.
	    if(nextIndex == -1 && !parent.isExpanded())
		return false;

	    // Check that it can have kids
	    if(childCount == 0)
		return false;
	    // Make sure next index not beyond child count.
	    else if(++nextIndex >= childCount)
		return false;

	    TreeStateNode       child = (TreeStateNode)parent.
		                        getChildAt(nextIndex);

	    if(child != null && child.isExpanded()) {
		parent = child;
		nextIndex = -1;
		childCount = child.getChildCount();
	    }
	    return true;
	}
    } // VariableHeightLayoutCache.VisibleTreeStateNodeEnumeration
}
