/*******************************************************************************
 * Copyright (c) 2011, 2022 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
 *     Nikolay Metchev <nikolaymetchev@gmail.com> - [inline] Inline local variable with initializer generates assignment where left-hand side is not a variable - https://bugs.eclipse.org/394721
 *******************************************************************************/

package org.eclipse.jdt.internal.core.manipulation.dom;

import java.util.Iterator;

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.AssertStatement;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.ConditionalExpression;
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.ForStatement;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.InfixExpression.Operator;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.SynchronizedStatement;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.WhileStatement;

import org.eclipse.jdt.internal.corext.dom.ASTNodes;

/**
 * Helper class to check if an expression requires parentheses.
 *
 * @see JDTUIHelperClasses
 * @since 3.7
 */
public class NecessaryParenthesesChecker {
	private static boolean expressionTypeNeedsParentheses(Expression expression) {
		int type= expression.getNodeType();
		return type == ASTNode.INFIX_EXPRESSION
				|| type == ASTNode.CONDITIONAL_EXPRESSION
				|| type == ASTNode.PREFIX_EXPRESSION
				|| type == ASTNode.POSTFIX_EXPRESSION
				|| type == ASTNode.CAST_EXPRESSION
				|| type == ASTNode.INSTANCEOF_EXPRESSION
				|| type == ASTNode.PATTERN_INSTANCEOF_EXPRESSION
				|| type == ASTNode.ARRAY_CREATION
				|| type == ASTNode.ASSIGNMENT;
	}

	private static boolean locationNeedsParentheses(StructuralPropertyDescriptor locationInParent) {
		if (locationInParent instanceof ChildListPropertyDescriptor && locationInParent != InfixExpression.EXTENDED_OPERANDS_PROPERTY) {
			// e.g. argument lists of MethodInvocation, ClassInstanceCreation, dimensions of ArrayCreation ...
			return false;
		}
		if (locationInParent == VariableDeclarationFragment.INITIALIZER_PROPERTY
				|| locationInParent == SingleVariableDeclaration.INITIALIZER_PROPERTY
				|| locationInParent == ReturnStatement.EXPRESSION_PROPERTY
				|| locationInParent == EnhancedForStatement.EXPRESSION_PROPERTY
				|| locationInParent == ForStatement.EXPRESSION_PROPERTY
				|| locationInParent == WhileStatement.EXPRESSION_PROPERTY
				|| locationInParent == DoStatement.EXPRESSION_PROPERTY
				|| locationInParent == AssertStatement.EXPRESSION_PROPERTY
				|| locationInParent == AssertStatement.MESSAGE_PROPERTY
				|| locationInParent == IfStatement.EXPRESSION_PROPERTY
				|| locationInParent == SwitchStatement.EXPRESSION_PROPERTY
				|| locationInParent == SwitchCase.EXPRESSION_PROPERTY
				|| locationInParent == SwitchCase.EXPRESSIONS2_PROPERTY
				|| locationInParent == ArrayAccess.INDEX_PROPERTY
				|| locationInParent == ThrowStatement.EXPRESSION_PROPERTY
				|| locationInParent == SynchronizedStatement.EXPRESSION_PROPERTY
				|| locationInParent == ParenthesizedExpression.EXPRESSION_PROPERTY) {
			return false;
		}
		return true;
	}

	/*
	 * Do all operands in expression have same type
	 */
	private static boolean isAllOperandsHaveSameType(InfixExpression expression, ITypeBinding leftOperandType, ITypeBinding rightOperandType) {
		ITypeBinding binding= leftOperandType;
		if (binding == null)
			return false;

		ITypeBinding current= rightOperandType;
		if (binding != current)
			return false;

		for (Iterator<Expression> iterator= expression.extendedOperands().iterator(); iterator.hasNext();) {
			Expression operand= iterator.next();
			current= operand.resolveTypeBinding();
			if (binding != current)
				return false;
		}

		return true;
	}

	private static boolean isIntegerType(ITypeBinding binding) {
		if (binding == null)
			return false;

		if (!binding.isPrimitive())
			return false;

		String name= binding.getName();
		if (isIntegerNumber(name))
			return true;

		return false;
	}

