/*******************************************************************************
 * 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
 *     Lukas Hanke <hanke@yatta.de> - [templates][content assist] Content assist for 'for' loop should suggest member variables - https://bugs.eclipse.org/117215
 *     Microsoft Corporation - moved template related code to jdt.core.manipulation - https://bugs.eclipse.org/549989
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.template.java;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.CompletionRequestor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.manipulation.JavaManipulation;


/**
 * A completion requester to collect informations on local variables.
 * This class is used for guessing variable names like arrays, collections, etc.
 */
final class CompilationUnitCompletion extends CompletionRequestor {

	/**
	 * Describes a local variable (including parameters) inside the method where
	 * code completion was invoked. Special predicates exist to query whether
	 * a variable can be iterated over.
	 */
	public final class Variable {
		private static final int UNKNOWN= 0, NONE= 0;
		private static final int ARRAY= 1;
		private static final int COLLECTION= 2;
		private static final int ITERABLE= 4;

		/**
		 * The name of the local variable.
		 */
		private final String name;

		/**
		 * The signature of the local variable's type.
		 */
		private final String signature;

		/* lazily computed properties */
		private int fType= UNKNOWN;
		private int fChecked= NONE;
		private String[] fMemberTypes;

		private Variable(String name, String signature) {
			this.name= name;
			this.signature= signature;
		}

		/**
		 * Returns the name of the variable.
		 *
		 * @return the name of the variable
		 */
		public String getName() {
			return name;
		}

		/**
		 * Returns <code>true</code> if the type of the local variable is an
		 * array type.
		 *
		 * @return <code>true</code> if the receiver's type is an array,
		 *         <code>false</code> if not
		 */
		public boolean isArray() {
			if (fType == UNKNOWN && (fChecked & ARRAY) == 0 && Signature.getTypeSignatureKind(signature) == Signature.ARRAY_TYPE_SIGNATURE)
				fType= ARRAY;
			fChecked |= ARRAY;
			return fType == ARRAY;
		}

		/**
		 * Returns <code>true</code> if the receiver's type is a subclass of
		 * <code>java.util.Collection</code>, <code>false</code> otherwise.
		 *
		 * @return <code>true</code> if the receiver's type is a subclass of
		 *         <code>java.util.Collection</code>, <code>false</code>
		 *         otherwise
		 */
		public boolean isCollection() {
			// Collection extends Iterable
			if ((fType == UNKNOWN || fType == ITERABLE) && (fChecked & COLLECTION) == 0 && isSubtypeOf("java.util.Collection")) //$NON-NLS-1$
				fType= COLLECTION;
			fChecked |= COLLECTION;
			return fType == COLLECTION;
		}

		/**
		 * Returns <code>true</code> if the receiver's type is a subclass of
		 * <code>java.lang.Iterable</code>, <code>false</code> otherwise.
		 *
		 * @return <code>true</code> if the receiver's type is a subclass of
		 *         <code>java.lang.Iterable</code>, <code>false</code>
		 *         otherwise
		 */
		public boolean isIterable() {
			if (fType == UNKNOWN && (fChecked & ITERABLE) == 0 && isSubtypeOf("java.lang.Iterable")) //$NON-NLS-1$
				fType= ITERABLE;
			fChecked |= ITERABLE;
			return fType == ITERABLE || fType == COLLECTION; // Collection extends Iterable
		}

