/*******************************************************************************
 * Copyright (c) 2007, 2011 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.jdt.internal.corext.fix;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;

import com.ibm.icu.text.Collator;

import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Text;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.util.SafeRunnable;

import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.cleanup.CleanUpOptions;
import org.eclipse.jdt.ui.cleanup.ICleanUp;
import org.eclipse.jdt.ui.cleanup.ICleanUpConfigurationUI;
import org.eclipse.jdt.ui.cleanup.ICleanUpOptionsInitializer;

import org.eclipse.jdt.internal.ui.IJavaStatusConstants;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.fix.MapCleanUpOptions;
import org.eclipse.jdt.internal.ui.preferences.cleanup.CleanUpTabPage;
import org.eclipse.jdt.internal.ui.preferences.cleanup.ContributedCleanUpTabPage;
import org.eclipse.jdt.internal.ui.util.SWTUtil;

/**
 * The clean up registry provides a set of clean ups and there corresponding UI representatives.
 *
 * @since 3.4
 */
public class CleanUpRegistry {

	private static final class ErrorPage implements ICleanUpConfigurationUI {

		private final Exception fException;

		private ErrorPage(Exception e) {
			fException= e;
		}

		@Override
		public Composite createContents(Composite parent) {
			Composite result= new Composite(parent, SWT.NONE);
			result.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
			result.setLayout(new GridLayout(1, false));

			Text text= new Text(result, SWT.MULTI | SWT.BORDER | SWT.READ_ONLY);
			SWTUtil.fixReadonlyTextBackground(text);
			text.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
			text.setText(Messages.format(FixMessages.CleanUpRegistry_ErrorTabPage_description, fException.getLocalizedMessage()));

			return result;
		}

		@Override
		public int getCleanUpCount() {
			return 0;
		}

		@Override
		public String getPreview() {
			return FixMessages.CleanUpRegistry_ErrorTabPage_preview;
		}

		@Override
		public int getSelectedCleanUpCount() {
			return 0;
		}

		@Override
		public void setOptions(CleanUpOptions options) {
		}
	}

	public static class CleanUpTabPageDescriptor {

		private static final String ATTRIBUTE_ID_CLASS= "class"; //$NON-NLS-1$
		private static final String ATTRIBUTE_ID_NAME= "name"; //$NON-NLS-1$
		private static final String ATTRIBUTE_NAME_KIND= "cleanUpKind"; //$NON-NLS-1$

		private final String fName;
		private final IConfigurationElement fElement;
		private int fKind;

		/**
		 * @param element the configuration element
		 */
		public CleanUpTabPageDescriptor(IConfigurationElement element) {
			fElement= element;
			fName= element.getAttribute(ATTRIBUTE_ID_NAME);
			String kind= fElement.getAttribute(ATTRIBUTE_NAME_KIND);
			fKind= getCleanUpKind(kind);
			if (fKind == -1)
				JavaPlugin.logErrorMessage(Messages.format(FixMessages.CleanUpRegistry_WrongKindForConfigurationUI_error, new String[] { fName, element.getContributor().getName(),
						kind }));
		}

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

		/**
		 * @return the kind of clean up
		 */
		public int getKind() {
			return fKind;
		}

		/**
		 * @return new instance of a tab page
		 */
		public CleanUpTabPage createTabPage() {
			try {
				ICleanUpConfigurationUI page= (ICleanUpConfigurationUI)fElement.createExecutableExtension(ATTRIBUTE_ID_CLASS);
				if (page instanceof CleanUpTabPage)
					return (CleanUpTabPage)page;

				return new ContributedCleanUpTabPage(page);
			} catch (final CoreException e) {
				JavaPlugin.log(e);
				return new ContributedCleanUpTabPage(new ErrorPage(e));
			}
		}
	}

	private static class CleanUpDescriptor {

		private static final String ATTRIBUTE_ID_CLASS= "class"; //$NON-NLS-1$
		private static final String ATTRIBURE_ID_RUNAFTER= "runAfter"; //$NON-NLS-1$
		private static final String ATTRIBUTE_ID_ID= "id"; //$NON-NLS-1$

