/*
 * Copyright (c) 2005-2010 Trident 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 Trident 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.trident;

import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.TimelineScenario.TimelineScenarioState;
import org.pushingpixels.trident.callback.RunOnUIThread;

/**
 * The Trident timeline engine. This is the main entry point to play
 * {@link Timeline}s and {@link TimelineScenario}s. Use the
 * {@link #getInstance()} method to get the timeline engine.
 * 
 * @author Kirill Grouchnikov
 */
class TimelineEngine {
	/**
	 * Debug mode indicator. Set to <code>true</code> to have trace messages on
	 * console.
	 */
	public static boolean DEBUG_MODE = false;

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

	/**
	 * All currently running timelines.
	 */
	private Set<Timeline> runningTimelines;

	enum TimelineOperationKind {
		PLAY, CANCEL, RESUME, SUSPEND, ABORT, END
	}

	class TimelineOperation {
		public TimelineOperationKind operationKind;

		Runnable operationRunnable;

		public TimelineOperation(TimelineOperationKind operationKind,
				Runnable operationRunnable) {
			this.operationKind = operationKind;
			this.operationRunnable = operationRunnable;
		}
	}

	private Set<TimelineScenario> runningScenarios;

	long lastIterationTimeStamp;

	/**
	 * Identifies a main object and an optional secondary ID.
	 * 
	 * @author Kirill Grouchnikov
	 */
	static class FullObjectID {
		/**
		 * Main object for the timeline.
		 */
		public Object mainObj;

		/**
		 * ID to distinguish between different sub-components of
		 * {@link #mainObj}. For example, the tabbed pane uses this field to
		 * make tab-specific animations.
		 */
		@SuppressWarnings("unchecked")
		public Comparable subID;

		/**
		 * Creates a new object ID.
		 * 
		 * @param mainObj
		 *            The main object.
		 * @param subID
		 *            ID to distinguish between different sub-components of
		 *            <code>mainObj</code>. Can be <code>null</code>.
		 */
		@SuppressWarnings("unchecked")
		public FullObjectID(Object mainObj, Comparable subID) {
			this.mainObj = mainObj;
			this.subID = subID;
		}

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

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		@SuppressWarnings("unchecked")
		public boolean equals(Object obj) {
			if (obj instanceof FullObjectID) {
				FullObjectID cid = (FullObjectID) obj;
				try {
					boolean result = (this.mainObj == cid.mainObj);
					if (this.subID == null) {
						result = result && (cid.subID == null);
					} else {
						result = result
								&& (this.subID.compareTo(cid.subID) == 0);
					}
					return result;
				} catch (Exception exc) {
					return false;
				}
			}
			return false;
		}

		@Override
		public String toString() {
			return this.mainObj.getClass().getSimpleName() + ":" + this.subID;
		}
	}

	/**
	 * The timeline thread.
	 */
	TridentAnimationThread animatorThread;

	private BlockingQueue<Runnable> callbackQueue;

	private TimelineCallbackThread callbackThread;

	class TridentAnimationThread extends Thread {
		public TridentAnimationThread() {
			super();
			this.setName("Trident pulse source thread");
			this.setDaemon(true);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Thread#run()
		 */
		@Override
		public final void run() {
			TridentConfig.PulseSource pulseSource = TridentConfig.getInstance()
					.getPulseSource();
			lastIterationTimeStamp = System.currentTimeMillis();
			while (true) {
				pulseSource.waitUntilNextPulse();
				updateTimelines();
				// engine.currLoopId++;
			}
		}

		@Override
		public void interrupt() {
			System.err.println("Interrupted");
			super.interrupt();
		}
	}

	private class TimelineCallbackThread extends Thread {
		public TimelineCallbackThread() {
			super();
			this.setName("Trident callback thread");
			this.setDaemon(true);
		}

		@Override
		public void run() {
			while (true) {
				try {
					Runnable runnable = callbackQueue.take();
					runnable.run();
				} catch (Throwable t) {
					t.printStackTrace();
				}
			}
		}
	}

