/*******************************************************************************
 * Copyright (c) 2019, 2020 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
 *******************************************************************************/

package org.eclipse.jdt.internal.ui.text.correction.proposals;

import java.util.ArrayList;
import java.util.List;

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.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Assignment;
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.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
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.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;

import org.eclipse.jdt.internal.core.manipulation.StubUtility;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.text.java.IProblemLocation;

import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.text.correction.CorrectionMessages;
import org.eclipse.jdt.internal.ui.util.ASTHelper;

public class InitializeFinalFieldProposal extends LinkedCorrectionProposal {
	private IProblemLocation fProblem;

	private ASTNode fAstNode;

	private final IVariableBinding fVariableBinding;

	private int fupdateType;

	public static final int UPDATE_AT_DECLARATION= 0;

	public static final int UPDATE_AT_CONSTRUCTOR= 1;

	public static final int UPDATE_CONSTRUCTOR_NEW_PARAMETER= 2;

	public InitializeFinalFieldProposal(IProblemLocation problem, ICompilationUnit cu, ASTNode astNode, IVariableBinding variableBinding, int relevance) {
		super(Messages.format(CorrectionMessages.InitializeFieldAtDeclarationCorrectionProposal_description, problem.getProblemArguments()[0]), cu, null, relevance, null);

		fProblem= problem;
		fAstNode= astNode;
		fVariableBinding= variableBinding;
		fupdateType= UPDATE_AT_DECLARATION;
		setImage(JavaPluginImages.get(JavaPluginImages.IMG_FIELD_PRIVATE));
	}

	public InitializeFinalFieldProposal(IProblemLocation problem, ICompilationUnit cu, ASTNode astNode, int relevance, int updateType) {
		super(Messages.format(CorrectionMessages.InitializeFieldInConstructorCorrectionProposal_description, problem.getProblemArguments()[0]), cu, null, relevance, null);
		if (updateType == UPDATE_CONSTRUCTOR_NEW_PARAMETER) {
			setDisplayName(Messages.format(CorrectionMessages.InitializeFieldWithConstructorParameterCorrectionProposal_description, problem.getProblemArguments()[0]));
		}

		fProblem= problem;
		fAstNode= astNode;
		fVariableBinding= null;
		fupdateType= updateType;
		setImage(JavaPluginImages.get(JavaPluginImages.IMG_FIELD_PRIVATE));
	}

	public boolean hasProposal() throws CoreException {
		return getRewrite() != null;
	}

	@Override
	protected ASTRewrite getRewrite() throws CoreException {
		switch (fupdateType) {
			case UPDATE_AT_CONSTRUCTOR:
				if (fAstNode != null
						&& fAstNode.getParent() instanceof RecordDeclaration
						&& ASTHelper.isRecordDeclarationNodeSupportedInAST(fAstNode.getAST())) {
					return doInitRecordComponentsInConstructor();
				}
				return doInitFieldInConstructor();
			case UPDATE_AT_DECLARATION:
				return doInitField();
			case UPDATE_CONSTRUCTOR_NEW_PARAMETER:
				return doUpdateConstructorWithParameter();
			default:
				break;
		}
		return null;
	}

	private ASTRewrite doInitField() {
		FieldDeclaration field= getFieldDeclaration(fVariableBinding.getName());
		if (field == null) {
			return null;
		}
		AST ast= fAstNode.getAST();

		ITypeBinding type= fVariableBinding.getType();
		String variableTypeName= type.getName();

		VariableDeclarationFragment newFragment= ast.newVariableDeclarationFragment();
		newFragment.setName(ast.newSimpleName(fVariableBinding.getName()));

		FieldDeclaration declaration= ast.newFieldDeclaration(newFragment);
		declaration.modifiers().addAll(ast.newModifiers(fVariableBinding.getModifiers()));

		Expression expression= null;
		if (type.isPrimitive()) {
			declaration.setType(ast.newPrimitiveType(PrimitiveType.toCode(variableTypeName)));
			expression= ast.newNumberLiteral();
		} else if ("String".equals(variableTypeName)) { //$NON-NLS-1$
			declaration.setType(ast.newSimpleType(ast.newName(variableTypeName)));
			expression= ast.newStringLiteral();
		} else {
			Type newType= null;
			if (type.isParameterizedType()) {
				newType= createParameterizedType(ast, type);
			} else {
				newType= ast.newSimpleType(ast.newName(variableTypeName));
			}
			declaration.setType(newType);

			if (hasDefaultConstructor()) {
				ClassInstanceCreation cic= ast.newClassInstanceCreation();
				cic.setType((Type) ASTNode.copySubtree(ast, newType));
				expression= cic;
			} else {
				expression= ast.newNullLiteral();
			}
		}
		newFragment.setInitializer(expression);

		ASTRewrite rewrite= ASTRewrite.create(ast);
		rewrite.replace(field, declaration, null);
		setEndPosition(rewrite.track(expression)); // set cursor after expression statement
		return rewrite;
	}

