/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.search;

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

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.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.EnumDeclaration;
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.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
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.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;

import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.dom.LocalVariableIndex;
import org.eclipse.jdt.internal.corext.dom.NodeFinder;
import org.eclipse.jdt.internal.corext.refactoring.code.flow.FlowContext;
import org.eclipse.jdt.internal.corext.refactoring.code.flow.FlowInfo;
import org.eclipse.jdt.internal.corext.refactoring.code.flow.InOutFlowAnalyzer;


public class MethodExitsFinder extends ASTVisitor {
	
	private AST fAST;
	private MethodDeclaration fMethodDeclaration;
	private List fResult;
	private List fCatchedExceptions;

	public String initialize(CompilationUnit root, int offset, int length) {
		return initialize(root, NodeFinder.perform(root, offset, length));
	}
	
	public String initialize(CompilationUnit root, ASTNode node) {
		fAST= root.getAST();
		
		if (node instanceof ReturnStatement) {
			fMethodDeclaration= (MethodDeclaration)ASTNodes.getParent(node, ASTNode.METHOD_DECLARATION);
			if (fMethodDeclaration == null)
				return SearchMessages.MethodExitsFinder_no_return_type_selected;
			return null;
			
		}
		
		Type type= null;
		if (node instanceof Type) {
			type= (Type)node;
		} else  if (node instanceof Name) {
			Name name= ASTNodes.getTopMostName((Name)node);
			if (name.getParent() instanceof Type) {
				type= (Type)name.getParent();
			}
		}
		if (type == null)
			return SearchMessages.MethodExitsFinder_no_return_type_selected; 
		type= ASTNodes.getTopMostType(type);
		if (!(type.getParent() instanceof MethodDeclaration))
			return SearchMessages.MethodExitsFinder_no_return_type_selected; 
		fMethodDeclaration= (MethodDeclaration)type.getParent();
		return null;
	}

	public List perform() {
		fResult= new ArrayList();
		markReferences();
		if (fResult.size() > 0) {
			Type returnType= fMethodDeclaration.getReturnType2();
			if (returnType != null)
				fResult.add(fMethodDeclaration.getReturnType2());
		}
		return fResult;
	}
	
	private void markReferences() {
		fCatchedExceptions= new ArrayList();
		boolean isVoid= true;
		Type returnType= fMethodDeclaration.getReturnType2();
		if (returnType != null) {
			ITypeBinding returnTypeBinding= returnType.resolveBinding();
			isVoid= returnTypeBinding != null && Bindings.isVoidType(returnTypeBinding);
		}
		fMethodDeclaration.accept(this);
		Block block= fMethodDeclaration.getBody();
		if (block != null) {
			List statements= block.statements();
			if (statements.size() > 0) {
				Statement last= (Statement)statements.get(statements.size() - 1);
				int maxVariableId= LocalVariableIndex.perform(fMethodDeclaration);
				FlowContext flowContext= new FlowContext(0, maxVariableId + 1);
				flowContext.setConsiderAccessMode(false);
				flowContext.setComputeMode(FlowContext.ARGUMENTS);
				InOutFlowAnalyzer flowAnalyzer= new InOutFlowAnalyzer(flowContext);
				FlowInfo info= flowAnalyzer.perform(new ASTNode[] {last});
				if (!info.isNoReturn() && !isVoid) {
					if (!info.isPartialReturn())
						return;
				}
			}
			SimpleName name= fAST.newSimpleName("x"); //$NON-NLS-1$
			name.setSourceRange(fMethodDeclaration.getStartPosition() + fMethodDeclaration.getLength() - 1, 1);
			fResult.add(name);
		}
	}

	public boolean visit(TypeDeclaration node) {
		// Don't dive into a local type.
		return false;
	}

	public boolean visit(AnonymousClassDeclaration node) {
		// Don't dive into a local type.
		return false;
	}

