/*
 * Copyright 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.javac;

import com.google.gwt.core.client.UnsafeNativeLong;
import com.google.gwt.dev.jdt.SafeASTVisitor;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.js.ast.JsBinaryOperation;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsModVisitor;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.util.InstalledHelpInfo;
import com.google.gwt.dev.util.JsniRef;
import com.google.gwt.dev.util.collect.Stack;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ImportReference;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.BaseTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
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.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.NestedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ProblemReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.SyntheticArgumentBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
import org.eclipse.jdt.internal.compiler.lookup.UnresolvedReferenceBinding;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
 * Resolves JSNI references to fields and methods and gives a informative errors if the references
 * cannot be resolved.
 * <p>
 *  * JSNI references consist of two parts @ClassDescriptor::memberDescriptor. Class descriptors
 * are source names and memberDescriptors are either a field name or a method name with a signature
 * specification. Signature specification a full signature or a wildcard (*).
 * <p>
 *  * The result of resolution will be a modified JSNI AST where all the resolved references will
 * carry the fully qualified class name and the full member reference, along with a mapping from
 * jsni references to JDT binding.
 * <p>
 * In addition in the following instances involving longs warning will be emitted to remind uses
 * that GWT longs are not JavaScript numbers:
 * <ul>
 * <li>JSNI methods with a parameter or return type of long or an array whose base type is long.
 * </li>
 * <li>Access from JSNI to a field whose type is long or an array whose base type is long.</li>
 * <li>Access from JSNI to a method with a parameter or return type of long or an array whose base
 * type is long.</li>
 * <li>JSNI references to anonymous classes.</li>
 * </ul>
 */
public class JsniReferenceResolver {

  /**
   * A call-back interface to resolve types.
   */
  public interface TypeResolver {
    /**
     * @param sourceOrBinaryName Either source or binary names are allowed in JSNI.
     */
    ReferenceBinding resolveType(String sourceOrBinaryName);
  }

  private class JsniDeclChecker extends SafeASTVisitor implements
      ClassFileConstants {

    @Override
    public void endVisit(MethodDeclaration method, ClassScope scope) {
      if (method.isNative()) {
        boolean hasUnsafeLongsAnnotation = hasUnsafeLongsAnnotation(method, scope);
        if (!hasUnsafeLongsAnnotation) {
          checkDecl(method, scope);
        }
        JsniMethod jsniMethod = jsniMethods.get(method);
        if (jsniMethod != null) {
          new JsniReferenceResolverVisitor(method, hasUnsafeLongsAnnotation).resolve(
              jsniMethod.function());
        }
      }
      suppressWarningsStack.pop();
    }

    @Override
    public void endVisit(TypeDeclaration typeDeclaration, ClassScope scope) {
      suppressWarningsStack.pop();
    }

    @Override
    public void endVisit(TypeDeclaration typeDeclaration,
        CompilationUnitScope scope) {
      suppressWarningsStack.pop();
    }

    @Override
    public void endVisitValid(TypeDeclaration typeDeclaration, BlockScope scope) {
      suppressWarningsStack.pop();
    }

    @Override
    public boolean visit(MethodDeclaration meth, ClassScope scope) {
      suppressWarningsStack.push(JdtUtil.getSuppressedWarnings(meth.annotations));
      return true;
    }

    @Override
    public boolean visit(TypeDeclaration typeDeclaration, ClassScope scope) {
      suppressWarningsStack.push(JdtUtil.getSuppressedWarnings(typeDeclaration.annotations));
      return true;
    }

    @Override
    public boolean visit(TypeDeclaration typeDeclaration,
        CompilationUnitScope scope) {
      suppressWarningsStack.push(JdtUtil.getSuppressedWarnings(typeDeclaration.annotations));
      return true;
    }

    @Override
    public boolean visitValid(TypeDeclaration typeDeclaration, BlockScope scope) {
      suppressWarningsStack.push(JdtUtil.getSuppressedWarnings(typeDeclaration.annotations));
      return true;
    }

    private void checkDecl(MethodDeclaration meth, ClassScope scope) {
      TypeReference returnType = meth.returnType;
      if (containsLong(returnType, scope)) {
        longAccessError(meth, "Type '" + typeString(returnType)
            + "' may not be returned from a JSNI method");
      }

      if (meth.arguments == null) {
        return;
      }

      for (Argument arg : meth.arguments) {
        if (containsLong(arg.type, scope)) {
          longAccessError(arg, "Parameter '" + String.valueOf(arg.name)
              + "': type '" + typeString(arg.type)
              + "' is not safe to access in JSNI code");
        }
      }
    }

    private boolean containsLong(final TypeReference type, ClassScope scope) {
      return type != null
          && JsniReferenceResolver.this.containsLong(type.resolveType(scope));
    }

    private String typeString(TypeReference type) {
      return type.toString();
    }
  }

