/*******************************************************************************
 * Copyright (c) 2000, 2018 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.texteditor.rulers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import com.ibm.icu.text.MessageFormat;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

import org.eclipse.ui.internal.texteditor.TextEditorPlugin;
import org.eclipse.ui.internal.texteditor.rulers.DAG;
import org.eclipse.ui.internal.texteditor.rulers.ExtensionPointHelper;
import org.eclipse.ui.internal.texteditor.rulers.RulerColumnMessages;
import org.eclipse.ui.internal.texteditor.rulers.RulerColumnPlacementConstraint;

import org.eclipse.ui.texteditor.ConfigurationElementSorter;


/**
 * A registry for all extensions to the
 * <code>rulerColumns</code> extension point.
 *
 * @since 3.3
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public final class RulerColumnRegistry {

	private static final String EXTENSION_POINT= "rulerColumns"; //$NON-NLS-1$
	private static final String QUALIFIED_EXTENSION_POINT= TextEditorPlugin.PLUGIN_ID + '.' + EXTENSION_POINT;

	/** The singleton instance. */
	private static RulerColumnRegistry fgSingleton= null;

	/**
	 * Returns the default computer registry.
	 * <p>
	 * TODO keep this or add some other singleton, e.g. TextEditorPlugin?
	 * </p>
	 *
	 * @return the singleton instance
	 */
	public static synchronized RulerColumnRegistry getDefault() {
		if (fgSingleton == null) {
			fgSingleton= new RulerColumnRegistry();
		}

		return fgSingleton;
	}

	/**
	 * All descriptors.
	 */
	private List<RulerColumnDescriptor> fDescriptors= null;
	/**
	 * All descriptors by id.
	 */
	private Map<String, RulerColumnDescriptor> fDescriptorMap= null;

	/**
	 * <code>true</code> if this registry has been loaded.
	 */
	private boolean fLoaded= false;

	/**
	 * Creates a new instance.
	 */
	RulerColumnRegistry() {
	}

	/**
	 * Returns the list of {@link RulerColumnDescriptor}s describing all extensions to the
	 * <code>rulerColumns</code> extension point. The list's iterator traverses the descriptors in
	 * the ordering implied by the placement specifications of the contributions.
	 * <p>
	 * The returned list is unmodifiable and guaranteed to never change. Note that the set of
	 * descriptors may change over time due to dynamic plug-in removal or addition.
	 * </p>
	 *
	 * @return the sorted list of extensions to the <code>rulerColumns</code> extension point
	 */
	public List<RulerColumnDescriptor> getColumnDescriptors() {
		ensureExtensionPointRead();
		return fDescriptors;
	}

	/**
	 * Returns the {@link RulerColumnDescriptor} with the given identity, <code>null</code> if no
	 * such descriptor exists.
	 *
	 * @param id the identity of the ruler contribution as given in the extension point xml.
	 * @return the {@link RulerColumnDescriptor} with the given identity, <code>null</code> if no
	 *         such descriptor exists
	 */
	public RulerColumnDescriptor getColumnDescriptor(String id) {
		Assert.isLegal(id != null);
		ensureExtensionPointRead();
		return fDescriptorMap.get(id);
	}

	/**
	 * Ensures that the extensions are read and stored in
	 * <code>fDescriptorsByPartition</code>.
	 */
	private void ensureExtensionPointRead() {
		boolean reload;
		synchronized (this) {
			reload= !fLoaded;
			fLoaded= true;
		}
		if (reload)
			reload();
	}

	/**
	 * Reloads the extensions to the extension point.
	 * <p>
	 * This method can be called more than once in order to reload from
	 * a changed extension registry.
	 * </p>
	 */
	public void reload() {
		IExtensionRegistry registry= Platform.getExtensionRegistry();
		List<IConfigurationElement> elements= new ArrayList<>(Arrays.asList(registry.getConfigurationElementsFor(TextEditorPlugin.PLUGIN_ID, EXTENSION_POINT)));

		List<RulerColumnDescriptor> descriptors= new ArrayList<>();
		Map<String, RulerColumnDescriptor> descriptorMap= new HashMap<>();

		for (Iterator<IConfigurationElement> iter= elements.iterator(); iter.hasNext();) {
			IConfigurationElement element= iter.next();
			try {
				RulerColumnDescriptor desc= new RulerColumnDescriptor(element, this);
				String id= desc.getId();
				if (descriptorMap.containsKey(id)) {
					noteDuplicateId(desc);
					continue;
				}

				descriptors.add(desc);
				descriptorMap.put(id, desc);
			} catch (InvalidRegistryObjectException x) {
				/*
				 * Element is not valid any longer as the contributing plug-in was unloaded or for
				 * some other reason. Do not include the extension in the list and inform the user
				 * about it.
				 */
				noteInvalidExtension(element, x);
			} catch (CoreException x) {
				warnUser(x.getStatus());
			}
		}

		sort(descriptors);

		synchronized (this) {
			fDescriptors= Collections.unmodifiableList(descriptors);
			fDescriptorMap= Collections.unmodifiableMap(descriptorMap);
		}
	}

	/**
	 * Sorts the column contributions.
	 *
	 * @param descriptors the descriptors to sort
	 */
	private void sort(List<RulerColumnDescriptor> descriptors) {
		/*
		 * Topological sort of the DAG defined by the plug-in dependencies
		 * 1. TopoSort descriptors by plug-in dependency
		 * 2. Insert into Directed Acyclic Graph
		 * 3. TopoSort DAG: pick the source with the lowest gravity and remove from DAG
		 */
		ConfigurationElementSorter sorter= new ConfigurationElementSorter() {
			@Override
			public IConfigurationElement getConfigurationElement(Object object) {
				return ((RulerColumnDescriptor) object).getConfigurationElement();
			}
		};
		RulerColumnDescriptor[] array= descriptors.toArray(new RulerColumnDescriptor[descriptors.size()]);
		sorter.sort(array);

		Map<String, RulerColumnDescriptor> descriptorsById= new HashMap<>();
		for (int i= 0; i < array.length; i++) {
			RulerColumnDescriptor desc= array[i];
			descriptorsById.put(desc.getId(), desc);
		}

		DAG<RulerColumnDescriptor> dag= new DAG<>();
		for (int i= 0; i < array.length; i++) {
			RulerColumnDescriptor desc= array[i];
			dag.addVertex(desc);

			Set<RulerColumnPlacementConstraint> before= desc.getPlacement().getConstraints();
			for (Iterator<RulerColumnPlacementConstraint> it= before.iterator(); it.hasNext();) {
				RulerColumnPlacementConstraint constraint= it.next();
				String id= constraint.getId();
				RulerColumnDescriptor target= descriptorsById.get(id);
				if (target == null) {
					noteUnknownTarget(desc, id);
				} else {
					boolean success;
					if (constraint.isBefore())
						success= dag.addEdge(desc, target);
					else
						success= dag.addEdge(target, desc);
					if (!success)
						noteCycle(desc, target);
				}
			}
		}

		Comparator<RulerColumnDescriptor> gravityComp = (o1, o2) -> {
			float diff = o1.getPlacement().getGravity() - o2.getPlacement().getGravity();
			if (diff == 0)
				return 0;
			if (diff < 0)
				return -1;
			return 1;
		};

		/* Topological sort - always select the source with the least gravity */
		Set<RulerColumnDescriptor> toProcess= dag.getSources();
		int index= 0;
		while (!toProcess.isEmpty()) {
			RulerColumnDescriptor next= Collections.min(toProcess, gravityComp);
			array[index]= next;
			index++;
			dag.removeVertex(next);
			toProcess= dag.getSources();
		}
		Assert.isTrue(index == array.length);

		ListIterator<RulerColumnDescriptor> it= descriptors.listIterator();
		for (int i= 0; i < index; i++) {
			it.next();
			it.set(array[i]);
		}
	}

	private void noteInvalidExtension(IConfigurationElement element, InvalidRegistryObjectException x) {
		String message= MessageFormat.format(RulerColumnMessages.RulerColumnRegistry_invalid_msg, new Object[] {ExtensionPointHelper.findId(element)});
		warnUser(message, x);
	}

	private void noteUnknownTarget(RulerColumnDescriptor desc, String referencedId) {
		String message= MessageFormat.format(RulerColumnMessages.RulerColumnRegistry_unresolved_placement_msg, new Object[] {QUALIFIED_EXTENSION_POINT, referencedId, desc.getName(), desc.getContributor()});
		warnUser(message, null);
	}

	private void noteCycle(RulerColumnDescriptor desc, RulerColumnDescriptor target) {
		String message= MessageFormat.format(RulerColumnMessages.RulerColumnRegistry_cyclic_placement_msg, new Object[] {QUALIFIED_EXTENSION_POINT, target.getName(), desc.getName(), desc.getContributor()});
		warnUser(message, null);
	}

	private void noteDuplicateId(RulerColumnDescriptor desc) {
		String message= MessageFormat.format(RulerColumnMessages.RulerColumnRegistry_duplicate_id_msg, new Object[] {QUALIFIED_EXTENSION_POINT, desc.getId(), desc.getContributor()});
		warnUser(message, null);
	}

	private void warnUser(String message, Exception exception) {
		IStatus status= new Status(IStatus.WARNING, TextEditorPlugin.PLUGIN_ID, IStatus.OK, message, exception);
		warnUser(status);
	}

	private void warnUser(IStatus status) {
		TextEditorPlugin.getDefault().getLog().log(status);
	}
}
