/*
 * 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.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.dev.javac.asm.CollectAnnotationData;
import com.google.gwt.dev.javac.asm.CollectAnnotationData.AnnotationData;
import com.google.gwt.dev.javac.asm.CollectClassData;
import com.google.gwt.dev.javac.asm.CollectClassData.AnnotationEnum;
import com.google.gwt.dev.javac.asm.CollectFieldData;
import com.google.gwt.dev.javac.asm.CollectMethodData;
import com.google.gwt.dev.javac.asm.CollectTypeParams;
import com.google.gwt.dev.javac.asm.ResolveClassSignature;
import com.google.gwt.dev.javac.asm.ResolveMethodSignature;
import com.google.gwt.dev.javac.asm.ResolveTypeSignature;
import com.google.gwt.dev.javac.typemodel.JAbstractMethod;
import com.google.gwt.dev.javac.typemodel.JArrayType;
import com.google.gwt.dev.javac.typemodel.JClassType;
import com.google.gwt.dev.javac.typemodel.JField;
import com.google.gwt.dev.javac.typemodel.JGenericType;
import com.google.gwt.dev.javac.typemodel.JMethod;
import com.google.gwt.dev.javac.typemodel.JPackage;
import com.google.gwt.dev.javac.typemodel.JParameterizedType;
import com.google.gwt.dev.javac.typemodel.JRawType;
import com.google.gwt.dev.javac.typemodel.JRealClassType;
import com.google.gwt.dev.javac.typemodel.JTypeParameter;
import com.google.gwt.dev.javac.typemodel.JWildcardType;
import com.google.gwt.dev.javac.typemodel.TypeOracle;
import com.google.gwt.dev.javac.typemodel.TypeOracleUpdater;
import com.google.gwt.dev.util.Name;
import com.google.gwt.dev.util.log.perf.AbstractJfrEvent;
import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.collect.Collections2;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Queues;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.util.concurrent.ThreadFactoryBuilder;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.util.TraceClassVisitor;

import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Builds or rebuilds a {@link com.google.gwt.core.ext.typeinfo.TypeOracle} from a set of
 * compilation units.
 */
public class CompilationUnitTypeOracleUpdater extends TypeOracleUpdater {

  /**
   * A container to hold all the information we need to add one type to the TypeOracle.
   */
  static class TypeData {

    private final byte[] byteCode;
    private CollectClassData classData;
    private final String internalName;

    /**
     * A timestamp as returned from {@link System#currentTimeMillis()}
     */
    private final long lastModifiedTime;
    private final String packageName;
    private final String sourceName;

    protected TypeData(String packageName, String sourceName, String internalName, byte[] byteCode,
        long lastModifiedTime) {
      this.packageName = packageName;
      this.sourceName = sourceName;
      this.internalName = internalName;
      this.byteCode = byteCode;
      this.lastModifiedTime = lastModifiedTime;
    }

    /**
     * Collects data about a class which only needs the bytecode and no TypeOracle data structures.
     * This is used to make the initial shallow identity pass for creating
     * JRealClassType/JGenericType objects.
     */
    synchronized CollectClassData getCollectClassData() {
      if (classData == null) {
        ClassReader reader = new ClassReader(byteCode);
        classData = new CollectClassData();
        ClassVisitor classVisitor = classData;
        if (TRACE_CLASSES) {
          classVisitor = new TraceClassVisitor(classVisitor, new PrintWriter(System.out));
        }
        reader.accept(classVisitor, 0);
      }
      return classData;
    }
  }

  private class CompilationUnitTypeOracleResolver implements Resolver {

    private final TypeOracleBuildContext context;

    public CompilationUnitTypeOracleResolver(TypeOracleBuildContext context) {
      this.context = context;
    }

    @Override
    public void addImplementedInterface(JRealClassType type, JClassType intf) {
      CompilationUnitTypeOracleUpdater.this.addImplementedInterface(type, intf);
    }

    @Override
    public void addThrows(JAbstractMethod method, JClassType exception) {
      CompilationUnitTypeOracleUpdater.this.addThrows(method, exception);
    }

    @Override
    public JRealClassType findByInternalName(String internalName) {
      return CompilationUnitTypeOracleUpdater.this.findByInternalName(internalName);
    }

    @Override
    public TypeOracle getTypeOracle() {
      return CompilationUnitTypeOracleUpdater.this.typeOracle;
    }

    @Override
    public JMethod newMethod(JClassType type, String name,
        Map<Class<? extends Annotation>, Annotation> declaredAnnotations,
        JTypeParameter[] typeParams) {
      return CompilationUnitTypeOracleUpdater.this.newMethod(
          type, name, declaredAnnotations, typeParams);
    }

    @Override
    public void newParameter(JAbstractMethod method, JType argType, String argName,
        Map<Class<? extends Annotation>, Annotation> declaredAnnotations, boolean argNamesAreReal) {
      CompilationUnitTypeOracleUpdater.this.newParameter(
          method, argType, argName, declaredAnnotations, argNamesAreReal);
    }

    @Override
    public JRealClassType newRealClassType(JPackage pkg, String enclosingTypeName,
        boolean isLocalType, String simpleSourceName, boolean isInterface) {
      return CompilationUnitTypeOracleUpdater.this.newRealClassType(
          pkg, enclosingTypeName, simpleSourceName, isInterface);
    }

    @Override
    public boolean resolveAnnotations(TreeLogger logger, List<CollectAnnotationData> annotations,
        Map<Class<? extends Annotation>, Annotation> declaredAnnotations) {
      return CompilationUnitTypeOracleUpdater.this.resolveAnnotations(
          logger, annotations, declaredAnnotations);
    }

    @Override
    public boolean resolveClass(TreeLogger logger, JRealClassType type) {
      return CompilationUnitTypeOracleUpdater.this.resolveClass(logger, type, context);
    }

    @Override
    public void setReturnType(JAbstractMethod method, JType returnType) {
      CompilationUnitTypeOracleUpdater.this.setReturnType(method, returnType);
    }

    @Override
    public void setSuperClass(JRealClassType type, JClassType superType) {
      CompilationUnitTypeOracleUpdater.this.setSuperClass(type, superType);
    }
  }

