/*******************************************************************************
 * Copyright (c) 2000, 2006 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
 *     Dmitry Stalnov (dstalnov@fusionone.com) - contributed fixes for:
 *       o bug "inline method - doesn't handle implicit cast" (see
 *         https://bugs.eclipse.org/bugs/show_bug.cgi?id=24941).
 *       o bug inline method: compile error (array related)
 *         (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38471)
 *       o inline call that is used in a field initializer 
 *         (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38137)
 *       o inline call a field initializer: could detect self reference 
 *         (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=44417)
 *       o Allow 'this' constructor to be inlined  
 *         (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38093)
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.code;

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

import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;

import org.eclipse.core.filebuffers.ITextFileBuffer;

import org.eclipse.jface.text.BadLocationException;

import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ArrayCreation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.LabeledStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.WhileStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;

import org.eclipse.jdt.internal.corext.Corext;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility;
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.CodeScopeBuilder;
import org.eclipse.jdt.internal.corext.dom.HierarchicalASTVisitor;
import org.eclipse.jdt.internal.corext.dom.LocalVariableIndex;
import org.eclipse.jdt.internal.corext.dom.Selection;
import org.eclipse.jdt.internal.corext.dom.TypeBindingVisitor;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;
import org.eclipse.jdt.internal.corext.refactoring.base.JavaStatusContext;
import org.eclipse.jdt.internal.corext.refactoring.base.RefactoringStatusCodes;
import org.eclipse.jdt.internal.corext.refactoring.code.flow.FlowContext;
import org.eclipse.jdt.internal.corext.refactoring.code.flow.FlowInfo;
import org.eclipse.jdt.internal.corext.refactoring.code.flow.InputFlowAnalyzer;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TypeEnvironment;
import org.eclipse.jdt.internal.corext.refactoring.util.NoCommentSourceRangeComputer;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringFileBuffers;

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

public class CallInliner {

	private ICompilationUnit fCUnit;
	private ASTRewrite fRewrite;
	private ImportRewrite fImportRewrite;
	private ITextFileBuffer fBuffer;
	private SourceProvider fSourceProvider;
	private TypeEnvironment fTypeEnvironment;
	
	private BodyDeclaration fBodyDeclaration;
	private CodeScopeBuilder.Scope fRootScope;
	private int fNumberOfLocals;
	
	private ASTNode fInvocation;
	
	private int fInsertionIndex;
	private ListRewrite fListRewrite;
	
	private boolean fNeedsStatement;
	private ASTNode fTargetNode;
	private FlowContext fFlowContext;
	private FlowInfo fFlowInfo;
	private CodeScopeBuilder.Scope fInvocationScope;
	private boolean fFieldInitializer;
	private List fLocals;
	private CallContext fContext;
	
	private class InlineEvaluator extends HierarchicalASTVisitor {
		private ParameterData fFormalArgument;
		private boolean fResult;
		public InlineEvaluator(ParameterData argument) {
			fFormalArgument= argument;
		}
		public boolean getResult() {
			return fResult;
		}
		private boolean setResult(boolean result) {
			fResult= result;
			return false;
		}
		public boolean visit(Expression node) {
			int accessMode= fFormalArgument.getSimplifiedAccessMode();
			if (accessMode == FlowInfo.WRITE)
				return setResult(false);
			if (accessMode == FlowInfo.UNUSED)
				return setResult(true);
			if (ASTNodes.isLiteral(node))
				return setResult(true);
			return setResult(fFormalArgument.getNumberOfAccesses() <= 1);
		}
		public boolean visit(SimpleName node) {
			IBinding binding= node.resolveBinding();
			if (binding instanceof IVariableBinding) {
				int accessMode = fFormalArgument.getSimplifiedAccessMode();
				if (accessMode == FlowInfo.READ || accessMode == FlowInfo.UNUSED)
					return setResult(true);
				// from now on we only have write accesses.
				IVariableBinding vb= (IVariableBinding)binding;
				if (vb.isField())
					return setResult(false);
				return setResult(fFlowInfo.hasAccessMode(fFlowContext, vb, FlowInfo.UNUSED | FlowInfo.WRITE));
			}
			return setResult(false);
		}
		public boolean visit(FieldAccess node) {
			return visit(node.getName());
		}
		public boolean visit(SuperFieldAccess node) {
			return visit(node.getName());
		}
		public boolean visit(ThisExpression node) {
			int accessMode= fFormalArgument.getSimplifiedAccessMode();
			if (accessMode == FlowInfo.READ || accessMode == FlowInfo.UNUSED)
				return setResult(true);
			return setResult(false);
		}
	}

	private static class AmbiguousMethodAnalyzer implements TypeBindingVisitor {
		private TypeEnvironment fTypeEnvironment;
		private TType[] fTypes;
		private IMethodBinding fOriginal;

		public AmbiguousMethodAnalyzer(TypeEnvironment typeEnvironment, IMethodBinding original, TType[] types) {
			fTypeEnvironment= typeEnvironment;
			fOriginal= original;
			fTypes= types;
		}
		public boolean visit(ITypeBinding node) {
			IMethodBinding[] methods= node.getDeclaredMethods();
			for (int i= 0; i < methods.length; i++) {
				IMethodBinding candidate= methods[i];
				if (candidate == fOriginal) {
					continue;
				}
				if (fOriginal.getName().equals(candidate.getName())) {
					if (canImplicitlyCall(candidate)) {
						return false;
					}
				}
			}
			return true;
		}
		/**
		 * Returns <code>true</code> if the method can be called without explicit casts; 
		 * otherwise <code>false</code>.
		 */
		private boolean canImplicitlyCall(IMethodBinding candidate) {
			ITypeBinding[] parameters= candidate.getParameterTypes();
			if (parameters.length != fTypes.length) {
				return false;
			}
			for (int i= 0; i < parameters.length; i++) {
				if (!fTypes[i].canAssignTo(fTypeEnvironment.create(parameters[i]))) {
					return false;
				}
			}
			return true;
		}
	}

	public CallInliner(ICompilationUnit unit, CompilationUnit targetAstRoot, SourceProvider provider) throws CoreException {
		super();
		fCUnit= unit;
		fBuffer= RefactoringFileBuffers.acquire(fCUnit);
		fSourceProvider= provider;
		fImportRewrite= StubUtility.createImportRewrite(targetAstRoot, true);
		fLocals= new ArrayList(3);
		fRewrite= ASTRewrite.create(targetAstRoot.getAST());
		fRewrite.setTargetSourceRangeComputer(new NoCommentSourceRangeComputer());
		fTypeEnvironment= new TypeEnvironment();
	}

	public void dispose() {
		try {
			RefactoringFileBuffers.release(fCUnit);
		} catch (CoreException exception) {
			JavaPlugin.log(exception);
		}
	}
	
	
	public ImportRewrite getImportEdit() {
		return fImportRewrite;
	}
	
	public ASTNode getTargetNode() {
		return fTargetNode;
	}
	
	public void initialize(BodyDeclaration declaration) {
		fBodyDeclaration= declaration;
		fRootScope= CodeScopeBuilder.perform(declaration, fSourceProvider.getDeclaration().resolveBinding());
		fNumberOfLocals= 0;
		switch (declaration.getNodeType()) {
			case ASTNode.METHOD_DECLARATION:
			case ASTNode.INITIALIZER:
				fNumberOfLocals= LocalVariableIndex.perform(declaration);
				break;
		}
	}

	public RefactoringStatus initialize(ASTNode invocation, int severity) {
		RefactoringStatus result= new RefactoringStatus();
		fInvocation= invocation;
		fLocals= new ArrayList(3);
		
		checkMethodDeclaration(result, severity);
		if (result.getSeverity() >= severity)
			return result;
		
		initializeRewriteState();
		initializeTargetNode();
		flowAnalysis();
		
		fContext= new CallContext(fInvocation, fInvocationScope, fTargetNode.getNodeType(), fImportRewrite);
		
		try {
			computeRealArguments();
			computeReceiver();
		} catch (BadLocationException exception) {
			JavaPlugin.log(exception);
		}
		checkInvocationContext(result, severity);
		
		return result;
	}

	private void initializeRewriteState() {
		// field initializer can be inside of a block if used in a local class
		// but block can't be a child of field initializer
		if(ASTNodes.getParent(fInvocation, ASTNode.FIELD_DECLARATION) != null) {
			fFieldInitializer= true;
		}
	}

	private void initializeTargetNode() {
		ASTNode parent= fInvocation.getParent();
		int nodeType= parent.getNodeType();
		if (nodeType == ASTNode.EXPRESSION_STATEMENT || nodeType == ASTNode.RETURN_STATEMENT) {
			fTargetNode= parent;
		} else {
			fTargetNode= fInvocation;
		}
	}

	// the checks depend on invocation context and therefore can't be done in SourceAnalyzer
	private void checkMethodDeclaration(RefactoringStatus result, int severity) {
		MethodDeclaration methodDeclaration= fSourceProvider.getDeclaration();
		// it is not allowed to inline constructor invocation only if it is used for class instance creation
		// if constructor is invoked from another constructor then we can inline such invocation
		if (fInvocation.getNodeType() != ASTNode.CONSTRUCTOR_INVOCATION && methodDeclaration.isConstructor()) {
			result.addEntry(new RefactoringStatusEntry(
				severity,  
				RefactoringCoreMessages.CallInliner_constructors, 
				JavaStatusContext.create(fCUnit, fInvocation)));
		}
		if (fSourceProvider.hasSuperMethodInvocation() && fInvocation.getNodeType() == ASTNode.METHOD_INVOCATION) {
			Expression receiver= ((MethodInvocation)fInvocation).getExpression();
			if (receiver instanceof ThisExpression) {
				result.addEntry(new RefactoringStatusEntry(
					severity,
					RefactoringCoreMessages.CallInliner_super_into_this_expression,
					JavaStatusContext.create(fCUnit, fInvocation)));
			}
		}
	}

	private void checkInvocationContext(RefactoringStatus result, int severity) {
		if (fInvocation.getNodeType() == ASTNode.METHOD_INVOCATION) {
			Expression exp= ((MethodInvocation)fInvocation).getExpression();
			if (exp != null && exp.resolveTypeBinding() == null) {
				addEntry(result, RefactoringCoreMessages.CallInliner_receiver_type, 
					RefactoringStatusCodes.INLINE_METHOD_NULL_BINDING, severity);
				return;
			}
		}
		int nodeType= fTargetNode.getNodeType();
		if (nodeType == ASTNode.EXPRESSION_STATEMENT) {
			if (fSourceProvider.isExecutionFlowInterrupted()) {
				addEntry(result, RefactoringCoreMessages.CallInliner_execution_flow,  
					RefactoringStatusCodes.INLINE_METHOD_EXECUTION_FLOW, severity);
				return;
			}
		} else if (nodeType == ASTNode.METHOD_INVOCATION) {
			ASTNode parent= fTargetNode.getParent();
			if (isReturnStatement(parent)) {
				//support inlining even if the execution flow is interrupted
				return;
			}
			if (fSourceProvider.isExecutionFlowInterrupted()) {
				addEntry(result, RefactoringCoreMessages.CallInliner_execution_flow,  
					RefactoringStatusCodes.INLINE_METHOD_EXECUTION_FLOW, severity);
				return;
			}
			if (isAssignment(parent) || isSingleDeclaration(parent)) {
				// we support inlining expression in assigment and initializers as
				// long as the execution flow isn't interrupted.
				return;
			} else {
				boolean isFieldDeclaration= ASTNodes.getParent(fInvocation, FieldDeclaration.class) != null;
				if (!fSourceProvider.isSimpleFunction()) {
					if (isMultiDeclarationFragment(parent)) {
						addEntry(result, RefactoringCoreMessages.CallInliner_multiDeclaration, 
							RefactoringStatusCodes.INLINE_METHOD_INITIALIZER_IN_FRAGEMENT, severity);
					} else if (isFieldDeclaration) {
						addEntry(result,
							RefactoringCoreMessages.CallInliner_field_initializer_simple, 
							RefactoringStatusCodes.INLINE_METHOD_FIELD_INITIALIZER, severity);
					} else {
						addEntry(result, RefactoringCoreMessages.CallInliner_simple_functions, 
							RefactoringStatusCodes.INLINE_METHOD_ONLY_SIMPLE_FUNCTIONS, severity);
					}
					return;
				}
				if (isFieldDeclaration) {
					int argumentsCount= fContext.arguments.length;
					for (int i= 0; i < argumentsCount; i++) {
						ParameterData parameter= fSourceProvider.getParameterData(i);
						if(parameter.isWrite()) {
							addEntry(result,
								RefactoringCoreMessages.CallInliner_field_initialize_write_parameter, 
								RefactoringStatusCodes.INLINE_METHOD_FIELD_INITIALIZER, severity);
							return;
						}
					}
					if(fLocals.size() > 0) {
						addEntry(result,
							RefactoringCoreMessages.CallInliner_field_initialize_new_local, 
							RefactoringStatusCodes.INLINE_METHOD_FIELD_INITIALIZER, severity);
						return;
					}
					// verify that the field is not referenced by the initializer method 
					VariableDeclarationFragment variable= (VariableDeclarationFragment)ASTNodes.getParent(fInvocation, ASTNode.VARIABLE_DECLARATION_FRAGMENT);
					if(fSourceProvider.isVariableReferenced(variable.resolveBinding())) {
						addEntry(result,
							RefactoringCoreMessages.CallInliner_field_initialize_self_reference, 
							RefactoringStatusCodes.INLINE_METHOD_FIELD_INITIALIZER, severity);
						return;
					}
				}
			}
		}		
	}

	private static boolean isAssignment(ASTNode node) {
		return node instanceof Assignment;
	}
	
	private static boolean isReturnStatement(ASTNode node) {
		return node instanceof ReturnStatement;
	}
	
	private static boolean isSingleDeclaration(ASTNode node) {
		int type= node.getNodeType();
		if (type == ASTNode.SINGLE_VARIABLE_DECLARATION)
			return true;
		if (type == ASTNode.VARIABLE_DECLARATION_FRAGMENT) {
			node= node.getParent();
			if (node.getNodeType() == ASTNode.VARIABLE_DECLARATION_STATEMENT) {
				VariableDeclarationStatement vs= (VariableDeclarationStatement)node;
				return vs.fragments().size() == 1;
			}
		}
		return false;
	}
	
	private static boolean isMultiDeclarationFragment(ASTNode node) {
		int nodeType= node.getNodeType();
		if (nodeType == ASTNode.VARIABLE_DECLARATION_FRAGMENT) {
			node= node.getParent();
			if (node.getNodeType() == ASTNode.VARIABLE_DECLARATION_STATEMENT) {
				VariableDeclarationStatement vs= (VariableDeclarationStatement)node;
				return vs.fragments().size() > 1;
			}
		}
		return false;
	}
	
	private void addEntry(RefactoringStatus result, String message, int code, int severity) {
		result.addEntry(new RefactoringStatusEntry(
			severity, message, 
			JavaStatusContext.create(fCUnit, fInvocation),
			Corext.getPluginId(),
			code, null));
	}

	private void flowAnalysis() {
		fInvocationScope= fRootScope.findScope(fTargetNode.getStartPosition(), fTargetNode.getLength());
		fInvocationScope.setCursor(fTargetNode.getStartPosition());
		fFlowContext= new FlowContext(0, fNumberOfLocals + 1);
		fFlowContext.setConsiderAccessMode(true);
		fFlowContext.setComputeMode(FlowContext.ARGUMENTS);
		Selection selection= Selection.createFromStartLength(fInvocation.getStartPosition(), fInvocation.getLength());
		switch (fBodyDeclaration.getNodeType()) {
			case ASTNode.INITIALIZER:
			case ASTNode.FIELD_DECLARATION:
			case ASTNode.METHOD_DECLARATION:
				fFlowInfo= new InputFlowAnalyzer(fFlowContext, selection, true).perform(fBodyDeclaration);
				break;
			default:
				Assert.isTrue(false, "Should not happen");			 //$NON-NLS-1$
		}
	}
	
	public RefactoringStatus perform(TextEditGroup textEditGroup) throws CoreException {
		RefactoringStatus result= new RefactoringStatus();
		String[] blocks= fSourceProvider.getCodeBlocks(fContext);
		if(!fFieldInitializer) {
			initializeInsertionPoint(fSourceProvider.getNumberOfStatements() + fLocals.size());
		}
		
		addNewLocals(textEditGroup);
		replaceCall(result, blocks, textEditGroup);
		return result;
	}
	
	public TextEdit getModifications() {
		return fRewrite.rewriteAST(fBuffer.getDocument(), fCUnit.getJavaProject().getOptions(true));
	}

	private void computeRealArguments() throws BadLocationException {
		List arguments= Invocations.getArguments(fInvocation);
		Set canNotInline= crossCheckArguments(arguments);
		boolean needsVarargBoxing= needsVarargBoxing(arguments);
		int varargIndex= fSourceProvider.getVarargIndex();
		String[] realArguments= new String[needsVarargBoxing ? varargIndex + 1 : arguments.size()];
		for (int i= 0; i < (needsVarargBoxing ? varargIndex : arguments.size()); i++) {
			Expression expression= (Expression)arguments.get(i);
			ParameterData parameter= fSourceProvider.getParameterData(i);
			if (canInline(expression, parameter) && !canNotInline.contains(expression)) {
				realArguments[i] = getContent(expression);
				// fixes bugs #35905, #38471
				if (argumentNeedsParenthesis(expression, parameter)) {
					realArguments[i] = "(" + realArguments[i] + ")"; //$NON-NLS-1$ //$NON-NLS-2$
				}
			} else {
				String name= fInvocationScope.createName(parameter.getName(), true);
				realArguments[i]= name;
				fLocals.add(createLocalDeclaration(
					parameter.getTypeBinding(), name, 
					(Expression)fRewrite.createCopyTarget(expression)));
			}
		}
		if (needsVarargBoxing) {
			ParameterData parameter= fSourceProvider.getParameterData(varargIndex);
			String name= fInvocationScope.createName(parameter.getName(), true);
			realArguments[varargIndex]= name;
			AST ast= fInvocation.getAST();
			Type type= fImportRewrite.addImport(parameter.getTypeBinding(), ast);
			VariableDeclarationFragment fragment= ast.newVariableDeclarationFragment();
			fragment.setName(ast.newSimpleName(name));
			ArrayInitializer initializer= ast.newArrayInitializer();
			for (int i= varargIndex; i < arguments.size(); i++) {
				initializer.expressions().add(fRewrite.createCopyTarget((ASTNode)arguments.get(i)));
			}
			fragment.setInitializer(initializer);
			VariableDeclarationStatement decl= ast.newVariableDeclarationStatement(fragment);
			decl.setType(type);
			fLocals.add(decl);
		}
		fContext.arguments= realArguments;
	}
	
	private boolean needsVarargBoxing(List arguments) {
		if (!fSourceProvider.isVarargs())
			return false;
		/*
		if (!fSourceProvider.hasArrayAccess())
			return false;
		*/
		int index= fSourceProvider.getVarargIndex();
		// we have varags but the call doesn't pass any arguments
		if (index >= arguments.size())
			return true;
		// parameter is array type
		// one arg
		if (index == arguments.size() - 1) {
			ITypeBinding argument= ((Expression)arguments.get(index)).resolveTypeBinding();
			if (argument == null)
				return false;
			ITypeBinding parameter= fSourceProvider.getParameterData(index).getTypeBinding();
			return !fTypeEnvironment.create(argument).canAssignTo(fTypeEnvironment.create(parameter));
		}
		return true;
	}
	
	private boolean argumentNeedsParenthesis(Expression expression, ParameterData param) {
		if (expression instanceof CastExpression || expression instanceof ArrayCreation)
			return true;
		int argPrecedence= OperatorPrecedence.getValue(expression);
		int paramPrecedence= param.getOperatorPrecedence();
		if (argPrecedence != -1 && paramPrecedence != -1)
			return argPrecedence < paramPrecedence;
		
		return false;
	}
	
	private void computeReceiver() throws BadLocationException {
		Expression receiver= Invocations.getExpression(fInvocation);
		if (receiver == null)
			return;
		final boolean isName= receiver instanceof Name;
		if (isName)
			fContext.receiverIsStatic= ((Name)receiver).resolveBinding() instanceof ITypeBinding;
		if (ASTNodes.isLiteral(receiver) || isName || receiver instanceof ThisExpression) {
			fContext.receiver= fBuffer.getDocument().get(receiver.getStartPosition(), receiver.getLength());
			return;
		}
		switch(fSourceProvider.getReceiversToBeUpdated()) {
			case 0:
				// Make sure we evaluate the current receiver. Best is to assign to
				// local.
				fLocals.add(createLocalDeclaration(
					receiver.resolveTypeBinding(), 
					fInvocationScope.createName("r", true),  //$NON-NLS-1$
					(Expression)fRewrite.createCopyTarget(receiver)));
				return;
			case 1:
				fContext.receiver= fBuffer.getDocument().get(receiver.getStartPosition(), receiver.getLength());
				return;
			default:
				String local= fInvocationScope.createName("r", true); //$NON-NLS-1$
					fLocals.add(createLocalDeclaration(
					receiver.resolveTypeBinding(), 
					local, 
					(Expression)fRewrite.createCopyTarget(receiver)));
				fContext.receiver= local;
				return;
		}
	}

	private void addNewLocals(TextEditGroup textEditGroup) {
		if (fLocals.isEmpty())
			return;
		for (Iterator iter= fLocals.iterator(); iter.hasNext();) {
			ASTNode element= (ASTNode)iter.next();
			fListRewrite.insertAt(element, fInsertionIndex++, textEditGroup);
		}
	}

	private void replaceCall(RefactoringStatus status, String[] blocks, TextEditGroup textEditGroup) {
		// Inline empty body
		if (blocks.length == 0) {
			if (fNeedsStatement) {
				fRewrite.replace(fTargetNode, fTargetNode.getAST().newEmptyStatement(), textEditGroup);
			} else {
				fRewrite.remove(fTargetNode, textEditGroup);
			}
		} else {
			ASTNode node= null;
			for (int i= 0; i < blocks.length - 1; i++) {
				node= fRewrite.createStringPlaceholder(blocks[i], ASTNode.RETURN_STATEMENT);
				fListRewrite.insertAt(node, fInsertionIndex++, textEditGroup);
			}
			String block= blocks[blocks.length - 1];
			// We can inline a call where the declaration is a function and the call itself
			// is a statement. In this case we have to create a temporary variable if the
			// returned expression must be evaluated.
			if (fContext.callMode == ASTNode.EXPRESSION_STATEMENT && fSourceProvider.hasReturnValue()) {
				if (fSourceProvider.mustEvaluateReturnedExpression()) {
					if (fSourceProvider.returnValueNeedsLocalVariable()) {
						IMethodBinding invocation= Invocations.resolveBinding(fInvocation);
						node= createLocalDeclaration(
							invocation.getReturnType(), 
							fInvocationScope.createName(fSourceProvider.getMethodName(), true), 
							(Expression)fRewrite.createStringPlaceholder(block, ASTNode.METHOD_INVOCATION));
					} else {
						node= fTargetNode.getAST().newExpressionStatement(
							(Expression)fRewrite.createStringPlaceholder(block, ASTNode.METHOD_INVOCATION));
					}
				} else {
					node= null;
				}
			} else if (fTargetNode instanceof Expression) {
				node= fRewrite.createStringPlaceholder(block, ASTNode.METHOD_INVOCATION);
				
				// fixes bug #24941
				if(needsExplicitCast(status)) {
					AST ast= node.getAST();
					CastExpression castExpression= ast.newCastExpression();
					Type returnType= fImportRewrite.addImport(fSourceProvider.getReturnType(), ast);
					castExpression.setType(returnType);
					castExpression.setExpression((Expression)node);
					node= castExpression;
				}
				
				if (needsParenthesis()) {
					ParenthesizedExpression pExp= fTargetNode.getAST().newParenthesizedExpression();
					pExp.setExpression((Expression)node);
					node= pExp;
				}
			} else {
				node= fRewrite.createStringPlaceholder(block, ASTNode.RETURN_STATEMENT);
			}
			
			// Now replace the target node with the source node
			if (node != null) {
				if (fTargetNode == null) {
					fListRewrite.insertAt(node, fInsertionIndex++, textEditGroup);
				} else {
					fRewrite.replace(fTargetNode, node, textEditGroup);
				}
			} else {
				if (fTargetNode != null) {
					fRewrite.remove(fTargetNode, textEditGroup);
				}
			}
		}
	}

	/**
	 * @return <code>true</code> if explicit cast is needed otherwise <code>false</code>
	 */
	private boolean needsExplicitCast(RefactoringStatus status) {
		// if the return type of the method is the same as the type of the
		// returned expression then we don't need an explicit cast.
		if (fSourceProvider.returnTypeMatchesReturnExpressions())
				return false;		 
		ASTNode parent= fTargetNode.getParent();
		int nodeType= parent.getNodeType();
		if (nodeType == ASTNode.METHOD_INVOCATION) {
			MethodInvocation methodInvocation= (MethodInvocation)parent;
			if(methodInvocation.getExpression() == fTargetNode)
				return false;
			IMethodBinding method= methodInvocation.resolveMethodBinding();
			if (method == null) {
				status.addError(RefactoringCoreMessages.CallInliner_cast_analysis_error,  
					JavaStatusContext.create(fCUnit, methodInvocation));
				return false;
			}
			ITypeBinding[] parameters= method.getParameterTypes();
			int argumentIndex= methodInvocation.arguments().indexOf(fInvocation);
			List returnExprs= fSourceProvider.getReturnExpressions();
			// it is inferred that only methods consisting of a single 
			// return statement can be inlined as parameters in other 
			// method invocations
			if (returnExprs.size() != 1)
				return false;
			parameters[argumentIndex]= ((Expression)returnExprs.get(0)).resolveTypeBinding();

			ITypeBinding type= ASTNodes.getReceiverTypeBinding(methodInvocation);
			TypeBindingVisitor visitor= new AmbiguousMethodAnalyzer(
				fTypeEnvironment, method, fTypeEnvironment.create(parameters));
			if(!visitor.visit(type)) {
				return true;
			}
			else if(type.isInterface()) {
				return !Bindings.visitInterfaces(type, visitor);
			}
			else if(Modifier.isAbstract(type.getModifiers())) {
				return !Bindings.visitHierarchy(type, visitor);
			}
			else {
				// it is not needed to visit interfaces if receiver is a concrete class
				return !Bindings.visitSuperclasses(type, visitor);
			}
		}
		return false;
	}

	private boolean needsParenthesis() {
		if (!fSourceProvider.needsReturnedExpressionParenthesis())
			return false;
		ASTNode parent= fTargetNode.getParent();
		int type= parent.getNodeType();
		return 
			type == ASTNode.METHOD_INVOCATION || 
			(parent instanceof Expression && type != ASTNode.ASSIGNMENT) ||
			(fSourceProvider.returnsConditionalExpression() &&
				type == ASTNode.VARIABLE_DECLARATION_FRAGMENT &&  
				((VariableDeclarationFragment)parent).getInitializer() == fTargetNode);
	}
	
	private VariableDeclarationStatement createLocalDeclaration(ITypeBinding type, String name, Expression initializer) {
		String typeName= fImportRewrite.addImport(type);
		VariableDeclarationStatement decl= (VariableDeclarationStatement)ASTNodeFactory.newStatement(
			fInvocation.getAST(), typeName + " " + name + ";"); //$NON-NLS-1$ //$NON-NLS-2$
		((VariableDeclarationFragment)decl.fragments().get(0)).setInitializer(initializer);
		return decl;
	}

    /**
     * Checks whether arguments are passed to the method which do some assignments
     * inside the expression. If so these arguments can't be inlined into the
     * calling method since the assignments might be reorder. An example is:
     * <code>
     *   add((field=args).length,field.hashCode());
     * </code>
     * Field might not be initialized when the arguments are reorder in the called
     * method.
     */
	private Set crossCheckArguments(List arguments) {
		final Set assigned= new HashSet();
		final Set result= new HashSet();
		for (Iterator iter= arguments.iterator(); iter.hasNext();) {
			final Expression expression= (Expression) iter.next();
			expression.accept(new ASTVisitor() {
				public boolean visit(Assignment node) {
					Expression lhs= node.getLeftHandSide();
					if (lhs instanceof Name) {
						IBinding binding= ((Name)lhs).resolveBinding();
						if (binding instanceof IVariableBinding) {
							assigned.add(binding);
							result.add(expression);
						}
					}
					return true;
				}
			});
		}
		for (Iterator iter= arguments.iterator(); iter.hasNext();) {
			final Expression expression= (Expression) iter.next();
			if (!result.contains(expression)) {
				expression.accept(new HierarchicalASTVisitor() {
					public boolean visit(Name node) {
						IBinding binding= node.resolveBinding();
						if (binding != null && assigned.contains(binding))
							result.add(expression);
						return false;
					}
				});
			}
		}
		return result;
	}
	
	private boolean canInline(Expression actualParameter, ParameterData formalParameter) {
		InlineEvaluator evaluator= new InlineEvaluator(formalParameter);
		actualParameter.accept(evaluator);
		return evaluator.getResult();
	}
	
	private void initializeInsertionPoint(int nos) {
		fInsertionIndex= -1;
		fNeedsStatement= false;
		// if we have a constructor invocation the invocation itself is already a statement
		ASTNode parentStatement= fInvocation instanceof Statement 
			? fInvocation 
			: ASTNodes.getParent(fInvocation, Statement.class);
		ASTNode container= parentStatement.getParent();
		int type= container.getNodeType();
		if (type == ASTNode.BLOCK) { 
			Block block= (Block)container;
			fListRewrite= fRewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY);
			fInsertionIndex= fListRewrite.getRewrittenList().indexOf(parentStatement);
		} else if (type == ASTNode.SWITCH_STATEMENT) {
			SwitchStatement switchStatement= (SwitchStatement)container;
			fListRewrite= fRewrite.getListRewrite(switchStatement, SwitchStatement.STATEMENTS_PROPERTY);
			fInsertionIndex= fListRewrite.getRewrittenList().indexOf(parentStatement);
		} else if (isControlStatement(container) || type == ASTNode.LABELED_STATEMENT) {
			fNeedsStatement= true;
			if (nos > 1) {
				Block block= fInvocation.getAST().newBlock();
				fInsertionIndex= 0;
				Statement currentStatement= null;
				switch(type) {
					case ASTNode.LABELED_STATEMENT:
						currentStatement= ((LabeledStatement)container).getBody();
						break;
					case ASTNode.FOR_STATEMENT:
						currentStatement= ((ForStatement)container).getBody();
						break;
					case ASTNode.ENHANCED_FOR_STATEMENT:
						currentStatement= ((EnhancedForStatement)container).getBody();
						break;
					case ASTNode.WHILE_STATEMENT:
						currentStatement= ((WhileStatement)container).getBody();
						break;
					case ASTNode.DO_STATEMENT:
						currentStatement= ((DoStatement)container).getBody();
						break;
					case ASTNode.IF_STATEMENT:
						IfStatement node= (IfStatement)container;
						Statement thenPart= node.getThenStatement();
						if (fTargetNode == thenPart || ASTNodes.isParent(fTargetNode, thenPart)) {
							currentStatement= thenPart;
						} else {
							currentStatement= node.getElseStatement();
						}
						break;
				}
				Assert.isNotNull(currentStatement);
				fRewrite.replace(currentStatement, block, null);
				fListRewrite= fRewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY);
				// The method to be inlined is not the body of the control statement.
				if (currentStatement != fTargetNode) {
					fListRewrite.insertLast(fRewrite.createCopyTarget(currentStatement), null);
				} else {
					// We can't replace a copy with something else. So we
					// have to insert all statements to be inlined.
					fTargetNode= null;
				}
			}
		}
		// We only insert one new statement or we delete the existing call. 
		// So there is no need to have an insertion index.
	}

	private String getContent(ASTNode node) throws BadLocationException {
		return fBuffer.getDocument().get(node.getStartPosition(), node.getLength());
	}

	private boolean isControlStatement(ASTNode node) {
		int type= node.getNodeType();
		return type == ASTNode.IF_STATEMENT || type == ASTNode.FOR_STATEMENT || type == ASTNode.ENHANCED_FOR_STATEMENT ||
		        type == ASTNode.WHILE_STATEMENT || type == ASTNode.DO_STATEMENT;
	}
}
