/*******************************************************************************
 * Copyright (c) 2010, 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.Iterator;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;

import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;

import org.eclipse.text.edits.TextEdit;

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

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;

import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationMessages;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2Core;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;


/**
 * <p>
 * A workspace runnable to add implementation for <code>{@link java.lang.Object#toString()}</code>
 * </p>
 *
 * @since 3.5
 */
public class GenerateToStringOperation implements IWorkspaceRunnable {

	/** The insertion point, or <code>null</code> */
	private IJavaElement fInsert;

	private CompilationUnitRewrite fRewrite;

	private ToStringGenerationContext fContext;

	private AbstractToStringGenerator fGenerator;

	private CompilationUnit fUnit;

	private boolean fApply= false;

	private boolean fSave= false;

	private TextEdit fEdit= null;

	private GenerateToStringOperation(IJavaElement insert, ToStringGenerationContext context, AbstractToStringGenerator generator, CompilationUnit unit, CompilationUnitRewrite rewrite, boolean apply, boolean save) {
		fInsert= insert;
		fContext= context;
		fRewrite= rewrite;
		fUnit= unit;
		fGenerator= generator;
		fApply= apply;
		fSave= save;
	}

	@Override
	public void run(IProgressMonitor monitor) throws CoreException {
		if (monitor == null)
			monitor= new NullProgressMonitor();
		try {
			monitor.beginTask("", 1); //$NON-NLS-1$
			monitor.setTaskName(CodeGenerationMessages.GenerateToStringOperation_description);


			AbstractTypeDeclaration declaration= (AbstractTypeDeclaration)ASTNodes.findDeclaration(fContext.getTypeBinding(), fRewrite.getRoot());
			ListRewrite rewriter= fRewrite.getASTRewrite().getListRewrite(declaration, declaration.getBodyDeclarationsProperty());
			if (fContext.getTypeBinding() != null && rewriter != null) {

				MethodDeclaration toStringMethod= fGenerator.generateToStringMethod();

				List<BodyDeclaration> list= declaration.bodyDeclarations();
				BodyDeclaration replace= findMethodToReplace(list, toStringMethod);
				if (replace == null || ((Boolean)toStringMethod.getProperty(AbstractToStringGenerator.OVERWRITE_METHOD_PROPERTY)).booleanValue())
					insertMethod(toStringMethod, rewriter, replace);

				for (MethodDeclaration method : fGenerator.generateHelperMethods()) {
					replace= findMethodToReplace(list, method);
					if (replace == null || ((Boolean)method.getProperty(AbstractToStringGenerator.OVERWRITE_METHOD_PROPERTY)).booleanValue()) {
						insertMethod(method, rewriter, replace);
					}
				}

				fEdit= fRewrite.createChange(true).getEdit();
				if (fApply) {
					JavaModelUtil.applyEdit((ICompilationUnit)fUnit.getJavaElement(), fEdit, fSave, monitor);
				}
			}

		} finally {
			monitor.done();
		}
	}

	/**
	 * Returns the resulting text edit.
	 *
	 * @return the resulting edit
	 */
	public final TextEdit getResultingEdit() {
		return fEdit;
	}

	/**
	 * @return RefactoringStatus with eventual errors and warnings
	 */
	public RefactoringStatus checkConditions() {
		return fGenerator.checkConditions();
	}



	protected void insertMethod(MethodDeclaration method, ListRewrite rewriter, BodyDeclaration replace) throws JavaModelException {
		if (replace != null) {
			rewriter.replace(replace, method, null);
		} else {
			ASTNode insertion= StubUtility2Core.getNodeToInsertBefore(rewriter, fInsert);
			if (insertion != null)
				rewriter.insertBefore(method, insertion, null);
			else
				rewriter.insertLast(method, null);
		}
	}

