/**
 * GUI Commands
 * Copyright 2004 Andrew Pietsch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Id: UndoController.java,v 1.7 2005/08/26 21:45:10 pietschy Exp $
 */
package org.pietschy.command.undo;

import org.pietschy.command.*;

import javax.swing.event.*;
import java.util.Iterator;

/**
 * The UndoController monitors instances of {@link UndoableEventSource} and manages undo
 * and redo via its {@link #getUndoCommand undo} and {@link #getRedoCommand redo} commands.  The
 * most notable instance of {@link UndoableEventSource} is {@link UndoableActionCommand}.
 * <p>
 * The undo state is managed by an instance of {@link UndoContext} and can be changed at any time.
 * This allows undo state to be split into separate contexts, such as for a multi file editor.
 *
 * @author andrewp
 * @version $Revision: 1.7 $
 * @see UndoableEventSource
 * @see UndoableActionCommand
 */
public class
UndoController
implements UndoableEditListener
{
   private UndoContext undoContext;
   private ChangeListener contextListener;

   private CommandManager commandManager;
   private CommandManagerListener commandContainerListener;

   private EventListenerList listenerList = new EventListenerList();

   private UndoCommand undoCommand;
   private RedoCommand redoCommand;

   /**
    * Creates a new undo controller.  The command id used for the {@link #getUndoCommand undo command}
    * will be "gui-commands.undo" and the id used for the {@link #getRedoCommand redo command} will be
    * "gui-commands.redo"
    * @param commandManager the {@link org.pietschy.command.CommandManager} to which the controller belongs.  It will
    * automatically export its undo and redo commands to the container and track any undoable commands
    * that are registered with it.
    */
   public UndoController(CommandManager commandManager)
   {
      this(commandManager, new UndoContext());
   }

   /**
    * Creates a new controller using the specified {@link UndoContext}.
    * The command id used for the {@link #getUndoCommand undo command}
    * will be "gui-commands.undo" and the id used for the {@link #getRedoCommand redo command} will be
    * "gui-commands.redo"
    *
    * @param commandManager the {@link org.pietschy.command.CommandManager} to which the controller belongs.  It will
    * automatically export its undo and redo commands to the container and track any undoable commands
    * that are registered with it.
    * @param undoContext    the {@link UndoContext} to use.
    */
   public UndoController(CommandManager commandManager, UndoContext undoContext)
   {
      this(commandManager, undoContext, "gui-commands.undo", "gui-commands.redo");
   }

   /**
    * Create a new controller using the specified ids for the {@link #getUndoCommand undo} and
    * {@link #getRedoCommand redo} commands
    *
    * @param commandManager the {@link org.pietschy.command.CommandManager} to which the controller belongs.  It will
    * automatically export its undo and redo commands to the container and track any undoable commands
    * that are registered with it.
    * @param undoCommandId the id to use for the undo command.
    * @param redoCommandId the id to use for the redo command.
    */
   public UndoController(CommandManager commandManager, String undoCommandId, String redoCommandId)
   {
      this(commandManager, new UndoContext(), undoCommandId, redoCommandId);
   }

   /**
    * Create a new controller using the specified {@link UndoContext} and ids for the
    * {@link #getUndoCommand undo} and {@link #getRedoCommand redo} commands
    *
    * @param commandManager the {@link org.pietschy.command.CommandManager} to which the controller belongs.  It will
    * automatically export its undo and redo commands to the container and track any undoable commands
    * that are registered with it.
    * @param undoContext the {@link UndoContext} to use.
    * @param undoCommandId the id to use for the undo command.
    * @param redoCommandId the id to use for the redo command.
    */
   public UndoController(CommandManager commandManager, UndoContext undoContext, String undoCommandId, String redoCommandId)
   {
      this.commandManager = commandManager;

      contextListener = new ChangeListener()
      {
         public void stateChanged(ChangeEvent e)
         {
            fireStateChaged();
         }
      };

      setUndoContext(undoContext);

      undoCommand = new UndoCommand(undoCommandId, this);
      redoCommand = new RedoCommand(redoCommandId, this);
      exportCommands();

      commandManager.addCommandManagerListener(new CommandManagerListener()
      {
         public void
         commandRegistered(CommandManagerEvent event)
         {
            Command command = event.getCommand();
            if (event.getSource().equals(getCommandManager()) && command instanceof UndoableEventSource)
            {
               registerUndoableCommand((UndoableEventSource) command);
            }
         }
      });

      for (Iterator iter = commandManager.commandIterator(); iter.hasNext();)
      {
         Command command = (Command) iter.next();
         if (command instanceof UndoableEventSource)
            registerUndoableCommand((UndoableEventSource) command);
      }
   }

   protected void
   exportCommands()
   {
      undoCommand.export();
      redoCommand.export();
   }

   public CommandManager
   getCommandManager()
   {
      return commandManager;
   }

   /**
    * Manually registers an {@link UndoableEventSource} with this controller.
    *
    * @param command the {@link UndoableEventSource} to register.
    * @see UndoableEventSource
    * @see UndoableActionCommand
    */
   public void
   registerUndoableCommand(UndoableEventSource command)
   {
      command.addUndoableEditListener(this);
   }

   /**
    * Notifies the controller that an undoable event has happened.  This event will be added to the
    * current {@link UndoContext} and the undo and redo commands udpated.
    *
    * @param e the {@link UndoableEditEvent}.
    */
   public void
   undoableEditHappened(UndoableEditEvent e)
   {
      if (undoContext != null)
      {
         undoContext.undoableEditHappened(e);
         fireStateChaged();
      }
   }

   /**
    * Configures the controller to use the specified {@link UndoContext}.  The undo and redo commands
    * will update according the the state of the context and all new {@link UndoableEditEvent}s
    * will be registed with it.
    *
    * @param undoContext the undo context to use.
    */
   public void
   setUndoContext(UndoContext undoContext)
   {
      if (this.undoContext != null)
         this.undoContext.removeChangeListener(contextListener);

      this.undoContext = undoContext;

      if (this.undoContext != null)
         this.undoContext.addChangeListener(contextListener);

      fireStateChaged();
   }

   /**
    * Gets the current {@link UndoContext} in use.
    *
    * @return the current {@link UndoContext} in use by the controller.
    */
   public UndoContext
   getUndoContext()
   {
      return undoContext;
   }

   protected void
   undo()
   {
      undoContext.undo();
   }

   protected void
   redo()
   {
      undoContext.redo();
   }

   /**
    * Gets an {@link ActionCommand} that when invoked will undo the last registered
    * {@link UndoableEditEvent}.
    *
    * @return an {@link ActionCommand} that will undo the the last registered
    *         {@link UndoableEditEvent}.
    */
   public ActionCommand
   getUndoCommand()
   {
      return undoCommand;
   }

   /**
    * Gets an {@link ActionCommand} that when invoked will redo the last undone {@link UndoableEditEvent}.
    *
    * @return an {@link ActionCommand} that will undo the the last undone {@link UndoableEditEvent}.
    */
   public ActionCommand
   getRedoCommand()
   {
      return redoCommand;
   }


   public boolean
   canUndo()
   {
      return undoContext != null && undoContext.canUndo();
   }

   public boolean
   canRedo()
   {
      return undoContext != null && undoContext.canRedo();
   }

   private void
   fireStateChaged()
   {
      ChangeEvent event = null;
      // Guaranteed to return a non-null array
      Object[] listeners = listenerList.getListenerList();
      // Process the listeners last to first, notifying
      // those that are interested in this event
      for (int i = listeners.length - 2; i >= 0; i -= 2)
      {
         if (listeners[i] == ChangeListener.class)
         {
            // Lazily create the event:
            if (event == null)
               event = new ChangeEvent(this);
            ((ChangeListener) listeners[i + 1]).stateChanged(event);
         }
      }
   }

   public void
   addChangeListener(ChangeListener l)
   {
      listenerList.add(ChangeListener.class, l);
   }

   public void
   removeChangeListener(ChangeListener l)
   {
      listenerList.add(ChangeListener.class, l);
   }
}
