/*******************************************************************************
 * Copyright (c) 2005, 2009 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.ltk.ui.refactoring.model;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.diff.ITwoWayDiff;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.core.mapping.IMergeContext;
import org.eclipse.team.core.mapping.IMergeStatus;
import org.eclipse.team.core.mapping.IResourceDiff;
import org.eclipse.team.core.mapping.ResourceMappingMerger;
import org.eclipse.team.core.mapping.provider.MergeStatus;

import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.mapping.ModelProvider;
import org.eclipse.core.resources.mapping.ResourceMapping;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;

import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;

import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptorProxy;
import org.eclipse.ltk.core.refactoring.history.RefactoringHistory;
import org.eclipse.ltk.internal.core.refactoring.history.RefactoringHistoryImplementation;
import org.eclipse.ltk.internal.core.refactoring.history.RefactoringHistoryService;
import org.eclipse.ltk.internal.ui.refactoring.RefactoringUIMessages;
import org.eclipse.ltk.internal.ui.refactoring.RefactoringUIPlugin;
import org.eclipse.ltk.internal.ui.refactoring.model.RefactoringHistoryMergeWizard;
import org.eclipse.ltk.ui.refactoring.history.RefactoringHistoryControlConfiguration;

/**
 * Partial implementation of a refactoring-aware resource mapping merger.
 * <p>
 * This class provides support to determine pending refactorings during model
 * merging and model update, and displays a refactoring wizard to apply the
 * refactorings to the local workspace.
 * </p>
 * <p>
 * Note: this class is designed to be extended by clients. Programming language
 * implementers which need a refactoring-aware resource mapping merger to
 * associated with their model provider may extend this class to implement
 * language-specific project dependency rules.
 * </p>
 *
 * @see org.eclipse.team.core.mapping.IResourceMappingMerger
 *
 * @since 3.2
 */
public abstract class AbstractResourceMappingMerger extends ResourceMappingMerger {

	/** Refactoring history model merge configuration */
	private static final class RefactoringHistoryModelMergeConfiguration extends RefactoringHistoryControlConfiguration {

		/**
		 * Creates a new refactoring history model merge configuration.
		 *
		 * @param project
		 *            the project, or <code>null</code>
		 */
		public RefactoringHistoryModelMergeConfiguration(final IProject project) {
			super(project, false, false);
		}

		@Override
		public String getProjectPattern() {
			return RefactoringUIMessages.RefactoringModelMerger_project_pattern;
		}

		@Override
		public String getWorkspaceCaption() {
			return RefactoringUIMessages.RefactoringModelMerger_workspace_caption;
		}
	}

	/** The refactoring history model merge wizard */
	private static final class RefactoringHistoryModelMergeWizard extends RefactoringHistoryMergeWizard {

		/**
		 * Creates a new refactoring history model merge wizard.
		 */
		public RefactoringHistoryModelMergeWizard() {
			super(RefactoringUIMessages.RefactoringWizard_refactoring, RefactoringUIMessages.AbstractRefactoringModelMerger_wizard_title, RefactoringUIMessages.AbstractRefactoringModelMerger_wizard_description);
		}
	}

	/**
	 * Returns the shell of the active workbench window.
	 *
	 * @return the active shell
	 */
	private static Shell getActiveShell() {
		final IWorkbench workbench= PlatformUI.getWorkbench();
		if (workbench != null) {
			final IWorkbenchWindow window= workbench.getActiveWorkbenchWindow();
			if (window != null)
				return window.getShell();
		}
		return null;
	}

	/**
	 * Returns the projects affected by the specified refactoring history.
	 *
	 * @param history
	 *            the refactoring history
	 * @return the affected projects, or <code>null</code> if the entire
	 *         workspace is affected
	 */
	private static IProject[] getAffectedProjects(final RefactoringHistory history) {
		final Set<IProject> set= new HashSet<>();
		final IWorkspaceRoot root= ResourcesPlugin.getWorkspace().getRoot();
		for (RefactoringDescriptorProxy proxy : history.getDescriptors()) {
			final String name= proxy.getProject();
			if (name != null && !"".equals(name)) //$NON-NLS-1$
				set.add(root.getProject(name));
			else
				return null;
		}
		final IProject[] projects= new IProject[set.size()];
		set.toArray(projects);
		return projects;
	}

	/**
	 * Return a shell that can be used by the operation to display dialogs, etc.
	 *
	 * @return a shell
	 */
	private static Shell getDialogShell() {
		final Shell[] shell= new Shell[] { null};
		Display.getDefault().syncExec(new Runnable() {

			@Override
			public final void run() {
				shell[0]= getActiveShell();
			}
		});
		return shell[0];
	}

	/** The model provider */
	private final ModelProvider fModelProvider;

	/**
	 * Creates a new abstract refactoring model merger.
	 *
	 * @param provider
	 *            the model provider
	 */
	protected AbstractResourceMappingMerger(final ModelProvider provider) {
		Assert.isNotNull(provider);
		fModelProvider= provider;
	}