  /**
   * This context keeps common data so we don't have to pass it around between methods for one pass
   * of {@link CompilationUnitTypeOracleUpdater#addNewTypesDontIndex(TreeLogger, Collection,
   * MethodArgNamesLookup)} .
   */
  protected class TypeOracleBuildContext {
    protected final MethodArgNamesLookup allMethodArgs;

    private final Map<String, CollectClassData> classDataByInternalName = Maps.newHashMap();

    private final Map<JRealClassType, CollectClassData> classDataByType = Maps.newHashMap();

    private final Resolver resolver = new CompilationUnitTypeOracleResolver(this);

    protected TypeOracleBuildContext(MethodArgNamesLookup allMethodArgs) {
      this.allMethodArgs = allMethodArgs;
    }
  }

  /**
   * Pairs of bits to convert from ASM Opcodes.* to Shared.* bitfields.
   */
  private static final int[] ASM_TO_SHARED_MODIFIERS =
      new int[] {Opcodes.ACC_PUBLIC, Shared.MOD_PUBLIC, //
          Opcodes.ACC_PRIVATE, Shared.MOD_PRIVATE, //
          Opcodes.ACC_PROTECTED, Shared.MOD_PROTECTED, //
          Opcodes.ACC_STATIC, Shared.MOD_STATIC, //
          Opcodes.ACC_FINAL, Shared.MOD_FINAL, //
          Opcodes.ACC_ABSTRACT, Shared.MOD_ABSTRACT, //
          Opcodes.ACC_VOLATILE, Shared.MOD_VOLATILE, //
          Opcodes.ACC_TRANSIENT, Shared.MOD_TRANSIENT, //
      };

  private static final JTypeParameter[] NO_TYPE_PARAMETERS = new JTypeParameter[0];

  /**
   * Turn on to trace class processing.
   */
  private static final boolean TRACE_CLASSES = false;

  /**
   * Suppress some warnings related to missing valiation.jar on classpath.
   */
  private static boolean warnedMissingValidationJar = false;

  private static JTypeParameter[] collectTypeParams(String signature) {
    if (signature != null) {
      List<JTypeParameter> params = Lists.newArrayList();
      SignatureReader reader = new SignatureReader(signature);
      reader.accept(new CollectTypeParams(params));
      return params.toArray(new JTypeParameter[params.size()]);
    }
    return NO_TYPE_PARAMETERS;
  }

  private static JTypeParameter[] getTypeParametersForClass(CollectClassData classData) {
    JTypeParameter[] typeParams = null;
    if (classData.getSignature() != null) {
      // TODO(jat): do we need to consider generic types w/ method type
      // params for local classes?
      typeParams = collectTypeParams(classData.getSignature());
    }
    return typeParams;
  }

  private static Class<?> getWrapperClass(Class<?> primitiveClass) {
    assert primitiveClass.isPrimitive();
    if (primitiveClass.equals(Integer.TYPE)) {
      return Integer.class;
    } else if (primitiveClass.equals(Boolean.TYPE)) {
      return Boolean.class;
    } else if (primitiveClass.equals(Byte.TYPE)) {
      return Byte.class;
    } else if (primitiveClass.equals(Character.TYPE)) {
      return Character.class;
    } else if (primitiveClass.equals(Short.TYPE)) {
      return Short.class;
    } else if (primitiveClass.equals(Long.TYPE)) {
      return Long.class;
    } else if (primitiveClass.equals(Float.TYPE)) {
      return Float.class;
    } else if (primitiveClass.equals(Double.TYPE)) {
      return Double.class;
    } else {
      throw new IllegalArgumentException(primitiveClass.toString() + " not a primitive class");
    }
  }

  /**
   * Returns whether this name is the special package-info type name.
   */
  private static boolean isPackageInfoTypeName(String simpleSourceName) {
    return "package-info".equals(simpleSourceName);
  }

  /**
   * Returns true if this class is a non-static class inside a generic class.
   */
  // TODO(jat): do we need to consider the entire hierarchy?
  private static boolean nonStaticInsideGeneric(
      CollectClassData classData, CollectClassData enclosingClassData) {
    if (enclosingClassData == null || (classData.getAccess() & Opcodes.ACC_STATIC) != 0) {
      return false;
    }
    return getTypeParametersForClass(enclosingClassData) != null;
  }

  /**
   * Returns the original type or its raw type if it is generic
   */
  private static JType possiblySubstituteRawType(JType type) {
    if (type != null) {
      JGenericType genericType = (JGenericType) type.isGenericType();
      if (genericType != null) {
        type = genericType.getRawType();
      }
    }
    return type;
  }

  private final Set<String> resolvedTypeSourceNames = Sets.newHashSet();
  private final Map<String, JRealClassType> typesByInternalName = Maps.newHashMap();
  /**
   * An executor service to parallelize some of the update process.
   */
  private static ExecutorService executor =
      new ThreadPoolExecutor(0, Runtime.getRuntime().availableProcessors(), 60L, TimeUnit.SECONDS,
          Queues.<Runnable>newLinkedBlockingQueue(),
          // Make sure this executor lets the whole process terminate correctly even if there
          // are still live threads.
          new ThreadFactoryBuilder().setDaemon(true).build());

  public CompilationUnitTypeOracleUpdater(TypeOracle typeOracle) {
    super(typeOracle);
  }

