/*******************************************************************************
 * Copyright (c) 2000, 2018 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
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.rename;

import java.lang.reflect.InvocationTargetException;

import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.internal.ui.refactoring.RefactoringExecutionHelper;
import org.eclipse.cdt.internal.ui.refactoring.RefactoringSaveHelper;
import org.eclipse.cdt.internal.ui.refactoring.RefactoringStarter;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringCore;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchWindow;

/**
 * Central access point to execute rename refactorings.
 */
public class RenameSupport {
	/** Flag indication that no additional update is to be performed. */
	public static final int NONE = 0;

	/** Flag indicating that references are to be updated as well. */
	public static final int UPDATE_REFERENCES = 1 << 0;

	/**
	 * Flag indicating that textual matches in comments and in string literals
	 * are to be updated as well.
	 */
	public static final int UPDATE_TEXTUAL_MATCHES = 1 << 6;

	/** Flag indicating that the getter method is to be updated as well. */
	public static final int UPDATE_GETTER_METHOD = 1 << 4;

	/** Flag indicating that the setter method is to be updated as well. */
	public static final int UPDATE_SETTER_METHOD = 1 << 5;

	/** @see #openDialog(Shell, CRenameRefactoring, DialogMode) */
	private enum DialogMode {
		ALL_PAGES, PREVIEW_ONLY, CONDITIONAL_PREVIEW
	}

	/** @see #openDialog(Shell, CRenameRefactoring, DialogMode) */
	private enum DialogResult {
		OK, CANCELED, SKIPPED
	}

	// Same as org.eclipse.ltk.internal.ui.refactoring.IErrorWizardPage#PAGE_NAME
	private static final String ERROR_PAGE_NAME = "ErrorPage"; //$NON-NLS-1$

	private CRenameRefactoring fRefactoring;
	private RefactoringStatus fPreCheckStatus;

	/**
	 * Executes some light weight precondition checking. If the returned status
	 * is an error then the refactoring can't be executed at all. However,
	 * returning an OK status doesn't guarantee that the refactoring can be
	 * executed. It may still fail while performing the exhaustive precondition
	 * checking done inside the methods <code>openDialog</code> or
	 * <code>perform</code>.
	 *
	 * The method is mainly used to determine enablement/disablement of actions.
	 *
	 * @return the result of the light weight precondition checking.
	 *
	 * @throws CoreException if an unexpected exception occurs while performing the checking.
	 *
	 * @see #openDialog(Shell)
	 * @see #perform(Shell, IWorkbenchWindow)
	 */
	public IStatus preCheck() throws CoreException {
		ensureChecked();
		if (fPreCheckStatus.hasFatalError())
			return fPreCheckStatus.getEntryMatchingSeverity(RefactoringStatus.FATAL).toStatus();
		else
			return Status.OK_STATUS;
	}

	/**
	 * Opens the refactoring dialog for this rename support.
	 *
	 * @param shell a shell used as a parent for the refactoring dialog.
	 * @throws CoreException if an unexpected exception occurs while opening the dialog.
	 *
	 * @see #openDialog(Shell, boolean)
	 */
	public boolean openDialog(Shell shell) throws CoreException {
		return openDialog(shell, false);
	}

	/**
	 * Opens the refactoring dialog for this rename support.
	 *
	 * <p>
	 * This method has to be called from within the UI thread.
	 * </p>
	 *
	 * @param shell a shell used as a parent for the refactoring, preview, or error dialog
	 * @param showPreviewOnly if <code>true</code>, the dialog skips all user input pages and
	 * directly shows the preview or error page. Otherwise, shows all pages.
	 * @return <code>true</code> if the refactoring has been executed successfully,
	 * <code>false</code> if it has been canceled or if an error has happened during
	 * initial conditions checking.
	 *
	 * @throws CoreException if an error occurred while executing the
	 * operation.
	 *
	 * @see #openDialog(Shell)
	 */
	public boolean openDialog(Shell shell, boolean showPreviewOnly) throws CoreException {
		ensureChecked();
		if (fPreCheckStatus.hasFatalError()) {
			showInformation(shell, fPreCheckStatus);
			return false;
		}

		DialogMode mode = showPreviewOnly ? DialogMode.PREVIEW_ONLY : DialogMode.ALL_PAGES;
		return openDialog(shell, fRefactoring, mode) == DialogResult.OK;
	}

