/*******************************************************************************
 * Copyright (c) 2009, 2015 QNX Software Systems 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:
 *     QNX Software Systems - initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.internal.ui.workingsets;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.internal.ui.workingsets.WorkspaceSnapshot.ProjectState;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectNatureDescriptor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExecutableExtension;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;

/**
 * Protocol for a factory of {@link IWorkingSetProjectConfiguration}s. Factories are {@linkplain Registry
 * registered} against project natures.
 *
 * @author Christian W. Damus (cdamus)
 *
 * @since 6.0
 */
public interface IWorkingSetProjectConfigurationFactory {
	/**
	 * Queries my factory ID. The ID is persisted in the working set configuration data so that the same
	 * factory can be used to reconstruct project configurations when loading the working set configurations.
	 *
	 * @return my unique identifier
	 */
	String getID();

	/**
	 * Creates a new project configuration element.
	 *
	 * @param parent
	 *            the working set configuration that owns the new project configuration
	 * @param project
	 *            the workspace project for which to create the configuration
	 *
	 * @return the new project configuration
	 */
	IWorkingSetProjectConfiguration createProjectConfiguration(IWorkingSetConfiguration parent, IProject project);

	/**
	 * Creates a UI controller to support editing the specified project configuration snapshot, which should
	 * have been obtained from a configuration that I previously
	 * {@linkplain #createProjectConfiguration(org.eclipse.cdt.internal.ui.workingsets.IWorkingSetConfiguration, IProject)
	 * created}, myself.
	 *
	 * @param config
	 *            a project configuration snapshot that I created
	 *
	 * @return a suitable controller for it. Must not be <code>null</code>
	 */
	IWorkingSetProjectConfigurationController createProjectConfigurationController(
			IWorkingSetProjectConfiguration.ISnapshot config);

	/**
	 * Creates a snapshot of the configuration state of a project in the workspace. This may capture
	 * additional build meta-data beyond just the "active configuration."
	 *
	 * @param project
	 *            a project to capture in a {@link WorkspaceSnapshot}
	 * @param desc
	 *            the project description, from which to capture the initial configuration data
	 *
	 * @return the project state capture. Must not be <code>null</code>
	 */
	WorkspaceSnapshot.ProjectState createProjectState(IProject project, ICProjectDescription desc);

	//
	// Nested types
	//

	/**
	 * A registry of {@linkplain IWorkingSetProjectConfigurationFactory project configuration factories}
	 * contributed on the <tt>org.eclipse.cdt.ui.workingSetConfigurations</tt> extension point.
	 *
	 * @author Christian W. Damus (cdamus)
	 *
	 * @since 6.0
	 */
	class Registry {
		private static final String EXT_PT_ID = "workingSetConfigurations"; //$NON-NLS-1$
		private static final String E_FACTORY = "projectConfigurationFactory"; //$NON-NLS-1$
		private static final String E_NATURE = "projectNature"; //$NON-NLS-1$
		private static final String A_ID = "id"; //$NON-NLS-1$
		private static final String A_CLASS = "class"; //$NON-NLS-1$

		/**
		 * The shared project configuration factory registry.
		 */
		public static Registry INSTANCE = new Registry();

		private final IWorkingSetProjectConfigurationFactory defaultFactory = new Default();
		private final Map<String, IWorkingSetProjectConfigurationFactory> factoriesByID = new java.util.HashMap<>();
		private final Map<String, IWorkingSetProjectConfigurationFactory> factoriesByNature = new java.util.HashMap<>();

		private Map<String, Set<String>> projectNaturePartOrdering;

		private Registry() {
			super();

			projectNaturePartOrdering = computeProjectNaturePartOrdering();

			loadExtensions();
		}

		public IWorkingSetProjectConfigurationFactory getFactory(String id) {
			IWorkingSetProjectConfigurationFactory result = get(factoriesByID, id);

			if (result == null) {
				result = defaultFactory;
			}

			return result;
		}

