/*******************************************************************************
 * 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.internal.handlers;

import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.commands.AbstractHandlerWithState;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.HandlerEvent;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.commands.IHandler2;
import org.eclipse.core.commands.IHandlerListener;
import org.eclipse.core.commands.IStateListener;
import org.eclipse.core.commands.State;
import org.eclipse.core.expressions.EvaluationResult;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.commands.IElementUpdater;
import org.eclipse.ui.handlers.RadioState;
import org.eclipse.ui.handlers.RegistryToggleState;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.internal.util.BundleUtility;
import org.eclipse.ui.internal.util.Util;
import org.eclipse.ui.menus.UIElement;
import org.eclipse.ui.services.IEvaluationReference;
import org.eclipse.ui.services.IEvaluationService;

/**
 * <p>
 * A proxy for a handler that has been defined in XML. This delays the class
 * loading until the handler is really asked for information (besides the
 * priority or the command identifier). Asking a proxy for anything but the
 * attributes defined publicly in this class will cause the proxy to instantiate
 * the proxied handler.
 * </p>
 *
 * @since 3.0
 */
public final class HandlerProxy extends AbstractHandlerWithState implements IElementUpdater {

	private static Map<IConfigurationElement, HandlerProxy> CEToProxyMap = new HashMap<>();

	/**
	 *
	 */
	private static final String PROP_ENABLED = "enabled"; //$NON-NLS-1$

	/**
	 * The configuration element from which the handler can be created. This value
	 * will exist until the element is converted into a real class -- at which point
	 * this value will be set to <code>null</code>.
	 */
	private IConfigurationElement configurationElement;

	/**
	 * The <code>enabledWhen</code> expression for the handler. Only if this
	 * expression evaluates to <code>true</code> (or the value is <code>null</code>)
	 * should we consult the handler.
	 */
	private final Expression enabledWhenExpression;

	/**
	 * The real handler. This value is <code>null</code> until the proxy is forced
	 * to load the real handler. At this point, the configuration element is
	 * converted, nulled out, and this handler gains a reference.
	 */
	private IHandler handler;

	/**
	 * The name of the configuration element attribute which contains the
	 * information necessary to instantiate the real handler.
	 */
	private final String handlerAttributeName;

	private IHandlerListener handlerListener;

	/**
	 * The evaluation service to use when evaluating
	 * <code>enabledWhenExpression</code>. This value may be <code>null</code> only
	 * if the <code>enabledWhenExpression</code> is <code>null</code>.
	 */
	private IEvaluationService evaluationService;

	private IPropertyChangeListener enablementListener;

	private IEvaluationReference enablementRef;

	private boolean proxyEnabled;

	private String commandId;

	//
	// state to support checked or radio commands.
	private State checkedState;

	private State radioState;

	// Exception that occurs while loading the proxied handler class
	private Exception loadException;

	/**
	 * Constructs a new instance of <code>HandlerProxy</code> with all the
	 * information it needs to try to avoid loading until it is needed.
	 *
	 * @param commandId            the id for this handler
	 * @param configurationElement The configuration element from which the real
	 *                             class can be loaded at run-time; must not be
	 *                             <code>null</code>.
	 * @param handlerAttributeName The name of the attibute or element containing
	 *                             the handler executable extension; must not be
	 *                             <code>null</code>.
	 */
	public HandlerProxy(final String commandId, final IConfigurationElement configurationElement,
			final String handlerAttributeName) {
		this(commandId, configurationElement, handlerAttributeName, null, null);
	}

	/**
	 * Constructs a new instance of <code>HandlerProxy</code> with all the
	 * information it needs to try to avoid loading until it is needed.
	 *
	 * @param commandId             the id for this handler
	 * @param configurationElement  The configuration element from which the real
	 *                              class can be loaded at run-time; must not be
	 *                              <code>null</code>.
	 * @param handlerAttributeName  The name of the attribute or element containing
	 *                              the handler executable extension; must not be
	 *                              <code>null</code>.
	 * @param enabledWhenExpression The name of the element containing the
	 *                              enabledWhen expression. This should be a child
	 *                              of the <code>configurationElement</code>. If
	 *                              this value is <code>null</code>, then there is
	 *                              no enablement expression (i.e., enablement will
	 *                              be delegated to the handler when possible).
	 * @param evaluationService     The evaluation service to manage enabledWhen
	 *                              expressions trying to evaluate the
	 *                              <code>enabledWhenExpression</code>. This value
	 *                              may be <code>null</code> only if the
	 *                              <code>enabledWhenExpression</code> is
	 *                              <code>null</code>.
	 */
	public HandlerProxy(final String commandId, final IConfigurationElement configurationElement,
			final String handlerAttributeName, final Expression enabledWhenExpression,
			final IEvaluationService evaluationService) {
		if (configurationElement == null) {
			throw new NullPointerException("The configuration element backing a handler proxy cannot be null"); //$NON-NLS-1$
		}

		if (handlerAttributeName == null) {
			throw new NullPointerException("The attribute containing the handler class must be known"); //$NON-NLS-1$
		}

		if ((enabledWhenExpression != null) && (evaluationService == null)) {
			throw new NullPointerException(
					"We must have a handler service and evaluation service to support the enabledWhen expression"); //$NON-NLS-1$
		}

		this.commandId = commandId;
		this.configurationElement = configurationElement;
		this.handlerAttributeName = handlerAttributeName;
		this.enabledWhenExpression = enabledWhenExpression;
		this.evaluationService = evaluationService;
		if (enabledWhenExpression != null) {
			setProxyEnabled(false);
			registerEnablement();
		} else {
			setProxyEnabled(true);
		}

		CEToProxyMap.put(configurationElement, this);
	}