  /**
   * Adds new units to an existing TypeOracle but does not yet index their type hierarchy.<br />
   *
   * It is ok for this function to recursive since no repeated or invalid type indexing will result.
   *
   * @param logger logger to use
   * @param typeDataList collection of data need to build types. (Doesn't retain references to
   *          TypeData instances.)
   * @param argsLookup Allows the caller to pass the method argument names which are not normally
   *          available in bytecode.
   */
  void addNewTypesDontIndex(
      TreeLogger logger, Collection<TypeData> typeDataList, MethodArgNamesLookup argsLookup) {
    final TypeOracleBuildContext context;
    try (TypeOracleUpdateEvent ignored = new TypeOracleUpdateEvent("Visit class files")) {
      // First collect all class data.
      context = getContext(argsLookup);

      for (TypeData typeData : typeDataList) {
        CollectClassData classData = typeData.getCollectClassData();
        // skip any classes that can't be referenced by name outside of
        // their local scope, such as anonymous classes and method-local classes
        if (classData.hasNoExternalName()) {
          continue;
        }
        // skip classes that have been previously added
        if (typesByInternalName.containsKey(classData.getInternalName())) {
          continue;
        }
        context.classDataByInternalName.put(typeData.internalName, classData);
      }
    }

    Set<JRealClassType> unresolvedTypes = Sets.newLinkedHashSet();
    try (TypeOracleUpdateEvent ignored = new TypeOracleUpdateEvent("Establish Identity")) {
      // Perform a shallow pass to establish identity for new and old types.
      for (TypeData typeData : typeDataList) {
        CollectClassData classData = context.classDataByInternalName.get(typeData.internalName);
        if (classData == null) {
          // ignore classes that were skipped earlier
          continue;
        }
        if (typesByInternalName.containsKey(classData.getInternalName())) {
          // skip classes that have been previously added
          continue;
        }
        JRealClassType type = createType(typeData, unresolvedTypes, context);
        if (type != null) {
          assert Name.isInternalName(typeData.internalName);
          typesByInternalName.put(typeData.internalName, type);
          context.classDataByType.put(type, classData);
        }
      }
    }

    TreeLogger branch = logger.branch(TreeLogger.SPAM, "Resolving enclosing classes");
    try (TypeOracleUpdateEvent ignored = new TypeOracleUpdateEvent("Resolve enclosing classes")) {
      // Hook up enclosing types
      for (Iterator<JRealClassType> unresolvedTypesIterator = unresolvedTypes.iterator();
          unresolvedTypesIterator.hasNext();) {
        JRealClassType unresolvedType = unresolvedTypesIterator.next();
        if (!resolveEnclosingClass(branch, unresolvedType, context)) {
          // already logged why it failed, don't try and use it further
          unresolvedTypesIterator.remove();
        }
      }
    }
    try (TypeOracleUpdateEvent ignored = new TypeOracleUpdateEvent("Resolve unresolved types")) {
      // Resolve unresolved types.
      for (JRealClassType unresolvedType : unresolvedTypes) {
        branch =
            logger.branch(TreeLogger.SPAM, "Resolving " + unresolvedType.getQualifiedSourceName());
        if (!resolveClass(branch, unresolvedType, context)) {
          // already logged why it failed.
          // TODO: should we do anything else here?
        }
      }
    }
  }

  private static void prefechTypeData(Collection<TypeData> typeDataList) {
    // Parse bytecode in parallel by calling {@code TypeData.getCollectClassData()} in parallel.
    try {
      executor.<Void>invokeAll(Collections2.transform(typeDataList,
          new Function<TypeData, Callable<Void>>() {
            @Override
            public Callable<Void> apply(final TypeData typeData) {
              return new Callable<Void>() {
                @Override
                public Void call() {
                  typeData.getCollectClassData();
                  return null;
                }
              };
            }
          }));
    } catch (InterruptedException e) {
      // InterruptedException can be safely ignored here as the threads interrupted are only
      // precomputing data in parallel that will otherwise be computed later sequentially if
      // the threads are aborted.
      // Anyway restore the thread interrupted state just in case.
      Thread.currentThread().interrupt();
    }
  }

  /**
   * Adds new units to an existing TypeOracle and indexes their type hierarchy.
   */
  public void addNewUnits(TreeLogger logger, Collection<CompilationUnit> compilationUnits) {
    addNewTypesDontIndex(logger, compilationUnits);
    indexTypes();
  }

  @VisibleForTesting
  void indexTypes() {
    try (TypeOracleUpdateEvent ignored = new TypeOracleUpdateEvent("Finish")) {
      super.finish();
    }
  }

  @jdk.jfr.Name("gwt.compiler.java.TypeOracleUpdate")
  public static class TypeOracleUpdateEvent extends AbstractJfrEvent {
    @jdk.jfr.Label("Phase")
    String phase;

    public TypeOracleUpdateEvent(String phase) {
      this.phase = phase;
    }
  }

  protected void addNewTypesDontIndex(TreeLogger logger,
      Collection<CompilationUnit> compilationUnits) {
    Collection<TypeData> typeDataList = Lists.newArrayList();

    // Create method args data for types to add
    MethodArgNamesLookup argsLookup = new MethodArgNamesLookup();
    for (CompilationUnit compilationUnit : compilationUnits) {
      argsLookup.mergeFrom(compilationUnit.getMethodArgs());
    }

    // Create list including byte code for each type to add
    // TODO(rluble): this process can be done in parallel. Probably best to merge this for
    // loop with the one in prefetchTypeData.
    for (CompilationUnit compilationUnit : compilationUnits) {
      for (CompiledClass compiledClass : compilationUnit.getCompiledClasses()) {
        TypeData typeData = new TypeData(compiledClass.getPackageName(),
            compiledClass.getSourceName(), compiledClass.getInternalName(),
            compiledClass.getBytes(), compiledClass.getUnit().getLastModified());
        typeDataList.add(typeData);
      }
    }

    prefechTypeData(typeDataList);

    // Add the new types to the type oracle build in progress.
    addNewTypesDontIndex(logger, typeDataList, argsLookup);
  }

  @VisibleForTesting
  public Resolver getMockResolver() {
    return new CompilationUnitTypeOracleResolver(
        new TypeOracleBuildContext(new MethodArgNamesLookup()));
  }

  public TypeOracle getTypeOracle() {
    return typeOracle;
  }

  @VisibleForTesting
  public Map<String, JRealClassType> getTypesByInternalName() {
    return typesByInternalName;
  }

  /**
   * Returns the type corresponding to the given internal name.<br />
   *
   * Implementations are free to service requests eagerly or lazily.
   */
  protected JRealClassType findByInternalName(String internalName) {
    assert Name.isInternalName(internalName);
    return typesByInternalName.get(internalName);
  }

