/*******************************************************************************
 * Copyright (c) 2006, 2015 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
 *     Andrey Loskutov <loskutov@gmx.de> - generified interface, bug 461762
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472784
 *******************************************************************************/
package org.eclipse.ui.ide.undo;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.internal.ide.undo.ContainerDescription;
import org.eclipse.ui.internal.ide.undo.FileDescription;
import org.eclipse.ui.internal.ide.undo.UndoMessages;


/**
 * WorkspaceUndoUtil defines common utility methods and constants used by
 * clients who create undoable workspace operations.
 *
 * @since 3.3
 */
public class WorkspaceUndoUtil {

	private static ObjectUndoContext tasksUndoContext;
	private static ObjectUndoContext bookmarksUndoContext;
	private static ObjectUndoContext problemsUndoContext;


	/**
	 * Return the undo context that should be used for workspace-wide operations
	 *
	 * @return the undo context suitable for workspace-level operations.
	 */
	public static IUndoContext getWorkspaceUndoContext() {
		return WorkbenchPlugin.getDefault().getOperationSupport()
				.getUndoContext();
	}

	/**
	 * Return the undo context that should be used for operations involving
	 * tasks.
	 *
	 * @return the tasks undo context
	 */
	public static IUndoContext getTasksUndoContext() {
		if (tasksUndoContext == null) {
			tasksUndoContext = new ObjectUndoContext(new Object(),
					"Tasks Context"); //$NON-NLS-1$
			tasksUndoContext.addMatch(getWorkspaceUndoContext());
		}
		return tasksUndoContext;
	}

	/**
	 * Return the undo context that should be used for operations involving
	 * bookmarks.
	 *
	 * @return the bookmarks undo context
	 */
	public static IUndoContext getBookmarksUndoContext() {
		if (bookmarksUndoContext == null) {
			bookmarksUndoContext = new ObjectUndoContext(new Object(),
					"Bookmarks Context"); //$NON-NLS-1$
			bookmarksUndoContext.addMatch(getWorkspaceUndoContext());
		}
		return bookmarksUndoContext;
	}

	/**
	 * Return the undo context that should be used for operations involving
	 * problems.
	 *
	 * @return the problems undo context
	 * @since 3.7
	 */
	public static IUndoContext getProblemsUndoContext() {
		if (problemsUndoContext == null) {
			problemsUndoContext = new ObjectUndoContext(new Object(),
					"Problems Context"); //$NON-NLS-1$
			problemsUndoContext.addMatch(getWorkspaceUndoContext());
		}
		return problemsUndoContext;
	}

	/**
	 * Make an <code>IAdaptable</code> that adapts to the specified shell,
	 * suitable for passing for passing to any
	 * {@link org.eclipse.core.commands.operations.IUndoableOperation} or
	 * {@link org.eclipse.core.commands.operations.IOperationHistory} method
	 * that requires an {@link org.eclipse.core.runtime.IAdaptable}
	 * <code>uiInfo</code> parameter.
	 *
	 * @param shell
	 *            the shell that should be returned by the IAdaptable when asked
	 *            to adapt a shell. If this parameter is <code>null</code>,
	 *            the returned shell will also be <code>null</code>.
	 *
	 * @return an IAdaptable that will return the specified shell.
	 */
	public static IAdaptable getUIInfoAdapter(final Shell shell) {
		return new IAdaptable() {
			@Override
			public <T> T getAdapter(Class<T> clazz) {
				if (clazz == Shell.class) {
					return clazz.cast(shell);
				}
				return null;
			}
		};
	}

	private WorkspaceUndoUtil() {
		// should not construct
	}