		private final IConfigurationElement fElement;
		private final String fId;
		private final String fRunAfter;

		/**
		 * @param element the configuration element
		 */
		public CleanUpDescriptor(IConfigurationElement element) {
			fElement= element;
			fId= element.getAttribute(ATTRIBUTE_ID_ID);
			fRunAfter= element.getAttribute(ATTRIBURE_ID_RUNAFTER);
		}

		/**
		 * @return the unique id of this clean up
		 */
		public String getId() {
			return fId;
		}

		/**
		 * @return the id of the clean up which must run before this clean up or
		 *         <strong>null</strong> if none specified
		 */
		public String getRunAfter() {
			return fRunAfter;
		}

		/**
		 * @return the clean up or <code>null</code> if the clean up could not be instantiated
		 */
		public ICleanUp createCleanUp() {
			try {
				return (ICleanUp)fElement.createExecutableExtension(ATTRIBUTE_ID_CLASS);
			} catch (CoreException e) {
				String msg= Messages.format(FixMessages.CleanUpRegistry_cleanUpCreation_error, new String[] { fElement.getAttribute(ATTRIBUTE_ID_ID), fElement.getContributor().getName() });
				JavaPlugin.logErrorStatus(msg, e.getStatus());
				return null;
			}
		}
	}

	private static final class CleanUpInitializerDescriptor {

		private static final String ATTRIBUTE_NAME_CLASS= "class"; //$NON-NLS-1$
		private static final String ATTRIBUTE_NAME_KIND= "cleanUpKind"; //$NON-NLS-1$

		private final IConfigurationElement fElement;

		private final int fKind;

		private ICleanUpOptionsInitializer fOptionsProvider;

		public CleanUpInitializerDescriptor(IConfigurationElement element) {
			fElement= element;
			String kind= fElement.getAttribute(ATTRIBUTE_NAME_KIND);
			fKind= getCleanUpKind(kind);
			if (fKind == -1)
				JavaPlugin.logErrorMessage(Messages.format(FixMessages.CleanUpRegistry_UnknownInitializerKind_errorMessage, new String[] { element.getContributor().getName(), kind }));
		}

		public int getKind() {
			return fKind;
		}

		public ICleanUpOptionsInitializer getOptionsInitializer() {
			if (fOptionsProvider == null) {
				try {
					fOptionsProvider= (ICleanUpOptionsInitializer)fElement.createExecutableExtension(ATTRIBUTE_NAME_CLASS);
				} catch (CoreException e) {
					JavaPlugin.log(e);
					fOptionsProvider= options -> {
					};
				}
			}
			return fOptionsProvider;
		}
	}

	private static final String EXTENSION_POINT_NAME= "cleanUps"; //$NON-NLS-1$
	private static final String CLEAN_UP_CONFIGURATION_ELEMENT_NAME= "cleanUp"; //$NON-NLS-1$
	private static final String TABPAGE_CONFIGURATION_ELEMENT_NAME= "cleanUpConfigurationUI"; //$NON-NLS-1$

	private static final String CLEAN_UP_INITIALIZER_CONFIGURATION_ELEMENT_NAME= "cleanUpOptionsInitializer"; //$NON-NLS-1$
	private static final String ATTRIBUTE_KIND_TYPE_SAVE_ACTION= "saveAction"; //$NON-NLS-1$
	private static final String ATTRIBUTE_KIND_TYPE_CLEAN_UP= "cleanUp"; //$NON-NLS-1$

	private CleanUpDescriptor[] fCleanUpDescriptors;
	private CleanUpTabPageDescriptor[] fPageDescriptors;

	private CleanUpInitializerDescriptor[] fCleanUpInitializerDescriptors;

	/**
	 * Creates and returns the registered clean ups that don't fail upon creation.
	 *
	 * @return an array of clean ups
	 */
	public synchronized ICleanUp[] createCleanUps() {
		return createCleanUps(null);
	}

