/*******************************************************************************
 * Copyright (c) 2005, 2012 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
 *     Marcel Bruch <bruch@cs.tu-darmstadt.de> - [content assist] Allow to re-sort proposals - https://bugs.eclipse.org/bugs/show_bug.cgi?id=350991
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.osgi.framework.Bundle;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.PerformanceStats;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;

import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer;

import org.eclipse.jdt.internal.ui.JavaPlugin;

/**
 * The description of an extension to the
 * <code>org.eclipse.jdt.ui.javaCompletionProposalComputer</code> extension point. Instances are
 * immutable. Instances can be obtained from a {@link CompletionProposalComputerRegistry}.
 *
 * @see CompletionProposalComputerRegistry
 * @since 3.2
 */
final class CompletionProposalComputerDescriptor {
	/** The default category id. */
	private static final String DEFAULT_CATEGORY_ID= "org.eclipse.jdt.ui.defaultProposalCategory"; //$NON-NLS-1$
	/** The extension schema name of the category id attribute. */
	private static final String CATEGORY_ID= "categoryId"; //$NON-NLS-1$
	/** The extension schema name of the partition type attribute. */
	private static final String TYPE= "type"; //$NON-NLS-1$
	/** The extension schema name of the class attribute. */
	private static final String CLASS= "class"; //$NON-NLS-1$
	/** The extension schema name of the activate attribute. */
	private static final String ACTIVATE= "activate"; //$NON-NLS-1$
	/** The extension schema name of the needsSortingAfterFiltering attribute. */
	private static final String NEEDS_SORTING_AFTER_FILTERING= "needsSortingAfterFiltering"; //$NON-NLS-1$
	/** The extension schema name of the partition child elements. */
	private static final String PARTITION= "partition"; //$NON-NLS-1$
	/** Set of Java partition types. */
	private static final Set<String> PARTITION_SET;
	/** The name of the performance event used to trace extensions. */
	private static final String PERFORMANCE_EVENT= JavaPlugin.getPluginId() + "/perf/content_assist/extensions"; //$NON-NLS-1$
	/**
	 * If <code>true</code>, execution time of extensions is measured and the data forwarded to
	 * core's {@link PerformanceStats} service.
	 */
	private static final boolean MEASURE_PERFORMANCE= PerformanceStats.isEnabled(PERFORMANCE_EVENT);
	/**
	 * Independently of the {@link PerformanceStats} service, any operation that takes longer than
	 * {@value} milliseconds will be flagged as an violation. This timeout does not apply to the
	 * first invocation, as it may take longer due to plug-in initialization etc. See also
	 * {@link #fIsReportingDelay}.
	 */
	private static final long MAX_DELAY= 5000;

	/* log constants */
	private static final String COMPUTE_COMPLETION_PROPOSALS= "computeCompletionProposals()"; //$NON-NLS-1$
	private static final String COMPUTE_CONTEXT_INFORMATION= "computeContextInformation()"; //$NON-NLS-1$
	private static final String SESSION_STARTED= "sessionStarted()"; //$NON-NLS-1$
	private static final String SESSION_ENDED= "sessionEnded()"; //$NON-NLS-1$

	static {
		Set<String> partitions= new HashSet<>();
		partitions.add(IDocument.DEFAULT_CONTENT_TYPE);
		partitions.add(IJavaPartitions.JAVA_DOC);
		partitions.add(IJavaPartitions.JAVA_MULTI_LINE_COMMENT);
		partitions.add(IJavaPartitions.JAVA_SINGLE_LINE_COMMENT);
		partitions.add(IJavaPartitions.JAVA_STRING);
		partitions.add(IJavaPartitions.JAVA_CHARACTER);

		PARTITION_SET= Collections.unmodifiableSet(partitions);
	}

