/*******************************************************************************
 * Copyright (c) 2000, 2010 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.gef.commands;

import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.Stack;

/**
 * An implementation of a command stack. A stack manages the executing, undoing,
 * and redoing of {@link Command Commands}. Executed commands are pushed onto a
 * a stack for undoing later. Commands which are undone are pushed onto a redo
 * stack. Whenever a new command is executed, the Redo stack is flushed.
 * <P>
 * A CommandStack contains a dirty property. This property can be used to
 * determine when persisting changes is required. The stack is dirty whenever
 * the last executed or redone command is different than the command that was at
 * the top of the undo stack when {@link #markSaveLocation()} was last called.
 * Initially, the undo stack is empty, and not dirty.
 * 
 * @author hudsonr
 */
public class CommandStack {

	/**
	 * Constant indicating notification after a command has been executed (value
	 * is 8).
	 */
	public static final int POST_EXECUTE = 8;
	/**
	 * Constant indicating notification after a command has been redone (value
	 * is 16).
	 */
	public static final int POST_REDO = 16;
	/**
	 * Constant indicating notification after a command has been undone (value
	 * is 32).
	 */
	public static final int POST_UNDO = 32;

	/**
	 * A bit-mask indicating notification after a command has done something.
	 * Currently this includes after a command has been undone, redone, or
	 * executed. This will include new events should they be introduced in the
	 * future.
	 * <P>
	 * Usage<BR/>
	 * 
	 * <PRE>
	 * if ((commandStackEvent.getDetail() &amp; CommandStack.POST_MASK) != 0) {
	 * 	// Do something, like:
	 * 	stopBatchingChanges();
	 * }
	 * </PRE>
	 */
	public static final int POST_MASK = new Integer(POST_EXECUTE | POST_UNDO
			| POST_REDO).intValue();

	/**
	 * Constant indicating notification prior to executing a command (value is
	 * 1).
	 */
	public static final int PRE_EXECUTE = 1;

	/**
	 * Constant indicating notification prior to redoing a command (value is 2).
	 */
	public static final int PRE_REDO = 2;

	/**
	 * Constant indicating notification prior to undoing a command (value is 4).
	 */
	public static final int PRE_UNDO = 4;

	/**
	 * A bit-mask indicating notification before a command makes a change.
	 * Currently this includes before a command has been undone, redone, or
	 * executed. This will include new events should they be introduced in the
	 * future.
	 * <P>
	 * Usage<BR/>
	 * 
	 * <PRE>
	 * if ((commandStackEvent.getDetail() &amp; CommandStack.PRE_MASK) != 0) {
	 * 	// Do something, like:
	 * 	startBatchingChanges();
	 * }
	 * </PRE>
	 * 
	 * @since 3.7 Had package visibility before.
	 */
	public static final int PRE_MASK = new Integer(PRE_EXECUTE | PRE_UNDO
			| PRE_REDO).intValue();

	private List eventListeners = new ArrayList();

	/**
	 * The list of {@link CommandStackListener}s.
	 * 
	 * @deprecated This field should not be referenced, use
	 *             {@link #notifyListeners()}
	 */
	protected List listeners = new ArrayList();

	private Stack redoable = new Stack();

	private int saveLocation = 0;

	private Stack undoable = new Stack();

	private int undoLimit = 0;

	/**
	 * Constructs a new command stack. By default, there is no undo limit, and
	 * isDirty() will return <code>false</code>.
	 */
	public CommandStack() {
	}

	/**
	 * Appends the listener to the list of command stack listeners. Multiple
	 * adds result in multiple notifications.
	 * 
	 * @since 3.1
	 * @param listener
	 *            the event listener
	 */
	public void addCommandStackEventListener(CommandStackEventListener listener) {
		eventListeners.add(listener);
	}

	/**
	 * Appends the listener to the list of command stack listeners. Multiple
	 * adds will result in multiple notifications.
	 * 
	 * @param listener
	 *            the listener
	 */
	public void addCommandStackListener(CommandStackListener listener) {
		listeners.add(listener);
	}

	/**
	 * @return <code>true</code> if it is appropriate to call {@link #redo()}.
	 */
	public boolean canRedo() {
		return !redoable.isEmpty();
	}

	/**
	 * @return <code>true</code> if {@link #undo()} can be called
	 */
	public boolean canUndo() {
		if (undoable.size() == 0)
			return false;
		return ((Command) undoable.lastElement()).canUndo();
	}

	/**
	 * This will <code>dispose()</code> all the commands in both the undo and
	 * redo stack. Both stacks will be empty afterwards.
	 */
	public void dispose() {
		flushUndo();
		flushRedo();
	}

	/**
	 * Executes the specified Command if possible. Prior to executing the
	 * command, a CommandStackEvent for {@link #PRE_EXECUTE} will be fired to
	 * event listeners. Similarly, after attempting to execute the command, an
	 * event for {@link #POST_EXECUTE} will be fired. If the execution of the
	 * command completely normally, stack listeners will receive
	 * {@link CommandStackListener#commandStackChanged(EventObject)
	 * stackChanged} notification.
	 * <P>
	 * If the command is <code>null</code> or cannot be executed, nothing
	 * happens.
	 * 
	 * @param command
	 *            the Command to execute
	 * @see CommandStackEventListener
	 */
	public void execute(Command command) {
		if (command == null || !command.canExecute())
			return;
		flushRedo();
		notifyListeners(command, PRE_EXECUTE);
		try {
			command.execute();
			if (getUndoLimit() > 0) {
				while (undoable.size() >= getUndoLimit()) {
					((Command) undoable.remove(0)).dispose();
					if (saveLocation > -1)
						saveLocation--;
				}
			}
			if (saveLocation > undoable.size())
				saveLocation = -1; // The save point was somewhere in the redo
									// stack
			undoable.push(command);
			notifyListeners();
		} finally {
			notifyListeners(command, POST_EXECUTE);
		}
	}

