/**
 * 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

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


/**
 * A command that comprises a sequence of subcommands.
 * Derived classes can control the way results are accumulated from the individual commands;
 * the default behaviour is to return the result of the last command.
 */
public class CompoundCommand extends AbstractCommand 
{
  /**
   * The list of subcommands.
   */
  protected List<Command> commandList;

  /**
   * When {@link #resultIndex} is set to this, 
   * {@link #getResult} and {@link #getAffectedObjects} are delegated to the last command, if any, in the list.
   */
  public static final int LAST_COMMAND_ALL = Integer.MIN_VALUE;

  /**
   * When {@link #resultIndex} is set to this, 
   * {@link #getResult} and {@link #getAffectedObjects}
   * are set to the result of merging the corresponding collection of each command in the list.
   */
  public static final int MERGE_COMMAND_ALL = Integer.MIN_VALUE - 1;

  /**
   * The index of the command whose result and affected objects are forwarded.
   * Negative values have special meaning, as defined by the static constants.
   * A value of -1 indicates that the last command in the list should be used.
   * We could have more special behaviours implemented for other negative values.
   */
  protected int resultIndex = MERGE_COMMAND_ALL;

  /**
   * Creates an empty instance.
   */
  public CompoundCommand()
  {
    super();
    commandList = new ArrayList<Command>();
  }

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

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

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

  /**
   * Creates an empty instance with the given result index.
   * @param resultIndex the {@link #resultIndex}.
   */
  public CompoundCommand(int resultIndex)
  {
    super();
    this.resultIndex = resultIndex;
    commandList = new ArrayList<Command>();
  }

  /**
   * Creates an instance with the given result index and label.
   * @param resultIndex the {@link #resultIndex}.
   * @param label the label.
   */
  public CompoundCommand(int resultIndex, String label) 
  {
    super(label);
    this.resultIndex = resultIndex;
    commandList = new ArrayList<Command>();
  }
  
  /**
   * Creates an instance with the given result index, label, and description.
   * @param resultIndex the {@link #resultIndex}.
   * @param label the label.
   * @param description the description.
   */
  public CompoundCommand(int resultIndex, String label, String description) 
  {
    super(label, description);
    this.resultIndex = resultIndex;
    commandList = new ArrayList<Command>();
  }
  
  /**
   * Creates an instance with the given result index and list.
   * @param resultIndex the {@link #resultIndex}.
   * @param commandList the list of commands.
   */
  public CompoundCommand(int resultIndex, List<Command> commandList)
  {
    super();
    this.resultIndex = resultIndex;
    this.commandList = commandList;
  }

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

  /**
   * Creates an instance with the given result index, label, description, and list.
   * @param resultIndex the {@link #resultIndex}.
   * @param label the label.
   * @param description the description.
   * @param commandList the list of commands.
   */
  public CompoundCommand(int resultIndex, String label, String description, List<Command> commandList)
  {
    super(label, description);
    this.resultIndex = resultIndex;
    this.commandList = commandList;
  }

  /**
   * Returns whether there are commands in the list.
   * @return whether there are commands in the list.
   */
  public boolean isEmpty()
  {
    return commandList.isEmpty(); 
  }

  /**
   * Returns an unmodifiable view of the commands in the list.
   * @return an unmodifiable view of the commands in the list.
   */
  public List<Command> getCommandList()
  {
    return Collections.unmodifiableList(commandList);
  }

  /**
   * Returns the index of the command whose result and affected objects are forwarded.
   * Negative values have special meaning, as defined by the static constants.
   * @return the index of the command whose result and affected objects are forwarded.
   * @see #LAST_COMMAND_ALL
   * @see #MERGE_COMMAND_ALL
   */
  public int getResultIndex()
  {
    return resultIndex;
  }

  /**
   * Returns whether all the commands can execute so that {@link #isExecutable} can be cached.
   * An empty command list causes <code>false</code> to be returned.
   * @return whether all the commands can execute.
   */
  @Override
  protected boolean prepare()
  {
    if (commandList.isEmpty())
    {
      return false;
    }
    else
    {
      for (Command command : commandList)
      {
        if (!command.canExecute())
        {
          return false;
        }
      }

      return true;
    }
  }

