/*******************************************************************************
 * Copyright (c) 2005, 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
 *******************************************************************************/

package org.eclipse.ui.internal.services;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.core.expressions.EvaluationContext;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.ISourceProvider;
import org.eclipse.ui.ISourceProviderListener;
import org.eclipse.ui.ISources;

/**
 * <p>
 * Provides common functionality for evaluating expressions and listening to
 * {@link ISourceProvider} (i.e., the common event framework for commands).
 * </p>
 * <p>
 * This class is not intended for use outside of the
 * <code>org.eclipse.ui.workbench</code> plug-in.
 * </p>
 *
 * @since 3.2
 * @see ISourceProvider
 * @see ISources
 * @see Expression
 * @see IEvaluationContext
 */
public abstract class ExpressionAuthority implements ISourceProviderListener {

	/**
	 * The evaluation context instance to use when evaluating expression. This
	 * context is shared, and so all calls into <code>sourceChanged</code>
	 * must happen on the event thread.
	 */
	private final IEvaluationContext context;

	/**
	 * The current state of this authority. This is a child of the
	 * {@link #context} that has been given the selection as the default
	 * variable. This value is cleared to <code>null</code> whenever the
	 * selection changes.
	 */
	private IEvaluationContext currentState = null;

	/**
	 * The collection of source providers used by this authority. This
	 * collection is consulted whenever a contribution is made. This collection
	 * only contains instances of <code>ISourceProvider</code>.
	 */
	private final Collection providers = new ArrayList();

	/**
	 * Constructs a new instance of <code>ExpressionAuthority</code>.
	 */
	protected ExpressionAuthority() {
		context = new EvaluationContext(null, this);
		context.setAllowPluginActivation(true);
		context.addVariable("org.eclipse.core.runtime.Platform", Platform.class); //$NON-NLS-1$
		// this is not as useful as it appears
		// context.addVariable("java.lang.System", System.class); //$NON-NLS-1$
	}

	/**
	 * Adds a source provider to a list of providers to check when updating.
	 * This also attaches this authority as a listener to the provider.
	 *
	 * @param provider
	 *            The provider to add; must not be <code>null</code>.
	 */
	public final void addSourceProvider(final ISourceProvider provider) {
		provider.addSourceProviderListener(this);
		providers.add(provider);

		// Update the current state.
		final Map currentState = provider.getCurrentState();
		final Iterator variableItr = currentState.entrySet().iterator();
		while (variableItr.hasNext()) {
			final Map.Entry entry = (Map.Entry) variableItr.next();
			final String variableName = (String) entry.getKey();
			final Object variableValue = entry.getValue();

			/*
			 * Bug 84056. If we update the active workbench window, then we risk
			 * falling back to that shell when the active shell has registered
			 * as "none".
			 */
			if ((variableName != null)
					&& (!ISources.ACTIVE_WORKBENCH_WINDOW_SHELL_NAME
							.equals(variableName))) {
				changeVariable(variableName, variableValue);
			}
		}
		sourceChanged(0, currentState);
	}

	/**
	 * Removes all of the source provider listeners. Subclasses may extend, but
	 * must not override.
	 */
	public void dispose() {
		final Iterator providerItr = providers.iterator();
		while (providerItr.hasNext()) {
			final ISourceProvider provider = (ISourceProvider) providerItr
					.next();
			provider.removeSourceProviderListener(this);
		}

		providers.clear();
	}

