/*******************************************************************************
 * Copyright (c) 2005, 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.compare.patch;

import java.io.BufferedReader;
import java.io.IOException;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.internal.ComparePreferencePage;
import org.eclipse.compare.internal.CompareUIPlugin;
import org.eclipse.compare.internal.core.patch.FilePatch2;
import org.eclipse.compare.internal.core.patch.PatchReader;
import org.eclipse.compare.internal.patch.FilePatch;
import org.eclipse.compare.internal.patch.PatchWizard;
import org.eclipse.compare.internal.patch.PatchWizardDialog;
import org.eclipse.compare.internal.patch.Utilities;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.ide.IDE;

/**
 * An operation that provides an interface to the Apply Patch Wizard. Users specify
 * the input in terms of an <code>IStorage</code> (note: input must be in unified diff
 * format), an <code>IResource</code> target to apply the patch to and can provide <code>CompareConfiguration</code>
 * elements to supply the label and images used on the preview page and hunk merge page. Finally, the
 * user can also supply a title and image to override the default ones provided by the Apply Patch Wizard.
 * Note that the Apply Patch Wizard does not require any particular set of inputs, and in the absence of
 * any user supplied values, it will work in default mode.
 *
 * @since 3.3
 *
 */
public class ApplyPatchOperation implements Runnable {

	private final IWorkbenchPart part;

	/**
	 * Used for the Preview Patch page.
	 */
	private final CompareConfiguration configuration;

	/**
	 * The patch to use as an input into the Apply Patch wizard
	 */
	private final IStorage patch;

	/**
	 * Specific <code>IResource</code> target to patch.
	 */
	private final IResource target;

	/**
	 * An optional image for the patch wizard
	 */
	private ImageDescriptor patchWizardImage;


	/**
	 * An optional title for the patchWizard
	 */
	private String patchWizardTitle;

	private boolean saveAllEditors = true;

	/**
	 * Return whether the given storage contains a patch.
	 * @param storage the storage
	 * @return whether the given storage contains a patch
	 * @throws CoreException if an error occurs reading the contents from the storage
	 */
	public static boolean isPatch(IStorage storage) throws CoreException {
		return internalParsePatch(storage).length > 0;
	}

	/**
	 * Parse the given patch and return the set of file patches that it contains.
	 * @param storage the storage that contains the patch
	 * @return the set of file patches that the storage contains
	 * @throws CoreException if an error occurs reading the contents from the storage
	 */
	public static IFilePatch[] parsePatch(IStorage storage) throws CoreException {
		return internalParsePatch(storage);
	}

	/**
	 * Creates a new ApplyPatchOperation with the supplied compare configuration, patch and target.
	 * The behaviour of the Apply Patch wizard is controlled by the number of parameters supplied:
	 * <ul>
	 * <li>If a patch is supplied, the initial input page is skipped. If a patch is not supplied the wizard
	 * will open on the input page.</li>
	 * <li>If the patch is a workspace patch, the target selection page is skipped and the preview page is
	 * displayed.</li>
	 * <li>If the patch is not a workspace patch and the target is specified, the target page is still
	 * shown with the target selected.</li>
	 * </ul>
	 *
	 * @param part 	an IWorkbenchPart or <code>null</code>
	 * @param patch		an IStorage containing a patch in unified diff format or <code>null</code>
	 * @param target	an IResource which the patch is to be applied to or <code>null</code>
	 * @param configuration	a CompareConfiguration supplying the labels and images for the preview patch page
	 */
	public ApplyPatchOperation(IWorkbenchPart part, IStorage patch, IResource target, CompareConfiguration configuration) {
		Assert.isNotNull(configuration);
		this.part = part;
		this.patch = patch;
		this.target = target;
		this.configuration = configuration;
	}

	/**
	 * Create an operation for the given part and resource. This method is a convenience
	 * method that calls {@link #ApplyPatchOperation(IWorkbenchPart, IStorage, IResource, CompareConfiguration)}
	 * with appropriate defaults for the other parameters.
	 * @param targetPart an IResource which the patch is to be applied to or <code>null</code>
	 * @param resource an IResource which the patch is to be applied to or <code>null</code>
	 * @see #ApplyPatchOperation(IWorkbenchPart, IStorage, IResource, CompareConfiguration)
	 */
	public ApplyPatchOperation(IWorkbenchPart targetPart, IResource resource) {
		this(targetPart, null, resource, new CompareConfiguration());
	}

	/**
	 * Open the Apply Patch wizard using the values associated with this operation.
	 * This method must be called from the UI thread.
	 */
	public void openWizard() {
		saveAllEditors();

		if (saveAllEditors) {
			PatchWizard wizard = new PatchWizard(patch, target, configuration);
			if (patchWizardImage != null)
				wizard.setDefaultPageImageDescriptor(patchWizardImage);
			if (patchWizardTitle != null)
				wizard.setWindowTitle(patchWizardTitle);
			wizard.setNeedsProgressMonitor(true);

			new PatchWizardDialog(getShell(), wizard).open();
		}
	}

	/**
	 * Return the parent shell to be used when the wizard is opened.
	 * By default, the site of the part is used to get the shell.
	 * Subclasses may override.
	 * @return the parent shell to be used when the wizard is opened
	 */
	protected Shell getShell() {
		if (part == null)
			return CompareUIPlugin.getShell();
		return part.getSite().getShell();
	}

	/**
	 * This method will save all dirty editors. It will prompt the user if the Compare preference to save
	 * dirty editors before viewing a patch is <code>false</code>. Clients can use this or provide their own
	 * implementation.
	 */
	protected void saveAllEditors(){
		saveAllEditors = IDE.saveAllEditors(new IResource[]{ResourcesPlugin.getWorkspace().getRoot()}, !ComparePreferencePage.getSaveAllEditors());
	}

	/**
	 * Sets the title of the patch wizard. Needs to be set before {@link #openWizard()} is called.
	 * @param title	a string to display in the title bar
	 */
	public void setPatchWizardTitle(String title){
		this.patchWizardTitle = title;
	}

	/**
	 * Sets the image descriptor to use in the patch wizard. Needs to be set before  {@link #openWizard()} is called.
	 * @param descriptor an image descriptor
	 */
	public void setPatchWizardImageDescriptor(ImageDescriptor descriptor){
		this.patchWizardImage = descriptor;
	}

	@Override
	public void run() {
		openWizard();
	}

	private static IFilePatch[] internalParsePatch(IStorage storage)
			throws CoreException {
		BufferedReader reader = Utilities.createReader(storage);
		try {
			PatchReader patchReader = new PatchReader() {
				@Override
				protected FilePatch2 createFileDiff(IPath oldPath, long oldDate,
						IPath newPath, long newDate) {
					return new FilePatch(oldPath, oldDate, newPath,
							newDate);
				}
			};
			patchReader.parse(reader);
			FilePatch2[] fileDiffs = patchReader.getAdjustedDiffs();

			IFilePatch[] filePatch = new IFilePatch[fileDiffs.length];
			for (int i = 0; i < fileDiffs.length; i++) {
				filePatch[i] = (FilePatch) fileDiffs[i];
			}

			return filePatch;
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR,
					CompareUIPlugin.PLUGIN_ID, 0, e.getMessage(), e));
		} finally {
			try {
				reader.close();
			} catch (IOException e) { // ignored
			}
		}
	}


}
