/*
 * 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 org.pushingpixels.trident.callback.TimelineScenarioCallback;

public class TimelineScenario {
	private Set<TimelineScenarioActor> waitingActors;

	private Set<TimelineScenarioActor> runningActors;

	private Set<TimelineScenarioActor> doneActors;

	private Map<TimelineScenarioActor, Set<TimelineScenarioActor>> dependencies;

	Chain callback;

	TimelineScenarioState state;

	TimelineScenarioState statePriorToSuspension;

	boolean isLooping;

	public enum TimelineScenarioState {
		DONE, PLAYING, IDLE, SUSPENDED
	}

	class Chain implements TimelineScenarioCallback {
		private List<TimelineScenarioCallback> callbacks;

		public Chain(TimelineScenarioCallback... callbacks) {
			this.callbacks = new ArrayList<TimelineScenarioCallback>();
			for (TimelineScenarioCallback callback : callbacks)
				this.callbacks.add(callback);
		}

		public void addCallback(TimelineScenarioCallback callback) {
			this.callbacks.add(callback);
		}

		@Override
		public void onTimelineScenarioDone() {
			for (TimelineScenarioCallback callback : this.callbacks)
				callback.onTimelineScenarioDone();
		}
	}

	public static interface TimelineScenarioActor {
		public boolean isDone();

		public boolean supportsReplay();

		public void resetDoneFlag();

		public void play();
	}

	public TimelineScenario() {
		this.waitingActors = new HashSet<TimelineScenarioActor>();
		this.runningActors = new HashSet<TimelineScenarioActor>();
		this.doneActors = new HashSet<TimelineScenarioActor>();

		this.dependencies = new HashMap<TimelineScenarioActor, Set<TimelineScenarioActor>>();
		this.callback = new Chain();
		this.state = TimelineScenarioState.IDLE;
	}

	public void addScenarioActor(TimelineScenarioActor actor) {
		if (actor.isDone()) {
			throw new IllegalArgumentException("Already finished");
		}
		this.waitingActors.add(actor);
	}

	public void addCallback(TimelineScenarioCallback callback) {
		if (this.doneActors.size() > 0) {
			throw new IllegalArgumentException(
					"Cannot change state of non-idle timeline scenario");
		}
		this.callback.addCallback(callback);
	}

	private void checkDependencyParam(TimelineScenarioActor actor) {
		if (!waitingActors.contains(actor)) {
			throw new IllegalArgumentException(
					"Must be first added with addScenarioActor() API");
		}
	}

	public void addDependency(TimelineScenarioActor actor,
			TimelineScenarioActor... waitFor) {
		// check params
		this.checkDependencyParam(actor);
		for (TimelineScenarioActor wait : waitFor) {
			this.checkDependencyParam(wait);
		}

		if (!this.dependencies.containsKey(actor))
			this.dependencies.put(actor, new HashSet<TimelineScenarioActor>());
		this.dependencies.get(actor).addAll(Arrays.asList(waitFor));
	}

	private void checkDoneActors() {
		synchronized (TimelineEngine.LOCK) {
			for (Iterator<TimelineScenarioActor> itRunning = this.runningActors
					.iterator(); itRunning.hasNext();) {
				TimelineScenarioActor stillRunning = itRunning.next();
				if (stillRunning.isDone()) {
					itRunning.remove();
					this.doneActors.add(stillRunning);
				}
			}
		}
	}

	Set<TimelineScenarioActor> getReadyActors() {
		synchronized (TimelineEngine.LOCK) {
			if (this.state == TimelineScenarioState.SUSPENDED)
				return new HashSet<TimelineScenarioActor>();

			this.checkDoneActors();

			Set<TimelineScenarioActor> result = new HashSet<TimelineScenarioActor>();
			for (Iterator<TimelineScenarioActor> itWaiting = this.waitingActors
					.iterator(); itWaiting.hasNext();) {
				TimelineScenarioActor waitingActor = itWaiting.next();
				boolean canRun = true;
				Set<TimelineScenarioActor> toWaitFor = this.dependencies
						.get(waitingActor);
				if (toWaitFor != null) {
					for (TimelineScenarioActor actorToWaitFor : toWaitFor) {
						if (!doneActors.contains(actorToWaitFor)) {
							canRun = false;
							break;
						}
					}
				}
				if (canRun) {
					runningActors.add(waitingActor);
					itWaiting.remove();
					result.add(waitingActor);
				}
			}

			if (this.waitingActors.isEmpty() && this.runningActors.isEmpty()) {
				if (!this.isLooping) {
					this.state = TimelineScenarioState.DONE;
				} else {
					for (TimelineScenarioActor done : this.doneActors)
						done.resetDoneFlag();
					this.waitingActors.addAll(this.doneActors);
					this.doneActors.clear();
				}
			}

			return result;
		}
	}

	public void cancel() {
		synchronized (TimelineEngine.LOCK) {
			TimelineScenarioState oldState = this.state;
			if (oldState != TimelineScenarioState.PLAYING)
				return;
			this.state = TimelineScenarioState.DONE;

			for (TimelineScenarioActor waiting : this.waitingActors) {
				if (waiting instanceof Timeline) {
					((Timeline) waiting).cancel();
				}
			}
			for (TimelineScenarioActor running : this.runningActors) {
				if (running instanceof Timeline) {
					((Timeline) running).cancel();
				}
			}
		}
	}

	public void suspend() {
		synchronized (TimelineEngine.LOCK) {
			TimelineScenarioState oldState = this.state;
			if (oldState != TimelineScenarioState.PLAYING)
				return;
			this.statePriorToSuspension = oldState;
			this.state = TimelineScenarioState.SUSPENDED;

			for (TimelineScenarioActor running : this.runningActors) {
				if (running instanceof Timeline) {
					((Timeline) running).suspend();
				}
			}
		}
	}

	public void resume() {
		synchronized (TimelineEngine.LOCK) {
			TimelineScenarioState oldState = this.state;
			if (oldState != TimelineScenarioState.SUSPENDED)
				return;
			this.state = this.statePriorToSuspension;

			for (TimelineScenarioActor running : this.runningActors) {
				if (running instanceof Timeline) {
					((Timeline) running).resume();
				}
			}
		}
	}

	public void play() {
		this.isLooping = false;
		this.state = TimelineScenarioState.PLAYING;

		TimelineEngine.getInstance().runTimelineScenario(this, new Runnable() {
			@Override
			public void run() {
				TimelineEngine.getInstance()
						.playScenario(TimelineScenario.this);
			}
		});
	}

	public void playLoop() {
		for (TimelineScenarioActor actor : this.waitingActors) {
			if (!actor.supportsReplay())
				throw new UnsupportedOperationException(
						"Can't loop scenario with actor(s) that don't support replay");
		}
		this.isLooping = true;
		this.state = TimelineScenarioState.PLAYING;
		TimelineEngine.getInstance().runTimelineScenario(this, new Runnable() {
			@Override
			public void run() {
				TimelineEngine.getInstance()
						.playScenario(TimelineScenario.this);
			}
		});
	}

	public static class Parallel extends TimelineScenario {
		@Override
		public void addDependency(TimelineScenarioActor actor,
				TimelineScenarioActor... waitFor) {
			throw new UnsupportedOperationException(
					"Explicit dependencies not supported");
		}
	}

	public static class Sequence extends TimelineScenario {
		private TimelineScenarioActor lastActor;

		@Override
		public void addDependency(TimelineScenarioActor actor,
				TimelineScenarioActor... waitFor) {
			throw new UnsupportedOperationException(
					"Explicit dependencies not supported");
		}

		@Override
		public void addScenarioActor(TimelineScenarioActor actor) {
			super.addScenarioActor(actor);
			if (this.lastActor != null) {
				super.addDependency(actor, this.lastActor);
			}
			this.lastActor = actor;
		}
	}

	public static class RendezvousSequence extends TimelineScenario {
		private Set<TimelineScenarioActor> addedSinceLastRendezvous;

		private Set<TimelineScenarioActor> addedPriorToLastRendezvous;

		public RendezvousSequence() {
			this.addedSinceLastRendezvous = new HashSet<TimelineScenarioActor>();
			this.addedPriorToLastRendezvous = new HashSet<TimelineScenarioActor>();
		}

		@Override
		public void addDependency(TimelineScenarioActor actor,
				TimelineScenarioActor... waitFor) {
			throw new UnsupportedOperationException(
					"Explicit dependencies not supported");
		}

		@Override
		public void addScenarioActor(TimelineScenarioActor actor) {
			super.addScenarioActor(actor);
			this.addedSinceLastRendezvous.add(actor);
		}

		public void rendezvous() {
			// make all actors added since last rendezvous to wait for
			// all actors added prior to last rendezvous
			if (this.addedPriorToLastRendezvous.size() > 0) {
				for (TimelineScenarioActor sinceLast : this.addedSinceLastRendezvous) {
					for (TimelineScenarioActor beforeLast : this.addedPriorToLastRendezvous) {
						super.addDependency(sinceLast, beforeLast);
					}
				}
			}

			this.addedPriorToLastRendezvous.clear();
			this.addedPriorToLastRendezvous
					.addAll(this.addedSinceLastRendezvous);
			this.addedSinceLastRendezvous.clear();
		}

		@Override
		public void play() {
			// add last implicit rendezvous
			this.rendezvous();
			super.play();
		}

		@Override
		public void playLoop() {
			// add last implicit rendezvous
			this.rendezvous();
			super.playLoop();
		}
	}

	public final TimelineScenarioState getState() {
		return this.state;
	}
}