		/**
		 * Returns <code>true</code> if the receiver's type is an implementor
		 * of <code>interfaceName</code>.
		 *
		 * @param supertype the fully qualified name of the interface
		 * @return <code>true</code> if the receiver's type implements the
		 *         type named <code>interfaceName</code>
		 */
		private boolean isSubtypeOf(String supertype) {
			String implementorName= SignatureUtil.stripSignatureToFQN(signature);
			if (implementorName.length() == 0)
				return false;

			int implementorDims= Signature.getArrayCount(signature);
			int superDimsIndex= supertype.indexOf("[]"); //$NON-NLS-1$
			int superDims;
			if (superDimsIndex != -1) {
				superDims= (supertype.length() - superDimsIndex) / 2;
				supertype= supertype.substring(0, superDimsIndex);
			} else {
				superDims= 0;
			}
			if (implementorDims > superDims) {
				return "java.lang.Object".equals(supertype); //$NON-NLS-1$
			} else if (superDims != implementorDims) {
				return false;
			}

			boolean qualified= supertype.indexOf('.') != -1;

			// try cheap test first
			if (implementorName.equals(supertype) || !qualified && Signature.getSimpleName(implementorName).equals(supertype))
				return true;

			if (fUnit == null)
				return false;

			IJavaProject project= fUnit.getJavaProject();

			try {
				IType sub= project.findType(implementorName);
				if (sub == null)
					return false;

				if (qualified) {
					IType sup= project.findType(supertype);
					if (sup == null)
						return false;
					ITypeHierarchy hierarchy= sub.newSupertypeHierarchy(null);
					return hierarchy.contains(sup);
				} else {
					ITypeHierarchy hierarchy= sub.newSupertypeHierarchy(null);
					IType[] allTypes= hierarchy.getAllTypes();
					for (IType type : allTypes) {
						if (type.getElementName().equals(supertype))
							return true;
					}
				}

			} catch (JavaModelException e) {
				// ignore and return false
			}

			return false;
		}

		private IType[] getSupertypes(String supertype) {
			IType[] empty= new IType[0];
			String implementorName= SignatureUtil.stripSignatureToFQN(signature);
			if (implementorName.length() == 0)
				return empty;

			boolean qualified= supertype.indexOf('.') != -1;

			if (fUnit == null)
				return empty;

			IJavaProject project= fUnit.getJavaProject();

			try {
				IType sub= project.findType(implementorName);
				if (sub == null)
					return empty;

				if (qualified) {
					IType sup= project.findType(supertype);
					if (sup == null)
						return empty;
					return new IType[] {sup};
				} else {
					ITypeHierarchy hierarchy= sub.newSupertypeHierarchy(null);
					IType[] allTypes= hierarchy.getAllTypes();
					List<IType> matches= new ArrayList<>();
					for (IType type : allTypes) {
						if (type.getElementName().equals(supertype))
							matches.add(type);
					}
					return matches.toArray(new IType[matches.size()]);
				}

			} catch (JavaModelException e) {
				// ignore and return false
			}

			return empty;
		}

		/**
		 * Returns the signature of the member type.
		 *
		 * @return the signature of the member type
		 */
		public String getMemberTypeSignature() {
			return getMemberTypeSignatures()[0];
		}

		/**
		 * Returns the signatures of all member type bounds.
		 *
		 * @return the signatures of all member type bounds
		 */
		public String[] getMemberTypeSignatures() {
			if (isArray()) {
				return new String[] {Signature.createArraySignature(Signature.getElementType(signature), Signature.getArrayCount(signature) - 1)};
			} else if (fUnit != null && (isIterable() || isCollection())) {
				if (fMemberTypes == null) {
					try {
						try {
							TypeParameterResolver util= new TypeParameterResolver(this);
							fMemberTypes= util.computeBinding("java.lang.Iterable", 0); //$NON-NLS-1$
						} catch (JavaModelException e) {
							try {
								TypeParameterResolver util= new TypeParameterResolver(this);
								fMemberTypes= util.computeBinding("java.util.Collection", 0); //$NON-NLS-1$
							} catch (JavaModelException x) {
								fMemberTypes= new String[0];
							}
						}
					} catch (IndexOutOfBoundsException e) {
						fMemberTypes= new String[0];
					}
				}
				if (fMemberTypes.length > 0)
					return fMemberTypes;
			}
			return new String[] {Signature.createTypeSignature("java.lang.Object", true)}; //$NON-NLS-1$
		}

