/*******************************************************************************
 * Copyright (c) 2003, 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.navigator.extensions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.TreeSet;

import org.eclipse.core.expressions.ElementHandler;
import org.eclipse.core.expressions.EvaluationResult;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.expressions.ExpressionConverter;
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.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IPluginContribution;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.internal.navigator.CommonNavigatorMessages;
import org.eclipse.ui.internal.navigator.CustomAndExpression;
import org.eclipse.ui.internal.navigator.NavigatorPlugin;
import org.eclipse.ui.internal.navigator.Policy;
import org.eclipse.ui.navigator.ICommonContentProvider;
import org.eclipse.ui.navigator.ICommonLabelProvider;
import org.eclipse.ui.navigator.INavigatorContentDescriptor;
import org.eclipse.ui.navigator.OverridePolicy;
import org.eclipse.ui.navigator.Priority;

/**
 * Encapsulates the <code>org.eclipse.ui.navigator.navigatorContent</code>
 * extension point.
 *
 * @since 3.2
 */
public final class NavigatorContentDescriptor implements
		INavigatorContentDescriptor, INavigatorContentExtPtConstants {

	private static final int HASH_CODE_NOT_COMPUTED = -1;
	private String id;

	private String name;

	private IConfigurationElement configElement;

	private int priority = Priority.NORMAL_PRIORITY_VALUE;

	/**
	 * This is calculated based on the priority and appearsBeforeId when all of the descriptors
	 * are first loaded. This is what's used to sort on after that.
	 */
	private int sequenceNumber;

	private String appearsBeforeId;

	private Expression enablement;

	private Expression possibleChildren;

	private Expression initialActivation;

	private String icon;

	private boolean activeByDefault;

	private IPluginContribution contribution;

	private boolean sortOnly;

	private Set overridingExtensions;
	private List overridingExtensionsList; // FIXME: will replace 'overridingExtensions' in 3.6

	private OverridePolicy overridePolicy;

	private String suppressedExtensionId;

	private INavigatorContentDescriptor overriddenDescriptor;

	private int hashCode = HASH_CODE_NOT_COMPUTED;

	private boolean providesSaveables;

	/**
	 * Creates a new content descriptor from a configuration element.
	 *
	 * @param configElement
	 *            configuration element to create a descriptor from
	 *
	 * @throws WorkbenchException
	 *             if the configuration element could not be parsed. Reasons
	 *             include:
	 *             <ul>
	 *             <li>A required attribute is missing.</li>
	 *             <li>More elements are define than is allowed.</li>
	 *             </ul>
	 */
	/* package */ NavigatorContentDescriptor(IConfigurationElement configElement)
			throws WorkbenchException {
		super();
		this.configElement = configElement;
		init();
	}

	@Override
	public String getId() {
		return id;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public int getPriority() {
		return priority;
	}

	/**
	 * @return the sequence number
	 */
	@Override
	public int getSequenceNumber() {
		return sequenceNumber;
	}

	void setSequenceNumber(int num) {
		sequenceNumber = num;
	}

	/**
	 *
	 * @return The value specified by the <i>appearsBefore</i> attribute of the
	 *         &lt;navigatorContent/&gt; element.
	 */
	@Override
	public String getAppearsBeforeId() {
		return appearsBeforeId;
	}

	@Override
	public boolean isSortOnly() {
		return sortOnly;
	}

	/**
	 * Parses the configuration element.
	 *
	 * @throws WorkbenchException
	 *             if the configuration element could not be parsed. Reasons
	 *             include:
	 *             <ul>
	 *             <li>A required attribute is missing.</li>
	 *             <li>More elements are define than is allowed.</li>
	 *             </ul>
	 */
	private void init() throws WorkbenchException {
		id = configElement.getAttribute(ATT_ID);
		name = configElement.getAttribute(ATT_NAME);
		String priorityString = configElement.getAttribute(ATT_PRIORITY);
		icon = configElement.getAttribute(ATT_ICON);

		String activeByDefaultString = configElement
				.getAttribute(ATT_ACTIVE_BY_DEFAULT);
		activeByDefault = (activeByDefaultString != null && activeByDefaultString
				.length() > 0) ? Boolean.parseBoolean(activeByDefaultString) : true;

		String providesSaveablesString = configElement
			.getAttribute(ATT_PROVIDES_SAVEABLES);
		providesSaveables = (providesSaveablesString != null && providesSaveablesString
				.length() > 0) ? Boolean.parseBoolean(providesSaveablesString) : false;
		appearsBeforeId = configElement.getAttribute(ATT_APPEARS_BEFORE);

		if (priorityString != null) {
			try {
				Priority p = Priority.get(priorityString);
				priority = p != null ? p.getValue()
						: Priority.NORMAL_PRIORITY_VALUE;
			} catch (NumberFormatException exception) {
				priority = Priority.NORMAL_PRIORITY_VALUE;
			}
		}

		// We start with this because the sort ExtensionPriorityComparator works
		// from the sequenceNumber
		sequenceNumber = priority;

		String sortOnlyString = configElement.getAttribute(ATT_SORT_ONLY);
		sortOnly = (sortOnlyString != null && sortOnlyString.length() > 0) ? Boolean.parseBoolean(sortOnlyString) : false;

		if (id == null) {
			throw new WorkbenchException(NLS.bind(
					CommonNavigatorMessages.Attribute_Missing_Warning,
					new Object[] {
							ATT_ID,
							id,
							configElement.getDeclaringExtension()
									.getContributor().getName() }));
		}

		contribution = new IPluginContribution() {

			@Override
			public String getLocalId() {
				return getId();
			}

			@Override
			public String getPluginId() {
				return configElement.getDeclaringExtension().getContributor().getName();
			}

		};

		IConfigurationElement[] children;

		children = configElement.getChildren(TAG_INITIAL_ACTIVATION);
		if (children.length > 0) {
			if (children.length == 1) {
				initialActivation = new CustomAndExpression(children[0]);
			} else {
				throw new WorkbenchException(NLS.bind(
						CommonNavigatorMessages.Attribute_Missing_Warning, new Object[] {
								TAG_INITIAL_ACTIVATION, id,
								configElement.getDeclaringExtension().getContributor().getName() }));
			}
		}

		if (sortOnly)
			return;

		children = configElement.getChildren(TAG_ENABLEMENT);
		if (children.length == 0) {

			children = configElement.getChildren(TAG_TRIGGER_POINTS);
			if (children.length == 1) {
				enablement = new CustomAndExpression(children[0]);
			} else {
				throw new WorkbenchException(NLS.bind(
						CommonNavigatorMessages.Attribute_Missing_Warning,
						new Object[] {
								TAG_TRIGGER_POINTS,
								id,
								configElement.getDeclaringExtension()
										.getContributor().getName() }));
			}

			children = configElement.getChildren(TAG_POSSIBLE_CHILDREN);
			if (children.length == 1) {
				possibleChildren = new CustomAndExpression(children[0]);
			} else if(children.length > 1){
				throw new WorkbenchException(NLS.bind(
						CommonNavigatorMessages.Attribute_Missing_Warning,
						new Object[] {
								TAG_POSSIBLE_CHILDREN,
								id,
								configElement.getDeclaringExtension()
										.getContributor().getName() }));
			}
		} else if (children.length == 1) {
			try {
				enablement = ElementHandler.getDefault().create(
						ExpressionConverter.getDefault(), children[0]);
			} catch (CoreException e) {
				NavigatorPlugin.log(IStatus.ERROR, 0, e.getMessage(), e);
			}
		} else if (children.length > 1) {
			throw new WorkbenchException(NLS.bind(
					CommonNavigatorMessages.Attribute_Missing_Warning,
					new Object[] {
							TAG_ENABLEMENT,
							id,
							configElement.getDeclaringExtension()
									.getContributor().getName() }));
		}

		children = configElement.getChildren(TAG_OVERRIDE);
		if (children.length == 0) {
			overridePolicy = OverridePolicy.get(OverridePolicy.InvokeAlwaysRegardlessOfSuppressedExt_LITERAL);
		} else if (children.length == 1) {
			suppressedExtensionId = children[0]
					.getAttribute(ATT_SUPPRESSED_EXT_ID);
			overridePolicy = OverridePolicy.get(children[0]
					.getAttribute(ATT_POLICY));
		} else if (children.length > 1) {
			throw new WorkbenchException(NLS.bind(
					CommonNavigatorMessages.Too_many_elements_Warning,
					new Object[] {
							TAG_OVERRIDE,
							id,configElement.getDeclaringExtension()
									.getContributor().getName() }));
		}

	}

	/**
	 * @return Returns the icon.
	 */
	public String getIcon() {
		return icon;
	}

	/**
	 * @return Returns the suppressedExtensionId or null if none specified.
	 */
	@Override
	public String getSuppressedExtensionId() {
		return suppressedExtensionId;
	}

	/**
	 * @return Returns the overridePolicy or null if this extension does not
	 *         override another extension.
	 */
	@Override
	public OverridePolicy getOverridePolicy() {
		return overridePolicy;
	}

	/**
	 * @return Returns the contribution.
	 */
	public IPluginContribution getContribution() {
		return contribution;
	}

	/**
	 * @return the configuration element
	 */
	public IConfigurationElement getConfigElement() {
		return configElement;
	}

	/**
	 * The content provider could be an instance of
	 * {@link ICommonContentProvider}, but only {@link ITreeContentProvider} is
	 * required.
	 *
	 *
	 * @return An instance of the Content provider defined for this extension.
	 * @throws CoreException
	 *             if an instance of the executable extension could not be
	 *             created for any reason
	 *
	 */
	public ITreeContentProvider createContentProvider() throws CoreException {
		if (Policy.DEBUG_EXTENSION_SETUP)
			System.out.println("createContentProvider: " + this); //$NON-NLS-1$
		return (ITreeContentProvider) configElement
				.createExecutableExtension(ATT_CONTENT_PROVIDER);
	}

	/**
	 *
	 * The content provider could be an instance of {@link ICommonLabelProvider},
	 * but only {@link ILabelProvider} is required.
	 *
	 * @return An instance of the Label provider defined for this extension
	 * @throws CoreException
	 *             if an instance of the executable extension could not be
	 *             created for any reason
	 */
	public ILabelProvider createLabelProvider() throws CoreException {
		if (Policy.DEBUG_EXTENSION_SETUP)
			System.out.println("createLabelProvider: " + this); //$NON-NLS-1$
		return (ILabelProvider) configElement
				.createExecutableExtension(ATT_LABEL_PROVIDER);
	}

	@Override
	public boolean isActiveByDefault() {
		if (activeByDefault)
			return true;
		if (initialActivation == null)
			return false;
		IEvaluationContext context = NavigatorPlugin.getEvalContext(new Object());
		return NavigatorPlugin.safeEvaluate(initialActivation, context) == EvaluationResult.TRUE;
	}

	/**
	 * Determine if this content extension would be able to provide children for
	 * the given element.
	 *
	 * @param anElement
	 *            The element that should be used for the evaluation.
	 * @return True if and only if the extension is enabled for the element.
	 */
	@Override
	public boolean isTriggerPoint(Object anElement) {

		if (enablement == null || anElement == null) {
			return false;
		}

		IEvaluationContext context = NavigatorPlugin.getEvalContext(anElement);
		return NavigatorPlugin.safeEvaluate(enablement, context) == EvaluationResult.TRUE;
	}

	/**
	 * Determine if this content extension could provide the given element as a
	 * child.
	 *
	 * <p>
	 * This method is used to determine what the parent of an element could be
	 * for Link with Editor support.
	 * </p>
	 *
	 * @param anElement
	 *            The element that should be used for the evaluation.
	 * @return True if and only if the extension might provide an object of this
	 *         type as a child.
	 */
	@Override
	public boolean isPossibleChild(Object anElement) {

		if ((enablement == null && possibleChildren == null)
				|| anElement == null) {
			return false;
		} else if(anElement instanceof IStructuredSelection) {
			return arePossibleChildren((IStructuredSelection) anElement);
		}

		IEvaluationContext context = NavigatorPlugin.getEvalContext(anElement);
		if (possibleChildren != null) {
			return NavigatorPlugin.safeEvaluate(possibleChildren, context) == EvaluationResult.TRUE;
		} else if (enablement != null) {
			return NavigatorPlugin.safeEvaluate(enablement, context) == EvaluationResult.TRUE;
		}
		return false;
	}

	/**
	 * A convenience method to check all elements in a selection.
	 *
	 * @param aSelection A non-null selection
	 * @return True if and only if every element in the selection is a possible child.
	 */
	@Override
	public boolean arePossibleChildren(IStructuredSelection aSelection) {
		if(aSelection.isEmpty()) {
			return false;
		}
		for (Object element : aSelection) {
			if(!isPossibleChild(element)) {
				return false;
			}
		}
		return true;
	}

	/**
	 *
	 * Does not force the creation of the set of overriding extensions.
	 *
	 * @return True if this extension has overriding extensions.
	 */
	@Override
	public boolean hasOverridingExtensions() {
		return overridingExtensions != null && overridingExtensions.size() > 0;
	}

	/**
	 * @return The set of overriding extensions (of type
	 *         {@link INavigatorContentDescriptor}
	 */
	@Override
	public Set getOverriddingExtensions() {
		if (overridingExtensions == null) {
			overridingExtensions = new TreeSet(ExtensionSequenceNumberComparator.DESCENDING);
		}
		return overridingExtensions;
	}

	/**
	 *  Returns a list iterator over the overriding extensions.
	 *
	 * @param fromStart
	 *            <code>true</code> if list iterator starts at the beginning and
	 *            <code>false</code> if it starts at the end of the list
	 * @return a list iterator over the overriding extensions which are ordered
	 *         by ExtensionPriorityComparator.DESCENDING
	 */
	public ListIterator getOverridingExtensionsListIterator(boolean fromStart) {
		if (overridingExtensions == null)
			return Collections.EMPTY_LIST.listIterator();

		if (overridingExtensionsList == null)
			overridingExtensionsList = new ArrayList(overridingExtensions);

		return overridingExtensionsList.listIterator(fromStart ? 0 : overridingExtensionsList.size());
	}

	@Override
	public String toString() {
		return "Content[" + id  + "(" + sequenceNumber + ") " + ", \"" + name + "\"]"; //$NON-NLS-1$ //$NON-NLS-2$//$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
	}

	@Override
	public int hashCode() {
		if (hashCode == HASH_CODE_NOT_COMPUTED) {
			String hashCodeString = configElement.getNamespaceIdentifier() + getId();
			hashCode = hashCodeString.hashCode();
			if (hashCode == HASH_CODE_NOT_COMPUTED)
				hashCode++;
		}
		return hashCode;
	}

	/**
	 * @return The descriptor of the <code>suppressedExtensionId</code> if
	 *         non-null.
	 */
	@Override
	public INavigatorContentDescriptor getOverriddenDescriptor() {
		return overriddenDescriptor;
	}

	/**
	 * @param theOverriddenDescriptor
	 *            The overriddenDescriptor to set.
	 */
	/* package */void setOverriddenDescriptor(
			INavigatorContentDescriptor theOverriddenDescriptor) {
		overriddenDescriptor = theOverriddenDescriptor;
	}

	@Override
	public boolean hasSaveablesProvider() {
		return providesSaveables;
	}

}
