/*******************************************************************************
 * 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] Generator uses wrong suffixes and prefixes - https://bugs.eclipse.org/bugs/show_bug.cgi?id=275370
 *     Red Hat Inc. - moved to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.codemanipulation.tostringgeneration;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;

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

import org.eclipse.jdt.core.NamingConventions;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayAccess;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
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.ForStatement;
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.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.InfixExpression.Operator;
import org.eclipse.jdt.core.dom.InstanceofExpression;
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.Modifier;
import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.Type;
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.core.manipulation.CodeGeneration;

import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2Core;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;

/**
 * <p>
 * This generator creates an implementation of <code>{@link java.lang.Object#toString()}</code>
 * which lists all selected fields and methods. What exactly is listed and how members are separated
 * is determined by a format template.
 * <p>
 *
 * <p>
 * To print out items of arrays and/or limit number items printed for arrays, collections and so on,
 * various methods are used according to actual jdk compatibility:
 * <table border="10">
 * <tr>
 * <td></td>
 * <th><code>java.util.List</code></th>
 * <th><code>java.util.Collection</code></th>
 * <th><code>java.util.Map</code></th>
 * <th>Array of primitive types</th>
 * <th>Array of non-primitive types</th>
 * </tr>
 * <tr>
 * <th>jdk 1.4</th>
 * <td>-</td>
 * <td>-</td>
 * <td>-</td>
 * <td>helper method <code>arrayToString(Object array, int length)</code></td>
 * <td><code>Arrays.asList(member)</code></td>
 * </tr>
 * <tr>
 * <th>jdk 1.4/1.5, limit elements</th>
 * <td><code>member.subList()</code></td>
 * <td>helper method <code>toSting(Collection)</code></td>
 * <td>helper method <code>toString(Collection)</code> with <code>member.entrySet()</code></td>
 * <td>helper method <code>arrayToString(Object array, int length)</code></td>
 * <td><code>Arrays.asList(member).subList()</code></td>
 * </tr>
 * <tr>
 * <th>jdk 1.5</th>
 * <td>-</td>
 * <td>-</td>
 * <td>-</td>
 * <td><code>Arrays.toString()<code></td>
 * <td><code>Arrays.asList(member)</code></td>
 * </tr>
 * <tr>
 * <th>jdk 1.6</th>
 * <td>-</td>
 * <td>-</td>
 * <td>-</td>
 * <td><code>Arrays.toString()</code></td>
 * <td><code>Arrays.toString()</code></td>
 * </tr>
 * <tr>
 * <th>jdk 1.6, limit elements</th>
 * <td><code>member.subList()</code></td>
 * <td>helper method <code>toSting(Collection)</code></td>
 * <td>helper method <code>toString(Collection)</code> with <code>member.entrySet()</code></td>
 * <td><code>Arrays.toString(Arrays.copyOf(member, ...))</code></td>
 * <td><code>Arrays.asList(member).subList()</code></td>
 * </tr>
 * </table>
 * Additionally, if helper method is generated it is also used for other members members (even if
 * other solution could be used), as it makes the code cleaner.
 * </p>
 *
 * @since 3.5
 */
public abstract class AbstractToStringGenerator {
	protected static final String METHODNAME_TO_STRING= "toString"; //$NON-NLS-1$
	protected static final String TYPENAME_STRING= "String"; //$NON-NLS-1$

	private static final String HELPER_ARRAYTOSTRING_METHOD_NAME= "arrayToString"; //$NON-NLS-1$
	private static final String MAX_LEN_VARIABLE_NAME= "maxLen"; //$NON-NLS-1$

	protected String fMaxLenVariableName= MAX_LEN_VARIABLE_NAME;

	/**
	* The name of the property that every <code>MethodDeclaration</code> generated should have.
	* This property determines whether the method should be overwritten if already exists. The data
	* of this property is a Boolean.
	*/
	protected static final String OVERWRITE_METHOD_PROPERTY= "override_method"; //$NON-NLS-1$

	public ToStringTemplateParser getTemplateParser() {
		return new ToStringTemplateParser();
	}

	protected ToStringGenerationContext fContext;

	/** The ast to be used. Convenience accessor field */
	protected AST fAst;

	protected MethodDeclaration toStringMethod;

	protected boolean needMaxLenVariable;

	protected boolean needCollectionToStringMethod;

	protected List<ITypeBinding> typesThatNeedArrayToStringMethod;

	public RefactoringStatus checkConditions() {
		return new RefactoringStatus();
	}

	/**
	 * This method is an implementation of Director in Builder pattern. It goes through all elements
	 * of the format template and calls methods responsible for processing them.
	 *
	 * @return declaration of the generated <code>toString()</code> method
	 * @throws CoreException if creation failed
	 */
	public MethodDeclaration generateToStringMethod() throws CoreException {
		initialize();

		for (String element : fContext.getTemplateParser().getBeginning()) {
			addElement(processElement(element, null));
		}

		Object[] members= fContext.getSelectedMembers();
		for (int i= 0; i < members.length; i++) {
			if (!fContext.isSkipNulls() || getMemberType(members[i]).isPrimitive())
				addMember(members[i], i != members.length - 1);
			else
				addMemberCheckNull(members[i], i != members.length - 1);
		}

		for (String element : fContext.getTemplateParser().getEnding()) {
			addElement(processElement(element, null));
		}

		complete();

		return toStringMethod;
	}