  private Annotation createAnnotation(TreeLogger logger,
      Class<? extends Annotation> annotationClass, AnnotationData annotationData) {
    // Make a copy before we mutate the collection.
    Map<String, Object> values = Maps.newHashMap(annotationData.getValues());
    logger =
        logger.branch(TreeLogger.TRACE, "Resolving annotation for " + annotationClass.getName());
    for (Map.Entry<String, Object> entry : values.entrySet()) {
      Method method = null;
      Throwable caught = null;
      try {
        method = annotationClass.getMethod(entry.getKey());
        entry.setValue(resolveAnnotationValue(logger, method.getReturnType(), entry.getValue()));
      } catch (SecurityException e) {
        caught = e;
      } catch (NoSuchMethodException e) {
        caught = e;
      }
      if (caught != null) {
        logger.log(TreeLogger.WARN,
            "Exception resolving " + annotationClass.getCanonicalName() + "." + entry.getKey() +
            " : " + caught.getMessage());
        return null;
      }
    }
    return AnnotationProxyFactory.create(annotationClass, values);
  }

  /**
   * Doesn't retain a reference to the TypeData.
   */
  private JRealClassType createType(
      TypeData typeData, CollectClassData collectClassData, CollectClassData enclosingClassData) {
    int access = collectClassData.getAccess();
    String simpleName = Shared.getShortName(typeData.sourceName);
    JRealClassType type = null;
    String packageName = typeData.packageName;
    JPackage pkg = typeOracle.getOrCreatePackage(packageName);
    boolean isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
    assert !collectClassData.hasNoExternalName();
    String enclosingSimpleName = null;
    if (enclosingClassData != null) {
      enclosingSimpleName = enclosingClassData.getNestedSourceName();
    }
    if ((access & Opcodes.ACC_ANNOTATION) != 0) {
      type = newAnnotationType(pkg, enclosingSimpleName, simpleName);
    } else if ((access & Opcodes.ACC_ENUM) != 0) {
      type = newEnumType(pkg, enclosingSimpleName, simpleName);
    } else {
      JTypeParameter[] typeParams = getTypeParametersForClass(collectClassData);
      if ((typeParams != null && typeParams.length > 0)
          || nonStaticInsideGeneric(collectClassData, enclosingClassData)) {
        type = new JGenericType(
            typeOracle, pkg, enclosingSimpleName, simpleName, isInterface, typeParams);
      } else {
        type = newRealClassType(pkg, enclosingSimpleName, simpleName, isInterface);
      }
    }

    type.addModifierBits(mapBits(ASM_TO_SHARED_MODIFIERS, access));
    if (isInterface) {
      // Always add implicit modifiers on interfaces.
      type.addModifierBits(Shared.MOD_STATIC | Shared.MOD_ABSTRACT);
    }
    type.addLastModifiedTime(typeData.lastModifiedTime);

    return type;
  }

  /**
   * Doesn't retain a reference to the TypeData.
   */
  private JRealClassType createType(
      TypeData typeData, Set<JRealClassType> unresolvedTypes, TypeOracleBuildContext context) {
    CollectClassData classData = context.classDataByInternalName.get(typeData.internalName);
    String enclosingClassInternalName = classData.getEnclosingInternalName();
    CollectClassData enclosingClassData = null;
    if (enclosingClassInternalName != null) {
      enclosingClassData = context.classDataByInternalName.get(enclosingClassInternalName);
      if (enclosingClassData == null) {
        // if our enclosing class was skipped, skip this one too
        return null;
      }
    }
    JRealClassType realClassType = createType(typeData, classData, enclosingClassData);
    unresolvedTypes.add(realClassType);
    return realClassType;
  }

  private Class<? extends Annotation> getAnnotationClass(
      TreeLogger logger, AnnotationData annotationData) {
    Type type = Type.getType(annotationData.getDesc());
    String binaryName = type.getClassName();
    try {
      Class<?> clazz =
          Class.forName(binaryName, false, Thread.currentThread().getContextClassLoader());
      if (!Annotation.class.isAssignableFrom(clazz)) {
        logger.log(TreeLogger.ERROR, "Type " + binaryName + " is not an annotation");
        return null;
      }
      return clazz.asSubclass(Annotation.class);
    } catch (ClassNotFoundException e) {
      TreeLogger.Type level = TreeLogger.WARN;
      if (shouldSuppressUnresolvableAnnotation(logger, binaryName)) {
        level = TreeLogger.DEBUG;
      }
      logger.log(level, "Ignoring unresolvable annotation type " + binaryName);
      return null;
    }
  }

  @SuppressWarnings("unused")
  private Class<?> getClassLiteralForPrimitive(Type type) {
    switch (type.getSort()) {
      case Type.BOOLEAN:
        return Boolean.TYPE;
      case Type.BYTE:
        return Byte.TYPE;
      case Type.CHAR:
        return Character.TYPE;
      case Type.SHORT:
        return Short.TYPE;
      case Type.INT:
        return Integer.TYPE;
      case Type.LONG:
        return Long.TYPE;
      case Type.FLOAT:
        return Float.TYPE;
      case Type.DOUBLE:
        return Double.TYPE;
      case Type.VOID:
        return Void.TYPE;
      default:
        assert false : "Unexpected primitive type " + type;
        return null;
    }
  }

  /**
   * Returns a new build context to use for the duration of one addNewTypesDontIndex() invocation.
   */
  protected TypeOracleBuildContext getContext(MethodArgNamesLookup argsLookup) {
    return new TypeOracleBuildContext(argsLookup);
  }

  /**
   * Map a bitset onto a different bitset.
   *
   * @param mapping int array containing a sequence of from/to pairs, each from entry should have
   *          exactly one bit set
   * @param input bitset to map
   * @return mapped bitset
   */
  private int mapBits(int[] mapping, int input) {
    int output = 0;
    for (int i = 0; i < mapping.length; i += 2) {
      if ((input & mapping[i]) != 0) {
        output |= mapping[i + 1];
      }
    }
    return output;
  }

