/**
 * Copyright (c) 2002-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 v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.common.command;


import java.util.List;
import java.util.ListIterator;

import org.eclipse.emf.common.CommonPlugin;
import org.eclipse.emf.common.util.WrappedException;


/**
 * A composite command which assumes that later commands in the list 
 * may depend on the results and side-effects of earlier commands in the list.
 * Because of this, it must implement {@link Command#canExecute} more carefully,
 * i.e., in order to determine canExecute for the composite, it doesn't simply test each command.
 * It tests the first command to see if it can execute;
 * then, if there is another command in the list, it checks if the first command can undo and then goes ahead and executes it!
 * This process is repeated until the last command that is not followed by another, which then determines the final result.
 * (For efficiency, when this processing gets to the last command, that command is tested for canUndo too and that result is cached.)
 * All the commands that have been executed are then undone, if {@link #isPessimistic} is <code>true</code>; 
 * by default it's <code>false</code>.
 *
 * <p>
 * It is important for all but the last command to have no visible side-effect!
 * Multiple commands with visible side-effects must be composed into a single command using just a {@link CompoundCommand}
 * and that composite could be the last command of a strict composite.
 *
 * <p>
 * Here is an example of how this can be used in conjunction with a {@link CommandWrapper}.
 * <pre>
 *   Command strictCompoundCommand = new StrictCompoundCommand();
 *   Command copyCommand = new CopyCommand(...);
 *   strictCompoundCommand.add(copyCommand);
 *
 *   Command addCommand =
 *     new CommandWrapper()
 *     {
 *       public Command createCommand()
 *       {
 *         new AddCommand(parent, copyCommand.getResult()); 
 *       }
 *     };
 *   strictCompoundCommand.append(addCommand);
 * </pre>
 * Here the add command won't know which command to create until it has the result of the copy command.
 * The proxy makes sure the creation of the add command is deferred and the strict composite ensures that execution dependencies are met.
 */
public class StrictCompoundCommand extends CompoundCommand 
{
  /**
   * The result for {@link Command#canUndo}.
   */
  protected boolean isUndoable;

  /**
   * Whether commands that have been tentatively executed need to be undone.
   */
  protected boolean isPessimistic;

  /**
   * Remember to call redo instead of execute for any command at or before this index in the list.
   */
  protected int rightMostExecutedCommandIndex = -1;

  /**
   * Creates an empty instance.
   */
  public StrictCompoundCommand()
  {
    super();
    resultIndex = LAST_COMMAND_ALL;
  }

  /**
   * Creates an instance with the given label.
   * @param label the label.
   */
  public StrictCompoundCommand(String label)
  {
    super(label);
    resultIndex = LAST_COMMAND_ALL;
  }

  /**
   * Creates an instance with the given label and description.
   * @param label the label.
   * @param description the description.
   */
  public StrictCompoundCommand(String label, String description)
  {
    super(label, description);
    resultIndex = LAST_COMMAND_ALL;
  }

  /**
   * Creates an instance with the given command list.
   * @param commandList the list of commands.
   */
  public StrictCompoundCommand(List<Command> commandList)
  {     
    super(commandList);
    resultIndex = LAST_COMMAND_ALL;
  }

  /**
   * Creates an instance with the given label and command list.
   * @param label the label.
   * @param commandList the list of commands.
   */
  public StrictCompoundCommand(String label, List<Command> commandList)
  {     
    super(label, commandList);
    resultIndex = LAST_COMMAND_ALL;
  }

  /**
   * Creates an instance with the given label, description, and command list.
   * @param label the label.
   * @param description the description.
   * @param commandList the list of commands.
   */
  public StrictCompoundCommand(String label, String description, List<Command> commandList)
  {     
    super(label, description, commandList);
    resultIndex = LAST_COMMAND_ALL;
  }

