/*******************************************************************************
 * Copyright (c) 2005, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.draw2d;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * A utility for coordinating figure animations. During animation, multiple
 * <i>animators</i> are employed to capture the <em>initial</em> and <em>final</em> states
 * for one or more figures. The animators then playback the animation by interpolating the
 * intermediate states for each figure using the initial and final "keyframes".
 * <P>
 * An animator is usually stateless and represents an specific technique. Any state
 * information is stored by the Animation utility. Therefore, one instance can be used
 * with multiple figures. Animators hook into the validation mechanism for figures and
 * connections. These hooks are used to capture the states, and to intercept the typical
 * layout process to insert the interpolated state.
 * <P>
 * To indicate that animation is desired, clients must call {@link #markBegin()} prior to
 * invalidating any figures that are to be included in the animation. After this method is
 * called, changes are made, and {@link #run()} is invoked. The run method will force a
 * validation pass to capture the final states, and then commence the animation. The
 * animation is synchronous and the method does not return until the animation has
 * completed.
 * @see LayoutAnimator
 * @since 3.2
 */
public class Animation {

static class AnimPair {

	final Animator animator;
	final IFigure figure;

	AnimPair(Animator animator, IFigure figure) {
		this.animator = animator;
		this.figure = figure;	
	}

	public boolean equals(Object obj) {
		AnimPair pair = (AnimPair)obj;
		return pair.animator == animator && pair.figure == figure;
	}

	public int hashCode() {
		return animator.hashCode() ^ figure.hashCode();
	}
}
private static final int DEFAULT_DELAY = 250;
private static Set figureAnimators;
private static Map finalStates;

private static Map initialStates;
private static final int PLAYBACK = 3;
private static float progress;
private static final int RECORD_FINAL = 2;

private static final int RECORD_INITIAL = 1;
private static long startTime;
private static int state;
private static Set toCapture;

private static UpdateManager updateManager;

private static void capture() {
	Iterator keys = figureAnimators.iterator();
	while (keys.hasNext()) {
		AnimPair pair = (AnimPair) keys.next();
		if (toCapture.contains(pair))
			pair.animator.capture(pair.figure);
		else
			keys.remove();
	}
}

static void cleanup() {
	if (figureAnimators != null) {
		Iterator keys = figureAnimators.iterator();
		while (keys.hasNext()) {
			AnimPair pair = (AnimPair) keys.next();
			pair.animator.tearDown(pair.figure);
		}
	}

	state = 0;
	step();
	//Allow layout to occur normally
	//updateManager.performUpdate();

	initialStates = null;
	finalStates = null;
	figureAnimators = null;
	updateManager = null;
    toCapture = null;
	state = 0;
}

private static void doRun(int duration) {
	state = RECORD_FINAL;
	findUpdateManager();
	updateManager.performValidation();
	capture();
	state = PLAYBACK;
	progress = 0.1f;
	startTime = System.currentTimeMillis();
	
	notifyPlaybackStarting();
	
	while (progress != 0) {
		step();
		updateManager.performUpdate();
		if (progress == 1.0)
			progress = 0;
		else {
			int delta = (int)(System.currentTimeMillis() - startTime);
			if (delta >= duration)
				progress = 1f;
			else
				progress = 0.1f + 0.9f * delta / duration;
		}
	}
}

private static void findUpdateManager() {
	AnimPair pair = (AnimPair) figureAnimators.iterator().next();
	updateManager = pair.figure.getUpdateManager();
}

/**
 * Returns the final animation state for the given figure.
 * @param animator the animator for the figure
 * @param figure the figure being animated
 * @return the final state
 * @since 3.2
 */
public static Object getFinalState(Animator animator, IFigure figure) {
	return finalStates.get(new AnimPair(animator, figure));
}

/**
 * Returns the initial animation state for the given animator and figure. If no state was
 * recorded, <code>null</code> is returned.
 * @param animator the animator for the figure
 * @param figure the figure being animated
 * @return the initial state
 * @since 3.2
 */
public static Object getInitialState(Animator animator, IFigure figure) {
	return initialStates.get(new AnimPair(animator, figure));
}

/**
 * Returns the animation progress, where 0.0 < progress &#8804; 1.0.
 * @return the progress of the animation
 * @since 3.2
 */
public static float getProgress() {
	return progress;
}

static void hookAnimator(IFigure figure, Animator animator) {
	AnimPair pair = new AnimPair(animator, figure);
	if (figureAnimators.add(pair))
		animator.init(figure);
}

static void hookNeedsCapture(IFigure figure, Animator animator) {
	AnimPair pair = new AnimPair(animator, figure);
	if (figureAnimators.contains(pair))
		toCapture.add(pair);
}

static boolean hookPlayback(IFigure figure, Animator animator) {
	if (toCapture.contains(new AnimPair(animator, figure)))
		return animator.playback(figure);
	return false;
}

/**
 * Returns <code>true</code> if animation is in progress.
 * @return <code>true</code> when animating
 * @since 3.2
 */
public static boolean isAnimating() {
	return state == PLAYBACK;
}

static boolean isFinalRecording() {
	return state == RECORD_FINAL;
}

static boolean isInitialRecording() {
	return state == RECORD_INITIAL;
}

/**
 * Marks the beginning of the animation process. If the beginning has already been marked,
 * this has no effect.
 * @return returns <code>true</code> if beginning was not previously marked
 * @since 3.2
 */
public static boolean markBegin() {
	if (state == 0) {
		state = RECORD_INITIAL;
		initialStates = new HashMap();
		finalStates = new HashMap();
		figureAnimators = new HashSet();
		toCapture = new HashSet();
		return true;
	}
	return false;
}

private static void notifyPlaybackStarting() {
	Iterator keys = figureAnimators.iterator();
	while (keys.hasNext()) {
		AnimPair pair = (AnimPair) keys.next();
		pair.animator.playbackStarting(pair.figure);
	}
}

static void putFinalState(Animator animator, IFigure key, Object state) {
	finalStates.put(new AnimPair(animator, key), state);
}

static void putInitialState(Animator animator, IFigure key, Object state) {
	initialStates.put(new AnimPair(animator, key), state);
}

/**
 * Runs animation using the recommended duration: 250 milliseconds.
 * @see #run(int)
 * @since 3.2
 */
public static void run() {
	run(DEFAULT_DELAY);
}

/**
 * Captures the final states for the animation and then plays the animation.
 * @param duration the length of animation in milliseconds
 * @since 3.2
 */
public static void run(int duration) {
	if (state == 0)
		return;
	try {
		if (!figureAnimators.isEmpty())
			doRun(duration);
	} finally {
		cleanup();
	}
}

private static void step() {
	Iterator iter = initialStates.keySet().iterator();
	while (iter.hasNext())
		((AnimPair)iter.next()).figure.revalidate();
}

}