  private class JsniReferenceResolverVisitor extends JsModVisitor {

    private final boolean hasUnsafeLongsAnnotation;
    private final MethodDeclaration method;

    public JsniReferenceResolverVisitor(MethodDeclaration method,
        boolean hasUnsafeLongsAnnotation) {
      this.method = method;
      this.hasUnsafeLongsAnnotation = hasUnsafeLongsAnnotation;
    }

    public void resolve(JsFunction function) {
      this.accept(function);
    }

    @Override
    public void endVisit(JsBinaryOperation x, JsContext ctx) {
      // Look for the patern a.f = o.@C::m();
      if (!x.getOperator().isAssignment()) {
        return;
      }
      if (!(x.getArg1() instanceof JsNameRef) || !(x.getArg2() instanceof JsNameRef)) {
        return;
      }

      JsNameRef lhs = (JsNameRef) x.getArg1();
      JsNameRef rhs = (JsNameRef) x.getArg2();

      if (!rhs.isJsniReference() || !(jsniRefs.get(rhs.getIdent()) instanceof MethodBinding)) {
        // Not a reference to a JSNI method.
        return;
      }

      if (rhs.getQualifier() == null) {
        // Unqualified JSNI reference is OK.
        return;
      }

      if (lhs.getQualifier() == null) {
        // Assignment to unqualified variable is OK.
        return;
      }

      // Here we have a qualified JSNI method reference assigned to a field.
      JsniRef jsniRef = JsniRef.parse(rhs.getIdent());
      emitWarning("unsafe", WARN_NOT_CAPTURING_QUALIFIER, x.getSourceInfo(), jsniRef,
          rhs.getQualifier().toSource());
    }

    @Override
    public void endVisit(JsNameRef x, JsContext ctx) {
      String ident = x.getIdent();
      if (!x.isJsniReference()) {
        // Not a jsni reference.
        return;
      }

      JsniRef jsniRef = JsniRef.parse(ident);
      if (jsniRef == null) {
        emitError(ERR_MALFORMED_JSNI_IDENTIFIER, x.getSourceInfo(), null, ident);
        return;
      }

      Binding binding = resolveReference(x.getSourceInfo(), jsniRef, x.getQualifier() != null,
          ctx.isLvalue());

      assert !x.isResolved();
      if (!ident.equals(jsniRef.getResolvedReference())) {
        // Replace by the resolved reference (consisting of the fully qualified classname and the
        // method description including actual signature) so that dispatch everywhere is consistent
        // with the one resolved here.
        ident = jsniRef.getResolvedReference();
        JsNameRef newRef = new JsNameRef(x.getSourceInfo(), ident, x.getQualifier());
        ctx.replaceMe(newRef);
      }
      if (binding != null) {
        jsniRefs.put(ident, binding);
      }
    }