	public static void updateStaleCEs(IConfigurationElement[] replacements) {
		for (IConfigurationElement replacement : replacements) {
			HandlerProxy proxy = CEToProxyMap.get(replacement);
			if (proxy != null)
				proxy.configurationElement = replacement;
		}
	}

	/**
	 *
	 */
	private void registerEnablement() {
		enablementRef = evaluationService.addEvaluationListener(enabledWhenExpression, getEnablementListener(),
				PROP_ENABLED);
	}

	@Override
	public void setEnabled(Object evaluationContext) {
		if (!(evaluationContext instanceof IEvaluationContext)) {
			return;
		}
		IEvaluationContext context = (IEvaluationContext) evaluationContext;
		if (enabledWhenExpression != null) {
			try {
				setProxyEnabled(enabledWhenExpression.evaluate(context) == EvaluationResult.TRUE);
			} catch (CoreException e) {
				// TODO should we log this exception, or just treat it as
				// a failure
			}
		}
		if (isOkToLoad() && loadHandler()) {
			if (handler instanceof IHandler2) {
				((IHandler2) handler).setEnabled(evaluationContext);
			}
		}
	}

	void setProxyEnabled(boolean enabled) {
		proxyEnabled = enabled;
	}

	boolean getProxyEnabled() {
		return proxyEnabled;
	}

	private IPropertyChangeListener getEnablementListener() {
		if (enablementListener == null) {
			enablementListener = event -> {
				if (event.getProperty() == PROP_ENABLED) {
					setProxyEnabled(
							event.getNewValue() == null ? false : ((Boolean) event.getNewValue()).booleanValue());
					fireHandlerChanged(new HandlerEvent(HandlerProxy.this, true, false));
				}
			};
		}
		return enablementListener;
	}

	/**
	 * Passes the dipose on to the proxied handler, if it has been loaded.
	 */
	@Override
	public void dispose() {
		if (handler != null) {
			if (handlerListener != null) {
				handler.removeHandlerListener(handlerListener);
				handlerListener = null;
			}
			handler.dispose();
			handler = null;
		}
		if (enablementListener != null) {
			evaluationService.removeEvaluationListener(enablementRef);
			enablementRef = null;
			enablementListener = null;
		}
	}

	@Override
	public Object execute(final ExecutionEvent event) throws ExecutionException {
		if (loadHandler()) {
			if (!isEnabled()) {
				MessageDialog.openInformation(Util.getShellToParentOn(), WorkbenchMessages.Information,
						WorkbenchMessages.PluginAction_disabledMessage);
				return null;
			}
			return handler.execute(event);
		}

		if (loadException != null)
			throw new ExecutionException("Exception occured when loading the handler", loadException); //$NON-NLS-1$

		return null;
	}

	@Override
	public boolean isEnabled() {
		if (enabledWhenExpression != null) {
			// proxyEnabled reflects the enabledWhen clause
			if (!getProxyEnabled()) {
				return false;
			}
			if (isOkToLoad() && loadHandler()) {
				return handler.isEnabled();
			}

			return true;
		}

		/*
		 * There is no enabled when expression, so we just need to consult the handler.
		 */
		if (isOkToLoad() && loadHandler()) {
			return handler.isEnabled();
		}
		return true;
	}

	@Override
	public boolean isHandled() {
		boolean okToLoad = isOkToLoad();
		if (okToLoad && loadHandler()) {
			return handler.isHandled();
		}
		if (!okToLoad) {
			// That is crazy, but we should answer "true" for contributions from not loaded
			// plugins, otherwise pure declarative commands defined in not active bundles
			// will not run for the first time
			return true;
		}
		return false;
	}

