/*******************************************************************************
 * Copyright (c) 2000, 2021 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
 *     Red Hat Inc. - refactored to jdt.core.manipulation
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.surround;

import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.CreationReference;
import org.eclipse.jdt.core.dom.ExpressionMethodReference;
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.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.MethodReference;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.SuperMethodReference;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeMethodReference;
import org.eclipse.jdt.core.dom.VariableDeclarationExpression;

import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.dom.Selection;
import org.eclipse.jdt.internal.corext.refactoring.util.AbstractExceptionAnalyzer;

import org.eclipse.jdt.internal.ui.text.correction.QuickAssistProcessorUtil;

/**
 * @since 1.16
 */
public class ExceptionAnalyzer extends AbstractExceptionAnalyzer {

	private Selection fSelection;
	private static  ASTNode fEnclosingNode;

	private static class ExceptionComparator implements Comparator<ITypeBinding> {
		@Override
		public int compare(ITypeBinding o1, ITypeBinding o2) {
			int d1= getDepth(o1);
			int d2= getDepth(o2);
			if (d1 < d2)
				return 1;
			if (d1 > d2)
				return -1;
			return 0;
		}
		private int getDepth(ITypeBinding binding) {
			int result= 0;
			while (binding != null) {
				binding= binding.getSuperclass();
				result++;
			}
			return result;
		}
	}

	private ExceptionAnalyzer(ASTNode enclosingNode, Selection selection) {
		Assert.isNotNull(selection);
		fEnclosingNode= enclosingNode;
		fSelection= selection;
	}

	public static ITypeBinding[] perform(ASTNode enclosingNode, Selection selection) {
		return perform(enclosingNode, selection, true);
	}

	/**
	 * @param enclosingNode The ASTNode to analyze
	 * @param selection Selection within ASTNode
	 * @param forceRemove <code>true</code> to remove exception if enclosed method throws same exception
	 * @return Array of ITypeBinding representing the uncaught exceptions
	 */
	public static ITypeBinding[] perform(ASTNode enclosingNode, Selection selection, boolean forceRemove) {
		ExceptionAnalyzer analyzer= new ExceptionAnalyzer(enclosingNode, selection);
		enclosingNode.accept(analyzer);
		List<ITypeBinding> exceptions= analyzer.getCurrentExceptions();
		if (enclosingNode.getNodeType() == ASTNode.METHOD_DECLARATION) {
			List<Type> thrownExceptions= ((MethodDeclaration) enclosingNode).thrownExceptionTypes();
			for (Type type : thrownExceptions) {
				ITypeBinding thrownException= type.resolveBinding();
				if (thrownException != null && forceRemove) {
					updateExceptionsList(exceptions, thrownException);
				}
			}
		} else {
			ITypeBinding typeBinding= null;
			if (enclosingNode.getLocationInParent() == LambdaExpression.BODY_PROPERTY) {
				typeBinding= ((LambdaExpression) enclosingNode.getParent()).resolveTypeBinding();
			} else if (enclosingNode instanceof MethodReference) {
				typeBinding= ((MethodReference) enclosingNode).resolveTypeBinding();
			}
			if (typeBinding != null) {
				IMethodBinding methodBinding= typeBinding.getFunctionalInterfaceMethod();
				if (methodBinding != null) {
					for (ITypeBinding thrownException : methodBinding.getExceptionTypes()) {
						updateExceptionsList(exceptions, thrownException);
					}
				}
			}
		}
		Collections.sort(exceptions, new ExceptionComparator());
		return exceptions.toArray(new ITypeBinding[exceptions.size()]);
	}

	private static void updateExceptionsList(List<ITypeBinding> exceptions, ITypeBinding thrownException) {
		for (Iterator<ITypeBinding> excep= exceptions.iterator(); excep.hasNext();) {
			ITypeBinding exception= excep.next();
			if (exception.isAssignmentCompatible(thrownException))
				excep.remove();
		}
	}

	@Override
	public boolean visit(ExpressionMethodReference node) {
		return handleMethodReference(node);
	}

	@Override
	public boolean visit(TypeMethodReference node) {
		return handleMethodReference(node);
	}

	@Override
	public boolean visit(SuperMethodReference node) {
		return handleMethodReference(node);
	}

	@Override
	public boolean visit(CreationReference node) {
		return handleMethodReference(node);
	}

	@Override
	public boolean visit(ThrowStatement node) {
		ITypeBinding exception= node.getExpression().resolveTypeBinding();
		if (!isSelected(node) || exception == null || Bindings.isRuntimeException(exception)) // Safety net for null bindings when compiling fails.
			return true;

		addException(exception, node.getAST());
		return true;
	}

	@Override
	public boolean visit(MethodInvocation node) {
		if (!isSelected(node))
			return false;
		return handleExceptions(node.resolveMethodBinding(), node);
	}

	@Override
	public boolean visit(SuperMethodInvocation node) {
		if (!isSelected(node))
			return false;
		return handleExceptions(node.resolveMethodBinding(), node);
	}

	@Override
	public boolean visit(ClassInstanceCreation node) {
		if (!isSelected(node))
			return false;
		return handleExceptions(node.resolveConstructorBinding(), node);
	}

	@Override
	public boolean visit(ConstructorInvocation node) {
		if (!isSelected(node))
			return false;
		return handleExceptions(node.resolveConstructorBinding(), node);
	}

	@Override
	public boolean visit(SuperConstructorInvocation node) {
		if (!isSelected(node))
			return false;
		return handleExceptions(node.resolveConstructorBinding(), node);
	}

	@Override
	public boolean visit(VariableDeclarationExpression node) {
		if (!isSelected(node))
			return false;
		return super.visit(node);
	}

	private boolean handleMethodReference(MethodReference node) {
		if (!isSelected(node))
			return false;
		if (!fEnclosingNode.equals(node))
			return false;
		IMethodBinding referredMethodBinding= node.resolveMethodBinding();
		if (referredMethodBinding == null)
			return false;
		IMethodBinding functionalMethod= QuickAssistProcessorUtil.getFunctionalMethodForMethodReference(node);
		if (functionalMethod == null || functionalMethod.isGenericMethod()) { // generic lambda expressions are not allowed
			return false;
		}
		return handleExceptions(referredMethodBinding, node);
	}

	private boolean handleExceptions(IMethodBinding binding, ASTNode node) {
		if (binding == null)
			return true;
		for (ITypeBinding exception : binding.getExceptionTypes()) {
			addException(exception, node.getAST());
		}
		return true;
	}

	private boolean isSelected(ASTNode node) {
		return fSelection.getVisitSelectionMode(node) == Selection.SELECTED;
	}
}