	/** The identifier of the extension. */
	private final String fId;
	/** The name of the extension. */
	private final String fName;
	/** The class name of the provided <code>IJavaCompletionProposalComputer</code>. */
	private final String fClass;
	/** The activate attribute value. */
	private final boolean fActivate;
	/** The partition of the extension (element type: {@link String}). */
	private final Set<String> fPartitions;
	/** The configuration element of this extension. */
	private final IConfigurationElement fElement;
	/** The registry we are registered with. */
	private final CompletionProposalComputerRegistry fRegistry;
	/** The computer, if instantiated, <code>null</code> otherwise. */
	private IJavaCompletionProposalComputer fComputer;
	/** The ui category. */
	private final CompletionProposalCategory fCategory;
	/** The first error message in the most recent operation, or <code>null</code>. */
	private String fLastError;
	/**
	 * Tells whether to inform the user when <code>MAX_DELAY</code> has been exceeded.
	 * We start timing execution after the first session because the first may take
	 * longer due to plug-in activation and initialization.
	 */
	private boolean fIsReportingDelay= false;
	/** The start of the last operation. */
	private long fStart;
	/**
	 * Tells whether we tried to load the computer.
	 * @since 3.4
	 */
	boolean fTriedLoadingComputer= false;
	
	/**
	 * Tells whether this proposal engine provides dynamic content that needs to be sorted after its
	 * proposal have been filtered. Filtering happens, e.g., when a user continues typing with an
	 * open completion window.
	 * 
	 * @since 3.8
	 */
	private boolean fNeedsSortingAfterFiltering;


	/**
	 * Creates a new descriptor.
	 *
	 * @param element the configuration element to read
	 * @param registry the computer registry creating this descriptor
	 * @param categories the categories
	 * @throws InvalidRegistryObjectException if this extension is no longer valid
	 * @throws CoreException if the extension contains invalid values
	 */
	CompletionProposalComputerDescriptor(IConfigurationElement element, CompletionProposalComputerRegistry registry, List<CompletionProposalCategory> categories) throws InvalidRegistryObjectException, CoreException {
		Assert.isLegal(registry != null);
		Assert.isLegal(element != null);

		fRegistry= registry;
		fElement= element;
		IExtension extension= element.getDeclaringExtension();
		fId= extension.getUniqueIdentifier();
		checkNotNull(fId, "id"); //$NON-NLS-1$

		String name= extension.getLabel();
		if (name.length() == 0)
			fName= fId;
		else
			fName= name;

		Set<String> partitions= new HashSet<>();
		IConfigurationElement[] children= element.getChildren(PARTITION);
		if (children.length == 0) {
			fPartitions= PARTITION_SET; // add to all partition types if no partition is configured
		} else {
			for (int i= 0; i < children.length; i++) {
				String type= children[i].getAttribute(TYPE);
				checkNotNull(type, TYPE);
				partitions.add(type);
			}
			fPartitions= Collections.unmodifiableSet(partitions);
		}

		String activateAttribute= element.getAttribute(ACTIVATE);
		fActivate= Boolean.valueOf(activateAttribute).booleanValue();

		String needsSortingAfterFilteringAttribute= element.getAttribute(NEEDS_SORTING_AFTER_FILTERING);
		fNeedsSortingAfterFiltering= Boolean.valueOf(needsSortingAfterFilteringAttribute).booleanValue();

		fClass= element.getAttribute(CLASS);
		checkNotNull(fClass, CLASS);

		String categoryId= element.getAttribute(CATEGORY_ID);
		if (categoryId == null)
			categoryId= DEFAULT_CATEGORY_ID;
		CompletionProposalCategory category= null;
		for (Iterator<CompletionProposalCategory> it= categories.iterator(); it.hasNext();) {
			CompletionProposalCategory cat= it.next();
			if (cat.getId().equals(categoryId)) {
				category= cat;
				break;
			}
		}
		if (category == null) {
			// create a category if it does not exist
			fCategory= new CompletionProposalCategory(categoryId, fName, registry);
			categories.add(fCategory);
		} else {
			fCategory= category;
		}
	}

