/*******************************************************************************
 * Copyright (c) 2000, 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.core.refactoring;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;

import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.ltk.internal.core.refactoring.NotCancelableProgressMonitor;

/**
 * Operation that, when run, performs a {@link Change} object. The operation
 * can be created in two different ways: with a given change or with a
 * {@link CreateChangeOperation}. If created the second way the given create
 * change operation will be used to create the actual change to perform.
 * <p>
 * If the change has been performed successfully (e.g. {@link #changeExecuted()} returns
 * <code>true</code>) then the operation has called {@link Change#dispose()} as well
 * to clear-up internal state in the change object. If it hasn't been executed the
 * change, the change is still intact and the client is responsible to dispose the
 * change object.
 * </p>
 * <p>
 * If an undo change has been provided by the change to execute then the operation
 * calls {@link Change#initializeValidationData(IProgressMonitor)} to initialize the
 * undo change's validation data.
 * </p>
 * <p>
 * If an undo manager has been set via the method {@link #setUndoManager(IUndoManager, String)}
 * then the undo object, if any has been provided, will be pushed onto the manager's
 * undo stack.
 * </p>
 * <p>
 * The operation should be executed via the run method offered by
 * <code>IWorkspace</code> to achieve proper delta batching.
 * </p>
 * <p>
 * Note: this class is not intended to be extended outside of the refactoring framework.
 * </p>
 *
 * @since 3.0
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class PerformChangeOperation implements IWorkspaceRunnable {

	private Change fChange;
	private CreateChangeOperation fCreateChangeOperation;
	private RefactoringStatus fValidationStatus;

	private Change fUndoChange;
	private String fUndoName;
	private IUndoManager fUndoManager;

	private boolean fChangeExecuted;
	private boolean fChangeExecutionFailed;
	private ISchedulingRule fSchedulingRule;

	/**
	 * Creates a new perform change operation instance for the given change.
	 *
	 * @param change the change to be applied to the workbench
	 */
	public PerformChangeOperation(Change change) {
		Assert.isNotNull(change);
		fChange= change;
		fSchedulingRule= ResourcesPlugin.getWorkspace().getRoot();
	}

	/**
	 * Creates a new <code>PerformChangeOperation</code> for the given {@link
	 * CreateChangeOperation}. The create change operation is used to create
	 * the actual change to execute.
	 *
	 * @param op the <code>CreateChangeOperation</code> used to create the
	 *  actual change object
	 */
	public PerformChangeOperation(CreateChangeOperation op) {
		Assert.isNotNull(op);
		fCreateChangeOperation= op;
		fSchedulingRule= ResourcesPlugin.getWorkspace().getRoot();
	}

	/**
	 * Returns <code>true</code> if the change execution failed.
	 *
	 * @return <code>true</code> if the change execution failed;
	 *  <code>false</code> otherwise
	 *
	 */
	public boolean changeExecutionFailed() {
		return fChangeExecutionFailed;
	}

	/**
	 * Returns <code>true</code> if the change has been executed. Otherwise <code>
	 * false</code> is returned.
	 *
	 * @return <code>true</code> if the change has been executed, otherwise
	 *  <code>false</code>
	 */
	public boolean changeExecuted() {
		return fChangeExecuted;
	}

	/**
	 * Returns the status of the condition checking. Returns <code>null</code> if
	 * no condition checking has been requested.
	 *
	 * @return the status of the condition checking
	 */
	public RefactoringStatus getConditionCheckingStatus() {
		if (fCreateChangeOperation != null)
			return fCreateChangeOperation.getConditionCheckingStatus();
		return null;
	}

	/**
	 * Returns the change used by this operation. This is either the change passed to
	 * the constructor or the one create by the <code>CreateChangeOperation</code>.
	 * Method returns <code>null</code> if the create operation did not create
	 * a corresponding change or hasn't been executed yet.
	 *
	 * @return the change used by this operation or <code>null</code> if no change
	 *  has been created
	 */
	public Change getChange() {
		return fChange;
	}

	/**
	 * Returns the undo change of the change performed by this operation. Returns
	 * <code>null</code> if the change hasn't been performed yet or if the change
	 * doesn't provide a undo.
	 *
	 * @return the undo change of the performed change or <code>null</code>
	 */
	public Change getUndoChange() {
		return fUndoChange;
	}

	/**
	 * Returns the refactoring status returned from the call <code>IChange#isValid()</code>.
	 * Returns <code>null</code> if the change has not been executed.
	 *
	 * @return the change's validation status
	 */
	public RefactoringStatus getValidationStatus() {
		return fValidationStatus;
	}

	/**
	 * Sets the undo manager. If the executed change provides an undo change,
	 * then the undo change is pushed onto this manager.
	 *
	 * @param manager the undo manager to use or <code>null</code> if no
	 *  undo recording is desired
	 * @param undoName the name used to present the undo change on the undo
	 *  stack. Must be a human-readable string. Must not be <code>null</code>
	 *  if manager is unequal <code>null</code>
	 */
	public void setUndoManager(IUndoManager manager, String undoName) {
		if (manager != null) {
			Assert.isNotNull(undoName);
		}
		fUndoManager= manager;
		fUndoName= undoName;
	}

	/**
	 * Sets the scheduling rule used to execute this operation. If
	 * not set then the workspace root is used. The supplied Change
	 * must be able to be performed in the provided scheduling rule.
	 *
	 * @param rule the rule to use, or <code>null</code> to use no scheduling rule
	 * @since 3.3
	 */
	public void setSchedulingRule(ISchedulingRule rule) {
		fSchedulingRule= rule;
	}

	@Override
	public void run(IProgressMonitor pm) throws CoreException {
		if (pm == null)
			pm= new NullProgressMonitor();
		try {
			fChangeExecuted= false;
			if (createChange()) {
				pm.beginTask("", 4); //$NON-NLS-1$
				pm.subTask(""); //$NON-NLS-1$
				fCreateChangeOperation.run(new SubProgressMonitor(pm, 3));
				// Check for cancellation before executing the change, since canceling
				// during change execution is not supported
				// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=187265 ):
				if (pm.isCanceled())
					throw new OperationCanceledException();

				fChange= fCreateChangeOperation.getChange();
				if (fChange != null) {
					executeChange(new SubProgressMonitor(pm, 1));
				} else {
					pm.worked(1);
				}
			} else {
				executeChange(pm);
			}
		} finally {
			pm.done();
		}
	}

	/**
	 * Actually executes the change.
	 *
	 * @param pm a progress monitor to report progress
	 *
	 * @throws CoreException if an unexpected error occurs during
	 *  change execution
	 */
	protected void executeChange(IProgressMonitor pm) throws CoreException {
		fChangeExecuted= false;
		if (!fChange.isEnabled())
			return;
		IWorkspaceRunnable runnable= monitor -> {
			boolean undoInitialized= false;
			try {
				monitor.beginTask("", 10); //$NON-NLS-1$
				fValidationStatus= fChange.isValid(new SubProgressMonitor(monitor, 1));
				if (fValidationStatus.hasFatalError())
					return;
				boolean aboutToPerformChangeCalled= false;
				try {
					if (fUndoManager != null) {
						ResourcesPlugin.getWorkspace().checkpoint(false);
						fUndoManager.aboutToPerformChange(fChange);
						aboutToPerformChangeCalled= true;
					}
					fChangeExecutionFailed= true;
					fUndoChange= fChange.perform(new SubProgressMonitor(monitor, 9));
					fChangeExecutionFailed= false;
					fChangeExecuted= true;
				} finally {
					if (fUndoManager != null) {
						ResourcesPlugin.getWorkspace().checkpoint(false);
						if (aboutToPerformChangeCalled)
							fUndoManager.changePerformed(fChange, !fChangeExecutionFailed);
					}
				}
				fChange.dispose();
				if (fUndoChange != null) {
					fUndoChange.initializeValidationData(new NotCancelableProgressMonitor(
						new SubProgressMonitor(monitor, 1)));
					undoInitialized= true;
				}
				if (fUndoManager != null) {
					if (fUndoChange != null) {
						fUndoManager.addUndo(fUndoName, fUndoChange);
					} else {
						fUndoManager.flush();
					}
				}
			} catch (CoreException | RuntimeException e) {
				if (fUndoManager != null)
					fUndoManager.flush();
				if (fUndoChange != null && undoInitialized) {
					Change ch= fUndoChange;
					fUndoChange= null;
					ch.dispose();
				}
				fUndoChange= null;
				throw e;
			} finally {
				monitor.done();
			}
		};
		ResourcesPlugin.getWorkspace().run(runnable, fSchedulingRule, IWorkspace.AVOID_UPDATE, pm);
	}

	private boolean createChange() {
		return fCreateChangeOperation != null;
	}
}
