/*******************************************************************************
 * Copyright (c) 2000, 2019 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
 *     Dmitry Stalnov (dstalnov@fusionone.com) - contributed fixes for:
 * 	     o bug "Inline refactoring showed bogus error" (see bugzilla
 *         https://bugs.eclipse.org/bugs/show_bug.cgi?id=42753)
 *       o Allow 'this' constructor to be inlined
 *         (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=38093)
 *     Microsoft Corporation - copied to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.code;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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

import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
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.LambdaExpression;
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.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.manipulation.ImportReferencesCollector;

import org.eclipse.jdt.internal.core.manipulation.dom.OperatorPrecedence;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.LocalVariableIndex;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;
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.InOutFlowAnalyzer;
import org.eclipse.jdt.internal.corext.refactoring.util.JavaStatusContext;

class SourceAnalyzer  {

	public static class NameData {
		private String fName;
		private List<SimpleName> fReferences;
		public NameData(String n) {
			fName= n;
			fReferences= new ArrayList<>(2);
		}
		public String getName() {
			return fName;
		}
		public void addReference(SimpleName ref) {
			fReferences.add(ref);
		}
		public List<SimpleName> references() {
			return fReferences;
		}
	}

	private class ActivationAnalyzer extends ASTVisitor {
		public RefactoringStatus status= new RefactoringStatus();
		private ASTNode fLastNode= getLastNode();
		private IMethodBinding fBinding= getBinding();
		@Override
		public boolean visit(ReturnStatement node) {
			if (node != fLastNode) {
				fInterruptedExecutionFlow= true;
			}
			return true;
		}
		@Override
		public boolean visit(EnumDeclaration node) {
			return false;
		}
		@Override
		public boolean visit(AnnotationTypeDeclaration node) {
			return false;
		}
		@Override
		public boolean visit(TypeDeclaration node) {
			return false;
		}
		@Override
		public boolean visit(AnonymousClassDeclaration node) {
			return false;
		}
		@Override
		public boolean visit(LambdaExpression node) {
			return false;
		}
		@Override
		public boolean visit(MethodInvocation node) {
			IMethodBinding methodBinding= node.resolveMethodBinding();
			if (methodBinding != null)
				methodBinding.getMethodDeclaration();
			if (fBinding != null && methodBinding != null && fBinding.isEqualTo(methodBinding) && !status.hasFatalError()) {
				status.addFatalError(RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_recursive_call);
				return false;
			}
			return true;
		}
		@Override
		public boolean visit(SimpleName node) {
			IBinding binding= node.resolveBinding();
			if (binding == null && !status.hasFatalError()) {
				// fixes bug #42753
				if (!ASTNodes.isLabel(node)) {
					status.addFatalError(
						RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_declaration_has_errors,
						JavaStatusContext.create(fTypeRoot, fDeclaration));
					return false;
				}
			}
			return true;
		}
		@Override
		public boolean visit(ThisExpression node) {
			if (node.getQualifier() != null) {
				status.addFatalError(
					RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_qualified_this_expressions,
					JavaStatusContext.create(fTypeRoot, node));
				return false;
			}
			return true;
		}
		private ASTNode getLastNode() {
			List<Statement> statements= fDeclaration.getBody().statements();
			if (statements.size() == 0)
				return null;
			return statements.get(statements.size() - 1);
		}
		private IMethodBinding getBinding() {
			IMethodBinding result= fDeclaration.resolveBinding();
			if (result != null)
				return result.getMethodDeclaration();
			return result;
		}
	}

	private class UpdateCollector extends ASTVisitor {
		private int fTypeCounter;
		@Override
		public boolean visit(TypeDeclaration node) {
			return visitType(node);
		}
		@Override
		public void endVisit(TypeDeclaration node) {
			fTypeCounter--;
		}
		@Override
		public boolean visit(EnumDeclaration node) {
			return visitType(node);
		}
		@Override
		public void endVisit(EnumDeclaration node) {
			fTypeCounter--;
		}
		@Override
		public boolean visit(AnnotationTypeDeclaration node) {
			return visitType(node);
		}
		@Override
		public void endVisit(AnnotationTypeDeclaration node) {
			fTypeCounter--;
		}
		private boolean visitType(AbstractTypeDeclaration node) {
			if (fTypeCounter++ == 0) {
				addNameReference(node.getName());
			}
			return true;
		}
		@Override
		public boolean visit(AnonymousClassDeclaration node) {
			fTypeCounter++;
			return true;
		}
		@Override
		public void endVisit(AnonymousClassDeclaration node) {
			fTypeCounter--;
		}
		@Override
		public boolean visit(FieldAccess node) {
			// only visit the expression and not the simple name
			node.getExpression().accept(this);
			addReferencesToName(node.getName());
			return false;
		}
		@Override
		public boolean visit(MethodDeclaration node) {
			if (node.isConstructor()) {
				AbstractTypeDeclaration decl= ASTNodes.getParent(node, AbstractTypeDeclaration.class);
				NameData name= fNames.get(decl.getName().resolveBinding());
				if (name != null) {
					name.addReference(node.getName());
				}
			}
			return true;
		}
		@Override
		public boolean visit(MethodInvocation node) {
			if (fTypeCounter == 0) {
				Expression receiver= node.getExpression();
				if (receiver == null && !isStaticallyImported(node.getName())) {
					fImplicitReceivers.add(node);
				}
			}
			return true;
		}
		@Override
		public boolean visit(SuperMethodInvocation node) {
			if (fTypeCounter == 0) {
				fHasSuperMethodInvocation= true;
			}
			return true;
		}
		@Override
		public boolean visit(SuperConstructorInvocation node) {
			if (fTypeCounter == 0) {
				fHasSuperMethodInvocation= true;
			}
			return true;
		}
		@Override
		public boolean visit(ClassInstanceCreation node) {
			if (fTypeCounter == 0) {
				Expression receiver= node.getExpression();
				if (receiver == null) {
					if (node.resolveTypeBinding().isLocal())
						fImplicitReceivers.add(node);
				}
			}
			return true;
		}
		@Override
		public boolean visit(SingleVariableDeclaration node) {
			if (fTypeCounter == 0)
				addNameReference(node.getName());
			return true;
		}
		@Override
		public boolean visit(VariableDeclarationFragment node) {
			if (fTypeCounter == 0)
				addNameReference(node.getName());
			return true;
		}
		@Override
		public boolean visit(SimpleName node) {
			addReferencesToName(node);
			IBinding binding= node.resolveBinding();
			if (binding instanceof ITypeBinding) {
				ITypeBinding type= (ITypeBinding)binding;
				if (type.isTypeVariable()) {
					addTypeVariableReference(type, node);
				}
			} else if (binding instanceof IVariableBinding) {
				IVariableBinding vb= (IVariableBinding)binding;
				if (vb.isField() && ! isStaticallyImported(node)) {
					Name topName= ASTNodes.getTopMostName(node);
					if (node == topName || node == ASTNodes.getLeftMostSimpleName(topName)) {
						StructuralPropertyDescriptor location= node.getLocationInParent();
						if (location != SingleVariableDeclaration.NAME_PROPERTY
							&& location != VariableDeclarationFragment.NAME_PROPERTY) {
							fImplicitReceivers.add(node);
						}
					}
				} else if (!vb.isField()) {
					// we have a local. Check if it is a parameter.
					ParameterData data= fParameters.get(binding);
					if (data != null) {
						ASTNode parent= node.getParent();
						if (parent instanceof Expression) {
							int precedence= OperatorPrecedence.getExpressionPrecedence((Expression)parent);
							if (precedence != Integer.MAX_VALUE) {
								data.setOperatorPrecedence(precedence);
							}
						}
					}
				}
			}
			return true;
		}
		@Override
		public boolean visit(ThisExpression node) {
			if (fTypeCounter == 0) {
				fImplicitReceivers.add(node);
			}
			return true;
		}
		private void addReferencesToName(SimpleName node) {
			IBinding binding= node.resolveBinding();
			ParameterData data= fParameters.get(binding);
			if (data != null)
				data.addReference(node);

			NameData name= fNames.get(binding);
			if (name != null)
				name.addReference(node);
		}
		private void addNameReference(SimpleName name) {
			fNames.put(name.resolveBinding(), new NameData(name.getIdentifier()));
		}
		private void addTypeVariableReference(ITypeBinding variable, SimpleName name) {
			NameData data= fTypeParameterMapping.get(variable);
			if (data == null) {
				data= fMethodTypeParameterMapping.get(variable);
			}
			data.addReference(name);
		}
		private boolean isStaticallyImported(Name name) {
			return fStaticsToImport.contains(name);
		}
	}

	private class VarargAnalyzer extends ASTVisitor {
		private IBinding fParameter;
		public VarargAnalyzer(IBinding parameter) {
			fParameter= parameter;
		}
		@Override
		public boolean visit(ArrayAccess node) {
			Expression array= node.getArray();
			if (array instanceof SimpleName && fParameter.isEqualTo(((SimpleName)array).resolveBinding())) {
				fArrayAccess= true;
			}
			return true;
		}
	}

	private ITypeRoot fTypeRoot;
	private MethodDeclaration fDeclaration;
	private Map<IVariableBinding, ParameterData> fParameters;
	private Map<IBinding, NameData> fNames;
	private List<Expression> fImplicitReceivers;

	private boolean fArrayAccess;
	private boolean fHasSuperMethodInvocation;

	private List<SimpleName> fTypesToImport;
	private List<SimpleName> fStaticsToImport;

	private List<NameData> fTypeParameterReferences;
	private Map<ITypeBinding, NameData> fTypeParameterMapping;

	private List<NameData> fMethodTypeParameterReferences;
	private Map<ITypeBinding, NameData> fMethodTypeParameterMapping;

	private boolean fInterruptedExecutionFlow;

	public SourceAnalyzer(ITypeRoot typeRoot, MethodDeclaration declaration) {
		super();
		fTypeRoot= typeRoot;
		fDeclaration= declaration;
	}

	public boolean isExecutionFlowInterrupted() {
		return fInterruptedExecutionFlow;
	}

	public RefactoringStatus checkActivation() throws JavaModelException {
		RefactoringStatus result= new RefactoringStatus();
		if (!fTypeRoot.isStructureKnown()) {
			result.addFatalError(
				RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_syntax_errors,
				JavaStatusContext.create(fTypeRoot));
			return result;
		}
		IProblem[] problems= ASTNodes.getProblems(fDeclaration, ASTNodes.NODE_ONLY, ASTNodes.ERROR);
		if (problems.length > 0) {
			result.addFatalError(
				RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_declaration_has_errors,
				JavaStatusContext.create(fTypeRoot, fDeclaration));
			return result;
		}
		final IMethodBinding declarationBinding= fDeclaration.resolveBinding();
		if (declarationBinding != null) {
			final int modifiers= declarationBinding.getModifiers();
			if (Modifier.isAbstract(modifiers)) {
				result.addFatalError(RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_abstract_methods, JavaStatusContext.create(fTypeRoot, fDeclaration));
				return result;
			} else if (Modifier.isNative(modifiers)) {
				result.addFatalError(RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_native_methods, JavaStatusContext.create(fTypeRoot, fDeclaration));
				return result;
			}
		} else {
			result.addFatalError(RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_methoddeclaration_has_errors, JavaStatusContext.create(fTypeRoot));
			return result;
		}
		ActivationAnalyzer analyzer= new ActivationAnalyzer();
		fDeclaration.accept(analyzer);
		result.merge(analyzer.status);
		if (!result.hasFatalError()) {
			List<SingleVariableDeclaration> parameters= fDeclaration.parameters();
			fParameters= new HashMap<>(parameters.size() * 2);
			for (SingleVariableDeclaration element : parameters) {
				IVariableBinding binding= element.resolveBinding();
				if (binding == null) {
					result.addFatalError(
						RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_declaration_has_errors,
						JavaStatusContext.create(fTypeRoot, fDeclaration));
					return result;
				}
				fParameters.put(binding, (ParameterData) element.getProperty(ParameterData.PROPERTY));
			}
			fNames= new HashMap<>();
			fImplicitReceivers= new ArrayList<>(2);

			fTypeParameterReferences= new ArrayList<>(0);
			fTypeParameterMapping= new HashMap<>();
			ITypeBinding declaringType= declarationBinding.getDeclaringClass();
			if (declaringType == null) {
				result.addFatalError(
					RefactoringCoreMessages.InlineMethodRefactoring_SourceAnalyzer_typedeclaration_has_errors,
					JavaStatusContext.create(fTypeRoot));
				return result;
			}
			for (ITypeBinding typeParameter : declaringType.getTypeParameters()) {
				NameData data= new NameData(typeParameter.getName());
				fTypeParameterReferences.add(data);
				fTypeParameterMapping.put(typeParameter, data);
			}

			fMethodTypeParameterReferences= new ArrayList<>(0);
			fMethodTypeParameterMapping= new HashMap<>();
			IMethodBinding method= declarationBinding;
			for (ITypeBinding typeParameter : method.getTypeParameters()) {
				NameData data= new NameData(typeParameter.getName());
				fMethodTypeParameterReferences.add(data);
				fMethodTypeParameterMapping.put(typeParameter, data);
			}

		}
		if (fDeclaration.isVarargs()) {
			List<SingleVariableDeclaration> parameters= fDeclaration.parameters();
			VarargAnalyzer vAnalyzer= new VarargAnalyzer(
				parameters.get(parameters.size() - 1).getName().resolveBinding());
			fDeclaration.getBody().accept(vAnalyzer);
		}
		return result;
	}

	public void initialize() {
		Block body= fDeclaration.getBody();
		// first collect the static imports. This is necessary to not mark
		// static imported fields and methods as implicit visible.
		fTypesToImport= new ArrayList<>();
		fStaticsToImport= new ArrayList<>();
		ImportReferencesCollector.collect(body, fTypeRoot.getJavaProject(), null, fTypesToImport, fStaticsToImport);

		// Now collect implicit references and name references
		body.accept(new UpdateCollector());

		int numberOfLocals= LocalVariableIndex.perform(fDeclaration);
		FlowContext context= new FlowContext(0, numberOfLocals + 1);
		context.setConsiderAccessMode(true);
		context.setComputeMode(FlowContext.MERGE);
		InOutFlowAnalyzer flowAnalyzer= new InOutFlowAnalyzer(context);
		FlowInfo info= flowAnalyzer.perform(getStatements());

		for (Iterator<SingleVariableDeclaration> iter= fDeclaration.parameters().iterator(); iter.hasNext();) {
			SingleVariableDeclaration element= iter.next();
			IVariableBinding binding= element.resolveBinding();
			ParameterData data= (ParameterData)element.getProperty(ParameterData.PROPERTY);
			data.setAccessMode(info.getAccessMode(context, binding));
		}
	}

	public Collection<NameData> getUsedNames() {
		return fNames.values();
	}

	public List<Expression> getImplicitReceivers() {
		return fImplicitReceivers;
	}

	public List<SimpleName> getTypesToImport() {
		return fTypesToImport;
	}

	public List<SimpleName> getStaticsToImport() {
		return fStaticsToImport;
	}

	public List<NameData> getTypeParameterReferences() {
		return fTypeParameterReferences;
	}

	public List<NameData> getMethodTypeParameterReferences() {
		return fMethodTypeParameterReferences;
	}

	public boolean hasArrayAccess() {
		return fArrayAccess;
	}

	public boolean hasSuperMethodInvocation() {
		return fHasSuperMethodInvocation;
	}

	private ASTNode[] getStatements() {
		List<Statement> statements= fDeclaration.getBody().statements();
		return statements.toArray(new ASTNode[statements.size()]);
	}

}