	/**
	 * Opens the refactoring dialog for a given rename refactoring.
	 *
	 * @param shell a shell used as a parent for the refactoring dialog.
	 * @param refactoring the refactoring object.
	 *
	 * @see #openDialog(Shell, boolean)
	 */
	public static void openDialog(Shell shell, CRenameRefactoring refactoring) {
		openDialog(shell, refactoring, DialogMode.ALL_PAGES);
	}

	/**
	 * Opens the refactoring dialog.
	 *
	 * <p>
	 * This method has to be called from within the UI thread.
	 * </p>
	 *
	 * @param shell A shell used as a parent for the refactoring, preview, or error dialog
	 * @param refactoring The refactoring.
	 * @param mode One of DialogMode values. ALL_PAGES opens wizard with all pages shown;
	 *     PREVIEW_ONLY opens the preview page only; CONDITIONAL_PREVIEW opens the wizard with
	 *     preview page only and only if a warning was generated during the final conditions check.
	 * @return One of DialogResult values. OK is returned if the dialog was shown and
	 *     the refactoring change was applied; CANCELED is returned if the refactoring was
	 *     cancelled. SKIPPED is returned if the dialog was skipped in CONDITIONAL_PREVIEW mode and
	 *     the refactoring change has not been applied yet.
	 */
	static DialogResult openDialog(Shell shell, CRenameRefactoring refactoring, final DialogMode mode) {
		try {
			final boolean[] dialogSkipped = new boolean[1];
			CRenameRefactoringWizard wizard;
			if (mode == DialogMode.ALL_PAGES) {
				wizard = new CRenameRefactoringWizard(refactoring);
			} else {
				wizard = new CRenameRefactoringWizard(refactoring) {
					@Override
					protected void addUserInputPages() {
						// Nothing to add
					}

					@Override
					public IWizardPage getStartingPage() {
						IWizardPage startingPage = super.getStartingPage();
						if (mode == DialogMode.CONDITIONAL_PREVIEW && !startingPage.getName().equals(ERROR_PAGE_NAME)) {
							dialogSkipped[0] = true;
							return null;
						}
						return startingPage;
					}
				};
				wizard.setForcePreviewReview(mode != DialogMode.ALL_PAGES);
			}
			RefactoringStarter starter = new RefactoringStarter();
			CRenameProcessor processor = refactoring.getProcessor();
			processor.lockIndex();
			try {
				RefactoringStatus status = processor.checkInitialConditions(new NullProgressMonitor());
				if (status.hasFatalError()) {
					showInformation(shell, status);
					return DialogResult.CANCELED;
				}
				if (starter.activate(wizard, shell, RenameMessages.CRefactory_title_rename, processor.getSaveMode())) {
					return DialogResult.OK;
				}
				if (dialogSkipped[0]) {
					// If the dialog was not shown, it is our responsibility to close it.
					wizard.getContainer().getShell().close();
					return DialogResult.SKIPPED;
				}
			} finally {
				processor.unlockIndex();
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		} catch (CoreException e) {
			CUIPlugin.log(e);
		}
		return DialogResult.CANCELED;
	}

	/**
	 * Opens the refactoring dialog for a resource rename refactoring.
	 *
	 * <p>
	 * This method has to be called from within the UI thread.
	 * </p>
	 *
	 * @param shell a shell used as a parent for the refactoring dialog.
	 * @param resource selected resource to rename
	 *
	 * @see #openRenameResourceDialog(Shell, DialogMode, IResource)
	 */
	public static void openRenameResourceDialog(Shell shell, IResource resource) {
		openRenameResourceDialog(shell, DialogMode.ALL_PAGES, resource);
	}

	/**
	 * Opens the resource renaming refactoring dialog.
	 *
	 * <p>
	 * This method has to be called from within the UI thread.
	 * </p>
	 *
	 * @param shell A shell used as a parent for the refactoring, preview, or error dialog
	 * @param mode One of DialogMode values. ALL_PAGES opens wizard with all pages shown;
	 *     PREVIEW_ONLY opens the preview page only; CONDITIONAL_PREVIEW opens the wizard with
	 *     preview page only and only if a warning was generated during the final conditions check.
	 * @param resource selected resource to rename
	 * @return One of DialogResult values. OK is returned if the dialog was shown and
	 *     the refactoring change was applied; CANCELED is returned if the refactoring was
	 *     cancelled. SKIPPED is returned if the dialog was skipped in CONDITIONAL_PREVIEW mode and
	 *     the refactoring change has not been applied yet.
	 */
	static DialogResult openRenameResourceDialog(Shell shell, final DialogMode mode, IResource resource) {
		CResourceRenameRefactoringWizard wizard = new CResourceRenameRefactoringWizard(resource);
		RefactoringStarter starter = new RefactoringStarter();
		if (starter.activate(wizard, shell, RenameMessages.RenameSupport_rename_resource,
				RefactoringSaveHelper.SAVE_ALL)) {
			return DialogResult.OK;
		}

		return DialogResult.CANCELED;
	}

	/**
	 * Executes the rename refactoring without showing a dialog to gather
	 * additional user input (for example the new name of the <tt>ICElement</tt>).
	 * Only an error dialog is shown (if necessary) to present the result
	 * of the refactoring's full precondition checking.
	 * <p>
	 * The method has to be called from within the UI thread.
	 * </p>
	 *
	 * @param parent a shell used as a parent for the error dialog.
	 * @param context a {@link IRunnableContext} to execute the operation.
	 *
	 * @throws InterruptedException if the operation has been canceled by the
	 * user.
	 * @throws InvocationTargetException if an error occurred while executing the
	 * operation.
	 *
	 * @see #openDialog(Shell)
	 * @see IRunnableContext#run(boolean, boolean, org.eclipse.jface.operation.IRunnableWithProgress)
	 */
	public boolean perform(Shell parent, IWorkbenchWindow context)
			throws InterruptedException, InvocationTargetException {
		try {
			ensureChecked();
			if (fPreCheckStatus.hasFatalError()) {
				showInformation(parent, fPreCheckStatus);
				return false;
			}

			CRenameProcessor renameProcessor = fRefactoring.getProcessor();
			renameProcessor.lockIndex();
			try {
				fPreCheckStatus = renameProcessor.checkInitialConditions(new NullProgressMonitor());
				if (fPreCheckStatus.hasFatalError()) {
					showInformation(parent, fPreCheckStatus);
					return false;
				}
				DialogResult result = openDialog(context.getShell(), fRefactoring, DialogMode.CONDITIONAL_PREVIEW);
				switch (result) {
				case OK:
					return true;
				case SKIPPED:
					RefactoringExecutionHelper helper = new RefactoringExecutionHelper(fRefactoring,
							RefactoringCore.getConditionCheckingFailedSeverity(), renameProcessor.getSaveMode(), parent,
							context);
					Change change = fRefactoring.getChange();
					Assert.isNotNull(change);
					helper.performChange(change, true);
					return true;
				default:
					return false;
				}
			} finally {
				renameProcessor.unlockIndex();
			}
		} catch (CoreException e) {
			throw new InvocationTargetException(e);
		}
	}

	private RenameSupport(CRenameProcessor processor) {
		fRefactoring = new CRenameRefactoring(processor);
	}

	/**
	 * Creates a new rename support for the given {@link ICProject}.
	 *
	 * @param processor the {@link CRenameProcessor}
	 * @return the {@link RenameSupport}.
	 * @throws CoreException if an unexpected error occurred while creating
	 * the {@link RenameSupport}.
	 */
	public static RenameSupport create(CRenameProcessor processor) throws CoreException {
		return new RenameSupport(processor);
	}

	private void ensureChecked() throws CoreException {
		if (fPreCheckStatus == null) {
			if (!fRefactoring.isApplicable()) {
				fPreCheckStatus = RefactoringStatus.createFatalErrorStatus(RenameMessages.RenameSupport_not_available);
			} else {
				fPreCheckStatus = new RefactoringStatus();
			}
		}
	}

	private static void showInformation(Shell parent, RefactoringStatus status) {
		String message = status.getMessageMatchingSeverity(RefactoringStatus.FATAL);
		MessageDialog.openInformation(parent, RenameMessages.RenameSupport_dialog_title, message);
	}
}