	/**
	 * Delete all of the specified resources, returning resource descriptions
	 * that can be used to restore them.
	 *
	 * @param resourcesToDelete
	 *            an array of resources to be deleted
	 * @param monitor
	 *            the progress monitor to use to show the operation's progress
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the
	 *            caller in order to supply UI information for prompting the
	 *            user if necessary. When this parameter is not
	 *            <code>null</code>, it contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 *
	 * @param deleteContent
	 *            a boolean indicating whether project content should be deleted
	 *            when a project resource is to be deleted
	 * @return an array of ResourceDescriptions that can be used to restore the
	 *         deleted resources.
	 * @throws CoreException
	 *             propagates any CoreExceptions thrown from the resources API
	 */
	static ResourceDescription[] delete(IResource[] resourcesToDelete, IProgressMonitor mon, IAdaptable uiInfo,
			boolean deleteContent) throws CoreException {
		SubMonitor subMonitor = SubMonitor.convert(mon, resourcesToDelete.length);

		final List<CoreException> exceptions = new ArrayList<>();
		boolean forceOutOfSyncDelete = false;
		ResourceDescription[] returnedResourceDescriptions = new ResourceDescription[resourcesToDelete.length];
		subMonitor.setTaskName(UndoMessages.AbstractResourcesOperation_DeleteResourcesProgress);
		for (int i = 0; i < resourcesToDelete.length; ++i) {
			IResource resource = resourcesToDelete[i];
			try {
				returnedResourceDescriptions[i] = delete(resource, subMonitor.split(1), uiInfo,
						forceOutOfSyncDelete, deleteContent);
			} catch (CoreException e) {
				if (resource.getType() == IResource.FILE) {
					IStatus[] children = e.getStatus().getChildren();
					if (children.length == 1 && children[0].getCode() == IResourceStatus.OUT_OF_SYNC_LOCAL) {
						int result = queryDeleteOutOfSync(resource, uiInfo);

						if (result == IDialogConstants.YES_ID) {
							// retry the delete with a force out of sync
							delete(resource, subMonitor.split(1), uiInfo, true, deleteContent);
						} else if (result == IDialogConstants.YES_TO_ALL_ID) {
							// all future attempts should force out of
							// sync
							forceOutOfSyncDelete = true;
							delete(resource, subMonitor.split(1), uiInfo, forceOutOfSyncDelete,
									deleteContent);
						} else if (result == IDialogConstants.CANCEL_ID) {
							throw new OperationCanceledException();
						} else {
							exceptions.add(e);
						}
					} else {
						exceptions.add(e);
					}
				} else {
					exceptions.add(e);
				}
			}
		}
		IStatus result = createResult(exceptions);
		if (!result.isOK()) {
			throw new CoreException(result);
		}
		return returnedResourceDescriptions;
	}

	/**
	 * Copies the resources to the given destination. This method can be called
	 * recursively to merge folders during folder copy.
	 *
	 * @param resources
	 *            the resources to be copied
	 * @param destination
	 *            the destination path for the resources, relative to the
	 *            workspace
	 * @param resourcesAtDestination
	 *            A list used to record the new copies.
	 * @param monitor
	 *            the progress monitor used to show progress
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the
	 *            caller in order to supply UI information for prompting the
	 *            user if necessary. When this parameter is not
	 *            <code>null</code>, it contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 * @param pathIncludesName
	 *            a boolean that indicates whether the specified path includes
	 *            the resource's name at the destination. If this value is
	 *            <code>true</code>, the destination will contain the desired
	 *            name of the resource (usually only desired when only one
	 *            resource is being copied). If this value is <code>false</code>,
	 *            each resource's name will be appended to the destination.
	 * @return an array of ResourceDescriptions describing any resources that
	 *         were overwritten by the copy operation
	 * @throws CoreException
	 *             propagates any CoreExceptions thrown from the resources API
	 */
	static ResourceDescription[] copy(IResource[] resources, IPath destination, List<IResource> resourcesAtDestination,
			IProgressMonitor monitor, IAdaptable uiInfo, boolean pathIncludesName) throws CoreException {
		return copy(resources, destination, resourcesAtDestination, monitor,
				uiInfo, pathIncludesName, false, false, null);
	}