	/**
	 * Flushes the entire stack and resets the save location to zero. This
	 * method might be called when performing "revert to saved".
	 */
	public void flush() {
		flushRedo();
		flushUndo();
		saveLocation = 0;
		notifyListeners();
	}

	private void flushRedo() {
		while (!redoable.isEmpty())
			((Command) redoable.pop()).dispose();
	}

	private void flushUndo() {
		while (!undoable.isEmpty())
			((Command) undoable.pop()).dispose();
	}

	/**
	 * @return an array containing all commands in the order they were executed
	 */
	public Object[] getCommands() {
		List commands = new ArrayList(undoable);
		for (int i = redoable.size() - 1; i >= 0; i--) {
			commands.add(redoable.get(i));
		}
		return commands.toArray();
	}

	/**
	 * Peeks at the top of the <i>redo</i> stack. This is useful for describing
	 * to the User what will be redone. The returned <code>Command</code> has a
	 * label describing it.
	 * 
	 * @return the top of the <i>redo</i> stack, which may be <code>null</code>
	 */
	public Command getRedoCommand() {
		return redoable.isEmpty() ? null : (Command) redoable.peek();
	}

	/**
	 * Peeks at the top of the <i>undo</i> stack. This is useful for describing
	 * to the User what will be undone. The returned <code>Command</code> has a
	 * label describing it.
	 * 
	 * @return the top of the <i>undo</i> stack, which may be <code>null</code>
	 */
	public Command getUndoCommand() {
		return undoable.isEmpty() ? null : (Command) undoable.peek();
	}

	/**
	 * Returns the undo limit. The undo limit is the maximum number of atomic
	 * operations that the User can undo. <code>-1</code> is used to indicate no
	 * limit.
	 * 
	 * @return the undo limit
	 */
	public int getUndoLimit() {
		return undoLimit;
	}

	/**
	 * Returns true if the stack is dirty. The stack is dirty whenever the last
	 * executed or redone command is different than the command that was at the
	 * top of the undo stack when {@link #markSaveLocation()} was last called.
	 * 
	 * @return <code>true</code> if the stack is dirty
	 */
	public boolean isDirty() {
		return undoable.size() != saveLocation;
	}

	/**
	 * Marks the last executed or redone Command as the point at which the
	 * changes were saved. Calculation of {@link #isDirty()} will be based on
	 * this checkpoint.
	 */
	public void markSaveLocation() {
		saveLocation = undoable.size();
		notifyListeners();
	}

	/**
	 * Sends notification to all {@link CommandStackListener}s.
	 * 
	 * @deprecated
	 */
	protected void notifyListeners() {
		EventObject event = new EventObject(this);
		for (int i = 0; i < listeners.size(); i++)
			((CommandStackListener) listeners.get(i))
					.commandStackChanged(event);
	}

	/**
	 * Notifies command stack event listeners that the command stack has changed
	 * to the specified state.
	 * 
	 * @param command
	 *            the command
	 * @param state
	 *            the current stack state
	 * @since 3.2
	 */
	protected void notifyListeners(Command command, int state) {
		CommandStackEvent event = new CommandStackEvent(this, command, state);
		for (int i = 0; i < eventListeners.size(); i++)
			((CommandStackEventListener) eventListeners.get(i))
					.stackChanged(event);
	}

	/**
	 * Calls redo on the Command at the top of the <i>redo</i> stack, and pushes
	 * that Command onto the <i>undo</i> stack. This method should only be
	 * called when {@link #canUndo()} returns <code>true</code>.
	 */
	public void redo() {
		// Assert.isTrue(canRedo())
		if (!canRedo())
			return;
		Command command = (Command) redoable.pop();
		notifyListeners(command, PRE_REDO);
		try {
			command.redo();
			undoable.push(command);
			notifyListeners();
		} finally {
			notifyListeners(command, POST_REDO);
		}
	}

	/**
	 * Removes the first occurrence of the specified listener.
	 * 
	 * @param listener
	 *            the listener
	 */
	public void removeCommandStackEventListener(
			CommandStackEventListener listener) {
		eventListeners.remove(listener);
	}

	/**
	 * Removes the first occurrence of the specified listener.
	 * 
	 * @param listener
	 *            the listener
	 */
	public void removeCommandStackListener(CommandStackListener listener) {
		listeners.remove(listener);
	}

	/**
	 * Sets the undo limit. The undo limit is the maximum number of atomic
	 * operations that the User can undo. <code>-1</code> is used to indicate no
	 * limit.
	 * 
	 * @param undoLimit
	 *            the undo limit
	 */
	public void setUndoLimit(int undoLimit) {
		this.undoLimit = undoLimit;
	}

	/**
	 * Undoes the most recently executed (or redone) Command. The Command is
	 * popped from the undo stack to and pushed onto the redo stack. This method
	 * should only be called when {@link #canUndo()} returns <code>true</code>.
	 */
	public void undo() {
		// Assert.isTrue(canUndo());
		Command command = (Command) undoable.pop();
		notifyListeners(command, PRE_UNDO);
		try {
			command.undo();
			redoable.push(command);
			notifyListeners();
		} finally {
			notifyListeners(command, POST_UNDO);
		}
	}

}