	/**
	 * Simple constructor. Defined private for singleton.
	 * 
	 * @see #getInstance()
	 */
	private TimelineEngine() {
		this.runningTimelines = new HashSet<Timeline>();
		this.runningScenarios = new HashSet<TimelineScenario>();

		this.callbackQueue = new LinkedBlockingQueue<Runnable>();
		this.callbackThread = this.getCallbackThread();
	}

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

	/**
	 * Updates all timelines that are currently registered with
	 * <code>this</code> tracker.
	 */
	void updateTimelines() {
		synchronized (LOCK) {
			if ((this.runningTimelines.size() == 0)
					&& (this.runningScenarios.size() == 0)) {
				this.lastIterationTimeStamp = System.currentTimeMillis();
				return;
			}

			long passedSinceLastIteration = (System.currentTimeMillis() - this.lastIterationTimeStamp);
			if (passedSinceLastIteration < 0) {
				// ???
				passedSinceLastIteration = 0;
			}
			if (DEBUG_MODE) {
				System.out.println("Elapsed since last iteration: "
						+ passedSinceLastIteration + "ms");
			}

			// System.err.println("Periodic update on "
			// + this.runningTimelines.size() + " timelines; "
			// + passedSinceLastIteration + " ms passed since last");
			// for (Timeline t : runningTimelines) {
			// if (t.mainObject != null
			// && t.mainObject.getClass().getName().indexOf(
			// "ProgressBar") >= 0) {
			// continue;
			// }
			// System.err.println("\tTimeline @"
			// + t.hashCode()
			// + " ["
			// + t.getName()
			// + "] on "
			// + (t.mainObject == null ? "null" : t.mainObject
			// .getClass().getName()));
			// }
			for (Iterator<Timeline> itTimeline = this.runningTimelines
					.iterator(); itTimeline.hasNext();) {
				Timeline timeline = itTimeline.next();
				if (timeline.getState() == TimelineState.SUSPENDED)
					continue;

				boolean timelineWasInReadyState = false;
				if (timeline.getState() == TimelineState.READY) {
					if ((timeline.timeUntilPlay - passedSinceLastIteration) > 0) {
						// still needs to wait in the READY state
						timeline.timeUntilPlay -= passedSinceLastIteration;
						continue;
					}

					// can go from READY to PLAYING
					timelineWasInReadyState = true;
					timeline.popState();
					this.callbackCallTimelineStateChanged(timeline,
							TimelineState.READY);
				}

				boolean hasEnded = false;
				if (DEBUG_MODE) {
					System.out.println("Processing " + timeline.id + "["
							+ timeline.mainObject.getClass().getSimpleName()
							+ "] from " + timeline.durationFraction
							+ ". Callback - "
							+ (timeline.callback == null ? "no" : "yes"));
				}
				// Component comp = entry.getKey();

				// at this point, the timeline must be playing
				switch (timeline.getState()) {
				case PLAYING_FORWARD:
					if (!timelineWasInReadyState) {
						timeline.durationFraction = timeline.durationFraction
								+ (float) passedSinceLastIteration
								/ (float) timeline.duration;
					}
					timeline.timelinePosition = timeline.ease
							.map(timeline.durationFraction);
					if (DEBUG_MODE) {
						System.out
								.println("Timeline position: "
										+ ((long) (timeline.durationFraction * timeline.duration))
										+ "/" + timeline.duration + " = "
										+ timeline.durationFraction);
					}
					if (timeline.durationFraction > 1.0f) {
						timeline.durationFraction = 1.0f;
						timeline.timelinePosition = 1.0f;
						if (timeline.isLooping) {
							boolean stopLoopingAnimation = timeline.toCancelAtCycleBreak;
							int loopsToLive = timeline.repeatCount;
							if (loopsToLive > 0) {
								loopsToLive--;
								stopLoopingAnimation = stopLoopingAnimation
										|| (loopsToLive == 0);
								timeline.repeatCount = loopsToLive;
							}
							if (stopLoopingAnimation) {
								// end looping animation
								hasEnded = true;
								itTimeline.remove();
							} else {
								if (timeline.repeatBehavior == Timeline.RepeatBehavior.REVERSE) {
									timeline
											.replaceState(TimelineState.PLAYING_REVERSE);
									if (timeline.cycleDelay > 0) {
										timeline.pushState(TimelineState.READY);
										timeline.timeUntilPlay = timeline.cycleDelay;
									}
									this.callbackCallTimelineStateChanged(
											timeline,
											TimelineState.PLAYING_FORWARD);
								} else {
									timeline.durationFraction = 0.0f;
									timeline.timelinePosition = 0.0f;
									if (timeline.cycleDelay > 0) {
										timeline.pushState(TimelineState.READY);
										timeline.timeUntilPlay = timeline.cycleDelay;
										this.callbackCallTimelineStateChanged(
												timeline,
												TimelineState.PLAYING_FORWARD);
									} else {
										// it's still playing forward, but lets
										// the app code know
										// that the new loop has begun
										this.callbackCallTimelineStateChanged(
												timeline,
												TimelineState.PLAYING_FORWARD);
									}
								}
							}
						} else {
							hasEnded = true;
							itTimeline.remove();
						}
					}
					break;
				case PLAYING_REVERSE:
					if (!timelineWasInReadyState) {
						timeline.durationFraction = timeline.durationFraction
								- (float) passedSinceLastIteration
								/ (float) timeline.duration;
					}
					timeline.timelinePosition = timeline.ease
							.map(timeline.durationFraction);
					// state.timelinePosition = state.timelinePosition
					// - stepFactor
					// * state.fadeStep.getNextStep(state.timelineKind,
					// state.timelinePosition,
					// state.isPlayingForward, state.isLooping);
					if (DEBUG_MODE) {
						System.out
								.println("Timeline position: "
										+ ((long) (timeline.durationFraction * timeline.duration))
										+ "/" + timeline.duration + " = "
										+ timeline.durationFraction);
					}
					if (timeline.durationFraction < 0) {
						timeline.durationFraction = 0.0f;
						timeline.timelinePosition = 0.0f;
						if (timeline.isLooping) {
							boolean stopLoopingAnimation = timeline.toCancelAtCycleBreak;
							int loopsToLive = timeline.repeatCount;
							if (loopsToLive > 0) {
								loopsToLive--;
								stopLoopingAnimation = stopLoopingAnimation
										|| (loopsToLive == 0);
								timeline.repeatCount = loopsToLive;
							}
							if (stopLoopingAnimation) {
								// end looping animation
								hasEnded = true;
								itTimeline.remove();
							} else {
								timeline
										.replaceState(TimelineState.PLAYING_FORWARD);
								if (timeline.cycleDelay > 0) {
									timeline.pushState(TimelineState.READY);
									timeline.timeUntilPlay = timeline.cycleDelay;
								}
								this.callbackCallTimelineStateChanged(timeline,
										TimelineState.PLAYING_REVERSE);
							}
						} else {
							hasEnded = true;
							itTimeline.remove();
						}
					}
					break;
				default:
					throw new IllegalStateException("Timeline cannot be in "
							+ timeline.getState() + " state");
				}
				if (hasEnded) {
					if (DEBUG_MODE) {
						System.out.println("Ending " + timeline.id + " on "
								// + timeline.timelineKind.toString()
								+ " in state " + timeline.getState().name()
								+ " at position " + timeline.durationFraction);
					}
					TimelineState oldState = timeline.getState();
					timeline.replaceState(TimelineState.DONE);
					this.callbackCallTimelineStateChanged(timeline, oldState);
					timeline.popState();
					if (timeline.getState() != TimelineState.IDLE) {
						throw new IllegalStateException(
								"Timeline should be IDLE at this point");
					}
					this.callbackCallTimelineStateChanged(timeline,
							TimelineState.DONE);
				} else {
					if (DEBUG_MODE) {
						System.out.println("Calling " + timeline.id + " on "
						// + timeline.timelineKind.toString() + " at "
								+ timeline.durationFraction);
					}
					this.callbackCallTimelinePulse(timeline);
				}
			}

			if (this.runningScenarios.size() > 0) {
				// System.err.println(Thread.currentThread().getName()
				// + " : updating");
				for (Iterator<TimelineScenario> it = this.runningScenarios
						.iterator(); it.hasNext();) {
					TimelineScenario scenario = it.next();
					if (scenario.state == TimelineScenarioState.DONE) {
						it.remove();
						this.callbackCallTimelineScenarioEnded(scenario);
						continue;
					}
					Set<TimelineScenario.TimelineScenarioActor> readyActors = scenario
							.getReadyActors();
					if (readyActors != null) {
						// if (readyActors.size() > 0)
						// System.out.println("Scenario : " + scenario.state +
						// ":"
						// + readyActors.size());
						for (TimelineScenario.TimelineScenarioActor readyActor : readyActors) {
							readyActor.play();
						}
					}
				}
			}
			// System.err.println("Periodic update done");

			// this.nothingTracked = (this.runningTimelines.size() == 0);
			this.lastIterationTimeStamp = System.currentTimeMillis();
		}
	}