	/**
	 * Copies the resources to the given destination. This method can be called
	 * recursively to merge folders during folder copy.
	 *
	 * @param resources
	 *            the resources to be copied
	 * @param destination
	 *            the destination path for the resources, relative to the
	 *            workspace
	 * @param resourcesAtDestination
	 *            A list used to record the new copies.
	 * @param monitor
	 *            the progress monitor used to show progress
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the
	 *            caller in order to supply UI information for prompting the
	 *            user if necessary. When this parameter is not
	 *            <code>null</code>, it contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 * @param pathIncludesName
	 *            a boolean that indicates whether the specified path includes
	 *            the resource's name at the destination. If this value is
	 *            <code>true</code>, the destination will contain the desired
	 *            name of the resource (usually only desired when only one
	 *            resource is being copied). If this value is <code>false</code>,
	 *            each resource's name will be appended to the destination.
	 * @param createVirtual
	 *            a boolean that indicates whether virtual folders should be
	 *            created instead of folders when a hierarchy of files is
	 *            copied.
	 * @param createLinks
	 *            a boolean that indicates whether linked resources should be
	 *            created instead of files and folders (if createGroups is
	 *            false) when copied.
	 * @param relativeToVariable
	 *            a String that indicates relative to which variable linked
	 *            resources should be created, if createLinks is set to true.
	 *            Absolute linked resources will be created if null is passed
	 *            otherwise (and createLinks is set to true).
	 * @return an array of ResourceDescriptions describing any resources that
	 *         were overwritten by the copy operation
	 * @throws CoreException
	 *             propagates any CoreExceptions thrown from the resources API
	 */
	static ResourceDescription[] copy(IResource[] resources, IPath destination, List<IResource> resourcesAtDestination,
			IProgressMonitor monitor, IAdaptable uiInfo, boolean pathIncludesName, boolean createVirtual,
			boolean createLinks, String relativeToVariable) throws CoreException {
		SubMonitor subMonitor = SubMonitor.convert(monitor, resources.length);
		subMonitor.setTaskName(UndoMessages.AbstractResourcesOperation_CopyingResourcesProgress);
		List<ResourceDescription> overwrittenResources = new ArrayList<>();
		for (IResource source : resources) {
			SubMonitor iterationProgress = subMonitor.split(1).setWorkRemaining(100);
			IPath destinationPath;
			if (pathIncludesName) {
				destinationPath = destination;
			} else {
				destinationPath = destination.append(source.getName());
			}
			IWorkspaceRoot workspaceRoot = getWorkspaceRoot();
			IResource existing = workspaceRoot.findMember(destinationPath);
			if (source.getType() == IResource.FOLDER && existing != null) {
				// The resource is a folder and it exists in the destination.
				// Copy its children to the existing destination.
				if ((source.isLinked() && existing.isLinked())
						|| (source.isVirtual() && existing.isVirtual())
						|| (!source.isLinked() && !existing.isLinked()
								&& !source.isVirtual() && !existing.isVirtual())) {
					IResource[] children = ((IContainer) source).members();
					// copy only linked resource children (267173)
					if (source.isLinked() && source.getLocation().equals(existing.getLocation()))
						children = filterNonLinkedResources(children);
					ResourceDescription[] overwritten = copy(children,
							destinationPath, resourcesAtDestination,
							iterationProgress, uiInfo, false,
							createVirtual, createLinks, relativeToVariable);
					// We don't record the copy since this recursive call will
					// do so. Just record the overwrites.
					for (ResourceDescription resourceDesc : overwritten) {
						overwrittenResources.add(resourceDesc);
					}
				} else {
					// delete the destination folder, copying a linked folder
					// over an unlinked one or vice versa. Fixes bug 28772.
					ResourceDescription[] deleted = delete(new IResource[] { existing }, iterationProgress.split(1),
							uiInfo, false);
					iterationProgress.setWorkRemaining(100);
					if ((createLinks || createVirtual) && (source.isLinked() == false)
							&& (source.isVirtual() == false)) {
						IFolder folder = workspaceRoot.getFolder(destinationPath);
						if (createVirtual) {
							folder.create(IResource.VIRTUAL, true, iterationProgress.split(1));
							IResource[] members = ((IContainer) source).members();
							if (members.length > 0) {
								overwrittenResources.addAll(Arrays.asList(copy(members, destinationPath,
										resourcesAtDestination, iterationProgress.split(99), uiInfo, false,
										createVirtual, createLinks, relativeToVariable)));

							}
						} else
							folder.createLink(createRelativePath(source.getLocationURI(), relativeToVariable, folder),
									0, iterationProgress.split(100));
					} else
						source.copy(destinationPath, IResource.SHALLOW, iterationProgress.split(100));
					// Record the copy
					resourcesAtDestination.add(getWorkspace().getRoot().findMember(destinationPath));
					for (ResourceDescription resourceDes : deleted) {
						overwrittenResources.add(resourceDes);
					}
				}
			} else {
				if (existing != null) {
					// source is a FILE and destination EXISTS
					if ((createLinks || createVirtual)
							&& (source.isLinked() == false)) {
						// we create a linked file, and overwrite the
						// destination
						ResourceDescription[] deleted = delete(
								new IResource[] { existing },
								iterationProgress.split(1), uiInfo,
								false);
						iterationProgress.setWorkRemaining(100);
						if (source.getType() == IResource.FILE) {
							IFile file = workspaceRoot.getFile(destinationPath);
							file.createLink(createRelativePath(
									source.getLocationURI(), relativeToVariable, file), 0,
									iterationProgress.split(100));
						} else {
							IFolder folder = workspaceRoot
									.getFolder(destinationPath);
							if (createVirtual) {
								folder.create(IResource.VIRTUAL, true, iterationProgress.split(1));
								IResource[] members = ((IContainer) source).members();
								if (members.length > 0) {
									overwrittenResources.addAll(Arrays.asList(copy(members, destinationPath,
											resourcesAtDestination, iterationProgress.split(99), uiInfo, false,
											createVirtual, createLinks, relativeToVariable)));
								}
							} else
								folder.createLink(
										createRelativePath(source.getLocationURI(), relativeToVariable, folder), 0,
										iterationProgress.split(100));
						}
						resourcesAtDestination.add(getWorkspace().getRoot()
								.findMember(destinationPath));
						for (ResourceDescription resourceDesc : deleted) {
							overwrittenResources.add(resourceDesc);
						}
					} else {
						if (source.isLinked() == existing.isLinked()) {
							overwrittenResources.add(copyOverExistingResource(source, existing,
									iterationProgress.split(100), uiInfo, false));
							// Record the "copy"
							resourcesAtDestination.add(existing);
						} else {
							// Copying a linked resource over unlinked or vice
							// versa. Can't use setContents here. Fixes bug
							// 28772.
							ResourceDescription[] deleted = delete(
									new IResource[] { existing },
									iterationProgress.split(1), uiInfo,
									false);
							source.copy(destinationPath, IResource.SHALLOW, iterationProgress.split(99));
							// Record the copy
							resourcesAtDestination.add(getWorkspace().getRoot()
									.findMember(destinationPath));
							for (ResourceDescription resourceDesc : deleted) {
								overwrittenResources.add(resourceDesc);
							}
						}
					}
				} else {
					// source is a FILE or FOLDER
					// no resources are being overwritten
					// ensure the destination path exists
					IPath parentPath = destination;
					if (pathIncludesName) {
						parentPath = destination.removeLastSegments(1);
					}
					IContainer generatedParent = generateContainers(parentPath);
					if ((createLinks || createVirtual)
							&& (source.isLinked() == false)) {
						if (source.getType() == IResource.FILE) {
							IFile file = workspaceRoot.getFile(destinationPath);
							file.createLink(createRelativePath(
									source.getLocationURI(), relativeToVariable, file), 0,
									iterationProgress.split(100));
						} else {
							IFolder folder = workspaceRoot
									.getFolder(destinationPath);
							if (createVirtual) {
								folder.create(IResource.VIRTUAL, true, iterationProgress.split(1));
								IResource[] members = ((IContainer) source).members();
								if (members.length > 0) {
									overwrittenResources.addAll(Arrays.asList(copy(members, destinationPath,
											resourcesAtDestination, iterationProgress.split(99), uiInfo, false,
											createVirtual, createLinks, relativeToVariable)));
								}
							} else
								folder.createLink(
										createRelativePath(source.getLocationURI(), relativeToVariable, folder), 0,
										iterationProgress.split(100));
						}
					} else
						source.copy(destinationPath, IResource.SHALLOW, iterationProgress.split(100));
					// Record the copy. If we had to generate a parent
					// folder, that should be recorded as part of the copy
					if (generatedParent == null) {
						resourcesAtDestination.add(getWorkspace().getRoot()
								.findMember(destinationPath));
					} else {
						resourcesAtDestination.add(generatedParent);
					}
				}
			}
		}
		return overwrittenResources
				.toArray(new ResourceDescription[overwrittenResources.size()]);
	}