  private boolean resolveAnnotation(TreeLogger logger, CollectAnnotationData annotationVisitor,
      Map<Class<? extends Annotation>, Annotation> declaredAnnotations) {
    AnnotationData annotationData = annotationVisitor.getAnnotation();
    Class<? extends Annotation> annotationClass = getAnnotationClass(logger, annotationData);
    if (annotationClass == null) {
      return false;
    }
    Annotation annotation = createAnnotation(logger, annotationClass, annotationData);
    if (annotation == null) {
      return false;
    }
    declaredAnnotations.put(annotationClass, annotation);
    return true;
  }

  private boolean resolveAnnotations(TreeLogger logger,
      List<CollectAnnotationData> annotationVisitors,
      Map<Class<? extends Annotation>, Annotation> declaredAnnotations) {
    boolean succeeded = true;
    if (annotationVisitors != null) {
      for (CollectAnnotationData annotationVisitor : annotationVisitors) {
        succeeded &= resolveAnnotation(logger, annotationVisitor, declaredAnnotations);
      }
    }
    return succeeded;
  }

  @SuppressWarnings("unchecked")
  private Object resolveAnnotationValue(TreeLogger logger, Class<?> expectedType, Object value) {
    if (expectedType.isArray()) {
      Class<?> componentType = expectedType.getComponentType();
      if (!value.getClass().isArray()) {
        logger.log(TreeLogger.WARN, "Annotation error: expected array of "
            + componentType.getCanonicalName() + ", got " + value.getClass().getCanonicalName());
        return null;
      }
      if (componentType.isPrimitive()) {
        // primitive arrays are already resolvedTypes
        return value;
      }
      // resolve each element in the array
      int n = Array.getLength(value);
      Object newArray = Array.newInstance(componentType, n);
      for (int i = 0; i < n; ++i) {
        Object valueElement = Array.get(value, i);
        Object resolvedValue = resolveAnnotationValue(logger, componentType, valueElement);
        if (resolvedValue == null || !componentType.isAssignableFrom(resolvedValue.getClass())) {
          logger.log(TreeLogger.ERROR,
              "Annotation error: expected " + componentType + ", got " + resolvedValue);
        } else {
          Array.set(newArray, i, resolvedValue);
        }
      }
      return newArray;
    } else if (expectedType.isEnum()) {
      if (!(value instanceof AnnotationEnum)) {
        logger.log(
            TreeLogger.ERROR, "Annotation error: expected an enum value," + " but got " + value);
        return null;
      }
      AnnotationEnum annotEnum = (AnnotationEnum) value;
      Class<? extends Enum> enumType = expectedType.asSubclass(Enum.class);
      try {
        return Enum.valueOf(enumType, annotEnum.getValue());
      } catch (IllegalArgumentException e) {
        logger.log(TreeLogger.WARN, "Unable to resolve annotation value '" + annotEnum.getValue()
            + "' within enum type '" + enumType.getName() + "'");
        return null;
      }
    } else if (Annotation.class.isAssignableFrom(expectedType)) {
      if (!(value instanceof AnnotationData)) {
        logger.log(TreeLogger.WARN, "Annotation error: expected annotation type "
            + expectedType.getCanonicalName() + ", got " + value.getClass().getCanonicalName());
        return null;
      }
      AnnotationData annotData = (AnnotationData) value;
      Class<? extends Annotation> annotationClass = getAnnotationClass(logger, annotData);
      if (!expectedType.isAssignableFrom(annotationClass)) {
        logger.log(TreeLogger.WARN, "Annotation error: expected " + expectedType.getCanonicalName()
            + ", got " + annotationClass.getCanonicalName());
        return null;
      }
      return createAnnotation(logger, annotationClass, annotData);
    } else if (expectedType.isPrimitive()) {
      Class<?> wrapper = getWrapperClass(expectedType);
      return wrapper.cast(value);
    } else {
      if (expectedType.isAssignableFrom(value.getClass())) {
        return value;
      }
      if (Class.class.equals(expectedType)) {
        if (!(value instanceof Type)) {
          logger.log(TreeLogger.WARN,
              "Annotation error: expected a class " + "literal, but received " + value);
          return null;
        }
        Type valueType = (Type) value;
        // See if we can use a binary only class here
        try {
          return forName(valueType.getClassName());
        } catch (ClassNotFoundException e) {
          logger.log(
              TreeLogger.WARN, "Annotation error: cannot resolve " + valueType.getClassName());
          return null;
        }
      }
      // TODO(jat) asserts about other acceptable types
      return value;
    }
  }

  private static final Map<String, Class<?>> BUILT_IN_PRIMITIVE_MAP;

  static {
    ImmutableMap.Builder<String, Class<?>> builder = ImmutableMap.<String, Class<?>>builder()
        .put("Z",  boolean.class)
        .put("B", byte.class)
        .put("C", char.class)
        .put("S", short.class)
        .put("I", int.class)
        .put("F", float.class)
        .put("D", double.class)
        .put("J", long.class)
        .put("V", void.class);

    for (Class c : new Class[] { void.class, boolean.class, byte.class, char.class,
        short.class, int.class, float.class, double.class, long.class }) {
      builder.put(c.getName(), c);
    }
    BUILT_IN_PRIMITIVE_MAP = builder.build();
  }

  public static Class forName(String name) throws ClassNotFoundException {
    Class c = BUILT_IN_PRIMITIVE_MAP.get(name);
    if (c == null) {
      c = Class.forName(name, false, Thread.currentThread().getContextClassLoader());
    }
    return c;
  }

  private JType resolveArray(Type type) {
    assert type.getSort() == Type.ARRAY;
    JType resolvedType = resolveType(type.getElementType());
    int dimensions = type.getDimensions();
    for (int i = 0; i < dimensions; ++i) {
      resolvedType = typeOracle.getArrayType(resolvedType);
    }
    return resolvedType;
  }

