/*******************************************************************************
 * Copyright (c) 2007, 2009 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.codeassist;

import java.util.Stack;

import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.AllocationExpression;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.Block;
import org.eclipse.jdt.internal.compiler.ast.MessageSend;
import org.eclipse.jdt.internal.compiler.ast.ThrowStatement;
import org.eclipse.jdt.internal.compiler.ast.TryStatement;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.util.SimpleSet;

public class ThrownExceptionFinder extends ASTVisitor {

	private SimpleSet thrownExceptions;
	private Stack exceptionsStack;

	public ReferenceBinding[] find(TryStatement tryStatement, BlockScope scope) {
		this.thrownExceptions = new SimpleSet();
		this.exceptionsStack = new Stack();
		tryStatement.traverse(this, scope);
		removeCaughtExceptions(tryStatement);

		ReferenceBinding[] result = new ReferenceBinding[this.thrownExceptions.elementSize];
		this.thrownExceptions.asArray(result);
		return result;
	}

	private void acceptException(ReferenceBinding binding) {
		if (binding != null && binding.isValidBinding()) {
			this.thrownExceptions.add(binding);
		}
	}

	public void endVisit(MessageSend messageSend, BlockScope scope) {
		if (messageSend.binding != null) {
			endVisitMethodInvocation(messageSend.binding);
		}
		super.endVisit(messageSend, scope);
	}

	public void endVisit(AllocationExpression allocationExpression, BlockScope scope) {
		if (allocationExpression.binding != null) {
			endVisitMethodInvocation(allocationExpression.binding);
		}
		super.endVisit(allocationExpression, scope);
	}

	public void endVisit(ThrowStatement throwStatement, BlockScope scope) {
		acceptException((ReferenceBinding)throwStatement.exception.resolvedType);
		super.endVisit(throwStatement, scope);
	}


	private void endVisitMethodInvocation(MethodBinding methodBinding) {
		ReferenceBinding[] thrownExceptionBindings = methodBinding.thrownExceptions;
		int length = thrownExceptionBindings == null ? 0 : thrownExceptionBindings.length;
		for (int i = 0; i < length; i++) {
			acceptException(thrownExceptionBindings[i]);
		}
	}

	public boolean visit(TypeDeclaration typeDeclaration, CompilationUnitScope scope) {
		return visitType(typeDeclaration);
	}

	public boolean visit(TypeDeclaration memberTypeDeclaration, ClassScope scope) {
		return visitType(memberTypeDeclaration);
	}

	public boolean visit(TypeDeclaration localTypeDeclaration, BlockScope scope) {
		return visitType(localTypeDeclaration);
	}

	private boolean visitType(TypeDeclaration typeDeclaration) {
		return false;
	}

	public boolean visit(TryStatement tryStatement, BlockScope scope) {
		this.exceptionsStack.push(this.thrownExceptions);
		SimpleSet exceptionSet = new SimpleSet();
		this.thrownExceptions = exceptionSet;
		tryStatement.tryBlock.traverse(this, scope);

		removeCaughtExceptions(tryStatement);

		this.thrownExceptions = (SimpleSet)this.exceptionsStack.pop();

		Object[] values = exceptionSet.values;
		for (int i = 0; i < values.length; i++) {
			if (values[i] != null) {
				this.thrownExceptions.add(values[i]);
			}
		}

		Block[] catchBlocks = tryStatement.catchBlocks;
		int length = catchBlocks == null ? 0 : catchBlocks.length;
		for (int i = 0; i < length; i++) {
			catchBlocks[i].traverse(this, scope);
		}
		return false;
	}

	private void removeCaughtExceptions(TryStatement tryStatement) {
		Argument[] catchArguments = tryStatement.catchArguments;
		int length = catchArguments == null ? 0 : catchArguments.length;
		for (int i = 0; i < length; i++) {
			TypeBinding exception = catchArguments[i].type.resolvedType;
			if (exception != null && exception.isValidBinding()) {
				removeCaughtException((ReferenceBinding)exception);

			}
		}
	}

	private void removeCaughtException(ReferenceBinding caughtException) {
		Object[] exceptions = this.thrownExceptions.values;
		for (int i = 0; i < exceptions.length; i++) {
			ReferenceBinding exception = (ReferenceBinding)exceptions[i];
			if (exception != null) {
				if (exception == caughtException || caughtException.isSuperclassOf(exception)) {
					this.thrownExceptions.remove(exception);
				}
			}
		}
	}
}
