/*******************************************************************************
 * Copyright (c) 2020 Fabrice TIERCELIN 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:
 *     Fabrice TIERCELIN - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.fix;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.CreationReference;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionMethodReference;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.SuperMethodReference;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeMethodReference;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.fix.CleanUpConstants;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;

import org.eclipse.jdt.ui.cleanup.CleanUpRequirements;
import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
import org.eclipse.jdt.ui.text.java.IProblemLocation;

/**
 * A fix that simplifies the lambda expression and the method reference syntax:
 * <ul>
 * <li>Parenthesis are not needed for a single untyped parameter,</li>
 * <li>Return statement is not needed for a single expression,</li>
 * <li>Brackets are not needed for a single statement,</li>
 * <li>A lambda expression can be replaced by a creation or a method reference in some cases.</li>
 * </ul>
 */
public class LambdaExpressionAndMethodRefCleanUp extends AbstractMultiFix {
	public LambdaExpressionAndMethodRefCleanUp() {
		this(Collections.emptyMap());
	}

	public LambdaExpressionAndMethodRefCleanUp(Map<String, String> options) {
		super(options);
	}

	@Override
	public CleanUpRequirements getRequirements() {
		boolean requireAST= isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF);
		Map<String, String> requiredOptions= null;
		return new CleanUpRequirements(requireAST, false, false, requiredOptions);
	}

	@Override
	public String[] getStepDescriptions() {
		if (isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF)) {
			return new String[] { MultiFixMessages.LambdaExpressionAndMethodRefCleanUp_description };
		}
		return new String[0];
	}

	@SuppressWarnings("nls")
	@Override
	public String getPreview() {
		StringBuilder bld= new StringBuilder();
		bld.append("\n");

		if (isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF)) {
			bld.append("someString -> someString.trim().toLowerCase();\n");
			bld.append("someString -> someString.trim().toLowerCase();\n");
			bld.append("someString -> (someString.trim().toLowerCase() + \"bar\");\n");
			bld.append("ArrayList::new;\n");
			bld.append("Date::getTime;\n");
		} else {
			bld.append("(someString) -> someString.trim().toLowerCase();\n");
			bld.append("someString -> {return someString.trim().toLowerCase();};\n");
			bld.append("someString -> {return someString.trim().toLowerCase() + \"bar\";};\n");
			bld.append("() -> new ArrayList<>();\n");
			bld.append("date -> date.getTime();\n");
		}

		return bld.toString();
	}

	@Override
	protected ICleanUpFix createFix(CompilationUnit unit) throws CoreException {
		if (!isEnabled(CleanUpConstants.SIMPLIFY_LAMBDA_EXPRESSION_AND_METHOD_REF) || !JavaModelUtil.is18OrHigher(unit.getJavaElement().getJavaProject())) {
			return null;
		}

		final List<CompilationUnitRewriteOperation> rewriteOperations= new ArrayList<>();

		unit.accept(new ASTVisitor() {
			@Override
			public boolean visit(final LambdaExpression node) {
				if (node.hasParentheses() && node.parameters().size() == 1
						&& node.parameters().get(0) instanceof VariableDeclarationFragment) {
					rewriteOperations.add(new RemoveParamParenthesesOperation(node));
					return false;
				} else if (node.getBody() instanceof Block) {
					List<Statement> statements= ((Block) node.getBody()).statements();

					if (statements.size() == 1 && statements.get(0) instanceof ReturnStatement) {
						rewriteOperations.add(new RemoveReturnAndBracketsOperation(node, statements));
						return false;
					}
				} else if (node.getBody() instanceof ClassInstanceCreation) {
					ClassInstanceCreation ci= (ClassInstanceCreation) node.getBody();

					List<Expression> arguments= ci.arguments();
					if (node.parameters().size() == arguments.size() && areSameIdentifiers(node, arguments)) {
						rewriteOperations.add(new ReplaceByCreationReferenceOperation(node, ci));
						return false;
					}
				} else if (node.getBody() instanceof SuperMethodInvocation) {
					SuperMethodInvocation smi= (SuperMethodInvocation) node.getBody();
					List<Expression> arguments= smi.arguments();

					if (node.parameters().size() == arguments.size() && areSameIdentifiers(node, arguments)) {
						rewriteOperations.add(new ReplaceBySuperMethodReferenceOperation(node, smi));
						return false;
					}
				} else if (node.getBody() instanceof MethodInvocation) {
					MethodInvocation methodInvocation= (MethodInvocation) node.getBody();
					Expression calledExpression= methodInvocation.getExpression();
					List<Expression> arguments= methodInvocation.arguments();

					if (node.parameters().size() == arguments.size()) {
						if (!areSameIdentifiers(node, arguments)) {
							return true;
						}

						IMethodBinding methodBinding= methodInvocation.resolveMethodBinding();

						if (Boolean.TRUE.equals(ASTNodes.isStatic(methodInvocation))) {
							if (methodBinding == null || methodBinding.getDeclaringClass() == null) {
								return true;
							}

							ITypeBinding calledType= methodBinding.getDeclaringClass();

							if (!arguments.isEmpty()) {
								String[] remainingParams= new String[arguments.size() - 1];

								for (int i= 0; i < arguments.size() - 1; i++) {
									ITypeBinding resolveTypeBinding= arguments.get(i + 1).resolveTypeBinding();

									if (resolveTypeBinding == null) {
										return true;
									}

									remainingParams[i]= resolveTypeBinding.getQualifiedName();
								}

								for (IMethodBinding declaredMethodBinding : calledType.getDeclaredMethods()) {
									if ((declaredMethodBinding.getModifiers() & Modifier.STATIC) == 0 && ASTNodes.usesGivenSignature(declaredMethodBinding,
											calledType.getQualifiedName(), methodInvocation.getName().getIdentifier(), remainingParams)) {
										return true;
									}
								}
							}

							rewriteOperations.add(new ReplaceByTypeReferenceOperation(node, methodInvocation, calledType));
							return false;
						}

						if (calledExpression instanceof StringLiteral || calledExpression instanceof NumberLiteral
								|| calledExpression instanceof ThisExpression) {
							rewriteOperations.add(new ReplaceByMethodReferenceOperation(node, methodInvocation));
							return false;
						} else if (calledExpression instanceof FieldAccess) {
							FieldAccess fieldAccess= (FieldAccess) calledExpression;

							if (fieldAccess.resolveFieldBinding() != null && fieldAccess.resolveFieldBinding().isEffectivelyFinal()) {
								rewriteOperations.add(new ReplaceByMethodReferenceOperation(node, methodInvocation));
								return false;
							}
						} else if (calledExpression instanceof SuperFieldAccess) {
							SuperFieldAccess fieldAccess= (SuperFieldAccess) calledExpression;

							if (fieldAccess.resolveFieldBinding() != null && fieldAccess.resolveFieldBinding().isEffectivelyFinal()) {
								rewriteOperations.add(new ReplaceByMethodReferenceOperation(node, methodInvocation));
								return false;
							}
						}
					} else if (calledExpression instanceof SimpleName && node.parameters().size() == arguments.size() + 1) {
						SimpleName calledObject= (SimpleName) calledExpression;

						if (isSameIdentifier(node, 0, calledObject)) {
							for (int i= 0; i < arguments.size(); i++) {
								ASTNode expression= ASTNodes.getUnparenthesedExpression(arguments.get(i));

								if (!(expression instanceof SimpleName) || !isSameIdentifier(node, i + 1, (SimpleName) expression)) {
									return true;
								}
							}

							ITypeBinding clazz= null;
							if (calledExpression.resolveTypeBinding() != null) {
								clazz= calledExpression.resolveTypeBinding();
							} else if (methodInvocation.resolveMethodBinding() != null && methodInvocation.resolveMethodBinding().getDeclaringClass() != null) {
								clazz= methodInvocation.resolveMethodBinding().getDeclaringClass();
							} else {
								return true;
							}

							String[] cumulativeParams= new String[arguments.size() + 1];
							cumulativeParams[0]= clazz.getQualifiedName();

							for (int i= 0; i < arguments.size(); i++) {
								ITypeBinding resolveTypeBinding= arguments.get(i).resolveTypeBinding();

								if (resolveTypeBinding == null) {
									return true;
								}

								cumulativeParams[i + 1]= resolveTypeBinding.getQualifiedName();
							}

							for (IMethodBinding declaredMethodBinding : clazz.getDeclaredMethods()) {
								if ((declaredMethodBinding.getModifiers() & Modifier.STATIC) > 0 && ASTNodes.usesGivenSignature(declaredMethodBinding,
										clazz.getQualifiedName(), methodInvocation.getName().getIdentifier(), cumulativeParams)) {
									return true;
								}
							}

							rewriteOperations.add(new ReplaceByTypeReferenceOperation(node, methodInvocation, clazz));
							return false;
						}
					}
				}

				return true;
			}

			/**
			 * In order to make parameters implicit, those ones should be the same in the same
			 * order.
			 *
			 * @param node the lambda expression
			 * @param arguments The arguments in the expression
			 * @return true if the parameters are obvious
			 */
			private boolean areSameIdentifiers(LambdaExpression node, List<Expression> arguments) {
				for (int i= 0; i < node.parameters().size(); i++) {
					Expression expression= ASTNodes.getUnparenthesedExpression(arguments.get(i));

					if (!(expression instanceof SimpleName) || !isSameIdentifier(node, i, (SimpleName) expression)) {
						return false;
					}
				}

				return true;
			}

			private boolean isSameIdentifier(final LambdaExpression node, final int i, final SimpleName argument) {
				Object param0= node.parameters().get(i);

				if (param0 instanceof VariableDeclarationFragment) {
					VariableDeclarationFragment vdf= (VariableDeclarationFragment) param0;
					return vdf.getName().getIdentifier().equals(argument.getIdentifier());
				}

				return false;
			}
		});

		if (rewriteOperations.isEmpty()) {
			return null;
		}

		return new CompilationUnitRewriteOperationsFix(MultiFixMessages.LambdaExpressionAndMethodRefCleanUp_description, unit,
				rewriteOperations.toArray(new CompilationUnitRewriteOperation[rewriteOperations.size()]));
	}

	@Override
	public boolean canFix(ICompilationUnit compilationUnit, IProblemLocation problem) {
		return false;
	}

	@Override
	protected ICleanUpFix createFix(CompilationUnit unit, IProblemLocation[] problems) throws CoreException {
		return null;
	}

	private static class RemoveParamParenthesesOperation extends CompilationUnitRewriteOperation {
		private final LambdaExpression node;

		public RemoveParamParenthesesOperation(final LambdaExpression node) {
			this.node= node;
		}

		@Override
		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
			ASTRewrite rewrite= cuRewrite.getASTRewrite();
			AST ast= cuRewrite.getRoot().getAST();

			LambdaExpression copyOfLambdaExpression= ast.newLambdaExpression();
			ASTNode copyOfParameter= rewrite.createCopyTarget((ASTNode) node.parameters().get(0));
			copyOfLambdaExpression.parameters().add(copyOfParameter);
			copyOfLambdaExpression.setBody(rewrite.createCopyTarget(node.getBody()));
			copyOfLambdaExpression.setParentheses(false);
			rewrite.replace(node, copyOfLambdaExpression, null);
		}
	}

	private static class RemoveReturnAndBracketsOperation extends CompilationUnitRewriteOperation {
		private final LambdaExpression node;

		private final List<Statement> statements;

		public RemoveReturnAndBracketsOperation(final LambdaExpression node, final List<Statement> statements) {
			this.node= node;
			this.statements= statements;
		}

		@Override
		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
			ASTRewrite rewrite= cuRewrite.getASTRewrite();
			AST ast= cuRewrite.getRoot().getAST();

			ReturnStatement returnStatement= (ReturnStatement) statements.get(0);
			Expression copyOfExpression= (Expression) rewrite.createCopyTarget(returnStatement.getExpression());
			rewrite.replace(node.getBody(), ASTNodeFactory.parenthesizeIfNeeded(ast, copyOfExpression), null);
		}
	}

	private static class ReplaceByCreationReferenceOperation extends CompilationUnitRewriteOperation {
		private final LambdaExpression node;

		private final ClassInstanceCreation classInstanceCreation;

		public ReplaceByCreationReferenceOperation(final LambdaExpression node, final ClassInstanceCreation classInstanceCreation) {
			this.node= node;
			this.classInstanceCreation= classInstanceCreation;
		}

		@Override
		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
			ASTRewrite rewrite= cuRewrite.getASTRewrite();
			AST ast= cuRewrite.getRoot().getAST();

			CreationReference creationRef= ast.newCreationReference();
			creationRef.setType(copyType(cuRewrite, ast, classInstanceCreation, classInstanceCreation.resolveTypeBinding()));
			rewrite.replace(node, creationRef, null);
		}
	}

	private static class ReplaceBySuperMethodReferenceOperation extends CompilationUnitRewriteOperation {
		private final LambdaExpression node;

		private final SuperMethodInvocation superMethodInvocation;

		public ReplaceBySuperMethodReferenceOperation(final LambdaExpression node, final SuperMethodInvocation superMethodInvocation) {
			this.node= node;
			this.superMethodInvocation= superMethodInvocation;
		}

		@Override
		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
			ASTRewrite rewrite= cuRewrite.getASTRewrite();
			AST ast= cuRewrite.getRoot().getAST();

			SuperMethodReference creationRef= ast.newSuperMethodReference();
			creationRef.setName((SimpleName) rewrite.createCopyTarget(superMethodInvocation.getName()));
			rewrite.replace(node, creationRef, null);
		}
	}

	private static class ReplaceByTypeReferenceOperation extends CompilationUnitRewriteOperation {
		private final LambdaExpression node;
		private final MethodInvocation methodInvocation;

		private final ITypeBinding type;

		public ReplaceByTypeReferenceOperation(LambdaExpression node, MethodInvocation methodInvocation, ITypeBinding type) {
			this.node= node;
			this.methodInvocation= methodInvocation;
			this.type= type;
		}

		@Override
		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
			ASTRewrite rewrite= cuRewrite.getASTRewrite();
			AST ast= cuRewrite.getRoot().getAST();

			TypeMethodReference typeMethodRef= ast.newTypeMethodReference();

			typeMethodRef.setType(copyType(cuRewrite, ast, methodInvocation, type));
			typeMethodRef.setName((SimpleName) rewrite.createCopyTarget(methodInvocation.getName()));
			rewrite.replace(node, typeMethodRef, null);
		}
	}

	private static class ReplaceByMethodReferenceOperation extends CompilationUnitRewriteOperation {
		private final LambdaExpression node;

		private final MethodInvocation methodInvocation;

		public ReplaceByMethodReferenceOperation(LambdaExpression node, MethodInvocation methodInvocation) {
			this.node= node;
			this.methodInvocation= methodInvocation;
		}

		@Override
		public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException {
			ASTRewrite rewrite= cuRewrite.getASTRewrite();
			AST ast= cuRewrite.getRoot().getAST();
			ExpressionMethodReference typeMethodRef= ast.newExpressionMethodReference();

			if (methodInvocation.getExpression() != null) {
				typeMethodRef.setExpression((Expression) rewrite.createCopyTarget(methodInvocation.getExpression()));
			} else {
				typeMethodRef.setExpression(ast.newThisExpression());
			}

			typeMethodRef.setName((SimpleName) rewrite.createCopyTarget(methodInvocation.getName()));
			rewrite.replace(node, typeMethodRef, null);
		}
	}

	private static Type copyType(final CompilationUnitRewrite cuRewrite, final AST ast, final ASTNode node, final ITypeBinding typeBinding) {
		ImportRewrite importRewrite= cuRewrite.getImportRewrite();
		ImportRewriteContext importContext= new ContextSensitiveImportRewriteContext(node, importRewrite);
		ITypeBinding modifiedType;

		if (typeBinding.getTypeParameters().length == 0) {
			modifiedType= typeBinding.getErasure();
		} else {
			modifiedType= typeBinding;
		}

		return ASTNodeFactory.newCreationType(ast, modifiedType, importRewrite, importContext);
	}
}