  // TODO(stalcup): refactor this recursive resolveFoo() process. At the moment there is a recursive
  // tree of resolveFoo() calls. Most of them return a boolean, but some do not. Some of the
  // booleans are read and acted upon, some are not. Some functions do their own logging and some
  // return a false to indicate that the caller should log. Sometimes the boolean return value is an
  // indication of whether logging should occur and other times it's an indication of whether
  // exploration should continue. It's a mess. Some ideas that would probably make this more sane:
  // be consistent about SPAM logging throughout and WARN logging only at the tip, and process types
  // in a queue instead of with recursion.
  private boolean resolveClass(
      TreeLogger logger, JRealClassType unresolvedType, TypeOracleBuildContext context) {
    assert unresolvedType != null;
    // Avoid cycles and useless computation.
    if (resolvedTypeSourceNames.contains(unresolvedType.getQualifiedSourceName())) {
      return true;
    }
    resolvedTypeSourceNames.add(unresolvedType.getQualifiedSourceName());

    // Make sure our enclosing type is resolved first.
    if (unresolvedType.getEnclosingType() != null
        && !resolveClass(logger, unresolvedType.getEnclosingType(), context)) {
      return false;
    }

    // Build a search list for type parameters to find their definition,
    // resolving enclosing classes as we go up.
    TypeParameterLookup typeParamLookup = new TypeParameterLookup();
    typeParamLookup.pushEnclosingScopes(unresolvedType);

    CollectClassData classData = context.classDataByType.get(unresolvedType);
    assert classData != null;
    int access = classData.getAccess();

    assert (!classData.getClassType().hasNoExternalName());

    logger = logger.branch(
        TreeLogger.SPAM, "Found type '" + unresolvedType.getQualifiedSourceName() + "'", null);

    // Handle package-info classes.
    if (isPackageInfoTypeName(unresolvedType.getSimpleSourceName())) {
      return resolvePackage(logger, unresolvedType, classData.getAnnotations());
    }

    // Resolve annotations
    Map<Class<? extends Annotation>, Annotation> declaredAnnotations = Maps.newHashMap();
    resolveAnnotations(logger, classData.getAnnotations(), declaredAnnotations);
    addAnnotations(unresolvedType, declaredAnnotations);

    String signature = classData.getSignature();

    /*
     * Note: Byte code from the OpenJDK compiler doesn't contain a type signature for non-static
     * inner classes of parameterized types that do not contain new parameters (but JDT compiled
     * byte code does). That can cause some differences in the way generic types are represented in
     * the type oracle.
     *
     * These differences also show up when using java.lang.reflect to look at types.
     */
    boolean isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
    if (signature != null) {
      // If we have a signature, use it for superclass and interfaces
      SignatureReader reader = new SignatureReader(signature);
      ResolveClassSignature classResolver = new ResolveClassSignature(
          context.resolver, logger, unresolvedType, typeParamLookup);
      reader.accept(classResolver);
      classResolver.finish();

      if (unresolvedType.getSuperclass() != null
          && !resolveClass(logger, unresolvedType.getSuperclass(), context)) {
        logger.log(TreeLogger.WARN,
            "Unable to resolve supertype " + unresolvedType.getSuperclass().getName());
        return false;
      }
    } else {
      // Set the super type for non-interfaces
      if (!isInterface) {
        String superInternalName = classData.getSuperInternalName();
        assert Name.isInternalName(superInternalName);
        if (superInternalName != null) {
          JClassType superType = findByInternalName(superInternalName);
          if (superType == null || !resolveClass(logger, superType, context)) {
            logger.log(TreeLogger.WARN, "Unable to resolve supertype " + superInternalName);
            return false;
          }
          setSuperClass(unresolvedType, (JClassType) possiblySubstituteRawType(superType));
        }
      }

      // Set interfaces
      for (String interfaceInternalName : classData.getInterfaceInternalNames()) {
        JClassType interfaceType = findByInternalName(interfaceInternalName);
        if (interfaceType == null || !resolveClass(logger, interfaceType, context)) {
          logger.log(TreeLogger.WARN, "Unable to resolve interface " + interfaceInternalName);
          return false;
        }
        addImplementedInterface(
            unresolvedType, (JClassType) possiblySubstituteRawType(interfaceType));
      }
    }
    if (!isInterface && unresolvedType.getSuperclass() == null) {
      // Only Object or interfaces should not have a superclass
      assert "java/lang/Object".equals(classData.getInternalName());
    }

    // Process methods
    for (CollectMethodData method : classData.getMethods()) {
      TreeLogger branch = logger.branch(TreeLogger.SPAM, "Resolving method " + method.getName());
      // TODO(rluble): Allow the users to ask for Java 8 features. For now these are filtered out.
      if (isInterface && isJava8InterfaceMethod(method)) {
        logger.log(TreeLogger.Type.SPAM, "Ignoring Java 8 interface method " + method.getName());
        continue;
      }
      if (!resolveMethod(branch, unresolvedType, method, typeParamLookup, context)) {
        // Already logged.
        return false;
      }
    }

    // Process fields
    // Track the next enum ordinal across resolveField calls.
    int[] nextEnumOrdinal = new int[] {0};
    for (CollectFieldData field : classData.getFields()) {
      TreeLogger branch = logger.branch(TreeLogger.SPAM, "Resolving field " + field.getName());
      if (!resolveField(branch, unresolvedType, field, typeParamLookup, nextEnumOrdinal, context)) {
        // Already logged.
        return false;
      }
    }
    return true;
  }

  private boolean isJava8InterfaceMethod(CollectMethodData method) {
    // (Normal) interface methods are abstract. Java 8 introduced the ability to declare default
    // methods and static methods both of which are exposed as non abstract methods.
    return (method.getAccess() & Opcodes.ACC_ABSTRACT) == 0;
  }

