/*******************************************************************************
 * 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
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.dom;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
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.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ForStatement;
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.ImportDeclaration;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.SuperFieldAccess;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchExpression;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclarationStatement;
import org.eclipse.jdt.core.dom.TypeParameter;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationExpression;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

import org.eclipse.jdt.internal.core.manipulation.dom.ASTResolving;

import org.eclipse.jdt.internal.ui.util.ASTHelper;

/**
 * Evaluates all fields, methods and types available (declared) at a given offset
 * in a compilation unit (Code assist that returns IBindings)
 */
public class ScopeAnalyzer {

	private static final IBinding[] NO_BINDING= new IBinding[0];

	/**
	 * Flag to specify that method should be reported.
	 */
	public static final int METHODS= 1;

	/**
	 * Flag to specify that variables should be reported.
	 */
	public static final int VARIABLES= 2;

	/**
	 * Flag to specify that types should be reported.
	 */
	public static final int TYPES= 4;

	/**
	 * Flag to specify that fields should not be reported.
	 */
	public static final int NO_FIELDS= 8;

	/**
	 * Flag to specify that only visible elements should be added.
	 */
	public static final int CHECK_VISIBILITY= 16;

	private interface IBindingRequestor {
		boolean acceptBinding(IBinding binding);
	}

	private static class DefaultBindingRequestor implements IBindingRequestor {

		private final List<IBinding> fResult;
		private final HashSet<String> fNamesAdded;
		private final int fFlags;
		private final ITypeBinding fParentTypeBinding;

		public DefaultBindingRequestor(ITypeBinding parentTypeBinding, int flags) {
			fParentTypeBinding= parentTypeBinding;
			fFlags= flags;
			fResult= new ArrayList<>();
			fNamesAdded= new HashSet<>();
		}

		public DefaultBindingRequestor() {
			this(null, 0);
		}

		@Override
		public boolean acceptBinding(IBinding binding) {
			if (binding == null)
				return false;

			String signature= getSignature(binding, fFlags);
			if (signature != null && fNamesAdded.add(signature)) { // avoid duplicated results from inheritance
				fResult.add(binding);
			}
			return false;
		}

		public List<IBinding> getResult() {
			if (hasFlag(CHECK_VISIBILITY, fFlags)) {
				for (int i= fResult.size() - 1; i >= 0; i--) {
					IBinding binding= fResult.get(i);
					if (!isVisible(binding, fParentTypeBinding)) {
						fResult.remove(i);
					}
				}
			}
			if (hasFlag(NO_FIELDS, fFlags)) {
				for (int i= fResult.size() - 1; i >= 0; i--) {
					IBinding binding= fResult.get(i);
					if (binding instanceof IVariableBinding && ((IVariableBinding) binding).isField()) {
						fResult.remove(i);
					}
				}
			}
			return fResult;
		}

	}

	private final HashSet<ITypeBinding> fTypesVisited;

	private final CompilationUnit fRoot;

	public ScopeAnalyzer(CompilationUnit root) {
		fTypesVisited= new HashSet<>();
		fRoot= root;
	}

	private void clearLists() {
		fTypesVisited.clear();
	}

	private static String getSignature(IBinding binding, int flags) {
		if (binding != null) {
			switch (binding.getKind()) {
				case IBinding.METHOD:
					StringBuilder buf= new StringBuilder();
					buf.append('M');
					buf.append(binding.getName()).append('(');
					ITypeBinding[] parameters= ((IMethodBinding) binding).getParameterTypes();
					for (int i= 0; i < parameters.length; i++) {
						if (i > 0) {
							buf.append(',');
						}
						ITypeBinding paramType= parameters[i].getErasure();
						buf.append(paramType.getQualifiedName());
					}
					buf.append(')');
					return buf.toString();

				case IBinding.VARIABLE:
					if (hasFlag(NO_FIELDS, flags) && ((IVariableBinding) binding).isField()) {
						return 'F' + binding.getName();
					}
					return 'V' + binding.getName();
				case IBinding.TYPE:
					return 'T' + binding.getName();
			}
		}
		return null;
	}