  /**
   * Calls {@link Command#execute} for each command in the list.
   */
  public void execute() 
  {
    for (ListIterator<Command> commands = commandList.listIterator(); commands.hasNext(); ) 
    {
      try
      {
        Command command = commands.next();
        command.execute();
      }
      catch (RuntimeException exception)
      {
        // Skip over the command that threw the exception.
        //
        commands.previous();

        try
        {
          // Iterate back over the executed commands to undo them.
          //
          while (commands.hasPrevious())
          {
            Command command = commands.previous();
            if (command.canUndo())
            {
              command.undo();
            }
            else
            {
              break;
            }
          }
        }
        catch (RuntimeException nestedException)
        {
          CommonPlugin.INSTANCE.log
            (new WrappedException
              (CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), nestedException).fillInStackTrace());
        }

        throw exception;
      }
    }
  }

  /**
   * Returns <code>false</code> if any of the commands return <code>false</code> for {@link Command#canUndo}.
   * @return <code>false</code> if any of the commands return <code>false</code> for <code>canUndo</code>.
   */
  @Override
  public boolean canUndo() 
  {
    for (Command command : commandList)
    {
      if (!command.canUndo())
      {
        return false;
      }
    }

    return true;
  }

  /**
   * Calls {@link Command#undo} for each command in the list, in reverse order.
   */
  @Override
  public void undo() 
  {
    for (ListIterator<Command> commands = commandList.listIterator(commandList.size()); commands.hasPrevious(); ) 
    {
      try
      {
        Command command = commands.previous();
        command.undo();
      }
      catch (RuntimeException exception)
      {
        // Skip over the command that threw the exception.
        //
        commands.next();

        try
        {
          // Iterate forward over the undone commands to redo them.
          //
          while (commands.hasNext())
          {
            Command command = commands.next();
            command.redo();
          }
        }
        catch (RuntimeException nestedException)
        {
          CommonPlugin.INSTANCE.log
            (new WrappedException
              (CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), nestedException).fillInStackTrace());
        }


        throw exception;
      }
    }
  }

  /**
   * Calls {@link Command#redo} for each command in the list.
   */
  public void redo() 
  {
    for (ListIterator<Command> commands = commandList.listIterator(); commands.hasNext(); ) 
    {
      try
      {
        Command command = commands.next();
        command.redo();
      }
      catch (RuntimeException exception)
      {
        // Skip over the command that threw the exception.
        //
        commands.previous();

        try
        {
          // Iterate back over the executed commands to undo them.
          //
          while (commands.hasPrevious())
          {
            Command command = commands.previous();
            command.undo();
          }
        }
        catch (RuntimeException nestedException)
        {
          CommonPlugin.INSTANCE.log
            (new WrappedException
              (CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), nestedException).fillInStackTrace());
        }

        throw exception;
      }
    }
  }

  /**
   * Determines the result by composing the results of the commands in the list;
   * this is affected by the setting of {@link #resultIndex}.
   * @return the result.
   */
  @Override
  public Collection<?> getResult()
  {
    if (commandList.isEmpty())
    {
      return Collections.EMPTY_LIST;
    }
    else if (resultIndex == LAST_COMMAND_ALL)
    {
      return commandList.get(commandList.size() - 1).getResult();
    }
    else if (resultIndex == MERGE_COMMAND_ALL)
    {
      return getMergedResultCollection();
    }
    else if (resultIndex < commandList.size())
    {
      return commandList.get(resultIndex).getResult();
    }
    else
    {
      return Collections.EMPTY_LIST;
    }
  }

  /**
   * Returns the merged collection of all command results.
   * @return the merged collection of all command results.
   */
  protected Collection<?> getMergedResultCollection()
  {
    Collection<Object> result = new ArrayList<Object>();

    for (Command command : commandList)
    {
      result.addAll(command.getResult());
    }

    return result;
  }


  /**
   * Determines the affected objects by composing the affected objects of the commands in the list;
   * this is affected by the setting of {@link #resultIndex}.
   * @return the affected objects.
   */
  @Override
  public Collection<?> getAffectedObjects()
  {
    if (commandList.isEmpty())
    {
      return Collections.EMPTY_LIST;
    }
    else if (resultIndex == LAST_COMMAND_ALL)
    {
      return commandList.get(commandList.size() - 1).getAffectedObjects();
    }
    else if (resultIndex == MERGE_COMMAND_ALL)
    {
      return getMergedAffectedObjectsCollection();
    }
    else if (resultIndex < commandList.size())
    {
      return commandList.get(resultIndex).getAffectedObjects();
    }
    else
    {
      return Collections.EMPTY_LIST;
    }
  }

  /**
   * Returns the merged collection of all command affected objects.
   * @return the merged collection of all command affected objects.
   */
  protected Collection<?> getMergedAffectedObjectsCollection()
  {
    Collection<Object> result = new ArrayList<Object>();

    for (Command command : commandList)
    {
      result.addAll(command.getAffectedObjects());
    }

    return result;
  }

  /**
   * Determines the label by composing the labels of the commands in the list;
   * this is affected by the setting of {@link #resultIndex}.
   * @return the label.
   */
  @Override
  public String getLabel()
  {
    if (label != null)
    {
      return label;
    } 
    else if (commandList.isEmpty())
    {
      return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_label");
    }
    else if (resultIndex == LAST_COMMAND_ALL || resultIndex == MERGE_COMMAND_ALL)
    {
      return commandList.get(commandList.size() - 1).getLabel();
    }
    else if (resultIndex < commandList.size())
    {
      return commandList.get(resultIndex).getLabel();
    }
    else
    {
      return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_label");
    }
  }

  /**
   * Determines the description by composing the descriptions of the commands in the list;
   * this is affected by the setting of {@link #resultIndex}.
   * @return the description.
   */
  @Override
  public String getDescription()
  {
    if (description != null)
    {
      return description;
    } 
    else if (commandList.isEmpty())
    {
      return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_description");
    }
    else if (resultIndex == LAST_COMMAND_ALL || resultIndex == MERGE_COMMAND_ALL)
    {
      return commandList.get(commandList.size() - 1).getDescription();
    }
    else if (resultIndex < commandList.size())
    {
      return commandList.get(resultIndex).getDescription();
    }
    else
    {
      return CommonPlugin.INSTANCE.getString("_UI_CompoundCommand_description");
    }
  }

  /**
   * Adds a command to this compound command's list of commands.
   * @param command the command to append.
   */
  public void append(Command command) 
  {
    if (isPrepared)
    {
      throw new IllegalStateException("The command is already prepared");
    }

    if (command != null)
    {
      commandList.add(command);
    }
  }

  /**
   * Checks if the command can execute; 
   * if so, it is executed, appended to the list, and true is returned,
   * if not, it is just disposed and false is returned.
   * A typical use for this is to execute commands created during the execution of another command, e.g.,
   * <pre>
   *   class MyCommand extends CommandBase
   *   {
   *     protected Command subcommand;
   *
   *     //...
   *
   *     public void execute()
   *     {
   *       // ...
   *       Compound subcommands = new CompoundCommand();
   *       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>
   * Another use is in an execute override of compound command itself:
   * <pre>
   *   class MyCommand extends CompoundCommand
   *   {
   *     public void execute()
   *     {
   *       // ...
   *       appendAndExecute(new AddCommand(...));
   *       if (condition) appendAndExecute(new AddCommand(...));
   *     }
   *   }
   * </pre>
   * Note that appending commands will modify what getResult and getAffectedObjects return,
   * so you may want to set the resultIndex flag.
   * @param command the command.
   * @return whether the command was successfully executed and appended.
   */
  public boolean appendAndExecute(Command command)
  {
    if (command != null)
    {
      if (!isPrepared)
      {
        if (commandList.isEmpty())
        {
          isPrepared = true;
          isExecutable = true;
        }
        else
        {
          isExecutable = prepare();
          isPrepared = true;
          if (isExecutable)
          {
            execute();
          }
        }
      }

      if (command.canExecute())
      {
        try
        {
          command.execute();
          commandList.add(command);
          return true;
        }
        catch (RuntimeException exception)
        {
          CommonPlugin.INSTANCE.log
            (new WrappedException
              (CommonPlugin.INSTANCE.getString("_UI_IgnoreException_exception"), exception).fillInStackTrace());
        }
      }

      command.dispose();
    }

    return false;
  }

  /**
   * Adds a command to this compound command's the list of commands and returns <code>true</code>, 
   * if <code>command.{@link org.eclipse.emf.common.command.Command#canExecute() canExecute()}</code> returns true;
   * otherwise, it simply calls <code>command.{@link org.eclipse.emf.common.command.Command#dispose() dispose()}</code> 
   * and returns <code>false</code>.
   * @param command the command.
   * @return whether the command was executed and appended.
   */
  public boolean appendIfCanExecute(Command command) 
  {
    if (command == null)
    {
      return false;
    }
    else if (command.canExecute())
    {
      commandList.add(command);
      return true;
    }
    else
    {
      command.dispose();
      return false;
    }
  }

  /**
   * Calls {@link Command#dispose} for each command in the list.
   */
  @Override
  public void dispose()
  {
    for (Command command : commandList)
    {
      command.dispose();
    }
  }

  /**
   * Returns one of three things: 
   * {@link org.eclipse.emf.common.command.UnexecutableCommand#INSTANCE}, if there are no commands,
   * the one command, if there is exactly one command,
   * or <code>this</code>, if there are multiple commands;
   * this command is {@link #dispose}d in the first two cases.
   * You should only unwrap a compound command if you created it for that purpose, e.g.,
   * <pre>
   *   CompoundCommand subcommands = new CompoundCommand();
   *   subcommands.append(x);
   *   if (condition) subcommands.append(y);
   *   Command result = subcommands.unwrap();
   * </pre>
   * is a good way to create an efficient accumulated result.
   * @return the unwrapped command.
   */
  public Command unwrap()
  {
    switch (commandList.size())
    {
      case 0:
      {
        dispose();
        return UnexecutableCommand.INSTANCE;
      }
      case 1:
      {
        Command result = commandList.remove(0);
        dispose();
        return result;
      }
      default:
      {
        return this;
      }
    }
  }

  @Override
  public String toString()
  {
    StringBuilder result = new StringBuilder(super.toString());
    result.append(" (commandList: #" + commandList.size() + ")");
    result.append(" (resultIndex: " + resultIndex + ")");

    return result.toString();
  }
}