	private void callbackCallTimelineStateChanged(final Timeline timeline,
			final TimelineState oldState) {
		final TimelineState newState = timeline.getState();
		final float durationFraction = timeline.durationFraction;
		final float timelinePosition = timeline.timelinePosition;
		Runnable callbackRunnable = new Runnable() {
			@Override
			public void run() {
				boolean shouldRunOnUIThread = false;
				Class<?> clazz = timeline.callback.getClass();
				while ((clazz != null) && !shouldRunOnUIThread) {
					shouldRunOnUIThread = clazz
							.isAnnotationPresent(RunOnUIThread.class);
					clazz = clazz.getSuperclass();
				}
				if (shouldRunOnUIThread && (timeline.uiToolkitHandler != null)) {
					timeline.uiToolkitHandler.runOnUIThread(
							timeline.mainObject, new Runnable() {
								public void run() {
									timeline.callback.onTimelineStateChanged(
											oldState, newState,
											durationFraction, timelinePosition);
								}
							});
				} else {
					timeline.callback.onTimelineStateChanged(oldState,
							newState, durationFraction, timelinePosition);
				}
			}
		};
		this.callbackQueue.add(callbackRunnable);
	}

	private void callbackCallTimelinePulse(final Timeline timeline) {
		final float durationFraction = timeline.durationFraction;
		final float timelinePosition = timeline.timelinePosition;
		Runnable callbackRunnable = new Runnable() {
			@Override
			public void run() {
				boolean shouldRunOnUIThread = false;
				Class<?> clazz = timeline.callback.getClass();
				while ((clazz != null) && !shouldRunOnUIThread) {
					shouldRunOnUIThread = clazz
							.isAnnotationPresent(RunOnUIThread.class);
					clazz = clazz.getSuperclass();
				}
				if (shouldRunOnUIThread && (timeline.uiToolkitHandler != null)) {
					timeline.uiToolkitHandler.runOnUIThread(
							timeline.mainObject, new Runnable() {
								public void run() {
									// System.err.println("Timeline @"
									// + timeline.hashCode());
									timeline.callback.onTimelinePulse(
											durationFraction, timelinePosition);
								}
							});
				} else {
					// System.err.println("Timeline @" + timeline.hashCode());
					timeline.callback.onTimelinePulse(durationFraction,
							timelinePosition);
				}
			}
		};
		this.callbackQueue.add(callbackRunnable);
	}