	/**
	 * Determines if given method exists in a given list
	 *
	 * @param list list of method to search through
	 * @param method method to find
	 * @return declaration of method from the list that has the same name and parameter types, or
	 *         null if not found
	 */
	protected BodyDeclaration findMethodToReplace(final List<BodyDeclaration> list, MethodDeclaration method) {
		for (BodyDeclaration bodyDecl : list) {
			if (bodyDecl instanceof MethodDeclaration) {
				final MethodDeclaration method2= (MethodDeclaration)bodyDecl;
				if (method2.getName().getIdentifier().equals(method.getName().getIdentifier()) && method2.parameters().size() == method.parameters().size()) {
					Iterator<SingleVariableDeclaration> iterator1= method.parameters().iterator();
					Iterator<SingleVariableDeclaration> iterator2= method2.parameters().iterator();
					boolean ok= true;
					while (iterator1.hasNext()) {
						if (!iterator1.next().getType().subtreeMatch(new ASTMatcher(), iterator2.next().getType())) {
							ok= false;
							break;
						}
					}
					if (ok)
						return method2;
				}
			}
		}
		return null;
	}

	public ISchedulingRule getSchedulingRule() {
		return ResourcesPlugin.getWorkspace().getRoot();
	}

	public static final int STRING_CONCATENATION= 0;

	public static final int STRING_BUILDER= 1;

	public static final int STRING_BUILDER_CHAINED= 2;

	public static final int STRING_FORMAT= 3;

	public static final int CUSTOM_BUILDER= 4;

	private final static String[] hardcoded_styles= {
			CodeGenerationMessages.GenerateToStringOperation_stringConcatenation_style_name,
			CodeGenerationMessages.GenerateToStringOperation_stringBuilder_style_name,
			CodeGenerationMessages.GenerateToStringOperation_StringBuilder_chained_style_name,
			CodeGenerationMessages.GenerateToStringOperation_string_format_style_name,
			CodeGenerationMessages.GenerateToStringOperation_customStringBuilder_style_name
			};

	/**
	 * @return Array containing names of implemented code styles
	 */
	public static String[] getStyleNames() {
		return hardcoded_styles;
	}

	/**
	 *
	 * @param toStringStyle id number of the code style (its position in the array returned by
	 *            {@link #getStyleNames()}
	 * @return a toString() generator implementing given code style
	 */
	private static AbstractToStringGenerator createToStringGenerator(int toStringStyle) {
		switch (toStringStyle) {
			case STRING_CONCATENATION:
				return new StringConcatenationGenerator();
			case STRING_BUILDER:
				return new StringBuilderGenerator();
			case STRING_BUILDER_CHAINED:
				return new StringBuilderChainGenerator();
			case STRING_FORMAT:
				return new StringFormatGenerator();
			case CUSTOM_BUILDER:
				return new CustomBuilderGenerator();
			default:
				throw new IllegalArgumentException("Undefined toString() code style: " + toStringStyle); //$NON-NLS-1$
		}
	}

	/**
	 * @param toStringStyle id number of the style (its position in the array returned by
	 *            {@link #getStyleNames()}
	 * @return a template parser that should be used with given code style
	 */
	public static ToStringTemplateParser createTemplateParser(int toStringStyle) {
		return new ToStringTemplateParser();
	}

	/**
	 * Creates new <code>GenerateToStringOperation</code>, using <code>settings.toStringStyle</code>
	 * field to choose the right subclass.
	 *
	 * @param typeBinding binding for the type for which the toString() method will be created
	 * @param selectedBindings bindings for the typetype's members to be used in created method
	 * @param unit a compilation unit containing the type
	 * @param elementPosition at this position in the compilation unit created method will be added
	 * @param settings the settings for toString() generator
	 * @param apply <code>true</code> if the resulting edit should be applied, <code>false</code> otherwise
	 * @param save <code>true</code> if the changed compilation unit should be saved, <code>false</code> otherwise
	 * @return a ready to use <code>GenerateToStringOperation</code> object
	 */
	public static GenerateToStringOperation createOperation(ITypeBinding typeBinding, Object[] selectedBindings, CompilationUnit unit, IJavaElement elementPosition,
			ToStringGenerationSettingsCore settings, boolean apply, boolean save) {
		AbstractToStringGenerator generator= createToStringGenerator(settings.toStringStyle);
		ToStringTemplateParser parser= createTemplateParser(settings.toStringStyle);
		parser.parseTemplate(settings.stringFormatTemplate);
		CompilationUnitRewrite rewrite= new CompilationUnitRewrite((ICompilationUnit)unit.getTypeRoot(), unit);
		ToStringGenerationContext context= new ToStringGenerationContext(parser, selectedBindings, settings, typeBinding, rewrite);
		generator.setContext(context);
		return new GenerateToStringOperation(elementPosition, context, generator, unit, rewrite, apply, save);
	}


}