	public List<MethodDeclaration> generateHelperMethods() {
		List<MethodDeclaration> result= new ArrayList<>();
		if (needCollectionToStringMethod)
			result.add(createHelperToStringMethod(false));

		if (!typesThatNeedArrayToStringMethod.isEmpty())
			result.add(createHelperToStringMethod(true));

		return result;
	}

	/**
	 * adds a comment (if necessary) and an <code>@Override</code> annotation to the generated
	 * method
	 *
	 * @throws CoreException if creation failed
	 */
	protected void createMethodComment() throws CoreException {
		ITypeBinding object= fAst.resolveWellKnownType("java.lang.Object"); //$NON-NLS-1$
		IMethodBinding objectMethod= null;
		for (IMethodBinding objm : object.getDeclaredMethods()) {
			if (METHODNAME_TO_STRING.equals(objm.getName()) && objm.getParameterTypes().length == 0) {
				objectMethod= objm;
			}
		}
		if (fContext.isCreateComments()) {
			String docString= CodeGeneration.getMethodComment(fContext.getCompilationUnit(), fContext.getTypeBinding().getQualifiedName(), toStringMethod, objectMethod, StubUtility
					.getLineDelimiterUsed(fContext.getCompilationUnit()));
			if (docString != null) {
				Javadoc javadoc= (Javadoc)fContext.getASTRewrite().createStringPlaceholder(docString, ASTNode.JAVADOC);
				toStringMethod.setJavadoc(javadoc);
			}
		}
		StubUtility2Core.addOverrideAnnotation(fContext.getCodeGenerationSettings(), fContext.getTypeBinding().getJavaElement().getJavaProject(), fContext.getASTRewrite(), fContext.getImportRewrite(), toStringMethod, objectMethod.getDeclaringClass().isInterface(), null);
	}