	private void callbackCallTimelineScenarioEnded(
			final TimelineScenario timelineScenario) {
		Runnable callbackRunnable = new Runnable() {
			@Override
			public void run() {
				timelineScenario.callback.onTimelineScenarioDone();
			}
		};
		this.callbackQueue.offer(callbackRunnable);
	}

	/**
	 * Returns an existing running timeline that matches the specified
	 * parameters.
	 * 
	 * @param timelineKind
	 *            Timeline kind.
	 * @param object
	 *            Component.
	 * @param secondaryId
	 *            Secondary id. Relevant for such components as tabbed panes
	 *            (where animation is performed on different tabs).
	 * @return An existing running timeline that matches the specified
	 *         parameters.
	 */
	private Timeline getRunningTimeline(Timeline timeline) {
		synchronized (LOCK) {
			if (this.runningTimelines.contains(timeline))
				return timeline;
			return null;
		}
	}

	/**
	 * Adds the specified timeline.
	 * 
	 * @param timeline
	 *            Timeline to add.
	 */
	private void addTimeline(Timeline timeline) {
		synchronized (LOCK) {
			FullObjectID cid = new FullObjectID(timeline.mainObject,
					timeline.secondaryId);
			timeline.fullObjectID = cid;
			this.runningTimelines.add(timeline);
			// this.nothingTracked = false;
			if (DEBUG_MODE) {
				System.out.println("Added (" + timeline.id + ") on "
						+ timeline.fullObjectID + "]. Fade "
						// + timeline.timelineKind.toString() + " with state "
						+ timeline.getState().name() + ". Callback - "
						+ (timeline.callback == null ? "no" : "yes"));
			}
		}
	}