	/**
	 * Transform an absolute path URI to a relative path one (i.e. from
	 * "C:\foo\bar\file.txt" to "VAR\file.txt" granted that the relativeVariable
	 * is "VAR" and points to "C:\foo\bar\").
	 *
	 * @param locationURI
	 * @param resource
	 * @return an URI that was made relative to a variable
	 */
	static private URI createRelativePath(URI locationURI, String relativeVariable, IResource resource) {
		if (relativeVariable == null)
			return locationURI;
		IPath location = URIUtil.toPath(locationURI);
		IPath result;
		try {
			result = URIUtil.toPath(resource.getPathVariableManager().convertToRelative(URIUtil.toURI(location), true, relativeVariable));
		} catch (CoreException e) {
			return locationURI;
		}
		return URIUtil.toURI(result);
	}

	/**
	 * Moves the resources to the given destination. This method can be called
	 * recursively to merge folders during folder move.
	 *
	 * @param resources
	 *            the resources to be moved
	 * @param destination
	 *            the destination path for the resources, relative to the
	 *            workspace
	 * @param resourcesAtDestination
	 *            A list used to record each moved resource.
	 * @param reverseDestinations
	 *            A list used to record each moved resource's original location
	 * @param mon
	 *            the progress monitor used to show progress
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the caller
	 *            in order to supply UI information for prompting the user if
	 *            necessary. When this parameter is not <code>null</code>, it
	 *            contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 * @param pathIncludesName
	 *            a boolean that indicates whether the specified path includes
	 *            the resource's name at the destination. If this value is
	 *            <code>true</code>, the destination will contain the desired
	 *            name of the resource (usually only desired when only one
	 *            resource is being moved). If this value is <code>false</code>,
	 *            each resource's name will be appended to the destination.
	 * @return an array of ResourceDescriptions describing any resources that
	 *         were overwritten by the move operation
	 * @throws CoreException
	 *             propagates any CoreExceptions thrown from the resources API
	 */
	static ResourceDescription[] move(IResource[] resources, IPath destination, List<IResource> resourcesAtDestination,
			List<IPath> reverseDestinations, IProgressMonitor mon, IAdaptable uiInfo, boolean pathIncludesName)
					throws CoreException {

		SubMonitor subMonitor = SubMonitor.convert(mon, resources.length);
		subMonitor.setTaskName(UndoMessages.AbstractResourcesOperation_MovingResources);
		List<ResourceDescription> overwrittenResources = new ArrayList<>();
		for (IResource resource : resources) {
			SubMonitor iterationProgress = subMonitor.split(1);
			IPath destinationPath;
			if (pathIncludesName) {
				destinationPath = destination;
			} else {
				destinationPath = destination.append(resource.getName());
			}
			IWorkspaceRoot workspaceRoot = getWorkspaceRoot();
			IResource existing = workspaceRoot.findMember(destinationPath);
			if (resource.getType() == IResource.FOLDER && existing != null) {
				// The resource is a folder and it exists in the destination.
				// Move its children to the existing destination.
				if (resource.isLinked() == existing.isLinked()) {
					IResource[] children = ((IContainer) resource).members();
					// move only linked resource children (267173)
					if (resource.isLinked() && resource.getLocation().equals(existing.getLocation()))
						children = filterNonLinkedResources(children);
					ResourceDescription[] overwritten = move(children, destinationPath, resourcesAtDestination,
							reverseDestinations, iterationProgress.split(90), uiInfo, false);
					// We don't record the moved resources since the recursive
					// call has done so. Just record the overwrites.
					for (ResourceDescription resourceDesc : overwritten) {
						overwrittenResources.add(resourceDesc);
					}
					// Delete the source. No need to record it since it
					// will get moved back.
					delete(resource, iterationProgress.split(10), uiInfo, false, false);
				} else {
					// delete the destination folder, moving a linked folder
					// over an unlinked one or vice versa. Fixes bug 28772.
					ResourceDescription[] deleted = delete(new IResource[] { existing }, iterationProgress.split(10),
							uiInfo, false);
					// Record the original path
					reverseDestinations.add(resource.getFullPath());
					resource.move(destinationPath, IResource.SHALLOW | IResource.KEEP_HISTORY,
							iterationProgress.split(90));
					// Record the resource at its destination
					resourcesAtDestination.add(getWorkspace().getRoot().findMember(destinationPath));
					for (ResourceDescription resourceDesc : deleted) {
						overwrittenResources.add(resourceDesc);
					}
				}
			} else {
				if (existing != null) {
					if (resource.isLinked() == existing.isLinked()) {
						// Record the original path
						reverseDestinations.add(resource.getFullPath());
						overwrittenResources.add(copyOverExistingResource(resource, existing,
								iterationProgress.split(100), uiInfo, true));
						resourcesAtDestination.add(existing);
					} else {
						// Moving a linked resource over unlinked or vice
						// versa. Can't use setContents here. Fixes bug 28772.
						ResourceDescription[] deleted = delete(
								new IResource[] { existing },
								iterationProgress.split(1), uiInfo,
								false);
						reverseDestinations.add(resource.getFullPath());
						resource.move(destinationPath, IResource.SHALLOW
								| IResource.KEEP_HISTORY,
								iterationProgress.split(99));
						// Record the resource at its destination
						resourcesAtDestination.add(getWorkspace().getRoot()
								.findMember(destinationPath));
						for (ResourceDescription resourceDesc : deleted) {
							overwrittenResources.add(resourceDesc);
						}
					}
				} else {
					// No resources are being overwritten.
					// First record the source path
					reverseDestinations.add(resource.getFullPath());
					// ensure the destination path exists
					IPath parentPath = destination;
					if (pathIncludesName) {
						parentPath = destination.removeLastSegments(1);
					}

					IContainer generatedParent = generateContainers(parentPath);
					resource.move(destinationPath, IResource.SHALLOW | IResource.KEEP_HISTORY,
							iterationProgress.split(100));
					// Record the move. If we had to generate a parent
					// folder, that should be recorded as part of the copy
					if (generatedParent == null) {
						resourcesAtDestination.add(getWorkspace().getRoot()
								.findMember(destinationPath));
					} else {
						resourcesAtDestination.add(generatedParent);
					}
				}
			}
		}
		return overwrittenResources
				.toArray(new ResourceDescription[overwrittenResources.size()]);

	}

