/*
 * Copyright (c) 2005-2010 Substance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o 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.
 *
 *  o Neither the name of Substance Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS 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 COPYRIGHT OWNER OR
 * 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.
 */
package org.pushingpixels.substance.internal.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.swing.ButtonModel;
import javax.swing.DefaultButtonModel;
import javax.swing.JComponent;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.IconUIResource;
import javax.swing.plaf.basic.BasicTreeUI;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;

import org.pushingpixels.lafwidget.LafWidgetUtilities;
import org.pushingpixels.lafwidget.utils.LookUtils;
import org.pushingpixels.substance.api.ColorSchemeAssociationKind;
import org.pushingpixels.substance.api.ComponentState;
import org.pushingpixels.substance.api.ComponentStateFacet;
import org.pushingpixels.substance.api.SubstanceColorScheme;
import org.pushingpixels.substance.api.SubstanceLookAndFeel;
import org.pushingpixels.substance.api.renderers.SubstanceDefaultTreeCellRenderer;
import org.pushingpixels.substance.internal.animation.StateTransitionMultiTracker;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker;
import org.pushingpixels.substance.internal.animation.StateTransitionTracker.RepaintCallback;
import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils;
import org.pushingpixels.substance.internal.painter.HighlightPainterUtils;
import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities;
import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils;
import org.pushingpixels.substance.internal.utils.SubstanceStripingUtils;
import org.pushingpixels.substance.internal.utils.icon.SubstanceIconFactory;
import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.callback.TimelineCallback;
import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter;

/**
 * UI for lists in <b>Substance</b> look and feel.
 * 
 * @author Kirill Grouchnikov
 */
public class SubstanceTreeUI extends BasicTreeUI {
	/**
	 * Holds the list of currently selected paths.
	 */
	protected Map<TreePathId, Object> selectedPaths;

	/**
	 * Holds the currently rolled-over path or <code>null</code> if none such.
	 */
	protected TreePathId currRolloverPathId;

	/**
	 * Listener that listens to changes on tree properties.
	 */
	protected PropertyChangeListener substancePropertyChangeListener;

	/**
	 * Listener for selection animations.
	 */
	protected TreeSelectionListener substanceSelectionFadeListener;

	/**
	 * Listener for transition animations on tree rollovers.
	 */
	protected RolloverFadeListener substanceFadeRolloverListener;

	/**
	 * Listener for selection of an entire row.
	 */
	protected MouseListener substanceRowSelectionListener;

	private StateTransitionMultiTracker<TreePathId> stateTransitionMultiTracker;

	/**
	 * The current default color scheme. Is computed in
	 * {@link #update(Graphics, JComponent)} and reused in
	 * {@link SubstanceDefaultTreeCellRenderer#getTreeCellRendererComponent(JTree, Object, boolean, boolean, boolean, int, boolean)}
	 * for performance optimizations.
	 */
	private SubstanceColorScheme currDefaultColorScheme;