	/**
	 * Checks that the given attribute value is not <code>null</code>.
	 *
	 * @param value the object to check if not null
	 * @param attribute the attribute
	 * @throws InvalidRegistryObjectException if the registry element is no longer valid
	 * @throws CoreException if <code>value</code> is <code>null</code>
	 */
	private void checkNotNull(Object value, String attribute) throws InvalidRegistryObjectException, CoreException {
		if (value == null) {
			Object[] args= { getId(), fElement.getContributor().getName(), attribute };
			String message= Messages.format(JavaTextMessages.CompletionProposalComputerDescriptor_illegal_attribute_message, args);
			IStatus status= new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, message, null);
			throw new CoreException(status);
		}
	}

	/**
	 * Returns the identifier of the described extension.
	 *
	 * @return Returns the id
	 */
	public String getId() {
		return fId;
	}

	/**
	 * Returns the name of the described extension.
	 *
	 * @return Returns the name
	 */
	public String getName() {
		return fName;
	}

	/**
	 * Returns the partition types of the described extension.
	 *
	 * @return the set of partition types (element type: {@link String})
	 */
	public Set<String> getPartitions() {
		return fPartitions;
	}

	/**
	 * Returns a cached instance of the computer as described in the
	 * extension's xml. If the computer is not yet created and
	 * <code>canCreate</code> is <code>true</code> then {@link #createComputer()}
	 * is called and the result cached.
	 *
	 * @param canCreate <code>true</code> if the proposal computer can be created
	 * @return a new instance of the completion proposal computer as
	 *         described by this descriptor
	 * @throws CoreException if the creation fails
	 * @throws InvalidRegistryObjectException if the extension is not
	 *         valid any longer (e.g. due to plug-in unloading)
	 */
	private synchronized IJavaCompletionProposalComputer getComputer(boolean canCreate) throws CoreException, InvalidRegistryObjectException {
		if (fComputer == null && canCreate && !fTriedLoadingComputer && (fActivate || isPluginLoaded())) {
			fTriedLoadingComputer= true;
			fComputer= createComputer();
		}
		return fComputer;
	}

	private boolean isPluginLoaded() {
		Bundle bundle= getBundle();
		return bundle != null && bundle.getState() == Bundle.ACTIVE;
	}

	private Bundle getBundle() {
		String namespace= fElement.getDeclaringExtension().getContributor().getName();
		Bundle bundle= Platform.getBundle(namespace);
		return bundle;
	}

	/**
	 * Returns a new instance of the computer as described in the
	 * extension's xml. Note that the safest way to access the computer
	 * is by using the
	 * {@linkplain #computeCompletionProposals(ContentAssistInvocationContext, IProgressMonitor) computeCompletionProposals}
	 * and
	 * {@linkplain #computeContextInformation(ContentAssistInvocationContext, IProgressMonitor) computeContextInformation}
	 * methods. These delegate the functionality to the contributed
	 * computer, but handle instance creation and any exceptions thrown.
	 *
	 * @return a new instance of the completion proposal computer as
	 *         described by this descriptor
	 * @throws CoreException if the creation fails
	 * @throws InvalidRegistryObjectException if the extension is not
	 *         valid any longer (e.g. due to plug-in unloading)
	 */
	public IJavaCompletionProposalComputer createComputer() throws CoreException, InvalidRegistryObjectException {
		return (IJavaCompletionProposalComputer) fElement.createExecutableExtension(CLASS);
	}

	/**
	 * Safely computes completion proposals through the described extension. If the extension
	 * is disabled, throws an exception or otherwise does not adhere to the contract described in
	 * {@link IJavaCompletionProposalComputer}, an empty list is returned.
	 *
	 * @param context the invocation context passed on to the extension
	 * @param monitor the progress monitor passed on to the extension
	 * @return the list of computed completion proposals (element type:
	 *         {@link org.eclipse.jface.text.contentassist.ICompletionProposal})
	 */
	public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {
		if (!isEnabled())
			return Collections.emptyList();

		IStatus status;
		try {
			IJavaCompletionProposalComputer computer= getComputer(true);
			if (computer == null) // not active yet
				return Collections.emptyList();

			try {
				PerformanceStats stats= startMeter(context, computer);
				List<ICompletionProposal> proposals= computer.computeCompletionProposals(context, monitor);
				stopMeter(stats, COMPUTE_COMPLETION_PROPOSALS);

				if (proposals != null) {
					fLastError= computer.getErrorMessage();
					return proposals;
				}
			} finally {
				fIsReportingDelay= true;
			}
			status= createAPIViolationStatus(COMPUTE_COMPLETION_PROPOSALS);
		} catch (InvalidRegistryObjectException x) {
			status= createExceptionStatus(x);
		} catch (CoreException x) {
			status= createExceptionStatus(x);
		} catch (RuntimeException x) {
			status= createExceptionStatus(x);
		} finally {
			monitor.done();
		}

		fRegistry.informUser(this, status);

		return Collections.emptyList();
	}

	/**
	 * Safely computes context information objects through the described extension. If the extension
	 * is disabled, throws an exception or otherwise does not adhere to the contract described in
	 * {@link IJavaCompletionProposalComputer}, an empty list is returned.
	 *
	 * @param context the invocation context passed on to the extension
	 * @param monitor the progress monitor passed on to the extension
	 * @return the list of computed context information objects (element type:
	 *         {@link org.eclipse.jface.text.contentassist.IContextInformation})
	 */
	public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context, IProgressMonitor monitor) {
		if (!isEnabled())
			return Collections.emptyList();

		IStatus status;
		try {
			IJavaCompletionProposalComputer computer= getComputer(true);
			if (computer == null) // not active yet
				return Collections.emptyList();

			PerformanceStats stats= startMeter(context, computer);
			List<IContextInformation> proposals= computer.computeContextInformation(context, monitor);
			stopMeter(stats, COMPUTE_CONTEXT_INFORMATION);

			if (proposals != null) {
				fLastError= computer.getErrorMessage();
				return proposals;
			}

			status= createAPIViolationStatus(COMPUTE_CONTEXT_INFORMATION);
		} catch (InvalidRegistryObjectException x) {
			status= createExceptionStatus(x);
		} catch (CoreException x) {
			status= createExceptionStatus(x);
		} catch (RuntimeException x) {
			status= createExceptionStatus(x);
		} finally {
			monitor.done();
		}

		fRegistry.informUser(this, status);

		return Collections.emptyList();
	}


	/**
	 * Notifies the described extension of a proposal computation session start.
	 * <p><em>
	 * Note: This method is called every time code assist is invoked and
	 * is <strong>not</strong> filtered by partition type.
	 * </em></p>
	 */
	public void sessionStarted() {
		if (!isEnabled())
			return;

		IStatus status;
		try {
			IJavaCompletionProposalComputer computer= getComputer(true);
			if (computer == null) // not active yet
				return;

			PerformanceStats stats= startMeter(SESSION_STARTED, computer);
			computer.sessionStarted();
			stopMeter(stats, SESSION_ENDED);

			return;
		} catch (InvalidRegistryObjectException x) {
			status= createExceptionStatus(x);
		} catch (CoreException x) {
			status= createExceptionStatus(x);
		} catch (RuntimeException x) {
			status= createExceptionStatus(x);
		}

		fRegistry.informUser(this, status);
	}

	/**
	 * Notifies the described extension of a proposal computation session end.
	 * <p><em>
	 * Note: This method is called every time code assist is invoked and
	 * is <strong>not</strong> filtered by partition type.
	 * </em></p>
	 */
	public void sessionEnded() {
		if (!isEnabled())
			return;

		IStatus status;
		try {
			IJavaCompletionProposalComputer computer= getComputer(false);
			if (computer == null) // not active yet
				return;

			PerformanceStats stats= startMeter(SESSION_ENDED, computer);
			computer.sessionEnded();
			stopMeter(stats, SESSION_ENDED);

			return;
		} catch (InvalidRegistryObjectException x) {
			status= createExceptionStatus(x);
		} catch (CoreException x) {
			status= createExceptionStatus(x);
		} catch (RuntimeException x) {
			status= createExceptionStatus(x);
		}

		fRegistry.informUser(this, status);
	}

	private PerformanceStats startMeter(Object context, IJavaCompletionProposalComputer computer) {
		final PerformanceStats stats;
		if (MEASURE_PERFORMANCE) {
			stats= PerformanceStats.getStats(PERFORMANCE_EVENT, computer);
			stats.startRun(context.toString());
		} else {
			stats= null;
		}

		if (fIsReportingDelay) {
			fStart= System.currentTimeMillis();
		}

		return stats;
	}

	private void stopMeter(final PerformanceStats stats, String operation) {
		if (MEASURE_PERFORMANCE) {
			stats.endRun();
			if (stats.isFailure()) {
				IStatus status= createPerformanceStatus(operation);
				fRegistry.informUser(this, status);
				return;
			}
		}

		if (fIsReportingDelay) {
			long current= System.currentTimeMillis();
			if (current - fStart > MAX_DELAY) {
				IStatus status= createPerformanceStatus(operation);
				fRegistry.informUser(this, status);
			}
		}
	}

	private IStatus createExceptionStatus(InvalidRegistryObjectException x) {
		// extension has become invalid - log & disable
		String blame= createBlameMessage();
		String reason= JavaTextMessages.CompletionProposalComputerDescriptor_reason_invalid;
		return new Status(IStatus.INFO, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
	}

	private IStatus createExceptionStatus(CoreException x) {
		// unable to instantiate the extension - log
		String blame= createBlameMessage();
		String reason= JavaTextMessages.CompletionProposalComputerDescriptor_reason_instantiation;
		return new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
	}

	private IStatus createExceptionStatus(RuntimeException x) {
		// misbehaving extension - log
		String blame= createBlameMessage();
		String reason= JavaTextMessages.CompletionProposalComputerDescriptor_reason_runtime_ex;
		return new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
	}

	private IStatus createAPIViolationStatus(String operation) {
		String blame= createBlameMessage();
		Object[] args= {operation};
		String reason= Messages.format(JavaTextMessages.CompletionProposalComputerDescriptor_reason_API, args);
		return new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, null); //$NON-NLS-1$
	}

	private IStatus createPerformanceStatus(String operation) {
		String blame= createBlameMessage();
		Object[] args= {operation};
		String reason= Messages.format(JavaTextMessages.CompletionProposalComputerDescriptor_reason_performance, args);
		return new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, null); //$NON-NLS-1$
	}

	private String createBlameMessage() {
		Object[] args= { getName(), fElement.getDeclaringExtension().getContributor().getName() };
		String disable= Messages.format( JavaTextMessages.CompletionProposalComputerDescriptor_blame_message, args);
		return disable;
	}

	/**
	 * Returns the enablement state of the described extension.
	 *
	 * @return the enablement state of the described extension
	 */
	private boolean isEnabled() {
		return fCategory.isEnabled();
	}

	CompletionProposalCategory getCategory() {
		return fCategory;
	}

	/**
	 * Returns the error message from the described extension.
	 *
	 * @return the error message from the described extension
	 */
	public String getErrorMessage() {
		return fLastError;
	}

	/**
	 * Returns the contributor of the described extension.
	 *
	 * @return the contributor of the described extension
	 */
    IContributor getContributor()  {
        try {
	        return fElement.getContributor();
        } catch (InvalidRegistryObjectException e) {
        	return null;
        }
    }

	/**
	 * Returns the <code>needsSortingAfterFiltering</code> flag of the described extension.
	 * 
	 * @return the needsSortingAfterFiltering flag of the described extension
	 * @since 3.8
	 */
	public boolean isSortingAfterFilteringNeeded() {
		return fNeedsSortingAfterFiltering;
	}
}