	/**
	 * Returns only the linked resources out of an array of resources
	 * @param resources The resources to filter
	 * @return The linked resources
	 */
	private static IResource[] filterNonLinkedResources(IResource[] resources) {
		List<IResource> result = new ArrayList<>();
		for (IResource resource : resources) {
			if (resource.isLinked())
				result.add(resource);
		}
		return result.toArray(new IResource[0]);
	}

	/**
	 * Recreate the resources from the specified resource descriptions.
	 *
	 * @param resourcesToRecreate
	 *            the ResourceDescriptions describing resources to be recreated
	 * @param monitor
	 *            the progress monitor used to show progress
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the
	 *            caller in order to supply UI information for prompting the
	 *            user if necessary. When this parameter is not
	 *            <code>null</code>, it contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 * @return an array of resources that were created
	 * @throws CoreException
	 *             propagates any CoreExceptions thrown from the resources API
	 */
	static IResource[] recreate(ResourceDescription[] resourcesToRecreate,
			IProgressMonitor monitor, IAdaptable uiInfo) throws CoreException {
		SubMonitor subMonitor = SubMonitor.convert(monitor, resourcesToRecreate.length);
		final List<CoreException> exceptions = new ArrayList<>();
		IResource[] resourcesToReturn = new IResource[resourcesToRecreate.length];
		subMonitor.setTaskName(UndoMessages.AbstractResourcesOperation_CreateResourcesProgress);
		for (int i = 0; i < resourcesToRecreate.length; ++i) {
			try {
				resourcesToReturn[i] = resourcesToRecreate[i].createResource(subMonitor.split(1));
			} catch (CoreException e) {
				exceptions.add(e);
			}
		}
		IStatus result = WorkspaceUndoUtil.createResult(exceptions);
		if (!result.isOK()) {
			throw new CoreException(result);
		}
		return resourcesToReturn;
	}

