/*******************************************************************************
 * Copyright (c) 2000, 2018 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
 *     Mateusz Matela <mateusz.matela@gmail.com> - [formatter] Formatter does not format Java code correctly, especially when max line width is set
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.codemanipulation;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.NamingConventions;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Assignment.Operator;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.manipulation.CodeGeneration;

import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.core.manipulation.dom.NecessaryParenthesesChecker;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.JdtFlags;

public class GetterSetterUtil {

	private static final String[] EMPTY= new String[0];

	//no instances
	private GetterSetterUtil(){
	}

	public static String getGetterName(IField field, String[] excludedNames) throws JavaModelException {
		boolean useIs= StubUtility.useIsForBooleanGetters(field.getJavaProject());
		return getGetterName(field, excludedNames, useIs);
	}

	private static String getGetterName(IField field, String[] excludedNames, boolean useIsForBoolGetters) throws JavaModelException {
		if (excludedNames == null) {
			excludedNames= EMPTY;
		}
		IType type= field.getDeclaringType();
		if (type != null && type.isRecord() && !Flags.isStatic(field.getFlags())) {
			return field.getElementName();
		}
		return getGetterName(field.getJavaProject(), field.getElementName(), field.getFlags(), useIsForBoolGetters && JavaModelUtil.isBoolean(field), excludedNames);
	}

	public static String getGetterName(IVariableBinding variableType, IJavaProject project, String[] excludedNames, boolean isBoolean) {
		boolean useIs= StubUtility.useIsForBooleanGetters(project) && isBoolean;
		return getGetterName(project, variableType.getName(), variableType.getModifiers(), useIs, excludedNames);
	}

	public static String getGetterName(IJavaProject project, String fieldName, int flags, boolean isBoolean, String[] excludedNames){
		return NamingConventions.suggestGetterName(project, fieldName, flags, isBoolean, excludedNames);
	}

	public static String getSetterName(IVariableBinding variableType, IJavaProject project, String[] excludedNames, boolean isBoolean) {
		return getSetterName(project, variableType.getName(), variableType.getModifiers(), isBoolean, excludedNames);
	}

	public static String getSetterName(IJavaProject project, String fieldName, int flags, boolean isBoolean, String[] excludedNames){
		boolean useIs= StubUtility.useIsForBooleanGetters(project);
		return NamingConventions.suggestSetterName(project, fieldName, flags, useIs && isBoolean, excludedNames);
	}

	public static String getSetterName(IField field, String[] excludedNames) throws JavaModelException {
		if (excludedNames == null) {
			excludedNames= EMPTY;
		}
		return getSetterName(field.getJavaProject(), field.getElementName(), field.getFlags(), JavaModelUtil.isBoolean(field), excludedNames);
	}

	public static IMethod getGetter(IField field) throws JavaModelException{
		String getterName= getGetterName(field, EMPTY, true);
		IMethod primaryCandidate= JavaModelUtil.findMethod(getterName, new String[0], false, field.getDeclaringType());
		if (! JavaModelUtil.isBoolean(field) || (primaryCandidate != null && primaryCandidate.exists()))
			return primaryCandidate;
		//bug 30906 describes why we need to look for other alternatives here (try with get... for booleans)
		String secondCandidateName= getGetterName(field, EMPTY, false);
		return JavaModelUtil.findMethod(secondCandidateName, new String[0], false, field.getDeclaringType());
	}

	public static IMethod getSetter(IField field) throws JavaModelException{
		String[] args= new String[] { field.getTypeSignature() };
		return JavaModelUtil.findMethod(getSetterName(field, EMPTY), args, false, field.getDeclaringType());
	}

	/**
	 * Create a stub for a getter of the given field using getter/setter templates. The resulting code
	 * has to be formatted and indented.
	 * @param field The field to create a getter for
	 * @param setterName The chosen name for the setter
	 * @param addComments If <code>true</code>, comments will be added.
	 * @param flags The flags signaling visibility, if static, synchronized or final
	 * @return Returns the generated stub.
	 * @throws CoreException when stub creation failed
	 */
	public static String getSetterStub(IField field, String setterName, boolean addComments, int flags) throws CoreException {

		String fieldName= field.getElementName();
		IType parentType= field.getDeclaringType();

		String returnSig= field.getTypeSignature();
		String typeName= Signature.toString(returnSig);

		IJavaProject project= field.getJavaProject();

		String accessorName= StubUtility.getBaseName(field);
		String argname= StubUtility.suggestArgumentName(project, accessorName, EMPTY);

		boolean isStatic= Flags.isStatic(flags);
		boolean isSync= Flags.isSynchronized(flags);
		boolean isFinal= Flags.isFinal(flags);

		String lineDelim= "\n"; // Use default line delimiter, as generated stub has to be formatted anyway //$NON-NLS-1$
		StringBuilder buf= new StringBuilder();
		if (addComments) {
			String comment= CodeGeneration.getSetterComment(field.getCompilationUnit(), parentType.getTypeQualifiedName('.'), setterName, field.getElementName(), typeName, argname, accessorName, lineDelim);
			if (comment != null) {
				buf.append(comment);
				buf.append(lineDelim);
			}
		}
		buf.append(JdtFlags.getVisibilityString(flags));
		buf.append(' ');
		if (isStatic)
			buf.append("static "); //$NON-NLS-1$
		if (isSync)
			buf.append("synchronized "); //$NON-NLS-1$
		if (isFinal)
			buf.append("final "); //$NON-NLS-1$

		buf.append("void "); //$NON-NLS-1$
		buf.append(setterName);
		buf.append('(');
		buf.append(typeName);
		buf.append(' ');
		buf.append(argname);
		buf.append(") {"); //$NON-NLS-1$
		buf.append(lineDelim);

		boolean useThis= StubUtility.useThisForFieldAccess(project);
		if (argname.equals(fieldName) || (useThis && !isStatic)) {
			if (isStatic)
				fieldName= parentType.getElementName() + '.' + fieldName;
			else
				fieldName= "this." + fieldName; //$NON-NLS-1$
		}
		String body= CodeGeneration.getSetterMethodBodyContent(field.getCompilationUnit(), parentType.getTypeQualifiedName('.'), setterName, fieldName, argname, lineDelim);
		if (body != null) {
			buf.append(body);
		}
		buf.append("}"); //$NON-NLS-1$
		return buf.toString();
	}

	/**
	 * Create a stub for a getter of the given field using getter/setter templates. The resulting code
	 * has to be formatted and indented.
	 * @param field The field to create a getter for
	 * @param getterName The chosen name for the getter
	 * @param addComments If <code>true</code>, comments will be added.
	 * @param flags The flags signaling visibility, if static, synchronized or final
	 * @return Returns the generated stub.
	 * @throws CoreException when stub creation failed
	 */
	public static String getGetterStub(IField field, String getterName, boolean addComments, int flags) throws CoreException {
		String fieldName= field.getElementName();
		IType parentType= field.getDeclaringType();

		boolean isStatic= Flags.isStatic(flags);
		boolean isSync= Flags.isSynchronized(flags);
		boolean isFinal= Flags.isFinal(flags);

		String typeName= Signature.toString(field.getTypeSignature());
		String accessorName= StubUtility.getBaseName(field);

		String lineDelim= "\n"; // Use default line delimiter, as generated stub has to be formatted anyway //$NON-NLS-1$
		StringBuilder buf= new StringBuilder();
		if (addComments) {
			String comment= CodeGeneration.getGetterComment(field.getCompilationUnit(), parentType.getTypeQualifiedName('.'), getterName, field.getElementName(), typeName, accessorName, lineDelim);
			if (comment != null) {
				buf.append(comment);
				buf.append(lineDelim);
			}
		}

		buf.append(JdtFlags.getVisibilityString(flags));
		buf.append(' ');
		if (isStatic)
			buf.append("static "); //$NON-NLS-1$
		if (isSync)
			buf.append("synchronized "); //$NON-NLS-1$
		if (isFinal)
			buf.append("final "); //$NON-NLS-1$

		buf.append(typeName);
		buf.append(' ');
		buf.append(getterName);
		buf.append("() {"); //$NON-NLS-1$
		buf.append(lineDelim);

		boolean useThis= StubUtility.useThisForFieldAccess(field.getJavaProject());
		if (useThis && !isStatic) {
			fieldName= "this." + fieldName; //$NON-NLS-1$
		}

		String body= CodeGeneration.getGetterMethodBodyContent(field.getCompilationUnit(), parentType.getTypeQualifiedName('.'), getterName, fieldName, lineDelim);
		if (body != null) {
			buf.append(body);
		}
		buf.append("}"); //$NON-NLS-1$
		return buf.toString();
	}

	/**
	 * Converts an assignment, postfix expression or prefix expression into an assignable equivalent expression using the getter.
	 *
	 * @param node the assignment/prefix/postfix node
	 * @param astRewrite the astRewrite to use
	 * @param getterExpression the expression to insert for read accesses or <code>null</code> if such an expression does not exist
	 * @param variableType the type of the variable that the result will be assigned to
	 * @param is50OrHigher <code>true</code> if a 5.0 or higher environment can be used
	 * @return an expression that can be assigned to the type variableType with node being replaced by a equivalent expression using the getter
	 */
	public static Expression getAssignedValue(ASTNode node, ASTRewrite astRewrite, Expression getterExpression, ITypeBinding variableType, boolean is50OrHigher) {
		InfixExpression.Operator op= null;
		AST ast= astRewrite.getAST();
		if (isNotInBlock(node))
			return null;
		if (node.getNodeType() == ASTNode.ASSIGNMENT) {
			Assignment assignment= ((Assignment) node);
			Expression rightHandSide= assignment.getRightHandSide();
			Expression copiedRightOp= (Expression) astRewrite.createCopyTarget(rightHandSide);
			if (assignment.getOperator() == Operator.ASSIGN) {
				ITypeBinding rightHandSideType= rightHandSide.resolveTypeBinding();
				copiedRightOp= createNarrowCastIfNessecary(copiedRightOp, rightHandSideType, ast, variableType, is50OrHigher);
				return copiedRightOp;
			}
			if (getterExpression != null) {
				InfixExpression infix= ast.newInfixExpression();
				infix.setLeftOperand(getterExpression);
				infix.setOperator(ASTNodes.convertToInfixOperator(assignment.getOperator()));
				ITypeBinding infixType= infix.resolveTypeBinding();
				if (NecessaryParenthesesChecker.needsParenthesesForRightOperand(rightHandSide, infix, variableType)) {
					ParenthesizedExpression p= ast.newParenthesizedExpression();
					p.setExpression(copiedRightOp);
					copiedRightOp= p;
				}
				infix.setRightOperand(copiedRightOp);
				return createNarrowCastIfNessecary(infix, infixType, ast, variableType, is50OrHigher);
			}
		} else if (node.getNodeType() == ASTNode.POSTFIX_EXPRESSION) {
			PostfixExpression po= (PostfixExpression) node;
			if (po.getOperator() == PostfixExpression.Operator.INCREMENT)
				op= InfixExpression.Operator.PLUS;
			if (po.getOperator() == PostfixExpression.Operator.DECREMENT)
				op= InfixExpression.Operator.MINUS;
		} else if (node.getNodeType() == ASTNode.PREFIX_EXPRESSION) {
			PrefixExpression pe= (PrefixExpression) node;
			if (pe.getOperator() == PrefixExpression.Operator.INCREMENT)
				op= InfixExpression.Operator.PLUS;
			if (pe.getOperator() == PrefixExpression.Operator.DECREMENT)
				op= InfixExpression.Operator.MINUS;
		}
		if (op != null && getterExpression != null) {
			return createInfixInvocationFromPostPrefixExpression(op, getterExpression, ast, variableType, is50OrHigher);
		}
		return null;
	}

	/*
	 * Check if the node is in a block. We don't want to update declarations
	 */
	private static boolean isNotInBlock(ASTNode parent) {
		ASTNode statement= parent.getParent();
		boolean isStatement= statement.getNodeType() != ASTNode.EXPRESSION_STATEMENT;
		ASTNode block= statement.getParent();
		boolean isBlock= block.getNodeType() == ASTNode.BLOCK || block.getNodeType() == ASTNode.SWITCH_STATEMENT;
		boolean isControlStatemenBody= ASTNodes.isControlStatementBody(statement.getLocationInParent());
		return isStatement || (!isBlock && !isControlStatemenBody);
	}

	private static Expression createInfixInvocationFromPostPrefixExpression(InfixExpression.Operator operator, Expression getterExpression, AST ast, ITypeBinding variableType, boolean is50OrHigher) {
		InfixExpression infix= ast.newInfixExpression();
		infix.setLeftOperand(getterExpression);
		infix.setOperator(operator);
		NumberLiteral number= ast.newNumberLiteral();
		number.setToken("1"); //$NON-NLS-1$
		infix.setRightOperand(number);
		ITypeBinding infixType= infix.resolveTypeBinding();
		return createNarrowCastIfNessecary(infix, infixType, ast, variableType, is50OrHigher);
	}

	/**
	 * Checks if the assignment needs a downcast and inserts it if necessary
	 *
	 * @param expression the right hand-side
	 * @param expressionType the type of the right hand-side. Can be null
	 * @param ast the AST
	 * @param variableType the Type of the variable the expression will be assigned to
	 * @param is50OrHigher if <code>true</code> java 5.0 code will be assumed
	 * @return the casted expression if necessary
	 */
	private static Expression createNarrowCastIfNessecary(Expression expression, ITypeBinding expressionType, AST ast, ITypeBinding variableType, boolean is50OrHigher) {
		PrimitiveType castTo= null;
		if (variableType.isEqualTo(expressionType))
			return expression; //no cast for same type
		if (is50OrHigher) {
			if (ast.resolveWellKnownType("java.lang.Character").isEqualTo(variableType)) //$NON-NLS-1$
				castTo= ast.newPrimitiveType(PrimitiveType.CHAR);
			if (ast.resolveWellKnownType("java.lang.Byte").isEqualTo(variableType)) //$NON-NLS-1$
				castTo= ast.newPrimitiveType(PrimitiveType.BYTE);
			if (ast.resolveWellKnownType("java.lang.Short").isEqualTo(variableType)) //$NON-NLS-1$
				castTo= ast.newPrimitiveType(PrimitiveType.SHORT);
		}
		if (ast.resolveWellKnownType("char").isEqualTo(variableType)) //$NON-NLS-1$
			castTo= ast.newPrimitiveType(PrimitiveType.CHAR);
		if (ast.resolveWellKnownType("byte").isEqualTo(variableType)) //$NON-NLS-1$
			castTo= ast.newPrimitiveType(PrimitiveType.BYTE);
		if (ast.resolveWellKnownType("short").isEqualTo(variableType)) //$NON-NLS-1$
			castTo= ast.newPrimitiveType(PrimitiveType.SHORT);
		if (castTo != null) {
			CastExpression cast= ast.newCastExpression();
			if (NecessaryParenthesesChecker.needsParentheses(expression, cast, CastExpression.EXPRESSION_PROPERTY)) {
				ParenthesizedExpression parenthesized= ast.newParenthesizedExpression();
				parenthesized.setExpression(expression);
				cast.setExpression(parenthesized);
			} else
				cast.setExpression(expression);
			cast.setType(castTo);
			return cast;
		}
		return expression;
	}

}