    private void resolveClassReference(JsniRef jsniRef) {
      // Precedence rules as of JLS 6.4.1.
      // 1. Enclosing type.
      // 2. Visible type in same compilation unit.
      // 3. Named import.
      // 4. Same package.
      // 5. Import on demand.

      String originalName = jsniRef.className();
      String importedClassName = originalName;
      if (importedClassName.contains(".")) {
        // Only retain up the first dot to support innerclasses. E.g. import c.g.A and reference
        // @A.I::f.
        importedClassName = importedClassName.substring(0,importedClassName.indexOf("."));
      }

      // 1 & 2. Check to see if this name refers to the enclosing class or is directly accessible
      // from it.
      ReferenceBinding declaringClass = method.binding.declaringClass;
      while (declaringClass != null) {
        String declaringClassName = JdtUtil.getSourceName(declaringClass);
        if (declaringClassName.equals(importedClassName) ||
            declaringClassName.endsWith("." + importedClassName)) {
          // Referring to declaring class name using unqualified name.
          jsniRef.setResolvedClassName(declaringClassName +
              originalName.substring(importedClassName.length()));
          return;
        }
        String fullClassName =
            JdtUtil.getBinaryName(declaringClass) + "$" + originalName.replace('.', '$');
        if (typeResolver.resolveType(fullClassName) != null) {
          jsniRef.setResolvedClassName(JdtUtil.getSourceName(declaringClass) + "." + originalName);
          return;
        }
        declaringClass = declaringClass.enclosingTypeAt(1);
      }

      // 3. Check to see if this name is one of the named imports.
      for (ImportReference importReference : cudImports) {
        String nameFromImport = JdtUtil.asDottedString(importReference.getImportName());
        if (!importReference.isStatic()  && importReference.trailingStarPosition == 0 &&
           nameFromImport.endsWith("." + importedClassName)) {
          jsniRef.setResolvedClassName(
              nameFromImport + originalName.substring(importedClassName.length()));
          return;
        }
      }

      // 4. Check to see if this name is resolvable from the current package.
      String currentPackageBinaryClassName =
          JdtUtil.getBinaryName(
              CharOperation.charToString(method.binding.declaringClass.qualifiedPackageName()),
              originalName);

      if (typeResolver.resolveType(currentPackageBinaryClassName) != null) {
        jsniRef.setResolvedClassName(
            JdtUtil.getSourceName(
                CharOperation.charToString(method.binding.declaringClass.qualifiedPackageName()),
                originalName));
        return;
      }

      // 5. Check to see if this name is resolvable as an import on demand.
      List<String> importPackages = Lists.newArrayList("java.lang");
      for (ImportReference importReference : cudImports) {
        if (importReference.isStatic() || importReference.trailingStarPosition == 0) {
          continue;
        }
        importPackages.add(JdtUtil.asDottedString(importReference.getImportName()));
      }
      for (String importPackage : importPackages) {
        String fullClassName = importPackage + "." + originalName.replace('.', '$');
        if (typeResolver.resolveType(fullClassName) != null) {
          jsniRef.setResolvedClassName(importPackage + "." + originalName);
          return;
        }
      }

      // Otherwise leave it as it is.
      // TODO(rluble): Maybe we should leave it null here.
      jsniRef.setResolvedClassName(jsniRef.className());
    }

    private boolean isEnclosingClass(TypeBinding clazz, TypeBinding maybeEnclosingClass) {
      for (TypeBinding currentClass = clazz; currentClass != null;
          currentClass = currentClass.enclosingType()) {
        if (currentClass == maybeEnclosingClass) {
          return true;
        }
      }
      return false;
    }

    private FieldBinding checkAndResolveFieldRef(SourceInfo errorInfo, ReferenceBinding clazz,
        JsniRef jsniRef, boolean hasQualifier, boolean isLvalue) {
      assert jsniRef.isField();
      FieldBinding target = getField(clazz, jsniRef);
      if (target == null) {
        emitError(ERR_UNABLE_TO_RESOLVE_FIELD, errorInfo, jsniRef);
        return null;
      }
      resolveJsniRef(jsniRef, target);
      if (target.isDeprecated() &&
          !isEnclosingClass(method.binding.declaringClass, target.declaringClass)) {
        emitWarning("deprecation", WARN_DEPRECATED_FIELD, errorInfo, jsniRef);
      }
      if (isLvalue && target.constant() != Constant.NotAConstant) {
        emitError(ERR_ILLEGAL_ASSIGNMENT_TO_COMPILE_TIME_CONSTANT, errorInfo, jsniRef);
      }
      if (target.isStatic() && hasQualifier) {
        emitError(ERR_UNNECESSARY_QUALIFIER_STATIC_FIELD, errorInfo, jsniRef);
      } else if (!target.isStatic() && !hasQualifier) {
        emitError(ERR_MISSING_QUALIFIER_INSTANCE_FIELD, errorInfo, jsniRef);
      }

      if (hasUnsafeLongsAnnotation) {
        return target;
      }
      if (containsLong(target.type)) {
        emitError(ERR_UNSAFE_FIELD_ACCESS, errorInfo, jsniRef, typeString(target.type));
      }
      return target;
    }