  /**
   * Returns <code>false</code> if any command on the list returns <code>false</code> for {@link Command#canExecute},
   * or if some command before the last one can't be undone and hence we can't test all the commands for executability.
   * @return whether the command can execute.
   */
  @Override
  protected boolean prepare() 
  {
    // Go through the commands of the list.
    //
    ListIterator<Command> commands = commandList.listIterator(); 

    // If there are some...
    //
    if (commands.hasNext())
    {
      boolean result = true;

      // The termination guard is in the body.
      //
      for (;;)
      {
        Command command = commands.next();
        if (command.canExecute())
        {
          if (commands.hasNext())
          {
            if (command.canUndo())
            {
              try
              {
                if (commands.previousIndex() <= rightMostExecutedCommandIndex)
                {
                  command.redo();
                }
                else
                {
                  ++rightMostExecutedCommandIndex;
                  command.execute();
                }
              }
              catch (RuntimeException exception)
              {
                CommonPlugin.INSTANCE.log
                  (new WrappedException
                    (CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), exception).fillInStackTrace());

                result = false;
                break;
              }
            }
            else
            {
              // We can't undo it, so we'd better give up.
              //
              result = false;
              break;
            }
          }
          else
          {
            // Now is the best time to record isUndoable because later we would have to do all the executes again!
            // This makes canUndo very simple!
            //
            isUndoable = command.canUndo();
            break;
          }
        }
        else
        {
          // If we can't execute this one, we just can't do it at all.
          //
          result = false;
          break;
        }
      }

      // If we are pessimistic, then we need to undo all the commands that we have executed so far.
      //
      if (isPessimistic)
      {
        // The most recently processed command will never have been executed.
        //
        commands.previous();
  
        // We want to unroll all the effects of the previous commands.
        //
        while (commands.hasPrevious()) 
        {
          Command command = commands.previous();
          command.undo();
        }
      }

      return result;
    }
    else
    {
      isUndoable = false;
      return false;
    }
  }

  /**
   * Calls {@link Command#execute} for each command in the list, 
   * but makes sure to call redo for any commands that were previously executed to compute canExecute.
   * In the case that {@link #isPessimistic} is false, only the last command will be executed
   * since the others will have been executed but not undone during {@link #prepare}.
   */
  @Override
  public void execute() 
  {
    if (isPessimistic)
    {
      for (ListIterator<Command> commands = commandList.listIterator(); commands.hasNext(); ) 
      {
        try
        {
          // Either execute or redo the command, as appropriate.
          //
          Command command = commands.next();
          if (commands.previousIndex() <= rightMostExecutedCommandIndex)
          {
            command.redo();
          }
          else
          {
            command.execute();
          }
        }
        catch (RuntimeException exception)
        {
          // Skip over the command that threw the exception.
          //
          commands.previous();

          // Iterate back over the executed commands to undo them.
          //
          while (commands.hasPrevious())
          {
            commands.previous();
            Command command = commands.previous();
            if (command.canUndo())
            {
              command.undo();
            }
            else
            {
              break;
            }
          }

          throw exception;
        }
      }
    }
    else if (!commandList.isEmpty())
    {
      Command command = commandList.get(commandList.size() - 1);
      command.execute();
    }
  }

  /**
   * Calls {@link Command#undo} for each command in the list.
   * In the case that {@link #isPessimistic} is false, only the last command will be undone
   * since the others will have been executed and not undo during {@link #prepare}.
   */
  @Override
  public void undo() 
  {
    if (isPessimistic)
    {
      super.undo();
    }
    else if (!commandList.isEmpty())
    {
      Command command = commandList.get(commandList.size() - 1);
      command.undo();
    }
  }

  /**
   * Calls {@link Command#redo} for each command in the list.
   * In the case that {@link #isPessimistic} is false, only the last command will be redone
   * since the others will have been executed and not undo during {@link #prepare}.
   */
  @Override
  public void redo() 
  {
    if (isPessimistic)
    {
      super.redo();
    }
    else if (!commandList.isEmpty())
    {
      Command command = commandList.get(commandList.size() - 1);
      command.redo();
    }
  }

  /**
   * Checks if the command can execute; 
   * if so, it is executed, appended to the list, and <code>true</code> is returned,
   * if not, it is just disposed and <code>false</code> is returned.
   * A typical use for this is to execute commands created during the execution of another command, e.g.,
   * <pre>
   *   class MyCommand extends AbstractCommand
   *   {
   *     protected Command subcommand;
   *
   *     //...
   *
   *     public void execute()
   *     {
   *       // ...
   *       StrictCompoundCommand subcommands = new StrictCompoundCommand();
   *       subcommands.appendAndExecute(new AddCommand(...));
   *       if (condition) subcommands.appendAndExecute(new AddCommand(...));
   *       subcommand = subcommands.unwrap();
   *     }
   *
   *     public void undo()
   *     {
   *       // ...
   *       subcommand.undo();
   *     }
   *
   *     public void redo()
   *     {
   *       // ...
   *       subcommand.redo();
   *     }
   *
   *     public void dispose()
   *     {
   *       // ...
   *       if (subcommand != null)
   *      {
   *         subcommand.dispose();
   *       }
   *     }
   *   }
   * </pre>
   * @return whether the command was successfully executed and appended.
   */
  @Override
  public boolean appendAndExecute(Command command)
  {
    if (command != null)
    {
      if (!isPrepared)
      {
        if (commandList.isEmpty())
        {
          isPrepared = true;
          isExecutable = true;
        }
        else
        {
          isExecutable = prepare();
          isPrepared = true;
          isPessimistic = true;
          if (isExecutable)
          {
            execute();
          }
        }
      }
  
      if (command.canExecute())
      {
        try
        {
          command.execute();
          commandList.add(command);
          ++rightMostExecutedCommandIndex;
          isUndoable = command.canUndo();
          return true;
        }
        catch (RuntimeException exception)
        {
          CommonPlugin.INSTANCE.log
            (new WrappedException
              (CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), exception).fillInStackTrace());
        }
      }
  
      command.dispose();
    }

    return false;
  }

  /*
   * Javadoc copied from base class.
   */
  @Override
  public String toString()
  {
    StringBuilder result = new StringBuilder(super.toString());
    result.append(" (isUndoable: " + isUndoable + ")");
    result.append(" (isPessimistic: " + isPessimistic + ")");
    result.append(" (rightMostExecutedCommandIndex: " + rightMostExecutedCommandIndex + ")");

    return result.toString();
  }
}
