/*******************************************************************************
 * Copyright (c) 2010, 2018 IBM Corporation and others.
 * All rights reserved. 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.wst.sse.ui.internal.contentassist;

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

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.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.swt.widgets.Display;
import org.eclipse.wst.sse.ui.contentassist.CompletionProposalInvocationContext;
import org.eclipse.wst.sse.ui.contentassist.IAsyncCompletionProposalComputer;
import org.eclipse.wst.sse.ui.contentassist.ICompletionProposalComputer;
import org.eclipse.wst.sse.ui.internal.Logger;
import org.eclipse.wst.sse.ui.internal.SSEUIPlugin;
import org.osgi.framework.Bundle;

/**
 * Wraps an {@link ICompletionProposalComputer} provided by an extension to the
 * <code>org.eclipse.wst.sse.ui.completionProposal</code> extension point.
 * Instances are immutable. Instances can be obtained from a
 * {@link CompletionProposalComputerRegistry}.
 *
 * @see CompletionProposalComputerRegistry
 */
final class CompletionProposalComputerDescriptor {
	/** The default category id. */
	private static final String DEFAULT_CATEGORY_ID= "org.eclipse.wst.sse.ui.defaultProposalCategory"; //$NON-NLS-1$
	
	/** The extension schema name of the category id attribute. */
	private static final String ATTR_CATEGORY_ID= "categoryId"; //$NON-NLS-1$
	
	/** The extension schema name for element ID attributes */
	private static final String ATTR_ID= "id"; //$NON-NLS-1$
	
	/** The extension schema name for element name attributes */
	private static final String ATTR_NAME= "name"; //$NON-NLS-1$
	
	/** The extension schema name of the class attribute. */
	private static final String ATTR_CLASS= "class"; //$NON-NLS-1$
	
	/** The extension schema name of the activate attribute. */
	private static final String ATTRACTIVATE= "activate"; //$NON-NLS-1$
	
	/** The extension schema name of the content type child elements. */
	private static final String ELEM_CONTENT_TYPE = "contentType"; //$NON-NLS-1$
	
	/** The extension schema name of the partition type child elements. */
	private static final String ELEM_PARTITION_TYPE= "partitionType"; //$NON-NLS-1$
	
	/** The name of the performance event used to trace extensions. */
	private static final String PERFORMANCE_EVENT= SSEUIPlugin.ID + "/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= 500000;

	/* 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$

	/** 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>ISSECompletionProposalComputer</code>. */
	private final String fClass;
	
	/** The activate attribute value. */
	private final boolean fActivate;
	
	/** The configuration element of this extension. */
	private final IConfigurationElement fElement;
	
	/** The computer, if instantiated, <code>null</code> otherwise. */
	private ICompletionProposalComputer 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;

	/**
	 * <p>Creates a new descriptor.</p>
	 * <p><b>NOTE: </b> This will not add this new descriptor to the given
	 * {@link CompletionProposalComputerRegistry}. That can not be done
	 * until this descriptor is done being constructed.  Therefore be sure
	 * to call {@link #addToRegistry()} after creating a new descriptor.</p>
	 *
	 * @param element the configuration element to read
	 * @param categories the categories
	 * 
	 * @throws InvalidRegistryObjectException if this extension is no longer valid
	 * @throws CoreException if the extension contains invalid values
	 */
	CompletionProposalComputerDescriptor(IConfigurationElement element, List categories) throws InvalidRegistryObjectException, CoreException {
		Assert.isLegal(element != null);
		fElement = element;
		
		//get & verify ID
		fId = fElement.getAttribute(ATTR_ID);
		ContentAssistUtils.checkExtensionAttributeNotNull(fId, ATTR_ID, fElement);

		//get & verify optional name
		String name= fElement.getAttribute(ATTR_NAME);
		if (name == null) {
			fName= fId;
		} else {
			fName= name;
		}

		//get & verify activate plugin attribute
		String activateAttribute= fElement.getAttribute(ATTRACTIVATE);
		fActivate = Boolean.valueOf(activateAttribute).booleanValue();

		//get & verify class
		fClass= fElement.getAttribute(ATTR_CLASS);
		ContentAssistUtils.checkExtensionAttributeNotNull(fClass, ATTR_CLASS, fElement);

		//get & verify optional category id
		String categoryId= fElement.getAttribute(ATTR_CATEGORY_ID);
		if (categoryId == null) {
			categoryId= DEFAULT_CATEGORY_ID;
		}
		
		//find the category with the determined category id
		CompletionProposalCategory category= null;
		for (Iterator it= categories.iterator(); it.hasNext();) {
			CompletionProposalCategory cat= (CompletionProposalCategory) it.next();
			if (cat.getId().equals(categoryId)) {
				category= cat;
				break;
			}
		}
		
		/* create a category if it does not exist
		 * else just set the category
		 */
		if (category == null) {
			fCategory = new CompletionProposalCategory(categoryId, fName);
			
			/* will add the new category to the registers list of categories,
			 *  by the magic of object references
			 */
			categories.add(fCategory);
		} else {
			fCategory = category;
		}
	}
	