	/**
	 * Creates and returns the registered clean ups that don't fail upon creation.
	 *
	 * @param ids the ids of the clean ups to create, or <code>null</code> to create all
	 * @return an array of clean ups
	 * @since 3.5
	 */
	public synchronized ICleanUp[] createCleanUps(Set<String> ids) {
		ensureCleanUpsRegistered();
		ArrayList<ICleanUp> result= new ArrayList<>(fCleanUpDescriptors.length);
		for (CleanUpDescriptor descriptor : fCleanUpDescriptors) {
			if (ids == null || ids.contains(descriptor.getId())) {
				ICleanUp cleanUp= descriptor.createCleanUp();
				if (cleanUp != null)
					result.add(cleanUp);
			}
		}
		return result.toArray(new ICleanUp[result.size()]);
	}

	/**
	 * @param kind the kind of clean up for which to retrieve the configuratin pages
	 *
	 * @return set of clean up tab page descriptors
	 *
	 * @see CleanUpConstants#DEFAULT_CLEAN_UP_OPTIONS
	 * @see CleanUpConstants#DEFAULT_SAVE_ACTION_OPTIONS
	 */
	public synchronized CleanUpTabPageDescriptor[] getCleanUpTabPageDescriptors(int kind) {
		ensurePagesRegistered();

		ArrayList<CleanUpTabPageDescriptor> result= new ArrayList<>();
		for (CleanUpTabPageDescriptor descriptor : fPageDescriptors) {
			if (descriptor.getKind() == kind) {
				result.add(descriptor);
			}
		}
		return result.toArray(new CleanUpTabPageDescriptor[result.size()]);
	}

	/**
	 * Returns the default options for the specified clean up kind.
	 *
	 * @param kind the kind of clean up for which to retrieve the options
	 * @return the default options
	 *
	 * @see CleanUpConstants#DEFAULT_CLEAN_UP_OPTIONS
	 * @see CleanUpConstants#DEFAULT_SAVE_ACTION_OPTIONS
	 */
	public MapCleanUpOptions getDefaultOptions(int kind) {
		ensureCleanUpInitializersRegistered();

		CleanUpOptions options= new CleanUpOptions();
		for (CleanUpInitializerDescriptor descriptor : fCleanUpInitializerDescriptors) {
			if (descriptor.getKind() == kind) {
				descriptor.getOptionsInitializer().setDefaultOptions(options);
			}
		}
		MapCleanUpOptions mapCleanUpOptions= new MapCleanUpOptions();
		mapCleanUpOptions.addAll(options);
		return mapCleanUpOptions;
	}

	private synchronized void ensureCleanUpsRegistered() {
		if (fCleanUpDescriptors != null)
			return;


		final ArrayList<CleanUpDescriptor> descriptors= new ArrayList<>();

		IExtensionPoint point= Platform.getExtensionRegistry().getExtensionPoint(JavaPlugin.getPluginId(), EXTENSION_POINT_NAME);
		for (IConfigurationElement element : point.getConfigurationElements()) {
			if (CLEAN_UP_CONFIGURATION_ELEMENT_NAME.equals(element.getName())) {
				descriptors.add(new CleanUpDescriptor(element));
			}
		}


		// Make sure we filter those who fail or misbehave
		for (int i= 0; i < descriptors.size(); i++) {
			final CleanUpDescriptor cleanUpDescriptor= descriptors.get(i);
			final boolean disable[]= new boolean[1];
			ISafeRunnable runnable= new SafeRunnable() {

				@Override
				public void run() throws Exception {
					ICleanUp cleanUp= cleanUpDescriptor.createCleanUp();
					if (cleanUp == null)
						disable[0]= true;
					else {
						cleanUp.setOptions(new CleanUpOptions());
						String[] enbledSteps= cleanUp.getStepDescriptions();
						if (enbledSteps != null && enbledSteps.length > 0) {
							JavaPlugin.logErrorMessage(
									Messages.format(FixMessages.CleanUpRegistry_cleanUpAlwaysEnabled_error, new String[] { cleanUpDescriptor.getId(),
									cleanUpDescriptor.fElement.getContributor().getName() }));
							disable[0]= true;
						}
					}
				}
				@Override
				public void handleException(Throwable t) {
					disable[0]= true;
					String message= Messages.format(FixMessages.CleanUpRegistry_cleanUpCreation_error, new String[] { cleanUpDescriptor.getId(),
							cleanUpDescriptor.fElement.getContributor().getName() });
					IStatus status= new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IJavaStatusConstants.INTERNAL_ERROR, message, t);
					JavaPlugin.log(status);
				}

			};
			SafeRunner.run(runnable);
			if (disable[0])
				descriptors.remove(i--);
		}