    private MethodBinding checkAndResolveMethodRef(SourceInfo errorInfo, ReferenceBinding clazz,
        JsniRef jsniRef, boolean hasQualifier, boolean isLvalue) {
      assert jsniRef.isMethod();
      List<MethodBinding> targets = getMatchingMethods(clazz, jsniRef);
      if (targets.size() > 1) {
          emitError(ERR_AMBIGUOUS_WILDCARD_MATCH, errorInfo, jsniRef,
              JdtUtil.formatBinding(targets.get(0)),
              JdtUtil.formatBinding(targets.get(1)));
        return null;
      } else if (targets.isEmpty()) {
        emitError(ERR_UNABLE_TO_RESOLVE_METHOD, errorInfo, jsniRef);
        return null;
      }
      MethodBinding target = targets.get(0);
      resolveJsniRef(jsniRef, target);
      if (target.isDeprecated() &&
          !isEnclosingClass(method.binding.declaringClass, target.declaringClass)) {
        emitWarning("deprecation", WARN_DEPRECATED_METHOD, errorInfo, jsniRef);
      }
      if (isLvalue) {
        emitError(ERR_ILLEGAL_ASSIGNMENT_TO_METHOD, errorInfo, jsniRef);
      }
      boolean needsQualifer = !target.isStatic() && !target.isConstructor();
      if (!needsQualifer && hasQualifier) {
        emitError(ERR_UNNECESSARY_QUALIFIER_STATIC_METHOD, errorInfo, jsniRef);
      } else if (needsQualifer && !hasQualifier) {
        emitError(ERR_MISSING_QUALIFIER_INSTANCE_METHOD, errorInfo, jsniRef);
      }

      if (hasUnsafeLongsAnnotation) {
        return target;
      }
      if (containsLong(target.returnType)) {
        emitError(ERR_ILLEGAL_RETURN_TYPE, errorInfo, jsniRef, typeString(target.returnType));
      }

      if (target.parameters != null) {
        int i = 0;
        for (TypeBinding paramType : target.parameters) {
          ++i;
          if (containsLong(paramType)) {
            // It would be nice to print the parameter name, but how to find it?
            emitError(ERR_ILLEGAL_PARAMETER, errorInfo, jsniRef, i, typeString(paramType));
          }
        }
      }
      return target;
    }