		/**
		 * Returns the type names of all member type bounds, as they would be
		 * appear when referenced in the current compilation unit.
		 *
		 * @return type names of all member type bounds
		 */
		public String[] getMemberTypeNames() {
			String[] signatures= getMemberTypeSignatures();
			String[] names= new String[signatures.length];

			for (int i= 0; i < signatures.length; i++) {
				String sig= signatures[i];
				String local= fLocalTypes.get(Signature.getElementType(sig));
				int dim= Signature.getArrayCount(sig);
				if (local != null && dim > 0) {
					StringBuilder array= new StringBuilder(local);
					for (int j= 0; j < dim; j++)
						array.append("[]"); //$NON-NLS-1$
					local= array.toString();
				}
				if (local != null)
					names[i]= local;
				else
					names[i]= Signature.getSimpleName(Signature.getSignatureSimpleName(sig));
			}
			return names;
		}

		/**
		 * Returns the type arguments of the declared type of the variable. Returns
		 * an empty array if it is not a parameterized type.
		 *
		 * @param type the fully qualified type name of which to match a type argument
		 * @param index the index of the type parameter in the type
		 * @return the type bounds for the specified type argument in this local variable
		 * @since 3.3
		 */
		public String[] getTypeArgumentBoundSignatures(String type, int index) {
			List<String> all= new ArrayList<>();
			IType[] supertypes= getSupertypes(type);
			if (fUnit != null) {
				for (IType supertype : supertypes) {
					try {
						TypeParameterResolver util= new TypeParameterResolver(this);
						String[] result= util.computeBinding(supertype.getFullyQualifiedName(), index);
						all.addAll(Arrays.asList(result));
					} catch (JavaModelException | IndexOutOfBoundsException e) {
					}
				}
			}
			if (all.isEmpty())
				return new String[] {Signature.createTypeSignature("java.lang.Object", true)}; //$NON-NLS-1$
			return all.toArray(new String[all.size()]);
		}

		/*
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			String type;
			switch (fType) {
				case ITERABLE:
					type= "ITERABLE"; //$NON-NLS-1$
					break;
				case COLLECTION:
					type= "COLLECTION"; //$NON-NLS-1$
					break;
				case ARRAY:
					type= "ARRAY"; //$NON-NLS-1$
					break;
				default:
					type= "UNKNOWN"; //$NON-NLS-1$
					break;
			}
			return "LocalVariable [name=\"" + name + "\" signature=\"" + signature + "\" type=\"" + type + "\" member=\"" + getMemberTypeSignature() + "\"]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
		}
	}

	/**
	 * Given a java type, a resolver computes the bounds of type variables
	 * declared in a super type, considering any type constraints along the
	 * inheritance path.
	 */
	private final class TypeParameterResolver {
		private static final String OBJECT_SIGNATURE= "Ljava.lang.Object;"; //$NON-NLS-1$

		private final ITypeHierarchy fHierarchy;
		private final Variable fVariable;
		private final IType fType;
		private final List<String> fBounds= new ArrayList<>();

		/**
		 * Creates a new type parameter resolver to compute the bindings of type
		 * parameters for the declared type of <code>variable</code>. For any
		 * super type of the type of <code>variable</code>, calling
		 * {@link #computeBinding(IType, int) computeBinding} will find the type
		 * bounds of type variables in the super type, considering any type
		 * constraints along the inheritance path.
		 *
		 * @param variable the local variable under investigation
		 * @throws JavaModelException if the type of <code>variable</code>
		 *         cannot be found
		 */
		public TypeParameterResolver(Variable variable) throws JavaModelException {
			String typeName= SignatureUtil.stripSignatureToFQN(variable.signature);
			IJavaProject project= fUnit.getJavaProject();
			fType= project.findType(typeName);
			fHierarchy= fType.newSupertypeHierarchy(null);
			fVariable= variable;
		}