	/**
	 * Cell renderer insets. Is computed in {@link #installDefaults()} and
	 * reused in
	 * {@link SubstanceDefaultTreeCellRenderer#getTreeCellRendererComponent(JTree, Object, boolean, boolean, boolean, int, boolean)}
	 * for performance optimizations.
	 */
	private Insets cellRendererInsets;

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
	 */
	public static ComponentUI createUI(JComponent comp) {
		SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp);
		return new SubstanceTreeUI();
	}

	/**
	 * Creates a UI delegate for tree.
	 */
	public SubstanceTreeUI() {
		super();
		this.selectedPaths = new HashMap<TreePathId, Object>();
		this.stateTransitionMultiTracker = new StateTransitionMultiTracker<TreePathId>();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.basic.BasicTreeUI#installDefaults()
	 */
	@Override
	protected void installDefaults() {
		super.installDefaults();
		if (SubstanceCoreUtilities.toDrawWatermark(this.tree))
			this.tree.setOpaque(false);

		if (this.tree.getSelectionPaths() != null) {
			for (TreePath selectionPath : this.tree.getSelectionPaths()) {
				TreePathId pathId = new TreePathId(selectionPath);
				selectedPaths.put(pathId, selectionPath.getLastPathComponent());
			}
		}

		setExpandedIcon(new IconUIResource(SubstanceIconFactory.getTreeIcon(
				this.tree, false)));
		setCollapsedIcon(new IconUIResource(SubstanceIconFactory.getTreeIcon(
				this.tree, true)));

		// instead of computing the cell renderer insets on
		// every cell rendering, compute it once and expose to the
		// SubstanceDefaultTreeCellRenderer
		this.cellRendererInsets = SubstanceSizeUtils
				.getTreeCellRendererInsets(SubstanceSizeUtils
						.getComponentFontSize(tree));
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.basic.BasicTreeUI#uninstallDefaults()
	 */
	@Override
	protected void uninstallDefaults() {
		this.selectedPaths.clear();
		super.uninstallDefaults();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.basic.BasicTreeUI#paintRow(java.awt.Graphics,
	 * java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle,
	 * javax.swing.tree.TreePath, int, boolean, boolean, boolean)
	 */
	@Override
	protected void paintRow(Graphics g, Rectangle clipBounds, Insets insets,
			Rectangle bounds, TreePath path, int row, boolean isExpanded,
			boolean hasBeenExpanded, boolean isLeaf) {
		// Don't paint the renderer if editing this row.
		if ((this.editingComponent != null) && (this.editingRow == row)) {
			// fix for issue 446 - paint the expand control
			// on the editing row
			if (shouldPaintExpandControl(path, row, isExpanded,
					hasBeenExpanded, isLeaf)) {
				if (!this.tree.getComponentOrientation().isLeftToRight()
						&& LookUtils.IS_JAVA_5) {
					bounds.x -= 4;
				}
				paintExpandControlEnforce(g, clipBounds, insets, bounds, path,
						row, isExpanded, hasBeenExpanded, isLeaf);
			}
		}

		int leadIndex;

		if (this.tree.hasFocus()) {
			TreePath leadPath = this.tree.getLeadSelectionPath();
			leadIndex = this.getRowForPath(this.tree, leadPath);
		} else {
			leadIndex = -1;
		}

		Component renderer = this.currentCellRenderer
				.getTreeCellRendererComponent(this.tree, path
						.getLastPathComponent(), this.tree.isRowSelected(row),
						isExpanded, isLeaf, row, (leadIndex == row));

		if (!(renderer instanceof SubstanceDefaultTreeCellRenderer)) {
			// if it's not Substance renderer - ask the Basic delegate to paint
			// it.
			super.paintRow(g, clipBounds, insets, bounds, path, row,
					isExpanded, hasBeenExpanded, isLeaf);
			if (shouldPaintExpandControl(path, row, isExpanded,
					hasBeenExpanded, isLeaf)) {
				paintExpandControlEnforce(g, clipBounds, insets, bounds, path,
						row, isExpanded, hasBeenExpanded, isLeaf);
			}
			return;
		}

		TreePathId pathId = new TreePathId(path);

		Graphics2D g2d = (Graphics2D) g.create();
		g2d.setComposite(LafWidgetUtilities.getAlphaComposite(tree, g));

		// Color background = renderer.getBackground();
		// if (background == null)
		// background = tree.getBackground();

		StateTransitionTracker.ModelStateInfo modelStateInfo = getModelStateInfo(pathId);
		Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = ((modelStateInfo == null) ? null
				: modelStateInfo.getStateContributionMap());
		ComponentState currState = ((modelStateInfo == null) ? getPathState(pathId)
				: modelStateInfo.getCurrModelState());

		// Compute the alpha values for the animation.
		boolean hasHighlights = false;
		if (renderer.isEnabled()) {
			if (activeStates != null) {
				for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> stateEntry : activeStates
						.entrySet()) {
					hasHighlights = (SubstanceColorSchemeUtilities
							.getHighlightAlpha(this.tree, stateEntry.getKey())
							* stateEntry.getValue().getContribution() > 0.0f);
					if (hasHighlights)
						break;
				}
			} else {
				hasHighlights = (SubstanceColorSchemeUtilities
						.getHighlightAlpha(this.tree, currState) > 0.0f);
			}
		}

		// System.out.println(row + ":" + prevTheme.getDisplayName() + "["
		// + alphaForPrevBackground + "]:" + currTheme.getDisplayName()
		// + "[" + alphaForCurrBackground + "]");

		// At this point the renderer is an instance of
		// SubstanceDefaultTreeCellRenderer
		JTree.DropLocation dropLocation = tree.getDropLocation();
		Rectangle rowRectangle = new Rectangle(this.tree.getInsets().left,
				bounds.y, this.tree.getWidth() - this.tree.getInsets().right
						- this.tree.getInsets().left, bounds.height);
		if (dropLocation != null && dropLocation.getChildIndex() == -1
				&& tree.getRowForPath(dropLocation.getPath()) == row) {
			// mark drop location
			SubstanceColorScheme scheme = SubstanceColorSchemeUtilities
					.getColorScheme(tree,
							ColorSchemeAssociationKind.TEXT_HIGHLIGHT,
							currState);
			SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
					.getColorScheme(tree, ColorSchemeAssociationKind.BORDER,
							currState);
			HighlightPainterUtils.paintHighlight(g2d, this.rendererPane,
					renderer, rowRectangle, 0.8f, null, scheme, borderScheme);
		} else {
			if (hasHighlights) {
				if (activeStates == null) {
					float alpha = SubstanceColorSchemeUtilities
							.getHighlightAlpha(this.tree, currState);
					if (alpha > 0.0f) {
						SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities
								.getColorScheme(this.tree,
										ColorSchemeAssociationKind.HIGHLIGHT,
										currState);
						SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
								.getColorScheme(this.tree,
										ColorSchemeAssociationKind.HIGHLIGHT,
										currState);
						g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
								this.tree, alpha, g));
						// Fix for defect 180 - painting the
						// highlight beneath the entire row
						HighlightPainterUtils.paintHighlight(g2d,
								this.rendererPane, renderer, rowRectangle,
								0.8f, null, fillScheme, borderScheme);
						g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
								this.tree, g));
					}
				} else {
					for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> stateEntry : activeStates
							.entrySet()) {
						ComponentState activeState = stateEntry.getKey();
						float alpha = SubstanceColorSchemeUtilities
								.getHighlightAlpha(this.tree, activeState)
								* stateEntry.getValue().getContribution();
						if (alpha == 0.0f)
							continue;
						SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities
								.getColorScheme(this.tree,
										ColorSchemeAssociationKind.HIGHLIGHT,
										activeState);
						SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
								.getColorScheme(this.tree,
										ColorSchemeAssociationKind.HIGHLIGHT,
										activeState);
						g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
								this.tree, alpha, g));
						// Fix for defect 180 - painting the
						// highlight beneath the entire row
						HighlightPainterUtils.paintHighlight(g2d,
								this.rendererPane, renderer, rowRectangle,
								0.8f, null, fillScheme, borderScheme);
						g2d.setComposite(LafWidgetUtilities.getAlphaComposite(
								this.tree, g));
					}
				}
			}
		}

		// System.out.println("Painting row " + row);
		// Play with opacity to make our own gradient background
		// on selected elements to show - safe to cast and set opacity
		// since at this point the renderer can only by the
		// SubstanceDefaultTreeCellRenderer
		JComponent jRenderer = (JComponent) renderer;
		boolean newOpaque = !this.tree.isRowSelected(row);
		if (SubstanceCoreUtilities.toDrawWatermark(this.tree))
			newOpaque = false;

		Map<Component, Boolean> opacity = new HashMap<Component, Boolean>();
		if (!newOpaque)
			SubstanceCoreUtilities.makeNonOpaque(jRenderer, opacity);
		this.rendererPane.paintComponent(g2d, renderer, this.tree, bounds.x,
				bounds.y, Math.max(this.tree.getWidth()
						- this.tree.getInsets().right
						- this.tree.getInsets().left - bounds.x, bounds.width),
				bounds.height, true);
		if (!newOpaque)
			SubstanceCoreUtilities.restoreOpaque(jRenderer, opacity);

		// Paint the expand control now after the row background has been
		// overlayed by the highlight background on selected and rolled over
		// rows. See comments on paintExpandControl().
		if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded,
				isLeaf)) {
			paintExpandControlEnforce(g2d, clipBounds, insets, bounds, path,
					row, isExpanded, hasBeenExpanded, isLeaf);
		}

		g2d.dispose();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * javax.swing.plaf.basic.BasicTreeUI#paintExpandControl(java.awt.Graphics,
	 * java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle,
	 * javax.swing.tree.TreePath, int, boolean, boolean, boolean)
	 */
	@Override
	protected void paintExpandControl(Graphics g, Rectangle clipBounds,
			Insets insets, Rectangle bounds, TreePath path, int row,
			boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) {
		// This does nothing. The base implementation of paint() paints
		// the tree lines and tree expand controls *before* painting the
		// renderer. In Substance, the highlights are painted in the
		// paintRow, and thus would overlay the expand controls. This results
		// in expand controls being much less visible under most of the skins.
		// So, Substance paints the expand controls *after* painting the
		// highlights (and the renderer which doesn't overlap with the expand
		// controls in any case). This is done in paintRow() by calling
		// the paintExpandControlEnforce() instead (that eventually calls the
		// super implementation of paintExpandControl().
	}

	/**
	 * Paints the expand control of the specified row.
	 *
	 * @param g
	 *            Graphics context.
	 * @param clipBounds
	 *            Clip bounds.
	 * @param insets
	 *            Insets.
	 * @param bounds
	 *            Row bounds.
	 * @param path
	 *            Tree path.
	 * @param row
	 *            Tree row.
	 * @param isExpanded
	 *            Expand indication.
	 * @param hasBeenExpanded
	 *            Indication whether this row has ever been expanded.
	 * @param isLeaf
	 *            Indication whether this row is a leaf.
	 */
	protected void paintExpandControlEnforce(Graphics g, Rectangle clipBounds,
			Insets insets, Rectangle bounds, TreePath path, int row,
			boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) {
		// boolean toPaint = (!this.tree.isEnabled()) || this.isInside;

		float alpha = SubstanceColorSchemeUtilities.getAlpha(this.tree,
				this.tree.isEnabled() ? ComponentState.ENABLED
						: ComponentState.DISABLED_UNSELECTED);

		Graphics2D graphics = (Graphics2D) g.create();
		// if (toPaint) {
		graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.tree,
				alpha, g));
		super.paintExpandControl(graphics, clipBounds, insets, bounds, path,
				row, isExpanded, hasBeenExpanded, isLeaf);
		// }
		graphics.dispose();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * javax.swing.plaf.basic.BasicTreeUI#paintHorizontalPartOfLeg(java.awt.
	 * Graphics, java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle,
	 * javax.swing.tree.TreePath, int, boolean, boolean, boolean)
	 */
	@Override
	protected void paintHorizontalPartOfLeg(Graphics g, Rectangle clipBounds,
			Insets insets, Rectangle bounds, TreePath path, int row,
			boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) {
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * javax.swing.plaf.basic.BasicTreeUI#paintVerticalPartOfLeg(java.awt.Graphics
	 * , java.awt.Rectangle, java.awt.Insets, javax.swing.tree.TreePath)
	 */
	@Override
	protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds,
			Insets insets, TreePath path) {
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.basic.BasicTreeUI#createDefaultCellRenderer()
	 */
	@Override
	protected TreeCellRenderer createDefaultCellRenderer() {
		return new SubstanceDefaultTreeCellRenderer();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.basic.BasicTreeUI#installListeners()
	 */
	@Override
	protected void installListeners() {
		super.installListeners();
		this.substancePropertyChangeListener = new PropertyChangeListener() {
			@Override
            public void propertyChange(PropertyChangeEvent evt) {
				if (SubstanceLookAndFeel.WATERMARK_VISIBLE.equals(evt
						.getPropertyName())) {
					tree.setOpaque(!SubstanceCoreUtilities
							.toDrawWatermark(tree));
				}
				if ("font".equals(evt.getPropertyName())) {
					SwingUtilities.invokeLater(new Runnable() {
						@Override
                        public void run() {
							tree.updateUI();
						}
					});
				}
				if ("dropLocation".equals(evt.getPropertyName())) {
					JTree.DropLocation oldValue = (JTree.DropLocation) evt
							.getOldValue();
					if (oldValue != null && oldValue.getPath() != null) {
						TreePath oldDrop = oldValue.getPath();
						Rectangle oldBounds = getPathBounds(tree, oldDrop);
						tree.repaint(0, oldBounds.y, tree.getWidth(),
								oldBounds.height);
					}
					JTree.DropLocation currLocation = tree.getDropLocation();
					if (currLocation != null) {
						TreePath newDrop = currLocation.getPath();
						if (newDrop != null) {
							Rectangle newBounds = getPathBounds(tree, newDrop);
							tree.repaint(0, newBounds.y, tree.getWidth(),
									newBounds.height);
						}
					}
				}
			}
		};
		this.tree
				.addPropertyChangeListener(this.substancePropertyChangeListener);

		this.substanceSelectionFadeListener = new MyTreeSelectionListener();
		this.tree.getSelectionModel().addTreeSelectionListener(
				this.substanceSelectionFadeListener);

		this.substanceRowSelectionListener = new RowSelectionListener();
		this.tree.addMouseListener(this.substanceRowSelectionListener);

		// Add listener for the fade animation
		this.substanceFadeRolloverListener = new RolloverFadeListener();
		this.tree.addMouseMotionListener(this.substanceFadeRolloverListener);
		this.tree.addMouseListener(this.substanceFadeRolloverListener);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.plaf.basic.BasicTreeUI#uninstallListeners()
	 */
	@Override
	protected void uninstallListeners() {
		this.tree.removeMouseListener(this.substanceRowSelectionListener);
		this.substanceRowSelectionListener = null;

		this.tree.getSelectionModel().removeTreeSelectionListener(
				this.substanceSelectionFadeListener);
		this.substanceSelectionFadeListener = null;

		this.tree
				.removePropertyChangeListener(this.substancePropertyChangeListener);
		this.substancePropertyChangeListener = null;

		// Remove listener for the fade animation
		this.tree.removeMouseMotionListener(this.substanceFadeRolloverListener);
		this.tree.removeMouseListener(this.substanceFadeRolloverListener);
		this.substanceFadeRolloverListener = null;

		super.uninstallListeners();
	}

	/**
	 * ID of a single tree path.
	 *
	 * @author Kirill Grouchnikov
	 */
	@SuppressWarnings("unchecked")
	public static class TreePathId implements Comparable {
		/**
		 * Tree path.
		 */
		protected TreePath path;

		/**
		 * Creates a tree path ID.
		 *
		 * @param path
		 *            Tree path.
		 */
		public TreePathId(TreePath path) {
			this.path = path;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see java.lang.Comparable#compareTo(java.lang.Object)
		 */
		@Override
        public int compareTo(Object o) {
			if (o instanceof TreePathId) {
				TreePathId otherId = (TreePathId) o;
				if ((this.path == null) && (otherId.path != null))
					return 1;
				if ((otherId.path == null) && (this.path != null))
					return -1;
				Object[] path1Objs = this.path.getPath();
				Object[] path2Objs = otherId.path.getPath();
				if (path1Objs.length != path2Objs.length)
					return 1;
				for (int i = 0; i < path1Objs.length; i++)
					if (!path1Objs[i].equals(path2Objs[i]))
						return 1;
				return 0;
			}
			return -1;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			return this.compareTo(obj) == 0;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			if (this.path == null)
				return 0;
			Object[] pathObjs = this.path.getPath();
			int result = pathObjs[0].hashCode();
			for (int i = 1; i < pathObjs.length; i++)
				result = result ^ pathObjs[i].hashCode();
			return result;
		}
	}

	/**
	 * Selection listener for selection animation effects.
	 *
	 * @author Kirill Grouchnikov
	 */
	protected class MyTreeSelectionListener implements TreeSelectionListener {
		/*
		 * (non-Javadoc)
		 *
		 * @see
		 * javax.swing.event.TreeSelectionListener#valueChanged(javax.swing.
		 * event.TreeSelectionEvent)
		 */
		@Override
        public void valueChanged(TreeSelectionEvent e) {
			// Map<TreePathId, Object> currSelected = (Map<TreePathId, Object>)
			// tree
			// .getClientProperty(SELECTED_INDICES);
			if (tree.getSelectionPaths() != null) {
				for (TreePath selectionPath : tree.getSelectionPaths()) {
					TreePathId pathId = new TreePathId(selectionPath);

					// check if was selected before
					if (!selectedPaths.containsKey(pathId)) {
						// start fading in
						StateTransitionTracker tracker = getTracker(pathId,
								(currRolloverPathId != null)
										&& pathId.equals(currRolloverPathId),
								false);
						tracker.getModel().setSelected(true);
						selectedPaths.put(pathId, selectionPath
								.getLastPathComponent());
					}
				}
			}

			for (Iterator<Map.Entry<TreePathId, Object>> it = selectedPaths
					.entrySet().iterator(); it.hasNext();) {
				Map.Entry<TreePathId, Object> entry = it.next();
				if (tree.getSelectionModel()
						.isPathSelected(entry.getKey().path))
					continue;
				// fade out for deselected path
				TreePathId pathId = entry.getKey();
				StateTransitionTracker tracker = getTracker(pathId,
						(currRolloverPathId != null)
								&& pathId.equals(currRolloverPathId), true);
				tracker.getModel().setSelected(false);
				it.remove();
			}
		}
	}

	/**
	 * Repaints a single path during the fade animation cycle.
	 *
	 * @author Kirill Grouchnikov
	 */
	protected class PathRepaintCallback extends UIThreadTimelineCallbackAdapter {
		/**
		 * Associated tree.
		 */
		protected JTree tree;

		/**
		 * Associated (animated) path.
		 */
		protected TreePath treePath;

		/**
		 * Creates a new animation repaint callback.
		 *
		 * @param tree
		 *            Associated tree.
		 * @param treePath
		 *            Associated (animated) path.
		 */
		public PathRepaintCallback(JTree tree, TreePath treePath) {
			super();
			this.tree = tree;
			this.treePath = treePath;
		}

		@Override
		public void onTimelinePulse(float durationFraction,
				float timelinePosition) {
			this.repaintPath();
		}

		@Override
		public void onTimelineStateChanged(TimelineState oldState,
				TimelineState newState, float durationFraction,
				float timelinePosition) {
			this.repaintPath();
		}

		/**
		 * Repaints the associated path.
		 */
		private void repaintPath() {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
                public void run() {
					if (SubstanceTreeUI.this.tree == null) {
						// may happen if the LAF was switched in the meantime
						return;
					}

					Rectangle boundsBuffer = new Rectangle();
					Rectangle bounds = treeState.getBounds(treePath,
							boundsBuffer);

					if (bounds != null) {
						// still visible

						// fix for defect 180 - refresh the entire row
						bounds.x = 0;
						bounds.width = tree.getWidth();

						// fix for defect 188 - rollover effects for trees
						// with insets
						Insets insets = tree.getInsets();
						bounds.x += insets.left;
						bounds.y += insets.top;

						tree.repaint(bounds);
					}
				}
			});
		}
	}

	/**
	 * Listener for rollover animation effects.
	 *
	 * @author Kirill Grouchnikov
	 */
	private class RolloverFadeListener implements MouseListener,
			MouseMotionListener {

		@Override
        public void mouseClicked(MouseEvent e) {
		}

		@Override
        public void mouseEntered(MouseEvent e) {
			if (!tree.isEnabled())
				return;
			// isInside = true;
		}

		@Override
        public void mousePressed(MouseEvent e) {
		}

		@Override
        public void mouseReleased(MouseEvent e) {
		}

		@Override
        public void mouseExited(MouseEvent e) {
			if (!tree.isEnabled())
				return;
			// isInside = false;
			this.fadeOut();
			// System.out.println("Nulling RO index");
			currRolloverPathId = null;
		}

		@Override
        public void mouseMoved(MouseEvent e) {
			if (!tree.isEnabled())
				return;
			// isInside = true;
			handleMove(e);
		}

		@Override
        public void mouseDragged(MouseEvent e) {
			if (!tree.isEnabled())
				return;
			handleMove(e);
		}

		/**
		 * Handles various mouse move events and initiates the fade animation if
		 * necessary.
		 *
		 * @param e
		 *            Mouse event.
		 */
		private void handleMove(MouseEvent e) {
			TreePath closestPath = tree.getClosestPathForLocation(e.getX(), e
					.getY());
			Rectangle bounds = tree.getPathBounds(closestPath);
			if (bounds == null) {
				this.fadeOut();
				currRolloverPathId = null;
				return;
			}
			if ((e.getY() < bounds.y)
					|| (e.getY() > (bounds.y + bounds.height))) {
				this.fadeOut();
				currRolloverPathId = null;
				return;
			}
			// check if this is the same index
			TreePathId newPathId = new TreePathId(closestPath);
			if ((currRolloverPathId != null)
					&& newPathId.equals(currRolloverPathId)) {
				// System.out.println("Same location " +
				// System.currentTimeMillis());
				// System.out.print("Current : ");
				// for (Object o1 : currPathId.path.getPath()) {
				// System.out.print(o1);
				// }
				// System.out.println("");
				// System.out.print("Closest : ");
				// for (Object o2 : newPathId.path.getPath()) {
				// System.out.print(o2);
				// }
				// System.out.println("");
				return;
			}

			this.fadeOut();

			StateTransitionTracker tracker = getTracker(newPathId, false,
					selectedPaths.containsKey(newPathId));
			tracker.getModel().setRollover(true);

			currRolloverPathId = newPathId;
		}

		/**
		 * Initiates the fade out effect.
		 */
		private void fadeOut() {
			if (currRolloverPathId == null)
				return;

			StateTransitionTracker tracker = getTracker(currRolloverPathId,
					true, selectedPaths.containsKey(currRolloverPathId));
			tracker.getModel().setRollover(false);
		}
	}

	/**
	 * Listener for selecting the entire rows.
	 *
	 * @author Kirill Grouchnikov
	 */
	private class RowSelectionListener extends MouseAdapter {
		/*
		 * (non-Javadoc)
		 *
		 * @see
		 * java.awt.event.MouseAdapter#mousePressed(java.awt.event.MouseEvent)
		 */
		@Override
		public void mousePressed(MouseEvent e) {
			if (!tree.isEnabled())
				return;
			TreePath closestPath = tree.getClosestPathForLocation(e.getX(), e
					.getY());
			if (closestPath == null)
				return;
			Rectangle bounds = tree.getPathBounds(closestPath);
			// Process events outside the immediate bounds - fix for defect
			// 19 on substance-netbeans. This properly handles Ctrl and Shift
			// selections on trees.
			if ((e.getY() >= bounds.y)
					&& (e.getY() < (bounds.y + bounds.height))
					&& ((e.getX() < bounds.x) || (e.getX() > (bounds.x + bounds.width)))) {
				// tree.setSelectionPath(closestPath);

				// fix - don't select a node if the click was on the
				// expand control
				if (isLocationInExpandControl(closestPath, e.getX(), e.getY()))
					return;
				selectPathForEvent(closestPath, e);
			}
		}
	}

	/**
	 * Returns the pivot X for the cells rendered in the specified area. Used
	 * for the smart tree scroll (
	 * {@link SubstanceLookAndFeel#TREE_SMART_SCROLL_ANIMATION_KIND}).
	 * 
	 * @param paintBounds
	 *            Area bounds.
	 * @return Pivot X for the cells rendered in the specified area
	 */
	public int getPivotRendererX(Rectangle paintBounds) {
		TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y);
		Enumeration<?> paintingEnumerator = treeState
				.getVisiblePathsFrom(initialPath);
		int endY = paintBounds.y + paintBounds.height;

		int totalY = 0;
		int count = 0;

		if (initialPath != null && paintingEnumerator != null) {
			boolean done = false;
			Rectangle boundsBuffer = new Rectangle();
			Rectangle bounds;
			TreePath path;
			Insets insets = tree.getInsets();

			while (!done && paintingEnumerator.hasMoreElements()) {
				path = (TreePath) paintingEnumerator.nextElement();
				if (path != null) {
					bounds = treeState.getBounds(path, boundsBuffer);
					bounds.x += insets.left;
					bounds.y += insets.top;

					int currMedianX = bounds.x;// + bounds.width / 2;
					totalY += currMedianX;
					count++;
					if ((bounds.y + bounds.height) >= endY)
						done = true;
				} else {
					done = true;
				}
			}
		}
		if (count == 0)
			return -1;
		return totalY
				/ count
				- 2
				* SubstanceSizeUtils.getTreeIconSize(SubstanceSizeUtils
						.getComponentFontSize(tree));
	}

	/**
	 * Returns the current state for the specified path.
	 * 
	 * @param pathId
	 *            Path index.
	 * @return The current state for the specified path.
	 */
	public ComponentState getPathState(TreePathId pathId) {
		boolean isEnabled = this.tree.isEnabled();
		StateTransitionTracker tracker = this.stateTransitionMultiTracker
				.getTracker(pathId);
		if (tracker == null) {
			int rowIndex = this.tree.getRowForPath(pathId.path);
			boolean isRollover = (this.currRolloverPathId != null)
					&& pathId.equals(this.currRolloverPathId);
			boolean isSelected = this.tree.isRowSelected(rowIndex);
			return ComponentState.getState(isEnabled, isRollover, isSelected);
		} else {
			ComponentState fromTracker = tracker.getModelStateInfo()
					.getCurrModelState();
			return ComponentState.getState(isEnabled, fromTracker
					.isFacetActive(ComponentStateFacet.ROLLOVER), fromTracker
					.isFacetActive(ComponentStateFacet.SELECTION));
		}
	}

	public StateTransitionTracker.ModelStateInfo getModelStateInfo(
			TreePathId pathId) {
		if (this.stateTransitionMultiTracker.size() == 0)
			return null;
		StateTransitionTracker tracker = this.stateTransitionMultiTracker
				.getTracker(pathId);
		if (tracker == null) {
			return null;
		} else {
			return tracker.getModelStateInfo();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#update(java.awt.Graphics,
	 * javax.swing.JComponent)
	 */
	@Override
	public void update(Graphics g, JComponent c) {
		BackgroundPaintingUtils.updateIfOpaque(g, c);

		// Should never happen if installed for a UI
		if (treeState == null) {
			return;
		}

		// compute the default color scheme - to optimize the performance
		// SubstanceColorScheme scheme = SubstanceColorSchemeUtilities
		// .getColorScheme(this.tree,
		// this.tree.isEnabled() ? ComponentState.DEFAULT
		// : ComponentState.DISABLED_UNSELECTED);
		// this.currHashColor = scheme.getLineColor();
		this.currDefaultColorScheme = SubstanceColorSchemeUtilities
				.getColorScheme(tree, ComponentState.ENABLED);

		Rectangle paintBounds = g.getClipBounds();
		Insets insets = tree.getInsets();

		TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y);
		Enumeration<?> paintingEnumerator = treeState
				.getVisiblePathsFrom(initialPath);
		int row = treeState.getRowForPath(initialPath);
		int endY = paintBounds.y + paintBounds.height;

		// second part - fix for defect 214 (rollover effects on non-opaque
		// trees resulted in inconsistent behaviour)
		boolean isWatermarkBleed = SubstanceCoreUtilities.toDrawWatermark(tree)
				|| !tree.isOpaque();

		Graphics2D g2d = (Graphics2D) g.create();

		SubstanceStripingUtils.setup(c);
		if (initialPath != null && paintingEnumerator != null) {
			boolean done = false;
			Rectangle boundsBuffer = new Rectangle();
			Rectangle bounds;
			TreePath path;

			while (!done && paintingEnumerator.hasMoreElements()) {
				path = (TreePath) paintingEnumerator.nextElement();
				if (path != null) {
					// respect the background color of the renderer.
					boolean isLeaf = treeModel.isLeaf(path
							.getLastPathComponent());
					boolean isExpanded = isLeaf ? false : treeState
							.getExpandedState(path);
					Component renderer = this.currentCellRenderer
							.getTreeCellRendererComponent(this.tree, path
									.getLastPathComponent(), this.tree
									.isRowSelected(row), isExpanded, isLeaf,
									row, tree.hasFocus() ? (tree
											.getLeadSelectionRow() == row)
											: false);
					Color background = renderer.getBackground();
					if (background == null)
						background = tree.getBackground();
					bounds = treeState.getBounds(path, boundsBuffer);
					if(bounds != null){
						bounds.x += insets.left;
						bounds.y += insets.top;
						if (!isWatermarkBleed) {
							g2d.setColor(background);
							g2d.fillRect(paintBounds.x, bounds.y,
									paintBounds.width, bounds.height);
						} else {
							if (this.tree.getComponentOrientation().isLeftToRight()) {
								BackgroundPaintingUtils.fillAndWatermark(g2d,
										this.tree, background, new Rectangle(
												paintBounds.x, bounds.y,
												paintBounds.width, bounds.height));
							} else {
								BackgroundPaintingUtils.fillAndWatermark(g2d,
										this.tree, background, new Rectangle(
												paintBounds.x, bounds.y,
												paintBounds.width, bounds.height));
							}
						}
						if ((bounds.y + bounds.height) >= endY)
							done = true;
					}
				} else {
					done = true;
				}
				row++;
			}
		}

		this.paint(g2d, c);
		SubstanceStripingUtils.tearDown(c);
		g2d.dispose();
	}

	// /*
	// * (non-Javadoc)
	// *
	// * @see javax.swing.plaf.basic.BasicTreeUI#getHashColor()
	// */
	// @Override
	// protected Color getHashColor() {
	// return this.currHashColor;
	// }

	/**
	 * Returns the default color scheme of this tree. Is for internal use only.
	 * 
	 * @return The default color scheme of this tree.
	 */
	public SubstanceColorScheme getDefaultColorScheme() {
		return this.currDefaultColorScheme;
	}

	/**
	 * Returns the cell renderer insets of this tree. Is for internal use only.
	 * 
	 * @return The cell renderer insets of this tree.
	 */
	public Insets getCellRendererInsets() {
		return cellRendererInsets;
	}

	@Override
	public Rectangle getPathBounds(JTree tree, TreePath path) {
		Rectangle result = super.getPathBounds(tree, path);
		if (result != null) {
			if (!tree.getComponentOrientation().isLeftToRight()) {
				int delta = result.x - tree.getInsets().left;
				result.x -= delta;
				result.width += delta;
			}
		}
		return result;
	}

	private StateTransitionTracker getTracker(final TreePathId pathId,
			boolean initialRollover, boolean initialSelected) {
		StateTransitionTracker tracker = stateTransitionMultiTracker
				.getTracker(pathId);
		if (tracker == null) {
			ButtonModel model = new DefaultButtonModel();
			model.setSelected(initialSelected);
			model.setRollover(initialRollover);
			tracker = new StateTransitionTracker(this.tree, model);
			tracker.registerModelListeners();
			tracker.setRepaintCallback(new RepaintCallback() {
				@Override
				public TimelineCallback getRepaintCallback() {
					return new PathRepaintCallback(tree, pathId.path);
				}
			});
			stateTransitionMultiTracker.addTracker(pathId, tracker);
		}
		return tracker;
	}

	public StateTransitionTracker getStateTransitionTracker(TreePathId pathId) {
		return this.stateTransitionMultiTracker.getTracker(pathId);
	}

}