	void play(Timeline timeline, boolean reset, long msToSkip) {
		synchronized (LOCK) {
			getAnimatorThread();

			// see if it's already tracked
			Timeline existing = this.getRunningTimeline(timeline);
			if (existing == null) {
				TimelineState oldState = timeline.getState();
				timeline.timeUntilPlay = timeline.initialDelay - msToSkip;
				if (timeline.timeUntilPlay < 0) {
					timeline.durationFraction = (float) -timeline.timeUntilPlay
							/ (float) timeline.duration;
					timeline.timelinePosition = timeline.ease
							.map(timeline.durationFraction);
					timeline.timeUntilPlay = 0;
				} else {
					timeline.durationFraction = 0.0f;
					timeline.timelinePosition = 0.0f;
				}
				timeline.pushState(TimelineState.PLAYING_FORWARD);
				timeline.pushState(TimelineState.READY);
				this.addTimeline(timeline);

				this.callbackCallTimelineStateChanged(timeline, oldState);
			} else {
				TimelineState oldState = existing.getState();
				if (oldState == TimelineState.READY) {
					// the timeline remains READY, but after that it will be
					// PLAYING_FORWARD
					existing.popState();
					existing.replaceState(TimelineState.PLAYING_FORWARD);
					existing.pushState(TimelineState.READY);
				} else {
					// change the timeline state
					existing.replaceState(TimelineState.PLAYING_FORWARD);
					if (oldState != existing.getState()) {
						this.callbackCallTimelineStateChanged(timeline,
								oldState);
					}
				}
				if (reset) {
					existing.durationFraction = 0.0f;
					existing.timelinePosition = 0.0f;
					this.callbackCallTimelinePulse(existing);
				}
			}
		}
	}

	void playScenario(TimelineScenario scenario) {
		synchronized (LOCK) {
			getAnimatorThread();
			Set<TimelineScenario.TimelineScenarioActor> readyActors = scenario
					.getReadyActors();

			// System.err.println(Thread.currentThread().getName() +
			// " : adding");
			this.runningScenarios.add(scenario);
			for (TimelineScenario.TimelineScenarioActor readyActor : readyActors) {
				readyActor.play();
			}
		}
	}