	static final boolean hasFlag(int property, int flags) {
		return (flags & property) != 0;
	}

	/**
	 * Collects all elements available in a type and its hierarchy
	 * @param binding The type binding
	 * @param flags Flags defining the elements to report
	 * @param requestor the requestor to which all results are reported
	 * @return return <code>true</code> if the requestor has reported the binding as found and no further results are required
	 */
	private boolean addInherited(ITypeBinding binding, int flags, IBindingRequestor requestor) {
		return addInherited(binding, false, flags, requestor);
	}

	private boolean addInherited(ITypeBinding binding, boolean isSuperInterfaceBinding, int flags, IBindingRequestor requestor) {
		if (!fTypesVisited.add(binding)) {
			return false;
		}
		if (hasFlag(VARIABLES, flags)) {
			for (IVariableBinding variableBinding : binding.getDeclaredFields()) {
				if (requestor.acceptBinding(variableBinding)) {
					return true;
				}
			}
		}

		if (hasFlag(METHODS, flags)) {
			for (IMethodBinding curr : binding.getDeclaredMethods()) {
				if (isSuperInterfaceBinding && Modifier.isStatic(curr.getModifiers())) {
					continue;
				}
				if (!curr.isSynthetic() && !curr.isConstructor()) {
					if (requestor.acceptBinding(curr))
						return true;
				}
			}
		}

		if (hasFlag(TYPES, flags)) {
			for (ITypeBinding curr : binding.getDeclaredTypes()) {
				if (requestor.acceptBinding(curr))
					return true;
			}
		}


		ITypeBinding superClass= binding.getSuperclass();
		if (superClass != null) {
			if (addInherited(superClass, flags, requestor)) // recursive
				return true;
		} else if (binding.isArray()) {
			if (addInherited(fRoot.getAST().resolveWellKnownType("java.lang.Object"), flags, requestor)) //$NON-NLS-1$
				return true;
		}

		for (ITypeBinding intf : binding.getInterfaces()) {// includes looking for methods: abstract, unimplemented methods
			if (addInherited(intf, true, flags, requestor)) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Collects all elements available in a type: its hierarchy and its outer scopes.
	 * @param binding The type binding
	 * @param flags Flags defining the elements to report
	 * @param requestor the requestor to which all results are reported
	 * @return return <code>true</code> if the requestor has reported the binding as found and no further results are required
	 */
	private boolean addTypeDeclarations(ITypeBinding binding, int flags, IBindingRequestor requestor) {
		if (hasFlag(TYPES, flags) && !binding.isAnonymous()) {
			if (requestor.acceptBinding(binding))
				return true;

			for (ITypeBinding typeParameter : binding.getTypeParameters()) {
				if (requestor.acceptBinding(typeParameter)) {
					return true;
				}
			}
		}

		addInherited(binding, flags, requestor); // add inherited

		if (binding.isLocal()) {
			addOuterDeclarationsForLocalType(binding, flags, requestor);
		} else {
			ITypeBinding declaringClass= binding.getDeclaringClass();
			if (declaringClass != null) {
				if (addTypeDeclarations(declaringClass, flags, requestor)) // Recursively add inherited
					return true;
			} else if (hasFlag(TYPES, flags)) {
				if (fRoot.findDeclaringNode(binding) != null) {
					List<AbstractTypeDeclaration> types= fRoot.types();
					for (AbstractTypeDeclaration type : types) {
						if (requestor.acceptBinding(type.resolveBinding()))
							return true;
					}
				}
			}
		}
		return false;
	}

	private boolean addOuterDeclarationsForLocalType(ITypeBinding localBinding, int flags, IBindingRequestor requestor) {
		ASTNode node= fRoot.findDeclaringNode(localBinding);
		if (node == null) {
			return false;
		}

		if (node instanceof AbstractTypeDeclaration || node instanceof AnonymousClassDeclaration) {
			if (addLocalDeclarations(node.getParent(), flags, requestor))
				return true;

			ITypeBinding parentTypeBinding= Bindings.getBindingOfParentType(node.getParent());
			if (parentTypeBinding != null) {
				if (addTypeDeclarations(parentTypeBinding, flags, requestor))
					return true;
			}

		}
		return false;
	}

	private static ITypeBinding getBinding(Expression node) {
		if (node != null) {
			return node.resolveTypeBinding();
		}
		return null;
	}

	private static ITypeBinding getQualifier(SimpleName selector) {
		ASTNode parent= selector.getParent();
		switch (parent.getNodeType()) {
			case ASTNode.METHOD_INVOCATION:
				MethodInvocation decl= (MethodInvocation) parent;
				if (selector == decl.getName()) {
					return getBinding(decl.getExpression());
				}
				return null;
			case ASTNode.QUALIFIED_NAME:
				QualifiedName qualifiedName= (QualifiedName) parent;
				if (selector == qualifiedName.getName()) {
					return getBinding(qualifiedName.getQualifier());
				}
				return null;
			case ASTNode.FIELD_ACCESS:
				FieldAccess fieldAccess= (FieldAccess) parent;
				if (selector == fieldAccess.getName()) {
					return getBinding(fieldAccess.getExpression());
				}
				return null;
			case ASTNode.SUPER_FIELD_ACCESS:
				SuperFieldAccess superFieldAccess= (SuperFieldAccess) parent;
				if (selector == superFieldAccess.getName()) {
					ITypeBinding curr= Bindings.getBindingOfParentType(parent);
					return curr.getSuperclass();
				}
				return null;
			case ASTNode.SUPER_METHOD_INVOCATION: {
				SuperMethodInvocation superInv= (SuperMethodInvocation) parent;
				if (selector == superInv.getName()) {
					ITypeBinding curr= Bindings.getBindingOfParentType(parent);
					return curr.getSuperclass();
				}
				return null;
			}
			default:
				if (parent instanceof Type) {
					// bug 67644: in 'a.new X()', all member types of A are visible as location of X.
					ASTNode normalizedNode= ASTNodes.getNormalizedNode(parent);
					if (normalizedNode.getLocationInParent() == ClassInstanceCreation.TYPE_PROPERTY) {
						ClassInstanceCreation creation= (ClassInstanceCreation) normalizedNode.getParent();
						return getBinding(creation.getExpression());
					}
				}
				return null;
		}
	}

	public IBinding[] getDeclarationsInScope(SimpleName selector, int flags) {
		try {
			// special case for switch on enum
			StructuralPropertyDescriptor locationInParent= selector.getLocationInParent();
			if (locationInParent == SwitchCase.EXPRESSION_PROPERTY || locationInParent == SwitchCase.EXPRESSIONS2_PROPERTY) {
				ASTNode caseParent= selector.getParent().getParent();
				ITypeBinding binding= null;
				if (caseParent instanceof SwitchStatement) {
					binding= ((SwitchStatement) caseParent).getExpression().resolveTypeBinding();
				} else if (caseParent instanceof SwitchExpression) {
					binding= ((SwitchExpression) caseParent).getExpression().resolveTypeBinding();
				}
				if (binding != null && binding.isEnum()) {
					return getEnumContants(binding);
				}
			}

			ITypeBinding parentTypeBinding= Bindings.getBindingOfParentType(selector);
			if (parentTypeBinding != null) {
				ITypeBinding binding= getQualifier(selector);
				DefaultBindingRequestor requestor= new DefaultBindingRequestor(parentTypeBinding, flags);
				if (binding == null) {
					addLocalDeclarations(selector, flags, requestor);
					addTypeDeclarations(parentTypeBinding, flags, requestor);
				} else {
					addInherited(binding, flags, requestor);
				}

				List<IBinding> result= requestor.getResult();
				return result.toArray(new IBinding[result.size()]);
			}
			return NO_BINDING;
		} finally {
			clearLists();
		}
	}

	private static class SearchRequestor implements IBindingRequestor {

		private final int fFlags;
		private final ITypeBinding fParentTypeBinding;
		private final IBinding fToSearch;
		private boolean fFound;
		private boolean fIsVisible;

		public SearchRequestor(IBinding toSearch, ITypeBinding parentTypeBinding, int flag) {
			fFlags= flag;
			fToSearch= toSearch;
			fParentTypeBinding= parentTypeBinding;
			fFound= false;
			fIsVisible= true;
		}

		@Override
		public boolean acceptBinding(IBinding binding) {
			if (fFound)
				return true;

			if (binding == null)
				return false;

			if (fToSearch.getKind() != binding.getKind()) {
				return false;
			}

			boolean checkVisibility= hasFlag(CHECK_VISIBILITY, fFlags);
			if (binding == fToSearch) {
				fFound= true;
			} else {
				IBinding bindingDeclaration= Bindings.getDeclaration(binding);
				if (bindingDeclaration == fToSearch) {
					fFound= true;
				} else if (bindingDeclaration.getName().equals(fToSearch.getName())) {
					String signature= getSignature(bindingDeclaration, fFlags);
					if (signature != null && signature.equals(getSignature(fToSearch, fFlags))) {
						if (checkVisibility) {
							fIsVisible= false;
						}
						return true; // found element that hides the binding to find
					}
				}
			}

			if (fFound && checkVisibility) {
				fIsVisible= ScopeAnalyzer.isVisible(binding, fParentTypeBinding);
			}
			return fFound;
		}

		public boolean found() {
			return fFound;
		}

		public boolean isVisible() {
			return fIsVisible;
		}
	}

	public boolean isDeclaredInScope(IBinding declaration, SimpleName selector, int flags) {
		try {
			// special case for switch on enum
			StructuralPropertyDescriptor locationInParent= selector.getLocationInParent();
			if (locationInParent == SwitchCase.EXPRESSION_PROPERTY || locationInParent == SwitchCase.EXPRESSIONS2_PROPERTY) {
				ASTNode caseParent= selector.getParent().getParent();
				ITypeBinding binding= null;
				if (caseParent instanceof SwitchStatement) {
					binding= ((SwitchStatement) caseParent).getExpression().resolveTypeBinding();
				} else if (caseParent instanceof SwitchExpression) {
					binding= ((SwitchExpression) caseParent).getExpression().resolveTypeBinding();
				}
				if (binding != null && binding.isEnum()) {
					return hasEnumContants(declaration, binding.getTypeDeclaration());
				}
			}

			ITypeBinding parentTypeBinding= Bindings.getBindingOfParentTypeContext(selector);
			if (parentTypeBinding != null) {
				ITypeBinding binding= getQualifier(selector);
				SearchRequestor requestor= new SearchRequestor(declaration, parentTypeBinding, flags);
				if (binding == null) {
					addLocalDeclarations(selector, flags, requestor);
					if (requestor.found())
						return requestor.isVisible();
					addTypeDeclarations(parentTypeBinding, flags, requestor);
					if (requestor.found())
						return requestor.isVisible();
				} else {
					addInherited(binding, flags, requestor);
					if (requestor.found())
						return requestor.isVisible();
				}
			}
			return false;
		} finally {
			clearLists();
		}
	}

	private IVariableBinding[] getEnumContants(ITypeBinding binding) {
		IVariableBinding[] declaredFields= binding.getDeclaredFields();
		ArrayList<IVariableBinding> res= new ArrayList<>(declaredFields.length);
		for (IVariableBinding curr : declaredFields) {
			if (curr.isEnumConstant()) {
				res.add(curr);
			}
		}
		return res.toArray(new IVariableBinding[res.size()]);
	}

	private boolean hasEnumContants(IBinding declaration, ITypeBinding binding) {
		for (IVariableBinding curr : binding.getDeclaredFields()) {
			if (curr == declaration)
				return true;
		}
		return false;
	}

	public IBinding[] getDeclarationsInScope(int offset, int flags) {
		org.eclipse.jdt.core.dom.NodeFinder finder= new org.eclipse.jdt.core.dom.NodeFinder(fRoot, offset, 0);
		ASTNode node= finder.getCoveringNode();
		if (node == null) {
			return NO_BINDING;
		}

		if (node instanceof SimpleName) {
			return getDeclarationsInScope((SimpleName) node, flags);
		}

		try {
			ITypeBinding binding= Bindings.getBindingOfParentType(node);
			DefaultBindingRequestor requestor= new DefaultBindingRequestor(binding, flags);
			addLocalDeclarations(node, offset, flags, requestor);
			if (binding != null) {
				addTypeDeclarations(binding, flags, requestor);
			}
			List<IBinding> result= requestor.getResult();
			return result.toArray(new IBinding[result.size()]);
		} finally {
			clearLists();
		}
	}

	private static ITypeBinding getDeclaringType(IBinding binding) {
		switch (binding.getKind()) {
			case IBinding.VARIABLE:
				return ((IVariableBinding) binding).getDeclaringClass();
			case IBinding.METHOD:
				return ((IMethodBinding) binding).getDeclaringClass();
			case IBinding.TYPE:
				ITypeBinding typeBinding= (ITypeBinding) binding;
				if (typeBinding.getDeclaringClass() != null) {
					return typeBinding;
				}
				return typeBinding;
		}
		return null;
	}

	/**
	 * Evaluates if the declaration is visible in a certain context.
	 * @param binding The binding of the declaration to examine
	 * @param context The context to test in
	 * @return Returns
	 */
	public static boolean isVisible(IBinding binding, ITypeBinding context) {
		if (binding.getKind() == IBinding.VARIABLE && !((IVariableBinding) binding).isField()) {
			return true; // all local variables found are visible
		}
		ITypeBinding declaring= getDeclaringType(binding);
		if (declaring == null) {
			return false;
		}

		declaring= declaring.getTypeDeclaration();

		int modifiers= binding.getModifiers();
		int contextModifiers= context.getModifiers();
		if (context.isClass() && Modifier.isStatic(contextModifiers)) {
			if (binding.getKind() == IBinding.VARIABLE && !Modifier.isStatic(modifiers)) {
				return context == declaring;
			}
		}

		if (Modifier.isPublic(modifiers) || declaring.isInterface()) {
			return true;
		} else if (Modifier.isProtected(modifiers) || !Modifier.isPrivate(modifiers)) {
			if (declaring.getPackage() == context.getPackage()) {
				return true;
			}
			return isTypeInScope(declaring, context, Modifier.isProtected(modifiers));
		}
		// private visibility
		return isTypeInScope(declaring, context, false);
	}

	private static boolean isTypeInScope(ITypeBinding declaring, ITypeBinding context, boolean includeHierarchy) {
		ITypeBinding curr= context.getTypeDeclaration();
		while (curr != null && curr != declaring) {
			if (includeHierarchy && isInSuperTypeHierarchy(declaring, curr)) {
				return true;
			}
			curr= curr.getDeclaringClass();
		}
		return curr == declaring;
	}

	/*
	 * This method is different from Binding.isSuperType as type declarations are compared
	 */
	private static boolean isInSuperTypeHierarchy(ITypeBinding possibleSuperTypeDecl, ITypeBinding type) {
		if (type == possibleSuperTypeDecl) {
			return true;
		}
		ITypeBinding superClass= type.getSuperclass();
		if (superClass != null) {
			if (isInSuperTypeHierarchy(possibleSuperTypeDecl, superClass.getTypeDeclaration())) {
				return true;
			}
		}
		if (possibleSuperTypeDecl.isInterface()) {
			for (ITypeBinding superInterface : type.getInterfaces()) {
				if (isInSuperTypeHierarchy(possibleSuperTypeDecl, superInterface.getTypeDeclaration())) {
					return true;
				}
			}
		}
		return false;
	}


	public IBinding[] getDeclarationsAfter(int offset, int flags) {
		try {
			org.eclipse.jdt.core.dom.NodeFinder finder= new org.eclipse.jdt.core.dom.NodeFinder(fRoot, offset, 0);
			ASTNode node= finder.getCoveringNode();
			if (node == null) {
				return null;
			}

			ASTNode declaration= ASTResolving.findParentStatement(node);
			while (declaration instanceof Statement && declaration.getNodeType() != ASTNode.BLOCK) {
				declaration= declaration.getParent();
			}

			if (declaration instanceof Block) {
				DefaultBindingRequestor requestor= new DefaultBindingRequestor();
				DeclarationsAfterVisitor visitor= new DeclarationsAfterVisitor(node.getStartPosition(), flags, requestor);
				declaration.accept(visitor);
				List<IBinding> result= requestor.getResult();
				return result.toArray(new IBinding[result.size()]);
			}
			return NO_BINDING;
		} finally {
			clearLists();
		}
	}


	private static class ScopeAnalyzerVisitor extends HierarchicalASTVisitor {

		private final int fPosition;
		private final int fFlags;
		private final IBindingRequestor fRequestor;
		private boolean fBreak;

		public ScopeAnalyzerVisitor(int position, int flags, IBindingRequestor requestor) {
			fPosition= position;
			fFlags= flags;
			fRequestor= requestor;
			fBreak= false;
		}

		private boolean isInside(ASTNode node) {
			int start= node.getStartPosition();
			int end= start + node.getLength();

			return start <= fPosition && fPosition < end;
		}

		@Override
		public boolean visit(MethodDeclaration node) {
			if (isInside(node)) {
				Block body= node.getBody();
				if (body != null) {
					body.accept(this);
				}
				visitBackwards(node.parameters());
				visitBackwards(node.typeParameters());
			}
			return false;
		}


		@Override
		public boolean visit(TypeParameter node) {
			if (hasFlag(TYPES, fFlags) && node.getStartPosition() < fPosition) {
				fBreak= fRequestor.acceptBinding(node.getName().resolveBinding());
			}
			return !fBreak;
		}

		@Override
		public boolean visit(SwitchCase node) {
			// switch on enum allows to use enum constants without qualification
			if (hasFlag(VARIABLES, fFlags) && !node.isDefault()) {
				if (ASTHelper.isSwitchCaseExpressionsSupportedInAST(node.getAST())) {
					List<Expression> expressions= node.expressions();
					for (Expression expression : expressions) {
						visitExpression(node, expression);
					}
				} else {
					Expression expression= node.getExpression();
					visitExpression(node, expression);
				}
			}
			return false;
		}

		private void visitExpression(SwitchCase node, Expression expression) {
			if (isInside(expression)) {
				ASTNode caseParent= node.getParent();
				ITypeBinding binding= null;
				if (caseParent instanceof SwitchStatement) {
					SwitchStatement switchStatement= (SwitchStatement) caseParent;
					binding= switchStatement.getExpression().resolveTypeBinding();
				} else if (caseParent instanceof SwitchExpression) {
					SwitchExpression switchExpression= (SwitchExpression) caseParent;
					binding= switchExpression.getExpression().resolveTypeBinding();
				}
				if (binding != null && binding.isEnum()) {
					for (IVariableBinding curr : binding.getDeclaredFields()) {
						if (curr.isEnumConstant()) {
							fBreak= fRequestor.acceptBinding(curr);
							if (fBreak)
								return;
						}
					}
				}
			}
		}

		@Override
		public boolean visit(Initializer node) {
			return !fBreak && isInside(node);
		}

		@Override
		public boolean visit(FieldDeclaration node) {
			return !fBreak && isInside(node);
		}

		@Override
		public boolean visit(Expression node) {
			return !fBreak && isInside(node);
		}

		@Override
		public boolean visit(Statement node) {
			return !fBreak && isInside(node);
		}

		@Override
		public boolean visit(ASTNode node) {
			return false;
		}

		@Override
		public boolean visit(Block node) {
			if (isInside(node)) {
				visitBackwards(node.statements());
			}
			return false;
		}

		@Override
		public boolean visit(VariableDeclaration node) {
			if (hasFlag(VARIABLES, fFlags) && node.getStartPosition() < fPosition) {
				fBreak= fRequestor.acceptBinding(node.resolveBinding());
			}
			return !fBreak;
		}

		@Override
		public boolean visit(VariableDeclarationStatement node) {
			visitBackwards(node.fragments());
			return false;
		}

		@Override
		public boolean visit(VariableDeclarationExpression node) {
			visitBackwards(node.fragments());
			return false;
		}

		@Override
		public boolean visit(CatchClause node) {
			if (isInside(node)) {
				node.getBody().accept(this);
				node.getException().accept(this);
			}
			return false;
		}

		@Override
		public boolean visit(ForStatement node) {
			if (isInside(node)) {
				node.getBody().accept(this);
				visitBackwards(node.initializers());
			}
			return false;
		}

		@Override
		public boolean visit(TypeDeclarationStatement node) {
			if (hasFlag(TYPES, fFlags) && node.getStartPosition() + node.getLength() < fPosition) {
				fBreak= fRequestor.acceptBinding(node.resolveBinding());
				return false;
			}
			return !fBreak && isInside(node);
		}

		private void visitBackwards(List<? extends ASTNode> list) {
			if (fBreak)
				return;

			for (int i= list.size() - 1; i >= 0; i--) {
				ASTNode curr= list.get(i);
				if (curr.getStartPosition() <  fPosition) {
					curr.accept(this);
				}
			}
		}
	}

	private static class DeclarationsAfterVisitor extends HierarchicalASTVisitor {
		private final int fPosition;
		private final int fFlags;
		private final IBindingRequestor fRequestor;
		private boolean fBreak;

		public DeclarationsAfterVisitor(int position, int flags, IBindingRequestor requestor) {
			fPosition= position;
			fFlags= flags;
			fRequestor= requestor;
			fBreak= false;
		}

		@Override
		public boolean visit(ASTNode node) {
			return !fBreak;
		}

		@Override
		public boolean visit(VariableDeclaration node) {
			if (hasFlag(VARIABLES, fFlags) && fPosition < node.getStartPosition()) {
				fBreak= fRequestor.acceptBinding(node.resolveBinding());
			}
			return false;
		}

		@Override
		public boolean visit(AnonymousClassDeclaration node) {
			return false;
		}

		@Override
		public boolean visit(TypeDeclarationStatement node) {
			if (hasFlag(TYPES, fFlags) && fPosition < node.getStartPosition()) {
				fBreak= fRequestor.acceptBinding(node.resolveBinding());
			}
			return false;
		}
	}

	private boolean addLocalDeclarations(ASTNode node, int flags, IBindingRequestor requestor) {
		return addLocalDeclarations(node, node.getStartPosition(), flags, requestor);
	}


	private boolean addLocalDeclarations(ASTNode node, int offset, int flags, IBindingRequestor requestor) {
		if (hasFlag(VARIABLES, flags) || hasFlag(TYPES, flags)) {
			BodyDeclaration declaration= ASTResolving.findParentBodyDeclaration(node);
			if (declaration instanceof MethodDeclaration || declaration instanceof Initializer || declaration instanceof FieldDeclaration) {
				ScopeAnalyzerVisitor visitor= new ScopeAnalyzerVisitor(offset, flags, requestor);
				declaration.accept(visitor);
				return visitor.fBreak;
			}
		}
		return false;
	}

	public Collection<String> getUsedVariableNames(int offset, int length) {
		HashSet<String> result= new HashSet<>();
		for (IBinding b : getDeclarationsInScope(offset, VARIABLES | CHECK_VISIBILITY)) {
			result.add(b.getName());
		}
		for (IBinding b : getDeclarationsAfter(offset + length, VARIABLES | CHECK_VISIBILITY)) {
			result.add(b.getName());
		}
		List<ImportDeclaration> imports= fRoot.imports();
		for (ImportDeclaration decl : imports) {
			if (decl.isStatic() && !decl.isOnDemand()) {
				result.add(ASTNodes.getSimpleNameIdentifier(decl.getName()));
			}
		}
		return result;
	}
}