	/**
	 * Creates a method that takes a <code>Collection</code> or an Array and returns a
	 * <code>String</code> containing it's first <code>fSettings.limitValue</code> elements
	 *
	 * @param array if true, generated method will convert array to string, otherwise -
	 *            <code>Collection</code>
	 * @return <code>arrayToString(Object[] array)</code> or </code>collectionToString(Collection
	 *         collection)</code> method
	 */
	protected MethodDeclaration createHelperToStringMethod(boolean array) {
		final String iteratorName= createNameSuggestion("iterator", NamingConventions.VK_LOCAL); //$NON-NLS-1$
		final String appendMethodName= "append"; //$NON-NLS-1$
		final String indexName= createNameSuggestion("i", NamingConventions.VK_LOCAL); //$NON-NLS-1$
		final String lengthParamName= createNameSuggestion("len", NamingConventions.VK_PARAMETER); //$NON-NLS-1$
		final String maxLenParamName= createNameSuggestion(MAX_LEN_VARIABLE_NAME, NamingConventions.VK_PARAMETER);
		String paramName;
		String stringBuilderName;
		String stringBuilderTypeName;

		if (fContext.is50orHigher()) {
			stringBuilderTypeName= "java.lang.StringBuilder"; //$NON-NLS-1$
			stringBuilderName= createNameSuggestion("builder", NamingConventions.VK_LOCAL); //$NON-NLS-1$
		} else {
			stringBuilderTypeName= "java.lang.StringBuffer"; //$NON-NLS-1$
			stringBuilderName= createNameSuggestion("buffer", NamingConventions.VK_LOCAL); //$NON-NLS-1$
		}

		//private arrayToString() {
		MethodDeclaration arrayToStringMethod= fAst.newMethodDeclaration();
		arrayToStringMethod.setName(fAst.newSimpleName(array ? HELPER_ARRAYTOSTRING_METHOD_NAME : METHODNAME_TO_STRING));
		arrayToStringMethod.modifiers().add(fAst.newModifier(Modifier.ModifierKeyword.PRIVATE_KEYWORD));
		arrayToStringMethod.setReturnType2(fAst.newSimpleType(fAst.newName(TYPENAME_STRING)));

		if (array) {
			//(Objec array, int length, ...)
			paramName= createNameSuggestion("array", NamingConventions.VK_PARAMETER); //$NON-NLS-1$
			SingleVariableDeclaration param= fAst.newSingleVariableDeclaration();
			param.setType(fAst.newSimpleType(addImport("java.lang.Object"))); //$NON-NLS-1$
			param.setName(fAst.newSimpleName(paramName));
			arrayToStringMethod.parameters().add(param);

			param= fAst.newSingleVariableDeclaration();
			param.setType(fAst.newPrimitiveType(PrimitiveType.INT));
			param.setName(fAst.newSimpleName(lengthParamName));
			arrayToStringMethod.parameters().add(param);
		} else {
			//(Collection<?> collection, ...)
			paramName= createNameSuggestion("collection", NamingConventions.VK_PARAMETER); //$NON-NLS-1$
			SingleVariableDeclaration param= fAst.newSingleVariableDeclaration();
			Type collectionType= fAst.newSimpleType(addImport("java.util.Collection")); //$NON-NLS-1$
			if (fContext.is50orHigher()) {
				ParameterizedType genericType= fAst.newParameterizedType(collectionType);
				genericType.typeArguments().add(fAst.newWildcardType());
				param.setType(genericType);
			} else {
				param.setType(collectionType);
			}
			param.setName(fAst.newSimpleName(paramName));
			arrayToStringMethod.parameters().add(param);
		}
		if (fContext.isLimitItems()) {
			SingleVariableDeclaration param= fAst.newSingleVariableDeclaration();
			param.setType(fAst.newPrimitiveType(PrimitiveType.INT));
			param.setName(fAst.newSimpleName(maxLenParamName));
			arrayToStringMethod.parameters().add(param);
		}

		Block body= fAst.newBlock();
		arrayToStringMethod.setBody(body);

		//StringBuilder stringBuilder= new StringBuilder();
		VariableDeclarationFragment fragment= fAst.newVariableDeclarationFragment();
		fragment.setName(fAst.newSimpleName(stringBuilderName));
		ClassInstanceCreation classInstance= fAst.newClassInstanceCreation();
		classInstance.setType(fAst.newSimpleType(addImport(stringBuilderTypeName)));
		fragment.setInitializer(classInstance);
		VariableDeclarationStatement vStatement= fAst.newVariableDeclarationStatement(fragment);
		vStatement.setType(fAst.newSimpleType(addImport(stringBuilderTypeName)));
		body.statements().add(vStatement);

		if (array && fContext.isLimitItems()) {
			//length = Math.min(length, maxLen);
			MethodInvocation minInvocation= createMethodInvocation(addImport("java.lang.Math"), "min", fAst.newSimpleName(lengthParamName)); //$NON-NLS-1$ //$NON-NLS-2$
			minInvocation.arguments().add(fAst.newSimpleName(maxLenParamName));
			Assignment lengthAssignment= fAst.newAssignment();
			lengthAssignment.setLeftHandSide(fAst.newSimpleName(lengthParamName));
			lengthAssignment.setRightHandSide(minInvocation);
			body.statements().add(fAst.newExpressionStatement(lengthAssignment));
		}

		//stringBuilder.add("[");
		StringLiteral literal= fAst.newStringLiteral();
		literal.setLiteralValue("["); //$NON-NLS-1$
		body.statements().add(fAst.newExpressionStatement(createMethodInvocation(stringBuilderName, appendMethodName, literal)));

		//for(...
		ForStatement forStatement= fAst.newForStatement();
		Block forBlock= fAst.newBlock();
		forStatement.setBody(forBlock);

		//int i = 0;
		VariableDeclarationFragment indexDeclFragment= fAst.newVariableDeclarationFragment();
		indexDeclFragment.setName(fAst.newSimpleName(indexName));
		indexDeclFragment.setInitializer(fAst.newNumberLiteral("0")); //$NON-NLS-1$
		VariableDeclarationExpression indexDeclExpression= fAst.newVariableDeclarationExpression(indexDeclFragment);
		indexDeclExpression.setType(fAst.newPrimitiveType(PrimitiveType.INT));

		// i++
		PostfixExpression postfixExpr= fAst.newPostfixExpression();
		postfixExpr.setOperand(fAst.newSimpleName(indexName));
		postfixExpr.setOperator(org.eclipse.jdt.core.dom.PostfixExpression.Operator.INCREMENT);
		forStatement.updaters().add(postfixExpr);

		//if (i > 0) builder.append(", ");
		IfStatement ifStatement= fAst.newIfStatement();
		ifStatement.setExpression(createInfixExpression(fAst.newSimpleName(indexName), Operator.GREATER, fAst.newNumberLiteral(String.valueOf(0))));
		literal= fAst.newStringLiteral();
		literal.setLiteralValue(", "); //$NON-NLS-1$
		ifStatement.setThenStatement(createOneStatementBlock(createMethodInvocation(stringBuilderName, appendMethodName, literal)));
		forBlock.statements().add(ifStatement);

		if (array) {
			forStatement.initializers().add(indexDeclExpression);

			// i < length;
			forStatement.setExpression(createInfixExpression(fAst.newSimpleName(indexName), Operator.LESS, fAst.newSimpleName(lengthParamName)));

			for (ITypeBinding typeBinding : typesThatNeedArrayToStringMethod) {
				//if (array instanceof int[]) {
				String typeName= typeBinding.getName();
				PrimitiveType.Code code= null;
				if ("byte".equals(typeName)) code= PrimitiveType.BYTE; //$NON-NLS-1$
				if ("short".equals(typeName)) code= PrimitiveType.SHORT; //$NON-NLS-1$
				if ("char".equals(typeName)) code= PrimitiveType.CHAR; //$NON-NLS-1$
				if ("int".equals(typeName)) code= PrimitiveType.INT; //$NON-NLS-1$
				if ("long".equals(typeName)) code= PrimitiveType.LONG; //$NON-NLS-1$
				if ("float".equals(typeName)) code= PrimitiveType.FLOAT; //$NON-NLS-1$
				if ("double".equals(typeName)) code= PrimitiveType.DOUBLE; //$NON-NLS-1$
				if ("boolean".equals(typeName)) code= PrimitiveType.BOOLEAN; //$NON-NLS-1$
				if (code == null && !"Object".equals(typeName))continue; //$NON-NLS-1$
				InstanceofExpression instanceOf= fAst.newInstanceofExpression();
				instanceOf.setLeftOperand(fAst.newSimpleName(paramName));
				instanceOf.setRightOperand(fAst.newArrayType(code != null ? (Type)fAst.newPrimitiveType(code) : fAst.newSimpleType(addImport("java.lang.Object")))); //$NON-NLS-1$
				ifStatement= fAst.newIfStatement();
				ifStatement.setExpression(instanceOf);

				//builder.append(((int[]) array)[i]);
				CastExpression arrayCast= fAst.newCastExpression();
				arrayCast.setExpression(fAst.newSimpleName(paramName));
				arrayCast.setType(fAst.newArrayType(code != null ? (Type)fAst.newPrimitiveType(code) : fAst.newSimpleType(addImport("java.lang.Object")))); //$NON-NLS-1$
				ParenthesizedExpression parenthesizedCast= fAst.newParenthesizedExpression();
				parenthesizedCast.setExpression(arrayCast);
				ArrayAccess arrayAccess= fAst.newArrayAccess();
				arrayAccess.setArray(parenthesizedCast);
				arrayAccess.setIndex(fAst.newSimpleName(indexName));
				ifStatement.setThenStatement(createOneStatementBlock(createMethodInvocation(stringBuilderName, appendMethodName, arrayAccess)));

				forBlock.statements().add(ifStatement);
			}
		} else {
			body.statements().add(fAst.newExpressionStatement(indexDeclExpression));

			//... Iterator iterator= collection.iterator() ...
			fragment= fAst.newVariableDeclarationFragment();
			fragment.setName(fAst.newSimpleName(iteratorName));
			fragment.setInitializer(createMethodInvocation(paramName, "iterator", null)); //$NON-NLS-1$
			VariableDeclarationExpression vExpression= fAst.newVariableDeclarationExpression(fragment);
			SimpleType iteratorType= fAst.newSimpleType(addImport("java.util.Iterator")); //$NON-NLS-1$
			if (fContext.is50orHigher()) {
				ParameterizedType pType= fAst.newParameterizedType(iteratorType);
				pType.typeArguments().add(fAst.newWildcardType());
				vExpression.setType(pType);
			} else {
				vExpression.setType(iteratorType);
			}

			forStatement.initializers().add(vExpression);

			//iterator.hasNext() && i < maxLen;
			Expression indexExpression= createInfixExpression(fAst.newSimpleName(indexName), Operator.LESS, fAst.newSimpleName(maxLenParamName));
			forStatement.setExpression(createInfixExpression(createMethodInvocation(iteratorName, "hasNext", null), Operator.CONDITIONAL_AND, indexExpression)); //$NON-NLS-1$

			//if (i > 0)
			//stringBuilder.append(iterator.next());
			MethodInvocation nextInvocation= createMethodInvocation(iteratorName, "next", null); //$NON-NLS-1$
			forBlock.statements().add(fAst.newExpressionStatement(createMethodInvocation(stringBuilderName, appendMethodName, nextInvocation)));

		}

		body.statements().add(forStatement);

		//stringBuilder.add("]");
		literal= fAst.newStringLiteral();
		literal.setLiteralValue("]"); //$NON-NLS-1$
		body.statements().add(fAst.newExpressionStatement(createMethodInvocation(stringBuilderName, appendMethodName, literal)));

		//return stringBuilder.toString();
		ReturnStatement returnStatement= fAst.newReturnStatement();
		returnStatement.setExpression(createMethodInvocation(stringBuilderName, METHODNAME_TO_STRING, null));
		body.statements().add(returnStatement);

		arrayToStringMethod.setProperty(OVERWRITE_METHOD_PROPERTY, Boolean.valueOf(array));

		return arrayToStringMethod;
	}


