/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.ui.editors.text.templates;

import java.util.ArrayList;
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.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.text.templates.ContextTypeRegistry;
import org.eclipse.jface.text.templates.TemplateContextType;
import org.eclipse.jface.text.templates.TemplateVariableResolver;

import org.eclipse.ui.internal.editors.text.EditorsPlugin;

import org.eclipse.ui.editors.text.EditorsUI;


/**
 * A registry for context types. Editor implementors will usually instantiate a
 * registry and configure the context types available in their editor.
 * <code>ContextType</code>s can be added either directly using
 * {@link #addContextType(TemplateContextType)} or by instantiating and adding a
 * contributed context type using {@link #addContextType(String)}.
 *
 * @since 3.0
 */
public class ContributionContextTypeRegistry extends ContextTypeRegistry {

	/* extension point string literals */
	private static final String TEMPLATES_EXTENSION_POINT= "org.eclipse.ui.editors.templates"; //$NON-NLS-1$

	private static final String CONTEXT_TYPE= "contextType"; //$NON-NLS-1$
	private static final String ID= "id"; //$NON-NLS-1$
	private static final String NAME= "name"; //$NON-NLS-1$
	private static final String CLASS= "class"; //$NON-NLS-1$

	private static final String RESOLVER= "resolver"; //$NON-NLS-1$
	private static final String CONTEXT_TYPE_ID= "contextTypeId"; //$NON-NLS-1$
	private static final String DESCRIPTION= "description"; //$NON-NLS-1$
	private static final String TYPE= "type"; //$NON-NLS-1$
	private static final String REGISTRY= "contextTypeRegistry"; //$NON-NLS-1$
	private static final String REGISTRY_ID= "registryId"; //$NON-NLS-1$


	/**
	 * Creates a new context type registry and registers all context types contributed for the given
	 * registry ID.
	 * 
	 * @param registryId the registry ID
	 * @since 3.5
	 */
	public ContributionContextTypeRegistry(String registryId) {
		readRegistry(registryId);
	}

	/**
	 * Creates a new context type registry.
	 * <p>
	 * Clients need to enable the desired context types by calling {@link #addContextType(String)}.
	 * </p>
	 */
	public ContributionContextTypeRegistry() {
	}

	/**
	 * Registers all context types contributed for the given registry ID.
	 * 
	 * @param registryId the registry ID
	 * @since 3.5
	 */
	private void readRegistry(String registryId) {
		Assert.isNotNull(registryId);

		IConfigurationElement[] extensions= getTemplateExtensions();

		for (int i= 0; i < extensions.length; i++) {
			if (extensions[i].getName().equals(REGISTRY)) {
				String id= extensions[i].getAttribute(ID);
				if (registryId.equals(id)) {
					for (int j= 0; j < extensions.length; j++) {
						if (extensions[j].getName().equals(CONTEXT_TYPE)) {
							if (registryId.equals(extensions[j].getAttribute(REGISTRY_ID)))
								addContextType(extensions[j].getAttribute(ID));
						}
					}
					return;
				}

			}
		}

		Assert.isTrue(false, "invalid registry id"); //$NON-NLS-1$
	}

	/**
	 * Tries to create a context type given an id. If there is already a context
	 * type registered under the given id, nothing happens. Otherwise,
	 * contributions to the <code>org.eclipse.ui.editors.templates</code>
	 * extension point are searched for the given identifier and the specified
	 * context type instantiated if it is found.
	 *
	 * @param id the id for the context type as specified in XML
	 */
	public void addContextType(String id) {
		Assert.isNotNull(id);
		if (getContextType(id) != null)
			return;

		TemplateContextType type= createContextType(id);
		if (type != null)
			addContextType(type);

	}

