/*******************************************************************************
 * Copyright (c) 2008, 2019 Mateusz Matela 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:
 *     Mateusz Matela <mateusz.matela@gmail.com> - [code manipulation] [dcr] toString() builder wizard - https://bugs.eclipse.org/bugs/show_bug.cgi?id=26070
 *     Mateusz Matela <mateusz.matela@gmail.com> - [toString] finish toString() builder wizard - https://bugs.eclipse.org/bugs/show_bug.cgi?id=267710
 *     Red Hat Inc. - moved to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.codemanipulation.tostringgeneration;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;

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

import org.eclipse.jdt.core.Flags;
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.dom.ASTNode;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression.Operator;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationMessages;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;


/**
 * <p>
 * Implementation of <code>AbstractToStringGenerator</code> that creates <code>toString()</code>
 * method using an external library. The library must supply a string builder, that is a class that
 * fulfills the following requirements:
 * <ul>
 * <li>Provides a constructor taking single Object</li>
 * <li>Provides methods for appending objects. There may be many such methods (with the same name,
 * for example <code>append(...)</code>), but there must be at least a version that takes single
 * Object or an Object and a String (in any order). These methods should return builder object,
 * otherwise generator will not be able to make call chains.</li>
 * <li>Provides a result method (usually <code>toString()</code>), that is a method that takes no
 * arguments and returns a String</li>
 * </ul>
 * </p>
 * <p>
 * Generated methods look like this:
 *
 * <pre>
 * public String toString() {
 * 	ExternalBuilder builder= new ExternalBuilder();
 * 	builder.append(&quot;field1&quot;, field1);
 * 	builder.append(&quot;field2&quot;, field2);
 * 	return builder.toString();
 * }
 * </pre>
 *
 * </p>
 *
 * @since 3.5
 */
public class CustomBuilderGenerator extends AbstractToStringGenerator {