	/**
	 * This method initializes all variables used in the process of generating <code>toString</code>
	 * method.
	 */
	protected void initialize() {
		needMaxLenVariable= false;
		needCollectionToStringMethod= false;
		typesThatNeedArrayToStringMethod= new ArrayList<>();

		checkNeedForHelperMethods();

		toStringMethod= fAst.newMethodDeclaration();
		toStringMethod.modifiers().addAll(ASTNodeFactory.newModifiers(fAst, Modifier.PUBLIC));
		toStringMethod.setName(fAst.newSimpleName(METHODNAME_TO_STRING));
		toStringMethod.setConstructor(false);
		toStringMethod.setReturnType2(fAst.newSimpleType(fAst.newName(TYPENAME_STRING)));

		Block body= fAst.newBlock();
		toStringMethod.setBody(body);

		fMaxLenVariableName= createNameSuggestion(MAX_LEN_VARIABLE_NAME, NamingConventions.VK_LOCAL);
	}

	/**
	 * This method is called at the end of the process of generating <code>toString</code> method.
	 * It should make sure the processed properly and clean the environment.
	 *
	 * @throws CoreException if creation failed
	 */
	protected void complete() throws CoreException {
		if (needMaxLenVariable) {
			toStringMethod.getBody().statements().add(0, createMaxLenDeclaration());
		}
		createMethodComment();
		toStringMethod.setProperty(OVERWRITE_METHOD_PROPERTY, Boolean.TRUE);
	}

