/*******************************************************************************
 * Copyright (c) 2010, 2018 BestSolution.at 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:
 *     Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 430075, 430080, 431464, 433336, 472654
 *     René Brandstetter - Bug 419749
 *     Brian de Alwis (MTI) - Bug 433053
 *     Alexandra Buzila - Refactoring, Bug 475934
 ******************************************************************************/

package org.eclipse.e4.ui.internal.workbench;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.inject.Inject;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.Execute;
import org.eclipse.e4.core.services.contributions.IContributionFactory;
import org.eclipse.e4.core.services.log.Logger;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.MApplicationElement;
import org.eclipse.e4.ui.model.fragment.MModelFragment;
import org.eclipse.e4.ui.model.fragment.MModelFragments;
import org.eclipse.e4.ui.model.fragment.MStringModelFragment;
import org.eclipse.e4.ui.model.fragment.impl.FragmentPackageImpl;
import org.eclipse.e4.ui.model.internal.ModelUtils;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.Diagnostician;
import org.eclipse.emf.ecore.util.EContentsEList;
import org.eclipse.emf.ecore.util.EcoreUtil;

/**
 * The ModelAssembler is responsible for adding {@link MModelFragment fragments}
 * and {@link MApplicationElement} imports to the application model and running
 * pre- and post-processors on the model.
 */
public class ModelAssembler {

	private class Bucket {
		SortedSet<ModelFragmentWrapper> wrapper = new TreeSet<>(new ModelFragmentComparator(application));
		Bucket dependentOn;
		Set<Bucket> dependencies = new LinkedHashSet<>();
		Set<String> containedElementIds = new LinkedHashSet<>();
	}

	@Inject
	private Logger logger;

	@Inject
	private MApplication application;

	@Inject
	private IEclipseContext context;

	@Inject
	private IExtensionRegistry registry;

	private static final String EXTENSION_POINT_ID = "org.eclipse.e4.workbench.model"; //$NON-NLS-1$

	private static final String INITIAL = "initial"; //$NON-NLS-1$
	private static final String NOTEXISTS = "notexists"; //$NON-NLS-1$

	/**
	 * Processes the application model. This will run pre-processors, process the
	 * fragments, resolve imports and run post-processors, in this order. <br>
	 * The <strong>org.eclipse.e4.workbench.model</strong> extension point will be
	 * used to retrieve the contributed fragments (with imports) and processors.<br>
	 * Extension points will be sorted based on the dependencies of their
	 * contributors.
	 *
	 * @param initial <code>true</code> if running from a non-persisted state
	 */
	public void processModel(boolean initial) {
		IExtensionPoint extPoint = registry.getExtensionPoint(EXTENSION_POINT_ID);
		IExtension[] extensions = new ExtensionsSort().sort(extPoint.getExtensions());

		// run processors which are marked to run before fragments
		runProcessors(extensions, initial, false);
		// process fragments (and resolve imports)
		processFragments(extensions, initial);
		// run processors which are marked to run after fragments
		runProcessors(extensions, initial, true);
	}

	/**
	 * Adds the {@link MApplicationElement model elements} contributed by the
	 * {@link IExtension extensions} to the {@link MApplication application model}.
	 *
	 * @param extensions the list of {@link IExtension} extension elements
	 * @param initial    <code>true</code> if running from a non-persisted state
	 *
	 */
	private void processFragments(IExtension[] extensions, boolean initial) {
		List<ModelFragmentWrapper> wrappers = new ArrayList<>();
		for (IExtension extension : extensions) {
			IConfigurationElement[] ces = extension.getConfigurationElements();
			for (IConfigurationElement ce : ces) {
				if ("fragment".equals(ce.getName()) && (initial || !INITIAL.equals(ce.getAttribute("apply")))) { //$NON-NLS-1$ //$NON-NLS-2$
					MModelFragments fragmentsContainer = getFragmentsContainer(ce);
					if (fragmentsContainer == null) {
						continue;
					}
					for (MModelFragment fragment : fragmentsContainer.getFragments()) {
						boolean checkExist = !initial && NOTEXISTS.equals(ce.getAttribute("apply")); //$NON-NLS-1$
						wrappers.add(new ModelFragmentWrapper(fragmentsContainer, fragment,
								ce.getContributor().getName(), URIHelper.constructPlatformURI(ce.getContributor()),
								checkExist)); // $NON-NLS-1$
					}
				}
			}
		}

		processFragmentWrappers(wrappers);
	}

