/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *     Nikolay Metchev <nikolaymetchev@gmail.com> - [introduce indirection] ClassCastException when introducing indirection on method in generic class - https://bugs.eclipse.org/395231
 *     Nikolay Metchev <nikolaymetchev@gmail.com> - [introduce indirection] Adds unneccessary import when inner class is used as parameter in surrounding class - https://bugs.eclipse.org/395228
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.code;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.resources.IFile;

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeParameter;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.TypeLocation;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.manipulation.CodeGeneration;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
import org.eclipse.jdt.core.refactoring.IJavaRefactorings;
import org.eclipse.jdt.core.refactoring.descriptors.IntroduceIndirectionDescriptor;
import org.eclipse.jdt.core.refactoring.descriptors.JavaRefactoringDescriptor;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchPattern;

import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.dom.ASTResolving;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.core.refactoring.descriptors.RefactoringSignatureDescriptorFactory;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.dom.BodyDeclarationRewrite;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.JDTRefactoringDescriptorComment;
import org.eclipse.jdt.internal.corext.refactoring.JavaRefactoringArguments;
import org.eclipse.jdt.internal.corext.refactoring.JavaRefactoringDescriptorUtil;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringAvailabilityTester;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringScopeFactory;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringSearchEngine;
import org.eclipse.jdt.internal.corext.refactoring.SearchResultGroup;
import org.eclipse.jdt.internal.corext.refactoring.changes.DynamicValidationRefactoringChange;
import org.eclipse.jdt.internal.corext.refactoring.rename.RippleMethodFinder2;
import org.eclipse.jdt.internal.corext.refactoring.structure.ASTNodeSearchUtil;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.refactoring.structure.MemberVisibilityAdjustor;
import org.eclipse.jdt.internal.corext.refactoring.structure.MemberVisibilityAdjustor.IncomingMemberVisibilityAdjustment;
import org.eclipse.jdt.internal.corext.refactoring.util.JavaStatusContext;
import org.eclipse.jdt.internal.corext.refactoring.util.ResourceUtil;
import org.eclipse.jdt.internal.corext.refactoring.util.TextChangeManager;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.JdtFlags;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.corext.util.MethodOverrideTester;

import org.eclipse.jdt.ui.JavaElementLabels;

import org.eclipse.jdt.internal.ui.JavaPlugin;

/**
 *
 * This refactoring creates a wrapper around a certain method and redirects callers of the original
 * method to the newly created method (called "intermediary method").
 *
 * If invoked on a method call, the user may select whether to only update the selected call or
 * all other calls as well. If invoked on a method declaration, the user may select whether to
 * update calls at all. An intermediary method will be created in both cases.
 *
 * Creating indirections is possible for both source and binary methods. Select an invocation of the method or
 * the declaring method itself, for example in the outline view.
 *
 * Note that in case of methods inside generic types, the parameters of the declaring type of the selected method
 * will be added to the method definition, rendering it generic as well.
 *
 * If any of the calls cannot see the intermediary method due to visibility problems with enclosing types
 * of the intermediary method, visibility will be adjusted. If the intermediary method is not able to
 * see the target method, this refactoring will try to adjust the visibility of the target method and
 * enclosing types as well. However, the latter is only possible if the target method is from source.
 *
 * @since 3.2
 *
 */
public class IntroduceIndirectionRefactoring extends Refactoring {

	/**
	 * The compilation unit in which the user invoked this refactoring (if any)
	 */
	private ICompilationUnit fSelectionCompilationUnit;
	/**
	 * The class file (with source) in which the user invoked this refactoring (if any)
	 */
	private IClassFile fSelectionClassFile;
	/**
	 * The start of the user selection inside the selected
	 * compilation unit (if any)
	 */
	private int fSelectionStart;
	/**
	 * The length of the user selection inside the selected
	 * compilation unit (if any)
	 */
	private int fSelectionLength;
	/**
	 * The selected MethodInvocation (if any). This field is used
	 * to update this particular invocation in non-reference mode.
	 */
	private MethodInvocation fSelectionMethodInvocation;

	// Intermediary information:

	/**
	 * The type in which to place the intermediary method
	 */
	private IType fIntermediaryType;
	/**
	 * The binding of the intermediary type
	 */
	private ITypeBinding fIntermediaryTypeBinding;
	/**
	 * The name of the intermediary method
	 */
	private String fIntermediaryMethodName;
	/**
	 * The type for the additional parameter for the intermediary. This
	 * type is determined from all known references.
	 */
	private ITypeBinding fIntermediaryFirstParameterType;

	// Target information:

	/**
	 * The originally selected target method (i.e., the one to be encapsulated)
	 */
	private IMethod fTargetMethod;
	/**
	 * The binding of the originally selected target method
	 */
	private IMethodBinding fTargetMethodBinding;

	// Other information:

	/**
	 * If true, all references to the target method are replaced with calls to
	 * the intermediary.
	 */
	private boolean fUpdateReferences;
	/**
	 * CompilationUnitRewrites for all affected cus
	 */
	private Map<ICompilationUnit, CompilationUnitRewrite> fRewrites;
	/**
	 * Text change manager (actually a CompilationUnitChange manager) which
	 * manages all changes.
	 */
	private TextChangeManager fTextChangeManager;

	// Visibility

	/**
	 * The visibility adjustor
	 */
	private MemberVisibilityAdjustor fAdjustor;
	/**
	 * Visibility adjustments for the intermediary
	 */
	private Map<IMember, IncomingMemberVisibilityAdjustment> fIntermediaryAdjustments;


	private static class NoOverrideProgressMonitor extends SubProgressMonitor {

		public NoOverrideProgressMonitor(IProgressMonitor monitor, int ticks) {
			super(monitor, ticks, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL);
		}

		@Override
		public void setTaskName(String name) {
			// do nothing
		}
	}

	// ********* CONSTRUCTORS AND CLASS CREATION ************

	public IntroduceIndirectionRefactoring(ICompilationUnit unit, int offset, int length) {
		fSelectionCompilationUnit= unit;
		initialize(offset, length);
	}

	public IntroduceIndirectionRefactoring(IClassFile file, int offset, int length) {
		fSelectionClassFile= file;
		initialize(offset, length);
	}

	public IntroduceIndirectionRefactoring(IMethod method) {
		fTargetMethod= method;
		initialize(0, 0);
	}

	public IntroduceIndirectionRefactoring(JavaRefactoringArguments arguments, RefactoringStatus status) {
   		this((ICompilationUnit) null, 0, 0);
   		RefactoringStatus initializeStatus= initialize(arguments);
   		status.merge(initializeStatus);
	}

	private void initialize(int offset, int length) {
		fSelectionStart= offset;
		fSelectionLength= length;
		fUpdateReferences= true;
	}

	// ********* UI INTERACTION AND STARTUP OPTIONS ************

	@Override
	public String getName() {
		return RefactoringCoreMessages.IntroduceIndirectionRefactoring_introduce_indirection_name;
	}

	public IJavaProject getProject() {
		if (fSelectionCompilationUnit != null)
			return fSelectionCompilationUnit.getJavaProject();
		if (fSelectionClassFile != null)
			return fSelectionClassFile.getJavaProject();
		if (fTargetMethod != null)
			return fTargetMethod.getJavaProject();
		return null;
	}

	public IPackageFragment getInvocationPackage() {
		return fSelectionCompilationUnit != null ? (IPackageFragment) fSelectionCompilationUnit.getAncestor(IJavaElement.PACKAGE_FRAGMENT) : null;
	}

	public boolean canEnableUpdateReferences() {
		return true;
	}

