/*******************************************************************************
 * Copyright (c) 2000, 2009 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;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;

import org.eclipse.core.commands.IHandler;
import org.eclipse.core.commands.common.EventManager;
import org.eclipse.core.expressions.EvaluationResult;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ExpressionInfo;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.SubMenuManager;
import org.eclipse.jface.action.SubStatusLineManager;
import org.eclipse.jface.action.SubToolBarManager;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.EditorActionBars;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.actions.CommandAction;
import org.eclipse.ui.internal.handlers.CommandLegacyActionWrapper;
import org.eclipse.ui.internal.handlers.IActionCommandMappingService;
import org.eclipse.ui.internal.services.SourcePriorityNameMapping;
import org.eclipse.ui.services.IServiceLocator;

/**
 * Generic implementation of the <code>IActionBars</code> interface.
 */
public class SubActionBars extends EventManager implements IActionBars {

	/**
	 * The expression to use when contributing handlers through
	 * {@link #setGlobalActionHandler(String, IAction)}}. This ensures that
	 * handlers contributed through {@link SubActionBars} are given priority
	 * over handlers contributed to the {@link IHandlerService}.
	 */
	private static final Expression EXPRESSION = new Expression() {
		public final EvaluationResult evaluate(final IEvaluationContext context) {
			return EvaluationResult.TRUE;
		}

		public final void collectExpressionInfo(final ExpressionInfo info) {
			info
					.addVariableNameAccess(SourcePriorityNameMapping.LEGACY_LEGACY_NAME);
		}
	};

	/**
	 * Property constant for changes to action handlers.
	 */
	public static final String P_ACTION_HANDLERS = "org.eclipse.ui.internal.actionHandlers"; //$NON-NLS-1$

	private Map actionHandlers;

	private boolean actionHandlersChanged;

	/**
	 * A map of handler activations ({@link IHandlerActivation} indexed by
	 * action id ({@link String}) indexed by service locator ({@link IServiceLocator}).
	 * This value is <code>null</code> if there are no activations.
	 */
	private Map activationsByActionIdByServiceLocator;

	private boolean active = false;

	private SubMenuManager menuMgr;

	private IActionBars parent;

	/**
	 * A service locator appropriate for this action bar. This value is never
	 * <code>null</code>. It must be capable of providing a
	 * {@link IHandlerService}.
	 */
	private IServiceLocator serviceLocator;

	private SubStatusLineManager statusLineMgr;

	private SubToolBarManager toolBarMgr;

	private Map actionIdByCommandId = new HashMap();

	/**
	 * Construct a new <code>SubActionBars</code> object. The service locator
	 * will simply be the service locator of the parent.
	 * 
	 * @param parent
	 *            The parent of this action bar; must not be <code>null</code>.
	 */
	public SubActionBars(final IActionBars parent) {
		this(parent, null);
	}

	/**
	 * Constructs a new instance of <code>SubActionBars</code>.
	 * 
	 * @param parent
	 *            The parent of this action bar; must not be <code>null</code>.
	 * @param serviceLocator
	 *            The service locator for this action bar; should not be
	 *            <code>null</code>.
	 *            
	 * @since 3.2
	 */
	public SubActionBars(final IActionBars parent,
			final IServiceLocator serviceLocator) {
		if (parent == null) {
			throw new NullPointerException("The parent cannot be null"); //$NON-NLS-1$
		}

		this.parent = parent;
		this.serviceLocator = serviceLocator;
	}

	/**
	 * Activate the contributions.
	 */
	public void activate() {
		activate(true);
	}

	/**
	 * Activate the contributions.
	 * <p>
	 * Workaround for toolbar layout flashing when editors contribute large
	 * amounts of items. In this case we want to force the items to be
	 * visible/hidden only when required, otherwise just change the enablement
	 * state.
	 * </p>
	 */
	public void activate(boolean forceVisibility) {
		setActive(true);
	}

	/**
	 * Adds a property change listener. Has no effect if an identical listener
	 * is already registered.
	 * 
	 * @param listener
	 *            a property change listener
	 */
	public void addPropertyChangeListener(IPropertyChangeListener listener) {
		addListenerObject(listener);
	}

	/**
	 * Sets the active flag. Clients should not call this method directly unless
	 * they are overriding the setActive() method.
	 */
	protected final void basicSetActive(boolean active) {
		this.active = active;
	}