	private Type createParameterizedType(AST ast, ITypeBinding typeBinding) {
		if (typeBinding.isParameterizedType() && !typeBinding.isRawType()) {
			Type baseType= ast.newSimpleType(ASTNodeFactory.newName(ast, typeBinding.getErasure().getName()));
			ParameterizedType newType= ast.newParameterizedType(baseType);
			for (int i= 0; i < typeBinding.getTypeArguments().length; i++) {
				ITypeBinding typeArg= typeBinding.getTypeArguments()[i];
				Type argType= createParameterizedType(ast, typeArg); // recursive call
				newType.typeArguments().add(argType);
			}
			return newType;
		} else {
			if (!typeBinding.isTypeVariable()) {
				if (typeBinding.isWildcardType()) {
					ImportRewrite importRewrite= createImportRewrite((CompilationUnit) fAstNode.getRoot());
					return importRewrite.addImport(typeBinding, ast);
				}
				return ast.newSimpleType(ASTNodeFactory.newName(ast, typeBinding.getErasure().getName()));
			}
			return ast.newSimpleType(ast.newSimpleName(typeBinding.getName()));
		}
	}

	private ASTRewrite doInitRecordComponentsInConstructor() {
		String variableName= fProblem.getProblemArguments()[0];
		SingleVariableDeclaration svd= getRecordComponentDeclaration(variableName);
		if (svd == null) {
			return null;
		}
		AST ast= svd.getAST();

		FieldAccess fieldAccess= ast.newFieldAccess();
		fieldAccess.setName(ast.newSimpleName(variableName));
		fieldAccess.setExpression(ast.newThisExpression());

		Assignment assignment= ast.newAssignment();
		assignment.setLeftHandSide(fieldAccess);
		assignment.setOperator(Assignment.Operator.ASSIGN);

		Type fieldType= svd.getType();
		if (fieldType.isPrimitiveType()) {
			assignment.setRightHandSide(ast.newNumberLiteral());
		} else if ("String".equals(fieldType.toString())) { //$NON-NLS-1$
			assignment.setRightHandSide(ast.newStringLiteral());
		} else {
			if (hasDefaultConstructor(fieldType.resolveBinding())) {
				ClassInstanceCreation cic= ast.newClassInstanceCreation();
				cic.setType(ast.newSimpleType(ast.newName(fieldType.toString())));
				assignment.setRightHandSide(cic);
			} else {
				assignment.setRightHandSide(ast.newNullLiteral());
			}
		}
		ExpressionStatement statement= ast.newExpressionStatement(assignment);

		ASTRewrite rewrite= ASTRewrite.create(ast);
		// find all constructors (methods with same name as the type name)
		ConstructorVisitor cv= new ConstructorVisitor(((RecordDeclaration) svd.getParent()).getName().toString());
		fAstNode.getRoot().accept(cv);

		for (MethodDeclaration md : cv.getNodes()) {
			if (!isCanonical(md)) {
				continue;
			}
			Block body= md.getBody();
			rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, 0, null);
			setEndPosition(rewrite.track(assignment)); // set cursor after expression statement
		}
		return rewrite;
	}

	private boolean isCanonical(MethodDeclaration md) {
		IMethodBinding methodBinding= md.resolveBinding();
		return ((methodBinding == null) ? false : methodBinding.isCanonicalConstructor());
	}

	private ASTRewrite doUpdateConstructorWithParameter() {
		String variableName= fProblem.getProblemArguments()[0];
		FieldDeclaration field= getFieldDeclaration(variableName);
		if (field == null) {
			return null;
		}
		Type fieldType= field.getType();
		ITypeBinding fieldBinding= fieldType.resolveBinding();
		if (fieldBinding == null) {
			return null;
		}
		// find all constructors (methods with same name as the type name)
		ConstructorVisitor cv= new ConstructorVisitor(((AbstractTypeDeclaration) field.getParent()).getName().toString());
		fAstNode.getRoot().accept(cv);
		if (cv.getNodes().size() != 1) { // we only handle the simple case of one constructor
			return null;
		}
		MethodDeclaration methodDeclaration= cv.getNodes().get(0); // only one constructor
		IMethodBinding methodBinding= methodDeclaration.resolveBinding();
		if (methodBinding == null) {
			return null;
		}
		AST ast= field.getAST();
		ASTRewrite rewrite= ASTRewrite.create(ast);

		SingleVariableDeclaration newSingleVariableDeclaration= ast.newSingleVariableDeclaration();

		String[] excludedNames= collectParameterNames(methodDeclaration);
		String newName= StubUtility.suggestArgumentName(getCompilationUnit().getJavaProject(), variableName, excludedNames);

		newSingleVariableDeclaration.setName(ast.newSimpleName(newName));
		Type copyType= ASTNodes.copySubtree(ast, fieldType);
		newSingleVariableDeclaration.setType(copyType);

		FieldAccess fieldAccess= ast.newFieldAccess();
		fieldAccess.setName(ast.newSimpleName(variableName));
		fieldAccess.setExpression(ast.newThisExpression());

		Assignment assignment= ast.newAssignment();
		assignment.setLeftHandSide(fieldAccess);
		assignment.setOperator(Assignment.Operator.ASSIGN);
		assignment.setRightHandSide(ast.newSimpleName(newName));
		ExpressionStatement statement= ast.newExpressionStatement(assignment);

		rewrite.getListRewrite(methodDeclaration, MethodDeclaration.PARAMETERS_PROPERTY).insertLast(newSingleVariableDeclaration, null);
		Block body= methodDeclaration.getBody();
		// check if we call this(), then we can't add initialization here
		if (!hasThisCall(body)) {
			List<ASTNode> decls= ((AbstractTypeDeclaration) methodDeclaration.getParent()).bodyDeclarations();
			List<String> finalFieldList= getFinalFieldList(decls);
			int insertIndex= 0;
			if (finalFieldList.size() > 1) {
				int findFirstFinalFieldReferenceIndex= findFirstFinalFieldReferenceIndex(body.statements(), variableName);
				int findFinalFieldInsertIndex= findFinalFieldAssignmentInsertIndex(body.statements(), variableName, finalFieldList);
				int index= findFirstFinalFieldReferenceIndex == -1 ? findFinalFieldInsertIndex : findFirstFinalFieldReferenceIndex;
				insertIndex= Math.min(body.statements().size(), Math.min(findFinalFieldInsertIndex, index));
			}
			rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, insertIndex, null);
			setEndPosition(rewrite.track(assignment)); // set cursor after expression statement
		}
		return rewrite;
	}

	private String[] collectParameterNames(MethodDeclaration methodDeclaration) {
		final List<String> names= new ArrayList<>();

		for (Object element : methodDeclaration.parameters()) {
			SingleVariableDeclaration svd= (SingleVariableDeclaration) element;
			names.add(svd.getName().getIdentifier());
		}
		ASTVisitor v = new ASTVisitor() {
			@Override
			public boolean visit(VariableDeclarationFragment node) {
				names.add(node.getName().getIdentifier());
				return super.visit(node);
			}
		};
		methodDeclaration.accept(v);
		return names.toArray(new String[names.size()]);
	}

	private ASTRewrite doInitFieldInConstructor() {
		String variableName= fProblem.getProblemArguments()[0];
		FieldDeclaration field= getFieldDeclaration(variableName);
		if (field == null) {
			return null;
		}
		AST ast= field.getAST();

		FieldAccess fieldAccess= ast.newFieldAccess();
		fieldAccess.setName(ast.newSimpleName(variableName));
		fieldAccess.setExpression(ast.newThisExpression());

		Assignment assignment= ast.newAssignment();
		assignment.setLeftHandSide(fieldAccess);
		assignment.setOperator(Assignment.Operator.ASSIGN);

		Type fieldType= field.getType();
		if (fieldType.isPrimitiveType()) {
			assignment.setRightHandSide(ast.newNumberLiteral());
		} else if ("String".equals(fieldType.toString())) { //$NON-NLS-1$
			assignment.setRightHandSide(ast.newStringLiteral());
		} else {
			if (hasDefaultConstructor(fieldType.resolveBinding())) {
				ClassInstanceCreation cic= ast.newClassInstanceCreation();
				cic.setType(ast.newSimpleType(ast.newName(fieldType.toString())));
				assignment.setRightHandSide(cic);
			} else {
				assignment.setRightHandSide(ast.newNullLiteral());
			}
		}
		ExpressionStatement statement= ast.newExpressionStatement(assignment);

		ASTRewrite rewrite= ASTRewrite.create(ast);
		// find all constructors (methods with same name as the type name)
		ConstructorVisitor cv= new ConstructorVisitor(((AbstractTypeDeclaration) field.getParent()).getName().toString());
		fAstNode.getRoot().accept(cv);

		for (MethodDeclaration md : cv.getNodes()) {
			Block body= md.getBody();
			// check if we call this(), then we can't add initialization here
			if (!hasThisCall(body)) {
				if (hasFieldInitialization(body, variableName)) {
					continue;
				}
				List<ASTNode> decls= ((AbstractTypeDeclaration) md.getParent()).bodyDeclarations();
				List<String> finalFieldList= getFinalFieldList(decls);
				int insertIndex= 0;
				if (finalFieldList.size() > 1) {
					int findFirstFinalFieldReferenceIndex= findFirstFinalFieldReferenceIndex(body.statements(), variableName);
					int findFinalFieldInsertIndex= findFinalFieldAssignmentInsertIndex(body.statements(), variableName, finalFieldList);
					int index= findFirstFinalFieldReferenceIndex == -1 ? findFinalFieldInsertIndex : findFirstFinalFieldReferenceIndex;
					insertIndex= Math.min(body.statements().size(), Math.min(findFinalFieldInsertIndex, index));
				}
				rewrite.getListRewrite(body, Block.STATEMENTS_PROPERTY).insertAt(statement, insertIndex, null);
				setEndPosition(rewrite.track(assignment)); // set cursor after expression statement
			}
		}
		return rewrite;
	}

	private boolean hasFieldInitialization(Block body, final String variableName) {
		boolean[] hasInit= new boolean[1];
		ASTVisitor v= new ASTVisitor() {
			@Override
			public boolean visit(SimpleName node) {
				if (!node.getIdentifier().equals(variableName)) {
					return true;
				}
				Assignment assignNode= ASTNodes.getFirstAncestorOrNull(node, Assignment.class);
				if (assignNode != null) {
					Expression lhs= assignNode.getLeftHandSide();
					IBinding resolveBinding= node.resolveBinding();
					if (resolveBinding != null && ((IVariableBinding) resolveBinding).isField()) {
						int nodeType= lhs.getNodeType();
						if (nodeType == ASTNode.SIMPLE_NAME) {
							String name= ((SimpleName) lhs).getIdentifier();
							if (variableName.equals(name)) {
								hasInit[0]= true;
								return false;
							}
						} else if (nodeType == ASTNode.FIELD_ACCESS) {
							String name= ((FieldAccess) lhs).getName().getIdentifier();
							if (variableName.equals(name)) {
								hasInit[0]= true;
								return false;
							}
						}
					}
				}
				return true;
			}
		};
		body.accept(v);
		return hasInit[0];
	}

	private List<String> getFinalFieldList(List<ASTNode> fieldDeclarations) {
		List<String> list= new ArrayList<>();
		for (ASTNode astNode : fieldDeclarations) {
			if (astNode instanceof FieldDeclaration) {
				int modifiers= ((FieldDeclaration) astNode).getModifiers();
				if (!Modifier.isFinal(modifiers)) {
					continue;
				}
				for (Object object : ((FieldDeclaration) astNode).fragments()) {
					list.add(((VariableDeclarationFragment) object).getName().getIdentifier());
				}
			}
		}
		return list;
	}

	/*
	 * Find insertion index in statements based on declaration order.
	 */
	private int findFinalFieldAssignmentInsertIndex(List<ASTNode> astNodes, String variableName, List<String> finalFieldList) {
		int index= 0;
		int fieldIndex= 0;
		int findFinalFieldDeclarationIndex= finalFieldList.indexOf(variableName);
		String[] fieldAccess= new String[1];

		for (ASTNode astNode : astNodes) {
			fieldAccess[0]= null;

			ASTVisitor v= new ASTVisitor() {
				@Override
				public boolean visit(FieldAccess node) {
					fieldAccess[0]= node.getName().getIdentifier();
					return false;
				}

				@Override
				public boolean visit(SimpleName node) {
					Assignment assignNode= ASTNodes.getFirstAncestorOrNull(node, Assignment.class);
					if (assignNode != null) {
						IBinding resolveBinding= node.resolveBinding();
						if (resolveBinding instanceof IVariableBinding
								&& ((IVariableBinding) resolveBinding).isField()
								&& resolveBinding.getKind() == IBinding.VARIABLE
								&& finalFieldList.contains(node.getIdentifier())) {
							fieldAccess[0]= node.getIdentifier();
							return false;
						}
					}
					return true;
				}
			};
			astNode.accept(v);
			if (fieldAccess[0] == null) {
				index++;
				continue;
			}
			String fieldId= fieldAccess[0];
			int indexOfField= finalFieldList.indexOf(fieldId);
			fieldIndex= index;
			if (indexOfField > findFinalFieldDeclarationIndex) {
				return fieldIndex;
			}
			fieldIndex++;
			index++;
		}
		return fieldIndex;
	}

	private int findFirstFinalFieldReferenceIndex(List<ASTNode> astNodes, String variableName) {
		int index= 0;
		String[] fieldAccess= new String[1];

		for (ASTNode astNode : astNodes) {
			fieldAccess[0]= null;

			ASTVisitor v= new ASTVisitor() {
				@Override
				public boolean visit(FieldAccess node) {
					fieldAccess[0]= node.getName().getIdentifier();
					return false;
				}

				@Override
				public boolean visit(SimpleName node) {
					IBinding resolveBinding= node.resolveBinding();
					if (resolveBinding != null && resolveBinding.getKind() == IBinding.VARIABLE) {
						if (((IVariableBinding) resolveBinding).isField()) {
							fieldAccess[0]= node.getIdentifier();
							return false;
						}
					}
					return true;
				}
			};
			astNode.accept(v);
			if (fieldAccess[0] != null && fieldAccess[0].equals(variableName)) {
				return index;
			}
			index++;
		}
		return -1;
	}

	private boolean hasThisCall(Block body) {
		for (Object object : body.statements()) {
			if (((ASTNode) object).getNodeType() == ASTNode.CONSTRUCTOR_INVOCATION) {
				return true;
			}
		}
		return false;
	}

	private boolean hasDefaultConstructor(ITypeBinding type) {
		for (IMethodBinding mb : type.getDeclaredMethods()) {
			String key= mb.getKey();
			if (key.contains(";.()V")) { //$NON-NLS-1$
				return true;
			}
		}
		return false;
	}

	private boolean hasDefaultConstructor() {
		return hasDefaultConstructor(fVariableBinding.getType());
	}

	private FieldDeclaration getFieldDeclaration(String name) {
		FieldVisitor fieldVisitor= new FieldVisitor();
		fAstNode.getRoot().accept(fieldVisitor);

		for (FieldDeclaration f : fieldVisitor.getNodes()) {
			for (Object object : f.fragments()) {
				VariableDeclarationFragment fragment= (VariableDeclarationFragment) object;
				if (fragment.getName().getIdentifier().equals(name)) {
					return f;
				}
			}
		}
		return null;
	}

	private SingleVariableDeclaration getRecordComponentDeclaration(String name) {
		RecordComponentVisitor recordComponentVisitor= new RecordComponentVisitor();
		fAstNode.getRoot().accept(recordComponentVisitor);

		for (SingleVariableDeclaration f : recordComponentVisitor.getNodes()) {
			if (f.getName().getIdentifier().equals(name)) {
				return f;
			}
		}
		return null;
	}

	private static final class FieldVisitor extends ASTVisitor {
		private final List<FieldDeclaration> fNodes= new ArrayList<>();

		public FieldVisitor() {
		}

		@Override
		public boolean visit(FieldDeclaration node) {
			fNodes.add(node);
			return true;
		}

		public List<FieldDeclaration> getNodes() {
			return fNodes;
		}
	}

	private static final class ConstructorVisitor extends ASTVisitor {
		private final List<MethodDeclaration> fNodes= new ArrayList<>();

		private String fTypeName;

		public ConstructorVisitor(String typeName) {
			fTypeName= typeName;
		}

		@Override
		public boolean visit(MethodDeclaration node) {
			if (node.getName().toString().equals(fTypeName)) {
				fNodes.add(node);
			}
			return true;
		}

		public List<MethodDeclaration> getNodes() {
			return fNodes;
		}
	}

	private static final class RecordComponentVisitor extends ASTVisitor {
		private final List<SingleVariableDeclaration> fNodes= new ArrayList<>();

		public RecordComponentVisitor() {
		}

		@Override
		public boolean visit(RecordDeclaration node) {
			List<SingleVariableDeclaration> decl= node.recordComponents();
			if (decl != null)
				fNodes.addAll(decl);
			return true;
		}

		public List<SingleVariableDeclaration> getNodes() {
			return fNodes;
		}
	}
}