	public void setEnableUpdateReferences(boolean updateReferences) {
		fUpdateReferences= updateReferences;
	}

	public RefactoringStatus setIntermediaryMethodName(String newMethodName) {
		Assert.isNotNull(newMethodName);
		fIntermediaryMethodName= newMethodName;
		IJavaElement context= fIntermediaryType != null ? fIntermediaryType : (IMember) fTargetMethod;
		RefactoringStatus stat= Checks.checkMethodName(newMethodName, context);
		stat.merge(checkOverloading());
		return stat;
	}

	private RefactoringStatus checkOverloading() {
		try {
			if (fIntermediaryType != null) {
				IMethod[] toCheck= fIntermediaryType.getMethods();
				for (IMethod method : toCheck) {
					if (method.getElementName().equals(fIntermediaryMethodName))
						return RefactoringStatus.createWarningStatus(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_duplicate_method_name_in_declaring_type_error,
							BasicElementLabels.getJavaElementName(fIntermediaryMethodName)));
				}
			}
		} catch (JavaModelException e) {
			return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_could_not_parse_declaring_type_error);
		}
		return new RefactoringStatus();
	}

	public String getIntermediaryMethodName() {
		return fIntermediaryMethodName;
	}

	/**
	 * @param fullyQualifiedTypeName the fully qualified name of the intermediary method
	 * @return status for type name. Use {@link #setIntermediaryMethodName(String)} to check for overridden methods.
	 */
	public RefactoringStatus setIntermediaryTypeName(String fullyQualifiedTypeName) {
		IType target= null;

		try {
			if (fullyQualifiedTypeName.length() == 0)
				return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_type_not_selected_error);

			// find type (now including secondaries)
			target= getProject().findType(fullyQualifiedTypeName, new NullProgressMonitor());
			if (target == null || !target.exists())
				return RefactoringStatus.createErrorStatus(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_type_does_not_exist_error, BasicElementLabels.getJavaElementName(fullyQualifiedTypeName)));
			if (target.isAnnotation())
				return RefactoringStatus.createErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_create_in_annotation);
			if (target.isInterface()
					&& (!JavaModelUtil.is1d8OrHigher(target.getJavaProject()) || !JavaModelUtil.is1d8OrHigher(getProject())))
				return RefactoringStatus.createErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_create_on_interface);
		} catch (JavaModelException e) {
			return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_unable_determine_declaring_type);
		}

		if (target.isReadOnly())
			return RefactoringStatus.createErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_create_in_readonly);

		if (target.isBinary())
			return RefactoringStatus.createErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_create_in_binary);

		fIntermediaryType= target;

		return new RefactoringStatus();
	}

	/**
	 * Returns the type name of the intermediary type, or the empty string if none has been set yet.
	 * @return the intermediary type name or the empty string
	 */
	public String getIntermediaryTypeName() {
		return fIntermediaryType != null ? fIntermediaryType.getFullyQualifiedName('.') : ""; //$NON-NLS-1$
	}

	// ********** CONDITION CHECKING **********

	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		try {
			pm.beginTask(RefactoringCoreMessages.IntroduceIndirectionRefactoring_checking_activation, 1);
			fRewrites= new HashMap<>();

			// This refactoring has been invoked on
			// (1) a TextSelection inside an ICompilationUnit or inside an IClassFile (definitely with source), or
			// (2) an IMethod inside a ICompilationUnit or inside an IClassFile (with or without source)

			if (fTargetMethod == null) {
				// (1) invoked on a text selection

				if (fSelectionStart == 0)
					return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_on_this_selection);

				// if a text selection exists, source is available.
				CompilationUnit selectionCURoot;
				ASTNode selectionNode;
				if (fSelectionCompilationUnit != null) {
					// compilation unit - could use CuRewrite later on
					selectionCURoot= getCachedCURewrite(fSelectionCompilationUnit).getRoot();
					selectionNode= getSelectedNode(fSelectionCompilationUnit, selectionCURoot, fSelectionStart, fSelectionLength);
				} else {
					// binary class file - no cu rewrite
					ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
					parser.setResolveBindings(true);
					parser.setSource(fSelectionClassFile);
					selectionCURoot= (CompilationUnit) parser.createAST(null);
					selectionNode= getSelectedNode(null, selectionCURoot, fSelectionStart, fSelectionLength);
				}

				if (selectionNode == null)
					return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_on_this_selection);

				IMethodBinding targetMethodBinding= null;

				if (selectionNode.getNodeType() == ASTNode.METHOD_INVOCATION) {
					targetMethodBinding= ((MethodInvocation) selectionNode).resolveMethodBinding();
				} else if (selectionNode.getNodeType() == ASTNode.METHOD_DECLARATION) {
					targetMethodBinding= ((MethodDeclaration) selectionNode).resolveBinding();
				} else if (selectionNode.getNodeType() == ASTNode.SUPER_METHOD_INVOCATION) {
					// Allow invocation on super methods calls. makes sense as other
					// calls or even only the declaration can be updated.
					targetMethodBinding= ((SuperMethodInvocation) selectionNode).resolveMethodBinding();
				}
				if (targetMethodBinding != null) {
					fTargetMethodBinding= targetMethodBinding.getMethodDeclaration(); // resolve generics
					if (fTargetMethodBinding != null) {
						fTargetMethod= (IMethod) fTargetMethodBinding.getJavaElement();
						//allow single updating mode if an invocation was selected and the invocation can be updated
						if (selectionNode instanceof MethodInvocation && fSelectionCompilationUnit != null)
							fSelectionMethodInvocation= (MethodInvocation) selectionNode;
					}
				}
				if (targetMethodBinding == null || fTargetMethodBinding == null) {
					return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_on_this_selection);
				}
			} else {
				// (2) invoked on an IMethod: Source may not be available

				if (fTargetMethod.getDeclaringType().isAnnotation())
					return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_on_annotation);

				if (fTargetMethod.getCompilationUnit() != null) {
					// source method
					CompilationUnit selectionCURoot= getCachedCURewrite(fTargetMethod.getCompilationUnit()).getRoot();
					MethodDeclaration declaration= ASTNodeSearchUtil.getMethodDeclarationNode(fTargetMethod, selectionCURoot);
					fTargetMethodBinding= declaration.resolveBinding().getMethodDeclaration();
				} else {
					// binary method - no CURewrite available (and none needed as we cannot update the method anyway)
					ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
					parser.setProject(fTargetMethod.getJavaProject());
					IBinding[] bindings= parser.createBindings(new IJavaElement[] { fTargetMethod }, null);
					fTargetMethodBinding= ((IMethodBinding) bindings[0]).getMethodDeclaration();
				}
			}

			if (fTargetMethod == null || fTargetMethodBinding == null || (!RefactoringAvailabilityTester.isIntroduceIndirectionAvailable(fTargetMethod)))
				return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_on_this_selection);

			if (fTargetMethod.getDeclaringType().isLocal() || fTargetMethod.getDeclaringType().isAnonymous())
				return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_for_local_or_anonymous_types);

			if (fTargetMethod.isConstructor())
				return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_not_available_for_constructors);

			if (fIntermediaryMethodName == null)
				fIntermediaryMethodName= fTargetMethod.getElementName();

			if (fIntermediaryType == null) {
				if (fSelectionCompilationUnit != null && !fSelectionCompilationUnit.isReadOnly())
					fIntermediaryType= getEnclosingInitialSelectionMember().getDeclaringType();
				else if (!fTargetMethod.isBinary() && !fTargetMethod.isReadOnly())
					fIntermediaryType= fTargetMethod.getDeclaringType();
			}

			return new RefactoringStatus();
		} finally {
			pm.done();
		}
	}

	@Override
	public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException {

		RefactoringStatus result= new RefactoringStatus();
		fTextChangeManager= new TextChangeManager();
		fIntermediaryFirstParameterType= null;
		fIntermediaryTypeBinding= null;
		for (CompilationUnitRewrite compilationUnitRewrite : fRewrites.values())
			compilationUnitRewrite.clearASTAndImportRewrites();

		int startupTicks= 5;
		int hierarchyTicks= 5;
		int visibilityTicks= 5;
		int referenceTicks= fUpdateReferences ? 30 : 5;
		int creationTicks= 5;

		pm.beginTask("", startupTicks + hierarchyTicks + visibilityTicks + referenceTicks + creationTicks); //$NON-NLS-1$
		pm.setTaskName(RefactoringCoreMessages.IntroduceIndirectionRefactoring_checking_conditions);

		result.merge(Checks.checkMethodName(fIntermediaryMethodName, fIntermediaryType));
		if (result.hasFatalError())
			return result;

		if (fIntermediaryType == null)
			return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_run_without_intermediary_type);

		// intermediary type is already non binary/non-enum
		CompilationUnitRewrite imRewrite= getCachedCURewrite(fIntermediaryType.getCompilationUnit());
		fIntermediaryTypeBinding= typeToBinding(fIntermediaryType, imRewrite.getRoot());

		fAdjustor= new MemberVisibilityAdjustor(fIntermediaryType, fIntermediaryType);
		fIntermediaryAdjustments= new HashMap<>();

		// check static method in non-static nested type
		if (fIntermediaryTypeBinding.isNested() && !Modifier.isStatic(fIntermediaryTypeBinding.getModifiers()))
			return RefactoringStatus.createFatalErrorStatus(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_create_in_nested_nonstatic, JavaStatusContext.create(fIntermediaryType));

		pm.worked(startupTicks);
		if (pm.isCanceled())
			throw new OperationCanceledException();

		if (fUpdateReferences) {
			pm.setTaskName(RefactoringCoreMessages.IntroduceIndirectionRefactoring_checking_conditions + " " + RefactoringCoreMessages.IntroduceIndirectionRefactoring_looking_for_references); //$NON-NLS-1$
			result.merge(updateReferences(new NoOverrideProgressMonitor(pm, referenceTicks)));
			pm.setTaskName(RefactoringCoreMessages.IntroduceIndirectionRefactoring_checking_conditions);
		} else {
			// only update the declaration and/or a selected method invocation
			if (fSelectionMethodInvocation != null) {
				fIntermediaryFirstParameterType= getExpressionType(fSelectionMethodInvocation);
				final IMember enclosing= getEnclosingInitialSelectionMember();
				// create an edit for this particular call
				result.merge(updateMethodInvocation(fSelectionMethodInvocation, enclosing, getCachedCURewrite(fSelectionCompilationUnit)));

				if (!isRewriteKept(fSelectionCompilationUnit))
					createChangeAndDiscardRewrite(fSelectionCompilationUnit);

				// does call see the intermediary method?
				// => increase visibility of the type of the intermediary method.
				result.merge(adjustVisibility(fIntermediaryType, enclosing.getDeclaringType(), new NoOverrideProgressMonitor(pm, 0)));
			}
			pm.worked(referenceTicks);
		}

		if (pm.isCanceled())
			throw new OperationCanceledException();

		if (fIntermediaryFirstParameterType == null)
			fIntermediaryFirstParameterType= fTargetMethodBinding.getDeclaringClass();

		// The target type and method may have changed - update them

		IType actualTargetType= (IType) fIntermediaryFirstParameterType.getJavaElement();
		if (!fTargetMethod.getDeclaringType().equals(actualTargetType)) {
			IMethod actualTargetMethod= new MethodOverrideTester(actualTargetType, actualTargetType.newSupertypeHierarchy(null)).findOverriddenMethodInHierarchy(actualTargetType, fTargetMethod);
			fTargetMethod= actualTargetMethod;
			fTargetMethodBinding= findMethodBindingInHierarchy(fIntermediaryFirstParameterType, actualTargetMethod);
			Assert.isNotNull(fTargetMethodBinding);
		}

		result.merge(checkCanCreateIntermediaryMethod());
		createIntermediaryMethod();
		pm.worked(creationTicks);

		pm.setTaskName(RefactoringCoreMessages.IntroduceIndirectionRefactoring_checking_conditions + " " + RefactoringCoreMessages.IntroduceIndirectionRefactoring_adjusting_visibility); //$NON-NLS-1$
		result.merge(updateTargetVisibility(new NoOverrideProgressMonitor(pm, 0)));
		result.merge(updateIntermediaryVisibility(new NoOverrideProgressMonitor(pm, 0)));
		pm.worked(visibilityTicks);
		pm.setTaskName(RefactoringCoreMessages.IntroduceIndirectionRefactoring_checking_conditions);

		createChangeAndDiscardRewrite(fIntermediaryType.getCompilationUnit());

		result.merge(Checks.validateModifiesFiles(getAllFilesToModify(), getValidationContext(), pm));
		pm.done();

		return result;
	}

	private RefactoringStatus updateTargetVisibility(IProgressMonitor monitor) throws JavaModelException, CoreException {

		RefactoringStatus result= new RefactoringStatus();

		// Adjust the visibility of the method and of the referenced type. Note that
		// the target method may not be in the target type; and in this case, the type
		// of the target method does not need a visibility adjustment.

		// This method is called after all other changes have been
		// created. Changes induced by this method will be attached to those changes.

		result.merge(adjustVisibility((IType) fIntermediaryFirstParameterType.getJavaElement(), fIntermediaryType, monitor));
		if (result.hasError())
			return result; // binary

		ModifierKeyword neededVisibility= getNeededVisibility(fTargetMethod, fIntermediaryType);

		result.merge(adjustVisibility(fTargetMethod, neededVisibility, monitor));
		if (result.hasError())
			return result; // binary

		// Need to adjust the overridden methods of the target method.
		ITypeHierarchy hierarchy= fTargetMethod.getDeclaringType().newTypeHierarchy(null);
		MethodOverrideTester tester= new MethodOverrideTester(fTargetMethod.getDeclaringType(), hierarchy);
		for (IType subtype : hierarchy.getAllSubtypes(fTargetMethod.getDeclaringType())) {
			IMethod method= tester.findOverridingMethodInType(subtype, fTargetMethod);
			if (method != null && method.exists()) {
				result.merge(adjustVisibility(method, neededVisibility, monitor));
				if (monitor.isCanceled())
					throw new OperationCanceledException();

				if (result.hasError())
					return result; // binary
			}
		}

		return result;
	}

	private RefactoringStatus updateIntermediaryVisibility(NoOverrideProgressMonitor monitor) throws JavaModelException {
		return rewriteVisibility(fIntermediaryAdjustments, fRewrites, monitor);
	}

	private RefactoringStatus updateReferences(IProgressMonitor monitor) throws CoreException {

		RefactoringStatus result= new RefactoringStatus();

		monitor.beginTask("", 90); //$NON-NLS-1$

		if (monitor.isCanceled())
			throw new OperationCanceledException();

		IMethod[] ripple= RippleMethodFinder2.getRelatedMethods(fTargetMethod, false, new NoOverrideProgressMonitor(monitor, 10), null);

		if (monitor.isCanceled())
			throw new OperationCanceledException();

		SearchResultGroup[] references= Checks.excludeCompilationUnits(getReferences(ripple, new NoOverrideProgressMonitor(monitor, 10), result), result);

		if (result.hasFatalError())
			return result;

		result.merge(Checks.checkCompileErrorsInAffectedFiles(references));

		if (monitor.isCanceled())
			throw new OperationCanceledException();

		int ticksPerCU= references.length == 0 ? 0 : 70 / references.length;

		for (SearchResultGroup group : references) {
			SearchMatch[] searchResults= group.getSearchResults();
			CompilationUnitRewrite currentCURewrite= getCachedCURewrite(group.getCompilationUnit());
			for (SearchMatch match : searchResults) {
				if (match.isInsideDocComment())
					continue;

				IMember enclosingMember= (IMember) match.getElement();
				ASTNode target= getSelectedNode(group.getCompilationUnit(), currentCURewrite.getRoot(), match.getOffset(), match.getLength());

				if (target instanceof SuperMethodInvocation) {
					// Cannot retarget calls to super - add a warning
					result.merge(createWarningAboutCall(enclosingMember, target, RefactoringCoreMessages.IntroduceIndirectionRefactoring_call_warning_super_keyword));
					continue;
				}

				Assert.isTrue(target instanceof MethodInvocation, "Element of call should be a MethodInvocation."); //$NON-NLS-1$

				MethodInvocation invocation= (MethodInvocation) target;
				ITypeBinding typeBinding= getExpressionType(invocation);

				if (fIntermediaryFirstParameterType == null) {
					// no highest type yet
					fIntermediaryFirstParameterType= typeBinding.getTypeDeclaration();
				} else {
					// check if current type is higher
					result.merge(findCommonParent(typeBinding.getTypeDeclaration()));
				}

				if (result.hasFatalError())
					return result;

				// create an edit for this particular call
				result.merge(updateMethodInvocation(invocation, enclosingMember, currentCURewrite));

				// does call see the intermediary method?
				// => increase visibility of the type of the intermediary method.
				result.merge(adjustVisibility(fIntermediaryType, enclosingMember.getDeclaringType(), new NoOverrideProgressMonitor(monitor, 0)));

				if (monitor.isCanceled())
					throw new OperationCanceledException();
			}
			if (!isRewriteKept(group.getCompilationUnit()))
				createChangeAndDiscardRewrite(group.getCompilationUnit());
			monitor.worked(ticksPerCU);
		}

		monitor.done();
		return result;
	}

	private RefactoringStatus findCommonParent(ITypeBinding typeBinding) {

		RefactoringStatus status= new RefactoringStatus();

		ITypeBinding highest= fIntermediaryFirstParameterType;
		ITypeBinding current= typeBinding;

		if (current.equals(highest) || Bindings.isSuperType(highest, current))
			// current is the same as highest or highest is already a supertype of current in the same hierarchy => no change
			return status;

		// find lowest common supertype with the method
		// search in bottom-up order
		ITypeBinding[] currentAndSupers= getTypeAndAllSuperTypes(current);
		ITypeBinding[] highestAndSupers= getTypeAndAllSuperTypes(highest);

		ITypeBinding foundBinding= null;
		for (ITypeBinding currentAndSuper : currentAndSupers) {
			for (ITypeBinding highestAndSuper : highestAndSupers) {
				if (highestAndSuper.isEqualTo(currentAndSuper) && (Bindings.findMethodInHierarchy(highestAndSuper, fTargetMethodBinding.getName(), fTargetMethodBinding.getParameterTypes()) != null)) {
					foundBinding= highestAndSuper;
					break;
				}
			}
			if (foundBinding != null)
				break;
		}

		if (foundBinding != null) {
			fIntermediaryFirstParameterType= foundBinding;
		} else {
			String type1= BasicElementLabels.getJavaElementName(fIntermediaryFirstParameterType.getQualifiedName());
			String type2= BasicElementLabels.getJavaElementName(current.getQualifiedName());
			status.addFatalError(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_open_hierarchy_error, new String[] { type1, type2 }));
		}

		return status;
	}

	// ******************** CHANGE CREATION ***********************

	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		final Map<String, String> arguments= new HashMap<>();
		String project= null;
		IJavaProject javaProject= fTargetMethod.getJavaProject();
		if (javaProject != null)
			project= javaProject.getElementName();
		int flags= JavaRefactoringDescriptor.JAR_MIGRATION | JavaRefactoringDescriptor.JAR_REFACTORING | RefactoringDescriptor.STRUCTURAL_CHANGE | RefactoringDescriptor.MULTI_CHANGE;
		final IType declaring= fTargetMethod.getDeclaringType();
		try {
			if (declaring.isLocal() || declaring.isAnonymous())
				flags|= JavaRefactoringDescriptor.JAR_SOURCE_ATTACHMENT;
		} catch (JavaModelException exception) {
			JavaPlugin.log(exception);
		}
		final String description= Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_descriptor_description_short, BasicElementLabels.getJavaElementName(fTargetMethod.getElementName()));
		final String header= Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_descriptor_description, new String[] { JavaElementLabels.getTextLabel(fTargetMethod, JavaElementLabels.ALL_FULLY_QUALIFIED), JavaElementLabels.getTextLabel(declaring, JavaElementLabels.ALL_FULLY_QUALIFIED)});
		final JDTRefactoringDescriptorComment comment= new JDTRefactoringDescriptorComment(project, this, header);
		comment.addSetting(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_original_pattern, JavaElementLabels.getTextLabel(fTargetMethod, JavaElementLabels.ALL_FULLY_QUALIFIED)));
		comment.addSetting(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_method_pattern, BasicElementLabels.getJavaElementName(fIntermediaryMethodName)));
		comment.addSetting(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_declaring_pattern, JavaElementLabels.getTextLabel(fIntermediaryType, JavaElementLabels.ALL_FULLY_QUALIFIED)));
		if (fUpdateReferences)
			comment.addSetting(RefactoringCoreMessages.JavaRefactoringDescriptor_update_references);
		final IntroduceIndirectionDescriptor descriptor= RefactoringSignatureDescriptorFactory.createIntroduceIndirectionDescriptor(project, description, comment.asString(), arguments, flags);
		arguments.put(JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT, JavaRefactoringDescriptorUtil.elementToHandle(project, fTargetMethod));
		arguments.put(JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME, fIntermediaryMethodName);
		arguments.put(JavaRefactoringDescriptorUtil.ATTRIBUTE_ELEMENT + 1, JavaRefactoringDescriptorUtil.elementToHandle(project, fIntermediaryType));
		arguments.put(JavaRefactoringDescriptorUtil.ATTRIBUTE_REFERENCES, Boolean.toString(fUpdateReferences));
		return new DynamicValidationRefactoringChange(descriptor, RefactoringCoreMessages.IntroduceIndirectionRefactoring_introduce_indirection, fTextChangeManager.getAllChanges());
	}

	// ******************* CREATE INTERMEDIARY **********************

	/**
	 * Checks whether the target method can be created. Note that this
	 * can only be done after fDelegateParameterType has been initialized.
	 * @return resulting status
	 * @throws JavaModelException should not happen
	 */
	private RefactoringStatus checkCanCreateIntermediaryMethod() throws JavaModelException {
		// check if method already exists:
		List<ITypeBinding> parameterBindings= new ArrayList<>();
		if (!isStaticTarget())
			parameterBindings.add(fIntermediaryFirstParameterType);
		parameterBindings.addAll(Arrays.asList(fTargetMethodBinding.getParameterTypes()));
		return Checks.checkMethodInType(fIntermediaryTypeBinding, fIntermediaryMethodName, parameterBindings.toArray(new ITypeBinding[parameterBindings.size()]));
	}

	private void createIntermediaryMethod() throws CoreException {

		CompilationUnitRewrite imRewrite= getCachedCURewrite(fIntermediaryType.getCompilationUnit());
		AST ast= imRewrite.getAST();
		MethodDeclaration intermediary= ast.newMethodDeclaration();

		// Intermediary class is non-anonymous
		AbstractTypeDeclaration type= (AbstractTypeDeclaration)typeToDeclaration(fIntermediaryType, imRewrite.getRoot());

		// Name
		intermediary.setName(ast.newSimpleName(fIntermediaryMethodName));

		// Flags
		List<IExtendedModifier> modifiers= intermediary.modifiers();
		if (!fIntermediaryType.isInterface()) {
			modifiers.add(imRewrite.getAST().newModifier(ModifierKeyword.PUBLIC_KEYWORD));
		}
		modifiers.add(imRewrite.getAST().newModifier(ModifierKeyword.STATIC_KEYWORD));

		// Parameters
		String targetParameterName= StubUtility.suggestArgumentName(getProject(), fIntermediaryFirstParameterType.getName(), fTargetMethod.getParameterNames());

		ImportRewriteContext context= new ContextSensitiveImportRewriteContext(type, imRewrite.getImportRewrite());
		if (!isStaticTarget()) {
			// Add first param
			SingleVariableDeclaration parameter= imRewrite.getAST().newSingleVariableDeclaration();
			Type t= imRewrite.getImportRewrite().addImport(fIntermediaryFirstParameterType, imRewrite.getAST(), context, TypeLocation.PARAMETER);
			if (fIntermediaryFirstParameterType.isGenericType()) {
				ParameterizedType parameterized= imRewrite.getAST().newParameterizedType(t);
				for (ITypeBinding typeParameter : fIntermediaryFirstParameterType.getTypeParameters()) {
					parameterized.typeArguments().add(imRewrite.getImportRewrite().addImport(typeParameter, imRewrite.getAST()));
				}
				t= parameterized;
			}
			parameter.setType(t);
			parameter.setName(imRewrite.getAST().newSimpleName(targetParameterName));
			intermediary.parameters().add(parameter);
		}
		// Add other params
		copyArguments(intermediary, imRewrite, context);

		// Add type parameters of declaring type (and enclosing types)
		if (!isStaticTarget() && fIntermediaryFirstParameterType.isGenericType())
			addTypeParameters(imRewrite, intermediary.typeParameters(), fIntermediaryFirstParameterType, context);

		// Add type params of method
		copyTypeParameters(intermediary, imRewrite, context);

		// Return type
		intermediary.setReturnType2(imRewrite.getImportRewrite().addImport(fTargetMethodBinding.getReturnType(), ast, context, TypeLocation.RETURN_TYPE));

		// Exceptions
		copyExceptions(intermediary, imRewrite, context);

		// Body
		MethodInvocation invocation= imRewrite.getAST().newMethodInvocation();
		invocation.setName(imRewrite.getAST().newSimpleName(fTargetMethod.getElementName()));
		if (isStaticTarget()) {
			Type importedType= imRewrite.getImportRewrite().addImport(fTargetMethodBinding.getDeclaringClass(), ast, context, TypeLocation.OTHER);
			invocation.setExpression(ASTNodeFactory.newName(ast, ASTNodes.asString(importedType)));
		} else {
			invocation.setExpression(imRewrite.getAST().newSimpleName(targetParameterName));
		}
		copyInvocationParameters(invocation, ast);
		Statement call= encapsulateInvocation(intermediary, invocation);

		final Block body= imRewrite.getAST().newBlock();
		body.statements().add(call);
		intermediary.setBody(body);

		// method comment
		ICompilationUnit targetCU= imRewrite.getCu();
		if (StubUtility.doAddComments(targetCU.getJavaProject())) {
			String comment= CodeGeneration.getMethodComment(targetCU, getIntermediaryTypeName(), intermediary, null, StubUtility.getLineDelimiterUsed(targetCU));
			if (comment != null) {
				Javadoc javadoc= (Javadoc) imRewrite.getASTRewrite().createStringPlaceholder(comment, ASTNode.JAVADOC);
				intermediary.setJavadoc(javadoc);
			}
		}

		// Add the completed method to the intermediary type:
		ChildListPropertyDescriptor typeBodyDeclarationsProperty= typeToBodyDeclarationProperty(fIntermediaryType, imRewrite.getRoot());

		ListRewrite bodyDeclarationsListRewrite= imRewrite.getASTRewrite().getListRewrite(type, typeBodyDeclarationsProperty);
		bodyDeclarationsListRewrite.insertAt(intermediary, BodyDeclarationRewrite.getInsertionIndex(intermediary, type.bodyDeclarations()), imRewrite
				.createGroupDescription(RefactoringCoreMessages.IntroduceIndirectionRefactoring_group_description_create_new_method));
	}

	private void addTypeParameters(CompilationUnitRewrite imRewrite, List<TypeParameter> list, ITypeBinding parent, ImportRewriteContext context) {

		ITypeBinding enclosing= parent.getDeclaringClass();
		if (enclosing != null)
			addTypeParameters(imRewrite, list, enclosing, context);

		for (ITypeBinding typeParameter : parent.getTypeParameters()) {
			TypeParameter ntp= imRewrite.getAST().newTypeParameter();
			ntp.setName(imRewrite.getAST().newSimpleName(typeParameter.getName()));
			for (ITypeBinding bound : typeParameter.getTypeBounds()) {
				if (!"java.lang.Object".equals(bound.getQualifiedName())) { //$NON-NLS-1$
					ntp.typeBounds().add(imRewrite.getImportRewrite().addImport(bound, imRewrite.getAST(), context, TypeLocation.TYPE_BOUND));
				}
			}
			list.add(ntp);
		}
	}

	private Statement encapsulateInvocation(MethodDeclaration declaration, MethodInvocation invocation) {
		final Type type= declaration.getReturnType2();

		if (type == null || (type instanceof PrimitiveType && PrimitiveType.VOID.equals( ((PrimitiveType) type).getPrimitiveTypeCode())))
			return invocation.getAST().newExpressionStatement(invocation);

		ReturnStatement statement= invocation.getAST().newReturnStatement();
		statement.setExpression(invocation);
		return statement;
	}

	private void copyInvocationParameters(MethodInvocation invocation, AST ast) throws JavaModelException {
		for (String name : fTargetMethod.getParameterNames()) {
			invocation.arguments().add(ast.newSimpleName(name));
		}
	}

	private void copyArguments(MethodDeclaration intermediary, CompilationUnitRewrite rew, ImportRewriteContext context) throws JavaModelException {
		String[] names= fTargetMethod.getParameterNames();
		ITypeBinding[] types= fTargetMethodBinding.getParameterTypes();
		for (int i= 0; i < names.length; i++) {
			ITypeBinding typeBinding= types[i];
			SingleVariableDeclaration newElement= rew.getAST().newSingleVariableDeclaration();
			newElement.setName(rew.getAST().newSimpleName(names[i]));

			if (i == (names.length - 1) && fTargetMethodBinding.isVarargs()) {
				newElement.setVarargs(true);
				if (typeBinding.isArray())
					typeBinding= typeBinding.getComponentType();
			}

			newElement.setType(rew.getImportRewrite().addImport(typeBinding, rew.getAST(), context, TypeLocation.PARAMETER));
			intermediary.parameters().add(newElement);
		}
	}

	private void copyTypeParameters(MethodDeclaration intermediary, CompilationUnitRewrite rew,  ImportRewriteContext context) {
		for (ITypeBinding current : fTargetMethodBinding.getTypeParameters()) {
			TypeParameter parameter= rew.getAST().newTypeParameter();
			parameter.setName(rew.getAST().newSimpleName(current.getName()));
			for (ITypeBinding bound : current.getTypeBounds()) {
				if (!"java.lang.Object".equals(bound.getQualifiedName())) { //$NON-NLS-1$
					parameter.typeBounds().add(rew.getImportRewrite().addImport(bound, rew.getAST(), context, TypeLocation.TYPE_BOUND));
				}
			}
			intermediary.typeParameters().add(parameter);
		}
	}

	private void copyExceptions(MethodDeclaration intermediary, CompilationUnitRewrite imRewrite,  ImportRewriteContext context) {
		for (ITypeBinding typebinding : fTargetMethodBinding.getExceptionTypes()) {
			Type exceptionType= imRewrite.getImportRewrite().addImport(typebinding, imRewrite.getAST(), context, TypeLocation.EXCEPTION);
			intermediary.thrownExceptionTypes().add(exceptionType);
		}
	}

	// ******************* UPDATE CALLS **********************

	private RefactoringStatus updateMethodInvocation(MethodInvocation originalInvocation, IMember enclosing, CompilationUnitRewrite unitRewriter) throws JavaModelException {

		RefactoringStatus status= new RefactoringStatus();

		// If the method invocation utilizes type arguments, skip this
		// call as the new target method may have additional parameters
		if (originalInvocation.typeArguments().size() > 0)
			return createWarningAboutCall(enclosing, originalInvocation, RefactoringCoreMessages.IntroduceIndirectionRefactoring_call_warning_type_arguments);

		MethodInvocation newInvocation= unitRewriter.getAST().newMethodInvocation();
		List<Expression> newInvocationArgs= newInvocation.arguments();
		List<Expression> originalInvocationArgs= originalInvocation.arguments();

		ContextSensitiveImportRewriteContext context= new ContextSensitiveImportRewriteContext(originalInvocation, unitRewriter.getImportRewrite());
		// static call => always use a qualifier
		String qualifier= unitRewriter.getImportRewrite().addImport(fIntermediaryTypeBinding, context);
		newInvocation.setExpression(ASTNodeFactory.newName(unitRewriter.getAST(), qualifier));
		newInvocation.setName(unitRewriter.getAST().newSimpleName(getIntermediaryMethodName()));

		final Expression expression= originalInvocation.getExpression();

		if (!isStaticTarget()) {
			// Add the expression as the first parameter
			if (expression == null) {
				// There is no expression for this call. Use a (possibly qualified) "this" expression.
				ThisExpression expr= unitRewriter.getAST().newThisExpression();
				RefactoringStatus qualifierStatus= qualifyThisExpression(expr, originalInvocation, enclosing, unitRewriter);
				status.merge(qualifierStatus);
				if (qualifierStatus.hasEntries())
					// warning means don't include this invocation
					return status;
				newInvocationArgs.add(expr);
			} else {
				Expression expressionAsParam= (Expression) unitRewriter.getASTRewrite().createMoveTarget(expression);
				newInvocationArgs.add(expressionAsParam);
			}
		} else {
			if (expression != null) {
				// Check if expression is the type name. If not, there may
				// be side effects (e.g. inside methods) -> don't update
				if (! (expression instanceof Name) || ASTNodes.getTypeBinding((Name) expression) == null)
					return createWarningAboutCall(enclosing, originalInvocation, RefactoringCoreMessages.IntroduceIndirectionRefactoring_call_warning_static_expression_access);
			}
		}

		for (int i= 0; i < originalInvocationArgs.size(); i++) {
			Expression originalInvocationArg= originalInvocationArgs.get(i);
			Expression movedArg= (Expression) unitRewriter.getASTRewrite().createMoveTarget(originalInvocationArg);
			newInvocationArgs.add(movedArg);
		}

		unitRewriter.getASTRewrite().replace(originalInvocation, newInvocation,
				unitRewriter.createGroupDescription(RefactoringCoreMessages.IntroduceIndirectionRefactoring_group_description_replace_call));

		return status;
	}

	/**
	 * Attempts to qualify a "this" expression for a method invocation with an appropriate qualifier.
	 * The invoked method is analyzed according to the following specs:
	 *
	 * 'this' must be qualified iff method is declared in an enclosing type or a supertype of an enclosing type
	 *
	 * 1) The method is declared somewhere outside of the cu of the invocation
	 *      1a) inside a supertype of the current type
	 *      1b) inside a supertype of an enclosing type
	 * 2) The method is declared inside of the cu of the invocation
	 * 		2a) inside the type of the invocation
	 * 		2b) outside the type of the invocation
	 *
	 * In case of 1a) and 2b), qualify with the enclosing type.
	 * @param expr a {@link ThisExpression}
	 * @param originalInvocation the original method invocation
	 * @param enclosing the enclosing member of the original method invocation
	 * @param unitRewriter the rewrite
	 * @return resulting status
	 *
	 */
	private RefactoringStatus qualifyThisExpression(ThisExpression expr, MethodInvocation originalInvocation, IMember enclosing, CompilationUnitRewrite unitRewriter) {

		RefactoringStatus status= new RefactoringStatus();

		IMethodBinding methodBinding= originalInvocation.resolveMethodBinding();
		MethodDeclaration methodDeclaration= (MethodDeclaration) ASTNodes.findDeclaration(methodBinding, originalInvocation.getRoot());

		ITypeBinding currentTypeBinding= null;
		if (methodDeclaration != null) {
			// Case 1) : Declaring type is inside this cu => use its name if it's declared in an enclosing type
			if (ASTNodes.isParent(originalInvocation, methodDeclaration.getParent()))
				currentTypeBinding= methodBinding.getDeclaringClass();
			else
				currentTypeBinding= ASTNodes.getEnclosingType(originalInvocation);
		} else {
			// Case 2) : Declaring type is outside of this cu => find subclass in this cu
			ASTNode currentTypeDeclaration= getEnclosingTypeDeclaration(originalInvocation);
			currentTypeBinding= ASTNodes.getEnclosingType(currentTypeDeclaration);
			while (currentTypeDeclaration != null && (Bindings.findMethodInHierarchy(currentTypeBinding, methodBinding.getName(), methodBinding.getParameterTypes()) == null)) {
				currentTypeDeclaration= getEnclosingTypeDeclaration(currentTypeDeclaration.getParent());
				currentTypeBinding= ASTNodes.getEnclosingType(currentTypeDeclaration);
			}
		}

		if (currentTypeBinding == null) {
			status.merge(createWarningAboutCall(enclosing, originalInvocation, RefactoringCoreMessages.IntroduceIndirectionRefactoring_call_warning_declaring_type_not_found));
			return status;
		}

		currentTypeBinding= currentTypeBinding.getTypeDeclaration();

		ITypeBinding typeOfCall= ASTNodes.getEnclosingType(originalInvocation);
		if (!typeOfCall.equals(currentTypeBinding)) {
			if (currentTypeBinding.isAnonymous()) {
				// Cannot qualify, see bug 115277
				status.merge(createWarningAboutCall(enclosing, originalInvocation, RefactoringCoreMessages.IntroduceIndirectionRefactoring_call_warning_anonymous_cannot_qualify));
			} else {
				expr.setQualifier(unitRewriter.getAST().newSimpleName(currentTypeBinding.getName()));
			}
		} else {
			// do not qualify, only use "this.".
		}

		return status;
	}

	// ********* SMALL HELPERS ********************

	/*
	 * Helper method for finding an IMethod inside a binding hierarchy
	 */
	private IMethodBinding findMethodBindingInHierarchy(ITypeBinding currentTypeBinding, IMethod methodDeclaration) {
		for (IMethodBinding binding : currentTypeBinding.getDeclaredMethods()) {
			if (methodDeclaration.equals(binding.getJavaElement())) {
				return binding;
			}
		}

		ITypeBinding superClass= currentTypeBinding.getSuperclass();
		if (superClass != null) {
			IMethodBinding b= findMethodBindingInHierarchy(superClass, methodDeclaration);
			if (b != null)
				return b;
		}
		for (ITypeBinding intf : currentTypeBinding.getInterfaces()) {
			IMethodBinding b= findMethodBindingInHierarchy(intf, methodDeclaration);
			if (b != null)
				return b;
		}
		return null;
	}

	/*
	 * Helper method for retrieving a *bottom-up* list of super type bindings
	 */
	private ITypeBinding[] getTypeAndAllSuperTypes(ITypeBinding type) {
		List<ITypeBinding> result= new ArrayList<>();
		collectSuperTypes(type, result);
		return result.toArray(new ITypeBinding[result.size()]);
	}

	private void collectSuperTypes(ITypeBinding curr, List<ITypeBinding> list) {
		if (list.add(curr.getTypeDeclaration())) {
			for (ITypeBinding intf : curr.getInterfaces()) {
				collectSuperTypes(intf, list);
			}
			ITypeBinding superClass= curr.getSuperclass();
			if (superClass != null) {
				collectSuperTypes(superClass, list);
			}
		}
	}

	private CompilationUnitRewrite getCachedCURewrite(ICompilationUnit unit) {
		CompilationUnitRewrite rewrite= fRewrites.get(unit);
		if (rewrite == null && fSelectionMethodInvocation != null) {
			CompilationUnit cuNode= ASTResolving.findParentCompilationUnit(fSelectionMethodInvocation);
			if (cuNode != null && cuNode.getJavaElement().equals(unit)) {
				rewrite= new CompilationUnitRewrite(unit, cuNode);
				fRewrites.put(unit, rewrite);
			}
		}
		if (rewrite == null) {
			rewrite= new CompilationUnitRewrite(unit);
			fRewrites.put(unit, rewrite);
		}
		return rewrite;
	}

	private boolean isRewriteKept(ICompilationUnit compilationUnit) {
		return fIntermediaryType.getCompilationUnit().equals(compilationUnit);
	}

	private void createChangeAndDiscardRewrite(ICompilationUnit compilationUnit) throws CoreException {
		CompilationUnitRewrite rewrite= fRewrites.get(compilationUnit);
		if (rewrite != null) {
			fTextChangeManager.manage(compilationUnit, rewrite.createChange(true));
			fRewrites.remove(compilationUnit);
		}
	}

	private SearchResultGroup[] getReferences(IMethod[] methods, IProgressMonitor pm, RefactoringStatus status) throws CoreException {
		SearchPattern pattern= RefactoringSearchEngine.createOrPattern(methods, IJavaSearchConstants.REFERENCES);
		IJavaSearchScope scope= RefactoringScopeFactory.create(fIntermediaryType, false);
		return RefactoringSearchEngine.search(pattern, scope, pm, status);
	}

	private ITypeBinding typeToBinding(IType type, CompilationUnit root) throws JavaModelException {
		ASTNode typeNode= typeToDeclaration(type, root);
		if (type.isAnonymous()) {
			return ((AnonymousClassDeclaration) typeNode).resolveBinding();
		} else {
			return ((AbstractTypeDeclaration) typeNode).resolveBinding();
		}
	}

	private ASTNode typeToDeclaration(IType type, CompilationUnit root) throws JavaModelException {
		Name intermediateName= (Name) NodeFinder.perform(root, type.getNameRange());
		if (type.isAnonymous()) {
			return ASTNodes.getParent(intermediateName, AnonymousClassDeclaration.class);
		} else {
			return ASTNodes.getParent(intermediateName, AbstractTypeDeclaration.class);
		}
	}

	private ASTNode getEnclosingTypeDeclaration(ASTNode node) {
		while (node != null) {
			if (node instanceof AbstractTypeDeclaration
					|| node instanceof AnonymousClassDeclaration) {
				return node;
			}
			node= node.getParent();
		}
		return null;
	}

	private ChildListPropertyDescriptor typeToBodyDeclarationProperty(IType type, CompilationUnit root) throws JavaModelException {
		ASTNode typeDeclaration= typeToDeclaration(type, root);
		if (typeDeclaration instanceof AbstractTypeDeclaration)
			return ((AbstractTypeDeclaration) typeDeclaration).getBodyDeclarationsProperty();
		else if (typeDeclaration instanceof AnonymousClassDeclaration)
			return AnonymousClassDeclaration.BODY_DECLARATIONS_PROPERTY;

		Assert.isTrue(false);
		return null;
	}

	private RefactoringStatus createWarningAboutCall(IMember enclosing, ASTNode concreteNode, String message) {
		String name= JavaElementLabels.getElementLabel(enclosing, JavaElementLabels.ALL_DEFAULT);
		String container= JavaElementLabels.getElementLabel(enclosing.getDeclaringType(), JavaElementLabels.ALL_FULLY_QUALIFIED);
		return RefactoringStatus.createWarningStatus(Messages.format(message, new String[] { name, container }), JavaStatusContext.create(enclosing.getCompilationUnit(), concreteNode));
	}

	private ITypeBinding getExpressionType(MethodInvocation invocation) {
		Expression expression= invocation.getExpression();
		ITypeBinding typeBinding= null;
		if (expression == null) {
			typeBinding= invocation.resolveMethodBinding().getDeclaringClass();
		} else {
			typeBinding= expression.resolveTypeBinding();
		}
		if (typeBinding != null && typeBinding.isTypeVariable()) {
			ITypeBinding[] typeBounds= typeBinding.getTypeBounds();
			if (typeBounds.length > 0) {
				for (ITypeBinding typeBound : typeBounds) {
					ITypeBinding expressionType= getExpressionType(invocation, typeBound);
					if (expressionType != null) {
						return expressionType;
					}
				}
				typeBinding= typeBounds[0].getTypeDeclaration();
			} else {
				typeBinding= invocation.getAST().resolveWellKnownType("java.lang.Object"); //$NON-NLS-1$
			}
		}
		Assert.isNotNull(typeBinding, "Type binding of target expression may not be null"); //$NON-NLS-1$
		return typeBinding;
	}

	private ITypeBinding getExpressionType(final MethodInvocation invocation, ITypeBinding typeBinding) {
		if (typeBinding == null)
			return null;
		for (IMethodBinding iMethodBinding : typeBinding.getDeclaredMethods()) {
			if (invocation.resolveMethodBinding() == iMethodBinding)
				return typeBinding.getTypeDeclaration();
		}
		ITypeBinding expressionType= getExpressionType(invocation, typeBinding.getSuperclass());
		if (expressionType != null) {
			return expressionType;
		}
		for (ITypeBinding interfaceBinding : typeBinding.getInterfaces()) {
			expressionType= getExpressionType(invocation, interfaceBinding);
			if (expressionType != null) {
				return expressionType;
			}
		}
		return null;
	}

	private IFile[] getAllFilesToModify() {
		return ResourceUtil.getFiles(fTextChangeManager.getAllCompilationUnits().clone());
	}

	private boolean isStaticTarget() throws JavaModelException {
		return Flags.isStatic(fTargetMethod.getFlags());
	}

	private IMember getEnclosingInitialSelectionMember() throws JavaModelException {
		return (IMember) fSelectionCompilationUnit.getElementAt(fSelectionStart);
	}

	private static ASTNode getSelectedNode(ICompilationUnit unit, CompilationUnit root, int offset, int length) {
		ASTNode node= null;
		try {
			if (unit != null)
				node= checkNode(NodeFinder.perform(root, offset, length, unit));
			else
				node= checkNode(NodeFinder.perform(root, offset, length));
		} catch (JavaModelException e) {
			// Do nothing
		}
		if (node != null)
			return node;
		return checkNode(NodeFinder.perform(root, offset, length));
	}

	private static ASTNode checkNode(ASTNode node) {
		if (node == null)
			return null;
		if (node.getNodeType() == ASTNode.SIMPLE_NAME) {
			node= node.getParent();
		} else if (node.getNodeType() == ASTNode.EXPRESSION_STATEMENT) {
			node= ((ExpressionStatement) node).getExpression();
		}
		switch (node.getNodeType()) {
			case ASTNode.METHOD_INVOCATION:
			case ASTNode.METHOD_DECLARATION:
			case ASTNode.SUPER_METHOD_INVOCATION:
				return node;
		}
		return null;
	}

	// ***************** VISIBILITY ********************

	private ModifierKeyword getNeededVisibility(IMember whoToAdjust, IMember fromWhereToLook) throws JavaModelException {
		return fAdjustor.getVisibilityThreshold(fromWhereToLook, whoToAdjust, new NullProgressMonitor());
	}

	private RefactoringStatus adjustVisibility(IMember whoToAdjust, IMember fromWhereToLook, IProgressMonitor monitor) throws CoreException {
		return adjustVisibility(whoToAdjust, getNeededVisibility(whoToAdjust, fromWhereToLook), true, monitor);
	}

	private RefactoringStatus adjustVisibility(IMember whoToAdjust, ModifierKeyword neededVisibility, IProgressMonitor monitor) throws CoreException {
		return adjustVisibility(whoToAdjust, neededVisibility, false, monitor);
	}

	private RefactoringStatus adjustVisibility(IMember whoToAdjust, ModifierKeyword neededVisibility, boolean alsoIncreaseEnclosing, IProgressMonitor monitor) throws CoreException {

		Map<IMember, IncomingMemberVisibilityAdjustment> adjustments;
		if (isRewriteKept(whoToAdjust.getCompilationUnit()))
			adjustments= fIntermediaryAdjustments;
		else
			adjustments= new HashMap<>();

		int existingAdjustments= adjustments.size();
		addAdjustment(whoToAdjust, neededVisibility, adjustments);

		if (alsoIncreaseEnclosing)
			while (whoToAdjust.getDeclaringType() != null) {
				whoToAdjust= whoToAdjust.getDeclaringType();
				addAdjustment(whoToAdjust, neededVisibility, adjustments);
			}

		boolean hasNewAdjustments= (adjustments.size() - existingAdjustments) > 0;
		if (hasNewAdjustments && ( (whoToAdjust.isReadOnly() || whoToAdjust.isBinary())))
			return RefactoringStatus.createErrorStatus(Messages.format(RefactoringCoreMessages.IntroduceIndirectionRefactoring_cannot_update_binary_target_visibility, new String[] { JavaElementLabels
					.getElementLabel(whoToAdjust, JavaElementLabels.ALL_DEFAULT) }), JavaStatusContext.create(whoToAdjust));

		RefactoringStatus status= new RefactoringStatus();

		// Don't create a rewrite if it is not necessary
		if (!hasNewAdjustments)
			return status;

		try {
			monitor.beginTask(RefactoringCoreMessages.MemberVisibilityAdjustor_adjusting, 2);
			Map<ICompilationUnit, CompilationUnitRewrite> rewrites;
			if (!isRewriteKept(whoToAdjust.getCompilationUnit())) {
				CompilationUnitRewrite rewrite= new CompilationUnitRewrite(whoToAdjust.getCompilationUnit());
				rewrite.setResolveBindings(false);
				rewrites= new HashMap<>();
				rewrites.put(whoToAdjust.getCompilationUnit(), rewrite);
				status.merge(rewriteVisibility(adjustments, rewrites, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL)));
				rewrite.attachChange((CompilationUnitChange) fTextChangeManager.get(whoToAdjust.getCompilationUnit()), true, new SubProgressMonitor(monitor, 1, SubProgressMonitor.SUPPRESS_SUBTASK_LABEL));
			}
		} finally {
			monitor.done();
		}
		return status;
	}

	private RefactoringStatus rewriteVisibility(Map<IMember, IncomingMemberVisibilityAdjustment> adjustments, Map<ICompilationUnit, CompilationUnitRewrite> rewrites, IProgressMonitor monitor) throws JavaModelException {
		RefactoringStatus status= new RefactoringStatus();
		fAdjustor.setRewrites(rewrites);
		fAdjustor.setAdjustments(adjustments);
		fAdjustor.setStatus(status);
		fAdjustor.rewriteVisibility(monitor);
		return status;
	}

	private void addAdjustment(IMember whoToAdjust, ModifierKeyword neededVisibility, Map<IMember, IncomingMemberVisibilityAdjustment> adjustments) throws JavaModelException {
		ModifierKeyword currentVisibility= ModifierKeyword.fromFlagValue(JdtFlags.getVisibilityCode(whoToAdjust));
		if (MemberVisibilityAdjustor.hasLowerVisibility(currentVisibility, neededVisibility)
				&& MemberVisibilityAdjustor.needsVisibilityAdjustments(whoToAdjust, neededVisibility, adjustments))
			adjustments.put(whoToAdjust, new MemberVisibilityAdjustor.IncomingMemberVisibilityAdjustment(whoToAdjust, neededVisibility,
					RefactoringStatus.createWarningStatus(Messages.format(MemberVisibilityAdjustor.getMessage(whoToAdjust), new String[] {
							MemberVisibilityAdjustor.getLabel(whoToAdjust), MemberVisibilityAdjustor.getLabel(neededVisibility) }), JavaStatusContext
							.create(whoToAdjust))));
	}

	private RefactoringStatus initialize(JavaRefactoringArguments arguments) {
		String handle= arguments.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT);
		if (handle != null) {
			final IJavaElement element= JavaRefactoringDescriptorUtil.handleToElement(arguments.getProject(), handle, false);
			if (element == null || !element.exists() || element.getElementType() != IJavaElement.METHOD)
				return JavaRefactoringDescriptorUtil.createInputFatalStatus(element, getName(), IJavaRefactorings.INTRODUCE_INDIRECTION);
			else
				fTargetMethod= (IMethod) element;
		} else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT));
		handle= arguments.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_ELEMENT + 1);
		if (handle != null) {
			final IJavaElement element= JavaRefactoringDescriptorUtil.handleToElement(arguments.getProject(), handle, false);
			if (element == null || !element.exists() || element.getElementType() != IJavaElement.TYPE)
				return JavaRefactoringDescriptorUtil.createInputFatalStatus(element, getName(), IJavaRefactorings.INTRODUCE_INDIRECTION);
			else
				fIntermediaryType= (IType) element;
		} else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, JavaRefactoringDescriptorUtil.ATTRIBUTE_ELEMENT + 1));
		final String references= arguments.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_REFERENCES);
		if (references != null) {
			fUpdateReferences= Boolean.parseBoolean(references);
		} else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, JavaRefactoringDescriptorUtil.ATTRIBUTE_REFERENCES));
		final String name= arguments.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME);
		if (name != null && !"".equals(name)) //$NON-NLS-1$
			return setIntermediaryMethodName(name);
		else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME));
	}
}