	/**
	 * Clear the global action handlers.
	 */
	public void clearGlobalActionHandlers() {
		if (actionHandlers != null) {
			actionHandlers.clear();
			actionHandlersChanged = true;
		}

		if (activationsByActionIdByServiceLocator != null) {
			// Clean up the activations.
			final Iterator activationItr = activationsByActionIdByServiceLocator
					.entrySet().iterator();
			while (activationItr.hasNext()) {
				final Map.Entry value = (Map.Entry) activationItr.next();
				final IServiceLocator locator = (IServiceLocator) value
						.getKey();
				final IHandlerService service = (IHandlerService) locator
						.getService(IHandlerService.class);
				final Map activationsByActionId = (Map) value.getValue();
				final Iterator iterator = activationsByActionId.values()
						.iterator();
				while (iterator.hasNext()) {
					final IHandlerActivation activation = (IHandlerActivation) iterator
							.next();
					if (service != null) {
						service.deactivateHandler(activation);
					}
					activation.getHandler().dispose();
				}
			}
			activationsByActionIdByServiceLocator.clear();
		}
	}

	/**
	 * Returns a new sub menu manager.
	 * 
	 * @param parent
	 *            the parent menu manager
	 * @return the menu manager
	 */
	protected SubMenuManager createSubMenuManager(IMenuManager parent) {
		return new SubMenuManager(parent);
	}

	/**
	 * Returns a new sub toolbar manager.
	 * 
	 * @param parent
	 *            the parent toolbar manager
	 * @return the tool bar manager
	 */
	protected SubToolBarManager createSubToolBarManager(IToolBarManager parent) {
		return new SubToolBarManager(parent);
	}

	/**
	 * Deactivate the contributions.
	 */
	public void deactivate() {
		deactivate(true);
	}

	/**
	 * Deactivate the contributions.
	 * <p>
	 * Workaround for menubar/toolbar layout flashing when editors have many
	 * contributions. In this case we want to force the contributions to be
	 * visible/hidden only when required, otherwise just change the enablement
	 * state.
	 * </p>
	 */
	public void deactivate(boolean forceHide) {
		setActive(false);
	}

	/**
	 * Dispose the contributions.
	 */
	public void dispose() {
		clearGlobalActionHandlers();
		if (menuMgr != null) {
			menuMgr.dispose();
			menuMgr.disposeManager();
		}
		if (statusLineMgr != null) {
			statusLineMgr.disposeManager();
		}
		if (toolBarMgr != null) {
			toolBarMgr.disposeManager();
		}
		clearListeners();
	}

	/**
	 * Notifies any property change listeners if the global action handlers have
	 * changed
	 */
	protected void fireActionHandlersChanged() {
		if (actionHandlersChanged) {
			// Doesn't actually pass the old and new values
			firePropertyChange(new PropertyChangeEvent(this, P_ACTION_HANDLERS,
					null, null));
			actionHandlersChanged = false;
		}
	}

	/**
	 * Notifies any property change listeners that a property has changed. Only
	 * listeners registered at the time this method is called are notified.
	 * 
	 * @param event
	 *            the property change event
	 * 
	 * @see IPropertyChangeListener#propertyChange
	 */
	protected void firePropertyChange(PropertyChangeEvent event) {
		Object[] listeners = getListeners();
		for (int i = 0; i < listeners.length; ++i) {
			((IPropertyChangeListener) listeners[i]).propertyChange(event);
		}
	}

	/**
	 * Return whether the manager is currently active or not.
	 */
	protected final boolean getActive() {
		return active;
	}

	/**
	 * Get the handler for a window action.
	 * 
	 * @param actionID
	 *            an action ID declared in the registry
	 * @return an action handler which implements the action ID, or
	 *         <code>null</code> if none is registered.
	 */
	public IAction getGlobalActionHandler(String actionID) {
		if (actionHandlers == null) {
			return null;
		}
		return (IAction) actionHandlers.get(actionID);
	}

	/**
	 * Returns the complete list of active global action handlers. If there are
	 * no global action handlers registered return null.
	 */
	public Map getGlobalActionHandlers() {
		return actionHandlers;
	}