	/**
	 * Hook method which is called before the actual merge process happens.
	 * <p>
	 * Subclasses may extend this method to perform any special processing. The
	 * default implementation checks whether there are any pending refactorings
	 * in the merge context and displays a refactoring wizard to let the user
	 * perform the pending refactorings before merge.
	 * </p>
	 * <p>
	 * Returning a status of severity {@link IStatus#ERROR} will terminate the
	 * merge process.
	 * </p>
	 *
	 * @param context
	 *            the merge context
	 * @param monitor
	 *            the progress monitor to use
	 * @return a status describing the outcome of the operation
	 */
	protected IStatus aboutToPerformMerge(final IMergeContext context, final IProgressMonitor monitor) {
		Assert.isNotNull(context);
		Assert.isNotNull(monitor);
		try {
			monitor.beginTask(RefactoringUIMessages.RefactoringModelMerger_merge_message, 100);
			final IDiff[] diffs= getDiffs(context);
			final RefactoringHistory history= getRefactoringHistory(diffs, monitor);
			if (history != null && !history.isEmpty()) {
				boolean execute= true;
				final IProject[] projects= getAffectedProjects(history);
				if (projects != null) {
					final IProject[] dependencies= getDependencies(projects);
					if (dependencies.length == 0)
						execute= false;
				}
				if (execute) {
					final Shell shell= getDialogShell();
					shell.getDisplay().syncExec(new Runnable() {

						@Override
						public final void run() {
							if (MessageDialog.openQuestion(shell, RefactoringUIMessages.RefactoringWizard_refactoring, RefactoringUIMessages.AbstractRefactoringModelMerger_accept_question)) {
								final RefactoringHistoryMergeWizard wizard= new RefactoringHistoryModelMergeWizard();
								int result= Window.OK;
								try {
									wizard.setConfiguration(new RefactoringHistoryModelMergeConfiguration((projects != null && projects.length == 1) ? projects[0] : null));
									wizard.setInput(history);
									result= new WizardDialog(shell, wizard).open();
								} finally {
									if (result != Window.CANCEL)
										wizard.resolveConflicts(context);
								}
							}
						}
					});
				}
			}
		} finally {
			monitor.done();
		}
		return Status.OK_STATUS;
	}

	/**
	 * Creates a merge status.
	 *
	 * @param context
	 *            the merge context
	 * @param status
	 *            the status
	 * @return the resulting merge status
	 */
	private IStatus createMergeStatus(final IMergeContext context, final IStatus status) {
		if (status.getCode() == IMergeStatus.CONFLICTS)
			return new MergeStatus(status.getPlugin(), status.getMessage(), context.getScope().getMappings(fModelProvider.getDescriptor().getId()));
		return status;
	}

	/**
	 * Returns the dependent projects of the projects associated with the
	 * incoming refactorings.
	 * <p>
	 * Subclasses must implement this method to return the dependent projects
	 * according to the semantics of the associated programming language. The
	 * result of this method is used to decide whether the resource mapping
	 * merger should execute the incoming refactorings in order to fix up
	 * references in dependent projects.
	 * </p>
	 *
	 * @param projects
	 *            the projects associated with the incoming refactorings in the
	 *            synchronization scope.
	 * @return the dependent projects, or an empty array
	 */
	protected abstract IProject[] getDependencies(IProject[] projects);

	/**
	 * Returns the diffs from the merge context.
	 *
	 * @param context
	 *            the merge context
	 * @return the diffs, or an empty array
	 */
	private IDiff[] getDiffs(final IMergeContext context) {
		final ResourceMapping[] mappings= context.getScope().getMappings(fModelProvider.getDescriptor().getId());
		final Set<IDiff> set= new HashSet<>();
		for (int index= 0; index < mappings.length; index++) {
			final IDiff[] diffs= context.getDiffTree().getDiffs(context.getScope().getTraversals(mappings[index]));
			set.addAll(Arrays.asList(diffs));
		}
		return set.toArray(new IDiff[set.size()]);
	}

	@Override
	protected final ModelProvider getModelProvider() {
		return fModelProvider;
	}