    private Binding resolveReference(SourceInfo errorInfo, JsniRef jsniRef, boolean hasQualifier,
        boolean isLvalue) {

      resolveClassReference(jsniRef);
      String className = jsniRef.getResolvedClassName();

      boolean isPrimitive;
      ReferenceBinding clazz;
      TypeBinding binding = method.scope.getBaseType(className.toCharArray());
      if (binding != null) {
        isPrimitive = true;
        clazz = null;
      } else {
        isPrimitive = false;
        binding = clazz = findClass(className);
      }

      if (binding != null && binding.isAnonymousType()) {
        // There seems that there is no way to write a JSNI reference to an anonymous class as
        // it will require to accept a source name of the form A.1 where one of the identifier parts
        // consists only of digits and therefore is not a valid identifier.
        // This error case is left here in case names of that form start appearing from the JSNI
        // parser.
        emitError(ERR_ILLEGAL_ANONYMOUS_INNER_CLASS, errorInfo, jsniRef);
        return null;
      } else if (binding == null) {
        emitError(ERR_UNABLE_TO_RESOLVE_CLASS, errorInfo, jsniRef);
        return null;
      }

      if (clazz != null && clazz.isDeprecated() &&
          !isEnclosingClass(method.binding.declaringClass, clazz)) {
        emitWarning("deprecation", WARN_DEPRECATED_CLASS, errorInfo, jsniRef);
      }

      if (jsniRef.isField() && "class".equals(jsniRef.memberName())) {
        if (isLvalue) {
          emitError(ERR_ILLEGAL_ASSIGNMENT_TO_CLASS_LITERAL, errorInfo, jsniRef);
          return null;
        }
        // Reference to the class itself.
        jsniRef.setResolvedClassName(JdtUtil.getSourceName(binding));
        jsniRef.setResolvedMemberWithSignature(jsniRef.memberSignature());
        if (jsniRef.isArray()) {
          ArrayBinding arrayBinding =
              method.scope.createArrayType(binding, jsniRef.getDimensions());
          return arrayBinding;
        } else {
          return binding;
        }
      }

      if (jsniRef.isArray() || isPrimitive) {
        emitError(ERR_ILLEGAL_ARRAY_OR_PRIMITIVE_REFERENCE, errorInfo, jsniRef);
        return null;
      }

      assert clazz != null;
      if (jsniRef.isMethod()) {
        return checkAndResolveMethodRef(errorInfo, clazz, jsniRef, hasQualifier, isLvalue);
      } else {
        return checkAndResolveFieldRef(errorInfo, clazz, jsniRef, hasQualifier, isLvalue);
      }
    }
    private static final String WARN_NOT_CAPTURING_QUALIFIER =
        "Instance method reference '%2$s.%3$s' loses its instance ('%8$s') when assigned; "
            + "to remove this warning either assign to a local variable or construct "
            + "the proper closure using an anonymous function or by calling "
            + "Function.prototype.bind";
    private static final String ERR_ILLEGAL_ARRAY_OR_PRIMITIVE_REFERENCE =
        "Referencing member '%2$s.%4$s': 'class' is " +
        "the only legal reference for arrays and primitive types";
    private static final String ERR_ILLEGAL_ASSIGNMENT_TO_CLASS_LITERAL =
        "Illegal assignment to class literal '%2$s.%3$s'";
    private static final String ERR_UNABLE_TO_RESOLVE_CLASS =
        "Referencing class '%2$s': unable to resolve class";
    private static final String ERR_ILLEGAL_ANONYMOUS_INNER_CLASS =
        "Referencing class '%2$s': JSNI references to anonymous classes are illegal";
    private static final String ERR_ILLEGAL_PARAMETER =
        "Parameter %8$d of method '%2$s.%3$s': type '%9$s' may not be passed out of JSNI code";
    private static final String ERR_ILLEGAL_RETURN_TYPE =
        "Referencing method '%2$s.%3$s': return type '%8$s' is not safe to access in JSNI code";
    private static final String ERR_MISSING_QUALIFIER_INSTANCE_METHOD =
        "Missing qualifier on instance method '%2$s.%3$s'";
    private static final String ERR_UNNECESSARY_QUALIFIER_STATIC_METHOD =
        "Unnecessary qualifier on static method '%2$s.%3$s'";
    private static final String ERR_MISSING_QUALIFIER_INSTANCE_FIELD =
        "Missing qualifier on instance field '%2$s.%3$s'";
    private static final String ERR_UNNECESSARY_QUALIFIER_STATIC_FIELD =
        "Unnecessary qualifier on static field '%2$s.%3$s'";
    private static final String ERR_ILLEGAL_ASSIGNMENT_TO_METHOD =
        "Illegal assignment to method '%2$s.%3$s'";
    private static final String ERR_UNABLE_TO_RESOLVE_METHOD =
        "Referencing method '%2$s.%4$s': unable to resolve method in class '%6$s'";
    private static final String ERR_UNABLE_TO_RESOLVE_FIELD =
        "Referencing field '%2$s.%4$s': unable to resolve field in class '%6$s'";
    private static final String ERR_AMBIGUOUS_WILDCARD_MATCH =
        "Referencing method '%2$s.%4$s': ambiguous wildcard match; "
            + "both '%8$s' and '%9$s' match";
    private static final String ERR_UNSAFE_FIELD_ACCESS =
        "Referencing field '%2$s.%3$s': type '%8$s' is not safe to access in JSNI code";
    private static final String ERR_ILLEGAL_ASSIGNMENT_TO_COMPILE_TIME_CONSTANT =
        "Illegal assignment to compile-time constant '%2$s.%3$s'";
    private static final String ERR_MALFORMED_JSNI_IDENTIFIER =
        "Malformed JSNI identifier '%8$s'";
    private static final String WARN_DEPRECATED_CLASS =
        "Referencing deprecated class '%2$s'";
    private static final String WARN_DEPRECATED_METHOD =
        "Referencing method '%2$s.%3$s': method '%6$s.%7$s' is deprecated";
    private static final String WARN_DEPRECATED_FIELD =
        "Referencing field '%2$s.%3$s': field '%6$s.%7$s' is deprecated";