	/**
	 * Returns the abstract menu manager. If items are added or removed from the
	 * manager be sure to call <code>updateActionBars</code>.
	 * 
	 * @return the menu manager
	 */
	public IMenuManager getMenuManager() {
		if (menuMgr == null) {
			menuMgr = createSubMenuManager(parent.getMenuManager());
			menuMgr.setVisible(active);
		}
		return menuMgr;
	}

	/**
	 * Return the parent action bar manager.
	 */
	protected final IActionBars getParent() {
		return parent;
	}

	/**
	 * Answer the service locator for this action bar.
	 * 
	 * @return an <code>IServiceLocater</code> or the parents if 
	 *     the receiver does not have one
	 *     
	 * @since 3.2
	 */
	public final IServiceLocator getServiceLocator() {
		if (serviceLocator != null) {
			return serviceLocator;
		}

		return parent.getServiceLocator();
	}

	/**
	 * Returns the status line manager. If items are added or removed from the
	 * manager be sure to call <code>updateActionBars</code>.
	 * 
	 * @return the status line manager
	 */
	public IStatusLineManager getStatusLineManager() {
		if (statusLineMgr == null) {
			statusLineMgr = new SubStatusLineManager(parent
					.getStatusLineManager());
			statusLineMgr.setVisible(active);
		}
		return statusLineMgr;
	}

	/**
	 * Returns the tool bar manager. If items are added or removed from the
	 * manager be sure to call <code>updateActionBars</code>.
	 * 
	 * @return the tool bar manager
	 */
	public IToolBarManager getToolBarManager() {
		if (toolBarMgr == null) {
			toolBarMgr = createSubToolBarManager(parent.getToolBarManager());
			toolBarMgr.setVisible(active);
		}
		return toolBarMgr;
	}

	/**
	 * Return whether the sub menu manager has been created yet.
	 */
	protected final boolean isSubMenuManagerCreated() {
		return menuMgr != null;
	}

	/**
	 * Return whether the sub status line manager has been created yet.
	 */
	protected final boolean isSubStatusLineManagerCreated() {
		return statusLineMgr != null;
	}

	/**
	 * Return whether the sub toolbar manager has been created yet.
	 */
	protected final boolean isSubToolBarManagerCreated() {
		return toolBarMgr != null;
	}

	/**
	 * Notification that the target part for the action bars has changed.
	 */
	public void partChanged(IWorkbenchPart part) {
	}

	/**
	 * Removes the given property change listener. Has no effect if an identical
	 * listener is not registered.
	 * 
	 * @param listener
	 *            a property change listener
	 */
	public void removePropertyChangeListener(IPropertyChangeListener listener) {
		removeListenerObject(listener);
	}

	/**
	 * Activate / deactivate the contributions.
	 */
	protected void setActive(boolean set) {
		active = set;
		if (menuMgr != null) {
			menuMgr.setVisible(set);
		}

		if (statusLineMgr != null) {
			statusLineMgr.setVisible(set);
		}

		if (toolBarMgr != null) {
			toolBarMgr.setVisible(set);
		}
	}