	void playReverse(Timeline timeline, boolean reset, long msToSkip) {
		synchronized (LOCK) {
			getAnimatorThread();
			if (timeline.isLooping) {
				throw new IllegalArgumentException(
						"Timeline must not be marked as looping");
			}

			// see if it's already tracked
			Timeline existing = this.getRunningTimeline(timeline);
			if (existing == null) {
				TimelineState oldState = timeline.getState();
				timeline.timeUntilPlay = timeline.initialDelay - msToSkip;
				if (timeline.timeUntilPlay < 0) {
					timeline.durationFraction = 1.0f
							- (float) -timeline.timeUntilPlay
							/ (float) timeline.duration;
					timeline.timelinePosition = timeline.ease
							.map(timeline.durationFraction);
					timeline.timeUntilPlay = 0;
				} else {
					timeline.durationFraction = 1.0f;
					timeline.timelinePosition = 1.0f;
				}
				timeline.pushState(TimelineState.PLAYING_REVERSE);
				timeline.pushState(TimelineState.READY);

				this.addTimeline(timeline);
				this.callbackCallTimelineStateChanged(timeline, oldState);
			} else {
				TimelineState oldState = existing.getState();
				if (oldState == TimelineState.READY) {
					// the timeline remains READY, but after that it will be
					// PLAYING_REVERSE
					existing.popState();
					existing.replaceState(TimelineState.PLAYING_REVERSE);
					existing.pushState(TimelineState.READY);
				} else {
					// change the timeline state
					existing.replaceState(TimelineState.PLAYING_REVERSE);
					if (oldState != existing.getState()) {
						this.callbackCallTimelineStateChanged(timeline,
								oldState);
					}
				}
				if (reset) {
					existing.durationFraction = 1.0f;
					existing.timelinePosition = 1.0f;
					this.callbackCallTimelinePulse(existing);
				}
			}
		}
	}

	void playLoop(Timeline timeline, long msToSkip) {
		synchronized (LOCK) {
			getAnimatorThread();
			if (!timeline.isLooping) {
				throw new IllegalArgumentException(
						"Timeline must be marked as looping");
			}

			// see if it's already tracked
			Timeline existing = this.getRunningTimeline(timeline);
			if (existing == null) {
				TimelineState oldState = timeline.getState();
				timeline.timeUntilPlay = timeline.initialDelay - msToSkip;
				if (timeline.timeUntilPlay < 0) {
					timeline.durationFraction = (float) -timeline.timeUntilPlay
							/ (float) timeline.duration;
					timeline.timelinePosition = timeline.ease
							.map(timeline.durationFraction);
					timeline.timeUntilPlay = 0;
				} else {
					timeline.durationFraction = 0.0f;
					timeline.timelinePosition = 0.0f;
				}
				timeline.pushState(TimelineState.PLAYING_FORWARD);
				timeline.pushState(TimelineState.READY);
				timeline.toCancelAtCycleBreak = false;

				this.addTimeline(timeline);
				this.callbackCallTimelineStateChanged(timeline, oldState);
			} else {
				existing.toCancelAtCycleBreak = false;
				existing.repeatCount = timeline.repeatCount;
			}
		}
	}

	/**
	 * Stops tracking of all timelines. Note that this function <b>does not</b>
	 * stop the timeline engine thread ({@link #animatorThread}) and the
	 * timeline callback thread ({@link #callbackThread}).
	 */
	public void cancelAllTimelines() {
		synchronized (LOCK) {
			getAnimatorThread();
			for (Timeline timeline : this.runningTimelines) {
				TimelineState oldState = timeline.getState();
				while (timeline.getState() != TimelineState.IDLE)
					timeline.popState();
				timeline.pushState(TimelineState.CANCELLED);
				this.callbackCallTimelineStateChanged(timeline, oldState);
				timeline.popState();
				this.callbackCallTimelineStateChanged(timeline,
						TimelineState.CANCELLED);
			}
			this.runningTimelines.clear();
			this.runningScenarios.clear();
		}
	}

	/**
	 * Returns an instance of the animator thread.
	 * 
	 * @return The animator thread.
	 */
	private TridentAnimationThread getAnimatorThread() {
		if (this.animatorThread == null) {
			this.animatorThread = new TridentAnimationThread();
			this.animatorThread.start();
		}
		return this.animatorThread;
	}

	/**
	 * Returns an instance of the callback thread.
	 * 
	 * @return The animator thread.
	 */
	private TimelineCallbackThread getCallbackThread() {
		if (this.callbackThread == null) {
			this.callbackThread = new TimelineCallbackThread();
			this.callbackThread.start();
		}
		return this.callbackThread;
	}