    /**
     * Formats messages for {@link #emitError} and {@link #emitWarning}, substituting as follows:
     * <ul>
     * <li> %1$s -> full original jsni string </li>
     * <li> %2$s -> full original jsni classname </li>
     * <li> %3$s -> full original jsni membername </li>
     * <li> %4$s -> full original jsni memberspec </li>
     * <li> %5$s -> full resolved jsni string </li>
     * <li> %6$s -> full resolved jsni classname </li>
     * <li> %7$s -> full resolved jsni member with signature </li>
     * </ul>
     */
    private String formatMessage(String msg, JsniRef jsniRef, Object... extraPars) {
      Object[] formatParameters = new Object[extraPars.length + 7];
      if (jsniRef != null) {
        formatParameters[0] = jsniRef.toString();
        formatParameters[1] = jsniRef.fullClassName();
        formatParameters[2] = jsniRef.memberName();
        formatParameters[3] = jsniRef.memberSignature();
        formatParameters[4] = jsniRef.getResolvedReference();
        formatParameters[5] = jsniRef.getFullResolvedClassName();
        formatParameters[6] = jsniRef.getResolvedMemberSignature();
      }

      for (int i = 0; i < extraPars.length; i++) {
        formatParameters[i + 7] = extraPars[i];
      }

      return String.format(msg, formatParameters);
    }


    private void emitError(String msg, SourceInfo errorInfo, JsniRef jsniRef, Object... extraPars) {
      JsniMethodCollector.reportJsniError(errorInfo, method, formatMessage(msg, jsniRef, extraPars));
    }

    private void emitWarning(String category, String msg, SourceInfo errorInfo, JsniRef jsniRef,
        Object... extraPars) {
      for (Set<String> suppressWarnings : suppressWarningsStack) {
        if (suppressWarnings.contains(category)
            || suppressWarnings.contains("all")) {
          return;
        }
      }
      JsniMethodCollector
          .reportJsniWarning(errorInfo, method, formatMessage(msg, jsniRef, extraPars));
    }

    private ReferenceBinding findClass(String className) {
      ReferenceBinding binding = typeResolver.resolveType(className);
      assert !(binding instanceof ProblemReferenceBinding);
      assert !(binding instanceof UnresolvedReferenceBinding);
      return binding;
    }

    private FieldBinding getField(ReferenceBinding clazz, JsniRef jsniRef) {
      assert jsniRef.isField();
      return clazz.getField(jsniRef.memberName().toCharArray(), false);
    }

    private MethodBinding getMatchingConstructor(ReferenceBinding clazz, JsniRef jsniRef) {
      for (MethodBinding constructorBinding : clazz.getMethods(INIT_CTOR_CHARS)) {
        StringBuilder methodSig = new StringBuilder();
        if (clazz instanceof NestedTypeBinding) {
          // Match synthetic args for enclosing instances.
          NestedTypeBinding nestedBinding = (NestedTypeBinding) clazz;
          if (nestedBinding.enclosingInstances != null) {
            for (int i = 0; i < nestedBinding.enclosingInstances.length; ++i) {
              SyntheticArgumentBinding arg = nestedBinding.enclosingInstances[i];
              methodSig.append(arg.type.signature());
            }
          }
        }
        if (constructorBinding.parameters != null) {
          for (TypeBinding binding : constructorBinding.parameters) {
            methodSig.append(binding.signature());
          }
        }
        if (methodSig.toString().equals(jsniRef.paramTypesString())) {
          return constructorBinding;
        }
      }
      return null;
    }

