/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Lars Vogel <Lars.Vogel@gmail.com> - Bug 440810
 *******************************************************************************/
package org.eclipse.ui;

import java.util.Collection;
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() {
		@Override
		public final EvaluationResult evaluate(final IEvaluationContext context) {
			return EvaluationResult.TRUE;
		}

		@Override
		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<String, IAction> 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<IServiceLocator, Map<String, IHandlerActivation>> 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<String, String> 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>
	 *
	 * @param forceVisibility (is not used)
	 */
	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.
	 */
	@Override
	public void clearGlobalActionHandlers() {
		if (actionHandlers != null) {
			actionHandlers.clear();
			actionHandlersChanged = true;
		}

		if (activationsByActionIdByServiceLocator != null) {
			// Clean up the activations.
			final Iterator<Map.Entry<IServiceLocator, Map<String, IHandlerActivation>>> activationItr = activationsByActionIdByServiceLocator
					.entrySet().iterator();
			while (activationItr.hasNext()) {
				final Map.Entry<IServiceLocator, Map<String, IHandlerActivation>> value = activationItr.next();
				final IServiceLocator locator = value.getKey();
				final IHandlerService service = locator.getService(IHandlerService.class);
				final Map<String, IHandlerActivation> activationsByActionId = value.getValue();
				Collection<IHandlerActivation> activations = activationsByActionId.values();
				if (service != null) {
					service.deactivateHandlers(activations);
				}

				for (Object activation : activations) {
					((IHandlerActivation) 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>
	 *
	 * @param forceHide (not used)
	 */
	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();
		}
		updateActionBars(); // Notify listeners and parent
		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 (Object listener : listeners) {
			((IPropertyChangeListener) listener).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.
	 */
	@Override
	public IAction getGlobalActionHandler(String actionID) {
		if (actionHandlers == null) {
			return null;
		}
		return actionHandlers.get(actionID);
	}

	/**
	 * Returns the complete list of active global action handlers. If there are no
	 * global action handlers registered return null.
	 *
	 * @return a map with global action handlers
	 */
	public Map<String, IAction> 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
	 */
	@Override
	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
	 */
	@Override
	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
	 */
	@Override
	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
	 */
	@Override
	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. This
	 * implementation does nothing.
	 *
	 * @param part the part to be notified.
	 */
	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.
	 */
	@Override
	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 = serviceLocator
						.getService(IActionCommandMappingService.class);
				if (mappingService != null) {
					commandId = mappingService.getCommandId(actionID);
				}
				if (commandId == null) {
					commandId = handler.getActionDefinitionId();
				}

				// Update the handler activations.
				final IHandlerService service = serviceLocator.getService(IHandlerService.class);
				Map<String, IHandlerActivation> activationsByActionId = null;
				if (activationsByActionIdByServiceLocator == null) {
					activationsByActionIdByServiceLocator = new WeakHashMap<>();
					activationsByActionId = new HashMap<>();
					activationsByActionIdByServiceLocator.put(serviceLocator, activationsByActionId);
				} else {
					activationsByActionId = 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 = serviceLocator.getService(IHandlerService.class);
				if (activationsByActionIdByServiceLocator != null) {
					final Map<String, IHandlerActivation> activationsByActionId = 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.
	 */
	@Override
	public void updateActionBars() {
		parent.updateActionBars();
		fireActionHandlersChanged();
	}
}