  private boolean resolveClass(
      TreeLogger logger, JType unresolvedType, TypeOracleBuildContext context) {
    if (!(unresolvedType instanceof JClassType)) {
      // non-classes are already resolvedTypes
      return true;
    }
    if (unresolvedType instanceof JRealClassType) {
      return resolveClass(logger, (JRealClassType) unresolvedType, context);
    }
    if (unresolvedType instanceof JArrayType) {
      return resolveClass(logger, ((JArrayType) unresolvedType).getComponentType(), context);
    }
    if (unresolvedType instanceof JParameterizedType) {
      return resolveClass(logger, ((JParameterizedType) unresolvedType).getBaseType(), context);
    }
    if (unresolvedType instanceof JRawType) {
      return resolveClass(logger, ((JRawType) unresolvedType).getBaseType(), context);
    }
    if (unresolvedType instanceof JTypeParameter) {
      JTypeParameter typeParam = (JTypeParameter) unresolvedType;
      if (!resolveClass(logger, typeParam.getDeclaringClass(), context)) {
        return false;
      }
      for (JClassType bound : typeParam.getBounds()) {
        if (!resolveClass(logger, bound, context)) {
          return false;
        }
      }
      return true;
    }
    if (unresolvedType instanceof JWildcardType) {
      JWildcardType wildcard = (JWildcardType) unresolvedType;
      for (JClassType bound : wildcard.getUpperBounds()) {
        if (!resolveClass(logger, bound, context)) {
          return false;
        }
      }
      for (JClassType bound : wildcard.getLowerBounds()) {
        if (!resolveClass(logger, bound, context)) {
          return false;
        }
      }
      return true;
    }
    return false;
  }

  private boolean resolveEnclosingClass(
      TreeLogger logger, JRealClassType unresolvedType, TypeOracleBuildContext context) {
    assert unresolvedType != null;
    if (unresolvedType.getEnclosingType() != null) {
      return true;
    }

    // Find our enclosing class and set it
    CollectClassData classData = context.classDataByType.get(unresolvedType);
    assert classData != null;
    String enclosingClassInternalName = classData.getEnclosingInternalName();
    JRealClassType enclosingType = null;
    if (enclosingClassInternalName != null) {
      enclosingType = findByInternalName(enclosingClassInternalName);
      // Ensure enclosing classes are resolved
      if (enclosingType != null) {
        if (!resolveEnclosingClass(logger, enclosingType, context)) {
          return false;
        }
        if (enclosingType.isGenericType() != null
            && (classData.getAccess() & (Opcodes.ACC_STATIC | Opcodes.ACC_INTERFACE)) != 0) {
          // If the inner class doesn't have access to it's enclosing type's
          // type variables, the enclosing type must be the raw type instead
          // of the generic type.
          JGenericType genericType = enclosingType.isGenericType();
          setEnclosingType(unresolvedType, genericType.getRawType());
        } else {
          setEnclosingType(unresolvedType, enclosingType);
        }
      }
    }
    return true;
  }

  private boolean resolveField(TreeLogger logger, JRealClassType unresolvedType,
      CollectFieldData field, TypeParameterLookup typeParamLookup, int[] nextEnumOrdinal,
      TypeOracleBuildContext context) {
    Map<Class<? extends Annotation>, Annotation> declaredAnnotations = Maps.newHashMap();
    resolveAnnotations(logger, field.getAnnotations(), declaredAnnotations);
    String name = field.getName();
    JField jfield;
    if ((field.getAccess() & Opcodes.ACC_ENUM) != 0) {
      assert (unresolvedType.isEnum() != null);
      jfield = newEnumConstant(unresolvedType, name, declaredAnnotations, nextEnumOrdinal[0]++);
    } else {
      JField newField = newField(unresolvedType, name, declaredAnnotations);
      jfield = newField;
    }

    // Get modifiers.
    addModifierBits(jfield, mapBits(ASM_TO_SHARED_MODIFIERS, field.getAccess()));

    String signature = field.getSignature();
    JType fieldJType;
    if (signature != null) {
      SignatureReader reader = new SignatureReader(signature);
      JType[] fieldTypeRef = new JType[1];
      reader.acceptType(new ResolveTypeSignature(
          context.resolver, logger, fieldTypeRef, typeParamLookup, null));
      fieldJType = fieldTypeRef[0];
      if (fieldJType == null) {
        logger.log(TreeLogger.ERROR, "Unable to resolve type in field signature " + signature);
        return false;
      }
    } else {
      Type fieldType = Type.getType(field.getDesc());
      fieldJType = resolveType(fieldType);
      if (fieldJType == null) {
        logger.log(TreeLogger.ERROR, "Unable to resolve type " + fieldType.getInternalName()
            + " of field " + field.getName());
        return false;
      }
    }
    setFieldType(jfield, fieldJType);
    return true;
  }