		public IWorkingSetProjectConfigurationFactory getFactory(IProject project) {
			IWorkingSetProjectConfigurationFactory result = null;

			for (String nature : getPartOrderedNatureIDs(project)) {
				result = get(factoriesByNature, nature);

				if (result != null) {
					break;
				}
			}

			return result;
		}

		private IWorkingSetProjectConfigurationFactory get(Map<?, IWorkingSetProjectConfigurationFactory> map,
				Object key) {
			return map.get(key);
		}

		private String[] getPartOrderedNatureIDs(IProject project) {
			String[] result;

			try {
				result = project.getDescription().getNatureIds();
			} catch (CoreException e) {
				CUIPlugin.log(e.getStatus());
				result = new String[0];
			}

			for (String natureId : result) {
				if (projectNaturePartOrdering.get(natureId) == null) {
					// re-do nature cache. Some natures were added recently
					projectNaturePartOrdering = computeProjectNaturePartOrdering();
					break;
				}
			}

			if (result.length > 0) {
				Arrays.sort(result, new Comparator<String>() {
					@Override
					public int compare(String nature1, String nature2) {
						Set<String> required1 = projectNaturePartOrdering.get(nature1);
						Set<String> required2 = projectNaturePartOrdering.get(nature2);
						if (required1 != null && required1.contains(nature2)) {
							return -1; // required1 precedes required2
						} else if (required2 != null && required2.contains(nature1)) {
							return +1; // required2 precedes required1
						} else if (nature1.startsWith("org.eclipse.cdt.") //$NON-NLS-1$
								&& !nature2.startsWith("org.eclipse.cdt.")) { //$NON-NLS-1$
							return +1; // lower priority to CDT natures
						} else if (nature2.startsWith("org.eclipse.cdt.") //$NON-NLS-1$
								&& !nature1.startsWith("org.eclipse.cdt.")) { //$NON-NLS-1$
							return -1; // lower priority to CDT natures
						}

						return 0; // not partially comparable
					}
				});
			}

			return result;
		}

		private Map<String, Set<String>> computeProjectNaturePartOrdering() {
			Map<String, Set<String>> result = new java.util.HashMap<>();

			// first pass to populate the map with immediate requireds
			IWorkspace ws = ResourcesPlugin.getWorkspace();
			for (IProjectNatureDescriptor next : ws.getNatureDescriptors()) {
				result.put(next.getNatureId(), new java.util.HashSet<>(Arrays.asList(next.getRequiredNatureIds())));
			}

			// now, iterate to add transitive requireds
			boolean loopAgain;
			do {
				loopAgain = false;

				for (Map.Entry<String, Set<String>> next : result.entrySet()) {
					Set<String> requireds = next.getValue();
					Set<String> newRequireds = new java.util.HashSet<>(requireds);

					boolean changed = false;

					for (String required : requireds) {
						changed |= newRequireds.addAll(result.get(required));
					}

					if (changed) {
						loopAgain = true;
						next.setValue(newRequireds);
					}
				}
			} while (loopAgain);

			return result;
		}

		private void loadExtensions() {
			IExtensionRegistry registry = Platform.getExtensionRegistry();
			for (IExtension ext : registry.getExtensionPoint(CUIPlugin.PLUGIN_ID, EXT_PT_ID).getExtensions()) {
				for (IConfigurationElement element : ext.getConfigurationElements()) {
					if (E_FACTORY.equals(element.getName())) {
						try {
							Descriptor desc = new Descriptor(element);

							synchronized (factoriesByID) {
								factoriesByID.put(desc.getID(), desc);
							}

							synchronized (factoriesByNature) {
								for (IConfigurationElement nature : element.getChildren(E_NATURE)) {
									String natureID = nature.getAttribute(A_ID);

									if (natureID != null) {
										factoriesByNature.put(natureID, desc);
									} else {
										CUIPlugin.log(NLS.bind(WorkingSetMessages.WSProjConfigFactory_noNatureID,
												ext.getContributor().getName()), null);
									}
								}
							}
						} catch (CoreException e) {
							CUIPlugin.log(e.getStatus());
						}
					}
				}
			}
		}