	/**
	 * Loads the handler, if possible. If the handler is loaded, then the member
	 * variables are updated accordingly.
	 *
	 * @return <code>true</code> if the handler is now non-null; <code>false</code>
	 *         otherwise.
	 */
	private boolean loadHandler() {
		if (handler == null) {
			// Load the handler.
			try {
				if (configurationElement != null) {
					handler = (IHandler) configurationElement.createExecutableExtension(handlerAttributeName);
					handler.addHandlerListener(getHandlerListener());
					setEnabled(evaluationService == null ? null : evaluationService.getCurrentState());
					refreshElements();
					return true;
				}

			} catch (final ClassCastException e) {
				final String message = "The proxied handler was the wrong class"; //$NON-NLS-1$
				final IStatus status = new Status(IStatus.ERROR, WorkbenchPlugin.PI_WORKBENCH, 0, message, e);
				WorkbenchPlugin.log(message, status);
				configurationElement = null;
				loadException = e;

			} catch (final CoreException e) {
				final String message = "The proxied handler for '" //$NON-NLS-1$
						+ configurationElement.getAttribute(handlerAttributeName) + "' could not be loaded"; //$NON-NLS-1$
				IStatus status = new Status(IStatus.ERROR, WorkbenchPlugin.PI_WORKBENCH, 0, message, e);
				WorkbenchPlugin.log(message, status);
				configurationElement = null;
				loadException = e;
			}
			return false;
		}

		return true;
	}

	private IHandlerListener getHandlerListener() {
		if (handlerListener == null) {
			handlerListener = handlerEvent -> fireHandlerChanged(new HandlerEvent(HandlerProxy.this,
					handlerEvent.isEnabledChanged(), handlerEvent.isHandledChanged()));
		}
		return handlerListener;
	}

	@Override
	public String toString() {
		if (handler == null) {
			if (configurationElement != null) {
				String configurationElementAttribute = getConfigurationElementAttribute();
				if (configurationElementAttribute != null) {
					return configurationElementAttribute;
				}
			}
			return "HandlerProxy()"; //$NON-NLS-1$
		}

		return handler.toString();
	}

	/**
	 * Retrives the ConfigurationElement attribute according to the
	 * <code>handlerAttributeName</code>.
	 *
	 * @return the handlerAttributeName value, may be <code>null</code>.
	 */
	private String getConfigurationElementAttribute() {
		String attribute = configurationElement.getAttribute(handlerAttributeName);
		if (attribute == null) {
			for (IConfigurationElement configElement : configurationElement.getChildren(handlerAttributeName)) {
				String childAttribute = configElement.getAttribute(IWorkbenchRegistryConstants.ATT_CLASS);
				if (childAttribute != null) {
					return childAttribute;
				}
			}
		}
		return attribute;
	}

	private boolean isOkToLoad() {
		if (PlatformUI.getWorkbench().isClosing())
			return handler != null;

		if (configurationElement != null && handler == null) {
			final String bundleId = configurationElement.getContributor().getName();
			return BundleUtility.isActive(bundleId);
		}
		return true;
	}

	@Override
	public void updateElement(UIElement element, Map parameters) {
		if (checkedState != null) {
			Boolean value = (Boolean) checkedState.getValue();
			element.setChecked(value.booleanValue());
		} else if (radioState != null) {
			String value = (String) radioState.getValue();
			Object parameter = parameters.get(RadioState.PARAMETER_ID);
			element.setChecked(value != null && value.equals(parameter));
		}
		if (handler != null && handler instanceof IElementUpdater) {
			((IElementUpdater) handler).updateElement(element, parameters);
		}
	}

	private void refreshElements() {
		if (commandId == null
				|| !(handler instanceof IElementUpdater) && (checkedState == null && radioState == null)) {
			return;
		}
		ICommandService cs = PlatformUI.getWorkbench().getService(ICommandService.class);
		cs.refreshElements(commandId, null);
	}

	@Override
	public void handleStateChange(State state, Object oldValue) {
		if (state.getId().equals(RegistryToggleState.STATE_ID)) {
			checkedState = state;
			refreshElements();
		} else if (state.getId().equals(RadioState.STATE_ID)) {
			radioState = state;
			refreshElements();
		}
		if (handler instanceof IStateListener) {
			((IStateListener) handler).handleStateChange(state, oldValue);
		}
	}

	/**
	 * @return the config element for use with the PDE framework.
	 */
	public IConfigurationElement getConfigurationElement() {
		return configurationElement;
	}

	public String getAttributeName() {
		return handlerAttributeName;
	}

	/**
	 * @return Returns the handler.
	 */
	public IHandler getHandler() {
		return handler;
	}
}
