/**
 * 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: CommandGroup.java,v 1.8 2007/01/03 22:41:23 pietschy Exp $
 */

package org.pietschy.command;

import javax.swing.*;
import javax.swing.event.EventListenerList;

/**
 * GroupCommands provide collections of other {@link ActionCommand} and {@link ToggleCommandGroup}s.
 */
public class
CommandGroup
extends Command  
implements CommandManagerListener
{
   protected MemberList memberList;

   /**
    * Creates a new anonymous GroupCommand. Anonymous groups must be fully programatically
    * generated and can't be automatically discovered by other groups.
    */
   public CommandGroup(CommandManager commandManager)
   {
      super(commandManager);
   }

   /**
    * Creates a group that is bound to {@link CommandManager#defaultInstance}.
    *
    * @param id the id of the group.
    */
   public CommandGroup(String id)
   {
      this(CommandManager.defaultInstance(), id);
   }

   /**
    * Creates a new command groups with the specified id.  CommandGroups are lazy loading in
    * that they will store the only the command ids until they are asked to create a button
    * or menu.  The command will use the specifed {@link CommandManager} to obtain commands.
    *
    * @param groupId this groups unique id.
    */
   public CommandGroup(CommandManager commandManager, String groupId)
   {
      super(commandManager, groupId);
      // groups listen to any changes in the command manager.
      commandManager.addCommandManagerListener(this);
   }

   /**
    * Creates a new menu item for this command.
    *
    * @return a new JMenuItem
    */
   public JMenuItem
   createMenuItem(MenuFactory factory, String faceName)
   {
      internalLog.enter("createMenuItem");
      JMenu menu = factory.createMenu();
      attach(menu, faceName);
      bindMembers(menu, factory, faceName);
      internalLog.exit("createMenuItem");
      return menu;
   }


   /**
    * Create a new button for this command using the specified {@link ButtonFactory}
    * and {@link Face}.
    *
    * @return a new JButton
    */
   public AbstractButton
   createButton(ButtonFactory factory, String faceName)
   {
      return createButton(factory, faceName, getMenuFactory(), Face.POPUP);
   }

   /**
    * Creates a new button using the specified id's for the button and menu faces and the specified factories for the
    * button and menu items.
    *
    * @return a new JButton
    */
   public AbstractButton
   createButton(ButtonFactory buttonFactory, String buttonFaceName,
                MenuFactory menuFactory, String menuFaceName)
   {
      internalLog.enter("createButton");
      AbstractButton button = buttonFactory.createToggleButton();

      attach(button, buttonFaceName);

      JPopupMenu popup = menuFactory.createPopupMenu();
      bindMembers(popup, menuFactory, menuFaceName);
      PopupAdapter.bind(button, popup);

      internalLog.exit("createButton");


      return button;
   }


   /**
    * Creates a popup menu using {@link Face#MENU} and the default {@link MenuFactory}.
    *
    * @return a popup menu for the group.
    */
   public JPopupMenu
   createPopupMenu()
   {
      return createPopupMenu(Face.POPUP, getMenuFactory());
   }

   /**
    * Creates a popup menu for this group using the specified {@link Face} id and {@link MenuFactory}.
    *
    * @param faceName the id of the {@link Face} to use.
    * @param factory  the {@link MenuFactory} to use to generate the menu items.
    * @return a new popup menu for this group.
    */
   public JPopupMenu
   createPopupMenu(String faceName, MenuFactory factory)
   {
      internalLog.enter("createPopupMenu");
      JPopupMenu popup = factory.createPopupMenu();

      bindMembers(popup, factory, faceName);

      internalLog.exit("createPopupMenu");
      return popup;
   }

   /**
    * Creates a new {@link JToolBar} bound to this GroupCommand.
    */
   public JToolBar
   createToolBar()
   {
      return createToolBar(Face.TOOLBAR, getToolbarFactory());
   }

   public JToolBar
   createToolBar(ToolbarFactory factory)
   {
      return createToolBar(Face.TOOLBAR, factory);
   }

   public JToolBar
   createToolBar(String faceName)
   {
      return createToolBar(faceName, getToolbarFactory());
   }

   /**
    * Creates a toolbar using the specified facename for the toolbar and the
    * specified toolbar factory for the buttons.
    *
    * @param faceName the face name to use for the toolbar name.
    * @param factory  the {@link ButtonFactory} to use to create the toolbar buttons.
    * @return a new JToolbar bound to this GroupCommand.
    */
   public JToolBar
   createToolBar(String faceName, ToolbarFactory factory)
   {
      internalLog.enter("createToolBar");
      JToolBar toolbar = factory.createToolbar();
      toolbar.setName(getFace(faceName).getText());

      bindMembers(toolbar, factory, faceName);

      internalLog.exit("createToolBar");
      return toolbar;
   }


   public JMenuBar
   createMenuBar()
   {
      return createMenuBar(Face.MENU, getMenuFactory());
   }

   public JMenuBar
   createMenuBar(String faceName)
   {
      return createMenuBar(faceName, getMenuFactory());
   }

   public JMenuBar
   createMenuBar(MenuFactory factory)
   {
      return createMenuBar(Face.MENU, factory);
   }

   public JMenuBar
   createMenuBar(String faceName, MenuFactory factory)
   {
      internalLog.enter("createMenuBar");
      JMenuBar menubar = factory.createMenuBar();

      bindMembers(menubar, factory, faceName);

      internalLog.exit("createMenuBar");
      return menubar;
   }


   /**
    * Binds this group to the specified container.  The group will ensure the containers contents
    * are aligned with this groups members.  This binding will insert {@link JMenuItem menu items}
    * into the container.
    *
    * @param container the {@link javax.swing.JComponent container} that will hold the members of the group.
    * @param factory   the {@link MenuFactory} used to create the menu items.
    * @param faceName  the face to use for the group members.
    */
   protected void
   bindMembers(JComponent container, MenuFactory factory, String faceName)
   {
      getMemberList().bindMembers(container, factory, faceName);
   }

   /**
    * Binds this group to the specified container.  The group will ensure the containers contents
    * are aligned with this groups members.  This binding will insert {@link AbstractButton buttons}
    * into the container.
    *
    * @param container the {@link javax.swing.JComponent container} that will hold the members of the group.
    * @param factory   the {@link ButtonFactory} used to create the buttons.
    * @param faceName  the face to use for the group members.
    */
   protected void
   bindMembers(JComponent container, ButtonFactory factory, String faceName)
   {
      getMemberList().bindMembers(container, factory, faceName);
   }


   /**
    * Returns the number of members in this group.  This includes commands that have been registered
    * with the group but may not as yet been exported, it also constains commands that have been programatically
    * added.  Thus this number may be higher than the actuall number of commands that are visible in the groups
    * menus and popups.
    *
    * @return the number of registered members of this group.
    */
   public int
   getMemberCount()
   {
      return getMemberList().size();
   }

   /**
    * @param visitor the visitor.
    * @deprecated use {@link #visit(GroupVisitor)} or {@link #visitChildren(GroupVisitor)}
    *             instead.
    */
   public void
   acceptVisitor(GroupVisitor visitor)
   {
      visit(visitor);
   }

   /**
    * Visits this group.  This method does not automatically traverse the children of the
    * group, to do this the visitor can call {@link #visitChildren(GroupVisitor)}.
    *
    * @param visitor the visitior.
    */
   public void visit(GroupVisitor visitor)
   {
      visitor.visit(this);
   }

   /**
    * Accepts a {@link GroupVisitor} to this group.  For each active member of this group the
    * {@link org.pietschy.command.GroupVisitor#visit} will be called.  Only active members will be visited, that is
    * commands that have been exported or explicitly added to the group.
    *
    * @param visitor the visitor.
    */
   public void
   visitChildren(GroupVisitor visitor)
   {
      getMemberList().acceptVisitor(visitor);
   }

   public boolean
   isMember(CommandGroup group)
   {
      return getMemberList().isMember(group);
   }

   public boolean
   isInlineMember(CommandGroup group)
   {
      return getMemberList().isInlineMember(group);
   }

   /**
    * {@link CommandManagerListener} implementation.
    */
   public void
   commandRegistered(CommandManagerEvent event)
   {
      getMemberList().commandRegistered(event);
   }


   protected void
   rebuildAllPopups()
   {
      internalLog.enter("rebuildAllPopups");

      getMemberList().rebuildAllPopups();

      internalLog.exit("rebuildAllPopups");
   }

   /**
    * Checks if this group contains the specified command.  This method only checks the direct
    * chidren, it will only recurse into {@link ToggleCommandGroup} children if they have been added
    * inline.
    *
    * @param c the command to check
    * @return <tt>true</tt> if this groups children contain the command, or if a child group
    *         specified as inline contains this command.
    */
   public boolean
   contains(Command c)
   {
      return getMemberList().containsDirectlyOrInline(c);
   }

   /**
    * Resest this group back to its original state before any commands were programatically added.
    * This is only useful if the group contains an expansion point.
    */
   public void
   reset()
   {
      reset(true);
   }

   /**
    * Resest this group back to its original state before any commands were programatically added.
    * This is only useful if the group contains an expansion point.
    */
   public void
   reset(boolean rebuild)
   {
      ExpansionGroupMember expansionPoint = getMemberList().insertOrGetExpansionPoint();
      if (!expansionPoint.isEmpty())
      {
         expansionPoint.clear();
         if (rebuild)
         {
            rebuildAllPopups();
            fireMembersChanged();
         }
      }
   }

   /**
    * Adds a command to the groups expansion point and rebuilds all dependant widgets.
    * Each command is appended to the list in order of addition.  Each command can only
    * be added once.
    *
    * @param command the command to installFace.
    * @see #add(org.pietschy.command.Command, boolean)
    */
   public void
   add(Command command)
   {
      add(command, true);
   }


   /**
    * Adds a command to the groups expansion point and optionally rebuilds all
    * dependant widgets.  Each command is appended to the list in order
    * of addition.
    *
    * @param command the command to installFace.
    * @param rebuild <tt>true</tt> to rebuild all the groups widgets, <tt>false</tt> to postpone
    *                the rebuild to some later time.
    */
   public void
   add(Command command, boolean rebuild)
   {
      internalLog.enter("installFace");
      internalLog.param("command", String.valueOf(command));

      if (getMemberList().containsDirectMemberFor(command))
      {
         internalLog.debug("Command already a member: " + command);
         internalLog.exit("installFace");
         return;
      }

      GroupMember member = getMemberFactory().createSimpleMember(this, command);
      getMemberList().insertOrGetExpansionPoint().add(member);

      if (rebuild)
      {
         rebuildAllPopups();
         fireMembersChanged();
      }

      internalLog.exit("installFace");
   }


   /**
    * Adds the specified group to this groups expansion point as an inline member.  The childen
    * of the specified group will appears to the user as though they are direct members of the
    * group.  This method will rebuild all dependant widgets.
    *
    * @param group the group to installFace inline.
    */
   public void
   addInline(CommandGroup group)
   {
      addInline(group, true);
   }

   /**
    * Adds the specified group to this groups expansion point as an inline member.  The childen
    * of the specified group will appears to the user as though they are direct members of the
    * group.  This method will optionally rebuild all dependant widgets.
    *
    * @param group   the group to installFace inline.
    * @param rebuild <tt>true</tt> to rebuild all the groups widgets, <tt>false</tt> to postpone
    *                the rebuild to some later time.
    */
   public void
   addInline(CommandGroup group, boolean rebuild)
   {
      internalLog.enter("addGroup");
      internalLog.param("group", String.valueOf(group));

      if (group == null)
         return;

      GroupMember member = getMemberFactory().createInlineMember(this, group);
      getMemberList().insertOrGetExpansionPoint().add(member);

      if (rebuild)
      {
         rebuildAllPopups();
         fireMembersChanged();
      }


      internalLog.exit("addGroup");
   }


   /**
    * Removes this specified command from the group and rebuilds all dependant widgets.
    *
    * @param command the command to remove.
    */
   public void
   remove(Command command)
   {
      remove(command, true);
   }

   /**
    * Removes this specified command from the group and optionally rebuilds all dependant widgets.
    *
    * @param command the command to remove.
    * @param rebuild <tt>true</tt> to rebuild all the groups widgets, <tt>false</tt> to postpone
    *                the rebuild to some later time.
    */
   public void
   remove(Command command, boolean rebuild)
   {
      internalLog.enter("remove");
      internalLog.param("command", String.valueOf(command));

      if (command == null)
         return;

      ExpansionGroupMember expansionPoint = getMemberList().insertOrGetExpansionPoint();
      GroupMember member = expansionPoint.getMemberFor(command);
      if (member != null)
      {
         expansionPoint.remove(member);
         member.removeNotify();

         if (rebuild)
         {
            rebuildAllPopups();
            fireMembersChanged();
         }
      }


      internalLog.exit("remove");
   }


   /**
    * Inserts a separator into the command list and rebuilds all the groups dependant widgets.
    */
   public void
   addSeparator()
   {
      addSeparator(true);
   }


   /**
    * Inserts a separator into the command list and optionally rebuilds all the groups dependant widgets.
    *
    * @param rebuild <tt>true</tt> to rebuild all the groups widgets, <tt>false</tt> to postpone
    *                the rebuild to some later time.
    */
   public void
   addSeparator(boolean rebuild)
   {
      internalLog.enter("addSeparator");
      getMemberList().insertOrGetExpansionPoint().add(getMemberFactory().createSeparatorMember());
      if (rebuild)
      {
         rebuildAllPopups();
         fireMembersChanged();
      }
      internalLog.exit("addSeparator");
   }

   /**
    * Inserts a glue member into the group and rebuilds all dependant widgets.
    */
   public void
   addGlue()
   {
      addGlue(true);
   }


   /**
    * Inserts a glue member into the group and optionally rebuilds all dependant widgets.
    *
    * @param rebuild <tt>true</tt> to rebuild all the groups widgets, <tt>false</tt> to postpone
    *                the rebuild to some later time.l
    */
   public void
   addGlue(boolean rebuild)
   {
      internalLog.enter("addGlue");
      getMemberList().insertOrGetExpansionPoint().add(getMemberFactory().createGlueMember());
      if (rebuild)
      {
         rebuildAllPopups();
         fireMembersChanged();
      }
      internalLog.exit("addGlue");
   }

   protected MemberList
   getMemberList()
   {
      if (memberList == null)
      {
         // The member list is used during the configuration of the group by the command manager
         // and is called by the super class during construction (and that's bad) so I have to
         // lazily create it or I'll end up with NullPointerExceptions.
         // I should really change this behaviour so that the group members are loaded
         // separately from the face configuration.
         memberList = new MemberList(this);
      }

      return memberList;
   }


   /**
    * Gets the groups {@link GroupMemberFactory}.  This is a convenience method that delegates directly to
    * {@link CommandManager#getGroupMemberFactory()}
    *
    * @return the {@link GroupMemberFactory} this group is to use.
    * @see CommandManager#getGroupMemberFactory()
    */
   protected GroupMemberFactory
   getMemberFactory()
   {
      // WARNING: This method is used during the configuration of the group by the command manager
      // and is called by the super class during construction (and that's bad).
      // I should probably change this behaviour so that the group members are loaded
      // separately from the face configuration.
      return getCommandManager().getGroupMemberFactory();
   }

   /**
    * Adds a {@link GroupListener} to the group.
    *
    * @param l the listener to installFace.
    */
   public void
   addGroupListener(GroupListener l)
   {
      listenerList.add(GroupListener.class, l);
   }

   /**
    * Removes a {@link GroupListener} from the group.
    *
    * @param l the listener to remove.
    */
   public void
   removeGroupListener(GroupListener l)
   {
      listenerList.remove(GroupListener.class, l);
   }


   protected void
   fireMembersChanged()
   {
      GroupEvent 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] == GroupListener.class)
         {
            // Lazily create the event:
            if (event == null)
               event = new GroupEvent(this);

            ((GroupListener) listeners[i + 1]).membersChanged(event);
         }
      }
   }

   /**
    * Checks if the specified {@link Command} can be added to this group.  By default this
    * method returns <tt>true</tt> but can be overriden by subclasses to control the command
    * types that are allowable.
    *
    * @param prospectiveMember the command that is to be added to the group.
    * @return <tt>true</tt> if the command type is allowable for this group, <tt>false</tt> otherwise.
    * @see ToggleCommandGroup#isAllowableMember
    */
   public boolean
   isAllowableMember(Command prospectiveMember)
   {
      internalLog.enter("isAllowableMember");
      internalLog.returned(String.valueOf(true));
      internalLog.exit("isAllowableMember()");
      return true;
   }
}