  private boolean resolveMethod(TreeLogger logger, JRealClassType unresolvedType,
      CollectMethodData methodData, TypeParameterLookup typeParamLookup,
      TypeOracleBuildContext context) {
    Map<Class<? extends Annotation>, Annotation> declaredAnnotations = Maps.newHashMap();
    resolveAnnotations(logger, methodData.getAnnotations(), declaredAnnotations);
    String name = methodData.getName();

    if ("<clinit>".equals(name) || (methodData.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
      // Ignore the following and leave them out of TypeOracle:
      // - static initializers
      // - synthetic methods
      return true;
    }

    if (unresolvedType.isEnum() != null && "<init>".equals(name)) {
      // Leave enum constructors out of TypeOracle
      return true;
    }

    JAbstractMethod method;

    // Declare the type parameters. We will pass them into the constructors for
    // JConstructor/JMethod/JAnnotatedMethod. Then, we'll do a second pass to
    // resolve the bounds on each JTypeParameter object.
    JTypeParameter[] typeParams = collectTypeParams(methodData.getSignature());

    typeParamLookup.pushScope(typeParams);
    boolean hasReturnType = true;
    if ("<init>".equals(name)) {
      name = unresolvedType.getSimpleSourceName();
      method = newConstructor(unresolvedType, name, declaredAnnotations, typeParams);
      hasReturnType = false;
    } else {
      if (unresolvedType.isAnnotation() != null) {
        // TODO(jat): actually resolve the default annotation value.
        method = newAnnotationMethod(unresolvedType, name, declaredAnnotations, typeParams, null);
      } else {
        method = newMethod(unresolvedType, name, declaredAnnotations, typeParams);
      }
    }

    addModifierBits(method, mapBits(ASM_TO_SHARED_MODIFIERS, methodData.getAccess()));
    if (unresolvedType.isInterface() != null) {
      // Always add implicit modifiers on interface methods.
      addModifierBits(method, Shared.MOD_PUBLIC | Shared.MOD_ABSTRACT);
    }

    if ((methodData.getAccess() & Opcodes.ACC_VARARGS) != 0) {
      setVarArgs(method);
    }

    String signature = methodData.getSignature();
    if (signature != null) {
      // If we have a signature, use it for superclass and interfaces
      SignatureReader reader = new SignatureReader(signature);
      ResolveMethodSignature methodResolver = new ResolveMethodSignature(context.resolver, logger,
          method, typeParamLookup, hasReturnType, methodData, methodData.getArgTypes(),
          methodData.getArgNames(), methodData.hasActualArgNames(), context.allMethodArgs);
      reader.accept(methodResolver);
      if (!methodResolver.finish()) {
        logger.log(TreeLogger.ERROR, "Failed to resolve.");
        return false;
      }
    } else {
      if (hasReturnType) {
        Type returnType = Type.getReturnType(methodData.getDesc());
        JType returnJType = resolveType(returnType);
        if (returnJType == null) {
          logger.log(TreeLogger.ERROR,
              "Unable to resolve return type " + returnType.getInternalName());
          return false;
        }
        setReturnType(method, returnJType);
      }

      if (!resolveParameters(logger, method, methodData, context)) {
        // Already logged.
        return false;
      }
    }
    // The signature might not actually include the exceptions if they don't
    // include a type variable, so resolveThrows is always used (it does
    // nothing if there are already exceptions defined)
    if (!resolveThrows(logger, method, methodData)) {
      // Already logged.
      return false;
    }
    typeParamLookup.popScope();
    return true;
  }

  private JRealClassType resolveObject(Type type) {
    assert type.getSort() == Type.OBJECT;
    String internalName = type.getInternalName();
    assert Name.isInternalName(internalName);
    JRealClassType classType = findByInternalName(internalName);
    return classType;
  }

  private boolean resolvePackage(TreeLogger logger, JRealClassType unresolvedType,
      List<CollectAnnotationData> annotationVisitors) {
    Map<Class<? extends Annotation>, Annotation> declaredAnnotations = Maps.newHashMap();
    resolveAnnotations(logger, annotationVisitors, declaredAnnotations);
    addAnnotations(unresolvedType.getPackage(), declaredAnnotations);
    return true;
  }

  private boolean resolveParameters(TreeLogger logger, JAbstractMethod method,
      CollectMethodData methodData, TypeOracleBuildContext context) {
    Type[] argTypes = methodData.getArgTypes();
    boolean argNamesAreReal = methodData.hasActualArgNames();
    String[] argNames = methodData.getArgNames();
    if (!argNamesAreReal) {
      String[] lookupNames = context.allMethodArgs.lookup(method, methodData);
      if (lookupNames != null) {
        argNames = lookupNames;
        argNamesAreReal = true;
      }
    }
    List<CollectAnnotationData>[] paramAnnot = methodData.getArgAnnotations();
    for (int i = 0; i < argTypes.length; ++i) {
      Type argType = argTypes[i];
      JType argJType = resolveType(argType);
      if (argJType == null) {
        logger.log(TreeLogger.ERROR, "Unable to resolve type " + argType.getInternalName()
            + " of argument " + methodData.getArgNames()[i]);
        return false;
      }
      // Try to resolve annotations, ignore any that fail.
      Map<Class<? extends Annotation>, Annotation> declaredAnnotations = Maps.newHashMap();
      resolveAnnotations(logger, paramAnnot[i], declaredAnnotations);

      newParameter(method, argJType, argNames[i], declaredAnnotations, argNamesAreReal);
    }
    return true;
  }

  private boolean resolveThrows(TreeLogger logger, JAbstractMethod method,
      CollectMethodData methodData) {
    if (method.getThrows().length == 0) {
      for (String exceptionName : methodData.getExceptions()) {
        Type exceptionType = Type.getObjectType(exceptionName);
        JType exceptionJType = resolveType(exceptionType);
        if (exceptionJType == null) {
          logger.log(TreeLogger.ERROR,
              "Unable to resolve type " + exceptionType.getInternalName() + " of thrown exception");
          return false;
        }
        addThrows(method, (JClassType) exceptionJType);
      }
    }
    return true;
  }

  /**
   * Returns a primitive, an array, or a JRealClassType.
   */
  private JType resolveType(Type type) {
    // Check for primitives.
    switch (type.getSort()) {
      case Type.BOOLEAN:
        return JPrimitiveType.BOOLEAN;
      case Type.BYTE:
        return JPrimitiveType.BYTE;
      case Type.CHAR:
        return JPrimitiveType.CHAR;
      case Type.SHORT:
        return JPrimitiveType.SHORT;
      case Type.INT:
        return JPrimitiveType.INT;
      case Type.LONG:
        return JPrimitiveType.LONG;
      case Type.FLOAT:
        return JPrimitiveType.FLOAT;
      case Type.DOUBLE:
        return JPrimitiveType.DOUBLE;
      case Type.VOID:
        return JPrimitiveType.VOID;
      case Type.ARRAY:
        return resolveArray(type);
      case Type.OBJECT:
        JRealClassType resolvedType = resolveObject(type);
        return possiblySubstituteRawType(resolvedType);
      default:
        assert false : "Unexpected type " + type;
        return null;
    }
  }

  /**
   * Suppress multiple validation related messages and replace with a hint.
   */
  // TODO(zundel): Can be removed when javax.validation is included in the JRE
  private boolean shouldSuppressUnresolvableAnnotation(TreeLogger logger, String sourceName) {
    if (sourceName.startsWith("javax.validation.")
        || sourceName.startsWith("com.google.gwt.validation.")) {
      if (!warnedMissingValidationJar) {
        warnedMissingValidationJar = true;
        logger.log(TreeLogger.WARN, "Detected warnings related to '" + sourceName + "'. "
            + "  Is validation-<version>.jar on the classpath?");
        logger.log(TreeLogger.INFO, "Specify -logLevel DEBUG to see all errors.");
        // Show the first error that matches
        return false;
      }
      // Suppress subsequent errors that match
      return true;
    }
    return false;
  }
}