	/**
	 * Returns whether at least one of the <code>IEvaluationResultCache</code>
	 * instances in <code>collection</code> evaluates to <code>true</code>.
	 *
	 * @param collection
	 *            The evaluation result caches to check; must not be
	 *            <code>null</code>, but may be empty.
	 * @return <code>true</code> if there is at least one expression that
	 *         evaluates to <code>true</code>; <code>false</code>
	 *         otherwise.
	 */
	protected final boolean evaluate(final Collection collection) {
		final Iterator iterator = collection.iterator();
		while (iterator.hasNext()) {
			final IEvaluationResultCache cache = (IEvaluationResultCache) iterator
					.next();
			if (evaluate(cache)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Returns whether the <code>IEvaluationResultCache</code> evaluates to
	 * <code>true</code>.
	 *
	 * @param expression
	 *            The evaluation result cache to check; must not be
	 *            <code>null</code>.
	 * @return <code>true</code> if the expression evaluates to
	 *         <code>true</code>; <code>false</code> otherwise.
	 */
	protected final boolean evaluate(final IEvaluationResultCache expression) {
		final IEvaluationContext contextWithDefaultVariable = getCurrentState();
		return expression.evaluate(contextWithDefaultVariable);
	}

	/**
	 * Creates a new evaluation context based on the current evaluation context
	 * (i.e., the current state), and places the current selection as the
	 * default variable.
	 *
	 * @return An evaluation context that can be used for evaluating
	 *         expressions; never <code>null</code>.
	 */
	public final IEvaluationContext getCurrentState() {
		if (currentState == null) {
			final Object defaultVariable = context
					.getVariable(ISources.ACTIVE_CURRENT_SELECTION_NAME);
			final IEvaluationContext contextWithDefaultVariable;
			if (defaultVariable instanceof IStructuredSelection) {
				final IStructuredSelection selection = (IStructuredSelection) defaultVariable;
				contextWithDefaultVariable = new EvaluationContext(context,
						selection.toList());
			} else if ((defaultVariable instanceof ISelection)
					&& (!((ISelection) defaultVariable).isEmpty())) {
				contextWithDefaultVariable = new EvaluationContext(context,
						Collections.singleton(defaultVariable));
			} else {
				contextWithDefaultVariable = new EvaluationContext(context,
						Collections.EMPTY_LIST);
			}
			currentState = contextWithDefaultVariable;
		}

		return currentState;
	}

	/**
	 * Returns the variable of the given name.
	 *
	 * @param name
	 *            The name of the variable to get; must not be <code>null</code>.
	 * @return The variable of the given name; <code>null</code> if none.
	 */
	protected final Object getVariable(final String name) {
		return context.getVariable(name);
	}

	/**
	 * Removes this source provider from the list, and detaches this authority
	 * as a listener.
	 *
	 * @param provider
	 *            The provider to remove; must not be <code>null</code>.
	 */
	public final void removeSourceProvider(final ISourceProvider provider) {
		provider.removeSourceProviderListener(this);
		providers.remove(provider);

		final Map currentState = provider.getCurrentState();
		final Iterator variableItr = currentState.entrySet().iterator();
		while (variableItr.hasNext()) {
			final Map.Entry entry = (Map.Entry) variableItr.next();
			final String variableName = (String) entry.getKey();
			changeVariable(variableName, null);
		}
	}

	/**
	 * Changes the variable of the given name. If the <code>value</code> is
	 * <code>null</code>, then the variable is removed.
	 *
	 * @param name
	 *            The name of the variable to change; must not be
	 *            <code>null</code>.
	 * @param value
	 *            The new value; the variable should be removed if this is
	 *            <code>null</code>.
	 */
	protected final void changeVariable(final String name, final Object value) {
		if (value == null) {
			context.removeVariable(name);
		} else {
			context.addVariable(name, value);
		}
	}

	/**
	 * Carries out the actual source change notification. It assumed that by the
	 * time this method is called, <code>getEvaluationContext()</code> is
	 * up-to-date with the current state of the application.
	 *
	 * @param sourcePriority
	 *            A bit mask of all the source priorities that have changed.
	 */
	protected abstract void sourceChanged(final int sourcePriority);

	/**
	 * Similar to sourceChanged(int) this notifies the subclass about the
	 * change, but using the array of source names that changed instead of the
	 * priority ... int based.
	 * <p>
	 * Clients may override this method.
	 * </p>
	 *
	 * @param sourceNames
	 *            The array of names that changed.
	 * @since 3.3
	 */
	protected void sourceChanged(final String[] sourceNames) {
		// this is a no-op, since we're late in the game
	}

	@Override
	public final void sourceChanged(final int sourcePriority,
			final Map sourceValuesByName) {
		// If the selection has changed, invalidate the current state.
		if (sourceValuesByName
				.containsKey(ISources.ACTIVE_CURRENT_SELECTION_NAME)) {
			currentState = null;
		}

		final Iterator entryItr = sourceValuesByName.entrySet().iterator();
		while (entryItr.hasNext()) {
			final Map.Entry entry = (Map.Entry) entryItr.next();
			final String sourceName = (String) entry.getKey();
			final Object sourceValue = entry.getValue();
			updateEvaluationContext(sourceName, sourceValue);
		}
		sourceChanged(sourcePriority, (String[]) sourceValuesByName.keySet()
				.toArray(new String[0]));
	}

	@Override
	public final void sourceChanged(final int sourcePriority,
			final String sourceName, final Object sourceValue) {
		// If the selection has changed, invalidate the current state.
		if (ISources.ACTIVE_CURRENT_SELECTION_NAME.equals(sourceName)) {
			currentState = null;
		}

		updateEvaluationContext(sourceName, sourceValue);
		sourceChanged(sourcePriority, new String[] { sourceName });
	}

	/**
	 * @param sourcePriority
	 * @param strings
	 */
	private void sourceChanged(int sourcePriority, String[] sourceNames) {
		sourceChanged(sourcePriority);
		sourceChanged(sourceNames);
	}

	/**
	 * Updates the evaluation context with the current state from all of the
	 * source providers.
	 */
	protected final void updateCurrentState() {
		final Iterator providerItr = providers.iterator();
		while (providerItr.hasNext()) {
			final ISourceProvider provider = (ISourceProvider) providerItr
					.next();
			final Map currentState = provider.getCurrentState();
			final Iterator variableItr = currentState.entrySet().iterator();
			while (variableItr.hasNext()) {
				final Map.Entry entry = (Map.Entry) variableItr.next();
				final String variableName = (String) entry.getKey();
				final Object variableValue = entry.getValue();
				/*
				 * Bug 84056. If we update the active workbench window, then we
				 * risk falling back to that shell when the active shell has
				 * registered as "none".
				 */
				if ((variableName != null)
						&& (!ISources.ACTIVE_WORKBENCH_WINDOW_SHELL_NAME
								.equals(variableName))) {
					changeVariable(variableName, variableValue);
				}
			}
		}
	}

	/**
	 * Updates this authority's evaluation context.
	 *
	 * @param name
	 *            The name of the variable to update; must not be
	 *            <code>null</code>.
	 * @param value
	 *            The new value of the variable. If this value is
	 *            <code>null</code>, then the variable is removed.
	 */
	protected void updateEvaluationContext(final String name, final Object value) {
		if (name != null) {
			changeVariable(name, value);
		}
	}
}