	/**
	 * Processes the given list of fragments wrapped in {@link ModelFragmentWrapper}
	 * elements.
	 *
	 * @param wrappers the list of fragments
	 */
	public void processFragmentWrappers(Collection<ModelFragmentWrapper> wrappers) {
		Map<String, Bucket> elementIdToBucket = new LinkedHashMap<>();
		Map<String, Bucket> parentIdToBuckets = new LinkedHashMap<>();
		for (ModelFragmentWrapper fragmentWrapper : wrappers) {
			MModelFragment fragment = fragmentWrapper.getModelFragment();
			String parentId = MStringModelFragment.class.cast(fragment).getParentElementId();
			if (!parentIdToBuckets.containsKey(parentId)) {
				parentIdToBuckets.put(parentId, new Bucket());
			}
			Bucket b = parentIdToBuckets.get(parentId);
			if (elementIdToBucket.containsKey(parentId)) {
				Bucket parentBucket = elementIdToBucket.get(parentId);
				parentBucket.dependencies.add(b);
				b.dependentOn = parentBucket;
			}
			b.wrapper.add(fragmentWrapper); // $NON-NLS-1$

			for (MApplicationElement e : fragment.getElements()) {
				// Error case -> clean up and ignore
				if (parentId == e.getElementId()) {
					continue;
				}
				elementIdToBucket.put(e.getElementId(), b);
				b.containedElementIds.add(e.getElementId());
				if (parentIdToBuckets.containsKey(e.getElementId())) {
					Bucket childBucket = parentIdToBuckets.get(e.getElementId());
					b.dependencies.add(childBucket);
					childBucket.dependentOn = b;
				}
			}
		}
		processFragments(createUnifiedFragmentList(elementIdToBucket));
	}

	private List<ModelFragmentWrapper> createUnifiedFragmentList(Map<String, Bucket> elementIdToBucket) {
		List<ModelFragmentWrapper> fragmentList = new ArrayList<>();
		Set<String> checkedElementIds = new LinkedHashSet<>();
		for (Entry<String, Bucket> entry : elementIdToBucket.entrySet()) {
			if (checkedElementIds.contains(entry.getKey())) {
				continue;
			}
			Bucket bucket = entry.getValue();
			while (bucket.dependentOn != null) {
				bucket = bucket.dependentOn;
			}
			addAllBucketFragmentWrapper(bucket, fragmentList, checkedElementIds);
		}
		return fragmentList;
	}

	private void addAllBucketFragmentWrapper(Bucket bucket, List<ModelFragmentWrapper> fragmentList,
			Set<String> checkedElementIds) {
		for (ModelFragmentWrapper wrapper : bucket.wrapper) {
			fragmentList.add(wrapper);
		}
		checkedElementIds.addAll(bucket.containedElementIds);
		for (Bucket child : bucket.dependencies) {
			addAllBucketFragmentWrapper(child, fragmentList, checkedElementIds);
		}
	}

	public void processFragments(Collection<ModelFragmentWrapper> fragmentList) {
		for (ModelFragmentWrapper fragmentWrapper : fragmentList) {
			processFragment(fragmentWrapper.getFragmentContainer(), fragmentWrapper.getModelFragment(),
					fragmentWrapper.getContributorName(), fragmentWrapper.getContributorURI(),
					fragmentWrapper.isCheckExists());
		}
	}

	/**
	 * Adds the {@link MApplicationElement model elements} contributed by the
	 * {@link IConfigurationElement} to the application model and resolves any
	 * fragment imports along the way.
	 *
	 * @param fragmentsContainer the {@link MModelFragments}
	 * @param fragment           the {@link MModelFragment}
	 * @param contributorName    the name of the element contributing the fragment
	 * @param contributorURI     the URI of the element contribution the fragment
	 * @param checkExist         specifies whether we should check that the
	 *                           application model doesn't already contain the
	 *                           elements contributed by the fragment before merging
	 *                           them
	 */
	public void processFragment(MModelFragments fragmentsContainer, MModelFragment fragment, String contributorName,
			String contributorURI, boolean checkExist) {
		/**
		 * The application elements that were added by the given
		 * IConfigurationElement to the application model
		 */
		List<MApplicationElement> addedElements = new ArrayList<>();

		if (fragmentsContainer == null) {
			return;
		}
		boolean evalImports = false;
		Diagnostic validationResult = Diagnostician.INSTANCE.validate((EObject) fragment);
		int severity = validationResult.getSeverity();
		if (severity == Diagnostic.ERROR) {
			logger.error(
					"Fragment from \"" + "uri.toString()" + "\" of \"" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
							+ contributorName + "\" could not be validated and was not merged \"{0}\"", //$NON-NLS-1$
					fragment.toString());
		}

		List<MApplicationElement> merged = processModelFragment(fragment, contributorURI, checkExist);
		if (!merged.isEmpty()) {
			evalImports = true;
			addedElements.addAll(merged);
		} else {
			logger.debug("Nothing to merge for fragment \"{0}\" of \"{1}\"", contributorURI, //$NON-NLS-1$
					contributorName);
		}
		if (evalImports && fragmentsContainer.getImports().size() > 0) {
			resolveImports(fragmentsContainer.getImports(), addedElements);
		}
	}

