/*
 * 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 net.jcip.annotations.GuardedBy;
import org.pushingpixels.trident.Timeline.TimelineState;
import org.pushingpixels.trident.TimelineScenario.TimelineScenarioState;
import org.pushingpixels.trident.callback.RunOnUIThread;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * 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;
    long scheduledPulseShutdown = Long.MAX_VALUE;
    boolean callbackWasIdle;

	/**
	 * 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;
		}
	}

    private final Object threadSemaphore = new Object();

	/**
	 * The timeline thread.
	 */
    @GuardedBy ("threadSemaphore")
	TridentAnimationThread animatorThread;

	private BlockingQueue<Runnable> callbackQueue;

    @GuardedBy ("threadSemaphore")
	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() {
            try {
                TridentConfig.PulseSource pulseSource = TridentConfig.getInstance()
                        .getPulseSource();
                lastIterationTimeStamp = System.currentTimeMillis();
                while (!isTimelinesEmpty() || (lastIterationTimeStamp < scheduledPulseShutdown)) {
                    pulseSource.waitUntilNextPulse();
                    updateTimelines();
                    // engine.currLoopId++;
                }
            } finally {
                synchronized (threadSemaphore) {
                    animatorThread = null;
                    checkAnimatorThread();
                }
            }
		}

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


    private void checkAnimatorThread() {
         if (!isTimelinesEmpty()) {
             getAnimatorThread();
         }
    }

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

		@Override
		public void run() {
            try {
                while (true) {
                    try {
                        Runnable runnable = callbackQueue.poll(30, TimeUnit.SECONDS);  // half a minute, twice to shut down
                        if (runnable != null) {
                            callbackWasIdle = false;
                            runnable.run();
                        } else if (callbackWasIdle) {
                            break;
                        } else {
                            callbackWasIdle = true;
                        }
                    } catch (Error e) {
                        e.printStackTrace();
                        throw e;
                    } catch (RuntimeException re) {
                        re.printStackTrace();
                        throw re;
                    } catch (InterruptedException ie) {
                        break;
                    }
                }
            } finally {
                synchronized (threadSemaphore) {
                    callbackThread = null;
                    checkCallbackThread();
                }
            }
		}
	}

    private void checkCallbackThread() {
         if (!callbackQueue.isEmpty()) {
             getCallbackThread();
         }
    }


	/**
	 * 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) {
            try {
                if (isTimelinesEmpty()) {
                    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");
            } finally {
                this.lastIterationTimeStamp = System.currentTimeMillis();
            }
		}
	}

    private boolean isTimelinesEmpty() {
        boolean empty = (this.runningTimelines.size() == 0)
            && (this.runningScenarios.size() == 0);
        if (empty) {
            if (scheduledPulseShutdown == Long.MAX_VALUE) {
                scheduledPulseShutdown = lastIterationTimeStamp + 60000; // one minute;
            }
        } else {
            scheduledPulseShutdown = Long.MAX_VALUE;
        }
        return empty;
    }


    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() {
								@Override
                                public void run() {
									timeline.callback.onTimelineStateChanged(
											oldState, newState,
											durationFraction, timelinePosition);
								}
							});
				} else {
					timeline.callback.onTimelineStateChanged(oldState,
							newState, durationFraction, timelinePosition);
				}
			}
		};
		this.callbackQueue.offer(callbackRunnable);
        checkCallbackThread();
	}

	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() {
								@Override
                                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.offer(callbackRunnable);
        checkCallbackThread();
	}

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

	/**
	 * Returns an existing running timeline that matches the specified
	 * parameters.
	 * 
	 * @param timeline
	 *            Timeline
	 * @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) {
            timeline.fullObjectID = new FullObjectID(timeline.mainObject,
                    timeline.secondaryId);
			this.runningTimelines.add(timeline);
            checkAnimatorThread();
			// 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);
            checkAnimatorThread();
			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() {
        synchronized (threadSemaphore) {
            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() {
        synchronized (threadSemaphore) {
            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();
}