/*******************************************************************************
 * Copyright (c) 2005, 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
 *******************************************************************************/
package org.eclipse.ui.operations;

import java.util.ArrayList;
import org.eclipse.core.commands.operations.IAdvancedUndoableOperation;
import org.eclipse.core.commands.operations.IOperationApprover;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.WorkbenchMessages;

/**
 * <p>
 * An operation approver that prompts the user to see if a non-local undo should
 * proceed inside an editor. A non-local undo is detected when an operation
 * being undone or redone affects elements other than those described by the
 * editor itself. Clients can optionally specify a class, the preferred
 * comparison class, that should be used when comparing objects affected by the
 * editor with objects affected by an undo or redo operation. Comparisons
 * between the affected objects inside the editor and those described by the
 * operation will first be done by simply performing an equality check, using
 * {@link java.lang.Object#equals(java.lang.Object)}. If an object described by
 * an operation is not equal to one of the objects affected by the editor, and
 * if it is not an instance of the preferred comparison class, but is an
 * instance of {@link org.eclipse.core.runtime.IAdaptable}, then the operation
 * approver will also attempt to retrieve an adapter on that object for the
 * preferred comparison class and perform a second equality check using the
 * adapter.
 * </p>
 * <p>
 * This class may be instantiated by clients.
 * </p>
 *
 *
 * @since 3.1
 */
public final class NonLocalUndoUserApprover implements IOperationApprover {

	private IUndoContext context;

	private IEditorPart part;

	private Object[] elements;

	private Class<?> affectedObjectsClass;

	private ArrayList elementsAndAdapters;

	/**
	 * Create a NonLocalUndoUserApprover associated with the specified editor and
	 * undo context
	 *
	 * @param context                  the undo context of operations in question.
	 * @param part                     the editor part that is displaying the
	 *                                 element
	 * @param affectedObjects          the objects that are affected by the editor
	 *                                 and considered to be objects local to the
	 *                                 editor. The objects are typically instances
	 *                                 of the preferredComparisonClass or else
	 *                                 provide adapters for the
	 *                                 preferredComparisonClass, although this is
	 *                                 not required.
	 * @param preferredComparisonClass the preferred class to be used when comparing
	 *                                 the editor's affectedObjects with those
	 *                                 provided by the undoable operation using
	 *                                 {@link org.eclipse.core.commands.operations.IAdvancedUndoableOperation#getAffectedObjects()}.
	 *                                 If the operation's affected objects are not
	 *                                 instances of the specified class, but are
	 *                                 instances of
	 *                                 {@link org.eclipse.core.runtime.IAdaptable},
	 *                                 then an adapter for this class will be
	 *                                 requested. The preferredComparisonClass may
	 *                                 be <code>null</code>, which indicates that
	 *                                 there is no expected class or adapter
	 *                                 necessary for the comparison.
	 */
	public NonLocalUndoUserApprover(IUndoContext context, IEditorPart part, Object[] affectedObjects,
			Class<?> preferredComparisonClass) {
		super();
		this.context = context;
		this.part = part;
		this.affectedObjectsClass = preferredComparisonClass;
		this.elements = affectedObjects;
	}

	@Override
	public IStatus proceedRedoing(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) {

		// return immediately if the operation is not relevant
		if (!requiresApproval(operation, uiInfo)) {
			return Status.OK_STATUS;
		}

		String message = NLS.bind(WorkbenchMessages.Operations_nonLocalRedoWarning, operation.getLabel(),
				part.getEditorInput().getName());
		return proceedWithOperation(operation, message, WorkbenchMessages.Operations_discardRedo,
				WorkbenchMessages.Workbench_redoToolTip);
	}

	@Override
	public IStatus proceedUndoing(IUndoableOperation operation, IOperationHistory history, IAdaptable uiInfo) {

		// return immediately if the operation is not relevant
		if (!requiresApproval(operation, uiInfo)) {
			return Status.OK_STATUS;
		}

		String message = NLS.bind(WorkbenchMessages.Operations_nonLocalUndoWarning, operation.getLabel(),
				part.getEditorInput().getName());
		return proceedWithOperation(operation, message, WorkbenchMessages.Operations_discardUndo,
				WorkbenchMessages.Workbench_undoToolTip);

	}