	/**
	 * Returns the incoming refactoring history from the diffs.
	 *
	 * @param diffs
	 *            the diffs
	 * @param monitor
	 *            the progress monitor to use
	 * @return the incoming refactoring history
	 */
	private RefactoringHistory getRefactoringHistory(final IDiff[] diffs, final IProgressMonitor monitor) {
		final Set<RefactoringDescriptor> result= new HashSet<>();
		try {
			monitor.beginTask(RefactoringUIMessages.RefactoringModelMerger_retrieving_refactorings, diffs.length * 2);
			for (IDiff diff : diffs) {
				if (diff instanceof IThreeWayDiff) {
					final IThreeWayDiff threeWay= (IThreeWayDiff) diff;
					final Set<RefactoringDescriptor> localDescriptors= new HashSet<>();
					final Set<RefactoringDescriptor> remoteDescriptors= new HashSet<>();
					final ITwoWayDiff localDiff= threeWay.getLocalChange();
					if (localDiff instanceof IResourceDiff && localDiff.getKind() != IDiff.NO_CHANGE) {
						final IResourceDiff resourceDiff= (IResourceDiff) localDiff;
						final IFileRevision revision= resourceDiff.getAfterState();
						if (revision != null) {
							final String name= revision.getName();
							if (name.equalsIgnoreCase(RefactoringHistoryService.NAME_HISTORY_FILE))
								getRefactoringDescriptors(revision, localDescriptors, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
						}
					}
					final ITwoWayDiff remoteDiff= threeWay.getLocalChange();
					if (remoteDiff instanceof IResourceDiff && remoteDiff.getKind() != IDiff.NO_CHANGE) {
						final IResourceDiff resourceDiff= (IResourceDiff) remoteDiff;
						final IFileRevision revision= resourceDiff.getAfterState();
						if (revision != null) {
							final String name= revision.getName();
							if (name.equalsIgnoreCase(RefactoringHistoryService.NAME_HISTORY_FILE))
								getRefactoringDescriptors(revision, remoteDescriptors, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
						}
					}
					remoteDescriptors.removeAll(localDescriptors);
					result.addAll(remoteDescriptors);
				}
			}
		} finally {
			monitor.done();
		}
		return new RefactoringHistoryImplementation(result.toArray(new RefactoringDescriptorProxy[result.size()]));
	}

	/**
	 * Retrieves the refactoring descriptors contained in the specified file
	 * revision.
	 *
	 * @param revision
	 *            the file revision
	 * @param descriptors
	 *            the refactoring descriptor set to populate
	 * @param monitor
	 *            the progress monitor to use
	 */
	static final void getRefactoringDescriptors(final IFileRevision revision, final Set<RefactoringDescriptor> descriptors, final IProgressMonitor monitor) {
		try {
			monitor.beginTask(RefactoringUIMessages.RefactoringModelMerger_retrieving_refactorings, 1);
			IStorage storage= null;
			try {
				storage= revision.getStorage(new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			} catch (CoreException exception) {
				RefactoringUIPlugin.log(exception);
			}
			if (storage != null) {
				InputStream stream= null;
				try {
					stream= storage.getContents();
					final RefactoringHistory history= RefactoringHistoryService.getInstance().readRefactoringHistory(stream, RefactoringDescriptor.MULTI_CHANGE);
					for (RefactoringDescriptorProxy proxy : history.getDescriptors()) {
						descriptors.add(proxy.requestDescriptor(null));
					}
				} catch (CoreException exception) {
					RefactoringUIPlugin.log(exception);
				} finally {
					if (stream != null) {
						try {
							stream.close();
						} catch (IOException exception) {
							// Do nothing
						}
					}
				}
			}
		} finally {
			monitor.done();
		}
	}

	@Override
	public IStatus merge(final IMergeContext context, IProgressMonitor monitor) throws CoreException {
		Assert.isNotNull(context);
		IStatus status= Status.OK_STATUS;
		if (monitor == null)
			monitor= new NullProgressMonitor();
		try {
			monitor.beginTask(RefactoringUIMessages.RefactoringModelMerger_merge_message, 200);
//			status= aboutToPerformMerge(context, new SubProgressMonitor(monitor, 75));
			if (status.getSeverity() != IStatus.ERROR) {
				final IDiff[] diffs= getDiffs(context);
				status= createMergeStatus(context, context.merge(diffs, false, new SubProgressMonitor(monitor, 100)));
				final int code= status.getCode();
				if (status.getSeverity() != IStatus.ERROR && code != IMergeStatus.CONFLICTS && code != IMergeStatus.INTERNAL_ERROR)
					status= mergePerformed(context, new SubProgressMonitor(monitor, 25));
			}
		} finally {
			monitor.done();
		}
		return status;
	}

	/**
	 * Hook method which is called after the actual merge process happened. This
	 * method is only called if {@link #merge(IMergeContext, IProgressMonitor)}
	 * returns a status with severity less than {@link IStatus#ERROR} and a
	 * status code unequal to {@link IMergeStatus#CONFLICTS} or
	 * {@link IMergeStatus#INTERNAL_ERROR}.
	 * <p>
	 * Subclasses may extend this method to perform any special processing. The
	 * default implementation does nothing.
	 * </p>
	 *
	 * @param context
	 *            the merge context
	 * @param monitor
	 *            the progress monitor to use
	 * @return a status describing the outcome of the operation
	 */
	protected IStatus mergePerformed(final IMergeContext context, final IProgressMonitor monitor) {
		Assert.isNotNull(context);
		Assert.isNotNull(monitor);
		try {
			monitor.beginTask(RefactoringUIMessages.RefactoringModelMerger_merge_message, 1);
			return Status.OK_STATUS;
		} finally {
			monitor.done();
		}
	}
}