	private static boolean isStringType(ITypeBinding binding) {
		if (binding == null)
			return false;

		return "java.lang.String".equals(binding.getQualifiedName()); //$NON-NLS-1$
	}

	/*
	 * Is the given expression associative?
	 *
	 * This is true if and only if:<br>
	 * <code>left operator (right) == (right) operator left == right operator left</code>
	 */
	private static boolean isAssociative(InfixExpression.Operator operator, ITypeBinding infixExprType, boolean isAllOperandsHaveSameType) {
		if (operator == InfixExpression.Operator.PLUS)
			return isStringType(infixExprType) || isIntegerType(infixExprType) && isAllOperandsHaveSameType;

		if (operator == InfixExpression.Operator.TIMES)
			return isIntegerType(infixExprType) && isAllOperandsHaveSameType;

		if (operator == InfixExpression.Operator.CONDITIONAL_AND
				|| operator == InfixExpression.Operator.CONDITIONAL_OR
				|| operator == InfixExpression.Operator.AND
				|| operator == InfixExpression.Operator.OR
				|| operator == InfixExpression.Operator.XOR)
			return true;

		return false;
	}

	private static boolean isIntegerNumber(String name) {
		return "int".equals(name) || "long".equals(name) || "byte".equals(name) || "char".equals(name) || "short".equals(name); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
	}

	private static boolean needsParenthesesInInfixExpression(Expression expression, InfixExpression parentInfix, StructuralPropertyDescriptor locationInParent,
			ITypeBinding leftOperandType) {
		InfixExpression.Operator parentInfixOperator= parentInfix.getOperator();
		ITypeBinding rightOperandType;
		ITypeBinding parentInfixExprType;
		if (leftOperandType == null) { // parentInfix has bindings
			leftOperandType= parentInfix.getLeftOperand().resolveTypeBinding();
			rightOperandType= parentInfix.getRightOperand().resolveTypeBinding();
			parentInfixExprType= parentInfix.resolveTypeBinding();
		} else {
			rightOperandType= expression.resolveTypeBinding();
			parentInfixExprType= getInfixExpressionType(parentInfixOperator, leftOperandType, rightOperandType);
		}
		boolean isAllOperandsHaveSameType= isAllOperandsHaveSameType(parentInfix, leftOperandType, rightOperandType);

		if (locationInParent == InfixExpression.LEFT_OPERAND_PROPERTY) {
			//we have (expr op expr) op expr
			//infix expressions are evaluated from left to right -> parentheses not needed
			return false;
		} else if (isAssociative(parentInfixOperator, parentInfixExprType, isAllOperandsHaveSameType)) {
			//we have parent op (expr op expr) and op is associative
			//left op (right) == (right) op left == right op left
			if (expression instanceof InfixExpression) {
				InfixExpression infixExpression= (InfixExpression)expression;
				Operator operator= infixExpression.getOperator();

				if (isStringType(parentInfixExprType)) {
					if (parentInfixOperator == InfixExpression.Operator.PLUS && operator == InfixExpression.Operator.PLUS && isStringType(infixExpression.resolveTypeBinding())) {
						// 1 + ("" + 2) == 1 + "" + 2
						// 1 + (2 + "") != 1 + 2 + ""
						// "" + (2 + "") == "" + 2 + ""
						return !isStringType(infixExpression.getLeftOperand().resolveTypeBinding()) && !isStringType(leftOperandType);
					}
					//"" + (1 + 2), "" + (1 - 2) etc
					return true;
				}

				if (parentInfixOperator != InfixExpression.Operator.TIMES)
					return false;

				if (operator == InfixExpression.Operator.TIMES)
					// x * (y * z) == x * y * z
					return false;

				if (operator == InfixExpression.Operator.REMAINDER || operator == InfixExpression.Operator.DIVIDE)
					// x * (y % z) != x * y % z , x * (y / z) == x * y / z rounding involved
					return true;

				return false;
			}
			return false;
		} else {
			return true;
		}
	}

