/*******************************************************************************
 * Copyright (c) 2000, 2020 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.jdt.internal.corext.refactoring.rename;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

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

import org.eclipse.core.resources.IFile;

import org.eclipse.text.edits.ReplaceEdit;

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.GroupCategorySet;
import org.eclipse.ltk.core.refactoring.RefactoringDescriptor;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.participants.CheckConditionsContext;
import org.eclipse.ltk.core.refactoring.participants.RenameArguments;

import org.eclipse.jdt.core.Flags;
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.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.WorkingCopyOwner;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.jdt.core.refactoring.IJavaRefactorings;
import org.eclipse.jdt.core.refactoring.descriptors.JavaRefactoringDescriptor;
import org.eclipse.jdt.core.refactoring.descriptors.RenameJavaElementDescriptor;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.MethodDeclarationMatch;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;

import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.core.refactoring.descriptors.RefactoringSignatureDescriptorFactory;
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.base.ReferencesInBinaryContext;
import org.eclipse.jdt.internal.corext.refactoring.changes.DynamicValidationRefactoringChange;
import org.eclipse.jdt.internal.corext.refactoring.changes.TextChangeCompatibility;
import org.eclipse.jdt.internal.corext.refactoring.delegates.DelegateCreator;
import org.eclipse.jdt.internal.corext.refactoring.delegates.DelegateMethodCreator;
import org.eclipse.jdt.internal.corext.refactoring.participants.JavaProcessors;
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.tagging.IDelegateUpdating;
import org.eclipse.jdt.internal.corext.refactoring.tagging.IReferenceUpdating;
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.CollectionsUtil;
import org.eclipse.jdt.internal.corext.util.JavaConventionsUtil;
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.SearchUtils;

import org.eclipse.jdt.ui.JavaElementLabels;
import org.eclipse.jdt.ui.refactoring.IRefactoringProcessorIds;
import org.eclipse.jdt.ui.refactoring.RefactoringSaveHelper;

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

public abstract class RenameMethodProcessor extends JavaRenameProcessor implements IReferenceUpdating, IDelegateUpdating {

	private static final String ATTRIBUTE_DELEGATE= "delegate"; //$NON-NLS-1$
	private static final String ATTRIBUTE_DEPRECATE= "deprecate"; //$NON-NLS-1$

	private SearchResultGroup[] fOccurrences;
	private boolean fUpdateReferences;
	private IMethod fMethod;
	private Set<IMethod> fMethodsToRename;
	private TextChangeManager fChangeManager;
	private WorkingCopyOwner fWorkingCopyOwner;
	private boolean fIsComposite;
	private GroupCategorySet fCategorySet;
	private boolean fDelegateUpdating;
	private boolean fDelegateDeprecation;
	protected boolean fInitialized= false;

	/**
	 * Creates a new rename method processor.
	 * @param method the method, or <code>null</code> if invoked by scripting
	 */
	protected RenameMethodProcessor(IMethod method) {
		this(method, new TextChangeManager(true), null);
		fIsComposite= false;
	}
	/**
	 * Creates a new rename method processor.
	 * <p>
	 * This constructor is only invoked by <code>RenameTypeProcessor</code>.
	 * </p>
	 *
	 * @param method the method
	 * @param manager the change manager
	 * @param categorySet the group category set
	 */
	protected RenameMethodProcessor(IMethod method, TextChangeManager manager, GroupCategorySet categorySet) {
		initialize(method);
		fChangeManager= manager;
		fCategorySet= categorySet;
		fDelegateUpdating= false;
		fDelegateDeprecation= true;
		fIsComposite= true;
	}

	protected void initialize(IMethod method) {
		assignMethod(method);
		if (!fInitialized) {
			if (method != null)
				setNewElementName(method.getElementName());
			fUpdateReferences= true;
			initializeWorkingCopyOwner();
		}
	}

	protected void initializeWorkingCopyOwner() {
		fWorkingCopyOwner= new WorkingCopyOwner() {/*must subclass*/};
	}

	protected void setData(RenameMethodProcessor other) {
		fUpdateReferences= other.fUpdateReferences;
		setNewElementName(other.getNewElementName());
	}

	@Override
	public String getIdentifier() {
		return IRefactoringProcessorIds.RENAME_METHOD_PROCESSOR;
	}

	@Override
	public boolean isApplicable() throws CoreException {
		return RefactoringAvailabilityTester.isRenameAvailable(fMethod);
	}

	@Override
	public String getProcessorName() {
		return RefactoringCoreMessages.RenameMethodRefactoring_name;
	}

	@Override
	protected String[] getAffectedProjectNatures() throws CoreException {
		return JavaProcessors.computeAffectedNatures(fMethod);
	}

	@Override
	public Object[] getElements() {
		return new Object[] {fMethod};
	}

	@Override
	protected RenameModifications computeRenameModifications() throws CoreException {
		RenameModifications result= new RenameModifications();
		RenameArguments args= new RenameArguments(getNewElementName(), getUpdateReferences());
		for (IMethod method : fMethodsToRename) {
			result.rename(method, args);
		}
		return result;
	}



	@Override
	protected IFile[] getChangedFiles() throws CoreException {
		return ResourceUtil.getFiles(fChangeManager.getAllCompilationUnits());
	}

	@Override
	public int getSaveMode() {
		return RefactoringSaveHelper.SAVE_REFACTORING;
	}

	//---- INameUpdating -------------------------------------

	@Override
	public final String getCurrentElementName(){
		return fMethod.getElementName();
	}

	@Override
	public final RefactoringStatus checkNewElementName(String newName) {
		Assert.isNotNull(newName, "new name"); //$NON-NLS-1$

		RefactoringStatus status= Checks.checkName(newName, JavaConventionsUtil.validateMethodName(newName, fMethod));
		if (status.isOK() && !Checks.startsWithLowerCase(newName))
			status= RefactoringStatus.createWarningStatus(fIsComposite
					? Messages.format(RefactoringCoreMessages.Checks_method_names_lowercase2, new String[] { BasicElementLabels.getJavaElementName(newName), getDeclaringTypeLabel()})
					: RefactoringCoreMessages.Checks_method_names_lowercase);

		if (Checks.isAlreadyNamed(fMethod, newName))
			status.addFatalError(fIsComposite
					? Messages.format(RefactoringCoreMessages.RenameMethodRefactoring_same_name2, new String[] { BasicElementLabels.getJavaElementName(newName), getDeclaringTypeLabel() } )
					: RefactoringCoreMessages.RenameMethodRefactoring_same_name,
					JavaStatusContext.create(fMethod));
		return status;
	}
	private String getDeclaringTypeLabel() {
		return JavaElementLabels.getElementLabel(fMethod.getDeclaringType(), JavaElementLabels.ALL_DEFAULT);
	}

	@Override
	public Object getNewElement() {
		return fMethod.getDeclaringType().getMethod(getNewElementName(), fMethod.getParameterTypes());
	}

	public final IMethod getMethod() {
		return fMethod;
	}

	private void initializeMethodsToRename(IProgressMonitor pm, ReferencesInBinaryContext binaryRefs) throws CoreException {
		if (fMethodsToRename == null) {
			IMethod[] rippleMethods= RippleMethodFinder2.getRelatedMethods(getMethod(), binaryRefs, pm, null);
			fMethodsToRename= new HashSet<>();
			for (IMethod method : rippleMethods) {
				if (!method.isLambdaMethod()) {
					fMethodsToRename.add(method);
				}
			}
		}
	}

	protected void setMethodsToRename(IMethod[] methods) {
		fMethodsToRename= new HashSet<>(Arrays.asList(methods));
	}

	protected Set<IMethod> getMethodsToRename() {
		return fMethodsToRename;
	}

	protected void assignMethod(IMethod method) {
		fMethod= method;
	}

	//---- IReferenceUpdating -----------------------------------

	@Override
	public final void setUpdateReferences(boolean update) {
		fUpdateReferences= update;
	}

	@Override
	public boolean getUpdateReferences() {
		return fUpdateReferences;
	}

	//------------------- IDelegateUpdating ----------------------

	@Override
	public boolean canEnableDelegateUpdating() {
		return true;
	}

	@Override
	public boolean getDelegateUpdating() {
		return fDelegateUpdating;
	}

	@Override
	public void setDelegateUpdating(boolean updating) {
		fDelegateUpdating= updating;
	}

	@Override
	public boolean getDeprecateDelegates() {
		return fDelegateDeprecation;
	}

	@Override
	public void setDeprecateDelegates(boolean deprecate) {
		fDelegateDeprecation= deprecate;
	}

	//----------- preconditions ------------------

	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException {
		if (! fMethod.exists()){
			String message= Messages.format(RefactoringCoreMessages.RenameMethodRefactoring_deleted,
								BasicElementLabels.getFileName(fMethod.getCompilationUnit()));
			return RefactoringStatus.createFatalErrorStatus(message);
		}

		RefactoringStatus result= Checks.checkAvailability(fMethod);
		if (result.hasFatalError())
				return result;
		result.merge(Checks.checkIfCuBroken(fMethod));
		if (JdtFlags.isNative(fMethod))
			result.addError(RefactoringCoreMessages.RenameMethodRefactoring_no_native);
		return result;
	}

	@Override
	protected RefactoringStatus doCheckFinalConditions(IProgressMonitor pm, CheckConditionsContext context) throws CoreException {
		try{
			RefactoringStatus result= new RefactoringStatus();
			pm.beginTask("", 9); //$NON-NLS-1$
			// TODO workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=40367
			if (!Checks.isAvailable(fMethod)) {
				result.addFatalError(RefactoringCoreMessages.RenameMethodProcessor_is_binary, JavaStatusContext.create(fMethod));
				return result;
			}
			result.merge(Checks.checkIfCuBroken(fMethod));
			if (result.hasFatalError())
				return result;
			pm.setTaskName(RefactoringCoreMessages.RenameMethodRefactoring_taskName_checkingPreconditions);
			result.merge(checkNewElementName(getNewElementName()));
			if (result.hasFatalError())
				return result;

			boolean mustAnalyzeShadowing;
			IMethod[] newNameMethods= searchForDeclarationsOfClashingMethods(new SubProgressMonitor(pm, 1));
			if (newNameMethods.length == 0) {
				mustAnalyzeShadowing= false;
				pm.worked(1);
			} else {
				IType[] outerTypes= searchForOuterTypesOfReferences(newNameMethods, new SubProgressMonitor(pm, 1));
				if (outerTypes.length > 0) {
					//There exists a reference to a clashing method, where the reference is in a nested type.
					//That nested type could be a type in a ripple method's hierarchy, which could
					//cause the reference to bind to the new ripple method instead of to
					//its old binding (a method of an enclosing scope).
					//-> Getting *more* references than before -> Semantics not preserved.
					//Examples: RenameVirtualMethodInClassTests#testFail39() and #testFail41()
					//TODO: could pass declaringTypes to the RippleMethodFinder and check whether
					//a hierarchy contains one of outerTypes (or an outer type of an outerType, recursively).
					mustAnalyzeShadowing= true;

				} else {
					boolean hasOldRefsInInnerTypes= true;
						//TODO: to implement this optimization:
						//- move search for references to before this check.
						//- collect references in inner types.
						//- for each reference, check for all supertypes and their enclosing types
						//(recursively), whether they declare a rippleMethod
					if (hasOldRefsInInnerTypes) {
						//There exists a reference to a ripple method in a nested type
						//of a type in the hierarchy of any ripple method.
						//When that reference is renamed, and one of the supertypes of the
						//nested type declared a method matching the new name, then
						//the renamed reference will bind to the method in its supertype,
						//since inherited methods bind stronger than methods from enclosing scopes.
						//Getting *less* references than before -> Semantics not preserved.
						//Examples: RenamePrivateMethodTests#testFail2(), RenamePrivateMethodTests#testFail5()
						mustAnalyzeShadowing= true;
					} else {
						mustAnalyzeShadowing= false;
					}
				}
			}

			String binaryRefsDescription= Messages.format(RefactoringCoreMessages.ReferencesInBinaryContext_ref_in_binaries_description , BasicElementLabels.getJavaElementName(getCurrentElementName()));
			ReferencesInBinaryContext binaryRefs= new ReferencesInBinaryContext(binaryRefsDescription);

			initializeMethodsToRename(new SubProgressMonitor(pm, 1), binaryRefs);
			pm.setTaskName(RefactoringCoreMessages.RenameMethodRefactoring_taskName_searchingForReferences);
			fOccurrences= getOccurrences(new SubProgressMonitor(pm, 3), result, binaryRefs);
			binaryRefs.addErrorIfNecessary(result);

			pm.setTaskName(RefactoringCoreMessages.RenameMethodRefactoring_taskName_checkingPreconditions);

			if (fUpdateReferences)
				result.merge(checkRelatedMethods());

			result.merge(analyzeCompilationUnits()); //removes CUs with syntax errors
			pm.worked(1);

			if (result.hasFatalError())
				return result;

			createChanges(new SubProgressMonitor(pm, 1), result);
			if (fUpdateReferences && mustAnalyzeShadowing)
				result.merge(analyzeRenameChanges(new SubProgressMonitor(pm, 1)));
			else
				pm.worked(1);

			return result;
		} finally{
			pm.done();
		}
	}

	private IType[] searchForOuterTypesOfReferences(IMethod[] newNameMethods, IProgressMonitor pm) throws CoreException {
		final Set<IType> outerTypesOfReferences= new HashSet<>();
		SearchPattern pattern= RefactoringSearchEngine.createOrPattern(newNameMethods, IJavaSearchConstants.REFERENCES);
		IJavaSearchScope scope= createRefactoringScope(getMethod());
		SearchRequestor requestor= new SearchRequestor() {
			@Override
			public void acceptSearchMatch(SearchMatch match) throws CoreException {
				Object element= match.getElement();
				if (!(element instanceof IMember))
					return; // e.g. an IImportDeclaration for a static method import
				IMember member= (IMember) element;
				IType declaring= member.getDeclaringType();
				if (declaring == null)
					return;
				IType outer= declaring.getDeclaringType();
				if (outer != null)
					outerTypesOfReferences.add(declaring);
			}
		};
		new SearchEngine().search(pattern, SearchUtils.getDefaultSearchParticipants(),
				scope, requestor, pm);
		return outerTypesOfReferences.toArray(new IType[outerTypesOfReferences.size()]);
	}

	private IMethod[] searchForDeclarationsOfClashingMethods(IProgressMonitor pm) throws CoreException {
		final List<IMethod> results= new ArrayList<>();
		SearchPattern pattern= createNewMethodPattern();
		if (pattern == null) {
			return new IMethod[0];
		}
		IJavaSearchScope scope= RefactoringScopeFactory.create(getMethod().getJavaProject());
		SearchRequestor requestor= new SearchRequestor() {
			@Override
			public void acceptSearchMatch(SearchMatch match) throws CoreException {
				Object method= match.getElement();
				if (method instanceof IMethod) // check for bug 90138: [refactoring] [rename] Renaming method throws internal exception
					results.add((IMethod) method);
				else
					JavaPlugin.logErrorMessage("Unexpected element in search match: " + match.toString()); //$NON-NLS-1$
			}
		};
		new SearchEngine().search(pattern, SearchUtils.getDefaultSearchParticipants(), scope, requestor, pm);
		return results.toArray(new IMethod[results.size()]);
	}

	private SearchPattern createNewMethodPattern() {
		StringBuilder stringPattern= new StringBuilder(getNewElementName()).append('(');
		int paramCount= getMethod().getNumberOfParameters();
		for (int i= 0; i < paramCount; i++) {
			if (i > 0)
				stringPattern.append(',');
			stringPattern.append('*');
		}
		stringPattern.append(')');

		return SearchPattern.createPattern(stringPattern.toString(), IJavaSearchConstants.METHOD,
				IJavaSearchConstants.DECLARATIONS, SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
	}

	protected final IJavaSearchScope createRefactoringScope() throws CoreException {
		return createRefactoringScope(fMethod);
	}
	//TODO: shouldn't scope take all ripple methods into account?
	protected static final IJavaSearchScope createRefactoringScope(IMethod method) throws CoreException {
		return RefactoringScopeFactory.create(method, true, false);
	}

	protected SearchPattern createOccurrenceSearchPattern() {
		HashSet<IMethod> methods= new HashSet<>(fMethodsToRename);
		methods.add(fMethod);
		IMethod[] ms= methods.toArray(new IMethod[methods.size()]);
		return RefactoringSearchEngine.createOrPattern(ms, IJavaSearchConstants.ALL_OCCURRENCES);
	}

	protected SearchResultGroup[] getOccurrences(){
		return fOccurrences;
	}

	private SearchResultGroup[] getOccurrences(IProgressMonitor pm, RefactoringStatus status, ReferencesInBinaryContext binaryRefs) throws CoreException {
		SearchPattern pattern= createOccurrenceSearchPattern();
		return RefactoringSearchEngine.search(pattern, createRefactoringScope(),
			new MethodOccurenceCollector(getMethod().getElementName(), binaryRefs), pm, status);
	}

	private RefactoringStatus checkRelatedMethods() throws CoreException {
		RefactoringStatus result= new RefactoringStatus();
		for (IMethod method : fMethodsToRename) {
			result.merge(Checks.checkIfConstructorName(method, getNewElementName(), method.getDeclaringType().getElementName()));

			String[] msgData= new String[]{BasicElementLabels.getJavaElementName(method.getElementName()), BasicElementLabels.getJavaElementName(method.getDeclaringType().getFullyQualifiedName('.'))};
			if (! method.exists()){
				result.addFatalError(Messages.format(RefactoringCoreMessages.RenameMethodRefactoring_not_in_model, msgData));
				continue;
			}
			if (method.isBinary())
				result.addFatalError(Messages.format(RefactoringCoreMessages.RenameMethodRefactoring_no_binary, msgData));
			if (method.isReadOnly())
				result.addFatalError(Messages.format(RefactoringCoreMessages.RenameMethodRefactoring_no_read_only, msgData));
			if (JdtFlags.isNative(method))
				result.addError(Messages.format(RefactoringCoreMessages.RenameMethodRefactoring_no_native_1, msgData));
		}
		return result;
	}

	private RefactoringStatus analyzeCompilationUnits() throws CoreException {
		if (fOccurrences.length == 0)
			return null;

		RefactoringStatus result= new RefactoringStatus();
		fOccurrences= Checks.excludeCompilationUnits(fOccurrences, result);
		if (result.hasFatalError())
			return result;

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

		return result;
	}

	//-------

	private RefactoringStatus analyzeRenameChanges(IProgressMonitor pm) throws CoreException {
		ICompilationUnit[] newDeclarationWCs= null;
		try {
			pm.beginTask("", 4); //$NON-NLS-1$
			RefactoringStatus result= new RefactoringStatus();
			ICompilationUnit[] declarationCUs= getDeclarationCUs();
			newDeclarationWCs= RenameAnalyzeUtil.createNewWorkingCopies(declarationCUs,
					fChangeManager, fWorkingCopyOwner, new SubProgressMonitor(pm, 1));

			IMethod[] wcOldMethods= new IMethod[fMethodsToRename.size()];
			IMethod[] wcNewMethods= new IMethod[fMethodsToRename.size()];
			int i= 0;
			for (Iterator<IMethod> iter= fMethodsToRename.iterator(); iter.hasNext(); i++) {
				IMethod method= iter.next();
				ICompilationUnit newCu= RenameAnalyzeUtil.findWorkingCopyForCu(newDeclarationWCs, method.getCompilationUnit());
				IType typeWc= (IType) JavaModelUtil.findInCompilationUnit(newCu, method.getDeclaringType());
				if (typeWc == null) {
					// should not happen
					i--;
					wcOldMethods= CollectionsUtil.toArray(Arrays.asList(wcOldMethods).subList(0, wcOldMethods.length - 1), IMethod.class);
					wcNewMethods= CollectionsUtil.toArray(Arrays.asList(wcNewMethods).subList(0, wcNewMethods.length - 1), IMethod.class);
					continue;
				}
				wcOldMethods[i]= getMethodInWorkingCopy(method, getCurrentElementName(), typeWc);
				wcNewMethods[i]= getMethodInWorkingCopy(method, getNewElementName(), typeWc);
			}

//			SearchResultGroup[] newOccurrences= findNewOccurrences(newMethods, newDeclarationWCs, new SubProgressMonitor(pm, 3));
			SearchResultGroup[] newOccurrences= batchFindNewOccurrences(wcNewMethods, wcOldMethods, newDeclarationWCs, new SubProgressMonitor(pm, 3), result);

			result.merge(RenameAnalyzeUtil.analyzeRenameChanges2(fChangeManager, fOccurrences, newOccurrences, getNewElementName()));
			return result;
		} finally{
			pm.done();
			if (newDeclarationWCs != null){
				for (ICompilationUnit newDeclarationWC : newDeclarationWCs) {
					newDeclarationWC.discardWorkingCopy();
				}
			}
		}
	}

	//Lower memory footprint than batchFindNewOccurrences. Not used because it is too slow.
	//Final solution is maybe to do searches in chunks of ~ 50 CUs.
//	private SearchResultGroup[] findNewOccurrences(IMethod[] newMethods, ICompilationUnit[] newDeclarationWCs, IProgressMonitor pm) throws CoreException {
//		pm.beginTask("", fOccurrences.length * 2); //$NON-NLS-1$
//
//		SearchPattern refsPattern= RefactoringSearchEngine.createOrPattern(newMethods, IJavaSearchConstants.REFERENCES);
//		SearchParticipant[] searchParticipants= SearchUtils.getDefaultSearchParticipants();
//		IJavaSearchScope scope= RefactoringScopeFactory.create(newMethods);
//		MethodOccurenceCollector requestor= new MethodOccurenceCollector(getNewElementName());
//		SearchEngine searchEngine= new SearchEngine(fWorkingCopyOwner);
//
//		//TODO: should process only references
//		for (int j= 0; j < fOccurrences.length; j++) { //should be getReferences()
//			//cut memory peak by holding only one reference CU at a time in memory
//			ICompilationUnit originalCu= fOccurrences[j].getCompilationUnit();
//			ICompilationUnit newWc= null;
//			try {
//				ICompilationUnit wc= RenameAnalyzeUtil.findWorkingCopyForCu(newDeclarationWCs, originalCu);
//				if (wc == null) {
//					newWc= RenameAnalyzeUtil.createNewWorkingCopy(originalCu, fChangeManager, fWorkingCopyOwner,
//							new SubProgressMonitor(pm, 1));
//				}
//				searchEngine.search(refsPattern, searchParticipants, scope,	requestor, new SubProgressMonitor(pm, 1));
//			} finally {
//				if (newWc != null)
//					newWc.discardWorkingCopy();
//			}
//		}
//		SearchResultGroup[] newResults= RefactoringSearchEngine.groupByResource(requestor.getResults());
//		pm.done();
//		return newResults;
//	}

	private SearchResultGroup[] batchFindNewOccurrences(IMethod[] wcNewMethods, final IMethod[] wcOldMethods, ICompilationUnit[] newDeclarationWCs, IProgressMonitor pm, RefactoringStatus status) throws CoreException {
		pm.beginTask("", 2); //$NON-NLS-1$

		SearchPattern refsPattern= RefactoringSearchEngine.createOrPattern(wcNewMethods, IJavaSearchConstants.REFERENCES);
		SearchParticipant[] searchParticipants= SearchUtils.getDefaultSearchParticipants();
		IJavaSearchScope scope= RefactoringScopeFactory.createProjectsScope(wcNewMethods, true);

		MethodOccurenceCollector requestor;
		if (getDelegateUpdating()) {
			// There will be two new matches inside the delegate(s) (the invocation
			// and the javadoc) which are OK and must not be reported.
			// Note that except these ocurrences, the delegate bodies are empty
			// (as they were created this way).
			requestor= new MethodOccurenceCollector(getNewElementName()) {
				@Override
				public void acceptSearchMatch(ICompilationUnit unit, SearchMatch match) throws CoreException {
					for (IMethod occurrence : wcOldMethods) {
						if (occurrence.equals(match.getElement())) {
							return;
						}
					}
					super.acceptSearchMatch(unit, match);
				}
			};
		} else
			requestor= new MethodOccurenceCollector(getNewElementName());

		SearchEngine searchEngine= new SearchEngine(fWorkingCopyOwner);

		ArrayList<ICompilationUnit> needWCs= new ArrayList<>();
		HashSet<ICompilationUnit> declaringCUs= new HashSet<>(newDeclarationWCs.length);
		for (ICompilationUnit newDeclarationWC : newDeclarationWCs) {
			declaringCUs.add(newDeclarationWC.getPrimary());
		}
		for (SearchResultGroup occurrence : fOccurrences) {
			ICompilationUnit cu= occurrence.getCompilationUnit();
			if (! declaringCUs.contains(cu))
				needWCs.add(cu);
		}
		ICompilationUnit[] otherWCs= null;
		try {
			otherWCs= RenameAnalyzeUtil.createNewWorkingCopies(
					needWCs.toArray(new ICompilationUnit[needWCs.size()]),
					fChangeManager, fWorkingCopyOwner, new SubProgressMonitor(pm, 1));
			searchEngine.search(refsPattern, searchParticipants, scope,	requestor, new SubProgressMonitor(pm, 1));
		} finally {
			pm.done();
			if (otherWCs != null) {
				for (ICompilationUnit otherWC : otherWCs) {
					otherWC.discardWorkingCopy();
				}
			}
		}
		SearchResultGroup[] newResults= RefactoringSearchEngine.groupByCu(requestor.getResults(), status);
		return newResults;
	}

	private ICompilationUnit[] getDeclarationCUs() {
		Set<ICompilationUnit> cus= new HashSet<>();
		for (IMethod method : fMethodsToRename) {
			cus.add(method.getCompilationUnit());
		}
		return cus.toArray(new ICompilationUnit[cus.size()]);
	}

	private IMethod getMethodInWorkingCopy(IMethod method, String elementName, IType typeWc) {
		String[] paramTypeSignatures= method.getParameterTypes();
		return typeWc.getMethod(elementName, paramTypeSignatures);
	}

	//-------
	private static IMethod[] classesDeclareMethodName(ITypeHierarchy hier, List<IType> classes, IMethod method, String newName)  throws CoreException {
		Set<IMethod> result= new HashSet<>();
		IType type= method.getDeclaringType();
		List<IType> subtypes= Arrays.asList(hier.getAllSubtypes(type));

		int parameterCount= method.getParameterTypes().length;
		boolean isMethodPrivate= JdtFlags.isPrivate(method);

		for (IType clazz : classes) {
			boolean isSubclass= subtypes.contains(clazz);
			for (IMethod m : clazz.getMethods()) {
				IMethod foundMethod= Checks.findMethod(newName, parameterCount, false, new IMethod[]{m});
				if (foundMethod == null)
					continue;
				if (isSubclass
						|| type.equals(clazz)
						|| (!isMethodPrivate && !JdtFlags.isPrivate(m))) {
					result.add(foundMethod);
				}
			}
		}
		return result.toArray(new IMethod[result.size()]);
	}

	final static IMethod[] hierarchyDeclaresMethodName(IProgressMonitor pm, ITypeHierarchy hierarchy, IMethod method, String newName) throws CoreException {
		try {
			Set<IMethod> result= new HashSet<>();
			IType type= method.getDeclaringType();
			IMethod foundMethod= Checks.findMethod(newName, method.getParameterTypes().length, false, type);
			if (foundMethod != null)
				result.add(foundMethod);
			IMethod[] foundInHierarchyClasses= classesDeclareMethodName(hierarchy, Arrays.asList(hierarchy.getAllClasses()), method, newName);
			if (foundInHierarchyClasses != null)
				result.addAll(Arrays.asList(foundInHierarchyClasses));
			IType[] implementingClasses= hierarchy.getImplementingClasses(type);
			IMethod[] foundInImplementingClasses= classesDeclareMethodName(hierarchy, Arrays.asList(implementingClasses), method, newName);
			if (foundInImplementingClasses != null)
				result.addAll(Arrays.asList(foundInImplementingClasses));
			return result.toArray(new IMethod[result.size()]);
		} finally {
			if (pm != null) {
				pm.done();
			}
		}
	}

	@Override
	public Change createChange(IProgressMonitor monitor) throws CoreException {
		try {
			final TextChange[] changes= fChangeManager.getAllChanges();
			final List<TextChange> list= new ArrayList<>(changes.length);
			list.addAll(Arrays.asList(changes));
			String project= null;
			IJavaProject javaProject= fMethod.getJavaProject();
			if (javaProject != null)
				project= javaProject.getElementName();
			int flags= JavaRefactoringDescriptor.JAR_MIGRATION | JavaRefactoringDescriptor.JAR_REFACTORING | RefactoringDescriptor.STRUCTURAL_CHANGE;
			try {
				if (!Flags.isPrivate(fMethod.getFlags()))
					flags|= RefactoringDescriptor.MULTI_CHANGE;
			} catch (JavaModelException exception) {
				JavaPlugin.log(exception);
			}
			final IType declaring= fMethod.getDeclaringType();
			try {
				if (declaring.isAnonymous() || declaring.isLocal())
					flags|= JavaRefactoringDescriptor.JAR_SOURCE_ATTACHMENT;
			} catch (JavaModelException exception) {
				JavaPlugin.log(exception);
			}
			final String description= Messages.format(RefactoringCoreMessages.RenameMethodProcessor_descriptor_description_short, BasicElementLabels.getJavaElementName(fMethod.getElementName()));
			final String header= Messages.format(RefactoringCoreMessages.RenameMethodProcessor_descriptor_description, new String[] { JavaElementLabels.getTextLabel(fMethod, JavaElementLabels.ALL_FULLY_QUALIFIED), BasicElementLabels.getJavaElementName(getNewElementName())});
			final String comment= new JDTRefactoringDescriptorComment(project, this, header).asString();
			final RenameJavaElementDescriptor descriptor= RefactoringSignatureDescriptorFactory.createRenameJavaElementDescriptor(IJavaRefactorings.RENAME_METHOD);
			descriptor.setProject(project);
			descriptor.setDescription(description);
			descriptor.setComment(comment);
			descriptor.setFlags(flags);
			descriptor.setJavaElement(fMethod);
			descriptor.setNewName(getNewElementName());
			descriptor.setUpdateReferences(fUpdateReferences);
			descriptor.setKeepOriginal(fDelegateUpdating);
			descriptor.setDeprecateDelegate(fDelegateDeprecation);
			return new DynamicValidationRefactoringChange(descriptor, RefactoringCoreMessages.RenameMethodProcessor_change_name, list.toArray(new Change[list.size()]));
		} finally {
			monitor.done();
		}
	}

	protected TextChangeManager createChanges(IProgressMonitor pm, RefactoringStatus status) throws CoreException {
		if (!fIsComposite)
			fChangeManager.clear();
		addOccurrences(fChangeManager, pm, status);
		return fChangeManager;
	}

	/**
	 * Add occurrences
	 *
	 * @param manager the text change manager
	 * @param pm the progress monitor
	 * @param status the status
	 * @throws CoreException if change creation failed
	 */
	protected void addOccurrences(TextChangeManager manager, IProgressMonitor pm, RefactoringStatus status) throws CoreException/*thrown in subtype*/{
		pm.beginTask("", fOccurrences.length);				 //$NON-NLS-1$
		for (SearchResultGroup occurrence : fOccurrences) {
			ICompilationUnit cu= occurrence.getCompilationUnit();
			if (cu == null)
				continue;
			// Split matches into declaration and non-declaration matches

			List<SearchMatch> declarationsInThisCu= new ArrayList<>();
			List<SearchMatch> referencesInThisCu= new ArrayList<>();
			for (SearchMatch result : occurrence.getSearchResults()) {
				if (result instanceof MethodDeclarationMatch) {
					declarationsInThisCu.add(result);
				} else {
					referencesInThisCu.add(result);
				}
			}
			// First, handle the declarations
			if (declarationsInThisCu.size() > 0) {

				if (fDelegateUpdating) {
					// Update with delegates
					CompilationUnitRewrite rewrite= new CompilationUnitRewrite(cu);
					rewrite.setResolveBindings(true);

					for (SearchMatch element : declarationsInThisCu) {
						MethodDeclaration method= ASTNodeSearchUtil.getMethodDeclarationNode((IMethod) element.getElement(), rewrite.getRoot());
						DelegateCreator creator= new DelegateMethodCreator();
						creator.setDeclareDeprecated(fDelegateDeprecation);
						creator.setDeclaration(method);
						creator.setSourceRewrite(rewrite);
						creator.setNewElementName(getNewElementName());
						creator.prepareDelegate();
						creator.createEdit();
					}
					// Need to handle all delegates first as this
					// creates a completely new change object.
					TextChange changeForThisCu= rewrite.createChange(true);
					changeForThisCu.setKeepPreviewEdits(true);
					manager.manage(cu, changeForThisCu);
				}

				// Update the normal methods
				for (SearchMatch element : declarationsInThisCu) {
					simpleUpdate(element, cu, manager.get(cu));
				}
			}

			// Second, handle references
			if (fUpdateReferences) {
				for (SearchMatch element : referencesInThisCu) {
					simpleUpdate(element, cu, manager.get(cu));
				}
			}

			pm.worked(1);
			if (pm.isCanceled())
				throw new OperationCanceledException();
		}
		pm.done();
	}

	private void simpleUpdate(SearchMatch element, ICompilationUnit cu, TextChange textChange) {
		String editName= RefactoringCoreMessages.RenameMethodRefactoring_update_occurrence;
		ReplaceEdit replaceEdit= createReplaceEdit(element, cu);
		addTextEdit(textChange, editName, replaceEdit);
	}

	protected final ReplaceEdit createReplaceEdit(SearchMatch searchResult, ICompilationUnit cu) {
		if (searchResult.isImplicit()) { // handle Annotation Element references, see bug 94062
			StringBuilder sb= new StringBuilder(getNewElementName());
			if (JavaCore.INSERT.equals(cu.getJavaProject().getOption(DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_BEFORE_ASSIGNMENT_OPERATOR, true)))
				sb.append(' ');
			sb.append('=');
			if (JavaCore.INSERT.equals(cu.getJavaProject().getOption(DefaultCodeFormatterConstants.FORMATTER_INSERT_SPACE_AFTER_ASSIGNMENT_OPERATOR, true)))
				sb.append(' ');
			return new ReplaceEdit(searchResult.getOffset(), 0, sb.toString());
		} else {
			return new ReplaceEdit(searchResult.getOffset(), searchResult.getLength(), getNewElementName());
		}
	}

	/**
	 * Initializes the refactoring from scripting arguments.
	 * Used by {@link RenameVirtualMethodProcessor} and {@link RenameNonVirtualMethodProcessor}
	 *
	 * @param extended the arguments
	 * @return the resulting status
	 */
	protected final RefactoringStatus initialize(JavaRefactoringArguments extended) {
		fInitialized= true;
		final String handle= extended.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT);
		if (handle != null) {
			final IJavaElement element= JavaRefactoringDescriptorUtil.handleToElement(extended.getProject(), handle, false);
			final String refactoring= getProcessorName();
			if (element instanceof IMethod) {
				final IMethod method= (IMethod) element;
				final IType declaring= method.getDeclaringType();
				if (declaring != null && declaring.exists()) {
					final IMethod[] methods= declaring.findMethods(method);
					if (methods != null && methods.length == 1 && methods[0] != null) {
						if (!methods[0].exists())
							return JavaRefactoringDescriptorUtil.createInputFatalStatus(methods[0], refactoring, IJavaRefactorings.RENAME_METHOD);
						assignMethod(methods[0]);
						initializeWorkingCopyOwner();
					} else
						return JavaRefactoringDescriptorUtil.createInputFatalStatus(null, refactoring, IJavaRefactorings.RENAME_METHOD);
				} else
					return JavaRefactoringDescriptorUtil.createInputFatalStatus(element, refactoring, IJavaRefactorings.RENAME_METHOD);
			} else
				return JavaRefactoringDescriptorUtil.createInputFatalStatus(element, refactoring, IJavaRefactorings.RENAME_METHOD);
		} else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, JavaRefactoringDescriptorUtil.ATTRIBUTE_INPUT));
		final String name= extended.getAttribute(JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME);
		if (name != null && !"".equals(name)) //$NON-NLS-1$
			setNewElementName(name);
		else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, JavaRefactoringDescriptorUtil.ATTRIBUTE_NAME));
		final String references= extended.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 delegate= extended.getAttribute(ATTRIBUTE_DELEGATE);
		if (delegate != null) {
			fDelegateUpdating= Boolean.parseBoolean(delegate);
		} else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, ATTRIBUTE_DELEGATE));
		final String deprecate= extended.getAttribute(ATTRIBUTE_DEPRECATE);
		if (deprecate != null) {
			fDelegateDeprecation= Boolean.parseBoolean(deprecate);
		} else
			return RefactoringStatus.createFatalErrorStatus(Messages.format(RefactoringCoreMessages.InitializableRefactoring_argument_not_exist, ATTRIBUTE_DEPRECATE));
		return new RefactoringStatus();
	}

	protected void addTextEdit(TextChange change, String editName, ReplaceEdit replaceEdit) {
		if (fIsComposite)
			TextChangeCompatibility.addTextEdit(change, editName, replaceEdit, fCategorySet);
		else
			TextChangeCompatibility.addTextEdit(change, editName, replaceEdit);

	}
}