	/**
	 * Cancels the specified timeline instance.
	 * 
	 * @param timeline
	 *            Timeline to cancel.
	 */
	private void cancelTimeline(Timeline timeline) {
		getAnimatorThread();
		if (this.runningTimelines.contains(timeline)) {
			this.runningTimelines.remove(timeline);
			TimelineState oldState = timeline.getState();
			while (timeline.getState() != TimelineState.IDLE)
				timeline.popState();
			timeline.pushState(TimelineState.CANCELLED);
			this.callbackCallTimelineStateChanged(timeline, oldState);
			timeline.popState();
			this.callbackCallTimelineStateChanged(timeline,
					TimelineState.CANCELLED);
		}
	}

	/**
	 * Ends the specified timeline instance.
	 * 
	 * @param timeline
	 *            Timeline to end.
	 */
	private void endTimeline(Timeline timeline) {
		getAnimatorThread();
		if (this.runningTimelines.contains(timeline)) {
			this.runningTimelines.remove(timeline);
			TimelineState oldState = timeline.getState();
			float endPosition = timeline.timelinePosition;
			while (timeline.getState() != TimelineState.IDLE) {
				TimelineState state = timeline.popState();
				if (state == TimelineState.PLAYING_FORWARD)
					endPosition = 1.0f;
				if (state == TimelineState.PLAYING_REVERSE)
					endPosition = 0.0f;
			}
			timeline.durationFraction = endPosition;
			timeline.timelinePosition = endPosition;
			timeline.pushState(TimelineState.DONE);
			this.callbackCallTimelineStateChanged(timeline, oldState);
			timeline.popState();
			this.callbackCallTimelineStateChanged(timeline, TimelineState.DONE);
		}
	}

	/**
	 * Cancels the specified timeline instance.
	 * 
	 * @param timeline
	 *            Timeline to cancel.
	 */
	private void abortTimeline(Timeline timeline) {
		getAnimatorThread();
		if (this.runningTimelines.contains(timeline)) {
			this.runningTimelines.remove(timeline);
			while (timeline.getState() != TimelineState.IDLE)
				timeline.popState();
		}
	}

	/**
	 * Suspends the specified timeline instance.
	 * 
	 * @param timeline
	 *            Timeline to suspend.
	 */
	private void suspendTimeline(Timeline timeline) {
		getAnimatorThread();
		if (this.runningTimelines.contains(timeline)) {
			TimelineState oldState = timeline.getState();
			if ((oldState != TimelineState.PLAYING_FORWARD)
					&& (oldState != TimelineState.PLAYING_REVERSE)
					&& (oldState != TimelineState.READY)) {
				return;
			}
			timeline.pushState(TimelineState.SUSPENDED);
			this.callbackCallTimelineStateChanged(timeline, oldState);
		}
	}

	/**
	 * Resume the specified timeline instance.
	 * 
	 * @param timeline
	 *            Timeline to resume.
	 */
	private void resumeTimeline(Timeline timeline) {
		getAnimatorThread();
		if (this.runningTimelines.contains(timeline)) {
			TimelineState oldState = timeline.getState();
			if (oldState != TimelineState.SUSPENDED)
				return;
			timeline.popState();
			this.callbackCallTimelineStateChanged(timeline, oldState);
		}
	}

	void runTimelineOperation(Timeline timeline,
			TimelineOperationKind operationKind, Runnable operationRunnable) {
		synchronized (LOCK) {
			this.getAnimatorThread();
			switch (operationKind) {
			case CANCEL:
				this.cancelTimeline(timeline);
				return;
			case END:
				this.endTimeline(timeline);
				return;
			case RESUME:
				this.resumeTimeline(timeline);
				return;
			case SUSPEND:
				this.suspendTimeline(timeline);
				return;
			case ABORT:
				this.abortTimeline(timeline);
				return;
			}
			operationRunnable.run();
		}
	}

	void runTimelineScenario(TimelineScenario timelineScenario,
			Runnable timelineScenarioRunnable) {
		synchronized (LOCK) {
			this.getAnimatorThread();
			timelineScenarioRunnable.run();
		}
	}

	static final Object LOCK = new Object();
}