	/**
	 * Returns the type of infix expression based on its operands and operator.
	 *
	 * @param operator the operator of infix expression
	 * @param leftOperandType the type of left operand of infix expression
	 * @param rightOperandType the type of right operand of infix expression
	 * @return the type of infix expression if the type of both the operands is same or if the type
	 *         of either operand of a + operator is String, <code>null</code> otherwise.
	 *
	 * @since 3.9
	 */
	private static ITypeBinding getInfixExpressionType(InfixExpression.Operator operator, ITypeBinding leftOperandType, ITypeBinding rightOperandType) {
		if (leftOperandType == rightOperandType) {
			return leftOperandType;
		}
		if (operator == InfixExpression.Operator.PLUS) {
			if (isStringType(leftOperandType)) {
				return leftOperandType;
			} else if (isStringType(rightOperandType)) {
				return rightOperandType;
			}
		}
		// If the left and right operand types are different, we assume that parentheses are needed.
		// This is to avoid complications of numeric promotions and for readability of complicated code.
		return null;
	}

	/**
	 * Can the parentheses be removed from the parenthesized expression ?
	 *
	 * <p>
	 * <b>Note:</b> The parenthesized expression must not be an unparented node.
	 * </p>
	 *
	 * @param expression the parenthesized expression
	 * @return <code>true</code> if parentheses can be removed, <code>false</code> otherwise.
	 */
	public static boolean canRemoveParentheses(Expression expression) {
		return canRemoveParentheses(expression, expression.getParent(), expression.getLocationInParent());
	}

	/**
	 * Can the parentheses be removed from the parenthesized expression when inserted into
	 * <code>parent</code> at <code>locationInParent</code> ?
	 *
	 * <p>
	 * <b>Note:</b> The parenthesized expression can be an unparented node.
	 * </p>
	 *
	 * @param expression the parenthesized expression
	 * @param parent the parent node
	 * @param locationInParent location of expression in the parent
	 * @return <code>true</code> if parentheses can be removed, <code>false</code> otherwise.
	 */
	public static boolean canRemoveParentheses(Expression expression, ASTNode parent, StructuralPropertyDescriptor locationInParent) {
		if (!(expression instanceof ParenthesizedExpression)) {
			return false;
		}
		return !needsParentheses(ASTNodes.getUnparenthesedExpression(expression), parent, locationInParent);
	}

	/**
	 * Does the <code>rightOperand</code> need parentheses when inserted into
	 * <code>infixExpression</code> ?
	 *
	 * <p>
	 * <b>Note:</b>
	 * <ul>
	 * <li>The <code>infixExpression</code> can be a new node (not from a resolved AST) with no
	 * bindings.</li>
	 * <li>The <code>infixExpression</code> must not have additional operands.</li>
	 * <li>The <code>rightOperand</code> node must have bindings.</li>
	 * </ul>
	 * </p>
	 *
	 * @param rightOperand the right operand in <code>infixExpression</code>
	 * @param infixExpression the parent infix expression
	 * @param leftOperandType the type of the left operand in <code>infixExpression</code>
	 * @return <code>true</code> if <code>rightOperand</code> needs parentheses, <code>false</code>
	 *         otherwise.
	 *
	 * @since 3.9
	 */
	public static boolean needsParenthesesForRightOperand(Expression rightOperand, InfixExpression infixExpression, ITypeBinding leftOperandType) {
		return needsParentheses(rightOperand, infixExpression, InfixExpression.RIGHT_OPERAND_PROPERTY, leftOperandType);
	}

	/**
	 * Does the <code>expression</code> need parentheses when inserted into <code>parent</code> at
	 * <code>locationInParent</code> ?
	 *
	 * <p>
	 * <b>Note:</b> The expression can be an unparented node.
	 * </p>
	 *
	 * @param expression the expression
	 * @param parent the parent node
	 * @param locationInParent location of expression in the parent
	 * @return <code>true</code> if <code>expression</code> needs parentheses, <code>false</code>
	 *         otherwise.
	 */
	public static boolean needsParentheses(Expression expression, ASTNode parent, StructuralPropertyDescriptor locationInParent) {
		return needsParentheses(expression, parent, locationInParent, null);
	}