		/**
		 * Given a type parameter of <code>superType</code> at position
		 * <code>index</code>, this method computes and returns the (lower)
		 * type bound(s) of that parameter for an instance of <code>fType</code>.
		 * <p>
		 * <code>superType</code> must be a super type of <code>fType</code>,
		 * and <code>superType</code> must have at least
		 * <code>index + 1</code> type parameters.
		 * </p>
		 *
		 * @param superType the qualified type name of the super type to compute
		 *        the type parameter binding for
		 * @param index the index into the list of type parameters of
		 *        <code>superType</code>
		 * @return the binding
		 * @throws JavaModelException if any java model operation fails
		 * @throws IndexOutOfBoundsException if the index is not valid
		 */
		public String[] computeBinding(String superType, int index) throws JavaModelException, IndexOutOfBoundsException {
			IJavaProject project= fUnit.getJavaProject();
			IType type= project.findType(superType);
			if (type == null)
				throw new JavaModelException(new CoreException(new Status(IStatus.ERROR, JavaManipulation.ID_PLUGIN, IStatus.OK, "No such type", null))); //$NON-NLS-1$
			return computeBinding(type, index);
		}

		/**
		 * Given a type parameter of <code>superType</code> at position
		 * <code>index</code>, this method computes and returns the (lower)
		 * type bound(s) of that parameter for an instance of <code>fType</code>.
		 * <p>
		 * <code>superType</code> must be a super type of <code>fType</code>,
		 * and <code>superType</code> must have at least
		 * <code>index + 1</code> type parameters.
		 * </p>
		 *
		 * @param superType the super type to compute the type parameter binding
		 *        for
		 * @param index the index into the list of type parameters of
		 *        <code>superType</code>
		 * @return the binding
		 * @throws JavaModelException if any java model operation fails
		 * @throws IndexOutOfBoundsException if the index is not valid
		 */
		public String[] computeBinding(IType superType, int index) throws JavaModelException, IndexOutOfBoundsException {
			initBounds();
			computeTypeParameterBinding(superType, index);
			return fBounds.toArray(new String[fBounds.size()]);
		}

		/**
		 * Given a type parameter of <code>superType</code> at position
		 * <code>index</code>, this method recursively computes the (lower)
		 * type bound(s) of that parameter for an instance of <code>fType</code>.
		 * <p>
		 * <code>superType</code> must be a super type of <code>fType</code>,
		 * and <code>superType</code> must have at least
		 * <code>index + 1</code> type parameters.
		 * </p>
		 * <p>
		 * The type bounds are stored in <code>fBounds</code>.
		 * </p>
		 *
		 * @param superType the super type to compute the type parameter binding
		 *        for
		 * @param index the index into the list of type parameters of
		 *        <code>superType</code>
		 * @throws JavaModelException if any java model operation fails
		 * @throws IndexOutOfBoundsException if the index is not valid
		 */
		private void computeTypeParameterBinding(final IType superType, final int index) throws JavaModelException, IndexOutOfBoundsException {
			int nParameters= superType.getTypeParameters().length;
			if (nParameters <= index)
				throw new IndexOutOfBoundsException();

			IType[] subTypes= fHierarchy.getSubtypes(superType);

			if (subTypes.length == 0) {
				// we have reached down to the base type
				Assert.isTrue(superType.equals(fType));

				String match= findMatchingTypeArgument(fVariable.signature, index, fUnit.findPrimaryType());
				String bound= SignatureUtil.getUpperBound(match);

				// use the match whether it is a concrete type or not - if not,
				// the generic type will at least be in visible in our context
				// and can be referenced
				addBound(bound);
				return;
			}

			IType subType= subTypes[0]; // take the first, as they all lead to fType

			String signature= findMatchingSuperTypeSignature(subType, superType);
			String match= findMatchingTypeArgument(signature, index, subType);

			if (isConcreteType(match, subType)) {
				addBound(match);
				return;
			}

			ITypeParameter[] typeParameters= subType.getTypeParameters();

			for (int k= 0; k < typeParameters.length; k++) {
				ITypeParameter formalParameter= typeParameters[k];
				if (formalParameter.getElementName().equals(SignatureUtil.stripSignatureToFQN(match))) {
					String[] bounds= formalParameter.getBounds();
					for (String bound : bounds) {
						String boundSignature= Signature.createTypeSignature(bound, true);
						addBound(SignatureUtil.qualifySignature(boundSignature, subType));
					}
					computeTypeParameterBinding(subType, k);
					return;
				}
			}

			// We have a non-concrete type argument T, but no matching type
			// parameter in the sub type. This can happen if T is declared in
			// the enclosing type. Since it the declaration is probably visible
			// then, its fine to simply copy the match to the bounds and return.
			addBound(match);
			return;
		}