	/**
	 * Delete the specified resources, returning a resource description that can
	 * be used to restore it.
	 *
	 * @param resourceToDelete
	 *            the resource to be deleted
	 * @param monitor
	 *            the progress monitor to use to show the operation's progress
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the
	 *            caller in order to supply UI information for prompting the
	 *            user if necessary. When this parameter is not
	 *            <code>null</code>, it contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 * @param forceOutOfSyncDelete
	 *            a boolean indicating whether a resource should be deleted even
	 *            if it is out of sync with the file system
	 * @param deleteContent
	 *            a boolean indicating whether project content should be deleted
	 *            when a project resource is to be deleted
	 * @return a ResourceDescription that can be used to restore the deleted
	 *         resource.
	 * @throws CoreException
	 *             propagates any CoreExceptions thrown from the resources API
	 */
	static ResourceDescription delete(IResource resourceToDelete,
			IProgressMonitor monitor, IAdaptable uiInfo,
			boolean forceOutOfSyncDelete, boolean deleteContent)
			throws CoreException {
		SubMonitor subMonitor = SubMonitor.convert(monitor);
		ResourceDescription resourceDescription = ResourceDescription
				.fromResource(resourceToDelete);
		if (resourceToDelete.getType() == IResource.PROJECT) {
			// it is a project
			subMonitor
					.setTaskName(UndoMessages.AbstractResourcesOperation_DeleteResourcesProgress);
			IProject project = (IProject) resourceToDelete;
			project.delete(deleteContent, forceOutOfSyncDelete, subMonitor);
		} else {
			// if it's not a project, just delete it
			subMonitor.setWorkRemaining(2);
			monitor
					.setTaskName(UndoMessages.AbstractResourcesOperation_DeleteResourcesProgress);
			int updateFlags;
			if (forceOutOfSyncDelete) {
				updateFlags = IResource.KEEP_HISTORY | IResource.FORCE;
			} else {
				updateFlags = IResource.KEEP_HISTORY;
			}
			resourceToDelete.delete(updateFlags, subMonitor.split(1));
			resourceDescription.recordStateFromHistory(resourceToDelete, subMonitor.split(1));
		}

		return resourceDescription;
	}