	/**
	 * <p>Adds this descriptor to the {@link CompletionProposalComputerRegistry}.</p>
	 * <p><b>NOTE: </b>Must be done after descriptor creation or the descriptor will
	 * not be added to the registry. Can not be done in constructor because
	 * descriptor must be constructed before it can be added to the registry</p>
	 * 
	 * 
	 * @throws InvalidRegistryObjectException
	 * @throws CoreException
	 */
	void addToRegistry() throws InvalidRegistryObjectException, CoreException {
		parseActivationAndAddToRegistry(this.fElement, this);
	}
	
	/**
	 * @return the category that the wrapped {@link ICompletionProposalComputer} is
	 * associated with.
	 */
	CompletionProposalCategory getCategory() {
		return fCategory;
	}
	
	/**
	 * @return the contributor of the described {@link ICompletionProposalComputer}
	 */
    IContributor getContributor()  {
        try {
	        return fElement.getContributor();
        } catch (InvalidRegistryObjectException e) {
        	return null;
        }
    }

	/**
	 * @return Returns the id of the described {@link ICompletionProposalComputer}
	 */
	public String getId() {
		return fId;
	}

	/**
	 * @return the name of the described {@link ICompletionProposalComputer}
	 */
	public String getName() {
		return fName;
	}

	/**
	 * 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 ICompletionProposalComputer createComputer() throws CoreException, InvalidRegistryObjectException {
		return (ICompletionProposalComputer) fElement.createExecutableExtension(ATTR_CLASS);
	}

	/**
	 * <p>Safely computes completion proposals through the described extension.</p>
	 *
	 * @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 computeCompletionProposals(CompletionProposalInvocationContext context, IProgressMonitor monitor) {
		List completionProposals = Collections.EMPTY_LIST;
		if (isEnabled()) {
			IStatus status = null;
			try {
				// plugin must be active to get computer
				ICompletionProposalComputer computer = getComputer(true);
				if (computer != null) {
					try {
						PerformanceStats stats= startMeter(context, computer);
						//ask the computer for the proposals
						List proposals[] = new List[1];
						if (computer instanceof IAsyncCompletionProposalComputer || Display.getCurrent() != null) {
							proposals[0] = computer.computeCompletionProposals(context, monitor);
						} else {
							Display.getDefault().syncExec(()-> {
								proposals[0] = computer.computeCompletionProposals(context, monitor);
							});
						}
						stopMeter(stats, COMPUTE_COMPLETION_PROPOSALS);
		
						if (proposals[0] != null) {
							fLastError = computer.getErrorMessage();
							completionProposals = proposals[0];
						} else {
							status = createAPIViolationStatus(COMPUTE_COMPLETION_PROPOSALS);
						}
					} finally {
						fIsReportingDelay = true;
					}
				}
			} catch (InvalidRegistryObjectException x) {
				status= createExceptionStatus(x);
			} catch (CoreException x) {
				status= createExceptionStatus(x);
			} catch (RuntimeException x) {
				status= createExceptionStatus(x);
			} finally {
				monitor.done();
			}
	
			if(status != null) {
				Logger.log(status);
			}
		}

		return completionProposals;
	}

	/**
	 * <p>Safely computes context information objects through the described extension.</p>
	 *
	 * @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 computeContextInformation(CompletionProposalInvocationContext context, IProgressMonitor monitor) {
		List contextInformation = Collections.EMPTY_LIST;
		if (isEnabled()) {
			IStatus status = null;
			try {
				// plugin must be active to get computer
				ICompletionProposalComputer computer = getComputer(true);
				if (computer != null) {
					PerformanceStats stats= startMeter(context, computer);
					
					final List proposals[]= new List[1];
					if (computer instanceof IAsyncCompletionProposalComputer || Display.getCurrent() != null) {
						proposals[0] = computer.computeContextInformation(context, monitor);
					} else {
						Display.getDefault().syncExec(()-> {
							proposals[0] = computer.computeContextInformation(context, monitor);
						}); 
					}
					stopMeter(stats, COMPUTE_CONTEXT_INFORMATION);
		
					if (proposals[0] != null) {
						fLastError= computer.getErrorMessage();
						contextInformation = proposals[0];
					} else {
						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();
			}
	
			if(status != null) {
				Logger.log(status);
			}
		}
		
		return contextInformation;
	}

	/**
	 * <p>Notifies the described extension of a proposal computation session start.</p>
	 * 
	 * <p><b>Note: </b>This method is called every time code assist is invoked and
	 * is <strong>not</strong> filtered by content type or partition type.</p>
	 */
	public void sessionStarted() {
		if (isEnabled()) {
			IStatus status = null;
			try {
				// plugin must be active to get computer
				ICompletionProposalComputer computer = getComputer(true);
				if (computer != null) {
					PerformanceStats stats = startMeter(SESSION_STARTED, computer);
					computer.sessionStarted();
					stopMeter(stats, SESSION_ENDED);
				}
			} catch (InvalidRegistryObjectException x) {
				status= createExceptionStatus(x);
			} catch (CoreException x) {
				status= createExceptionStatus(x);
			} catch (RuntimeException x) {
				status= createExceptionStatus(x);
			}
	
			if(status != null) {
				Logger.log(status);
			}
		}
	}

