/*******************************************************************************
 * Copyright (c) 2005, 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 v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal.menus;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.commands.common.HandleObject;
import org.eclipse.core.commands.common.HandleObjectManager;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.ui.internal.misc.Policy;

/**
 * <p>
 * Manages a group of menu elements. These menu elements include things like
 * menus, groups, and items. The manager is responsible for handling the layout
 * of these items with respect to each other. Visibility is controlled
 * externally by updating the visibility of the menu elements themselves. The
 * painting of these menu elements on the application window(s) is handler by a
 * renderer.
 * </p>
 * <p>
 * Clients may instantiate, but must not extend.
 * </p>
 * <p>
 * <strong>PROVISIONAL</strong>. This class or interface has been added as
 * part of a work in progress. There is a guarantee neither that this API will
 * work nor that it will remain the same. Please do not use this API without
 * consulting with the Platform/UI team.
 * </p>
 * <p>
 * This class will eventually exist in <code>org.eclipse.jface.menus</code>.
 * </p>
 * 
 * @since 3.2
 */
public final class SMenuManager extends HandleObjectManager implements
		IPropertyChangeListener {

	/**
	 * The map of action set identifiers (<code>String</code>) to action set (
	 * <code>SActionSet</code>). This collection may be empty, but it is
	 * never <code>null</code>.
	 */
	private final Map actionSetsById = new HashMap();

	/**
	 * The set of action sets that are defined. This value may be empty, but it
	 * is never <code>null</code>.
	 */
	private final Set definedActionSets = new HashSet();

	/**
	 * The set of groups that are defined. This value may be empty, but it is
	 * never <code>null</code>.
	 */
	private final Set definedGroups = new HashSet();

	/**
	 * The set of identifiers for those menus that are defined. This value may
	 * be empty, but it is never <code>null</code>.
	 */
	private final Set definedMenus = new HashSet();

	/**
	 * The set of identifiers for those widgets that are defined. This value may
	 * be empty, but it is never <code>null</code>.
	 */
	private final Set definedWidgets = new HashSet();

	/**
	 * The map of group identifiers (<code>String</code>) to groups (
	 * <code>SGroup</code>). This collection may be empty, but it is never
	 * <code>null</code>.
	 */
	private final Map groupsById = new HashMap();

	/**
	 * The layout for this menu manager. This layout is <code>null</code> if
	 * the menu manager has changed since the last layout was built.
	 */
	private SMenuLayout layout = null;

	/**
	 * The map of menu identifiers (<code>String</code>) to menus (
	 * <code>SMenu</code>). This collection may be empty, but it is never
	 * <code>null</code>.
	 */
	private final Map menusById = new HashMap();

	/**
	 * The map of widget identifiers (<code>String</code>) to widgets (
	 * <code>SWidget</code>). This collection may be empty, but it is never
	 * <code>null</code>.
	 */
	private final Map widgetsById = new HashMap();

	/**
	 * Adds a listener to this menu manager that will be notified when this
	 * manager's state changes.
	 * 
	 * @param listener
	 *            The listener to be added; must not be <code>null</code>.
	 */
	public final void addListener(final IMenuManagerListener listener) {
		addListenerObject(listener);
	}

	/**
	 * Notifies listeners to this menu manager that it has changed in some way.
	 * 
	 * @param event
	 *            The event to fire; may be <code>null</code>.
	 */
	private final void fireMenuManagerChanged(final MenuManagerEvent event) {
		if (event == null) {
			return;
		}

		final Object[] listeners = getListeners();
		for (int i = 0; i < listeners.length; i++) {
			final IMenuManagerListener listener = (IMenuManagerListener) listeners[i];
			listener.menuManagerChanged(event);
		}
	}

	/**
	 * Gets the action set with the given identifier. If no such action set
	 * currently exists, then the action set will be created (but will be
	 * undefined).
	 * 
	 * @param actionSetId
	 *            The identifier to find; must not be <code>null</code> and
	 *            must not be zero-length.
	 * @return The action set with the given identifier; this value will never
	 *         be <code>null</code>, but it might be undefined.
	 * @see SActionSet
	 */
	public final SActionSet getActionSet(final String actionSetId) {
		checkId(actionSetId);

		SActionSet actionSet = (SActionSet) actionSetsById.get(actionSetId);
		if (actionSet == null) {
			actionSet = new SActionSet(actionSetId);
			actionSetsById.put(actionSetId, actionSet);
			actionSet.addListener(this);
		}

		return actionSet;
	}

	/**
	 * Returns those action sets that are defined.
	 * 
	 * @return The defined action sets; this value may be empty, but it is never
	 *         <code>null</code>.
	 */
	public final SActionSet[] getDefinedActionSets() {
		return (SActionSet[]) definedActionSets
				.toArray(new SActionSet[definedActionSets.size()]);
	}

	/**
	 * Returns those groups that are defined.
	 * 
	 * @return The defined groups; this value may be empty, but it is never
	 *         <code>null</code>.
	 */
	public final SGroup[] getDefinedGroups() {
		return (SGroup[]) definedGroups
				.toArray(new SGroup[definedGroups.size()]);
	}

	/**
	 * Returns those items that are defined.
	 * 
	 * @return The defined items; this value may be empty, but it is never
	 *         <code>null</code>.
	 */
	public final SItem[] getDefinedItems() {
		return (SItem[]) definedHandleObjects
				.toArray(new SItem[definedHandleObjects.size()]);
	}

	/**
	 * Returns those menus that are defined.
	 * 
	 * @return The defined menus; this value may be empty, but it is never
	 *         <code>null</code>.
	 */
	public final SMenu[] getDefinedMenus() {
		return (SMenu[]) definedMenus.toArray(new SMenu[definedMenus.size()]);
	}

	/**
	 * Returns those widgets that are defined.
	 * 
	 * @return The defined widgets; this value may be empty, but it is never
	 *         <code>null</code>.
	 */
	public final SWidget[] getDefinedWidgets() {
		return (SWidget[]) definedWidgets.toArray(new SWidget[definedWidgets
				.size()]);
	}

	/**
	 * Gets the group with the given identifier. If no such group currently
	 * exists, then the group will be created (but will be undefined).
	 * 
	 * @param groupId
	 *            The identifier to find; must not be <code>null</code> and
	 *            must not be zero-length.
	 * @return The group with the given identifier; this value will never be
	 *         <code>null</code>, but it might be undefined.
	 * @see SGroup
	 */
	public final SGroup getGroup(final String groupId) {
		checkId(groupId);

		if (Policy.EXPERIMENTAL_MENU 
				&& groupId.indexOf(LeafLocationElement.BREAKPOINT_PATH)>-1) {
			System.err.println("getMenu: " + groupId); //$NON-NLS-1$
		}
		SGroup group = (SGroup) groupsById.get(groupId);
		if (group == null) {
			group = new SGroup(groupId);
			groupsById.put(groupId, group);
			group.addListener(this);
		}

		return group;
	}

	/**
	 * Gets the item with the given identifier. If no such item currently
	 * exists, then the item will be created (but will be undefined).
	 * 
	 * @param itemId
	 *            The identifier to find; must not be <code>null</code> and
	 *            must not be zero-length.
	 * @return The item with the given identifier; this value will never be
	 *         <code>null</code>, but it might be undefined.
	 * @see SItem
	 */
	public final SItem getItem(final String itemId) {
		checkId(itemId);

		SItem item = (SItem) handleObjectsById.get(itemId);
		if (item == null) {
			item = new SItem(itemId);
			handleObjectsById.put(itemId, item);
			item.addListener(this);
		}

		return item;
	}

	/**
	 * <p>
	 * Retrieves the layout for the menu elements held by this menu manager.
	 * This layout does not consider visibility or whether the elements are
	 * currently shown. It is simply the layout if everything was visible and
	 * showing. It also does not consider dynamic menu elements, which will be
	 * asked to make changes to the layout before the menu element is shown.
	 * </p>
	 * <p>
	 * The result of this computation is cached between subsequent calls. So, if
	 * no changes are made to the menu elements, the layout can be retrieved in
	 * constant time. Otherwise, it will take <code>O(n)</code> time to
	 * compute, where <code>n</code> is the number of menu elements held by
	 * this manager.
	 * </p>
	 * 
	 * @return The menu layout; never <code>null</code>.
	 */
	public final SMenuLayout getLayout() {
//		if (layout == null) {
			// Generate the layout
			final Collection menuElements = new ArrayList(definedMenus.size()
					+ definedGroups.size() + definedHandleObjects.size()
					+ definedWidgets.size());
			menuElements.addAll(definedMenus);
			menuElements.addAll(definedGroups);
			menuElements.addAll(definedHandleObjects);
			menuElements.addAll(definedWidgets);
			layout = SMenuLayout.computeLayout(menuElements);
//		}

		return layout;
	}

	/**
	 * Gets the menu with the given identifier. If no such menu currently
	 * exists, then the menu will be created (but will be undefined).
	 * 
	 * @param menuId
	 *            The identifier to find; must not be <code>null</code> and
	 *            must not be zero-length.
	 * @return The menu with the given identifier; this value will never be
	 *         <code>null</code>, but it might be undefined.
	 * @see SMenu
	 */
	public final SMenu getMenu(final String menuId) {
		checkId(menuId);

		if (Policy.EXPERIMENTAL_MENU 
				&& menuId.indexOf(LeafLocationElement.BREAKPOINT_PATH)>-1) {
			System.err.println("getMenu: " + menuId); //$NON-NLS-1$
		}
		
		SMenu menu = (SMenu) menusById.get(menuId);
		if (menu == null) {
			menu = new SMenu(menuId);
			menusById.put(menuId, menu);
			menu.addListener(this);
		}

		return menu;
	}

	/**
	 * Gets the widget with the given identifier. If no such widget currently
	 * exists, then the widget will be created (but will be undefined).
	 * 
	 * @param widgetId
	 *            The identifier to find; must not be <code>null</code> and
	 *            must not be zero-length.
	 * @return The item with the given identifier; this value will never be
	 *         <code>null</code>, but it might be undefined.
	 * @see SWidget
	 */
	public final SWidget getWidget(final String widgetId) {
		checkId(widgetId);

		SWidget widget = (SWidget) widgetsById.get(widgetId);
		if (widget == null) {
			widget = new SWidget(widgetId);
			widgetsById.put(widgetId, widget);
			widget.addListener(this);
		}

		return widget;
	}

	public final void propertyChange(final PropertyChangeEvent event) {
		if (MenuElement.PROPERTY_DEFINED.equals(event.getProperty())) {
			final HandleObject handle = (HandleObject) event.getSource();
			final String id = handle.getId();
			final boolean added = ((Boolean) event.getNewValue())
					.booleanValue();
			if (added) {
				if (handle instanceof SActionSet) {
					definedActionSets.add(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, null, false, null, false, null, false,
								id, added));
					}
				} else if (handle instanceof SGroup) {
					definedGroups.add(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, id,
								added, null, false, null, false, null, false,
								null, false));
					}
				} else if (handle instanceof SItem) {
					definedHandleObjects.add(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, id, added, null, false, null, false,
								null, false));
					}
				} else if (handle instanceof SMenu) {
					definedMenus.add(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, null, false, id, added, null, false,
								null, false));
					}
				} else if (handle instanceof SWidget) {
					definedWidgets.add(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, null, false, null, false, id, added,
								null, false));
					}
				}
			} else {
				if (handle instanceof SActionSet) {
					definedActionSets.remove(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, null, false, null, false, null, false,
								id, added));
					}
				} else if (handle instanceof SGroup) {
					definedGroups.remove(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, id,
								added, null, false, null, false, null, false,
								null, false));
					}
				} else if (handle instanceof SItem) {
					definedHandleObjects.remove(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, id, added, null, false, null, false,
								null, false));
					}
				} else if (handle instanceof SMenu) {
					definedMenus.remove(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, null, false, id, added, null, false,
								null, false));
					}
				} else if (handle instanceof SWidget) {
					definedWidgets.remove(handle);
					if (isListenerAttached()) {
						fireMenuManagerChanged(new MenuManagerEvent(this, null,
								false, null, false, null, false, id, added,
								null, false));
					}
				}
			}
		}
	}

	/**
	 * Removes a listener from this menu manager.
	 * 
	 * @param listener
	 *            The listener to be removed; must not be <code>null</code>.
	 */
	public final void removeListener(final IMenuManagerListener listener) {
		removeListenerObject(listener);
	}
}