		/**
		 * Finds and returns the type argument with index <code>index</code>
		 * in the given type super type signature. If <code>signature</code>
		 * is a generic signature, the type parameter at <code>index</code> is
		 * extracted. If the type parameter is an upper bound (<code>? super SomeType</code>),
		 * the type signature of <code>java.lang.Object</code> is returned.
		 * <p>
		 * Also, if <code>signature</code> has no type parameters (i.e. is a
		 * reference to the raw type), the type signature of
		 * <code>java.lang.Object</code> is returned.
		 * </p>
		 *
		 * @param signature the super type signature from a type's
		 *        <code>extends</code> or <code>implements</code> clause
		 * @param index the index of the type parameter to extract from
		 *        <code>signature</code>
		 * @param context the type context inside which unqualified types should
		 *        be resolved
		 * @return the type argument signature of the type parameter at
		 *         <code>index</code> in <code>signature</code>
		 * @throws IndexOutOfBoundsException if the index is not valid
		 */
		private String findMatchingTypeArgument(String signature, int index, IType context) throws IndexOutOfBoundsException {
			String[] typeArguments= Signature.getTypeArguments(signature);
			if (typeArguments.length > 0 && typeArguments.length <= index)
				throw new IndexOutOfBoundsException();
			if (typeArguments.length == 0) {
				// raw binding - bound to Object
				return OBJECT_SIGNATURE;
			} else {
				String bound= SignatureUtil.getUpperBound(typeArguments[index]);
				return SignatureUtil.qualifySignature(bound, context);
			}
		}

		/**
		 * Finds and returns the super type signature in the
		 * <code>extends</code> or <code>implements</code> clause of
		 * <code>subType</code> that corresponds to <code>superType</code>.
		 *
		 * @param subType a direct and true sub type of <code>superType</code>
		 * @param superType a direct super type (super class or interface) of
		 *        <code>subType</code>
		 * @return the super type signature of <code>subType</code> referring
		 *         to <code>superType</code>
		 * @throws JavaModelException if extracting the super type signatures
		 *         fails, or if <code>subType</code> contains no super type
		 *         signature to <code>superType</code>
		 */
		private String findMatchingSuperTypeSignature(IType subType, IType superType) throws JavaModelException {
			String[] signatures= getSuperTypeSignatures(subType, superType);
			for (String signature : signatures) {
				String qualified= SignatureUtil.qualifySignature(signature, subType);
				String subFQN= SignatureUtil.stripSignatureToFQN(qualified);

				String superFQN= superType.getFullyQualifiedName();
				if (subFQN.equals(superFQN)) {
					return signature;
				}

				// handle local types
				if (fLocalTypes.containsValue(subFQN)) {
					return signature;
				}
			}

			throw new JavaModelException(new CoreException(new Status(IStatus.ERROR, JavaManipulation.ID_PLUGIN, IStatus.OK, "Illegal hierarchy", null))); //$NON-NLS-1$
		}

