/*
 * Copyright (c) 2005-2007 Laf-Widget 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 Laf-Widget 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.jvnet.lafwidget.utils;

import java.awt.Component;
import java.awt.Container;
import java.util.*;

import javax.swing.SwingUtilities;

import org.jvnet.lafwidget.LafWidgetUtilities;
import org.jvnet.lafwidget.utils.LafConstants.AnimationKind;

/**
 * Tracker class for fade animations.
 * 
 * @author Kirill Grouchnikov
 */
public class FadeTracker {
	/**
	 * Max value for fade cycle.
	 */
	public static final int END_VALUE = 10;

	/**
	 * Single instance of <code>this</code> class.
	 */
	private static FadeTracker instance;

	/**
	 * All currently tracked components. Only components that are being animated
	 * (fade-in or fade-out) are in <code>this</code> map. Key is
	 * {@link FadeKind}, value is map from {@link ComponentId} to
	 * {@link FadeState}.
	 */
	private Map<FadeKind, Map<ComponentId, FadeState>> trackedComponents;

	/**
	 * All currently executing fade instances. Key is {@link Long} (corresponds
	 * to the {@link FadeState#id}, value is {@link FadeKind} - which is a key
	 * in {@link #trackedComponents}.
	 */
	private Map<Long, FadeKind> fadeInstances;

	/**
	 * ID of the current loop iteration.
	 */
	private long currLoopId;

	/**
	 * Fade animation kind.
	 * 
	 * @author Kirill Grouchnikov.
	 */
	public static class FadeKind {
		/**
		 * Fade kind ID.
		 */
		protected String id;

		/**
		 * Creates a new fade kind which is allowed by default.
		 * 
		 * @param id
		 *            Fade kind ID.
		 */
		public FadeKind(String id) {
			this(id, true);
		}

		/**
		 * Creates a new fade kind.
		 * 
		 * @param id
		 *            Fade kind ID.
		 * @param isDefaultAllowed
		 *            Indicates whether the newly create fade kind is allowed by
		 *            default.
		 */
		public FadeKind(String id, boolean isDefaultAllowed) {
			this.id = id;
			if (isDefaultAllowed) {
				FadeConfigurationManager.getInstance().allowFades(this);
			}
		}

		/**
		 * Arming a component.
		 */
		public static final FadeKind ARM = new FadeKind("lafwidgets.core.arm");

		/**
		 * Pressing a component.
		 */
		public static final FadeKind PRESS = new FadeKind(
				"lafwidgets.core.press");

		/**
		 * Focusing a component.
		 */
		public static final FadeKind FOCUS = new FadeKind(
				"lafwidgets.core.focus");

		/**
		 * <p>
		 * Fade kind for focus loop animation. Disabled by default, use
		 * {@link FadeConfigurationManager#allowFades(FadeKind)} to enable.
		 * </p>
		 * 
		 * @since version 3.0
		 */
		public static final FadeKind FOCUS_LOOP_ANIMATION = new FadeKind(
				"lafwidgets.core.focusLoopAnimation", false);

		/**
		 * Enabling a component.
		 */
		public static final FadeKind ENABLE = new FadeKind(
				"lafwidgets.core.enable");

		/**
		 * Rollover a component.
		 */
		public static final FadeKind ROLLOVER = new FadeKind(
				"lafwidgets.core.rollover");

		/**
		 * Selecting a component.
		 */
		public static final FadeKind SELECTION = new FadeKind(
				"lafwidgets.core.selection");

		/**
		 * Fade kind that specifies the "ghosting image" effects on button icons
		 * when the button is rolled-over. Disabled by default, use
		 * {@link FadeConfigurationManager#allowFades(FadeKind)} to enable.
		 */
		public static final FadeKind GHOSTING_ICON_ROLLOVER = new FadeKind(
				"lafwidgets.core.ghosting.iconRollover", false);

