/*******************************************************************************
 * Copyright (c) 2010, 2014 Marc-Andre Laperle 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:
 *     Marc-Andre Laperle - Initial API and implementation
 *     Tomasz Wesolowski  - Extension for fixes
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.cdt.codan.internal.checkers;

import org.eclipse.cdt.codan.checkers.CodanCheckersActivator;
import org.eclipse.cdt.codan.core.cxx.CxxAstUtils;
import org.eclipse.cdt.codan.core.cxx.model.AbstractIndexAstChecker;
import org.eclipse.cdt.codan.core.model.CheckerLaunchMode;
import org.eclipse.cdt.codan.core.model.IProblemWorkingCopy;
import org.eclipse.cdt.core.dom.ast.ASTTypeUtil;
import org.eclipse.cdt.core.dom.ast.ASTVisitor;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.IASTDeclarator;
import org.eclipse.cdt.core.dom.ast.IASTExpression;
import org.eclipse.cdt.core.dom.ast.IASTFieldReference;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.IASTFunctionDefinition;
import org.eclipse.cdt.core.dom.ast.IASTIdExpression;
import org.eclipse.cdt.core.dom.ast.IASTImplicitName;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNamedTypeSpecifier;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IFunctionType;
import org.eclipse.cdt.core.dom.ast.IProblemBinding;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTDeclarator;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTLiteralExpression;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTQualifiedName;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTTemplateId;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPClassType;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPFunction;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPMethod;
import org.eclipse.cdt.core.dom.ast.cpp.SemanticQueries;
import org.eclipse.cdt.internal.core.dom.parser.ASTQueries;
import org.eclipse.cdt.internal.core.dom.parser.cpp.semantics.CPPSemantics;

public class ProblemBindingChecker extends AbstractIndexAstChecker {
	public static String ERR_ID_OverloadProblem = "org.eclipse.cdt.codan.internal.checkers.OverloadProblem"; //$NON-NLS-1$
	public static String ERR_ID_AmbiguousProblem = "org.eclipse.cdt.codan.internal.checkers.AmbiguousProblem"; //$NON-NLS-1$
	public static String ERR_ID_CircularReferenceProblem = "org.eclipse.cdt.codan.internal.checkers.CircularReferenceProblem"; //$NON-NLS-1$
	public static String ERR_ID_RedeclarationProblem = "org.eclipse.cdt.codan.internal.checkers.RedeclarationProblem"; //$NON-NLS-1$
	public static String ERR_ID_RedefinitionProblem = "org.eclipse.cdt.codan.internal.checkers.RedefinitionProblem"; //$NON-NLS-1$
	public static String ERR_ID_MemberDeclarationNotFoundProblem = "org.eclipse.cdt.codan.internal.checkers.MemberDeclarationNotFoundProblem"; //$NON-NLS-1$
	public static String ERR_ID_LabelStatementNotFoundProblem = "org.eclipse.cdt.codan.internal.checkers.LabelStatementNotFoundProblem"; //$NON-NLS-1$
	public static String ERR_ID_InvalidTemplateArgumentsProblem = "org.eclipse.cdt.codan.internal.checkers.InvalidTemplateArgumentsProblem"; //$NON-NLS-1$
	public static String ERR_ID_TypeResolutionProblem = "org.eclipse.cdt.codan.internal.checkers.TypeResolutionProblem"; //$NON-NLS-1$
	public static String ERR_ID_FunctionResolutionProblem = "org.eclipse.cdt.codan.internal.checkers.FunctionResolutionProblem"; //$NON-NLS-1$
	public static String ERR_ID_InvalidArguments = "org.eclipse.cdt.codan.internal.checkers.InvalidArguments"; //$NON-NLS-1$
	public static String ERR_ID_MethodResolutionProblem = "org.eclipse.cdt.codan.internal.checkers.MethodResolutionProblem"; //$NON-NLS-1$
	public static String ERR_ID_FieldResolutionProblem = "org.eclipse.cdt.codan.internal.checkers.FieldResolutionProblem"; //$NON-NLS-1$
	public static String ERR_ID_VariableResolutionProblem = "org.eclipse.cdt.codan.internal.checkers.VariableResolutionProblem"; //$NON-NLS-1$
	public static String ERR_ID_Candidates = "org.eclipse.cdt.codan.internal.checkers.Candidates"; //$NON-NLS-1$

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

	@Override
	public void initPreferences(IProblemWorkingCopy problem) {
		super.initPreferences(problem);
		// This checker should not run on full or incremental build
		getLaunchModePreference(problem).enableInLaunchModes(CheckerLaunchMode.RUN_AS_YOU_TYPE,
				CheckerLaunchMode.RUN_ON_DEMAND);
	}

	@Override
	public void processAst(IASTTranslationUnit ast) {
		try {
			ast.accept(new ASTVisitor() {
				{
					shouldVisitNames = true;
					shouldVisitImplicitNames = true;
				}

				@Override
				public int visit(IASTName name) {
					try {
						IBinding binding = name.resolveBinding();
						if (binding instanceof IProblemBinding) {
							IASTNode parentNode = name.getParent();
							// Don't report multiple problems with qualified names.
							if (parentNode instanceof ICPPASTQualifiedName) {
								if (((ICPPASTQualifiedName) parentNode).resolveBinding() instanceof IProblemBinding)
									return PROCESS_CONTINUE;
							}
							String contextFlagsString = createContextFlagsString(name);
							IProblemBinding problemBinding = (IProblemBinding) binding;
							int id = problemBinding.getID();
							if (id == IProblemBinding.SEMANTIC_INVALID_OVERLOAD) {
								reportProblem(ERR_ID_OverloadProblem, name, name.getRawSignature(), contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_AMBIGUOUS_LOOKUP) {
								String candidatesString = getCandidatesString(problemBinding);
								reportProblem(ERR_ID_AmbiguousProblem, name, name.getRawSignature(), candidatesString,
										contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_CIRCULAR_INHERITANCE) {
								String typeString;
								IASTNode problemNode;
								if (parentNode instanceof IASTFieldReference) {
									IASTExpression ownerExpression = ((IASTFieldReference) parentNode).getFieldOwner();
									typeString = ASTTypeUtil.getType(ownerExpression.getExpressionType());
									problemNode = ownerExpression;
								} else {
									problemNode = name;
									typeString = name.getRawSignature();
								}
								reportProblem(ERR_ID_CircularReferenceProblem, problemNode, typeString,
										contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_INVALID_REDECLARATION) {
								reportProblem(ERR_ID_RedeclarationProblem, name, name.getRawSignature(),
										contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_INVALID_REDEFINITION) {
								reportProblem(ERR_ID_RedefinitionProblem, name, name.getRawSignature(),
										contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_MEMBER_DECLARATION_NOT_FOUND) {
								reportProblem(ERR_ID_MemberDeclarationNotFoundProblem, name, contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_LABEL_STATEMENT_NOT_FOUND) {
								reportProblem(ERR_ID_LabelStatementNotFoundProblem, name, name.getRawSignature(),
										contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_INVALID_TEMPLATE_ARGUMENTS) {
								// We use the templateName since we don't want the whole
								// argument list to be underlined. That way we can see which argument is invalid.
								IASTNode templateName = getTemplateName(name);
								reportProblem(ERR_ID_InvalidTemplateArgumentsProblem, templateName, contextFlagsString);
								return PROCESS_CONTINUE;
							}
							if (id == IProblemBinding.SEMANTIC_INVALID_TYPE) {
								reportProblem(ERR_ID_TypeResolutionProblem, name, name.getRawSignature(),
										contextFlagsString);
								return PROCESS_CONTINUE;
							}
							// From this point, we'll deal only with NAME_NOT_FOUND problems.
							// If it's something else continue because we don't want to give bad messages.
							if (id != IProblemBinding.SEMANTIC_NAME_NOT_FOUND) {
								return PROCESS_CONTINUE;
							}
							if (SemanticQueries.isUnknownBuiltin(problemBinding, name)) {
								return PROCESS_CONTINUE; // Ignore an unknown built-in.
							}
							if (isFunctionCall(name, parentNode)) {
								handleFunctionProblem(name, problemBinding, contextFlagsString);
							} else if (parentNode instanceof IASTFieldReference) {
								handleMemberProblem(name, parentNode, problemBinding, contextFlagsString);
							} else if (parentNode instanceof IASTNamedTypeSpecifier) {
								reportProblem(ERR_ID_TypeResolutionProblem, name, name.getRawSignature(),
										contextFlagsString);
							} else {
								// Probably a variable.
								handleVariableProblem(name, contextFlagsString);
							}
						}
					} catch (DOMException e) {
						CodanCheckersActivator.log(e);
					}
					return PROCESS_CONTINUE;
				}
			});
		} catch (Exception e) {
			CodanCheckersActivator.log(e);
		}
	}

	protected String createContextFlagsString(IASTName name) {
		StringBuilder buf = new StringBuilder();
		if (isInClassContext(name)) {
			buf.append(":class"); //$NON-NLS-1$
		}
		if (isInFunctionContext(name)) {
			buf.append(":func"); //$NON-NLS-1$
		}
		return buf.toString();
	}

	private boolean isInClassContext(IASTName name) {
		if (CxxAstUtils.getEnclosingCompositeTypeSpecifier(name) != null) {
			return true;
		}
		IASTFunctionDefinition function = CxxAstUtils.getEnclosingFunction(name);
		if (function == null) {
			return false;
		}
		IASTDeclarator innermostDeclarator = ASTQueries.findInnermostDeclarator(function.getDeclarator());
		IBinding binding = innermostDeclarator.getName().resolveBinding();
		return (binding instanceof ICPPMethod);
	}

	private boolean isInFunctionContext(IASTName name) {
		IASTFunctionDefinition function = CxxAstUtils.getEnclosingFunction(name);
		return (function != null);
	}

	private void handleFunctionProblem(IASTName name, IProblemBinding problemBinding, String contextFlagsString)
			throws DOMException {
		if (problemBinding.getCandidateBindings().length == 0) {
			reportProblem(ERR_ID_FunctionResolutionProblem, name.getLastName(), new String(name.getSimpleID()),
					contextFlagsString);
		} else {
			String candidatesString = getCandidatesString(problemBinding);
			reportProblem(ERR_ID_InvalidArguments, name.getLastName(), candidatesString, contextFlagsString);
		}
	}

	private void handleMemberProblem(IASTName name, IASTNode parentNode, IProblemBinding problemBinding,
			String contextFlagsString) throws DOMException {
		IASTNode parentParentNode = parentNode.getParent();

		// An IASTFieldReference corresponds to a method if it's the first child in the parent function call expression
		// For example,
		//   func(x.y()); the field reference is first in the x.y() function call expression -> y is a method
		//   func(x.y); the field reference is second in the func(x.y) function call expression, func is first as a Id Expression -> y is a field
		boolean isMethod = parentParentNode instanceof IASTFunctionCallExpression
				&& parentParentNode.getChildren()[0] == parentNode;
		if (isMethod) {
			if (problemBinding.getCandidateBindings().length == 0) {
				reportProblem(ERR_ID_MethodResolutionProblem, name.getLastName(), new String(name.getSimpleID()),
						contextFlagsString);
			} else {
				String candidatesString = getCandidatesString(problemBinding);
				reportProblem(ERR_ID_InvalidArguments, name.getLastName(), candidatesString, contextFlagsString);
			}
		} else {
			reportProblem(ERR_ID_FieldResolutionProblem, name.getLastName(), name.getRawSignature(),
					contextFlagsString);
		}
	}

	private void handleVariableProblem(IASTName name, String contextFlagsString) {
		reportProblem(ERR_ID_VariableResolutionProblem, name, name.getBinding().getName(), contextFlagsString,
				name.getRawSignature());
	}

	private boolean isFunctionCall(IASTName name, IASTNode parentNode) {
		if (parentNode instanceof IASTIdExpression) {
			IASTIdExpression expression = (IASTIdExpression) parentNode;
			IASTNode parentParentNode = expression.getParent();
			if (parentParentNode instanceof IASTFunctionCallExpression
					&& expression.getPropertyInParent() == IASTFunctionCallExpression.FUNCTION_NAME) {
				return true;
			}
		} else if (parentNode instanceof ICPPASTDeclarator && name instanceof IASTImplicitName) {
			return true;
		} else if (parentNode instanceof ICPPASTLiteralExpression && name instanceof IASTImplicitName) {
			// Implicit name for user-defined literal operator.
			return true;
		}
		return false;
	}

	protected IASTNode getTemplateName(IASTName name) {
		IASTName nameToGetTempate = name.getLastName();
		if (nameToGetTempate instanceof ICPPASTTemplateId) {
			return ((ICPPASTTemplateId) nameToGetTempate).getTemplateName();
		}
		return nameToGetTempate;
	}

	/**
	 * Returns a string of the candidates for the binding
	 *
	 * @param problemBinding
	 * @return A string of the candidates, one per line
	 * @throws DOMException
	 */
	private String getCandidatesString(IProblemBinding problemBinding) throws DOMException {
		String candidatesString = "\n" + CheckersMessages.ProblemBindingChecker_Candidates + "\n"; //$NON-NLS-1$ //$NON-NLS-2$
		String lastSignature = ""; //$NON-NLS-1$
		for (IBinding candidateBinding : problemBinding.getCandidateBindings()) {
			if (candidateBinding instanceof ICPPFunction) {
				ICPPFunction functionBinding = (ICPPFunction) candidateBinding;
				String signature = getFunctionSignature(functionBinding);
				if (!signature.equals(lastSignature)) {
					candidatesString += signature + "\n"; //$NON-NLS-1$
					lastSignature = signature;
				}
			} else if (candidateBinding instanceof ICPPClassType) {
				ICPPClassType classType = (ICPPClassType) candidateBinding;
				try {
					CPPSemantics.pushLookupPoint(problemBinding.getASTNode());
					for (ICPPFunction constructor : classType.getConstructors()) {
						String signature = getFunctionSignature(constructor);
						if (!signature.equals(lastSignature)) {
							candidatesString += signature + "\n"; //$NON-NLS-1$
							lastSignature = signature;
						}
					}
				} finally {
					CPPSemantics.popLookupPoint();
				}
			}
		}
		return candidatesString;
	}

	/**
	 * Returns a string of the function signature : returntype + function + parameters
	 *
	 * @param functionBinding The function to get the signature
	 * @return A string of the function signature
	 * @throws DOMException
	 */
	private String getFunctionSignature(ICPPFunction functionBinding) throws DOMException {
		IFunctionType functionType = functionBinding.getType();
		String returnTypeString = ASTTypeUtil.getType(functionBinding.getType().getReturnType()) + " "; //$NON-NLS-1$
		String functionName = functionBinding.getName();
		String parameterTypeString = ASTTypeUtil.getParameterTypeString(functionType);
		return returnTypeString + functionName + parameterTypeString;
	}
}