	/*
	 * Copy the content of the specified resource to the existing resource,
	 * returning a ResourceDescription that can be used to restore the original
	 * content. Do nothing if the resources are not files.
	 */
	private static ResourceDescription copyOverExistingResource(
			IResource source, IResource existing, IProgressMonitor monitor,
			IAdaptable uiInfo, boolean deleteSourceFile) throws CoreException {
		if (!(source instanceof IFile && existing instanceof IFile)) {
			return null;
		}
		IFile file = (IFile) source;
		IFile existingFile = (IFile) existing;
		SubMonitor subMonitor = SubMonitor.convert(monitor,
				UndoMessages.AbstractResourcesOperation_CopyingResourcesProgress, deleteSourceFile ? 3 : 2);
		if (file != null) {
			if (validateEdit(file, existingFile, getShell(uiInfo))) {
				// Remember the state of the existing file so it can be
				// restored.
				FileDescription fileDescription = new FileDescription(existingFile);
				// Reset the contents to that of the file being moved
				existingFile.setContents(file.getContents(), IResource.KEEP_HISTORY, subMonitor.split(1));
				fileDescription.recordStateFromHistory(existingFile, subMonitor.split(1));
				// Now delete the source file if requested
				// We don't need to remember anything about it, because
				// any undo involving this operation will move the original
				// content back to it.
				if (deleteSourceFile) {
					file.delete(IResource.KEEP_HISTORY, subMonitor.split(1));
				}
				return fileDescription;
			}
		}
		return null;
	}

	/*
	 * Check for existence of the specified path and generate any containers
	 * that do not yet exist. Return any generated containers, or null if no
	 * container had to be generated.
	 */
	private static IContainer generateContainers(IPath path)
			throws CoreException {
		IResource container;
		if (path.segmentCount() == 0) {
			// nothing to generate
			return null;
		}
		container = getWorkspaceRoot().findMember(path);
		// Nothing to generate because container exists
		if (container != null) {
			return null;
		}

		// Now make a non-existent handle representing the desired container
		if (path.segmentCount() == 1) {
			container = ResourcesPlugin.getWorkspace().getRoot().getProject(
					path.segment(0));
		} else {
			container = ResourcesPlugin.getWorkspace().getRoot()
					.getFolder(path);
		}
		ContainerDescription containerDescription = ContainerDescription
				.fromContainer((IContainer) container);
		container = containerDescription.createResourceHandle();
		containerDescription.createExistentResourceFromHandle(container,
				new NullProgressMonitor());
		return (IContainer) container;
	}