	/**
	 * Does the <code>expression</code> need parentheses when inserted into <code>parent</code> at
	 * <code>locationInParent</code> ?
	 *
	 * @param expression the expression
	 * @param parent the parent node
	 * @param locationInParent location of expression in the parent
	 * @param leftOperandType the type of the left operand in <code>parent</code> if
	 *            <code>parent</code> is an infix expression with no bindings and
	 *            <code>expression</code> is the right operand in it, <code>null</code> otherwise
	 * @return <code>true</code> if <code>expression</code> needs parentheses, <code>false</code>
	 *         otherwise.
	 *
	 * @since 3.9
	 */
	private static boolean needsParentheses(Expression expression, ASTNode parent, StructuralPropertyDescriptor locationInParent, ITypeBinding leftOperandType) {
		if (!expressionTypeNeedsParentheses(expression))
			return false;

		if (!locationNeedsParentheses(locationInParent)) {
			return false;
		}

		if (parent instanceof Expression) {
			Expression parentExpression= (Expression)parent;

			if (expression instanceof PrefixExpression) { // see bug 405096
				return needsParenthesesForPrefixExpression(parentExpression, ((PrefixExpression) expression).getOperator());
			}

			if (expression instanceof ArrayCreation) { // see bug 394721
				return parentExpression instanceof ArrayAccess && ((ArrayCreation) expression).getInitializer() == null;
			}

			int expressionPrecedence= OperatorPrecedence.getExpressionPrecedence(expression);
			int parentPrecedence= OperatorPrecedence.getExpressionPrecedence(parentExpression);

			if (expressionPrecedence > parentPrecedence)
				//(opEx) opParent and opEx binds more -> parentheses not needed
				return false;

			if (expressionPrecedence < parentPrecedence)
				//(opEx) opParent and opEx binds less -> parentheses needed
				return true;

			//(opEx) opParent binds equal

			if (parentExpression instanceof InfixExpression) {
				return needsParenthesesInInfixExpression(expression, (InfixExpression) parentExpression, locationInParent, leftOperandType);
			}

			if (parentExpression instanceof ConditionalExpression && locationInParent == ConditionalExpression.EXPRESSION_PROPERTY) {
				return true;
			}

			return false;
		}

		return true;
	}

	private static boolean needsParenthesesForPrefixExpression(Expression parentExpression, PrefixExpression.Operator expressionOperator) {
		if (parentExpression instanceof PrefixExpression) {
			PrefixExpression.Operator parentOperator= ((PrefixExpression) parentExpression).getOperator();
			if (parentOperator == PrefixExpression.Operator.PLUS &&
					(expressionOperator == PrefixExpression.Operator.PLUS || expressionOperator == PrefixExpression.Operator.INCREMENT)) {
				return true;
			}
			if (parentOperator == PrefixExpression.Operator.MINUS &&
					(expressionOperator == PrefixExpression.Operator.MINUS || expressionOperator == PrefixExpression.Operator.DECREMENT)) {
				return true;
			}
		} else if (parentExpression instanceof InfixExpression) {
			InfixExpression.Operator parentOperator= ((InfixExpression) parentExpression).getOperator();
			if (parentOperator == InfixExpression.Operator.PLUS &&
					(expressionOperator == PrefixExpression.Operator.PLUS || expressionOperator == PrefixExpression.Operator.INCREMENT)) {
				return true;
			}
			if (parentOperator == InfixExpression.Operator.MINUS &&
					(expressionOperator == PrefixExpression.Operator.MINUS || expressionOperator == PrefixExpression.Operator.DECREMENT)) {
				return true;
			}
		} else if (parentExpression instanceof CastExpression) {
			CastExpression castExp= (CastExpression) parentExpression;
			Type type= castExp.getType();
			if (type != null && type.isPrimitiveType()) {
				return false;
			}
			return true;
		}
		return false;
	}

	private NecessaryParenthesesChecker() {
	}
}