/*******************************************************************************
 * Copyright (c) 2000, 2012 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.generics;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.ArrayCreation;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.BooleanLiteral;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.CharacterLiteral;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConditionalExpression;
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.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.Javadoc;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
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.StringLiteral;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.VariableDeclarationExpression;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.dom.HierarchicalASTVisitor;
import org.eclipse.jdt.internal.corext.refactoring.rename.MethodChecks;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.GenericType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.ParameterizedType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.TypeVariable;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.WildcardType;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ArrayElementVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.CollectionElementVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ConstraintVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ImmutableTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.IndependentTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ParameterTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ParameterizedTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.ReturnTypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.TTypes;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.TypeVariable2;
import org.eclipse.jdt.internal.corext.refactoring.typeconstraints2.VariableVariable2;
import org.eclipse.jdt.internal.corext.util.JdtFlags;

public class InferTypeArgumentsConstraintCreator extends HierarchicalASTVisitor {

	/**
	 * Property in <code>ASTNode</code>s that holds the node's <code>ConstraintVariable</code>.
	 */
	private static final String CV_PROP= "org.eclipse.jdt.internal.corext.refactoring.typeconstraints.CONSTRAINT_VARIABLE"; //$NON-NLS-1$

	private InferTypeArgumentsTCModel fTCModel;
	private ICompilationUnit fCU;

	private final boolean fAssumeCloneReturnsSameType;


	public InferTypeArgumentsConstraintCreator(InferTypeArgumentsTCModel model, boolean assumeCloneReturnsSameType) {
		fTCModel= model;
		fAssumeCloneReturnsSameType= assumeCloneReturnsSameType;
	}

	@Override
	public boolean visit(CompilationUnit node) {
		fTCModel.newCu(); //TODO: make sure that accumulators are reset after last CU!
		fCU= (ICompilationUnit) node.getJavaElement();
		fTCModel.getTypeEnvironment().initializeJavaLangObject(fCU.getJavaProject());
		return super.visit(node);
	}

	@Override
	public boolean visit(Javadoc node) {
		return false;
	}

	@Override
	public boolean visit(Type node) {
		return false; //TODO
	}

	/*
	 * @see org.eclipse.jdt.internal.corext.dom.HierarchicalASTVisitor#endVisit(org.eclipse.jdt.core.dom.Type)
	 */
	@Override
	public void endVisit(Type node) {
		if (node.isParameterizedType()) {
			// retain already parameterized types
			ImmutableTypeVariable2 typeVariable= fTCModel.makeImmutableTypeVariable(node.resolveBinding(), /*no boxing*/null);
			setConstraintVariable(node, typeVariable);
		} else {
			TypeVariable2 typeVariable= fTCModel.makeTypeVariable(node);
			setConstraintVariable(node, typeVariable);
		}
	}

	@Override
	public void endVisit(SimpleName node) {
		if (node.resolveBoxing()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
			setConstraintVariable(node, boxed);
			return;
		}
		IBinding binding= node.resolveBinding();
		if (binding instanceof IVariableBinding) {
			//TODO: code is similar to handling of method return value
			IVariableBinding variableBinding= (IVariableBinding) binding;
			ITypeBinding declaredVariableType= variableBinding.getVariableDeclaration().getType();
			if (declaredVariableType.isTypeVariable()) {
				Expression receiver= getSimpleNameReceiver(node);
				if (receiver != null) {
					ConstraintVariable2 receiverCv= getConstraintVariable(receiver);
					Assert.isNotNull(receiverCv); // the type variable must come from the receiver!

					ConstraintVariable2 elementCv= fTCModel.getElementVariable(receiverCv, declaredVariableType);
					// [retVal] =^= Elem[receiver]:
					setConstraintVariable(node, elementCv);
					return;
				}

			} else if (declaredVariableType.isParameterizedType()){
				Expression receiver= getSimpleNameReceiver(node);
				if (receiver != null) {
					ConstraintVariable2 receiverCv= getConstraintVariable(receiver);
					if (receiverCv != null) {
//						ITypeBinding genericVariableType= declaredVariableType.getTypeDeclaration();
						ConstraintVariable2 returnTypeCv= fTCModel.makeParameterizedTypeVariable(declaredVariableType);
						setConstraintVariable(node, returnTypeCv);
						// Elem[retVal] =^= Elem[receiver]
						TType declaredVariableTType= fTCModel.createTType(declaredVariableType);
						fTCModel.createTypeVariablesEqualityConstraints(receiverCv, Collections.<String, IndependentTypeVariable2>emptyMap(), returnTypeCv, declaredVariableTType);
						return;
					}
				}

			} else {
				//TODO: array...
				//logUnexpectedNode(node, null);
			}

			// default:
			VariableVariable2 cv= fTCModel.makeVariableVariable(variableBinding);
			setConstraintVariable(node, cv);
		}
		// TODO else?
	}

	private Expression getSimpleNameReceiver(SimpleName node) {
		Expression receiver;
		if (node.getParent() instanceof QualifiedName && node.getLocationInParent() == QualifiedName.NAME_PROPERTY) {
			receiver= ((QualifiedName) node.getParent()).getQualifier();
		} else if (node.getParent() instanceof FieldAccess && node.getLocationInParent() == FieldAccess.NAME_PROPERTY) {
			receiver= ((FieldAccess) node.getParent()).getExpression();
		} else {
			//TODO other cases? (ThisExpression, SuperAccessExpression, ...)
			receiver= null;
		}
		if (receiver instanceof ThisExpression)
			return null;
		else
			return receiver;
	}

	@Override
	public void endVisit(FieldAccess node) {
		if (node.resolveBoxing()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
			setConstraintVariable(node, boxed);
			return;
		}
		ConstraintVariable2 nameCv= getConstraintVariable(node.getName());
		setConstraintVariable(node, nameCv);
	}

	@Override
	public void endVisit(QualifiedName node) {
		if (node.resolveBoxing()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
			setConstraintVariable(node, boxed);
			return;
		}
		ConstraintVariable2 cv= getConstraintVariable(node.getName());
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(ArrayAccess node) {
		if (node.resolveBoxing()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
			setConstraintVariable(node, boxed);
			return;
		}

		ConstraintVariable2 arrayCv= getConstraintVariable(node.getArray());
		if (arrayCv == null)
			return;

		ArrayElementVariable2 arrayElementCv= fTCModel.getArrayElementVariable(arrayCv);
		setConstraintVariable(node, arrayElementCv);
	}

	@Override
	public void endVisit(Assignment node) {
		Expression lhs= node.getLeftHandSide();
		Expression rhs= node.getRightHandSide();

		ConstraintVariable2 left= getConstraintVariable(lhs);
		ConstraintVariable2 right= getConstraintVariable(rhs);
		if (node.resolveBoxing()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
			setConstraintVariable(node, boxed);
		} else {
			setConstraintVariable(node, left); // type of assignement is type of 'left'
		}
		if (left == null || right == null)
			return;

		Assignment.Operator op= node.getOperator();
		if (op == Assignment.Operator.PLUS_ASSIGN && (lhs.resolveTypeBinding() == node.getAST().resolveWellKnownType("java.lang.String"))) { //$NON-NLS-1$
			//Special handling for automatic String conversion: do nothing; the RHS can be anything.
		} else {
			fTCModel.createElementEqualsConstraints(left, right);
			fTCModel.createSubtypeConstraint(right, left); // left= right;  -->  [right] <= [left]
		}
		//TODO: other implicit conversions: numeric promotion, autoboxing?
	}

	@Override
	public void endVisit(CastExpression node) {
//		if (! (expressionCv instanceof CollectionElementVariable2))
//			return; //TODO: returns too early when dealing with nested collections.

		Type type= node.getType();
		ITypeBinding typeBinding= type.resolveBinding();
		if (typeBinding.isPrimitive()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(typeBinding, node);
			setConstraintVariable(node, boxed);
			return; // avoid removing numeric conversions
		}

		ConstraintVariable2 typeCv= getConstraintVariable(type);
		if (typeCv == null)
			return;

		//TODO: can this be loosened when we remove casts?
		setConstraintVariable(node, typeCv);

		Expression expression= node.getExpression();
		ConstraintVariable2 expressionCv= getConstraintVariable(expression);

		//Avoid removing casts that have not been made obsolete by this refactoring:
		if (expressionCv == null)
			return;
		if (expressionCv instanceof ImmutableTypeVariable2)
			return;
		if (! (expressionCv instanceof TypeVariable2 || expressionCv instanceof IndependentTypeVariable2 || expressionCv instanceof CollectionElementVariable2)
				&& fTCModel.getElementVariables(expressionCv).isEmpty() && fTCModel.getArrayElementVariable(expressionCv) == null)
			return;

		fTCModel.createAssignmentElementConstraints(typeCv, expressionCv);

		if (expression instanceof MethodInvocation) {
			MethodInvocation invoc= (MethodInvocation) expression;
			if (! isSpecialCloneInvocation(invoc.resolveMethodBinding(), invoc.getExpression())) {
				fTCModel.makeCastVariable(node, expressionCv);
			}
		} else {
			fTCModel.makeCastVariable(node, expressionCv);
		}

		boolean eitherIsIntf= typeBinding.isInterface() || expression.resolveTypeBinding().isInterface();
		if (eitherIsIntf)
			return;

		//TODO: preserve up- and down-castedness!

	}

	@Override
	public void endVisit(ParenthesizedExpression node) {
		if (node.resolveBoxing()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
			setConstraintVariable(node, boxed);
			return;
		}
		ConstraintVariable2 expressionCv= getConstraintVariable(node.getExpression());
		setConstraintVariable(node, expressionCv);
	}

	@Override
	public void endVisit(ConditionalExpression node) {
		// for now, no support for passing generic types through conditional expressions
		ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(node.resolveTypeBinding(), node);
		setConstraintVariable(node, boxed);
	}

	@Override
	public boolean visit(CatchClause node) {
		SingleVariableDeclaration exception= node.getException();
		IVariableBinding variableBinding= exception.resolveBinding();
		VariableVariable2 cv= fTCModel.makeDeclaredVariableVariable(variableBinding, fCU);
		setConstraintVariable(exception, cv);
		return true;
	}

	@Override
	public void endVisit(StringLiteral node) {
		ITypeBinding typeBinding= node.resolveTypeBinding();
		ImmutableTypeVariable2 cv= fTCModel.makeImmutableTypeVariable(typeBinding, /*no boxing*/null);
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(NumberLiteral node) {
		ITypeBinding typeBinding= node.resolveTypeBinding();
		ImmutableTypeVariable2 cv= fTCModel.makeImmutableTypeVariable(typeBinding, node);
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(BooleanLiteral node) {
		ITypeBinding typeBinding= node.resolveTypeBinding();
		ImmutableTypeVariable2 cv= fTCModel.makeImmutableTypeVariable(typeBinding, node);
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(CharacterLiteral node) {
		ITypeBinding typeBinding= node.resolveTypeBinding();
		ImmutableTypeVariable2 cv= fTCModel.makeImmutableTypeVariable(typeBinding, node);
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(ThisExpression node) {
		ITypeBinding typeBinding= node.resolveTypeBinding();
		ImmutableTypeVariable2 cv= fTCModel.makeImmutableTypeVariable(typeBinding, /*no boxing*/null);
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(TypeLiteral node) {
		ITypeBinding typeBinding= node.resolveTypeBinding();
		ImmutableTypeVariable2 cv= fTCModel.makeImmutableTypeVariable(typeBinding, /*no boxing*/null);
		setConstraintVariable(node, cv);
	}

	@Override
	public void endVisit(MethodDeclaration node) {
		IMethodBinding methodBinding= node.resolveBinding();

		if (methodBinding == null)
			return; //TODO: emit error?

		int parameterCount= node.parameters().size();
		ConstraintVariable2[] parameterTypeCvs= new ConstraintVariable2[parameterCount];
		for (int i= 0; i < parameterCount; i++) {
			SingleVariableDeclaration paramDecl= (SingleVariableDeclaration) node.parameters().get(i);
			//parameterTypeVariable currently not used, but need to register in order to store source range
			ConstraintVariable2 parameterTypeCv= fTCModel.makeDeclaredParameterTypeVariable(methodBinding, i, fCU);
			parameterTypeCvs[i]= parameterTypeCv;
			if (parameterTypeCv == null)
				continue;

			//creating equals constraint between parameterTypeVariable's elements and the Type's elements
			ConstraintVariable2 typeCv= getConstraintVariable(paramDecl.getType());
			fTCModel.createElementEqualsConstraints(parameterTypeCv, typeCv);

			//TODO: should avoid having a VariableVariable as well as a ParameterVariable for a parameter
			ConstraintVariable2 nameCv= getConstraintVariable(paramDecl.getName());
			fTCModel.createElementEqualsConstraints(parameterTypeCv, nameCv);
		}

		ConstraintVariable2 returnTypeCv= null;
		if (! methodBinding.isConstructor()) {
			//TODO: should only create return type variable if type is generic?
			ConstraintVariable2 returnTypeBindingCv= fTCModel.makeDeclaredReturnTypeVariable(methodBinding, fCU);
			if (returnTypeBindingCv != null) {
				returnTypeCv= getConstraintVariable(node.getReturnType2());
				fTCModel.createElementEqualsConstraints(returnTypeBindingCv, returnTypeCv);
			}
		}
		if (MethodChecks.isVirtual(methodBinding)) {
			//TODO: RippleMethod constraints for corner cases: see testCuRippleMethods3, bug 41989
			addConstraintsForOverriding(methodBinding, returnTypeCv, parameterTypeCvs);
		}
	}

	private void addConstraintsForOverriding(IMethodBinding methodBinding, ConstraintVariable2 returnTypeCv, ConstraintVariable2[] parameterTypeCvs) {
		boolean hasParameterElementCvs= false;
		for (ConstraintVariable2 parameterTypeCv : parameterTypeCvs) {
			if (parameterTypeCv != null) {
				hasParameterElementCvs= true;
			}
		}

		if (returnTypeCv == null && ! hasParameterElementCvs)
			return;

		for (ITypeBinding superType : Bindings.getAllSuperTypes(methodBinding.getDeclaringClass())) {
			IMethodBinding superMethod= Bindings.findOverriddenMethodInType(superType, methodBinding);
			if (superMethod == null)
				continue;

			for (int p= 0; p < parameterTypeCvs.length; p++) {
				if (parameterTypeCvs[p] == null)
					continue;
				ParameterTypeVariable2 parameterTypeCv= fTCModel.makeParameterTypeVariable(superMethod, p);
				fTCModel.createElementEqualsConstraints(parameterTypeCv, parameterTypeCvs[p]);
			}

			if (returnTypeCv != null) {
				ReturnTypeVariable2 superMethodReturnTypeCv= fTCModel.makeReturnTypeVariable(superMethod);
				fTCModel.createElementEqualsConstraints(superMethodReturnTypeCv, returnTypeCv);
			}
		}
	}

	@Override
	public void endVisit(MethodInvocation node) {
		IMethodBinding methodBinding= node.resolveMethodBinding();
		if (methodBinding == null)
			return;

		Expression receiver;
		if (JdtFlags.isStatic(methodBinding))
			receiver= null;
		else
			receiver= node.getExpression();

		//TODO: Expression can be null when visiting a non-special method in a subclass of a container type.

		if (isSpecialCloneInvocation(methodBinding, receiver)) {
			ConstraintVariable2 expressionCv= getConstraintVariable(receiver);
			// [retVal] =^= [receiver]:
			setConstraintVariable(node, expressionCv);

		} else if ("getClass".equals(methodBinding.getName()) && methodBinding.getParameterTypes().length == 0) { //$NON-NLS-1$
			//special case: see JLS3 4.3.2
			ITypeBinding returnType= node.resolveTypeBinding();
			ITypeBinding returnTypeDeclaration= returnType.getTypeDeclaration();
			ParameterizedTypeVariable2 expressionCv= fTCModel.makeParameterizedTypeVariable(returnTypeDeclaration);
			setConstraintVariable(node, expressionCv);
			ConstraintVariable2 classTypeVariable= fTCModel.getElementVariable(expressionCv, returnTypeDeclaration.getTypeParameters()[0]);

			//type of expression 'e.getClass()' is 'Class<? extends X>' where X is the static type of e
			ITypeBinding capture= returnType.getTypeArguments()[0];
			ITypeBinding wildcard= capture.getWildcard();
			if (wildcard.getBound() == null)
				return; // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=234619

			ImmutableTypeVariable2 wildcardType= fTCModel.makeImmutableTypeVariable(wildcard,  /*no boxing*/null);
			fTCModel.createSubtypeConstraint(classTypeVariable, wildcardType);

//			ITypeBinding bound= wildcard.getBound();
//			ImmutableTypeVariable2 boundType= fTCModel.makeImmutableTypeVariable(bound, node.getAST());
//			fTCModel.createSubtypeConstraint(classTypeVariable, boundType);

		} else {
			Map<String, IndependentTypeVariable2> methodTypeVariables= createMethodTypeArguments(methodBinding);

			doVisitMethodInvocationReturnType(node, methodBinding, receiver, methodTypeVariables);
			doVisitMethodInvocationArguments(methodBinding, node.arguments(), receiver, methodTypeVariables, /*no created type*/null);
		}

	}

	/**
	 * @return a map from type variable key to type variable constraint variable
	 */
	private Map<String, IndependentTypeVariable2> createMethodTypeArguments(IMethodBinding methodBinding) {
		ITypeBinding[] methodTypeParameters= methodBinding.getMethodDeclaration().getTypeParameters();
		Map<String, IndependentTypeVariable2> methodTypeVariables;
		if (methodTypeParameters.length == 0) {
			methodTypeVariables= Collections.emptyMap();
		} else {
			methodTypeVariables= new HashMap<>();
			for (ITypeBinding methodTypeParameter : methodTypeParameters) {
				//TODO: typeVariable does not need a type binding - only used in equality constraints
				TypeVariable typeVariable= (TypeVariable) fTCModel.createTType(methodTypeParameter);
				IndependentTypeVariable2 typeVariableCv= fTCModel.makeIndependentTypeVariable(typeVariable);
				methodTypeVariables.put(methodTypeParameter.getKey(), typeVariableCv);
			}
		}
		return methodTypeVariables;
	}

	private void doVisitMethodInvocationReturnType(MethodInvocation node, IMethodBinding methodBinding, Expression receiver, Map<String, IndependentTypeVariable2> methodTypeVariables) {
		ITypeBinding declaredReturnType= methodBinding.getMethodDeclaration().getReturnType();

		if (declaredReturnType.isPrimitive()) {
			ImmutableTypeVariable2 boxed= fTCModel.makeImmutableTypeVariable(declaredReturnType, node);
			setConstraintVariable(node, boxed);

		} else 	if (declaredReturnType.isTypeVariable()) {
			ConstraintVariable2 methodTypeVariableCv= methodTypeVariables.get(declaredReturnType.getKey());
			if (methodTypeVariableCv != null) {
				// e.g. in Collections: <T ..> T min(Collection<? extends T> coll):
				setConstraintVariable(node, methodTypeVariableCv); //TODO: should be [retVal] <= Elem[arg] in this case?

	//			TODO:
	//			} else if (methodBinding.getErasure().getTypeParameters().length == 1 &&
	//					(genericReturnType.isTypeVariable() || genericReturnType.isWildcardType()) &&
	//					methodBinding.getParameterTypes().length == 1 &&
	//					methodBinding.getParameterTypes()[0].getErasure().isGenericType()) {
	//				// e.g. in Collections: <T ..> T min(Collection<? extends T> coll):
	//				TypeConstraintVariable2 argCv= (TypeConstraintVariable2) getConstraintVariable((Expression) node.arguments().get(0));
	//				ConstraintVariable2 elementCv= fTCModel.makeElementVariable(argCv);
	//				// [retVal] =^= Elem[arg]:
	//				setConstraintVariable(node, elementCv); //TODO: should be [retVal] <= Elem[arg]

			} else {
				//TODO: nested generic classes and methods?

				if (receiver == null) //TODO: deal with methods inside generic types
					return;
				// e.g. in List<E>: E get(int index):
				ConstraintVariable2 expressionCv= getConstraintVariable(receiver);
				ConstraintVariable2 elementCv= fTCModel.getElementVariable(expressionCv, declaredReturnType);
				// [retVal] =^= Elem[receiver]:
				setConstraintVariable(node, elementCv);
			}

		} else if (declaredReturnType.isParameterizedType()) {
			ConstraintVariable2 returnTypeCv= fTCModel.makeParameterizedTypeVariable(declaredReturnType.getTypeDeclaration());
			setConstraintVariable(node, returnTypeCv);
			//e.g. List<E>: Iterator<E> iterator()
			ConstraintVariable2 receiverCv= null;
			if (receiver != null) //TODO: deal with methods inside generic types
				receiverCv= getConstraintVariable(receiver);
			// Elem[retVal] =^= Elem[receiver]
			TType declaredReturnTType= fTCModel.createTType(declaredReturnType);
			fTCModel.createTypeVariablesEqualityConstraints(receiverCv, methodTypeVariables, returnTypeCv, declaredReturnTType);

		} else if (declaredReturnType.isArray()) {
			ConstraintVariable2 returnTypeCv= fTCModel.makeArrayTypeVariable(declaredReturnType);
			setConstraintVariable(node, returnTypeCv);
			//e.g. List<E>: <T> T[] toArray(T[] a)
			ConstraintVariable2 receiverCv= null;
			if (receiver != null) { //TODO: deal with methods inside generic types
				receiverCv= getConstraintVariable(receiver);

				//TODO: is this necessary elsewhere?
				fTCModel.setMethodReceiverCV(returnTypeCv, receiverCv);
			}
			// Elem[retVal] =^= Elem[receiver]
			TType declaredReturnTType= fTCModel.createTType(declaredReturnType);
			fTCModel.createTypeVariablesEqualityConstraints(receiverCv, methodTypeVariables, returnTypeCv, declaredReturnTType);

		} else {
			ReturnTypeVariable2 returnTypeCv= fTCModel.makeReturnTypeVariable(methodBinding);
			setConstraintVariable(node, returnTypeCv);
		}
	}

	private boolean isSpecialCloneInvocation(IMethodBinding methodBinding, Expression receiver) {
		return fAssumeCloneReturnsSameType
				&& "clone".equals(methodBinding.getName()) //$NON-NLS-1$
				&& methodBinding.getParameterTypes().length == 0
				&& receiver != null
				&& receiver.resolveTypeBinding() != methodBinding.getMethodDeclaration().getReturnType();
	}

	private void doVisitMethodInvocationArguments(IMethodBinding methodBinding, List<Expression> arguments, Expression receiver, Map<String, IndependentTypeVariable2> methodTypeVariables, Type createdType) {
		//TODO: connect generic method type parameters, e.g. <T> void take(T t, List<T> ts)
		ITypeBinding[] declaredParameterTypes= methodBinding.getMethodDeclaration().getParameterTypes();
		int lastParamIdx= declaredParameterTypes.length - 1;
		for (int i= 0; i < arguments.size(); i++) {
			Expression arg= arguments.get(i);
			ConstraintVariable2 argCv= getConstraintVariable(arg);
			if (argCv == null)
				continue;

			TType declaredParameterType;
			int iParam;
			if (! methodBinding.isVarargs() || i < lastParamIdx) {
				iParam= i;
				declaredParameterType= fTCModel.createTType(declaredParameterTypes[iParam]);
			} else { // isVararg() && i >= lastParamIdx
				iParam= lastParamIdx;
				declaredParameterType= fTCModel.createTType(declaredParameterTypes[iParam]);
				if (i == lastParamIdx && canAssignToVararg(
						fTCModel.createTType(arg.resolveTypeBinding()),
						(org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.ArrayType) declaredParameterType)) {
					//OK: argument will not be packed into an array
				} else {
					declaredParameterType= ((org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.ArrayType) declaredParameterType).getComponentType();
				}
			}

			if (declaredParameterType.isTypeVariable()) {

				ConstraintVariable2 methodTypeVariableCv= methodTypeVariables.get(declaredParameterType.getBindingKey());
				if (methodTypeVariableCv != null) {
					// e.g. t in "<T> void take(T t, List<T> ts)"
					fTCModel.createSubtypeConstraint(argCv, methodTypeVariableCv);

				} else {
					if (createdType != null) {
						//e.g. Tuple<T1, T2>: constructor Tuple(T1 t1, T2 t2)
						ConstraintVariable2 createdTypeCv= getConstraintVariable(createdType);
						ConstraintVariable2 elementCv= fTCModel.getElementVariable(createdTypeCv, (TypeVariable) declaredParameterType);
						// [arg] <= Elem[createdType]:
						fTCModel.createSubtypeConstraint(argCv, elementCv);
					}
					if (receiver != null) {
						//e.g. "Collection<E>: boolean add(E o)"
						ConstraintVariable2 expressionCv= getConstraintVariable(receiver);
						ConstraintVariable2 elementCv= fTCModel.getElementVariable(expressionCv, (TypeVariable) declaredParameterType);

						//	//TypeVariableConstraintVariable2 typeVariableCv= fTCModel.makeTypeVariableVariable(declaredParameterType);
						//				ConstraintVariable2 elementCv= fTCModel.makeElementVariable(expressionCv, typeVariableCv);
						//TODO: Somebody must connect typeVariableCv to corresponding typeVariableCVs of supertypes.
						//- Do only once for binaries.
						//- Do when passing for sources.
						//- Keep a flag in CV whether done?
						//- Do in one pass over all TypeVarCvs at the end?

						// [arg] <= Elem[receiver]:
						fTCModel.createSubtypeConstraint(argCv, elementCv);
					} else {
						//TODO: ???
					}
				}
			} else if (declaredParameterType.isParameterizedType()) {
				TType[] typeArguments= ((ParameterizedType) declaredParameterType).getTypeArguments();
				TypeVariable[] typeParameters= ((GenericType) declaredParameterType.getTypeDeclaration()).getTypeParameters();
				for (int ta= 0; ta < typeArguments.length; ta++) {
					TType typeArgument= typeArguments[ta];
					CollectionElementVariable2 argElementCv= fTCModel.getElementVariable(argCv, typeParameters[ta]);
					if (typeArgument.isWildcardType()) {
						// Elem[arg] <= Elem[receiver]
						WildcardType wildcardTypeArgument= (WildcardType) typeArgument;
						TType bound= wildcardTypeArgument.getBound();
						if (bound != null && bound.isTypeVariable()) {
							ConstraintVariable2 methodTypeVariableCv= methodTypeVariables.get(bound.getBindingKey());
							if (methodTypeVariableCv != null) {
								//e.g. in Collections: <T ..> T min(Collection<? extends T> coll):
								createWildcardConstraint(wildcardTypeArgument, argElementCv, methodTypeVariableCv);
							} else {
								if (createdType != null) {
									ConstraintVariable2 createdTypeCv= getConstraintVariable(createdType);
									CollectionElementVariable2 elementCv= fTCModel.getElementVariable(createdTypeCv, typeParameters[ta]);
									createWildcardConstraint(wildcardTypeArgument, argElementCv, elementCv);
								}
								if (receiver != null) {
									//e.g. Collection<E>: boolean addAll(Collection<? extends E> c)
									ConstraintVariable2 expressionCv= getConstraintVariable(receiver);
									CollectionElementVariable2 elementCv= fTCModel.getElementVariable(expressionCv, typeParameters[ta]);
									createWildcardConstraint(wildcardTypeArgument, argElementCv, elementCv);
								} else {
									//TODO: ???
								}
							}

						} else {
							//TODO
						}

					} else if (typeArgument.isTypeVariable()) {
						ConstraintVariable2 methodTypeVariableCv= methodTypeVariables.get(typeArgument.getBindingKey());
						if (methodTypeVariableCv != null) {
							//e.g. in Collections: <T> List<T> synchronizedList(List<T> list)
							fTCModel.createEqualsConstraint(argElementCv, methodTypeVariableCv);
						} else {
							if (createdType != null) {
								ConstraintVariable2 createdTypeCv= getConstraintVariable(createdType);
								ConstraintVariable2 elementCv= fTCModel.getElementVariable(createdTypeCv, (TypeVariable) typeArgument);
								fTCModel.createEqualsConstraint(argElementCv, elementCv);
							}
							if (receiver != null) {
								ConstraintVariable2 expressionCv= getConstraintVariable(receiver);
								ConstraintVariable2 elementCv= fTCModel.getElementVariable(expressionCv, (TypeVariable) typeArgument);
								fTCModel.createEqualsConstraint(argElementCv, elementCv);
							} else {
								//TODO: ???
							}
						}

					} else {
						ImmutableTypeVariable2 typeArgumentCv= fTCModel.makeImmutableTypeVariable(typeArgument);
						fTCModel.createEqualsConstraint(argElementCv, typeArgumentCv);
					}
				}

			} else if (declaredParameterType.isArrayType()) {
				//TODO: check methodBinding.isVarargs() !
				TType declaredElementType= ((org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.ArrayType) declaredParameterType).getElementType();
				if (declaredElementType.isTypeVariable()) {
					ConstraintVariable2 methodTypeVariableCv= methodTypeVariables.get(declaredElementType.getBindingKey());
					if (methodTypeVariableCv != null) {
						ArrayElementVariable2 argElementCv= fTCModel.getArrayElementVariable(argCv);
						//e.g. in Arrays: <T> List<T> asList(T... a): //<T> List<T> asList(T[] a)
						fTCModel.createEqualsConstraint(argElementCv, methodTypeVariableCv);
					} else {
						//TODO: receiver, createdType
					}
				} else {
					//TODO
				}

			} else { //TODO: not else, but always? Other kinds of type references?
				if (! InferTypeArgumentsTCModel.isAGenericType(declaredParameterType))
					continue;
				ParameterTypeVariable2 parameterTypeCv= fTCModel.makeParameterTypeVariable(methodBinding, iParam);
				// Elem[param] =^= Elem[arg]
				fTCModel.createElementEqualsConstraints(parameterTypeCv, argCv);
			}
		}
	}

	private boolean canAssignToVararg(TType rhs, org.eclipse.jdt.internal.corext.refactoring.typeconstraints.types.ArrayType lhs) {
		return TTypes.canAssignTo(rhs.getErasure(), lhs.getErasure());
	}

	private void createWildcardConstraint(WildcardType typeArgument, CollectionElementVariable2 argElementCv, ConstraintVariable2 paramElementCv) {
		if (typeArgument.isExtendsWildcardType())
			fTCModel.createSubtypeConstraint(argElementCv, paramElementCv);
		else
			fTCModel.createSubtypeConstraint(paramElementCv, argElementCv);
	}

	@Override
	public void endVisit(ClassInstanceCreation node) {
		Expression receiver= node.getExpression();
		Type createdType= node.getType();

		ConstraintVariable2 typeCv;
		if (node.getAnonymousClassDeclaration() == null) {
			typeCv= getConstraintVariable(createdType);
		} else {
			typeCv= fTCModel.makeImmutableTypeVariable(createdType.resolveBinding(), null);
			setConstraintVariable(createdType, typeCv);
		}
		setConstraintVariable(node, typeCv);

		IMethodBinding methodBinding= node.resolveConstructorBinding();
		Map<String, IndependentTypeVariable2> methodTypeVariables= createMethodTypeArguments(methodBinding);
		List<Expression> arguments= node.arguments();
		doVisitMethodInvocationArguments(methodBinding, arguments, receiver, methodTypeVariables, createdType);
	}

	@Override
	public void endVisit(ArrayCreation node) {
		ArrayType arrayType= node.getType();
		TypeVariable2 arrayTypeCv= (TypeVariable2) getConstraintVariable(arrayType);
		if (arrayTypeCv == null)
			return;
		setConstraintVariable(node, arrayTypeCv);
		//TODO: constraints for array initializer?
	}

	@Override
	public void endVisit(ReturnStatement node) {
		Expression expression= node.getExpression();
		if (expression == null)
			return;
		ConstraintVariable2 expressionCv= getConstraintVariable(expression);
		if (expressionCv == null)
			return;

		MethodDeclaration methodDeclaration= (MethodDeclaration) ASTNodes.getParent(node, ASTNode.METHOD_DECLARATION);
		if (methodDeclaration == null)
			return;
		IMethodBinding methodBinding= methodDeclaration.resolveBinding();
		if (methodBinding == null)
			return;
		ReturnTypeVariable2 returnTypeCv= fTCModel.makeReturnTypeVariable(methodBinding);
		if (returnTypeCv == null)
			return;

		fTCModel.createElementEqualsConstraints(returnTypeCv, expressionCv);
	}

	@Override
	public void endVisit(VariableDeclarationExpression node) {
		// Constrain the types of the child VariableDeclarationFragments to be equal to one
		// another, since the initializers in a 'for' statement can only have one type.
		// Pairwise constraints between adjacent variables is enough.
		Type type= node.getType();
		ConstraintVariable2 typeCv= getConstraintVariable(type);
		if (typeCv == null)
			return;

		setConstraintVariable(node, typeCv);

		List<VariableDeclarationFragment> fragments= node.fragments();
		for (VariableDeclarationFragment fragment : fragments) {
			ConstraintVariable2 fragmentCv= getConstraintVariable(fragment);
			fTCModel.createElementEqualsConstraints(typeCv, fragmentCv);
		}
	}

	@Override
	public void endVisit(VariableDeclarationStatement node) {
		// TODO: in principle, no need to tie the VariableDeclarationFragments together.
		// The VariableDeclarationExpression can be split up when fragments get different types.
		// Warning: still need to connect fragments with type!
		endVisitFieldVariableDeclaration(node.getType(), node.fragments());
	}

	@Override
	public void endVisit(FieldDeclaration node) {
		// TODO: in principle, no need to tie the VariableDeclarationFragments together.
		// The FieldDeclaration can be split up when fragments get different types.
		// Warning: still need to connect fragments with type!
		endVisitFieldVariableDeclaration(node.getType(), node.fragments());
	}

	private void endVisitFieldVariableDeclaration(Type type, List<VariableDeclarationFragment> list) {
		ConstraintVariable2 typeCv= getConstraintVariable(type);
		if (typeCv == null)
			return;

		for (VariableDeclarationFragment fragment : list) {
			ConstraintVariable2 fragmentCv= getConstraintVariable(fragment);
			fTCModel.createElementEqualsConstraints(typeCv, fragmentCv);
		}
	}

	@Override
	public void endVisit(SingleVariableDeclaration node) {
		// used for formal method parameters and catch clauses
		//TODO: extra dimensions?

//		ConstraintVariable2 typeCv= getConstraintVariable(node.getType()); //TODO: who needs this?

//		ConstraintVariable2 nameCv;
//		switch (node.getParent().getNodeType()) {
//			case ASTNode.METHOD_DECLARATION :
//				MethodDeclaration parent= (MethodDeclaration) node.getParent();
//				int index= parent.parameters().indexOf(node);
//				nameCv= fTCFactory.makeParameterTypeVariable(parent.resolveBinding(), index, node.getType());
//				//store source range even if variable not used in constraint here. TODO: move to visit(MethodDeclaration)?
//				break;
//			case ASTNode.CATCH_CLAUSE :
//				nameCv= fTCFactory.makeVariableVariable(node.resolveBinding());
//
//				break;
//			default:
//				unexpectedNode(node.getParent());
//		}
//		setConstraintVariable(node, nameCv);

		//TODO: Move this into visit(SimpleName) or leave it here?
//		ExpressionVariable2 name= fTCFactory.makeExpressionVariable(node.getName());
//		TypeVariable2 type= fTCFactory.makeTypeVariable(node.getType());
//		ITypeConstraint2[] nameEqualsType= fTCFactory.createEqualsConstraint(name, type);
//		addConstraints(nameEqualsType);

		//TODO: When can a SingleVariableDeclaration have an initializer? Never up to Java 1.5?
		Expression initializer= node.getInitializer();
		if (initializer == null)
			return;

//		ConstraintVariable2 initializerCv= getConstraintVariable(initializer);
//		ConstraintVariable2 nameCv= getConstraintVariable(node);
		//TODO: check: property has been set in visit(CatchClause), visit(MethodDeclaration), visit(EnhancedForStatament)
		//fTCFactory.createSubtypeConstraint(initializerCv, nameCv); //TODO: not for augment raw container clients
	}

	@Override
	public void endVisit(VariableDeclarationFragment node) {
		VariableVariable2 cv= fTCModel.makeDeclaredVariableVariable(node.resolveBinding(), fCU);
		if (cv == null)
			return;

		setConstraintVariable(node, cv);

		//TODO: prune unused CV for local variables (but not fields)

		Expression initializer= node.getInitializer();
		if (initializer == null)
			return;

		ConstraintVariable2 initializerCv= getConstraintVariable(initializer);
		if (initializerCv == null)
			return;

		fTCModel.createElementEqualsConstraints(cv, initializerCv);

		// name= initializer  -->  [initializer] <= [name]
//		if (initializerCv instanceof CollectionElementVariable2)
//			fTCModel.createSubtypeConstraint(initializerCv, cv);
	}

	//--------- private helpers ----------------//

	public InferTypeArgumentsTCModel getTCModel() {
		return fTCModel;
	}

	/**
	 * @param node the ASTNode
	 * @return the {@link ConstraintVariable2} associated with the node, or <code>null</code>
	 */
	protected static ConstraintVariable2 getConstraintVariable(ASTNode node) {
		return (ConstraintVariable2) node.getProperty(CV_PROP);
	}

	/**
	 * @param node the ASTNode
	 * @param constraintVariable the {@link ConstraintVariable2} to be associated with node
	 */
	protected static void setConstraintVariable(ASTNode node, ConstraintVariable2 constraintVariable) {
		node.setProperty(CV_PROP, constraintVariable);
	}

//	private void logUnexpectedNode(ASTNode node, String msg) {
//		String message= msg == null ? "" : msg + ":\n";  //$NON-NLS-1$//$NON-NLS-2$
//		if (node == null) {
//			message+= "ASTNode was not expected to be null"; //$NON-NLS-1$
//		} else {
//			message+= "Found unexpected node (type: " + node.getNodeType() + "):\n" + node.toString(); //$NON-NLS-1$ //$NON-NLS-2$
//		}
//		JavaPlugin.log(new Exception(message).fillInStackTrace());
//	}

}