	private MModelFragments getFragmentsContainer(IConfigurationElement ce) {
		E4XMIResource applicationResource = (E4XMIResource) ((EObject) application).eResource();
		ResourceSet resourceSet = applicationResource.getResourceSet();
		IContributor contributor = ce.getContributor();
		String attrURI = ce.getAttribute("uri"); //$NON-NLS-1$
		String bundleName = contributor.getName();
		if (attrURI == null) {
			logger.warn("Unable to find location for the model extension \"{0}\"", bundleName); //$NON-NLS-1$
			return null;
		}

		URI uri;
		try {
			// check if the attrURI is already a platform URI
			if (URIHelper.isPlatformURI(attrURI)) {
				uri = URI.createURI(attrURI);
			} else {
				String path = bundleName + '/' + attrURI;
				uri = URI.createPlatformPluginURI(path, false);
			}
		} catch (RuntimeException e) {
			logger.warn(e, "Invalid location \"" + attrURI + "\" of model extension \"" + bundleName + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			return null;
		}

		Resource resource;
		try {
			resource = resourceSet.getResource(uri, true);
		} catch (RuntimeException e) {
			logger.warn(e, "Unable to read model extension from \"" + uri.toString() + "\" of \"" + bundleName + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			return null;
		}

		EList<?> contents = resource.getContents();
		if (contents.isEmpty()) {
			return null;
		}

		Object extensionRoot = contents.get(0);

		if (!(extensionRoot instanceof MModelFragments)) {
			logger.warn("Unable to create model extension \"{0}\"", bundleName); //$NON-NLS-1$
			return null;
		}
		return (MModelFragments) extensionRoot;
	}

	/**
	 * Contributes the given {@link MModelFragment} to the application model.
	 *
	 * @param fragment       the fragment to add to the application model
	 * @param contributorURI the URI of the element that contributes this fragment
	 * @param checkExist     specifies whether we should check that the application
	 *                       model doesn't already contain the elements contributed
	 *                       by the fragment before merging them
	 * @return a list of the {@link MApplicationElement} elements that were merged
	 *         into the application model by the fragment
	 */
	public List<MApplicationElement> processModelFragment(MModelFragment fragment, String contributorURI,
			boolean checkExist) {

		E4XMIResource applicationResource = (E4XMIResource) ((EObject) application).eResource();

		List<MApplicationElement> elements = fragment.getElements();
		if (elements.isEmpty()) {
			return new ArrayList<>();
		}

		for (MApplicationElement el : elements) {
			EObject o = (EObject) el;

			E4XMIResource r = (E4XMIResource) o.eResource();

			if (checkExist && applicationResource.getIDToEObjectMap().containsKey(r.getID(o))) {
				continue;
			}

			applicationResource.setID(o, r.getID(o));

			if (contributorURI != null) {
				el.setContributorURI(contributorURI);
			}

			// Remember IDs of subitems
			TreeIterator<EObject> treeIt = EcoreUtil.getAllContents(o, true);
			while (treeIt.hasNext()) {
				EObject eObj = treeIt.next();
				r = (E4XMIResource) eObj.eResource();
				if (contributorURI != null && (eObj instanceof MApplicationElement)) {
					((MApplicationElement) eObj).setContributorURI(contributorURI);
				}
				applicationResource.setID(eObj, r.getInternalId(eObj));
			}
		}

		return fragment.merge(application);
	}

	/**
	 * Executes the processors as declared in provided {@link IExtension extensions}
	 * array.
	 *
	 * @param extensions     the array of {@link IExtension} extensions containing
	 *                       the processors
	 * @param initial        <code>true</code> if the application is running from a
	 *                       non-persisted state
	 * @param afterFragments <code>true</code> if the processors that should be run
	 *                       before model fragments are merged are to be executed,
	 *                       <code>false</code> otherwise
	 */
	public void runProcessors(IExtension[] extensions, boolean initial, boolean afterFragments) {
		for (IExtension extension : extensions) {
			IConfigurationElement[] ces = extension.getConfigurationElements();
			for (IConfigurationElement ce : ces) {
				boolean parseBoolean = Boolean.parseBoolean(ce.getAttribute("beforefragment")); //$NON-NLS-1$
				if ("processor".equals(ce.getName()) && afterFragments != parseBoolean) { //$NON-NLS-1$
					if (initial || !INITIAL.equals(ce.getAttribute("apply"))) { //$NON-NLS-1$
						runProcessor(ce);
					}
				}
			}
		}
	}

	private void runProcessor(IConfigurationElement ce) {
		IEclipseContext localContext = EclipseContextFactory.create();
		IContributionFactory factory = context.get(IContributionFactory.class);

		for (IConfigurationElement ceEl : ce.getChildren("element")) { //$NON-NLS-1$
			String id = ceEl.getAttribute("id"); //$NON-NLS-1$

			if (id == null) {
				logger.warn("No element id given"); //$NON-NLS-1$
				continue;
			}

			String key = ceEl.getAttribute("contextKey"); //$NON-NLS-1$
			if (key == null) {
				key = id;
			}

			MApplicationElement el = ModelUtils.findElementById(application, id);
			if (el == null) {
				logger.warn("Could not find element with id '" + id + "'"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			localContext.set(key, el);
		}

		try {
			Object o = factory.create("bundleclass://" + ce.getContributor().getName() + "/" + ce.getAttribute("class"), //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
					context, localContext);
			if (o == null) {
				logger.warn("Unable to create processor " + ce.getAttribute("class") + " from " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						+ ce.getContributor().getName());
			} else {
				ContextInjectionFactory.invoke(o, Execute.class, context, localContext);
			}
		} catch (Exception e) {
			logger.warn(e, "Could not run processor"); //$NON-NLS-1$
		}
	}

	/**
	 * Resolves the given list of imports used by the specified
	 * <code>addedElements</code> in the application model.
	 *
	 * @param imports       the list of elements that were imported by fragments and
	 *                      should be resolved in the application model
	 * @param addedElements the list of elements contributed by the fragments to the
	 *                      application model
	 */
	public void resolveImports(List<MApplicationElement> imports, List<MApplicationElement> addedElements) {
		if (imports.isEmpty()) {
			return;
		}
		// now that we have all components loaded, resolve imports
		Map<MApplicationElement, MApplicationElement> importMaps = new HashMap<>();
		for (MApplicationElement importedElement : imports) {
			MApplicationElement realElement = ModelUtils.findElementById(application, importedElement.getElementId());
			if (realElement == null) {
				logger.warn("Could not resolve an import element for '" + importedElement.getElementId() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			importMaps.put(importedElement, realElement);
		}

		TreeIterator<EObject> it = EcoreUtil.getAllContents(addedElements);
		List<Runnable> commands = new ArrayList<>();

		while (it.hasNext()) {
			EObject o = it.next();

			EContentsEList.FeatureIterator<EObject> featureIterator = (EContentsEList.FeatureIterator<EObject>) o
					.eCrossReferences().iterator();
			while (featureIterator.hasNext()) {
				EObject importObject = featureIterator.next();
				if (importObject.eContainmentFeature() == FragmentPackageImpl.Literals.MODEL_FRAGMENTS__IMPORTS) {
					EStructuralFeature feature = featureIterator.feature();

					MApplicationElement el = null;
					if (importObject instanceof MApplicationElement) {
						el = importMaps.get((MApplicationElement) importObject);
					}

					if (el == null) {
						logger.warn("Could not resolve import for " + el); //$NON-NLS-1$
					}

					final EObject interalTarget = o;
					final EStructuralFeature internalFeature = feature;
					final MApplicationElement internalElment = el;
					final EObject internalImportObject = importObject;

					commands.add(() -> {
						if (internalFeature.isMany()) {
							logger.error("Replacing"); //$NON-NLS-1$
							@SuppressWarnings("unchecked")
							List<Object> l = (List<Object>) interalTarget.eGet(internalFeature);
							int index = l.indexOf(internalImportObject);
							if (index >= 0) {
								l.set(index, internalElment);
							}
						} else {
							interalTarget.eSet(internalFeature, internalElment);
						}
					});
				}
			}
		}

		for (Runnable cmd : commands) {
			cmd.run();
		}
	}
}