	/**
	 * <p>Notifies the described extension of a proposal computation session end.</p>
	 * 
	 * <p><b>Note: </b>This method is called every time code assist is invoked and
	 * is <strong>not</strong> filtered by content type or partition type.</p>
	 */
	public void sessionEnded() {
		if (isEnabled()) {
			IStatus status = null;
			try {
				// plugin must be active to get computer
				ICompletionProposalComputer computer = getComputer(true);
				if (computer != null) {
					PerformanceStats stats= startMeter(SESSION_ENDED, computer);
					computer.sessionEnded();
					stopMeter(stats, SESSION_ENDED);
				}
			} catch (InvalidRegistryObjectException x) {
				status= createExceptionStatus(x);
			} catch (CoreException x) {
				status= createExceptionStatus(x);
			} catch (RuntimeException x) {
				status= createExceptionStatus(x);
			}
	
			if(status != null) {
				Logger.log(status);
			}
		}
	}
	
	/**
	 * @return the error message from the described {@link ICompletionProposalComputer}
	 */
	public String getErrorMessage() {
		return fLastError;
	}
	
	/**
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return fId + ": " + fName; //$NON-NLS-1$
	}
	
	/**
	 * <p>Parses the given configuration element for its activation context,
	 * that is to say the content types and partiton types and updates the registry
	 * to associated the given computer descriptor with the parsed activation contexts.</P>
	 * 
	 * <p>This is useful for parsing both <tt>proposalComputer</tt> elements and
	 * <tt>proposalComputerExtendedActivation</tt> elements.</p>
	 * 
	 * @param element {@link IConfigurationElement} containing the activation context
	 * @param desc {@link CompletionProposalComputerDescriptor} to associate with the parsed activation context
	 * 
	 * @throws InvalidRegistryObjectException
	 * @throws CoreException
	 */
	protected static void parseActivationAndAddToRegistry(IConfigurationElement element,
			CompletionProposalComputerDescriptor desc) throws InvalidRegistryObjectException, CoreException {
		
		/* if this descriptor is specific to a content type/s add it to the registry as such
		 * else add to registry for all content types
		 */
		IConfigurationElement[] contentTypes = element.getChildren(ELEM_CONTENT_TYPE);
		if(contentTypes.length > 0) {
			for(int contentTypeIndex = 0; contentTypeIndex < contentTypes.length; ++contentTypeIndex) {
				String contentTypeID = contentTypes[contentTypeIndex].getAttribute(ATTR_ID);
				ContentAssistUtils.checkExtensionAttributeNotNull(contentTypeID, ATTR_ID, contentTypes[contentTypeIndex]);
				
				/* if this descriptor is for specific partition types in the content type
				 * add it to the registry as such
				 * else add to the registry for all partition types in the content type
				 */
				IConfigurationElement[] partitionTypes = contentTypes[contentTypeIndex].getChildren(ELEM_PARTITION_TYPE);
				if(partitionTypes.length > 0) {
					for (int partitionTypeIndex = 0; partitionTypeIndex < partitionTypes.length; ++partitionTypeIndex) {
						String partitionTypeID = partitionTypes[partitionTypeIndex].getAttribute(ATTR_ID);
						ContentAssistUtils.checkExtensionAttributeNotNull(partitionTypeID, ATTR_ID, partitionTypes[partitionTypeIndex]);
						
						CompletionProposalComputerRegistry.getDefault().putDescription(contentTypeID, partitionTypeID, desc);
						CompletionProposalComputerRegistry.getDefault().putAutoActivator(contentTypeID, partitionTypeID, partitionTypes[partitionTypeIndex]);
					}
				} else {
					CompletionProposalComputerRegistry.getDefault().putDescription(contentTypeID, null, desc);
				}
			}
		} else {
			Logger.log(Logger.WARNING, "The configuration element: " + element + " does not contain any content types."); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	/**
	 * @return <code>true</code> if the plugin that contains the {@link IConfigurationElement}
	 * associated with this descriptor is loaded, <code>false</code> otherwise.
	 */
	private boolean isPluginLoaded() {
		Bundle bundle= getBundle();
		return bundle != null && bundle.getState() == Bundle.ACTIVE;
	}

	/**
	 * @return the {@link Bundle} that contains the {@link IConfigurationElement} associated
	 * with this descriptor
	 */
	private Bundle getBundle() {
		String namespace= fElement.getDeclaringExtension().getContributor().getName();
		Bundle bundle= Platform.getBundle(namespace);
		return bundle;
	}
	
	/**
	 * <p>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.</p>
	 *
	 * @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 ICompletionProposalComputer getComputer(boolean canCreate) throws CoreException, InvalidRegistryObjectException {
		if (fComputer == null && canCreate && !fTriedLoadingComputer && (fActivate || isPluginLoaded())) {
			fTriedLoadingComputer= true;
			fComputer= createComputer();
		}
		return fComputer;
	}
	
	/**
	 * @return the enablement state of the category this describer is associated with
	 */
	private boolean isEnabled() {
		return fCategory.isEnabled();
	}
	
	/**
	 * <p>Starts the meter for measuring the computers performance</p>
	 * 
	 * @param context
	 * @param computer
	 * @return
	 */
	private PerformanceStats startMeter(Object context, ICompletionProposalComputer 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;
	}

	/**
	 * <p>Stops the meter for measuring the computers performance</p>
	 * 
	 * @param context
	 * @param computer
	 * @return
	 */
	private void stopMeter(final PerformanceStats stats, String operation) {
		if (MEASURE_PERFORMANCE) {
			stats.endRun();
			if (stats.isFailure()) {
				IStatus status= createPerformanceStatus(operation);
				Logger.log(status);
				return;
			}
		}

		if (fIsReportingDelay) {
			long current= System.currentTimeMillis();
			if (current - fStart > MAX_DELAY) {
				IStatus status= createPerformanceStatus(operation);
				Logger.log(status);
			}
		}
	}
	
	/**
	 * @return A message explaining that the described {@link ICompletionProposalComputer} failed in some way
	 */
	private String createBlameMessage() {
		return "The ''" + getName() + "'' proposal computer from the ''" + //$NON-NLS-1$ //$NON-NLS-2$
			fElement.getDeclaringExtension().getContributor().getName() + "'' plug-in did not complete normally."; //$NON-NLS-1$
	}
	
	/**
	 * <p>Create a status message describing that the extension has become invalid</p>
	 * 
	 * @param x the associated {@link InvalidRegistryObjectException}
	 * @return the created {@link IStatus}
	 */
	private IStatus createExceptionStatus(InvalidRegistryObjectException x) {
		String blame= createBlameMessage();
		String reason= "The extension has become invalid."; //$NON-NLS-1$
		return new Status(IStatus.INFO, SSEUIPlugin.ID, IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
	}

	/**
	 * <p>create a status message explaining that the extension could not be instantiated</p>
	 * 
	 * @param x the associated {@link CoreException}
	 * @return the created {@link IStatus}
	 */
	private IStatus createExceptionStatus(CoreException x) {
		String blame = createBlameMessage();
		String reason = "Unable to instantiate the extension."; //$NON-NLS-1$
		return new Status(IStatus.ERROR, SSEUIPlugin.ID, IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
	}

	/**
	 * <p>Create a status message explaining the extension has thrown a runtime exception</p>
	 * 
	 * @param x the associated {@link RuntimeException}
	 * @return the created {@link IStatus}
	 */
	private IStatus createExceptionStatus(RuntimeException x) {
		String blame= createBlameMessage();
		String reason= "The extension has thrown a runtime exception."; //$NON-NLS-1$
		return new Status(IStatus.WARNING, SSEUIPlugin.ID, IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
	}

	/**
	 * <p>Create a status message explaining the extension has violated the API of the extension point</p>
	 * 
	 * @param operation the operation that created the API violation
	 * @return the created {@link IStatus}
	 */
	private IStatus createAPIViolationStatus(String operation) {
		String blame = createBlameMessage();
		String reason = "The extension violated the API contract of the ''" + operation + "'' operation."; //$NON-NLS-1$ //$NON-NLS-2$
		return new Status(IStatus.WARNING, SSEUIPlugin.ID, IStatus.OK, blame + " " + reason, null); //$NON-NLS-1$
	}

	/**
	 * <p>Create a status message explaining that the extension took to long during an operation</p>
	 * 
	 * @param operation the operation that took to long
	 * @return the created {@link IStatus}
	 */
	private IStatus createPerformanceStatus(String operation) {
		String blame= createBlameMessage();
		String reason = "The extension took too long to return from the ''" + operation + "'' operation."; //$NON-NLS-1$ //$NON-NLS-2$
		return new Status(IStatus.WARNING, SSEUIPlugin.ID, IStatus.OK, blame + " " + reason, null); //$NON-NLS-1$
	}
}