	/**
	 * Iterates over selected members to determine whether helper methods will be needed.
	 */
	protected void checkNeedForHelperMethods() {
		if ((!fContext.isLimitItems() && !fContext.isCustomArray()) || (fContext.isLimitItems() && fContext.getLimitItemsValue() == 0))
			return;

		boolean isNonPrimitive= false;
		for (Object selectedMember : fContext.getSelectedMembers()) {
			ITypeBinding memberType= getMemberType(selectedMember);
			boolean[] implementsInterfaces= implementsInterfaces(memberType.getErasure(), new String[] { "java.util.Collection", "java.util.List", "java.util.Map" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			boolean isCollection= implementsInterfaces[0];
			boolean isList= implementsInterfaces[1];
			boolean isMap= implementsInterfaces[2];
			if (fContext.isLimitItems() && (isCollection || isMap) && !isList) {
				needCollectionToStringMethod= true;
			}
			if (fContext.isCustomArray() && memberType.isArray()) {
				ITypeBinding componentType= memberType.getComponentType();
				if (componentType.isPrimitive() && (!fContext.is50orHigher() || (!fContext.is60orHigher() && fContext.isLimitItems()))) {
					if (!typesThatNeedArrayToStringMethod.contains(componentType))
						typesThatNeedArrayToStringMethod.add(componentType);
				} else if (!componentType.isPrimitive())
					isNonPrimitive= true;
			}
		}
		if (!typesThatNeedArrayToStringMethod.isEmpty() && isNonPrimitive)
			typesThatNeedArrayToStringMethod.add(fAst.resolveWellKnownType("java.lang.Object")); //$NON-NLS-1$
	}

	/**
	 *
	 * @param templateElement the template element, see constants in {@link ToStringTemplateParser}
	 * @param member the member
	 * @return <code>String</code> or <code>Expression</code> switching
	 */
	protected Object processElement(String templateElement, Object member) {
		Object result= templateElement;
		if (ToStringTemplateParser.OBJECT_NAME_VARIABLE.equals(templateElement)) {
			result= fContext.getTypeBinding().getName();
		}
		if (ToStringTemplateParser.OBJECT_GET_NAME_VARIABLE.equals(templateElement)) {
			//this.getClass().getName()
			MethodInvocation getClassInvocation= fAst.newMethodInvocation();
			if (fContext.isKeywordThis())
				getClassInvocation.setExpression(fAst.newThisExpression());
			getClassInvocation.setName(fAst.newSimpleName("getClass")); //$NON-NLS-1$
			MethodInvocation getNameInvocation= fAst.newMethodInvocation();
			getNameInvocation.setExpression(getClassInvocation);
			getNameInvocation.setName(fAst.newSimpleName("getName")); //$NON-NLS-1$
			result= getNameInvocation;
		}
		if (ToStringTemplateParser.OBJECT_SUPER_TOSTRING_VARIABLE.equals(templateElement)) {
			//super.toString()
			SuperMethodInvocation superToStringInvocation= fAst.newSuperMethodInvocation();
			superToStringInvocation.setName(fAst.newSimpleName(METHODNAME_TO_STRING));
			result= superToStringInvocation;
		}
		if (ToStringTemplateParser.OBJECT_HASHCODE_VARIABLE.equals(templateElement)) {
			//this.hashCode()
			MethodInvocation hashCodeInvocation= fAst.newMethodInvocation();
			if (fContext.isKeywordThis())
				hashCodeInvocation.setExpression(fAst.newThisExpression());
			hashCodeInvocation.setName(fAst.newSimpleName("hashCode")); //$NON-NLS-1$
			result= hashCodeInvocation;
		}
		if (ToStringTemplateParser.OBJECT_SYSTEM_HASHCODE_VARIABLE.equals(templateElement)) {
			//system.identityHashCode(this)
			result= createMethodInvocation(addImport("java.lang.System"), "identityHashCode", fAst.newThisExpression()); //$NON-NLS-1$ //$NON-NLS-2$
		}
		if (ToStringTemplateParser.MEMBER_NAME_VARIABLE.equals(templateElement) || ToStringTemplateParser.MEMBER_NAME_PARENTHESIS_VARIABLE.equals(templateElement)) {
			result= getMemberName(member, templateElement);
		}
		if (ToStringTemplateParser.MEMBER_VALUE_VARIABLE.equals(templateElement)) {
			result= createMemberAccessExpression(member, false, fContext.isSkipNulls());
		}
		if (result instanceof StringLiteral)
			return ((StringLiteral)result).getLiteralValue();
		else
			return result;
	}

	/**
	 * Adds an element to the generated <code>toString</code> method. This method is called for
	 * every element of the format template.
	 *
	 * @param element String or expression to be added (<code>IVariableBinding</code> or
	 *            <code>IMethodBinding</code>
	 */
	protected abstract void addElement(Object element);

	/**
	 * Adds a member to the <code>toString</code> method. This method is called for every member if
	 * "Skip null values" options is unchecked.
	 *
	 * @param member a member to be added
	 * @param addSeparator true, if separator should be added after the member (i.e. this is not the
	 *            last member)
	 */
	protected void addMember(Object member, boolean addSeparator) {
		for (String element : fContext.getTemplateParser().getBody()) {
			addElement(processElement(element, member));
		}
		if (addSeparator)
			addElement(fContext.getTemplateParser().getSeparator());
	}

	/**
	 * Adds a code checking if member's value is not null and adding it to the generated string.
	 * This method is called for every non-primitive type member if "Skip null values" options is
	 * checked, or for every <code>Collection</code> and <code>Map</code> member if there's a limit
	 * for number of elements.
	 *
	 * @param member a member to be added
	 * @param addSeparator true, if separator should be added after the member (i.e. this is not the
	 *            last member)
	 */
	protected void addMemberCheckNull(Object member, boolean addSeparator) {
		addMember(member, addSeparator);
	}

	/**
	 * Creates an invocation of a method that takes zero or one argument
	 *
	 * @param expression the receiver expression
	 * @param methodName the method name
	 * @param argument the argument, can be <code>null</code> if the method does not take any arguments
	 * @return MethodInvocation in following form: <code>expression.methodName(argument)</code>
	 */
	protected MethodInvocation createMethodInvocation(Expression expression, String methodName, Expression argument) {
		MethodInvocation invocation= fAst.newMethodInvocation();
		invocation.setExpression(expression);
		invocation.setName(fAst.newSimpleName(methodName));
		if (argument != null)
			invocation.arguments().add(argument);
		return invocation;
	}

	/**
	 * Creates an invocation of a method that takes zero or one argument
	 *
	 * @param receiver the receiver name
	 * @param methodName the method name
	 * @param argument the argument, can be <code>null</code> if the method does not take any arguments
	 * @return MethodInvocation in following form: <code>expression.methodName(argument)</code>
	 */
	protected MethodInvocation createMethodInvocation(String receiver, String methodName, Expression argument) {
		return createMethodInvocation(fAst.newName(receiver), methodName, argument);
	}

	/**
	 * Creates a statement that can be used as for/while/if-then-else block
	 *
	 * @param expression an expression
	 * @return a single-line statement, or a block, depending on settings
	 */
	protected Statement createOneStatementBlock(Expression expression) {
		if (fContext.isForceBlocks()) {
			Block forBlock= fAst.newBlock();
			forBlock.statements().add(fAst.newExpressionStatement(expression));
			return forBlock;
		} else {
			return fAst.newExpressionStatement(expression);
		}
	}

	protected InfixExpression createInfixExpression(Expression leftOperand, Operator operator, Expression rightOperand) {
		InfixExpression expression= fAst.newInfixExpression();
		expression.setLeftOperand(leftOperand);
		expression.setOperator(operator);
		expression.setRightOperand(rightOperand);
		return expression;
	}

	/**
	 * @return a statement in form of <code>final int maxLen = 10;</code>
	 */
	protected VariableDeclarationStatement createMaxLenDeclaration() {
		VariableDeclarationFragment fragment= fAst.newVariableDeclarationFragment();
		fragment.setName(fAst.newSimpleName(fMaxLenVariableName));
		fragment.setInitializer(fAst.newNumberLiteral(String.valueOf(fContext.getLimitItemsValue())));
		VariableDeclarationStatement declExpression= fAst.newVariableDeclarationStatement(fragment);
		declExpression.setType(fAst.newPrimitiveType(PrimitiveType.INT));
		declExpression.modifiers().add(fAst.newModifier(ModifierKeyword.FINAL_KEYWORD));
		return declExpression;
	}

	/**
	 * @param member <code>IVariableBinding</code> or <code>IMethodBinding</code> representing a
	 *            member
	 * @param ignoreArraysCollections if false and <i>limit number of items</i> is set, this method
	 *            will use custom methods to print out Arrays, Collections and Sets
	 * @param ignoreNulls if false, method will add checking for nulls when using custom methods
	 * @return an expression that accesses given member
	 */
	protected Expression createMemberAccessExpression(Object member, boolean ignoreArraysCollections, boolean ignoreNulls) {
		if (!ignoreArraysCollections) {
			ITypeBinding memberType= getMemberType(member);

			boolean isArray= memberType.isArray();

			boolean[] implementsInterfaces= implementsInterfaces(memberType.getErasure(), new String[] { "java.util.Collection", "java.util.List", "java.util.Map" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			boolean isCollection= implementsInterfaces[0];
			boolean isList= implementsInterfaces[1];
			boolean isMap= implementsInterfaces[2];

			if (isCollection || isMap || (isArray && fContext.isCustomArray())) {
				Expression accessExpression= null;
				if (fContext.isLimitItems()) {
					if (fContext.getLimitItemsValue() == 0) {
						accessExpression= fAst.newStringLiteral();
						((StringLiteral)accessExpression).setLiteralValue("[]"); //$NON-NLS-1$
					} else {
						if (isList && !needCollectionToStringMethod) {
							//member.subList(0, Math.min(maxLen, member.size())
							MethodInvocation memberSizeInvocation= fAst.newMethodInvocation();
							memberSizeInvocation.setExpression(createMemberAccessExpression(member, true, true));
							memberSizeInvocation.setName(fAst.newSimpleName("size")); //$NON-NLS-1$

							accessExpression= createSubListInvocation(createMemberAccessExpression(member, true, true), memberSizeInvocation);
							needMaxLenVariable= true;
						} else if (isCollection || isMap) {
							//toString(member, maxLen)
							Expression memberAccess= createMemberAccessExpression(member, true, true);
							if (isMap) {
								//member.entrySet();
								MethodInvocation entrySetInvocation= fAst.newMethodInvocation();
								entrySetInvocation.setExpression(memberAccess);
								entrySetInvocation.setName(fAst.newSimpleName("entrySet")); //$NON-NLS-1$
								memberAccess= entrySetInvocation;
							}
							MethodInvocation toStringInvocation= fAst.newMethodInvocation();
							if (fContext.isKeywordThis())
								toStringInvocation.setExpression(fAst.newThisExpression());
							toStringInvocation.setName(fAst.newSimpleName(METHODNAME_TO_STRING));
							toStringInvocation.arguments().add(memberAccess);
							toStringInvocation.arguments().add(fAst.newSimpleName(fMaxLenVariableName));
							needMaxLenVariable= true;
							accessExpression= toStringInvocation;
						} else if (isArray) {
							FieldAccess lengthAccess= fAst.newFieldAccess();
							lengthAccess.setExpression(createMemberAccessExpression(member, true, true));
							lengthAccess.setName(fAst.newSimpleName("length")); //$NON-NLS-1$
							ITypeBinding arrayComponentType= memberType.getComponentType();
							if (!arrayComponentType.isPrimitive() && typesThatNeedArrayToStringMethod.isEmpty()) {
								//Arrays.asList(member).subList(0, Math.min(maxLen, member.length))
								MethodInvocation asListInvocation= createMethodInvocation(addImport("java.util.Arrays"), "asList", createMemberAccessExpression(member, true, true)); //$NON-NLS-1$ //$NON-NLS-2$
								accessExpression= createSubListInvocation(asListInvocation, lengthAccess);
							} else {
								if (fContext.is60orHigher()) {
									// Arrays.toString( Arrays.copyOf ( member, Math.min (maxLen, member.length) )
									Name arraysImport= addImport("java.util.Arrays"); //$NON-NLS-1$
									MethodInvocation minInvocation= createMethodInvocation(addImport("java.lang.Math"), "min", lengthAccess); //$NON-NLS-1$ //$NON-NLS-2$
									minInvocation.arguments().add(fAst.newSimpleName(fMaxLenVariableName));
									needMaxLenVariable= true;
									MethodInvocation copyOfInvocation= createMethodInvocation(arraysImport, "copyOf", createMemberAccessExpression(member, true, true)); //$NON-NLS-1$
									copyOfInvocation.arguments().add(minInvocation);
									Name arraysImportCopy= (Name)ASTNode.copySubtree(fAst, arraysImport);
									accessExpression= createMethodInvocation(arraysImportCopy, METHODNAME_TO_STRING, copyOfInvocation);
								} else {
									// arrayToString(member, member.length, maxLen)
									MethodInvocation arrayToStringInvocation= fAst.newMethodInvocation();
									if (fContext.isKeywordThis())
										arrayToStringInvocation.setExpression(fAst.newThisExpression());
									arrayToStringInvocation.setName(fAst.newSimpleName(HELPER_ARRAYTOSTRING_METHOD_NAME));
									arrayToStringInvocation.arguments().add(createMemberAccessExpression(member, true, true));
									arrayToStringInvocation.arguments().add(lengthAccess);
									arrayToStringInvocation.arguments().add(fAst.newSimpleName(fMaxLenVariableName));
									needMaxLenVariable= true;
									accessExpression= arrayToStringInvocation;
								}
							}
						}
					}
				} else {
					if (isArray && fContext.isCustomArray()) {
						if (fContext.is50orHigher()) {
							// Arrays.toString(member)
							return createMethodInvocation(addImport("java.util.Arrays"), METHODNAME_TO_STRING, createMemberAccessExpression(member, true, true)); //$NON-NLS-1$
						} else {
							ITypeBinding arrayComponentType= memberType.getComponentType();
							if (!arrayComponentType.isPrimitive() && typesThatNeedArrayToStringMethod.isEmpty()) {
								// Arrays.asList(member)
								accessExpression= createMethodInvocation(addImport("java.util.Arrays"), "asList", createMemberAccessExpression(member, true, true)); //$NON-NLS-1$ //$NON-NLS-2$
							} else {
								// arrayToString(member, member.length, maxLen)
								FieldAccess lengthAccess= fAst.newFieldAccess();
								lengthAccess.setExpression(createMemberAccessExpression(member, true, true));
								lengthAccess.setName(fAst.newSimpleName("length")); //$NON-NLS-1$
								MethodInvocation arrayToStringInvocation= fAst.newMethodInvocation();
								if (fContext.isKeywordThis())
									arrayToStringInvocation.setExpression(fAst.newThisExpression());
								arrayToStringInvocation.setName(fAst.newSimpleName(HELPER_ARRAYTOSTRING_METHOD_NAME));
								arrayToStringInvocation.arguments().add(createMemberAccessExpression(member, true, true));
								arrayToStringInvocation.arguments().add(lengthAccess);
								accessExpression= arrayToStringInvocation;
							}
						}
					}
				}
				if (accessExpression != null) {
					if (!ignoreNulls) {
						ConditionalExpression conditional= fAst.newConditionalExpression();
						conditional.setExpression(createInfixExpression(createMemberAccessExpression(member, true, true), Operator.NOT_EQUALS, fAst.newNullLiteral()));
						conditional.setThenExpression(accessExpression);
						conditional.setElseExpression(fAst.newNullLiteral());
						return conditional;
					}
					return accessExpression;
				}
			}
		}
		if (member instanceof IVariableBinding) {
			if (fContext.isKeywordThis()) {
				FieldAccess fa= fAst.newFieldAccess();
				fa.setExpression(fAst.newThisExpression());
				fa.setName(fAst.newSimpleName(((IVariableBinding)member).getName()));
				return fa;
			}
			return fAst.newSimpleName(((IVariableBinding)member).getName());
		}
		if (member instanceof IMethodBinding) {
			if (METHODNAME_TO_STRING.equals(((IMethodBinding)member).getName())) {
				SuperMethodInvocation invocation= fAst.newSuperMethodInvocation();
				invocation.setName(fAst.newSimpleName(((IMethodBinding)member).getName()));
				return invocation;
			}
			MethodInvocation invocation= fAst.newMethodInvocation();
			if (fContext.isKeywordThis()) {
				invocation.setExpression(fAst.newThisExpression());
			}
			invocation.setName(fAst.newSimpleName(((IMethodBinding)member).getName()));
			return invocation;
		}
		return null;
	}

	protected Expression createSubListInvocation(Expression memberAccess, Expression sizeAccess) {
		MethodInvocation subListInvocation= fAst.newMethodInvocation();
		subListInvocation.setExpression(memberAccess);
		subListInvocation.setName(fAst.newSimpleName("subList")); //$NON-NLS-1$
		subListInvocation.arguments().add(fAst.newNumberLiteral(String.valueOf(0)));

		MethodInvocation minInvocation= createMethodInvocation(addImport("java.lang.Math"), "min", sizeAccess); //$NON-NLS-1$ //$NON-NLS-2$
		minInvocation.arguments().add(fAst.newSimpleName(fMaxLenVariableName));
		subListInvocation.arguments().add(minInvocation);
		needMaxLenVariable= true;
		return subListInvocation;
	}

	/**
	 * Adds an import to the class. This method should be used for every class reference added to
	 * the generated code.
	 *
	 * @param typeName a fully qualified name of a type
	 * @return simple name of a class if the import was added and fully qualified name if there was
	 *         a conflict
	 */
	protected Name addImport(String typeName) {
		String importedName= fContext.getImportRewrite().addImport(typeName);
		return fAst.newName(importedName);
	}

	private Set<String> excluded;
	protected String createNameSuggestion(String baseName, int variableKind) {
		if (excluded == null) {
			excluded= new HashSet<>();
			for (IVariableBinding field : fContext.getTypeBinding().getDeclaredFields()) {
				excluded.add(field.getName());
			}
			ITypeBinding superType= fContext.getTypeBinding().getSuperclass();
			while (superType != null) {
				for (IVariableBinding field : superType.getDeclaredFields()) {
					if (!Modifier.isPrivate(field.getModifiers())) {
						excluded.add(field.getName());
					}
				}
				superType= superType.getSuperclass();
			}
			for (ITypeBinding type : fContext.getTypeBinding().getDeclaredTypes()) {
				excluded.add(type.getName());
			}
			superType= fContext.getTypeBinding().getSuperclass();
			while (superType != null) {
				for (ITypeBinding type : superType.getDeclaredTypes()) {
					if (!Modifier.isPrivate(type.getModifiers())) {
						excluded.add(type.getName());
					}
				}
				superType= superType.getSuperclass();
			}
		}
		return StubUtility.getVariableNameSuggestions(variableKind, fContext.getCompilationUnit().getJavaProject(), baseName, 0, excluded, true)[0];
	}

	/**
	 * Checks whether given type implements given interface
	 *
	 * @param memberType binding of the type to check
	 * @param interfaceNames fully qualified names of the interfaces to seek for
	 * @return array of booleans, every element is set to true if interface at the same position in
	 *         <code>interfaceNames</code> is implemented by <code>memberType</code>
	 */
	protected boolean[] implementsInterfaces(ITypeBinding memberType, String[] interfaceNames) {
		boolean[] result= new boolean[interfaceNames.length];
		for (int i= 0; i < interfaceNames.length; i++) {
			if (memberType.getQualifiedName().equals(interfaceNames[i]))
				result[i]= true;
		}
		for (ITypeBinding intf : memberType.getInterfaces()) {
			boolean[] deeper= implementsInterfaces(intf.getErasure(), interfaceNames);
			for (int j= 0; j < interfaceNames.length; j++) {
				result[j]= result[j] || deeper[j];
			}
		}
		return result;
	}

	/**
	 *
	 * @param member <code>IVariableBinding</code> or <code>IMethodBinding</code> representing a
	 *            member
	 * @param templateElement the template element
	 * @return the name of the member, with parenthesis at the end if the member is a method
	 */
	protected String getMemberName(Object member, String templateElement) {
		if (member instanceof IVariableBinding) {
			return ((IVariableBinding)member).getName();
		}
		if (member instanceof IMethodBinding) {
			String result= ((IMethodBinding)member).getName();
			if (ToStringTemplateParser.MEMBER_NAME_PARENTHESIS_VARIABLE.equals(templateElement))
				result+= "()"; //$NON-NLS-1$
			return result;
		}
		return null;
	}

	/**
	 *
	 * @param member member to check
	 * @return type of field or method's return type
	 */
	protected ITypeBinding getMemberType(Object member) {
		if (member instanceof IVariableBinding) {
			return ((IVariableBinding)member).getType();
		}
		if (member instanceof IMethodBinding) {
			return ((IMethodBinding)member).getReturnType();
		}
		return null;
	}

	public void setContext(ToStringGenerationContext context) {
		fContext= context;
		fAst= fContext.getAST();
		excluded= null;
	}

	public ToStringGenerationContext getContext() {
		return fContext;
	}

}
