content: /*******************************************************************************
   * Copyright (c) 2000, 2008 IBM Corporation and others.
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   *
   * Contributors:
   *     IBM Corporation - initial API and implementation
   *******************************************************************************/
  
  package org.eclipse.wst.jsdt.internal.ui.text.correction;
  
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.swt.graphics.Image;
  import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
  import org.eclipse.wst.jsdt.core.dom.AST;
  import org.eclipse.wst.jsdt.core.dom.ASTNode;
  import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;
  import org.eclipse.wst.jsdt.core.dom.IBinding;
  import org.eclipse.wst.jsdt.core.dom.IFunctionBinding;
  import org.eclipse.wst.jsdt.core.dom.ITypeBinding;
  import org.eclipse.wst.jsdt.core.dom.IVariableBinding;
  import org.eclipse.wst.jsdt.core.dom.JSdoc;
  import org.eclipse.wst.jsdt.core.dom.FunctionDeclaration;
  import org.eclipse.wst.jsdt.core.dom.Name;
  import org.eclipse.wst.jsdt.core.dom.SimpleName;
  import org.eclipse.wst.jsdt.core.dom.SingleVariableDeclaration;
  import org.eclipse.wst.jsdt.core.dom.TagElement;
  import org.eclipse.wst.jsdt.core.dom.TextElement;
  import org.eclipse.wst.jsdt.core.dom.Type;
  import org.eclipse.wst.jsdt.core.dom.rewrite.ASTRewrite;
  import org.eclipse.wst.jsdt.core.dom.rewrite.ImportRewrite;
  import org.eclipse.wst.jsdt.core.dom.rewrite.ListRewrite;
  import org.eclipse.wst.jsdt.internal.corext.codemanipulation.StubUtility;
  import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodeFactory;
  import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodes;
  import org.eclipse.wst.jsdt.internal.corext.dom.Bindings;
  import org.eclipse.wst.jsdt.internal.corext.dom.LinkedNodeFinder;
  import org.eclipse.wst.jsdt.internal.corext.dom.ScopeAnalyzer;
  
  public class ChangeMethodSignatureProposal extends LinkedCorrectionProposal {
  
  	public static interface ChangeDescription {
  	}
  
  	public static class SwapDescription implements ChangeDescription {
  		final int index;
  		public SwapDescription(int index) {
  			this.index= index;
  		}
  	}
  
  	public static class RemoveDescription implements ChangeDescription {
  	}
  
  	static class ModifyDescription implements ChangeDescription {
  		public final String name;
  		public final ITypeBinding type;
  		Type resultingParamType;
  		SimpleName[] resultingParamName;
  		SimpleName resultingTagArg;
  
  		private ModifyDescription(ITypeBinding type, String name) {
  			this.type= type;
  			this.name= name;
  		}
  	}
  
  	public static class EditDescription extends ModifyDescription {
  		String orginalName;
  
  		public EditDescription(ITypeBinding type, String name) {
  			super(type, name);
  		}
  	}
  
  	public static class InsertDescription extends ModifyDescription {
  		public InsertDescription(ITypeBinding type, String name) {
  			super(type, name);
  		}
  	}
  
  	private ASTNode fInvocationNode;
  	private IFunctionBinding fSenderBinding;
  	private ChangeDescription[] fParameterChanges;
  	private ChangeDescription[] fExceptionChanges;
  
  	public ChangeMethodSignatureProposal(String label, IJavaScriptUnit targetCU, ASTNode invocationNode, IFunctionBinding binding, ChangeDescription[] paramChanges, ChangeDescription[] exceptionChanges, int relevance, Image image) {
  		super(label, targetCU, null, relevance, image);
  
  		Assert.isTrue(binding != null && Bindings.isDeclarationBinding(binding));
  
  		fInvocationNode= invocationNode;
  		fSenderBinding= binding;
  		fParameterChanges= paramChanges;
  		fExceptionChanges= exceptionChanges;
  	}
  
  	protected ASTRewrite getRewrite() throws CoreException {
  		JavaScriptUnit astRoot= (JavaScriptUnit) fInvocationNode.getRoot();
  		ASTNode methodDecl= astRoot.findDeclaringNode(fSenderBinding);
  		ASTNode newMethodDecl= null;
  		boolean isInDifferentCU;
  		if (methodDecl != null) {
  			isInDifferentCU= false;
  			newMethodDecl= methodDecl;
  		} else {
  			isInDifferentCU= true;
  			astRoot= ASTResolving.createQuickFixAST(getCompilationUnit(), null);
  			newMethodDecl= astRoot.findDeclaringNode(fSenderBinding.getKey());
  		}
  		createImportRewrite(astRoot);
  		
  		if (newMethodDecl instanceof FunctionDeclaration) {
  			FunctionDeclaration decl= (FunctionDeclaration) newMethodDecl;
  
  			ASTRewrite rewrite= ASTRewrite.create(astRoot.getAST());
  			if (fParameterChanges != null) {
  				modifyParameters(rewrite, decl, isInDifferentCU);
  			}
  			if (fExceptionChanges != null) {
  				modifyExceptions(rewrite, decl);
  			}
  			return rewrite;
  		}
  		return null;
  	}
  
  	private void modifyParameters(ASTRewrite rewrite, FunctionDeclaration methodDecl, boolean isInDifferentCU) throws CoreException {
  		AST ast= methodDecl.getAST();
  
  		ArrayList usedNames= new ArrayList();
  		boolean hasCreatedVariables= false;
  
  		IVariableBinding[] declaredFields= fSenderBinding.getDeclaringClass().getDeclaredFields();
  		for (int i= 0; i < declaredFields.length; i++) { // avoid to take parameter names that are equal to field names
  			usedNames.add(declaredFields[i].getName());
  		}
  
  		ImportRewrite imports= getImportRewrite();
  		ListRewrite listRewrite= rewrite.getListRewrite(methodDecl, FunctionDeclaration.PARAMETERS_PROPERTY);
  
  		List parameters= methodDecl.parameters(); // old parameters
  		int k= 0; // index over the oldParameters
  
  		for (int i= 0; i < fParameterChanges.length; i++) {
  			ChangeDescription curr= fParameterChanges[i];
  
  			if (curr == null) {
  				SingleVariableDeclaration oldParam= (SingleVariableDeclaration) parameters.get(k);
  				usedNames.add(oldParam.getName().getIdentifier());
  				k++;
  			} else if (curr instanceof InsertDescription) {
  				InsertDescription desc= (InsertDescription) curr;
  				SingleVariableDeclaration newNode= ast.newSingleVariableDeclaration();
  				newNode.setType(imports.addImport(desc.type, ast));
  				newNode.setName(ast.newSimpleName("x")); //$NON-NLS-1$
  
  				// remember to set name later
  				desc.resultingParamName= new SimpleName[] {newNode.getName()};
  				desc.resultingParamType= newNode.getType();
  				hasCreatedVariables= true;
  
  				listRewrite.insertAt(newNode, i, null);
  
  				JSdoc javadoc= methodDecl.getJavadoc();
  				if (javadoc != null) {
  					TagElement newTagElement= ast.newTagElement();
  					newTagElement.setTagName(TagElement.TAG_PARAM);
  					SimpleName arg= ast.newSimpleName("x"); //$NON-NLS-1$
  					newTagElement.fragments().add(arg);
  					insertTabStop(rewrite, newTagElement.fragments(), "param_tagcomment" + i); //$NON-NLS-1$
  					insertParamTag(rewrite.getListRewrite(javadoc, JSdoc.TAGS_PROPERTY), parameters, k, newTagElement);
  					desc.resultingTagArg= arg; // set the name later
  				} else {
  					desc.resultingTagArg= null;
  				}
  			} else if (curr instanceof RemoveDescription) {
  				SingleVariableDeclaration decl= (SingleVariableDeclaration) parameters.get(k);
  
  				listRewrite.remove(decl, null);
  				k++;
  
  				TagElement tagNode= findParamTag(methodDecl, decl);
  				if (tagNode != null) {
  					rewrite.remove(tagNode, null);
  				}
  			} else if (curr instanceof EditDescription) {
  				EditDescription desc= (EditDescription) curr;
  
  				ITypeBinding newTypeBinding= desc.type;
  				SingleVariableDeclaration decl= (SingleVariableDeclaration) parameters.get(k);
  				
  				if (k == parameters.size() - 1 && i == fParameterChanges.length - 1 && decl.isVarargs() && newTypeBinding.isArray()) {
  					newTypeBinding= newTypeBinding.getElementType(); // stick with varargs if it was before
  				} else {
  					rewrite.set(decl, SingleVariableDeclaration.VARARGS_PROPERTY, Boolean.FALSE, null);
  				}
  				
  				Type newType= imports.addImport(newTypeBinding, ast);
  				rewrite.replace(decl.getType(), newType, null);
  				rewrite.set(decl, SingleVariableDeclaration.EXTRA_DIMENSIONS_PROPERTY, Integer.valueOf(0), null);
  				
  				IBinding binding= decl.getName().resolveBinding();
  				if (binding != null) {
  					SimpleName[] names= LinkedNodeFinder.findByBinding(decl.getRoot(), binding);
  					SimpleName[] newNames= new SimpleName[names.length];
  					for (int j= 0; j < names.length; j++) {
  						SimpleName newName= ast.newSimpleName("x"); //$NON-NLS-1$  // name will be set later
  						newNames[j]= newName;
  						rewrite.replace(names[j], newName, null); 
  						
  					}
  					desc.resultingParamName= newNames;
  				} else {
  					SimpleName newName= ast.newSimpleName("x"); //$NON-NLS-1$  // name will be set later
  					rewrite.replace(decl.getName(), newName, null); 
  					// remember to set name later
  					desc.resultingParamName= new SimpleName[] {newName};
  				}
  
  				desc.resultingParamType= newType;
  				desc.orginalName= decl.getName().getIdentifier();
  				hasCreatedVariables= true;
  
  				k++;
  
  				TagElement tagNode= findParamTag(methodDecl, decl);
  				if (tagNode != null) {
  					List fragments= tagNode.fragments();
  					if (!fragments.isEmpty()) {
  						SimpleName arg= ast.newSimpleName("x"); //$NON-NLS-1$
  						rewrite.replace((ASTNode) fragments.get(0), arg, null);
  						desc.resultingTagArg= arg;
  					}
  				}
  
  			} else if (curr instanceof SwapDescription) {
  				SingleVariableDeclaration decl1= (SingleVariableDeclaration) parameters.get(k);
  				SingleVariableDeclaration decl2= (SingleVariableDeclaration) parameters.get(((SwapDescription) curr).index);
  
  				rewrite.replace(decl1, rewrite.createCopyTarget(decl2), null);
  				rewrite.replace(decl2, rewrite.createCopyTarget(decl1), null);
  
  				usedNames.add(decl1.getName().getIdentifier());
  				k++;
  
  				TagElement tagNode1= findParamTag(methodDecl, decl1);
  				TagElement tagNode2= findParamTag(methodDecl, decl2);
  				if (tagNode1 != null && tagNode2 != null) {
  					rewrite.replace(tagNode1, rewrite.createCopyTarget(tagNode2), null);
  					rewrite.replace(tagNode2, rewrite.createCopyTarget(tagNode1), null);
  				}
  			}
  		}
  		if (!hasCreatedVariables) {
  			return;
  		}
  
  		if (methodDecl.getBody() != null) {
  			// avoid take a name of a local variable inside
  			JavaScriptUnit root= (JavaScriptUnit) methodDecl.getRoot();
  			IBinding[] bindings= (new ScopeAnalyzer(root)).getDeclarationsAfter(methodDecl.getBody().getStartPosition(), ScopeAnalyzer.VARIABLES);
  			for (int i= 0; i < bindings.length; i++) {
  				usedNames.add(bindings[i].getName());
  			}
  		}
  
  		fixupNames(rewrite, usedNames, methodDecl, isInDifferentCU);
  	}
  
  	private void fixupNames(ASTRewrite rewrite, ArrayList usedNames, FunctionDeclaration methodDecl, boolean isInDifferentCU) {
  		AST ast= rewrite.getAST();
  		// set names for new parameters
  		for (int i= 0; i < fParameterChanges.length; i++) {
  			ChangeDescription curr= fParameterChanges[i];
  			if (curr instanceof ModifyDescription) {
  				ModifyDescription desc= (ModifyDescription) curr;
  
  				String typeKey= getParamTypeGroupId(i);
  				String nameKey= getParamNameGroupId(i);
  
  				// collect name suggestions
  				String favourite= null;
  				String[] excludedNames= (String[]) usedNames.toArray(new String[usedNames.size()]);
  				
  				String suggestedName= desc.name;
  				if (suggestedName != null) {
  					favourite= StubUtility.suggestArgumentName(getCompilationUnit().getJavaScriptProject(), suggestedName, excludedNames);
  					addLinkedPositionProposal(nameKey, favourite, null);
  				}
  				
  				if (desc instanceof EditDescription) {
  					addLinkedPositionProposal(nameKey, ((EditDescription)desc).orginalName, null);
  				}
  				
  				Type type= desc.resultingParamType;
  				String[] suggestedNames= StubUtility.getArgumentNameSuggestions(getCompilationUnit().getJavaScriptProject(), type, excludedNames);
  				for (int k= 0; k < suggestedNames.length; k++) {
  					addLinkedPositionProposal(nameKey, suggestedNames[k], null);
  				}
  				if (favourite == null) {
  					favourite= suggestedNames[0];
  				}		
  				usedNames.add(favourite);
  				
  				SimpleName[] names= desc.resultingParamName;
  				for (int j= 0; j < names.length; j++) {
  					names[j].setIdentifier(favourite);
  					addLinkedPosition(rewrite.track(names[j]), false, nameKey);
  				}
  				
  				addLinkedPosition(rewrite.track(desc.resultingParamType), true, typeKey);
  
  				// collect type suggestions
  				ITypeBinding[] bindings= ASTResolving.getRelaxingTypes(ast, desc.type);
  				for (int k= 0; k < bindings.length; k++) {
  					addLinkedPositionProposal(typeKey, bindings[k]);
  				}
  
  				SimpleName tagArg= desc.resultingTagArg;
  				if (tagArg != null) {
  					tagArg.setIdentifier(favourite);
  					addLinkedPosition(rewrite.track(tagArg), false, nameKey);
  				}
  			}
  		}
  	}
  
  	private TagElement findParamTag(FunctionDeclaration decl, SingleVariableDeclaration param) {
  		JSdoc javadoc= decl.getJavadoc();
  		if (javadoc != null) {
  			return JavadocTagsSubProcessor.findParamTag(javadoc, param.getName().getIdentifier());
  		}
  		return null;
  	}
  
  	private TagElement insertParamTag(ListRewrite tagRewriter, List parameters, int currentIndex, TagElement newTagElement) {
  		HashSet previousNames= new HashSet();
  		for (int n = 0; n < currentIndex; n++) {
  			SingleVariableDeclaration var= (SingleVariableDeclaration) parameters.get(n);
  			previousNames.add(var.getName().getIdentifier());
  		}
  
  		JavadocTagsSubProcessor.insertTag(tagRewriter, newTagElement, previousNames);
  		return newTagElement;
  	}
  
  	private void modifyExceptions(ASTRewrite rewrite, FunctionDeclaration methodDecl) throws CoreException {
  		AST ast= methodDecl.getAST();
  
  		ImportRewrite imports= getImportRewrite();
  		ListRewrite listRewrite= rewrite.getListRewrite(methodDecl, FunctionDeclaration.THROWN_EXCEPTIONS_PROPERTY);
  
  		List exceptions= methodDecl.thrownExceptions(); // old exceptions
  		int k= 0; // index over the old exceptions
  
  		for (int i= 0; i < fExceptionChanges.length; i++) {
  			ChangeDescription curr= fExceptionChanges[i];
  
  			if (curr == null) {
  				k++;
  			} else if (curr instanceof InsertDescription) {
  				InsertDescription desc= (InsertDescription) curr;
  				String type= imports.addImport(desc.type);
  				ASTNode newNode= ASTNodeFactory.newName(ast, type);
  
  				listRewrite.insertAt(newNode, i, null);
  
  				String key= getExceptionTypeGroupId(i);
  				addLinkedPosition(rewrite.track(newNode), false, key);
  
  				JSdoc javadoc= methodDecl.getJavadoc();
  				if (javadoc != null) {
  					TagElement newTagElement= ast.newTagElement();
  					newTagElement.setTagName(TagElement.TAG_THROWS);
  					ASTNode newRef= ASTNodeFactory.newName(ast, type);
  					newTagElement.fragments().add(newRef);
  					insertTabStop(rewrite, newTagElement.fragments(), "throws_tagcomment" + i); //$NON-NLS-1$
  					insertThrowsTag(rewrite.getListRewrite(javadoc, JSdoc.TAGS_PROPERTY), exceptions, k, newTagElement);
  
  					addLinkedPosition(rewrite.track(newRef), false, key);
  				}
  
  			} else if (curr instanceof RemoveDescription) {
  				Name node= (Name) exceptions.get(k);
  
  				listRewrite.remove(node, null);
  				k++;
  
  				TagElement tagNode= findThrowsTag(methodDecl, node);
  				if (tagNode != null) {
  					rewrite.remove(tagNode, null);
  				}
  			} else if (curr instanceof EditDescription) {
  				EditDescription desc= (EditDescription) curr;
  
  				Name oldNode= (Name) exceptions.get(k);
  
  				String type= imports.addImport(desc.type);
  				ASTNode newNode= ASTNodeFactory.newName(ast, type);
  
  				listRewrite.replace(oldNode, newNode, null);
  				String key= getExceptionTypeGroupId(i);
  				addLinkedPosition(rewrite.track(newNode), false, key);
  
  				k++;
  
  				TagElement tagNode= findThrowsTag(methodDecl, oldNode);
  				if (tagNode != null) {
  					ASTNode newRef= ASTNodeFactory.newName(ast, type);
  					rewrite.replace((ASTNode) tagNode.fragments().get(0), newRef, null);
  					addLinkedPosition(rewrite.track(newRef), false, key);
  				}
  
  			} else if (curr instanceof SwapDescription) {
  				Name decl1= (Name) exceptions.get(k);
  				Name decl2= (Name) exceptions.get(((SwapDescription) curr).index);
  
  				rewrite.replace(decl1, rewrite.createCopyTarget(decl2), null);
  				rewrite.replace(decl2, rewrite.createCopyTarget(decl1), null);
  
  				k++;
  
  				TagElement tagNode1= findThrowsTag(methodDecl, decl1);
  				TagElement tagNode2= findThrowsTag(methodDecl, decl2);
  				if (tagNode1 != null && tagNode2 != null) {
  					rewrite.replace(tagNode1, rewrite.createCopyTarget(tagNode2), null);
  					rewrite.replace(tagNode2, rewrite.createCopyTarget(tagNode1), null);
  				}
  			}
  		}
  	}
  
  	private void insertTabStop(ASTRewrite rewriter, List fragments, String linkedName) {
  		TextElement textElement= rewriter.getAST().newTextElement();
  		textElement.setText(""); //$NON-NLS-1$
  		fragments.add(textElement);
  		addLinkedPosition(rewriter.track(textElement), false, linkedName);
  	}
  
  	private TagElement findThrowsTag(FunctionDeclaration decl, Name exception) {
  		JSdoc javadoc= decl.getJavadoc();
  		if (javadoc != null) {
  			String name= ASTNodes.getSimpleNameIdentifier(exception);
  			return JavadocTagsSubProcessor.findThrowsTag(javadoc, name);
  		}
  		return null;
  	}
  
  	private TagElement insertThrowsTag(ListRewrite tagRewriter, List exceptions, int currentIndex, TagElement newTagElement) {
  		HashSet previousNames= new HashSet();
  		for (int n = 0; n < currentIndex; n++) {
  			Name curr= (Name) exceptions.get(n);
  			previousNames.add(ASTNodes.getSimpleNameIdentifier(curr));
  		}
  
  		JavadocTagsSubProcessor.insertTag(tagRewriter, newTagElement, previousNames);
  		return newTagElement;
  	}
  
  
  	public String getParamNameGroupId(int idx) {
  		return "param_name_" + idx; //$NON-NLS-1$
  	}
  
  	public String getParamTypeGroupId(int idx) {
  		return "param_type_" + idx; //$NON-NLS-1$
  	}
  
  	public String getExceptionTypeGroupId(int idx) {
  		return "exc_type_" + idx; //$NON-NLS-1$
  	}
  
  }
  