/**
 *
 */
package codemining.js.codeutils.binding;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang.NotImplementedException;
import org.eclipse.wst.jsdt.core.dom.*;

import codemining.js.codeutils.JavascriptASTExtractor;
import codemining.languagetools.bindings.TokenNameBinding;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Retrieve the variable bindings, given an ASTNode. This finds exact bindings
 * to the detriment of recall. Partial code snippets are not supported.
 *
 * @author Miltos Allamanis <[email protected]>
 *
 */
public class JavascriptExactVariableBindingsExtractor extends
		AbstractJavascriptNameBindingsExtractor {

	/**
	 * This class looks for declarations of variables and the references to
	 * them.
	 *
	 */
	private static class VariableBindingFinder extends ASTVisitor {
		/**
		 * Map of variables (represented as bindings) to all token positions
		 * where the variable is referenced.
		 */
		Map<IVariableBinding, List<ASTNode>> variableScope = Maps
				.newIdentityHashMap();

		private void addBinding(final IVariableBinding binding) {
			variableScope.put(binding, Lists.<ASTNode> newArrayList());
		}

		/**
		 * @param binding
		 */
		private void addBindingData(final IVariableBinding binding,
				final ASTNode nameNode) {
			if (binding == null) {
				return; // Sorry, cannot do anything.
			}
			final List<ASTNode> thisVarBindings = checkNotNull(
					variableScope.get(binding),
					"Binding was not previously found");
			thisVarBindings.add(nameNode);
		}

		/**
		 * Looks for field declarations (i.e. class member variables).
		 */
		@Override
		public boolean visit(final FieldDeclaration node) {
			for (final Object fragment : node.fragments()) {
				final VariableDeclarationFragment frag = (VariableDeclarationFragment) fragment;
				final IVariableBinding binding = frag.resolveBinding();
				addBinding(binding);
			}
			return true;
		}

		/**
		 * Visits {@link SimpleName} AST nodes. Resolves the binding of the
		 * simple name and looks for it in the {@link #variableScope} map. If
		 * the binding is found, this is a reference to a variable.
		 *
		 * @param node
		 *            the node to visit
		 */
		@Override
		public boolean visit(final SimpleName node) {
			final IBinding binding = node.resolveBinding();
			if (variableScope.containsKey(binding)) {
				addBindingData((IVariableBinding) binding, node);
			}
			return true;
		}

		/**
		 * Looks for Method Parameters.
		 */
		@Override
		public boolean visit(final SingleVariableDeclaration node) {
			final IVariableBinding binding = node.resolveBinding();
			if (binding != null) {
				addBinding(binding);
			}
			return true;
		}

		/**
		 * Looks for variables declared in for loops.
		 */
		@Override
		public boolean visit(final VariableDeclarationExpression node) {
			for (final Object fragment : node.fragments()) {
				final VariableDeclarationFragment frag = (VariableDeclarationFragment) fragment;
				final IVariableBinding binding = frag.resolveBinding();
				if (binding != null) {
					addBinding(binding);
				}
			}
			return true;
		}

		/**
		 * Looks for local variable declarations. For every declaration of a
		 * variable, the parent {@link Block} denoting the variable's scope is
		 * stored in {@link #variableScope} map.
		 *
		 * @param node
		 *            the node to visit
		 */
		@Override
		public boolean visit(final VariableDeclarationStatement node) {
			for (final Object fragment : node.fragments()) {
				final VariableDeclarationFragment frag = (VariableDeclarationFragment) fragment;
				final IVariableBinding binding = frag.resolveBinding();
				if (binding != null) {
					addBinding(binding);
				}
			}
			return true;
		}
	}

	@Override
	protected JavascriptASTExtractor createExtractor() {
		return new JavascriptASTExtractor(true);
	}

	@Override
	public Set<?> getAvailableFeatures() {
		return Collections.emptySet();
	}

	@Override
	public Set<Set<ASTNode>> getNameBindings(final ASTNode node) {
		final VariableBindingFinder bindingFinder = new VariableBindingFinder();
		node.accept(bindingFinder);

		final Set<Set<ASTNode>> nameBindings = Sets.newHashSet();
		for (final Entry<IVariableBinding, List<ASTNode>> variableBindings : bindingFinder.variableScope
				.entrySet()) {
			final Set<ASTNode> boundNodes = Sets.newIdentityHashSet();
			boundNodes.addAll(variableBindings.getValue());
			nameBindings.add(boundNodes);
		}
		return nameBindings;
	}

	@Override
	public List<TokenNameBinding> getNameBindings(final String code) {
		throw new UnsupportedOperationException(
				"Partial snippets cannot be resolved due to the "
						+ "lack of support from Eclipse JSDT. Consider using the approximate binding extractor.");
	}

	@Override
	public void setActiveFeatures(final Set<?> activeFeatures) {
		throw new NotImplementedException();
	}
}