		/**
		 * Returns the super interface signatures of <code>subType</code> if
		 * <code>superType</code> is an interface, otherwise returns the super
		 * type signature.
		 *
		 * @param subType the sub type signature
		 * @param superType the super type signature
		 * @return the super type signatures of <code>subType</code>
		 * @throws JavaModelException if any java model operation fails
		 */
		private String[] getSuperTypeSignatures(IType subType, IType superType) throws JavaModelException {
			if (superType.isInterface())
				return subType.getSuperInterfaceTypeSignatures();
			else
				return new String[] {subType.getSuperclassTypeSignature()};
		}

		/**
		 * Clears the collected type bounds and initializes it with
		 * <code>java.lang.Object</code>.
		 */
		private void initBounds() {
			fBounds.clear();
			fBounds.add(OBJECT_SIGNATURE);
		}

		/**
		 * Filters the current list of type bounds through the additional type
		 * bound described by <code>boundSignature</code>.
		 *
		 * @param boundSignature the additional bound to add to the list of
		 *        collected bounds
		 */
		private void addBound(String boundSignature) {
			if (SignatureUtil.isJavaLangObject(boundSignature))
				return;

			boolean found= false;
			for (ListIterator<String> it= fBounds.listIterator(); it.hasNext();) {
				String old= it.next();
				if (isTrueSubtypeOf(boundSignature, old)) {
					if (!found) {
						it.set(boundSignature);
						found= true;
					} else {
						it.remove();
					}
				}
			}
			if (!found)
				fBounds.add(boundSignature);
		}

		/**
		 * Returns <code>true</code> if <code>subTypeSignature</code>
		 * describes a type which is a true sub type of the type described by
		 * <code>superTypeSignature</code>.
		 *
		 * @param subTypeSignature the potential subtype's signature
		 * @param superTypeSignature the potential supertype's signature
		 * @return <code>true</code> if the inheritance relationship holds
		 */
		private boolean isTrueSubtypeOf(String subTypeSignature, String superTypeSignature) {
			// try cheap test first
			if (subTypeSignature.equals(superTypeSignature))
				return true;

			if (SignatureUtil.isJavaLangObject(subTypeSignature))
				return false; // Object has no super types

			if (Signature.getTypeSignatureKind(subTypeSignature) != Signature.BASE_TYPE_SIGNATURE && SignatureUtil.isJavaLangObject(superTypeSignature))
				return true;

			IJavaProject project= fUnit.getJavaProject();

			try {

				if ((Signature.getTypeSignatureKind(subTypeSignature) & (Signature.TYPE_VARIABLE_SIGNATURE | Signature.CLASS_TYPE_SIGNATURE)) == 0)
					return false;
				IType subType= project.findType(SignatureUtil.stripSignatureToFQN(subTypeSignature));
				if (subType == null)
					return false;

				if ((Signature.getTypeSignatureKind(superTypeSignature) & (Signature.TYPE_VARIABLE_SIGNATURE | Signature.CLASS_TYPE_SIGNATURE)) == 0)
					return false;
				IType superType= project.findType(SignatureUtil.stripSignatureToFQN(superTypeSignature));
				if (superType == null)
					return false;

				ITypeHierarchy hierarchy= subType.newSupertypeHierarchy(null);
				IType[] types= hierarchy.getAllSupertypes(subType);

				for (IType type : types)
					if (type.equals(superType))
						return true;
			} catch (JavaModelException e) {
				// ignore and return false
			}

			return false;
		}

		/**
		 * Returns <code>true</code> if <code>signature</code> is a concrete type signature,
		 * <code>false</code> if it is a type variable.
		 *
		 * @param signature the signature to check
		 * @param context the context inside which to resolve the type
		 * @return <code>true</code> if the given signature is a concrete type signature
		 * @throws JavaModelException if finding the type fails
		 */
		private boolean isConcreteType(String signature, IType context) throws JavaModelException {
			// Inexpensive check for the variable type first
			if (Signature.TYPE_VARIABLE_SIGNATURE == Signature.getTypeSignatureKind(signature))
				return false;

			// try and resolve otherwise
			if (context.isBinary()) {
				return fUnit.getJavaProject().findType(SignatureUtil.stripSignatureToFQN(signature)) != null;
			} else {
				return context.resolveType(SignatureUtil.stripSignatureToFQN(signature)) != null;
			}
		}
	}