		//
		// Nested classes
		//

		/**
		 * A self-resolving descriptor for lazy instantiation of a factory.
		 */
		private class Descriptor implements IWorkingSetProjectConfigurationFactory {
			private final IConfigurationElement extension;
			private final String id;

			Descriptor(IConfigurationElement extension) throws CoreException {
				this.extension = extension;
				id = extension.getAttribute(A_ID);

				if (id == null) {
					throw new CoreException(new Status(IStatus.ERROR, CUIPlugin.PLUGIN_ID, NLS.bind(
							WorkingSetMessages.WSProjConfigFactory_noFactoryID, extension.getContributor().getName())));
				}
			}

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

			@Override
			public IWorkingSetProjectConfiguration createProjectConfiguration(IWorkingSetConfiguration parent,
					IProject project) {
				return resolve().createProjectConfiguration(parent, project);
			}

			@Override
			public IWorkingSetProjectConfigurationController createProjectConfigurationController(
					IWorkingSetProjectConfiguration.ISnapshot config) {

				return resolve().createProjectConfigurationController(config);
			}

			@Override
			public ProjectState createProjectState(IProject project, ICProjectDescription desc) {
				return resolve().createProjectState(project, desc);
			}

			private IWorkingSetProjectConfigurationFactory resolve() {
				IWorkingSetProjectConfigurationFactory result = null;

				try {
					result = (IWorkingSetProjectConfigurationFactory) extension.createExecutableExtension(A_CLASS);
				} catch (ClassCastException e) {
					CUIPlugin.log(NLS.bind(WorkingSetMessages.WSProjConfigFactory_badFactory,
							extension.getContributor().getName()), e);
				} catch (CoreException e) {
					CUIPlugin.log(new MultiStatus(CUIPlugin.PLUGIN_ID, 0, new IStatus[] { e.getStatus() },
							WorkingSetMessages.WSProjConfigFactory_factoryFailed, null));
				}

				if (result == null) {
					result = defaultFactory;
				}

				// replace the descriptor in the maps
				synchronized (factoriesByID) {
					factoriesByID.put(getID(), result);
				}
				synchronized (factoriesByNature) {
					for (Map.Entry<String, IWorkingSetProjectConfigurationFactory> next : factoriesByNature
							.entrySet()) {
						if (next.getValue().getID().equals(getID())) {
							next.setValue(result);
						}
					}
				}

				return result;
			}
		}

		/**
		 * The default project configuration factory. Clients may extend this class to implement custom
		 * factories for their project natures.
		 *
		 * @author Christian W. Damus (cdamus)
		 *
		 * @since 6.0
		 */
		public static class Default implements IWorkingSetProjectConfigurationFactory, IExecutableExtension {

			private String id;

			public Default() {
				super();
			}

			@Override
			public IWorkingSetProjectConfiguration createProjectConfiguration(IWorkingSetConfiguration parent,
					IProject project) {

				WorkingSetProjectConfiguration result = createProjectConfiguration(parent);
				result.setProjectName(project.getName());
				return result;
			}

			protected WorkingSetProjectConfiguration createProjectConfiguration(IWorkingSetConfiguration parent) {
				return new WorkingSetProjectConfiguration(parent);
			}

			@Override
			public IWorkingSetProjectConfigurationController createProjectConfigurationController(
					IWorkingSetProjectConfiguration.ISnapshot config) {

				return new ProjectConfigurationController(config);
			}

			@Override
			public ProjectState createProjectState(IProject project, ICProjectDescription desc) {
				return new WorkspaceSnapshot.ProjectState(project, desc);
			}

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

			@Override
			public void setInitializationData(IConfigurationElement config, String propertyName, Object data)
					throws CoreException {

				this.id = config.getAttribute(A_ID);
			}

		}
	}
}