    /**
     * Returns true if {@code method}, is a method in {@code jsniClassQualifier} (to support current
     * private method access} or is a visible method in the super class per Java import rules.
     */
    private boolean isMethodVisibleToJsniRef(ReferenceBinding jsniClassQualifier,
        MethodBinding targetMethod) {
      return
          // All methods are visible (regardless of access) as long as the JSNI class reference is
          // explicit.
          jsniClassQualifier == targetMethod.declaringClass ||
          // All public superclass methods are visible.
          targetMethod.isPublic()
          // Protected and package private are visible from the same package.
          || !targetMethod.isPrivate() && method.binding.declaringClass.getPackage() ==
          targetMethod.declaringClass.getPackage()
          // Protected super class methods are visible from any subclass.
          || targetMethod.isProtected() &&
          targetMethod.declaringClass.isSuperclassOf(method.binding.declaringClass);
    }

    private List<MethodBinding> getMatchingMethods(ReferenceBinding clazz, JsniRef jsniRef) {
      assert jsniRef.isMethod();
      List<MethodBinding> foundMethods = Lists.newArrayList();
      String methodName = jsniRef.memberName();
      if ("new".equals(methodName)) {
        MethodBinding constructorBinding = getMatchingConstructor(clazz, jsniRef);
        if (constructorBinding != null) {
          foundMethods.add(constructorBinding);
        }
      } else {
        Queue<ReferenceBinding> work = Lists.newLinkedList();
        work.add(clazz);
        while (!work.isEmpty()) {
          ReferenceBinding currentClass = work.remove();
          NEXT_METHOD:
          for (MethodBinding findMethod : currentClass.getMethods(methodName.toCharArray())) {
            if (!isMethodVisibleToJsniRef(clazz, findMethod)) {
              continue;
            }
            if (!paramTypesMatch(findMethod, jsniRef)) {
              continue;
            }
            for (MethodBinding alreadyFound : foundMethods) {
              // Only collect methods with different signatures (same signatures are overloads
              // hence they are ok.
              if (paramTypesMatch(alreadyFound, findMethod)) {
                break NEXT_METHOD;
              }
            }
            foundMethods.add(findMethod);
          }
          ReferenceBinding[] superInterfaces = currentClass.superInterfaces();
          if (superInterfaces != null) {
            work.addAll(Arrays.asList(superInterfaces));
          }
          ReferenceBinding superclass = currentClass.superclass();
          if (superclass != null) {
            work.add(superclass);
          }
        }
      }
      return foundMethods;
    }

    private boolean paramTypesMatch(MethodBinding method, JsniRef jsniRef) {
      if (jsniRef.matchesAnyOverload()) {
        return true;
      }
      StringBuilder methodSig = new StringBuilder();
      if (method.parameters != null) {
        for (TypeBinding binding : method.parameters) {
          methodSig.append(binding.signature());
        }
      }
      return methodSig.toString().equals(jsniRef.paramTypesString());
    }

    private boolean paramTypesMatch(MethodBinding thisMethod, MethodBinding thatMethod) {
      int thisParameterCount = thisMethod.parameters == null ? 0 : thisMethod.parameters.length;
      int thatParameterCount = thatMethod.parameters == null ? 0 : thatMethod.parameters.length;

      if (thisParameterCount != thatParameterCount) {
        return false;
      }

      for (int i = 0; i < thisParameterCount; i++) {
        TypeBinding thisBinding = thisMethod.parameters[i];
        TypeBinding thatBinding = thatMethod.parameters[i];
        if (!new String(thisBinding.signature()).equals(new String(thatBinding.signature()))) {
          return false;
        }
      }
      return true;
    }