	public boolean visit(AnnotationTypeDeclaration node) {
		// Don't dive into a local type.
		return false;
	}

	public boolean visit(EnumDeclaration node) {
		// Don't dive into a local type.
		return false;
	}

	public boolean visit(ReturnStatement node) {
		fResult.add(node);
		return super.visit(node);
	}
	
	public boolean visit(TryStatement node) {
		int currentSize= fCatchedExceptions.size();
		List catchClauses= node.catchClauses();
		for (Iterator iter= catchClauses.iterator(); iter.hasNext();) {
			IVariableBinding variable= ((CatchClause)iter.next()).getException().resolveBinding();
			if (variable != null && variable.getType() != null) {
				fCatchedExceptions.add(variable.getType());
			}
		}
		node.getBody().accept(this);
		int toRemove= fCatchedExceptions.size() - currentSize;
		for(int i= toRemove; i > 0; i--) {
			fCatchedExceptions.remove(currentSize);
		}
		
		// visit catch and finally
		for (Iterator iter= catchClauses.iterator(); iter.hasNext(); ) {
			((CatchClause)iter.next()).accept(this);
		}
		if (node.getFinally() != null)
			node.getFinally().accept(this);
			
		// return false. We have visited the body by ourselves.	
		return false;
	}
	
	public boolean visit(ThrowStatement node) {
		ITypeBinding exception= node.getExpression().resolveTypeBinding();
		if (isExitPoint(exception)) {
			SimpleName name= fAST.newSimpleName("xxxxx"); //$NON-NLS-1$
			name.setSourceRange(node.getStartPosition(), 5);
			fResult.add(name);
		}	
		return true;
	}
	
	public boolean visit(MethodInvocation node) {
		if (isExitPoint(node.resolveMethodBinding())) {
			fResult.add(node.getName());
		}
		return true;
	}
	
	public boolean visit(SuperMethodInvocation node) {
		if (isExitPoint(node.resolveMethodBinding())) {
			fResult.add(node.getName());
		}
		return true;
	}
	
	public boolean visit(ClassInstanceCreation node) {
		if (isExitPoint(node.resolveConstructorBinding())) {
			fResult.add(node.getType());
		}
		return true;
	}
	
	public boolean visit(ConstructorInvocation node) {
		if (isExitPoint(node.resolveConstructorBinding())) {
			// mark this
			SimpleName name= fAST.newSimpleName("xxxx"); //$NON-NLS-1$
			name.setSourceRange(node.getStartPosition(), 4);
			fResult.add(name);
		}
		return true;
	}
	
	public boolean visit(SuperConstructorInvocation node) {
		if (isExitPoint(node.resolveConstructorBinding())) {
			SimpleName name= fAST.newSimpleName("xxxxx"); //$NON-NLS-1$
			name.setSourceRange(node.getStartPosition(), 5);
			fResult.add(name);
		}
		return true;
	}
	
	private boolean isExitPoint(ITypeBinding binding) {
		if (binding == null)
			return false;
		return !isCatched(binding);
	}
	
	private boolean isExitPoint(IMethodBinding binding) {
		if (binding == null)
			return false;
		ITypeBinding[] exceptions= binding.getExceptionTypes();
		for (int i= 0; i < exceptions.length; i++) {
			if (!isCatched(exceptions[i]))
				return true;
		}
		return false;
	}
	
	private boolean isCatched(ITypeBinding binding) {
		for (Iterator iter= fCatchedExceptions.iterator(); iter.hasNext();) {
			ITypeBinding catchException= (ITypeBinding)iter.next();
			if (catches(catchException, binding))
				return true;
		}
		return false;
	}
	
	private boolean catches(ITypeBinding catchTypeBinding, ITypeBinding throwTypeBinding) {
		while(throwTypeBinding != null) {
			if (throwTypeBinding == catchTypeBinding)
				return true;
			throwTypeBinding= throwTypeBinding.getSuperclass();	
		}
		return false;
	}	
}