	private ICompilationUnit fUnit;

	private List<Variable> fLocalVariables= new ArrayList<>();
	private List<Variable> fFields= new ArrayList<>();
	private Map<String, String> fLocalTypes= new HashMap<>();

	private boolean fError;

	/**
	 * Creates a compilation unit completion.
	 *
	 * @param unit the compilation unit, may be <code>null</code>.
	 */
	CompilationUnitCompletion(ICompilationUnit unit) {
		reset(unit);
		setIgnored(CompletionProposal.ANONYMOUS_CLASS_DECLARATION, true);
		setIgnored(CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION, true);
		setIgnored(CompletionProposal.KEYWORD, true);
		setIgnored(CompletionProposal.LABEL_REF, true);
		setIgnored(CompletionProposal.METHOD_DECLARATION, true);
		setIgnored(CompletionProposal.METHOD_NAME_REFERENCE, true);
		setIgnored(CompletionProposal.METHOD_REF, true);
		setIgnored(CompletionProposal.CONSTRUCTOR_INVOCATION, true);
		setIgnored(CompletionProposal.METHOD_REF_WITH_CASTED_RECEIVER, true);
		setIgnored(CompletionProposal.PACKAGE_REF, true);
		setIgnored(CompletionProposal.MODULE_REF, true);
		setIgnored(CompletionProposal.MODULE_DECLARATION, true);
		setIgnored(CompletionProposal.POTENTIAL_METHOD_DECLARATION, true);
		setIgnored(CompletionProposal.VARIABLE_DECLARATION, true);
		setIgnored(CompletionProposal.TYPE_REF, true);
	}

	/**
	 * Resets the completion requester.
	 *
	 * @param unit the compilation unit, may be <code>null</code>.
	 */
	private void reset(ICompilationUnit unit) {
		fUnit= unit;
		fLocalVariables.clear();
		fFields.clear();
		fLocalTypes.clear();

		if (fUnit != null) {
			try {
				IType[] cuTypes= fUnit.getAllTypes();
				for (IType cuType : cuTypes) {
					String fqn= cuType.getFullyQualifiedName();
					String sig= Signature.createTypeSignature(fqn, true);
					fLocalTypes.put(sig, cuType.getElementName());
				}
			} catch (JavaModelException e) {
				// ignore
			}
		}
		fError= false;
	}

	/*
	 * @see org.eclipse.jdt.core.CompletionRequestor#accept(org.eclipse.jdt.core.CompletionProposal)
	 */
	@Override
	public void accept(CompletionProposal proposal) {

		String name= String.valueOf(proposal.getCompletion());
		String signature= String.valueOf(proposal.getSignature());

		switch (proposal.getKind()) {

			case CompletionProposal.LOCAL_VARIABLE_REF:
				// collect local variables
				fLocalVariables.add(new Variable(name, signature));
				break;
			case CompletionProposal.FIELD_REF:
				// collect local variables
				fFields.add(new Variable(name, signature));
				break;

			default:
				break;
		}
	}

	/*
	 * @see org.eclipse.jdt.core.CompletionRequestor#completionFailure(org.eclipse.jdt.core.compiler.IProblem)
	 */
	@Override
	public void completionFailure(IProblem problem) {
		fError= true;
	}

	/**
	 * Tests if the code completion process produced errors.
	 *
	 * @return <code>true</code> if there are errors, <code>false</code>
	 *         otherwise
	 */
	public boolean hasErrors() {
		return fError;
	}