	private final List<String> primitiveTypes= Arrays.asList("byte", "short", "char", "int", "long", "float", "double", "boolean"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$

	private final String[] wrapperTypes= new String[] { "java.lang.Byte", "java.lang.Short", "java.lang.Character", "java.lang.Integer", "java.lang.Long", "java.lang.Float", "java.lang.Double", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
			"java.lang.Boolean" }; //$NON-NLS-1$

	/**
	 * true, if the last expression created with {@link #createAppendMethodForMember(Object)}
	 * returns builder type and therefore can be used to chain calls
	 **/
	private boolean canChainLastAppendCall;

	/**
	 * Class for storing information about versions of append method in the builder class that can
	 * be used for different member types
	 */
	private static class AppendMethodInformation {
		/**
		 * Type of method with respect to taken parameter types. Possible values:
		 * <ol>
		 * <li>method takes one type parameter</li>
		 * <li>method takes one type parameter and one string</li>
		 * <li>method takes one string and one type parameter</li>
		 * </ol>
		 */
		public int methodType;

		/**
		 * true if method returns the builder class object so it can be used to form chains of calls
		 **/
		public boolean returnsBuilder;
	}

	/**
	 * Information about versions of append method in the builder type
	 *
	 * key: String - fully qualified name of a member type
	 *
	 * value: {@link AppendMethodInformation} - information about corresponding method
	 */
	private HashMap<String, AppendMethodInformation> appendMethodSpecificTypes= new HashMap<>();

	@Override
	public RefactoringStatus checkConditions() {
		RefactoringStatus status= super.checkConditions();
		if (fContext.isCustomArray() || fContext.isLimitItems())
			status.addWarning(CodeGenerationMessages.GenerateToStringOperation_warning_no_arrays_collections_with_this_style);
		return status;
	}

	@Override
	protected void addElement(Object element) {
	}

	@Override
	protected void initialize() {
		super.initialize();

		fillAppendMethodsMap();

		tidyAppendsMethodsMap();
	}

	@Override
	public MethodDeclaration generateToStringMethod() throws CoreException {
		initialize();

		//ToStringBuilder builder= new ToStringBuilder(this);
		String builderVariableName= createNameSuggestion(getContext().getCustomBuilderVariableName(), NamingConventions.VK_LOCAL);
		VariableDeclarationFragment fragment= fAst.newVariableDeclarationFragment();
		fragment.setName(fAst.newSimpleName(builderVariableName));
		ClassInstanceCreation classInstance= fAst.newClassInstanceCreation();
		Name typeName= addImport(getContext().getCustomBuilderClass());
		classInstance.setType(fAst.newSimpleType(typeName));
		classInstance.arguments().add(fAst.newThisExpression());
		fragment.setInitializer(classInstance);
		VariableDeclarationStatement vStatement= fAst.newVariableDeclarationStatement(fragment);
		vStatement.setType(fAst.newSimpleType((Name)ASTNode.copySubtree(fAst, typeName)));
		toStringMethod.getBody().statements().add(vStatement);

		/* expression for accumulating chained calls */
		Expression expression= null;

		for (int i= 0; i < getContext().getSelectedMembers().length; i++) {
			//builder.append("member", member);
			MethodInvocation appendInvocation= createAppendMethodForMember(getContext().getSelectedMembers()[i]);
			if (getContext().isSkipNulls() && !getMemberType(getContext().getSelectedMembers()[i]).isPrimitive()) {
				if (expression != null) {
					toStringMethod.getBody().statements().add(fAst.newExpressionStatement(expression));
					expression= null;
				}
				appendInvocation.setExpression(fAst.newSimpleName(builderVariableName));
				IfStatement ifStatement= fAst.newIfStatement();
				ifStatement.setExpression(createInfixExpression(createMemberAccessExpression(getContext().getSelectedMembers()[i], true, true), Operator.NOT_EQUALS, fAst.newNullLiteral()));
				ifStatement.setThenStatement(createOneStatementBlock(appendInvocation));
				toStringMethod.getBody().statements().add(ifStatement);
			} else {
				if (expression != null) {
					appendInvocation.setExpression(expression);
				} else {
					appendInvocation.setExpression(fAst.newSimpleName(builderVariableName));
				}
				if (getContext().isCustomBuilderChainedCalls() && canChainLastAppendCall) {
					expression= appendInvocation;
				} else {
					expression= null;
					toStringMethod.getBody().statements().add(fAst.newExpressionStatement(appendInvocation));
				}
			}
		}

		if (expression != null) {
			toStringMethod.getBody().statements().add(fAst.newExpressionStatement(expression));
		}
		// return builder.toString();
		ReturnStatement rStatement= fAst.newReturnStatement();
		rStatement.setExpression(createMethodInvocation(builderVariableName, getContext().getCustomBuilderResultMethod(), null));
		toStringMethod.getBody().statements().add(rStatement);

		complete();

		return toStringMethod;
	}

	/**
	 * Searches through methods with proper name and for each argument type remembers the best
	 * option
	 */
	private void fillAppendMethodsMap() {
		try {
			IJavaProject javaProject= getContext().getTypeBinding().getJavaElement().getJavaProject();
			IType type= javaProject.findType(getContext().getCustomBuilderClass());
			for (IType type2 : type.newSupertypeHierarchy(null).getAllClasses()) {
				for (IMethod method : type2.getMethods()) {
					if (!Flags.isPublic(method.getFlags()) || !method.getElementName().equals(getContext().getCustomBuilderAppendMethod()))
						continue;
					String[] parameterTypes= method.getParameterTypes();
					AppendMethodInformation appendMethodInformation= new AppendMethodInformation();
					String specyficType;
					switch (parameterTypes.length) {
					case 1:
						specyficType= JavaModelUtil.getResolvedTypeName(parameterTypes[0], type2);
						appendMethodInformation.methodType= 1;
						break;
					case 2:
						String resolvedParameterTypeName1= JavaModelUtil.getResolvedTypeName(parameterTypes[0], type2);
						String resolvedParameterTypeName2= JavaModelUtil.getResolvedTypeName(parameterTypes[1], type2);
						if ("java.lang.String".equals(resolvedParameterTypeName1)) {//$NON-NLS-1$
							specyficType= resolvedParameterTypeName2;
							appendMethodInformation.methodType= 3;
						} else if ("java.lang.String".equals(resolvedParameterTypeName2)) {//$NON-NLS-1$
							specyficType= resolvedParameterTypeName1;
							appendMethodInformation.methodType= 2;
						} else
							continue;
						break;
					default:
						continue;
					}

					String returnTypeName= JavaModelUtil.getResolvedTypeName(method.getReturnType(), type2);
					IType returnType= javaProject.findType(returnTypeName);
					appendMethodInformation.returnsBuilder= (returnType != null) && returnType.newSupertypeHierarchy(null).contains(type);

					AppendMethodInformation oldAMI= appendMethodSpecificTypes.get(specyficType);
					if (oldAMI == null || oldAMI.methodType < appendMethodInformation.methodType) {
						appendMethodSpecificTypes.put(specyficType, appendMethodInformation);
					}
				}
			}
		} catch (JavaModelException e) {
			throw new RuntimeException("couldn't initialize custom toString() builder generator", e); //$NON-NLS-1$
		}
	}

	/**
	 * Removes information about types from {@link #appendMethodSpecificTypes} if their
	 * parametersType is worse than for java.lang.Object.
	 */
	private void tidyAppendsMethodsMap() {
		int objectParametersType= appendMethodSpecificTypes.get("java.lang.Object").methodType; //$NON-NLS-1$
		Set<Map.Entry<String, AppendMethodInformation>> entrySet= appendMethodSpecificTypes.entrySet();
		for (Iterator<Map.Entry<String, AppendMethodInformation>> iterator= entrySet.iterator(); iterator.hasNext();) {
			Map.Entry<String, AppendMethodInformation> entry= iterator.next();
			if (entry.getValue().methodType < objectParametersType) {
				iterator.remove();
			}
		}
	}

	private MethodInvocation createAppendMethodForMember(Object member) {
		ITypeBinding memberType= getMemberType(member);
		String memberTypeName= memberType.getQualifiedName();

		Expression memberAccessExpression= null;

		AppendMethodInformation ami= appendMethodSpecificTypes.get(memberTypeName);
		if (ami == null && memberType.isPrimitive()) {
			memberTypeName= wrapperTypes[primitiveTypes.indexOf(memberTypeName)];
			memberType= fAst.resolveWellKnownType(memberTypeName);
			ami= appendMethodSpecificTypes.get(memberTypeName);
			if (!getContext().is50orHigher()) {
				ClassInstanceCreation classInstance= fAst.newClassInstanceCreation();
				classInstance.setType(fAst.newSimpleType(addImport(memberTypeName)));
				classInstance.arguments().add(createMemberAccessExpression(member, true, true));
				memberAccessExpression= classInstance;
			}
		}
		while (ami == null) {
			memberType= memberType.getSuperclass();
			if (memberType != null)
				memberTypeName= memberType.getQualifiedName();
			else
				memberTypeName= "java.lang.Object"; //$NON-NLS-1$
			ami= appendMethodSpecificTypes.get(memberTypeName);
		}

		if (memberAccessExpression == null) {
			memberAccessExpression= createMemberAccessExpression(member, false, getContext().isSkipNulls());
		}

		MethodInvocation appendInvocation= fAst.newMethodInvocation();
		appendInvocation.setName(fAst.newSimpleName(getContext().getCustomBuilderAppendMethod()));
		if (ami.methodType == 1 || ami.methodType == 2) {
			appendInvocation.arguments().add(memberAccessExpression);
		}
		if (ami.methodType == 2 || ami.methodType == 3) {
			StringLiteral literal= fAst.newStringLiteral();
			literal.setLiteralValue(getMemberName(member, ToStringTemplateParser.MEMBER_NAME_PARENTHESIS_VARIABLE));
			appendInvocation.arguments().add(literal);
		}
		if (ami.methodType == 3) {
			appendInvocation.arguments().add(memberAccessExpression);
		}

		canChainLastAppendCall= ami.returnsBuilder;

		return appendInvocation;
	}
}