	/**
	 * Add a handler for a window action.
	 * 
	 * @param actionID
	 *            an action ID declared in the registry
	 * @param handler
	 *            an action which implements the action ID. <code>null</code>
	 *            may be passed to deregister a handler.
	 */
	public void setGlobalActionHandler(String actionID, IAction handler) {
		if (actionID == null) {
			/*
			 * Bug 124061. It used to be invalid to pass null as an action id,
			 * but some people still did it. Handle this case by trapping the
			 * exception and logging it.
			 */
			WorkbenchPlugin
					.log("Cannot set the global action handler for a null action id"); //$NON-NLS-1$
			return;
		}
		
		if (handler instanceof CommandLegacyActionWrapper) {
        	// this is a registration of a fake action for an already
			// registered handler
			WorkbenchPlugin
					.log("Cannot feed a CommandLegacyActionWrapper back into the system"); //$NON-NLS-1$
			return;
		}
		
		if (handler instanceof CommandAction) {
			// we unfortunately had to allow these out into the wild, but they
			// still must not feed back into the system
			return;
		}
		
		if (handler != null) {
			// Update the action handlers.
			if (actionHandlers == null) {
				actionHandlers = new HashMap(11);
			}
			actionHandlers.put(actionID, handler);

			// Add a mapping from this action id to the command id.
			if (serviceLocator != null) {
				String commandId = null;
				final IActionCommandMappingService mappingService = (IActionCommandMappingService) serviceLocator
						.getService(IActionCommandMappingService.class);
				if (mappingService != null) {
					commandId = mappingService.getCommandId(actionID);
				}
				if (commandId == null) {
					commandId = handler.getActionDefinitionId();
				}

				// Update the handler activations.
				final IHandlerService service = (IHandlerService) serviceLocator
						.getService(IHandlerService.class);
				Map activationsByActionId = null;
				if (activationsByActionIdByServiceLocator == null) {
					activationsByActionIdByServiceLocator = new WeakHashMap();
					activationsByActionId = new HashMap();
					activationsByActionIdByServiceLocator.put(serviceLocator,
							activationsByActionId);
				} else {
					activationsByActionId = (Map) activationsByActionIdByServiceLocator
							.get(serviceLocator);
					if (activationsByActionId == null) {
						activationsByActionId = new HashMap();
						activationsByActionIdByServiceLocator.put(
								serviceLocator, activationsByActionId);
					} else if (activationsByActionId.containsKey(actionID)) {
						final Object value = activationsByActionId
								.remove(actionID);
						if (value instanceof IHandlerActivation) {
							final IHandlerActivation activation = (IHandlerActivation) value;
							actionIdByCommandId.remove(activation.getCommandId());
							if (service != null) {
								service.deactivateHandler(activation);
							}
							activation.getHandler().dispose();
						}
					} else if (commandId != null
							&& actionIdByCommandId.containsKey(commandId)) {
						final Object value = activationsByActionId
								.remove(actionIdByCommandId.remove(commandId));
						if (value instanceof IHandlerActivation) {
							final IHandlerActivation activation = (IHandlerActivation) value;
							if (service != null) {
								service.deactivateHandler(activation);
							}
							activation.getHandler().dispose();
						}
					}
				}

				if (commandId != null) {
					actionIdByCommandId.put(commandId, actionID);
					// Register this as a handler with the given definition id.
					// the expression gives the setGlobalActionHandler() a
					// priority.
					final IHandler actionHandler = new ActionHandler(handler);
					Expression handlerExpression = EXPRESSION;
					//XXX add new API in next release to avoid down-casting (bug 137091)
					if (this instanceof EditorActionBars) {
						handlerExpression = ((EditorActionBars)this).getHandlerExpression();
					}
					if (service != null) {
						final IHandlerActivation activation = service
								.activateHandler(commandId, actionHandler,
										handlerExpression);
						activationsByActionId.put(actionID, activation);
					}
				}
			}

		} else {
			if (actionHandlers != null) {
				actionHandlers.remove(actionID);
			}

			// Remove the handler activation.
			if (serviceLocator != null) {
				final IHandlerService service = (IHandlerService) serviceLocator
						.getService(IHandlerService.class);
				if (activationsByActionIdByServiceLocator != null) {
					final Map activationsByActionId = (Map) activationsByActionIdByServiceLocator
							.get(serviceLocator);
					if ((activationsByActionId != null)
							&& (activationsByActionId.containsKey(actionID))) {
						final Object value = activationsByActionId
								.remove(actionID);
						if (value instanceof IHandlerActivation) {
							final IHandlerActivation activation = (IHandlerActivation) value;
							actionIdByCommandId.remove(activation.getCommandId());
							service.deactivateHandler(activation);
							activation.getHandler().dispose();
						}
					}
				}
			}
		}
		actionHandlersChanged = true;
	}

	/**
	 * Sets the service locator for this action bar.
	 * 
	 * @param locator
	 *            The new locator; must not be <code>null</code>.
	 *            
	 * @since 3.2
	 */
	protected final void setServiceLocator(final IServiceLocator locator) {
		if (locator == null) {
			throw new NullPointerException("The service locator cannot be null"); //$NON-NLS-1$
		}
		this.serviceLocator = locator;
	}

	/**
	 * Commits all UI changes. This should be called after additions or
	 * subtractions have been made to a menu, status line, or toolbar.
	 */
	public void updateActionBars() {
		parent.updateActionBars();
		fireActionHandlersChanged();
	}
}