	/**
	 * Returns all local variable names.
	 *
	 * @return all local variable names
	 */
	public String[] getLocalVariableNames() {
		String[] names= new String[fLocalVariables.size()];
		int i= 0;
		for (ListIterator<Variable> iterator= fLocalVariables.listIterator(fLocalVariables.size()); iterator.hasPrevious();) {
			Variable localVariable= iterator.previous();
			names[i++]= localVariable.getName();
		}
		return names;
	}

	/**
	 * Returns all field names.
	 *
	 * @return all field names
	 * @since 3.3
	 */
	public String[] getFieldNames() {
		String[] names= new String[fFields.size()];
		int i= 0;
		for (ListIterator<Variable> iterator= fFields.listIterator(fFields.size()); iterator.hasPrevious();) {
			Variable field= iterator.previous();
			names[i++]= field.getName();
		}
		return names;
	}

	/**
	 * Returns all arrays, visible in the current context's scope, in the order that they appear.
	 *
	 * @return all visible arrays
	 */
	public Variable[] findArraysInCurrentScope() {
		List<Variable> arrays= new ArrayList<>();

		// local variables
		for (ListIterator<Variable> iterator= fLocalVariables.listIterator(fLocalVariables.size()); iterator.hasPrevious();) {
			Variable localVariable= iterator.previous();

			if (localVariable.isArray())
				arrays.add(localVariable);
		}

		// fields
		for (ListIterator<Variable> iterator= fFields.listIterator(fFields.size()); iterator.hasPrevious();) {
			Variable field= iterator.previous();

			if (field.isArray())
				arrays.add(field);
		}

		return arrays.toArray(new Variable[arrays.size()]);
	}

	/**
	 * Returns all local variables implementing or extending
	 * <code>clazz</code> in the order that they appear.
	 *
	 * @param clazz the fully qualified type name of the class to match
	 * @return all local variables matching <code>clazz</code>
	 */
	public Variable[] findLocalVariables(String clazz) {
		List<Variable> matches= new ArrayList<>();

		for (ListIterator<Variable> iterator= fLocalVariables.listIterator(fLocalVariables.size()); iterator.hasPrevious();) {
			Variable localVariable= iterator.previous();

			if (localVariable.isSubtypeOf(clazz))
				matches.add(localVariable);
		}

		return matches.toArray(new Variable[matches.size()]);
	}

	/**
	 * Returns all local variables implementing or extending
	 * <code>clazz</code> in the order that they appear.
	 *
	 * @param clazz the fully qualified type name of the class to match
	 * @return all local variables matching <code>clazz</code>
	 */
	public Variable[] findFieldVariables(String clazz) {
		List<Variable> matches= new ArrayList<>();

		for (ListIterator<Variable> iterator= fFields.listIterator(fFields.size()); iterator.hasPrevious();) {
			Variable localVariable= iterator.previous();

			if (localVariable.isSubtypeOf(clazz))
				matches.add(localVariable);
		}

		return matches.toArray(new Variable[matches.size()]);
	}

	/**
	 * Returns all variables, visible in the current context's scope, implementing
	 * <code>java.lang.Iterable</code> <em>and</em> all arrays, in the order that they appear. That
	 * is, the returned variables can be used within the <code>foreach</code> language construct.
	 *
	 * @return all visible <code>Iterable</code>s and arrays
	 */
	public Variable[] findIterablesInCurrentScope() {
		List<Variable> iterables= new ArrayList<>();

		// local variables
		for (ListIterator<Variable> iterator= fLocalVariables.listIterator(fLocalVariables.size()); iterator.hasPrevious();) {
			Variable localVariable= iterator.previous();

			if (localVariable.isArray() || localVariable.isIterable())
				iterables.add(localVariable);
		}

		// fields
		for (ListIterator<Variable> iterator= fFields.listIterator(fFields.size()); iterator.hasPrevious();) {
			Variable field= iterator.previous();

			if (field.isArray() || field.isIterable())
				iterables.add(field);
		}

		return iterables.toArray(new Variable[iterables.size()]);
	}

}