	/*
	 * Ask the user whether the given resource should be deleted despite being
	 * out of sync with the file system.
	 *
	 * Return one of the IDialogConstants constants indicating which of the Yes,
	 * Yes to All, No, Cancel options has been selected by the user.
	 */
	private static int queryDeleteOutOfSync(IResource resource,
			IAdaptable uiInfo) {
		Shell shell = getShell(uiInfo);
		final MessageDialog dialog = new MessageDialog(
				shell,
				UndoMessages.AbstractResourcesOperation_deletionMessageTitle,
				null,
				NLS
						.bind(
								UndoMessages.AbstractResourcesOperation_outOfSyncQuestion,
								resource.getName()), MessageDialog.QUESTION,
						0,
						IDialogConstants.YES_LABEL,
						IDialogConstants.YES_TO_ALL_LABEL,
						IDialogConstants.NO_LABEL,
						IDialogConstants.CANCEL_LABEL) {
			@Override
			protected int getShellStyle() {
				return super.getShellStyle() | SWT.SHEET;
			}
		};
		shell.getDisplay().syncExec(() -> dialog.open());
		int result = dialog.getReturnCode();
		if (result == 0) {
			return IDialogConstants.YES_ID;
		}
		if (result == 1) {
			return IDialogConstants.YES_TO_ALL_ID;
		}
		if (result == 2) {
			return IDialogConstants.NO_ID;
		}
		return IDialogConstants.CANCEL_ID;
	}

	/*
	 * Creates and return a result status appropriate for the given list of
	 * exceptions.
	 */
	private static IStatus createResult(List<CoreException> exceptions) {
		if (exceptions.isEmpty()) {
			return Status.OK_STATUS;
		}
		final int exceptionCount = exceptions.size();
		if (exceptionCount == 1) {
			return exceptions.get(0).getStatus();
		}
		CoreException[] children = exceptions
				.toArray(new CoreException[exceptionCount]);
		boolean outOfSync = false;
		for (CoreException exception : children) {
			if (exception.getStatus().getCode() == IResourceStatus.OUT_OF_SYNC_LOCAL) {
				outOfSync = true;
				break;
			}
		}
		String title = outOfSync ? UndoMessages.AbstractResourcesOperation_outOfSyncError
				: UndoMessages.AbstractResourcesOperation_deletionExceptionMessage;
		final MultiStatus multi = new MultiStatus(
				IDEWorkbenchPlugin.IDE_WORKBENCH, 0, title, null);
		for (int i = 0; i < exceptionCount; i++) {
			CoreException exception = children[i];
			IStatus status = exception.getStatus();
			multi.add(new Status(status.getSeverity(), status.getPlugin(),
					status.getCode(), status.getMessage(), exception));
		}
		return multi;
	}

	/*
	 * Return the workspace.
	 */
	private static IWorkspace getWorkspace() {
		return ResourcesPlugin.getWorkspace();
	}

	/*
	 * Return the workspace root.
	 */
	private static IWorkspaceRoot getWorkspaceRoot() {
		return getWorkspace().getRoot();
	}

	/*
	 * Validate the destination file if it is read-only and additionally the
	 * source file if both are read-only. Returns true if both files could be
	 * made writeable.
	 */
	private static boolean validateEdit(IFile source, IFile destination,
			Shell shell) {
		if (destination.isReadOnly()) {
			IWorkspace workspace = WorkspaceUndoUtil.getWorkspace();
			IStatus status;
			if (source.isReadOnly()) {
				status = workspace.validateEdit(new IFile[] { source,
						destination }, shell);
			} else {
				status = workspace.validateEdit(new IFile[] { destination },
						shell);
			}
			return status.isOK();
		}
		return true;
	}

	/**
	 * Return the shell described by the specified adaptable, or the active
	 * shell if no shell has been specified in the adaptable.
	 *
	 * @param uiInfo
	 *            the IAdaptable (or <code>null</code>) provided by the
	 *            caller in order to supply UI information for prompting the
	 *            user if necessary. When this parameter is not
	 *            <code>null</code>, it contains an adapter for the
	 *            org.eclipse.swt.widgets.Shell.class
	 *
	 * @return the Shell that can be used to show information
	 */
	public static Shell getShell(IAdaptable uiInfo) {
		Shell shell = Adapters.adapt(uiInfo, Shell.class);
		if (shell != null) {
			return shell;
		}
		return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
	}
}