		fCleanUpDescriptors= descriptors.toArray(new CleanUpDescriptor[descriptors.size()]);
		sort(fCleanUpDescriptors);

	}

	private static void sort(CleanUpDescriptor[] data) {
		int lastSwapI= -1;
		int lastSwapJ= -1;
		mainLoop: for (int i= 0; i < data.length; i++) {
			String runAfter= data[i].getRunAfter();
			if (runAfter == null)
				continue;
			int jStart= i + 1;
			for (int j= jStart; j < data.length; j++) {
				String jID= data[j].getId();
				if (runAfter.equals(jID)) {
					if (lastSwapI == i && j >= lastSwapJ) {
						JavaPlugin.logErrorMessage("Problem reading cleanUps extensions: cannot satisfy rule for '" + data[i].getId() + "' to runAfter '" + runAfter + "'");   //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
						continue mainLoop;
					}
					lastSwapI= i;
					lastSwapJ= j;
					swap(data, i, j);
					i--;
					continue mainLoop;
				}
			}
			for (int j= 0; j < jStart; j++) {
				String jID= data[j].getId();
				if (runAfter.equals(jID))
					continue mainLoop;
			}
			JavaPlugin.logErrorMessage("Problem reading cleanUps extensions: cannot satisfy rule for '" + data[i].getId() + "' to runAfter '" + runAfter + "' because the runAfter clean up does not exist."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		}
	}

	private static void swap(CleanUpDescriptor[] data, int i, int j) {
		CleanUpDescriptor o= data[i];
		data[i]= data[j];
		data[j]= o;
	}

	private synchronized void ensurePagesRegistered() {
		if (fPageDescriptors != null)
			return;

		ArrayList<CleanUpTabPageDescriptor> result= new ArrayList<>();

		IExtensionPoint point= Platform.getExtensionRegistry().getExtensionPoint(JavaPlugin.getPluginId(), EXTENSION_POINT_NAME);
		for (IConfigurationElement element : point.getConfigurationElements()) {
			if (TABPAGE_CONFIGURATION_ELEMENT_NAME.equals(element.getName())) {
				result.add(new CleanUpTabPageDescriptor(element));
			}
		}

		fPageDescriptors= result.toArray(new CleanUpTabPageDescriptor[result.size()]);
		Arrays.sort(fPageDescriptors, (o1, o2) -> {
			String name1= o1.getName();
			String name2= o2.getName();
			return Collator.getInstance().compare(name1.replace("&", ""), name2.replace("&", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		});
	}

	private synchronized void ensureCleanUpInitializersRegistered() {
		if (fCleanUpInitializerDescriptors != null)
			return;

		ArrayList<CleanUpInitializerDescriptor> result= new ArrayList<>();

		IExtensionPoint point= Platform.getExtensionRegistry().getExtensionPoint(JavaPlugin.getPluginId(), EXTENSION_POINT_NAME);
		for (IConfigurationElement element : point.getConfigurationElements()) {
			if (CLEAN_UP_INITIALIZER_CONFIGURATION_ELEMENT_NAME.equals(element.getName())) {
				result.add(new CleanUpInitializerDescriptor(element));
			}
		}

		fCleanUpInitializerDescriptors= result.toArray(new CleanUpInitializerDescriptor[result.size()]);
	}

	private static int getCleanUpKind(String kind) {
		switch (kind) {
		case ATTRIBUTE_KIND_TYPE_CLEAN_UP:
			return CleanUpConstants.DEFAULT_CLEAN_UP_OPTIONS;
		case ATTRIBUTE_KIND_TYPE_SAVE_ACTION:
			return CleanUpConstants.DEFAULT_SAVE_ACTION_OPTIONS;
		default:
			return -1;
		}
	}

}