		/**
		 * Fade kind that specifies the "ghosting image" effects on buttons when
		 * the button is pressed. Disabled by default, use
		 * {@link FadeConfigurationManager#allowFades(FadeKind)} to enable.
		 */
		public static final FadeKind GHOSTING_BUTTON_PRESS = new FadeKind(
				"lafwidgets.core.ghosting.buttonPress", false);

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#hashCode()
		 */
		public int hashCode() {
			return this.id.hashCode();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		public boolean equals(Object obj) {
			if (obj instanceof FadeKind) {
				return this.id.equals(((FadeKind) obj).id);
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			return this.id;
		}

		// /**
		// * Marking a component as modified.
		// */
		// public static final FadeKind MARKED_MODIFIED = new FadeKind(
		// "lafwidgets.core.arm");
	}

	/**
	 * Information on a single component under fade.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected static class ComponentId {
		/**
		 * UI component itself.
		 */
		public Component component;

		/**
		 * ID to distinguish between different sub-components of the UI
		 * component. For example, the tabbed pane uses this field to make
		 * tab-specific animations.
		 */
		// public int id;
		public Comparable id;

		/**
		 * Creates a new component ID.
		 * 
		 * @param component
		 *            UI component itself.
		 * @param id
		 *            ID to distinguish between different sub-components of the
		 *            UI component.
		 */
		public ComponentId(Component component, Comparable id) {
			this.component = component;
			this.id = id;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#hashCode()
		 */
		public int hashCode() {
			int result = this.component.hashCode();
			if (this.id != null)
				result &= (this.id.hashCode());
			return result;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		public boolean equals(Object obj) {
			if (obj instanceof ComponentId) {
				ComponentId cid = (ComponentId) obj;
				try {
					boolean result = (this.component == cid.component);
					if (this.id == null) {
						result = result && (cid.id == null);
					} else {
						result = result && (this.id.compareTo(cid.id) == 0);
					}
					return result;
				} catch (Exception exc) {
					return false;
				}
			}
			return false;
		}
	}

	/**
	 * Callback for the fade tracker. Is used when the application (some UI
	 * delegate) wishes to execute some code on the fade.
	 * 
	 * @author Kirill Grouchnikov
	 */
	public static interface FadeTrackerCallback {
		/**
		 * Indicates that a single cycle of fade has been performed. Can be used
		 * in order to repaint specific portion of the component.
		 * 
		 * @param fadeKind
		 *            Fade animation kind.
		 * @param fadeCycle10
		 *            The current fade cycle. Is guaranteed to be in 0.0-10.0
		 *            range.
		 */
		public void fadePerformed(FadeKind fadeKind, float fadeCycle10);

		/**
		 * Indicates that the fade sequence has ended. Can be used in order to
		 * repaint specific portion of the component.
		 * 
		 * @param fadeKind
		 *            Fade animation kind.
		 */
		public void fadeEnded(FadeKind fadeKind);
	}

	/**
	 * Information on the state of the faded component.
	 * 
	 * @author Kirill Grouchnikov
	 */
	private static class FadeState {
		/**
		 * Is used to create unique value for the {@link #id} field.
		 */
		protected static long counter;

		/**
		 * Unique ID.
		 */
		protected long id;

		/**
		 * Fade cycle.
		 */
		public float fadePosition;

		/**
		 * Indication whether it's a fade-in or fade-out.
		 */
		public boolean isFadingIn;

		/**
		 * Indication whether it's looping (infinite).
		 */
		public boolean isLooping;

		/**
		 * Indication whether the looping fade starts fading out after reaching
		 * the maximum value. Relevant only when {@link #isLooping} is
		 * <code>true</code>.
		 */
		public boolean isLoopingReverse;

		/**
		 * Indication whether the looping fade should stop at reaching the end
		 * of the fade-out cycle. Relevant only when {@link #isLooping} is
		 * <code>true</code>.
		 */
		public boolean toStopAtCycleBreak;

		/**
		 * Indication whether the component parent should be repainted on each
		 * fade cycle.
		 */
		public boolean toRepaintParent;

		/**
		 * The optional callback to be called on each fade cycle.
		 */
		public FadeTrackerCallback callback;

		/**
		 * Fade animation step.
		 */
		public FadeStep fadeStep;

		/**
		 * Fade animation kind.
		 */
		public FadeKind fadeKind;

		/**
		 * Simple constructor.
		 * 
		 * @param fadeKind
		 *            Fade animation kind.
		 * @param fadePosition
		 *            Fade cycle.
		 * @param isFadingIn
		 *            Indication whether it's a fade-in or fade-out.
		 * @param toRepaintParent
		 *            Indication whether the component parent should be
		 *            repainted on each fade cycle.
		 */
		public FadeState(FadeKind fadeKind, float fadePosition,
				boolean isFadingIn, boolean toRepaintParent) {
			super();
			this.fadeKind = fadeKind;
			this.fadePosition = fadePosition;
			this.isFadingIn = isFadingIn;
			this.toRepaintParent = toRepaintParent;
			this.id = FadeState.getId();
			this.isLooping = false;
			this.toStopAtCycleBreak = false;
		}

		/**
		 * Sets the looping indication.
		 * 
		 * @param isLooping
		 *            New value for the looping indication.
		 */
		public void setLooping(boolean isLooping) {
			this.isLooping = isLooping;
		}

		/**
		 * Sets indication that looping fade should stop at the end of the
		 * fade-out cycle.
		 */
		public void toStopAtCycleBreak() {
			if (!this.isLooping)
				throw new IllegalArgumentException(
						"Supported only on looping fades");
			this.toStopAtCycleBreak = true;
		}

		/**
		 * Returns a unique ID.
		 * 
		 * @return Unique ID.
		 */
		protected static synchronized long getId() {
			return counter++;
		}
	}

	// private static List<JComponent>[] trackerValues = initializeLists();
	//	
	// static List<JComponent>[]

	/**
	 * The fade thread.
	 */
	private FadeTrackerThread trackerThread;

	/**
	 * The fade thread class.
	 * 
	 * @author Kirill Grouchnikov
	 */
	private class FadeTrackerThread extends TrackableThread {
		/**
		 * Fade thread delay.
		 */
		public static final int DELAY = 40;

		/**
		 * Indication whether a stop request has been issued on
		 * <code>this</code> thread.
		 */
		private boolean stopRequested;

		/**
		 * Simple constructor. Defined private for singleton.
		 * 
		 * @see #getInstance()
		 */
		public FadeTrackerThread() {
			super();
			this.setName("Laf-Widget fade tracker");
			this.stopRequested = false;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Thread#run()
		 */
		public void run() {
			while (!this.stopRequested) {
				try {
					Thread.sleep(FadeTrackerThread.DELAY);
				} catch (InterruptedException ie) {
					ie.printStackTrace();
				}
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						FadeTracker.this.updateComponents();
						currLoopId++;
					}
				});
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.jvnet.substance.utils.SubstanceThread#requestStop()
		 */
		protected void requestStop() {
			this.stopRequested = true;
			FadeTracker.this.trackerThread = null;
		}
	}

	/**
	 * Simple constructor. Defined private for singleton.
	 * 
	 * @see #getInstance()
	 */
	private FadeTracker() {
		this.trackerThread = this.getThread();
		this.trackedComponents = new HashMap<FadeKind, Map<ComponentId, FadeState>>();
		this.fadeInstances = new HashMap<Long, FadeKind>();
		this.currLoopId = 0;

		// FadeConfigurationManager configManager = FadeConfigurationManager
		// .getInstance();
		// configManager.allowFades(FadeKind.ARM);
		// configManager.allowFades(FadeKind.ENABLE);
		// configManager.allowFades(FadeKind.FOCUS);
		// configManager.allowFades(FadeKind.PRESS);
		// configManager.allowFades(FadeKind.ROLLOVER);
		// configManager.allowFades(FadeKind.SELECTION);
		// this.sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss.SSS");
		// this.int2comp = new ArrayList<Set<JComponent>>();
	}

	/**
	 * Gets singleton instance.
	 * 
	 * @return Singleton instance.
	 */
	public synchronized static FadeTracker getInstance() {
		if (FadeTracker.instance == null) {
			FadeTracker.instance = new FadeTracker();
		}
		return FadeTracker.instance;
	}

	/**
	 * Updates all components that are currently registered with
	 * <code>this</code> tracker. The update include repaint (based on the
	 * {@link FadeState#toRepaintParent}) and optional call to the callback.
	 */
	private synchronized void updateComponents() {
		// if (this.comp2int.size() > 0)
		// System.out.println("Periodic update");
		Set<Container> parents = new HashSet<Container>();
		for (Iterator<Map.Entry<FadeKind, Map<ComponentId, FadeState>>> itComp = this.trackedComponents
				.entrySet().iterator(); itComp.hasNext();) {
			Map.Entry<FadeKind, Map<ComponentId, FadeState>> entryComp = itComp
					.next();
			// FadeKind fadeKind = entryComp.getKey();
			Map<ComponentId, FadeState> mapComp = entryComp.getValue();
			for (Iterator<Map.Entry<ComponentId, FadeState>> itKind = mapComp
					.entrySet().iterator(); itKind.hasNext();) {
				Map.Entry<ComponentId, FadeState> entryKind = itKind.next();
				ComponentId compId = entryKind.getKey();
				FadeState state = entryKind.getValue();
				boolean hasEnded = false;
				// Component comp = entry.getKey();
				if (state.isFadingIn) {
					state.fadePosition += state.fadeStep.getNextStep(
							state.fadeKind, state.fadePosition,
							state.isFadingIn, state.isLooping);
					if (state.fadePosition > FadeTracker.END_VALUE) {
						state.fadePosition = FadeTracker.END_VALUE;
						if (state.isLooping) {
							if (state.isLoopingReverse) {
								state.isFadingIn = false;
							} else {
								state.fadePosition = 0.0f;
								if (state.toStopAtCycleBreak) {
									this.fadeInstances
											.remove(new Long(state.id));
									hasEnded = true;
									itKind.remove();
								}
							}
						} else {
							this.fadeInstances.remove(new Long(state.id));
							hasEnded = true;
							itKind.remove();
						}
					}
				} else {
					state.fadePosition -= state.fadeStep.getNextStep(
							state.fadeKind, state.fadePosition,
							state.isFadingIn, state.isLooping);
					if (state.fadePosition < 0) {
						if (state.isLooping && !state.toStopAtCycleBreak) {
							state.fadePosition = 0.0f;
							state.isFadingIn = true;
						} else {
							this.fadeInstances.remove(new Long(state.id));
							hasEnded = true;
							itKind.remove();
						}
					}
				}
				if (compId.component.isDisplayable()) {
					if (state.callback != null) {
						if (hasEnded) {
							state.callback.fadeEnded(state.fadeKind);
						} else {
							state.callback.fadePerformed(state.fadeKind,
									state.fadePosition);
						}
						continue;
					}
					if (state.toRepaintParent)
						parents.add(compId.component.getParent());
					else
						compId.component.repaint();
				}
			}
			if (mapComp.size() == 0)
				itComp.remove();
			// System.out.println("\tState of " + comp.hashCode() + " is "
			// + state.fadePosition + " fading "
			// + (state.isFadingIn ? "in" : "out"));
			// long time0 = System.nanoTime();

			// long time1 = System.nanoTime();
			// if (entry.getKey() instanceof JButton) {
			// System.out.println(((JButton) entry.getKey()).getText() + " [" +
			// state.isFadingIn + "]: "
			// + (time1 - time0));
			// }
		}
		for (Iterator<Container> itParent = parents.iterator(); itParent
				.hasNext();) {
			Container parent = itParent.next();
			parent.repaint();
		}
	}

	/**
	 * Returns the state of the component under the specified fade kind.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes
	 *            (where fade is performed on component "sub" parts).
	 * @return Component state under the specified fade kind.
	 */
	private synchronized FadeState getState(FadeKind fadeKind, Component comp,
			Comparable componentId) {
		ComponentId cid = new ComponentId(comp, componentId);
		Map<ComponentId, FadeState> map = this.trackedComponents.get(fadeKind);
		if (map == null) {
			map = new HashMap<ComponentId, FadeState>();
			this.trackedComponents.put(fadeKind, map);
		}
		return map.get(cid);
	}

	/**
	 * Adds new fade state for the specified component.
	 * 
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes
	 *            (where fade is performed on component "sub" parts).
	 * @param fadeKind
	 *            Fade kind.
	 * @param fadeState
	 *            Fade state object.
	 */
	private synchronized void addState(Component comp, Comparable componentId,
			FadeKind fadeKind, FadeState fadeState) {
		ComponentId cid = new ComponentId(comp, componentId);
		Map<ComponentId, FadeState> map = this.trackedComponents.get(fadeKind);
		if (map == null) {
			map = new HashMap<ComponentId, FadeState>();
			this.trackedComponents.put(fadeKind, map);
		}
		map.put(cid, fadeState);
		this.fadeInstances.put(fadeState.id, fadeKind);
	}

	/**
	 * Requests start of fade-in tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeIn(FadeKind fadeKind, Component comp,
			boolean toRepaintParent, FadeTrackerCallback callback) {
		return this
				.trackFadeIn(fadeKind, comp, null, toRepaintParent, callback);
	}

	/**
	 * Requests start of fade-in tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes
	 *            (where fade is performed on component "sub" parts).
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeIn(FadeKind fadeKind, Component comp,
			int componentId, boolean toRepaintParent,
			FadeTrackerCallback callback) {
		return this.trackFadeIn(fadeKind, comp, new Integer(componentId),
				toRepaintParent, callback);
	}

	/**
	 * Requests start of fade-in tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeIn(FadeKind fadeKind, Component comp,
			Comparable componentId, boolean toRepaintParent,
			FadeTrackerCallback callback) {
		return this.trackFadeIn(fadeKind, comp, componentId, toRepaintParent,
				callback, LafWidgetUtilities.getAnimationKind(comp));
	}

	/**
	 * Requests start of fade-in tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @param aKind
	 *            Animation kind. Should not be <code>null</code>.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeIn(FadeKind fadeKind, Component comp,
			Comparable componentId, boolean toRepaintParent,
			FadeTrackerCallback callback, AnimationKind aKind) {
//		System.out.println(fadeKind.toString() + " on "
//				+ comp.getClass().getName());
		if (fadeKind == null) {
			throw new IllegalArgumentException("Can't pass null fadeKind");
		}

		if ((comp == null) || (!comp.isDisplayable())) {
			if (callback != null) {
				callback.fadeEnded(fadeKind);
			}
			return -1;
		}

		if (LafWidgetUtilities.hasNoFades(comp, fadeKind)) {
			if (callback != null) {
				callback.fadeEnded(fadeKind);
			} else {
				if (toRepaintParent && comp.getParent() != null)
					comp.getParent().repaint();
				else
					comp.repaint();
			}
			return -1;
		}

		this.getThread();

		// see if it's already tracked
		FadeState fadeState = this.getState(fadeKind, comp, componentId);
		if (fadeState == null) {
			fadeState = new FadeState(fadeKind, 0, true, toRepaintParent);
			fadeState.fadeStep = aKind.getStep();
			if (fadeKind == FadeKind.SELECTION) {
				fadeState.fadeStep = new FadeAccelerationStep(
						fadeState.fadeStep, 2.0f);
			}
			this.addState(comp, componentId, fadeKind, fadeState);
		} else {
			fadeState.isFadingIn = true;
		}
		fadeState.callback = callback;
		return fadeState.id;
		// System.out.println(sdf.format(new Date())
		// + " : starting tracking fade in on "
		// + comp.getClass().getName() + " [" + comp.hashCode()
		// + "] from " + fadeState.fadePosition);
	}

	/**
	 * Requests start of fade-out tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeOut(FadeKind fadeKind, Component comp,
			boolean toRepaintParent, FadeTrackerCallback callback) {
		return this.trackFadeOut(fadeKind, comp, null, toRepaintParent,
				callback);
	}

	/**
	 * Requests start of fade-out tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes
	 *            (where fade is performed on component "sub" parts).
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeOut(FadeKind fadeKind, Component comp,
			int componentId, boolean toRepaintParent,
			FadeTrackerCallback callback) {
		return this.trackFadeOut(fadeKind, comp, new Integer(componentId),
				toRepaintParent, callback);
	}

	/**
	 * Requests start of fade-out tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeOut(FadeKind fadeKind, Component comp,
			Comparable componentId, boolean toRepaintParent,
			FadeTrackerCallback callback) {
		return this.trackFadeOut(fadeKind, comp, componentId, toRepaintParent,
				callback, LafWidgetUtilities.getAnimationKind(comp));
	}

	/**
	 * Requests start of fade-out tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @param aKind
	 *            Animation kind. Should not be <code>null</code>.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeOut(FadeKind fadeKind, Component comp,
			Comparable componentId, boolean toRepaintParent,
			FadeTrackerCallback callback, AnimationKind aKind) {
		if (fadeKind == null) {
			throw new IllegalArgumentException("Can't pass null fadeKind");
		}

		if ((comp == null) || (!comp.isDisplayable())) {
			if (callback != null) {
				callback.fadeEnded(fadeKind);
			}
			return -1;
		}

		if (LafWidgetUtilities.hasNoFades(comp, fadeKind)) {
			if (callback != null) {
				callback.fadeEnded(fadeKind);
			} else {
				if (toRepaintParent && comp.getParent() != null)
					comp.getParent().repaint();
				else
					comp.repaint();
			}
			return -1;
		}

		this.getThread();

		// see if it's already tracked
		FadeState fadeState = this.getState(fadeKind, comp, componentId);
		if (fadeState == null) {
			fadeState = new FadeState(fadeKind, FadeTracker.END_VALUE, false,
					toRepaintParent);
			fadeState.fadeStep = aKind.getStep();
			if (fadeKind == FadeKind.SELECTION)
				fadeState.fadeStep = new FadeAccelerationStep(
						fadeState.fadeStep, 2.0f);
			this.addState(comp, componentId, fadeKind, fadeState);
		} else {
			fadeState.isFadingIn = false;
		}
		fadeState.callback = callback;
		return fadeState.id;
		// System.out.println(sdf.format(new Date())
		// + " : starting tracking fade out on "
		// + comp.getClass().getName() + " [" + comp.hashCode()
		// + "] from " + fadeState.fadePosition);
	}

	/**
	 * Requests start of fade tracking on the specified component.
	 * 
	 * @param comp
	 *            The component to track.
	 * @param fadeKind
	 *            Fade kind.
	 * @param isFadeIn
	 *            Indication whether fade-in or fade-out should be commenced.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFade(Component comp, FadeKind fadeKind,
			boolean isFadeIn, boolean toRepaintParent) {
		return this.trackFade(comp, fadeKind, isFadeIn, toRepaintParent, null);
	}

	/**
	 * Requests start of fade tracking on the specified component.
	 * 
	 * @param comp
	 *            The component to track.
	 * @param fadeKind
	 *            Fade kind.
	 * @param isFadeIn
	 *            Indication whether fade-in or fade-out should be commenced.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFade(Component comp, FadeKind fadeKind,
			boolean isFadeIn, boolean toRepaintParent,
			FadeTrackerCallback callback) {
		if (isFadeIn)
			return this.trackFadeIn(fadeKind, comp, toRepaintParent, callback);
		else
			return this.trackFadeOut(fadeKind, comp, toRepaintParent, callback);
	}

	/**
	 * Requests start of looping fade tracking on the specified component.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @param animationKind
	 *            Animation kind.
	 * @param comp
	 *            The component to track.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param toRepaintParent
	 *            Indication whether the component parent should be repainted.
	 * @param callback
	 *            Optional callback to be called on each fade cycle.
	 * @param isLoopingReverse
	 *            If <code>true</code>, when the fade value gets to the
	 *            maximal value, the fade cycle will begin fade-out. Otherwise
	 *            the fade cycle will begin from 0, continuing to fade-in.
	 * @return ID of the fade instance. Guaranteed to be unique. Value
	 *         <code>-1</code> signifies that there was no fade instance
	 *         created (this may happen when the specified component is not
	 *         visible).
	 */
	public synchronized long trackFadeLooping(FadeKind fadeKind,
			AnimationKind animationKind, Component comp,
			Comparable componentId, boolean toRepaintParent,
			FadeTrackerCallback callback, boolean isLoopingReverse) {
		if (fadeKind == null) {
			throw new IllegalArgumentException("Can't pass null fadeKind");
		}

		this.getThread();

		if (LafWidgetUtilities.hasNoFades(comp, fadeKind)) {
			if (callback != null) {
				callback.fadeEnded(fadeKind);
			} else {
				if (toRepaintParent && comp.getParent() != null)
					comp.getParent().repaint();
				else
					comp.repaint();
			}
			return -1;
		}

		// see if it's already tracked
		FadeState fadeState = this.getState(fadeKind, comp, componentId);
		if (fadeState == null) {
			fadeState = new FadeState(fadeKind, 0, true, toRepaintParent);
			fadeState.fadeStep = animationKind.getStep();
			fadeState.isLooping = true;
			fadeState.isLoopingReverse = isLoopingReverse;
			this.addState(comp, componentId, fadeKind, fadeState);
		} else {
			fadeState.isLooping = true;
			fadeState.isLoopingReverse = isLoopingReverse;
		}
		fadeState.callback = callback;
		return fadeState.id;
	}

	/**
	 * Checks whether the specified component is being tracked by
	 * <code>this</code> tracker. Effectively returns indication whether the
	 * specified component is in fade-in or fade-out animation of the specified
	 * kind.
	 * 
	 * @param comp
	 *            Component.
	 * @param fadeKind
	 *            Fade kind.
	 * @return <code>true</code> if the specified component is being tracked
	 *         by <code>this</code> tracker, <code>false</code> otherwise.
	 */
	public boolean isTracked(Component comp, FadeKind fadeKind) {
		return this.isTracked(comp, null, fadeKind);
	}

	/**
	 * Checks whether the specified component is being tracked by
	 * <code>this</code> tracker. Effectively returns indication whether the
	 * specified component is in fade-in or fade-out animation of the specified
	 * kind.
	 * 
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes
	 *            (where fade is performed on component "sub" parts).
	 * @param fadeKind
	 *            Fade kind.
	 * @return <code>true</code> if the specified component is being tracked
	 *         by <code>this</code> tracker, <code>false</code> otherwise.
	 */
	public boolean isTracked(Component comp, int componentId, FadeKind fadeKind) {
		return this.isTracked(comp, new Integer(componentId), fadeKind);
	}

	/**
	 * Checks whether the specified component is being tracked by
	 * <code>this</code> tracker. Effectively returns indication whether the
	 * specified component is in fade-in or fade-out animation of the specified
	 * kind.
	 * 
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param fadeKind
	 *            Fade kind.
	 * @return <code>true</code> if the specified component is being tracked
	 *         by <code>this</code> tracker, <code>false</code> otherwise.
	 */
	public boolean isTracked(Component comp, Comparable componentId,
			FadeKind fadeKind) {
		ComponentId cid = new ComponentId(comp, componentId);
		Map<ComponentId, FadeState> map = this.trackedComponents.get(fadeKind);
		if (map == null)
			return false;
		return map.containsKey(cid);
	}

	/**
	 * Checks whether the specified component is being tracked by
	 * <code>this</code> tracker. Effectively returns indication whether the
	 * specified component is in the specified animation (fade-in / fade-out) of
	 * the specified kind.
	 * 
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param fadeKind
	 *            Fade kind. May be <code>null</code> - not recommended.
	 * @param checkFadeIn
	 *            Specifies whether the check should be done for fade-in state.
	 *            If <code>true</code>, the returned value will be
	 *            <code>true</code> only if the component is tracked <b>and</b>
	 *            it is in the fade-in state.
	 * @return <code>true</code> if the specified component is being tracked
	 *         by <code>this</code> tracker, <code>false</code> otherwise.
	 */
	public boolean isTracked(Component comp, Comparable componentId,
			FadeKind fadeKind, boolean checkFadeIn) {
		if (fadeKind == null) {
			throw new IllegalArgumentException("Can't pass null fadeKind");
		}

		ComponentId cid = new ComponentId(comp, componentId);
		Map<ComponentId, FadeState> map = this.trackedComponents.get(fadeKind);
		if (map == null)
			return false;

		FadeState fState = map.get(cid);
		if (fState == null)
			return false;

		return (fState.isFadingIn == checkFadeIn);
	}

	/**
	 * Returns the fade cycle for the specified component. The result will be in
	 * 0.0-10.0 range.
	 * 
	 * @param comp
	 *            Component.
	 * @param fadeKind
	 *            Fade kind.
	 * @return The fade cycle for the specified component. For components that
	 *         are not tracked (when
	 *         {@link #isTracked(Component, org.jvnet.lafwidget.utils.FadeTracker.FadeKind)}
	 *         returns <code>false</code>), value 0 (zero) is returned.
	 */
	public synchronized float getFade10(Component comp, FadeKind fadeKind) {
		return this.getFade10(comp, null, fadeKind);
	}

	/**
	 * Returns the fade cycle for the specified component. The result will be in
	 * 0.0-10.0 range.
	 * 
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes
	 *            (where fade is performed on component "sub" parts).
	 * @param fadeKind
	 *            Fade kind.
	 * @return The fade cycle for the specified component. For components that
	 *         are not tracked (when
	 *         {@link #isTracked(Component, org.jvnet.lafwidget.utils.FadeTracker.FadeKind)}
	 *         returns <code>false</code>), value 0 (zero) is returned.
	 */
	public synchronized float getFade10(Component comp, int componentId,
			FadeKind fadeKind) {
		return this.getFade10(comp, new Integer(componentId), fadeKind);
	}

	/**
	 * Returns the fade cycle for the specified component. The result will be in
	 * 0.0-10.0 range.
	 * 
	 * @param comp
	 *            Component.
	 * @param componentId
	 *            Component id. Relevant for such components as tabbed panes,
	 *            lists, tables, trees and others (where fade is performed on
	 *            component "sub" parts). May be <code>null</code>.
	 * @param fadeKind
	 *            Fade kind.
	 * @return The fade cycle for the specified component. For components that
	 *         are not tracked (when
	 *         {@link #isTracked(Component, org.jvnet.lafwidget.utils.FadeTracker.FadeKind)}
	 *         returns <code>false</code>), value 0 (zero) is returned.
	 */
	public synchronized float getFade10(Component comp, Comparable componentId,
			FadeKind fadeKind) {
		if (fadeKind == null) {
			throw new IllegalArgumentException("Can't pass null fadeKind");
		}

		ComponentId cid = new ComponentId(comp, componentId);
		Map<ComponentId, FadeState> map = this.trackedComponents.get(fadeKind);
		if ((map == null) || (map.size() == 0))
			return 0;

		FadeState state = map.get(cid);
		if (state == null)
			return 0;
		return state.fadePosition;
	}

	/**
	 * Stops tracking of all components. Note that this function <b>does not</b>
	 * stop the fade thread ({@link #trackerThread}).
	 */
	public synchronized void stopAllTimers() {
		for (Iterator<Map.Entry<FadeKind, Map<ComponentId, FadeState>>> itComp = this.trackedComponents
				.entrySet().iterator(); itComp.hasNext();) {
			Map.Entry<FadeKind, Map<ComponentId, FadeState>> entryComp = itComp
					.next();
			Map<ComponentId, FadeState> mapComp = entryComp.getValue();
			for (Iterator<Map.Entry<ComponentId, FadeState>> itKind = mapComp
					.entrySet().iterator(); itKind.hasNext();) {
				Map.Entry<ComponentId, FadeState> entryKind = itKind.next();
				FadeState state = entryKind.getValue();
				if (state.callback != null)
					state.callback.fadeEnded(state.fadeKind);
			}
		}
		this.trackedComponents.clear();
		this.fadeInstances.clear();
	}

	/**
	 * Returns an instance of the tracker thread.
	 * 
	 * @return The tracker thread.
	 */
	private synchronized FadeTrackerThread getThread() {
		if (this.trackerThread == null) {
			this.trackerThread = new FadeTrackerThread();
			this.trackerThread.start();
		}
		return this.trackerThread;
	}

	/**
	 * Cancels the specified fade instance.
	 * 
	 * @param fadeInstanceId
	 *            Fade instance ID.
	 */
	public synchronized void cancelFadeInstance(long fadeInstanceId) {
		FadeKind fadeKind = this.fadeInstances.get(new Long(fadeInstanceId));
		if (fadeKind != null) {
			Map<ComponentId, FadeState> compFades = this.trackedComponents
					.get(fadeKind);
			for (Iterator<Map.Entry<ComponentId, FadeState>> itKind = compFades
					.entrySet().iterator(); itKind.hasNext();) {
				Map.Entry<ComponentId, FadeState> entryKind = itKind.next();
				FadeState state = (FadeState) entryKind.getValue();
				if (fadeInstanceId == state.id) {
					itKind.remove();
					state.callback.fadeEnded(state.fadeKind);
				}
			}
			if (compFades.size() == 0) {
				this.trackedComponents.remove(fadeKind);
			}

			this.fadeInstances.remove(new Long(fadeInstanceId));
		}
	}

	/**
	 * Requests that the specified fade instance should stop at the end of the
	 * fade-out. This method should be called only on looping fades.
	 * 
	 * @param fadeInstanceId
	 *            Fade instance ID.
	 */
	public synchronized void requestStopOnCycleBreak(long fadeInstanceId) {
		FadeKind fadeKind = this.fadeInstances.get(fadeInstanceId);
		if (fadeKind != null) {
			Map<ComponentId, FadeState> compFades = this.trackedComponents
					.get(fadeKind);
			for (FadeState fadeState : compFades.values()) {
				if (fadeInstanceId == fadeState.id) {
					fadeState.toStopAtCycleBreak();
				}
			}
		}
	}

	/**
	 * Returns all components that are currently under the specified fade kind.
	 * 
	 * @param fadeKind
	 *            Fade kind.
	 * @return Non-<code>null</code> set of all components that are currently
	 *         under the specified fade kind.
	 */
	public synchronized Set<Component> getAllComponentsByFadeKind(
			FadeKind fadeKind) {
		Set<Component> result = new HashSet<Component>();
		Map<ComponentId, FadeState> comps = this.trackedComponents
				.get(fadeKind);
		if (comps != null) {
			for (ComponentId cid : comps.keySet()) {
				result.add(cid.component);
			}
		}
		return result;
	}

	/**
	 * Returns the ID of the current loop iteration.
	 * 
	 * @return ID of the current loop iteration.
	 */
	public long getCurrLoopId() {
		return currLoopId;
	}
}
