/**
 * 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: CommandManager.java,v 1.30 2006/02/26 00:59:04 pietschy Exp $
 */

package org.pietschy.command;

import org.pietschy.command.log.Logger;
import org.pietschy.command.log.LoggerFactory;
import org.pietschy.command.log.NullLoggerFactory;

import javax.swing.*;
import javax.swing.event.EventListenerList;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.*;

/**
 * The CommandManger is the core of the GUI Command libarary.  It provides the mechanism to load
 * command configuration files and to manage and locate {@link ActionCommand} and {@link CommandGroup}
 * instances.
 *
 * @see #load(java.io.File)
 * @see #load(java.net.URL)
 * @see #load(java.io.InputStream)
 * @see #getGroup
 * @see #getCommand
 */
public class
CommandManager
implements CommandManagerListener
{

   private static Boolean macOs;

   private static LoggerFactory loggerFactory = new NullLoggerFactory();

   private static CommandManager defaultInstance;

   private static MenuFactory defaultMenuFactory = new DefaultMenuFactory();
   private static ButtonFactory defaultButtonFactory = new DefaultButtonFactory();
   private static ToolbarFactory defaultToolbarFactory = new DefaultToolbarFactory();


   private CommandManager parent;
   private Object context;
   protected HashMap registeredCommands = new HashMap();
   private EventListenerList listenerList = new EventListenerList();

   private BuilderRegistry commandBuilders;
   private ConfigurationManager configManager;
   private FaceManager faceManager;
   private ConditionEvaluator conditionEvaluator;
   private ResourceBundle resourceBundle;
   private IconFactory iconFactory;

   private MenuFactory menuFactory;
   private ButtonFactory buttonFactory;
   private ToolbarFactory toolbarFactory;

   private GroupFactory groupFactory;
   private GroupMemberFactory groupMemberFactory;

   private ClassLoader classLoader;
   private EventListenerList hoverListeners = new EventListenerList();

   private HoverListener hoverAdapter = new HoverListener()
   {
      public void hoverStarted(HoverEvent e)
      {
         fireHoverStarted(e);
      }

      public void hoverEnded(HoverEvent e)
      {
         fireHoverEnded(e);
      }
   };



   //--------------------------
   // Static methods.
   //

   /**
    * Returns an instance of the CommandManager.  The actuall object returned can be specified
    * by calling {@link #setDefaultInstance} before calling this method.
    *
    * @return the current installed CommandManager
    * @deprecated {@link #defaultInstance} should now be the preferred method for obtaining the
    *             global command manager.  This method will be removed in a future release.
    */
   public static CommandManager
   instance()
   {
      return defaultInstance();
   }

   /**
    * Returns an instance of the CommandManager.  The actuall object returned can be specified
    * by calling {@link #setDefaultInstance} before calling this method.
    *
    * @return the current installed CommandManager
    */
   public static CommandManager
   defaultInstance()
   {
      if (defaultInstance == null)
         defaultInstance = new CommandManager(null, null);

      return defaultInstance;
   }


   /**
    * Sets the command manager to use for {@link #defaultInstance} singleton.  The method must be
    * called before
    * {@link #defaultInstance()} is called.  Otherwise an {@link IllegalStateException} will be
    * thrown.
    *
    * @param manager the CommandManager instance to use as the default.
    */
   public static void
   setDefaultInstance(CommandManager manager)
   {
      if (manager == null)
         throw new NullPointerException("manager is null");

      if (defaultInstance != null)
         throw new IllegalStateException("Default CommandManager instance already installed");

      defaultInstance = manager;
   }

   /**
    * Gets a new logger for the command library.
    *
    * @see #setLoggerFactory
    */
   public static Logger
   getLogger(Class aClass)
   {
      return loggerFactory.getLogger(aClass);
   }

   /**
    * Sets the log factory to use for creating new logger instances.
    *
    * @param loggerFactory the factory to use.
    * @see #getLogger
    */
   public static void
   setLoggerFactory(LoggerFactory loggerFactory)
   {
      CommandManager.loggerFactory = loggerFactory;
   }

   /**
    * A convenience method for GUI Commands to check if we're running in an
    * apple Mac environment.
    *
    * @return <code>true</code> if running in the Mac environment, <code>false</code> otherwise.
    */
   public static boolean
   isMacOS()
   {
      if (macOs == null)
      {
         try
         {
            macOs = Boolean.valueOf(System.getProperty("mrj.version") != null);
         }
         catch (Throwable e)
         {
            // oops, we're probably running in an unsigned webstart application..
            // lets just see if the system look and feel is in the apple package
            macOs = Boolean.valueOf(UIManager.getSystemLookAndFeelClassName().startsWith("apple."));
         }
      }
      return macOs.booleanValue();
   }


   /**
    * Gets the default {@link MenuFactory}.
    *
    * @return the default {@link MenuFactory}.
    */
   public static MenuFactory
   getDefaultMenuFactory()
   {
      return defaultMenuFactory;
   }

   /**
    * Configures the default factory to be used when creating menus.  This can be overriden
    * on a per command manager basis by calling {@link CommandManager#setMenuFactory(MenuFactory)}.
    *
    * @param defaultMenuFactory the factory to use when creating menus.
    */
   public static void
   setDefaultMenuFactory(MenuFactory defaultMenuFactory)
   {
      CommandManager.defaultMenuFactory = defaultMenuFactory;
   }

   /**
    * Gets the default {@link ButtonFactory}.
    *
    * @return the default {@link ButtonFactory}.
    */
   public static ButtonFactory
   getDefaultButtonFactory()
   {
      return defaultButtonFactory;
   }

   /**
    * Configures the default factory to be used when creating buttons.  This can be overriden
    * on a per command manager basis by calling {@link CommandManager#setButtonFactory(ButtonFactory)}.
    *
    * @param defaultButtonFactory the factory to use when creating buttons.
    */
   public static void
   setDefaultButtonFactory(ButtonFactory defaultButtonFactory)
   {
      CommandManager.defaultButtonFactory = defaultButtonFactory;
   }

   /**
    * Gets the default {@link ToolbarFactory}.
    *
    * @return the default {@link ToolbarFactory}.
    */
   public static ToolbarFactory
   getDefaultToolbarFactory()
   {
      return defaultToolbarFactory;
   }

   /**
    * Configures the default factory to be used when creating toolbars and toolbar buttons.  This
    * can be overriden on a per command manager basis by calling {@link CommandManager#setToolbarFactory(ToolbarFactory)}.
    *
    * @param defaultToolbarFactory the factory to use when creating toolbar and toolbar buttons.
    */
   public static void
   setDefaultToolbarFactory(ToolbarFactory defaultToolbarFactory)
   {
      CommandManager.defaultToolbarFactory = defaultToolbarFactory;
   }

   //--------------------------
   // Class Implementation
   //

   /**
    * Constructs and initializes the command manager.
    */
   public
   CommandManager()
   {
      this(null);
   }

   /**
    * Constructs and initializes the command manager.
    */
   public
   CommandManager(Object context)
   {
      this.context = context;
      init();
   }

   /**
    * Constructs and initializes the command manager.
    */
   public
   CommandManager(CommandManager parent)
   {
      this(parent, null);
   }

   /**
    * Constructs and initializes the command manager.
    */
   public
   CommandManager(CommandManager parent, Object context)
   {
      this.parent = parent;
      this.context = context;
      init();

      if (this.parent != null)
         this.parent.addCommandManagerListener(this);
   }

   private void
   init()
   {
      groupFactory = new DefaultGroupFactory();
      groupMemberFactory = new DefaultGroupMemberFactory();

      conditionEvaluator = new DefaultConditionEvaluator();

      commandBuilders = new BuilderRegistry(this);
      faceManager = new FaceManager(this);
      faceManager.setFaceBuilder(new DefaultFaceBuilder());
      configManager = new ConfigurationManager(this, faceManager, commandBuilders);

      commandBuilders.register(ActionCommand.class, new ActionCommandBuilder());
      commandBuilders.register(CommandGroup.class, new CommandGroupBuilder());

      classLoader = getClass().getClassLoader();
   }


   /**
    * Gets the parent of this {@link CommandManager}.  If present, the parent is used to resolve
    * requests for {@link ActionCommand ActionCommands} that aren't present in this
    * {@link CommandManager}
    *
    * @return the parent {@link CommandManager} or <tt>null</tt> if it doesn't have a parent.
    */
   public CommandManager
   getParent()
   {
      return parent;
   }


   /**
    * This method removes any listeners from this command managers parent (if it exists).  This method
    * should only be called when the command manager is no longer in use.
    */
   public void
   dispose()
   {
      if (parent != null)
      {
         parent.removeCommandManagerListener(this);
         parent = null;
      }
   }

   /**
    * Gets the context object specified in the constructor.
    *
    * @return this managers context object, or <tt>null</tt> if there is none.
    */
   public Object
   getContext()
   {
      return context;
   }

   /**
    * Gets the class loader the library is to use.  If not explicitly set, this defaults
    * to the class loader that loaded the command manager.
    *
    * @return the {@link ClassLoader} the library is to use.
    * @see #setClassLoader
    */
   public ClassLoader
   getClassLoader()
   {
      return classLoader;
   }

   /**
    * Sets the class loader the libary is to use for opertations like loading icons from the
    * classpath. This parameter must be set before calling {@link #load} to take affect.
    *
    * @param classLoader the class loader to use for resource loading.
    */
   public void
   setClassLoader(ClassLoader classLoader)
   {
      this.classLoader = classLoader;
   }

   /**
    * Gets the {@link java.util.ResourceBundle} for resolving i18n configuration parameters.  This value is
    * <tt>null</tt> by default.
    *
    * @return the command managers ResourceBundle
    * @see #setResourceBundle(java.util.ResourceBundle)
    */
   public ResourceBundle
   getResourceBundle()
   {
      return resourceBundle;
   }

   /**
    * Sets the {@link java.util.ResourceBundle} for resolving i18n configuration parameters.
    *
    * @param resourceBundle the ResourceBundle for resolving i18n configuration parameters.
    * @see #getResourceBundle()
    */
   public void
   setResourceBundle(ResourceBundle resourceBundle)
   {
      this.resourceBundle = resourceBundle;
   }

   /**
    * Gets the commands managers current condition evaluator.  Conditions are used by
    * groups to determine if a given command should be included.
    *
    * @return the current condition evaluator.
    */
   public ConditionEvaluator
   getConditionEvaluator()
   {
      return conditionEvaluator;
   }

   /**
    * Sets the commands managers condition evaluator.  Conditions are used by
    * groups to determine if a given command should be included.  This must be configured
    * before an command files are loaded.  <p>
    * The default implementation is an instance of {@link DefaultConditionEvaluator}.
    *
    * @param conditionEvaluator the new condition evaluator.
    */
   public void
   setConditionEvaluator(ConditionEvaluator conditionEvaluator)
   {
      this.conditionEvaluator = conditionEvaluator;
   }

   /**
    * Gets this command managers {@link IconFactory}.  If the icon factory
    * hasn't been configured, the parent, if present, is queried.
    *
    * @return this command managers {@link IconFactory}.
    */
   public IconFactory
   getIconFactory()
   {
      if (iconFactory == null && parent != null)
         return parent.getIconFactory();

      return iconFactory;
   }

   /**
    * Sets this command managers {@link IconFactory}.
    *
    * @param iconFactory this command managers {@link IconFactory}.
    */
   public void
   setIconFactory(IconFactory iconFactory)
   {
      this.iconFactory = iconFactory;
   }

   /**
    * Checks if tooltip are globally enabled on menus.  This setting will only be used by
    * {@link Face faces} that haven't been explicitly configured to enable or disable tooltips on
    * menus.
    *
    * @return <tt>true</tt> if tooltips are enabled on menus, <tt>false</tt> otherwise.
    */
   public boolean
   isMenuTooltipsEnabled()
   {
      return faceManager.isMenuTooltipsEnabled();
   }

   /**
    * Configures the current menu tooltip state and notifies all {@link Face faces} that the
    * state has changed.  This setting will be used by all {@link Face faces} that haven't been
    * explicitly configured to enable or disable tooltips on menus.
    *
    * @param menuTooltipsEnabled <tt>true</tt> to enable tooltips on menus, <tt>false</tt> to
    *                            disable them.
    */
   public void
   setMenuTooltipsEnabled(boolean menuTooltipsEnabled)
   {
      faceManager.setMenuTooltipsEnabled(menuTooltipsEnabled);
   }

   /**
    * Gets the {@link MenuFactory} that is to be used by commands to create menu items.  If not
    * explicitly set, the global {@link #getDefaultMenuFactory()} will be used.
    *
    * @return the {@link MenuFactory} that is to be used by commands to create menu items.
    * @see #setMenuFactory
    */
   public MenuFactory
   getMenuFactory()
   {
      if (menuFactory != null)
         return menuFactory;

      return getDefaultMenuFactory();
   }

   /**
    * Sets the {@link MenuFactory} that is to be used by commands to create menu items.  This
    * factory will be used unless a specific factory has been configured for an individual
    * command.
    */
   public void
   setMenuFactory(MenuFactory factory)
   {
      menuFactory = factory;
   }

   /**
    * Gets the default {@link ButtonFactory} to use for buttons created by this command manager.
    * If explicitly configured the global {@link #getDefaultButtonFactory()} will be used.
    *
    * @return the button factory.
    */
   public ButtonFactory
   getButtonFactory()
   {
      if (buttonFactory != null)
         return buttonFactory;

      return getDefaultButtonFactory();
   }

   /**
    * Sets the {@link ButtonFactory} that will be the default used by all {@link ActionCommand}s and
    * {@link CommandGroup}s.
    *
    * @param factory the default {@link ButtonFactory}
    */
   public void
   setButtonFactory(ButtonFactory factory)
   {
      buttonFactory = factory;
   }

   /**
    * Gets the default {@link ToolbarFactory} to be used by commands to create toolbars and their
    * associated buttons.  If not explicitly set, the global {@link #getDefaultToolbarFactory()}
    * will be used.
    *
    * @return the default {@link ToolbarFactory}.
    */
   public ToolbarFactory
   getToolbarFactory()
   {
      if (toolbarFactory != null)
         return toolbarFactory;

      return getDefaultToolbarFactory();
   }

   /**
    * Sets the {@link ToolbarFactory} that will be the default used by all commands when creating
    * toolbars and their associated buttons.
    *
    * @param factory the default {@link ToolbarFactory}.
    */
   public void
   setToolbarFactory(ToolbarFactory factory)
   {
      toolbarFactory = factory;
   }


   /**
    * Gets the {@link GroupFactory} being used by the library.  This factory is used
    * by the library whenever a group must be created.  This is typically during calls
    * to {@link #getGroup}.
    *
    * @return the {@link GroupFactory} being used by the library.
    * @see #setGroupFactory
    */
   public GroupFactory
   getGroupFactory()
   {
      return groupFactory;
   }

   /**
    * Sets the {@link GroupFactory} being used by the library.  This factory will be used
    * by the library whenever a group must be created.  This is typically during calls
    * to {@link #getGroup}.
    *
    * @param groupFactory the {@link GroupFactory} to use.
    */
   public void
   setGroupFactory(GroupFactory groupFactory)
   {
      this.groupFactory = groupFactory;
   }

   /**
    * Gets the {@link GroupMemberFactory} used by the library to construct {@link GroupMember}.  This factory
    * will be used by groups to create the infrastructure for managing its members.  By supplying custom implementations
    * you can control the way groups populate their containers.
    *
    * @return the default {@link GroupMemberFactory}.
    * @see CommandGroup#getMemberFactory()
    */
   public GroupMemberFactory
   getGroupMemberFactory()
   {
      return groupMemberFactory;
   }

   /**
    * Gets the {@link GroupMemberFactory} used by the library to construct {@link GroupMember}.  This factory
    * will be used by groups to create the infrastructure for managing its members.  By pluggin custome implementations
    * you can control the way groups populate their containers.
    *
    * @param groupMemberFactory the default {@link GroupMemberFactory} for all groups to use.
    */
   public void
   setGroupMemberFactory(GroupMemberFactory groupMemberFactory)
   {
      this.groupMemberFactory = groupMemberFactory;
   }

   /**
    * Gets the {@link FaceManager} in use by the CommandManager.
    *
    * @return the {@link FaceManager} in use by the CommandManager.
    */
   public FaceManager
   getFaceManager()
   {
      return faceManager;
   }


   /**
    * Load the command definitions in the specified file.
    *
    * @param file the file to load.
    * @throws LoadException if an error occurs loading the file.
    */
   public void
   load(File file)
   throws LoadException
   {
      load(file, Locale.getDefault());
   }

   /**
    * Load the command definitions in the specified file.
    *
    * @param file   the file to load.
    * @param locale the locale to load.
    * @throws LoadException if an error occurs loading the file.
    */
   public void
   load(File file, Locale locale)
   throws LoadException
   {
      configManager.load(file, locale);
   }

   /**
    * Load the command definitions from the specified URL.
    *
    * @param url the url of the command definition file.
    * @throws LoadException if an error occurs loading the file.
    */
   public void
   load(URL url)
   throws LoadException
   {
      load(url, Locale.getDefault());
   }

   /**
    * Load the command definitions from the specified URL.
    *
    * @param url    the url of the command definition file.
    * @param locale the locale to load.
    * @throws LoadException if an error occurs loading the file.
    */
   public void
   load(URL url, Locale locale)
   throws LoadException
   {
      configManager.load(url, locale);
   }

   /**
    * Load the command definitions using the specified reader.
    *
    * @param in the reader to use.
    * @throws LoadException if an error occurs loading the file.
    */
   public void
   load(InputStream in)
   throws LoadException
   {
      load(in, Locale.getDefault());
   }

   /**
    * Load the command definitions using the specified reader.
    *
    * @param in     the reader to use.
    * @param locale the locale to load.
    * @throws LoadException if an error occurs loading the file.
    */
   public void
   load(InputStream in, Locale locale)
   throws LoadException
   {
      configManager.load(in, locale);
   }


   /**
    * Initialises the command from the current loaded set of properties.
    *
    * @param command the {@link Command} to initialise.
    */
   protected void
   configure(Command command)
   {
      command.addHoverListener(hoverAdapter);
      configManager.configure(command);
   }


   /**
    * Checks if the command identified by the specified id is a group.
    *
    * @param commandId the command id.
    * @return <tt>true</tt> if the command id represents a {@link CommandGroup}, <tt>false</tt>
    *         otherwise.
    */
   public boolean
   isGroup(String commandId)
   {
      return configManager.isGroup(commandId);
   }

   /**
    * Adds a {@link HoverListener} to the command manager.  The listener will be notified when
    * ever the mouse hovers over a command.
    *
    * @param l the hover listener
    * @see HoverListener
    * @see #removeHoverListener
    */
   public void
   addHoverListener(HoverListener l)
   {
      hoverListeners.add(HoverListener.class, l);
   }

   /**
    * Removes the {@link HoverListener} from the command manager.
    *
    * @param l the hover listener
    * @see HoverListener
    * @see #addHoverListener
    */
   public void
   removeHoverListener(HoverListener l)
   {
      hoverListeners.remove(HoverListener.class, l);
   }

   protected void
   fireHoverStarted(HoverEvent e)
   {
      // Guaranteed to return a non-null array
      Object[] listeners = hoverListeners.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] == HoverListener.class)
         {
            ((HoverListener) listeners[i + 1]).hoverStarted(e);
         }
      }
   }

   protected void
   fireHoverEnded(HoverEvent e)
   {
      // Guaranteed to return a non-null array
      Object[] listeners = hoverListeners.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] == HoverListener.class)
         {
            ((HoverListener) listeners[i + 1]).hoverEnded(e);
         }
      }
   }

   /**
    * Registers this command with the manager so it can be accessed by the
    * rest of the application.
    *
    * @param command the command to register
    */
   protected void
   registerCommand(Command command)
   {
      if (command == null)
         throw new NullPointerException("command is null");

      if (command.isAnonymous())
         throw new IllegalStateException("Command is anonymous");

      if (registeredCommands.containsKey(command.getId()))
         throw new IllegalStateException("Command " + command.getId() + " already registered");

      registeredCommands.put(command.getId(), command);

      fireCommandRegistered(command);
   }

   protected boolean
   isRegistered(Command command)
   {
      return registeredCommands.containsKey(command.getId());
   }

   /**
    * Gets the specifed command from this container.  If the command isn't registered with this
    * container then the request is delegated to the containers parent.
    *
    * @param commandId the commands id.
    * @return the command with the specified id, or <tt>null</tt> if it hasn't been registered
    *         with this container of one of its parents.
    */
   public ActionCommand
   getCommand(String commandId)
   {
      ActionCommand c = (ActionCommand) registeredCommands.get(commandId);

      // try and get the command from our parent if would couldn't find it here.
      if (c == null && parent != null)
         c = parent.getCommand(commandId);

      return c;
   }


   /**
    * Gets the specifed group from the container.  If the group hasn't already beed registered
    * with this container, it will be be created automatically and registered.  Please note that
    * parent containers are never checked.
    *
    * @param groupId the Id of the group.
    * @return the {@link CommandGroup} with the specified Id.
    */
   public CommandGroup
   getGroup(String groupId)
   {
      CommandGroup group = (CommandGroup) registeredCommands.get(groupId);

      // if this is the first time the group has been requested, we create it.
      if (group == null)
      {
         group = (CommandGroup) configManager.instantiateGroup(groupId, getGroupFactory());
         // it's possible the group isn't event defined and we return null in this case.
         if (group != null)
         {
            // this will register the command so we don't create it again.. I should
            // really clean this up a bit..
            group.export();
         }
      }

      return group;
   }


   public Iterator
   commandIterator()
   {
      return Collections.unmodifiableCollection(registeredCommands.values()).iterator();
   }

   /**
    * Pass on events from our parent.
    *
    * @param event
    */
   public void
   commandRegistered(CommandManagerEvent event)
   {
      fireCommandRegistered(event.getCommand());
   }

   public void
   addCommandManagerListener(CommandManagerListener l)
   {
      listenerList.add(CommandManagerListener.class, l);
   }

   public void
   removeCommandManagerListener(CommandManagerListener l)
   {
      listenerList.remove(CommandManagerListener.class, l);
   }

   public CommandManagerListener[]
   getCommandManagerListeners()
   {
      return (CommandManagerListener[]) listenerList.getListeners(CommandManagerListener.class);
   }

   protected void
   fireCommandRegistered(Command command)
   {
      if (command == null)
         throw new NullPointerException("command is null");

      CommandManagerEvent 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] == CommandManagerListener.class)
         {
            // Lazily create the event:
            if (event == null)
               event = new CommandManagerEvent(this, command);

            ((CommandManagerListener) listeners[i + 1]).commandRegistered(event);
         }
      }
   }
}


