/*******************************************************************************
 * Copyright (c) 2000, 2019 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
 *     Microsoft Corporation - copied to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.delegates;

import java.util.List;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ChildPropertyDescriptor;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.MethodRef;
import org.eclipse.jdt.core.dom.MethodRefParameter;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.Type;

import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.refactoring.RefactoringCoreMessages;

/**
 * Delegate creator for static and non-static methods.
 *
 * @since 3.2
 */
public class DelegateMethodCreator extends DelegateCreator {

	private ASTNode fDelegateInvocation;
	private MethodRef fDocMethodReference;

	@Override
	protected void initialize() {

		Assert.isTrue(getDeclaration() instanceof MethodDeclaration);

		if (getNewElementName() == null)
			setNewElementName(((MethodDeclaration) getDeclaration()).getName().getIdentifier());

		setInsertBefore(true);
	}

	@Override
	protected ASTNode createBody(BodyDeclaration bd) throws JavaModelException {

		MethodDeclaration methodDeclaration= (MethodDeclaration) bd;

		// interface or abstract method ? => don't create a method body.
		if (methodDeclaration.getBody() == null)
			return null;

		return createDelegateMethodBody(methodDeclaration);
	}

	@Override
	protected ASTNode createDocReference(final BodyDeclaration declaration) throws JavaModelException {
		fDocMethodReference= getAst().newMethodRef();
		fDocMethodReference.setName(getAst().newSimpleName(getNewElementName()));
		if (isMoveToAnotherFile())
			fDocMethodReference.setQualifier(createDestinationTypeName());
		createArguments((MethodDeclaration) declaration, fDocMethodReference.parameters(), false);
		return fDocMethodReference;
	}

	@Override
	protected ASTNode getBodyHead(BodyDeclaration result) {
		return result;
	}

	@Override
	protected ChildPropertyDescriptor getJavaDocProperty() {
		return MethodDeclaration.JAVADOC_PROPERTY;
	}

	@Override
	protected ChildPropertyDescriptor getBodyProperty() {
		return MethodDeclaration.BODY_PROPERTY;
	}

	/**
	 * @return the delegate incovation, either a {@link ConstructorInvocation}
	 *         or a {@link MethodInvocation}. May be null if the delegate
	 *         method is abstract (and therefore has no body at all)
	 */
	public ASTNode getDelegateInvocation() {
		return fDelegateInvocation;
	}

	/**
	 * @return the javadoc reference to the old method in the javadoc comment.
	 * 		   May be null if no comment was created.
	 */
	public MethodRef getJavadocReference() {
		return fDocMethodReference;
	}

	/**
	 * Creates the corresponding statement for the method invocation, based on
	 * the return type.
	 *
	 * @param declaration the method declaration where the invocation statement
	 *            is inserted
	 * @param invocation the method invocation being encapsulated by the
	 *            resulting statement
	 * @return the corresponding statement
	 */
	protected Statement createMethodInvocation(final MethodDeclaration declaration, final MethodInvocation invocation) {
		Assert.isNotNull(declaration);
		Assert.isNotNull(invocation);
		Statement statement= null;
		final Type type= declaration.getReturnType2();
		if (type == null)
			statement= createExpressionStatement(invocation);
		else {
			if (type instanceof PrimitiveType) {
				final PrimitiveType primitive= (PrimitiveType) type;
				if (primitive.getPrimitiveTypeCode().equals(PrimitiveType.VOID))
					statement= createExpressionStatement(invocation);
				else
					statement= createReturnStatement(invocation);
			} else
				statement= createReturnStatement(invocation);
		}
		return statement;
	}

	@Override
	protected IBinding getDeclarationBinding() {
		final MethodDeclaration declaration= (MethodDeclaration) getDeclaration();
		return declaration.resolveBinding();
	}

	@SuppressWarnings("unchecked")
	private void createArguments(final MethodDeclaration declaration, final List<? extends ASTNode> arguments, boolean methodInvocation) {
		Assert.isNotNull(declaration);
		Assert.isNotNull(arguments);
		SingleVariableDeclaration variable= null;
		final int size= declaration.parameters().size();
		for (int index= 0; index < size; index++) {
			variable= (SingleVariableDeclaration) declaration.parameters().get(index);

			if (methodInvocation) {
				// we are creating method invocation parameters
				final SimpleName expression= getAst().newSimpleName(variable.getName().getIdentifier());
				((List<Expression>) arguments).add(expression);
			} else {
				// we are creating type info for the javadoc
				final MethodRefParameter parameter= getAst().newMethodRefParameter();
				parameter.setType(ASTNodeFactory.newType(getAst(), variable));
				if ((index == size - 1) && declaration.isVarargs())
					parameter.setVarargs(true);
				((List<MethodRefParameter>) arguments).add(parameter);
			}
		}
	}

	private Block createDelegateMethodBody(final MethodDeclaration declaration) {
		Assert.isNotNull(declaration);

		MethodDeclaration old= (MethodDeclaration) getDeclaration();
		List<Expression> arguments;
		Statement call;
		if (old.isConstructor()) {
			ConstructorInvocation invocation= getAst().newConstructorInvocation();
			arguments= invocation.arguments();
			call= invocation;
			fDelegateInvocation= invocation;
		} else {
			MethodInvocation invocation= getAst().newMethodInvocation();
			invocation.setName(getAst().newSimpleName(getNewElementName()));
			invocation.setExpression(getAccess());
			arguments= invocation.arguments();
			call= createMethodInvocation(declaration, invocation);
			fDelegateInvocation= invocation;
		}
		createArguments(declaration, arguments, true);

		final Block body= getAst().newBlock();
		body.statements().add(call);

		return body;
	}

	/**
	 * Creates a new expression statement for the method invocation.
	 *
	 * @param invocation the method invocation
	 * @return the corresponding statement
	 */
	private ExpressionStatement createExpressionStatement(final MethodInvocation invocation) {
		Assert.isNotNull(invocation);
		return invocation.getAST().newExpressionStatement(invocation);
	}

	/**
	 * Creates a new return statement for the method invocation.
	 *
	 * @param invocation the method invocation to create a return statement for
	 * @return the corresponding statement
	 */
	private ReturnStatement createReturnStatement(final MethodInvocation invocation) {
		Assert.isNotNull(invocation);
		final ReturnStatement statement= invocation.getAST().newReturnStatement();
		statement.setExpression(invocation);
		return statement;
	}

	@Override
	protected String getTextEditGroupLabel() {
		return RefactoringCoreMessages.DelegateMethodCreator_text_edit_group_field;
	}
}