    private String typeString(TypeBinding type) {
      return String.valueOf(type.shortReadableName());
    }
  }

  private static final char[] INIT_CTOR_CHARS = "<init>".toCharArray();

  private static final char[][] UNSAFE_LONG_ANNOTATION_CHARS = CharOperation.splitOn(
      '.', UnsafeNativeLong.class.getName().toCharArray());

  /**
   * Resolve JSNI references in an entire
   * {@link org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration}.
   */
  public static void resolve(CompilationUnitDeclaration cud,
      List<ImportReference> cudOriginalImports,
      Map<MethodDeclaration, JsniMethod> jsniMethods,
      Map<String, Binding> jsniRefs, TypeResolver typeResolver) {
    new JsniReferenceResolver(cud, cudOriginalImports, typeResolver, jsniMethods, jsniRefs)
        .resolve();
  }

  private final CompilationUnitDeclaration cud;
  private final List<ImportReference> cudImports;
  private final Map<MethodDeclaration, JsniMethod> jsniMethods;
  private final Map<String, Binding> jsniRefs;
  private final Stack<Set<String>> suppressWarningsStack = new Stack<Set<String>>();
  private final TypeResolver typeResolver;

  private JsniReferenceResolver(CompilationUnitDeclaration cud, List<ImportReference> cudImports,
      TypeResolver typeResolver, Map<MethodDeclaration, JsniMethod> jsniMethods,
      Map<String, Binding> jsniRefs) {
    this.cud = cud;
    this.cudImports = cudImports;
    this.typeResolver = typeResolver;
    this.jsniMethods = jsniMethods;
    this.jsniRefs = jsniRefs;
  }

  private void resolve() {
    // First resolve the declarations.
    cud.traverse(new JsniDeclChecker(), cud.scope);
  }

  /**
   * Check whether the argument type is the <code>long</code> primitive type. If
   * the argument is <code>null</code>, returns <code>false</code>.
   */
  private boolean containsLong(TypeBinding type) {
    if (!(type instanceof BaseTypeBinding)) {
      return false;
    }

    BaseTypeBinding btb = (BaseTypeBinding) type;
    if (btb.id == TypeIds.T_long) {
        return true;
    }

    return false;
  }

  private boolean hasUnsafeLongsAnnotation(MethodDeclaration meth,
      ClassScope scope) {
    if (meth.annotations == null) {
      return false;
    }

    for (Annotation annot : meth.annotations) {
      if (isUnsafeLongAnnotation(annot, scope)) {
        return true;
      }
    }
    return false;
  }

  private boolean isUnsafeLongAnnotation(Annotation annot, ClassScope scope) {
    if (annot.type == null) {
      return false;
    }

    TypeBinding resolved = annot.type.resolveType(scope);
    if (resolved == null || !(resolved instanceof ReferenceBinding)) {
      return false;
    }

    ReferenceBinding rb = (ReferenceBinding) resolved;
    if (CharOperation.equals(rb.compoundName, UNSAFE_LONG_ANNOTATION_CHARS)) {
      return true;
    }
    return false;
  }

  private void longAccessError(ASTNode node, String message) {
    GWTProblem.recordError(node, cud, message, new InstalledHelpInfo(
        "longJsniRestriction.html"));
  }

  private static void resolveJsniRef(JsniRef jsniRef, FieldBinding fieldBinding) {
    if (fieldBinding  == null) {
      return;
    }
    jsniRef.setResolvedClassName(JdtUtil.getSourceName(fieldBinding.declaringClass));
    jsniRef.setResolvedMemberWithSignature(new String(fieldBinding.name));
  }

  private static void resolveJsniRef(JsniRef jsniRef, MethodBinding methodBinding) {
    if (methodBinding  == null) {
      return;
    }
    ReferenceBinding declaringClassBinding = methodBinding.declaringClass;

    jsniRef.setResolvedClassName(JdtUtil.getSourceName(declaringClassBinding));
    jsniRef.setResolvedMemberWithSignature(JdtUtil.formatMethodSignature(methodBinding));
  }
}