	/**
	 * Tries to create a context type given an id. Contributions to the
	 * <code>org.eclipse.ui.editors.templates</code> extension point are
	 * searched for the given identifier and the specified context type
	 * instantiated if it is found. Any contributed
	 * {@link org.eclipse.jface.text.templates.TemplateVariableResolver}s
	 * are also instantiated and added to the context type.
	 *
	 * @param id the id for the context type as specified in XML
	 * @return the instantiated and configured context type, or
	 *         <code>null</code> if it is not found or cannot be instantiated
	 */
	public static TemplateContextType createContextType(String id) {
		Assert.isNotNull(id);

		IConfigurationElement[] extensions= getTemplateExtensions();
		TemplateContextType type;
		try {
			type= createContextType(extensions, id);
			if (type != null) {
				TemplateVariableResolver[] resolvers= createResolvers(extensions, id);
				for (int i= 0; i < resolvers.length; i++)
					type.addResolver(resolvers[i]);
			}
		} catch (CoreException e) {
			EditorsPlugin.log(e);
			type= null;
		}

		return type;
	}

	private static TemplateContextType createContextType(IConfigurationElement[] extensions, String contextTypeId) throws CoreException {
		for (int i= 0; i < extensions.length; i++) {
			// TODO create half-order over contributions
			if (extensions[i].getName().equals(CONTEXT_TYPE)) {
				String id= extensions[i].getAttribute(ID);
				if (contextTypeId.equals(id))
					return createContextType(extensions[i]);
			}
		}

		return null;
	}

	/**
	 * Instantiates the resolvers contributed to the context type with id
	 * <code>contextTypeId</code>. If instantiation of one resolver fails,
	 * the exception are logged and operation continues.
	 *
	 * @param extensions the configuration elements to parse
	 * @param contextTypeId the id of the context type for which resolvers are
	 *        instantiated
	 * @return the instantiated resolvers
	 */
	private static TemplateVariableResolver[] createResolvers(IConfigurationElement[] extensions, String contextTypeId) {
		List<TemplateVariableResolver> resolvers= new ArrayList<>();
		for (int i= 0; i < extensions.length; i++) {
			if (extensions[i].getName().equals(RESOLVER)) {
				String declaredId= extensions[i].getAttribute(CONTEXT_TYPE_ID);
				if (contextTypeId.equals(declaredId)) {
					try {
						TemplateVariableResolver resolver= createResolver(extensions[i]);
						if (resolver != null)
							resolvers.add(resolver);
					} catch (CoreException e) {
						EditorsPlugin.log(e);
					}
				}
			}
		}

		return resolvers.toArray(new TemplateVariableResolver[resolvers.size()]);

	}

	private static IConfigurationElement[] getTemplateExtensions() {
		return Platform.getExtensionRegistry().getConfigurationElementsFor(TEMPLATES_EXTENSION_POINT);
	}

	private static TemplateContextType createContextType(IConfigurationElement element) throws CoreException {
		String id= element.getAttribute(ID);
		try {
			TemplateContextType contextType= (TemplateContextType) element.createExecutableExtension(CLASS);
			String name= element.getAttribute(NAME);
			if (name == null)
				name= id;

			if (contextType.getId() == null)
				contextType.setId(id);
			if (contextType.getName() == null)
				contextType.setName(name);

			return contextType;
		} catch (ClassCastException e) {
			throw new CoreException(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, IStatus.OK, "extension does not implement " + TemplateContextType.class.getName(), e)); //$NON-NLS-1$
		}
	}

	private static TemplateVariableResolver createResolver(IConfigurationElement element) throws CoreException {
		try {
			String type= element.getAttribute(TYPE);
			if (type != null) {

				TemplateVariableResolver resolver= (TemplateVariableResolver) element.createExecutableExtension(CLASS);
				if (resolver.getType() != null) {
					throw new CoreException(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, IStatus.OK, resolver.getClass() + "() must call super() and not set the variable's type", null)); //$NON-NLS-1$
				}
				resolver.setType(type);

				if (resolver.getDescription() != null) {
					throw new CoreException(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, IStatus.OK, resolver.getClass() + "() must call super() and not set the variable's description", null)); //$NON-NLS-1$
				}
				String desc= element.getAttribute(DESCRIPTION);
				resolver.setDescription(desc == null ? "" : desc); //$NON-NLS-1$

				return resolver;
			}
		} catch (ClassCastException e) {
			throw new CoreException(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, IStatus.OK, "extension does not implement " + TemplateVariableResolver.class.getName(), e)); //$NON-NLS-1$
		}

		return null;
	}
}