	/*
	 * Determine whether the operation in question affects elements outside of the
	 * editor. If this can be determined and it does affect other elements, prompt
	 * the user as to whether the operation should proceed.
	 */
	private IStatus proceedWithOperation(IUndoableOperation operation, final String message, final String discardButton,
			final String title) {

		// if the operation cannot tell us about its modified elements, there's
		// nothing we can do.
		if (!(operation instanceof IAdvancedUndoableOperation)) {
			return Status.OK_STATUS;
		}

		// Obtain the operation's affected objects.
		Object[] modifiedElements = ((IAdvancedUndoableOperation) operation).getAffectedObjects();

		// Since the operation participates in describing its affected objects,
		// we assume for the rest of this method that an inability to
		// determine a match implies that a non-local operation is occurring.
		// This is a conservative assumption that provides more user prompting.

		boolean local;
		if (modifiedElements == null) {
			// The operation could not determine which elements are affected.
			// Consider the operation non-local.
			local = false;
		} else {
			// The operation answered some array of affected objects. Consider
			// the operation local until a non-match is found. Note that an
			// empty
			// array of affected objects is considered a local change.
			local = true;
			for (Object modifiedElement : modifiedElements) {
				if (!elementsContains(modifiedElement)) {
					// the modified element is not known by the editor
					local = false;
					// one last try - try to adapt the modified element if a
					// preferred
					// comparison class has been provided.
					if (affectedObjectsClass != null) {
						Object adapter = Adapters.adapt(modifiedElement, affectedObjectsClass);
						if (adapter != null && elementsContains(adapter)) {
							local = true;
						}
					}
					// if the element did not match the affected objects, no
					// need to check any others.
					if (!local) {
						break;
					}
				}
			}
		}
		if (local) {
			return Status.OK_STATUS;
		}

		// The operation affects more than just our element. Find out if
		// we should proceed, cancel, or discard the undo. Must be done in
		// a syncExec because operation approval notifications may come from
		// a background thread.
		final int[] answer = new int[1];
		PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
			MessageDialog dialog = new MessageDialog(part.getSite().getShell(), title, null, message,
					MessageDialog.QUESTION, 0, IDialogConstants.OK_LABEL, discardButton, IDialogConstants.CANCEL_LABEL); // yes
																															// is
																															// the
																															// default
			answer[0] = dialog.open();
		});
		switch (answer[0]) {
		case 0:
			return Status.OK_STATUS;
		case 1:
			return IOperationHistory.OPERATION_INVALID_STATUS;
		default:
			// Cancel by default to include ESC key and shell close,
			// which return SWT.DEFAULT, and any other unexpected return codes
			return Status.CANCEL_STATUS;
		}
	}

	/*
	 * Answer whether this operation is relevant enough to this operation approver
	 * that it should be examined in detail.
	 */
	private boolean requiresApproval(IUndoableOperation operation, IAdaptable uiInfo) {
		// no approval is required if the operation doesn't have our undo
		// context
		if (!(operation.hasContext(context))) {
			return false;
		}

		// no approval is required if the operation only has our context
		if (operation.getContexts().length == 1) {
			return false;
		}

		// no approval is required if we can ascertain that the operation did
		// not originate
		// in our context.
		if (uiInfo != null) {
			IUndoContext originatingContext = Adapters.adapt(uiInfo, IUndoContext.class);
			if (originatingContext != null && !(originatingContext.matches(context))) {
				return false;
			}
		}

		return true;
	}

	/*
	 * Return whether or not the collection of editor elements plus any of their
	 * adapters contains the specified object.
	 */
	private boolean elementsContains(Object someObject) {
		if (elements == null) {
			return false;
		}
		if (elementsAndAdapters == null) {
			// Compute a list of not just the elements, but any adapters they
			// may provide on the preferred class if they are not instances of
			// the preferred class. This is done only once.
			elementsAndAdapters = new ArrayList(elements.length);
			for (Object element : elements) {
				elementsAndAdapters.add(element);
				if (affectedObjectsClass != null && !affectedObjectsClass.isInstance(element)) {
					Object adapter = Adapters.adapt(element, affectedObjectsClass);
					if (adapter != null) {
						elementsAndAdapters.add(adapter);
					}
				}
			}
		}
		for (Object elementOrAdapter : elementsAndAdapters) {
			if (elementOrAdapter.equals(someObject)) {
				return true;
			}
		}
		return false;
	}
}
