// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.android.desugar;

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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.devtools.build.android.desugar.io.BitFlags;
import com.google.devtools.build.android.desugar.langmodel.ClassName;
import com.google.devtools.build.android.r8.DependencyCollector;
import org.objectweb.asm.Opcodes;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

/**
 * Fixer of classes that extend interfaces with default methods to declare any missing methods
 * explicitly and call the corresponding companion method generated by {@link InterfaceDesugaring}.
 */
public class DefaultMethodClassFixer extends ClassVisitor {

  private final boolean useGeneratedBaseClasses;
  private final ClassReaderFactory classpath;
  private final ClassReaderFactory bootclasspath;
  private final ClassLoader targetLoader;
  private final DependencyCollector depsCollector;
  @Nullable private final CoreLibrarySupport coreLibrarySupport;
  private final LinkedHashSet<String> instanceMethods = new LinkedHashSet<>();

  private boolean isInterface;
  private String internalName;
  private ImmutableList<String> directInterfaces;
  private String superName;
  /** Interface whose companion was chosen as base class, if any. */
  @Nullable private Class<?> newSuperName;
  /** This method node caches <clinit>, and flushes out in {@code visitEnd()}; */
  private MethodNode clInitMethodNode;

  private ImmutableSortedSet<Class<?>> interfacesToStub;

  public DefaultMethodClassFixer(
      ClassVisitor dest,
      boolean useGeneratedBaseClasses,
      ClassReaderFactory classpath,
      DependencyCollector depsCollector,
      @Nullable CoreLibrarySupport coreLibrarySupport,
      ClassReaderFactory bootclasspath,
      ClassLoader targetLoader) {
    super(Opcodes.ASM9, dest);
    this.useGeneratedBaseClasses = useGeneratedBaseClasses;
    this.classpath = classpath;
    this.coreLibrarySupport = coreLibrarySupport;
    this.bootclasspath = bootclasspath;
    this.targetLoader = targetLoader;
    this.depsCollector = depsCollector;
  }

  @Override
  public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName,
      String[] interfaces) {
    checkState(this.directInterfaces == null);
    isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
    internalName = name;
    checkArgument(
        superName != null || "java/lang/Object".equals(name), // ASM promises this
        "Type without superclass: %s",
        name);
    this.directInterfaces = ImmutableList.copyOf(interfaces);
    this.superName = superName;

    if (!isInterface
        && (mayNeedInterfaceStubsForEmulatedSuperclass()
            || defaultMethodsDefined(directInterfaces))) {
      TreeMap<Class<?>, Long> allInterfaces = new TreeMap<>(SubtypeComparator.INSTANCE);
      for (String direct : interfaces) {
        if (InterfaceDesugaring.getCompanionClassName(direct).equals(name)) {
          checkState(useGeneratedBaseClasses, "%s shouldn't implement %s", name, direct);
          continue; // InterfaceDesugaring already made stubs for this interface
        }
        // Loading ensures all transitively implemented interfaces can be loaded, which is necessary
        // to produce correct default method stubs in all cases.  We could do without classloading
        // but it's convenient to rely on Class.isAssignableFrom to compute subtype relationships,
        // and we'd still have to insist that all transitively implemented interfaces can be loaded.
        // We don't load the visited class, however, in case it's generated.
        Class<?> itf = loadFromInternal(direct);
        collectInterfaces(itf, allInterfaces);
      }
      if (mayNeedInterfaceStubsForEmulatedSuperclass()) {
        // Collect interfaces inherited from emulated superclasses as well, to handle things like
        // extending AbstractList without explicitly implementing List.
        for (Class<?> clazz = loadFromInternal(superName);
            clazz != null;
            clazz = clazz.getSuperclass()) {
          for (Class<?> itf : clazz.getInterfaces()) {
            collectInterfaces(itf, allInterfaces);
          }
        }
      }

      if (useGeneratedBaseClasses && "java/lang/Object".equals(superName)) {
        // If the class directly extends Object, use the generated base class for the interface with
        // the most default methods instead.  This can help avoid stubbed default methods when
        // they're already defined in the base class.
        // This is an imperfect heuristic, not least b/c we don't know yet what methods would need
        // to be stubbed, but we decide this here up-front b/c that lets us fix up the class's
        // constructors' super calls when we see them.
        // Note inherited default methods are included in the count, so we will choose subtypes over
        // supertypes, which is desirable.
        long maxBaseMethodCount = 0;
        for (Map.Entry<Class<?>, Long> itf : allInterfaces.entrySet()) {
          if (itf.getValue() > maxBaseMethodCount) {
            maxBaseMethodCount = itf.getValue();
            newSuperName = itf.getKey();
            superName = InterfaceDesugaring.getCompanionClassName(internalName(itf.getKey()));
            signature = null; // Changing superclass invalidates signature
          }
        }
        if (newSuperName != null && !bootclasspath.isKnown(internalName(newSuperName))) {
          // Record dependency on the chosen companion class
          depsCollector.assumeCompanionClass(
              internalName, InterfaceDesugaring.getCompanionClassName(internalName(newSuperName)));
        }
      }
      interfacesToStub = ImmutableSortedSet.copyOfSorted(allInterfaces.navigableKeySet());
    } else {
      interfacesToStub = ImmutableSortedSet.of();
    }
    super.visit(version, access, name, signature, superName, interfaces);
  }

  @Override
  public void visitEnd() {
    if (!interfacesToStub.isEmpty()) {
      // Inherited methods take precedence over default methods, so visit all superclasses and
      // figure out what methods they declare before stubbing in any missing default methods.
      recordInheritedMethods();
      stubMissingDefaultAndBridgeMethods();
      // Check whether there are interfaces with default methods and <clinit>. If yes, the following
      // method call will return a list of interface fields to access in the <clinit> to trigger
      // the initialization of these interfaces.
      ImmutableList<String> companionsToTriggerInterfaceClinit =
          collectOrderedCompanionsToTriggerInterfaceClinit(directInterfaces);
      if (!companionsToTriggerInterfaceClinit.isEmpty()) {
        if (clInitMethodNode == null) {
          clInitMethodNode = new MethodNode(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
        }
        desugarClinitToTriggerInterfaceInitializers(companionsToTriggerInterfaceClinit);
      }
    }
    if (clInitMethodNode != null && super.cv != null) { // Write <clinit> to the chained visitor.
      clInitMethodNode.accept(super.cv);
    }
    super.visitEnd();
  }

  private boolean isClinitAlreadyDesugared(
      ImmutableList<String> companionsToAccessToTriggerInterfaceClinit) {
    InsnList instructions = clInitMethodNode.instructions;
    if (instructions.size() <= companionsToAccessToTriggerInterfaceClinit.size()) {
      // The <clinit> must end with RETURN, so if the instruction count is less than or equal to
      // the companion class count, this <clinit> has not been desugared.
      return false;
    }
    Iterator<AbstractInsnNode> iterator = instructions.iterator();
    for (String companion : companionsToAccessToTriggerInterfaceClinit) {
      if (!iterator.hasNext()) {
        return false;
      }
      AbstractInsnNode insn = iterator.next();
      if (!(insn instanceof MethodInsnNode)) {
        return false;
      }
      MethodInsnNode methodInsnNode = (MethodInsnNode) insn;
      if (methodInsnNode.getOpcode() != Opcodes.INVOKESTATIC
          || !methodInsnNode.owner.equals(companion)
          || !methodInsnNode.name.equals(
              InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME)) {
        return false;
      }
      checkState(
          methodInsnNode.desc.equals(
              InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC),
          "Inconsistent method desc: %s vs %s",
          methodInsnNode.desc,
          InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC);
    }
    return true;
  }

  private void desugarClinitToTriggerInterfaceInitializers(
      ImmutableList<String> companionsToTriggerInterfaceClinit) {
    if (isClinitAlreadyDesugared(companionsToTriggerInterfaceClinit)) {
      return;
    }
    InsnList desugarInsts = new InsnList();
    for (String companionClass : companionsToTriggerInterfaceClinit) {
      desugarInsts.add(
          new MethodInsnNode(
              Opcodes.INVOKESTATIC,
              companionClass,
              InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_NAME,
              InterfaceDesugaring.COMPANION_METHOD_TO_TRIGGER_INTERFACE_CLINIT_DESC,
              false));
    }
    if (clInitMethodNode.instructions.size() == 0) {
      clInitMethodNode.instructions.insert(new InsnNode(Opcodes.RETURN));
    }
    clInitMethodNode.instructions.insertBefore(
        clInitMethodNode.instructions.getFirst(), desugarInsts);
  }

  @Override
  public MethodVisitor visitMethod(
      int access, String name, String desc, String signature, String[] exceptions) {
    // Keep track of instance methods implemented in this class for later.
    if (!isInterface) {
      recordIfInstanceMethod(access, name, desc);
    }
    if ("<clinit>".equals(name)) {
      checkState(clInitMethodNode == null, "This class fixer has been used. ");
      clInitMethodNode = new MethodNode(access, name, desc, signature, exceptions);
      return clInitMethodNode;
    }

    MethodVisitor dest = super.visitMethod(access, name, desc, signature, exceptions);
    if (newSuperName != null && "<init>".equals(name)) {
      // Since we changed the base class we need to fix up super constructor calls.
      return new ConstructorFixer(
          dest, InterfaceDesugaring.getCompanionClassName(internalName(newSuperName)));
    }
    return dest;
  }

  private boolean mayNeedInterfaceStubsForEmulatedSuperclass() {
    return coreLibrarySupport != null
        && !coreLibrarySupport.isEmulatedCoreClassOrInterface(internalName)
        && coreLibrarySupport.isEmulatedCoreClassOrInterface(superName);
  }

  private void stubMissingDefaultAndBridgeMethods() {
    Class<?> superclass = loadFromInternal(superName);
    boolean mayNeedStubsForSuperclass = mayNeedInterfaceStubsForEmulatedSuperclass();
    for (Class<?> interfaceToVisit : interfacesToStub) {
      // if J extends I, J is allowed to redefine I's default methods.  The comparator we used
      // above makes sure we visit J before I in that case so we can use J's definition.
      if (!mayNeedStubsForSuperclass && interfaceToVisit.isAssignableFrom(superclass)) {
        // superclass is also rewritten and already implements this interface, so we _must_ skip it.
        continue;
      }
      stubMissingDefaultAndBridgeMethods(internalName(interfaceToVisit), mayNeedStubsForSuperclass);
    }
  }

  private void stubMissingDefaultAndBridgeMethods(
      String implemented, boolean mayNeedStubsForSuperclass) {
    ClassReader bytecode;
    boolean isBootclasspath;
    if (bootclasspath.isKnown(implemented)) {
      if (coreLibrarySupport != null
          && (coreLibrarySupport.isRenamedCoreLibrary(implemented)
              || coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented))) {
        bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
        isBootclasspath = true;
      } else {
        // Default methods from interfaces on the bootclasspath that we're not renaming or emulating
        // are assumed available at runtime, so just ignore them.
        return;
      }
    } else {
      bytecode =
          checkNotNull(
              classpath.readIfKnown(implemented),
              "Couldn't find interface %s implemented by %s",
              implemented,
              internalName);
      isBootclasspath = false;
    }
    bytecode.accept(
        new DefaultMethodStubber(isBootclasspath, mayNeedStubsForSuperclass),
        ClassReader.SKIP_DEBUG);
  }

  private Class<?> loadFromInternal(String internalName) {
    try {
      return targetLoader.loadClass(internalName.replace('/', '.'));
    } catch (ClassNotFoundException | NoClassDefFoundError e) {
      throw new IllegalStateException(
          "Couldn't load " + internalName + " to stub default methods for " + this.internalName, e);
    }
  }

  /** Returns the internal name used for this class in bytecode. */
  static String internalName(Class<?> clazz) {
    return clazz.getName().replace('.', '/');
  }

  private void collectInterfaces(Class<?> itf, Map<Class<?>, Long> dest) {
    checkArgument(itf.isInterface());
    if (dest.containsKey(itf)) {
      return;
    }
    if (useGeneratedBaseClasses && hasCompanionClass(itf)) {
      // Count inherited and declared default methods
      long defaultMethodCount = Arrays.stream(itf.getMethods()).filter(Method::isDefault).count();
      dest.put(itf, defaultMethodCount);
    } else {
      // Use -1 if there's no base class we could use.  This makes sure we don't try to use a
      // companion class as base class that doesn't exist.
      dest.put(itf, -1L);
    }
    for (Class<?> implemented : itf.getInterfaces()) {
      collectInterfaces(implemented, dest);
    }
  }

  private boolean hasCompanionClass(Class<?> itf) {
    checkArgument(itf.isInterface());
    if (Arrays.stream(itf.getDeclaredMethods()).noneMatch(m -> m.isDefault() && !m.isBridge())) {
      return false;
    }
    String implemented = internalName(itf);
    if (implemented.startsWith("java/")
        || implemented.startsWith(ClassName.IN_PROCESS_LABEL + "java/")) {
      return coreLibrarySupport != null
          && (coreLibrarySupport.isRenamedCoreLibrary(implemented)
              || coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented));
    } else {
      return !bootclasspath.isKnown(implemented);
    }
  }

  private void recordInheritedMethods() {
    InstanceMethodRecorder recorder =
        new InstanceMethodRecorder(mayNeedInterfaceStubsForEmulatedSuperclass());
    if (newSuperName != null) {
      checkState(useGeneratedBaseClasses);
      // If we're using a generated base class, walk the implemented interfaces and record default
      // methods we won't need to stub.  That reflects the methods that will be in the generated
      // base class (and its superclasses, if applicable), without looking at the generated class.
      // We go through sub-interfaces before their super-interfaces (same order as when we stub)
      // to encounter overriding before overridden default methods.  We don't record methods from
      // interfaces the chosen base class doesn't implement, even if those methods also appear in
      // interfaces we would normally record later.  That ensures we generate a stub when a default
      // method is available in the base class but needs to be overridden due to an overriding
      // default method in a sub-interface not implemented by the base class.
      LinkedHashSet<String> allSeen = new LinkedHashSet<>();
      for (Class<?> itf : interfacesToStub) {
        boolean willBeInBaseClass = itf.isAssignableFrom(newSuperName);
        for (Method m : itf.getDeclaredMethods()) {
          if (!m.isDefault()) {
            continue;
          }
          String desc = Type.getMethodDescriptor(m);
          if (coreLibrarySupport != null) {
            // Foreshadow any type renaming to avoid issues with double-desugaring (b/111447199)
            desc = coreLibrarySupport.getRemapper().mapMethodDesc(desc);
          }
          String methodKey = m.getName() + ":" + desc;
          if (allSeen.add(methodKey) && willBeInBaseClass) {
            // Only record never-seen methods, and only for super-types of newSuperName (see longer
            // explanation above)
            instanceMethods.add(methodKey);
          }
        }
      }

      // Fall through to the logic below to record j.l.Object's methods.
      checkState(superName.equals("java/lang/Object"));
    }

    // Walk superclasses
    String internalName = superName;
    while (internalName != null) {
      ClassReader bytecode = bootclasspath.readIfKnown(internalName);
      if (bytecode == null) {
        bytecode =
            checkNotNull(
                classpath.readIfKnown(internalName), "Superclass not found: %s", internalName);
      }
      bytecode.accept(recorder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
      internalName = bytecode.getSuperName();
    }
  }

  /**
   * Starting from the given interfaces, this method scans the interface hierarchy, finds the
   * interfaces that have default methods and <clinit>, and returns the companion class names of
   * these interfaces.
   *
   * <p>Note that the returned companion classes are ordered in the order of the interface
   * initialization, which is consistent with the JVM behavior. For example, "class A implements I1,
   * I2", the returned list would be [I1$$CC, I2$$CC], not [I2$$CC, I1$$CC].
   */
  private ImmutableList<String> collectOrderedCompanionsToTriggerInterfaceClinit(
      ImmutableList<String> interfaces) {
    ImmutableList.Builder<String> companionCollector = ImmutableList.builder();
    LinkedHashSet<String> visitedInterfaces = new LinkedHashSet<>();
    for (String anInterface : interfaces) {
      collectOrderedCompanionsToTriggerInterfaceClinit(
          anInterface, visitedInterfaces, companionCollector);
    }
    return companionCollector.build();
  }

  private void collectOrderedCompanionsToTriggerInterfaceClinit(
      String anInterface,
      LinkedHashSet<String> visitedInterfaces,
      ImmutableList.Builder<String> companionCollector) {
    if (!visitedInterfaces.add(anInterface)) {
      return;
    }
    ClassReader bytecode = classpath.readIfKnown(anInterface);
    if (bytecode == null || bootclasspath.isKnown(anInterface)) {
      return;
    }
    String[] parentInterfaces = bytecode.getInterfaces();
    if (parentInterfaces != null && parentInterfaces.length > 0) {
      for (String parentInterface : parentInterfaces) {
        collectOrderedCompanionsToTriggerInterfaceClinit(
            parentInterface, visitedInterfaces, companionCollector);
      }
    }
    InterfaceInitializationNecessityDetector necessityDetector =
        new InterfaceInitializationNecessityDetector(bytecode.getClassName());
    bytecode.accept(necessityDetector, ClassReader.SKIP_DEBUG);
    if (necessityDetector.needsToInitialize()) {
      // If we need to initialize this interface, we initialize its companion class, and its
      // companion class will initialize the interface then. This desigin decision is made to avoid
      // access issue, e.g., package-private interfaces.
      companionCollector.add(InterfaceDesugaring.getCompanionClassName(anInterface));
    }
  }

  /**
   * Recursively searches the given interfaces for default methods not implemented by this class
   * directly. If this method returns true we need to think about stubbing missing default methods.
   */
  private boolean defaultMethodsDefined(ImmutableList<String> interfaces) {
    for (String implemented : interfaces) {
      if (useGeneratedBaseClasses
          && InterfaceDesugaring.getCompanionClassName(implemented).equals(internalName)) {
        continue; // InterfaceDesugaring already made stubs for this interface
      }
      ClassReader bytecode;
      if (bootclasspath.isKnown(implemented)) {
        if (coreLibrarySupport != null
            && coreLibrarySupport.isEmulatedCoreClassOrInterface(implemented)) {
          return true; // need to stub in emulated interface methods such as Collection.stream()
        } else if (coreLibrarySupport != null
            && coreLibrarySupport.isRenamedCoreLibrary(implemented)) {
          // Check default methods of renamed interfaces
          bytecode = checkNotNull(bootclasspath.readIfKnown(implemented), implemented);
        } else {
          continue;
        }
      } else {
        bytecode = classpath.readIfKnown(implemented);
        if (bytecode == null) {
          // Interface isn't on the classpath, which indicates incomplete classpaths. Record missing
          // dependency so we can check it later.  If we don't check then we may get runtime
          // failures or wrong behavior from default methods that should've been stubbed in.
          // TODO(kmb): Print a warning so people can start fixing their deps?
          depsCollector.missingImplementedInterface(internalName, implemented);
          continue;
        }
      }

      // Class in classpath and bootclasspath is a bad idea but in any event, assume the
      // bootclasspath will take precedence like in a classloader.
      // We can skip code attributes as we just need to find default methods to stub.
      DefaultMethodFinder finder = new DefaultMethodFinder();
      bytecode.accept(finder, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
      if (finder.foundDefaultMethods()) {
        return true;
      }
    }
    return false;
  }

  /** Returns {@code true} for non-bridge default methods not in {@link #instanceMethods}. */
  private boolean shouldStubAsDefaultMethod(int access, String name, String desc) {
    // Ignore private methods, which technically aren't default methods and can only be called from
    // other methods defined in the interface.  This also ignores lambda body methods, which is fine
    // as we don't want or need to stub those.  Also ignore bridge methods as javac adds them to
    // concrete classes as needed anyway and we handle them separately for generated lambda classes.
    // Note that an exception is that, if a bridge method is for a default interface method, javac
    // will NOT generate the bridge method in the implementing class. So we need extra logic to
    // handle these bridge methods.
    return isNonBridgeDefaultMethod(access) && !recordedInstanceMethod(name, desc);
  }

  private static boolean isNonBridgeDefaultMethod(int access) {
    return BitFlags.noneSet(
        access,
        Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC | Opcodes.ACC_BRIDGE | Opcodes.ACC_PRIVATE);
  }

  /**
   * Check whether an interface method is a bridge method for a default interface method. This type
   * of bridge methods is special, as they are not put in the implementing classes by javac.
   */
  private boolean shouldStubAsBridgeDefaultMethod(int access, String name, String desc) {
    return BitFlags.isSet(access, Opcodes.ACC_BRIDGE | Opcodes.ACC_PUBLIC)
        && BitFlags.noneSet(access, Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC)
        && !recordedInstanceMethod(name, desc);
  }

  private void recordIfInstanceMethod(int access, String name, String desc) {
    if (BitFlags.noneSet(access, Opcodes.ACC_STATIC)) {
      // Record all declared instance methods, including abstract, bridge, and native methods, as
      // they all take precedence over default methods.
      if (coreLibrarySupport != null) {
        // Foreshadow any type renaming to avoid issues with double-desugaring (b/111447199)
        desc = coreLibrarySupport.getRemapper().mapMethodDesc(desc);
      }
      instanceMethods.add(name + ":" + desc);
    }
  }

  private boolean recordedInstanceMethod(String name, String desc) {
    if (coreLibrarySupport != null) {
      // Foreshadow any type renaming to avoid issues with double-desugaring (b/111447199)
      desc = coreLibrarySupport.getRemapper().mapMethodDesc(desc);
    }
    return instanceMethods.contains(name + ":" + desc);
  }

  /**
   * Visitor for interfaces that produces delegates in the class visited by the outer {@link
   * DefaultMethodClassFixer} for every default method encountered.
   */
  private class DefaultMethodStubber extends ClassVisitor {

    private final boolean isBootclasspathInterface;
    private final boolean mayNeedStubsForSuperclass;

    private String stubbedInterfaceName;

    public DefaultMethodStubber(
        boolean isBootclasspathInterface, boolean mayNeedStubsForSuperclass) {
      super(Opcodes.ASM9);
      this.isBootclasspathInterface = isBootclasspathInterface;
      this.mayNeedStubsForSuperclass = mayNeedStubsForSuperclass;
    }

    @Override
    public void visit(
        int version,
        int access,
        String name,
        String signature,
        String superName,
        String[] interfaces) {
      checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
      checkState(stubbedInterfaceName == null);
      stubbedInterfaceName = name;
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {
      if (shouldStubAsDefaultMethod(access, name, desc)) {
        // Remember we stubbed this method in case it's also defined by subsequently visited
        // interfaces.  javac would force the method to be defined explicitly if there any two
        // definitions conflict, but see stubMissingDefaultMethods() for how we deal with default
        // methods redefined in interfaces extending another.
        recordIfInstanceMethod(access, name, desc);
        String owner = InterfaceDesugaring.getCompanionClassName(stubbedInterfaceName);
        if (!isBootclasspathInterface) {
          // Don't record these dependencies, as we can't check them
          depsCollector.assumeCompanionClass(internalName, owner);
        }

        // Add this method to the class we're desugaring and stub in a body to call the default
        // implementation in the interface's companion class. ijar omits these methods when setting
        // ACC_SYNTHETIC modifier, so don't.
        // Signatures can be wrong, e.g., when type variables are introduced, instantiated, or
        // refined in the class we're processing, so drop them.
        MethodVisitor stubMethod =
            DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);

        String receiverName = stubbedInterfaceName;
        String calledMethodName = name + InterfaceDesugaring.DEFAULT_COMPANION_METHOD_SUFFIX;
        if (mayNeedStubsForSuperclass) {
          // Reflect what CoreLibraryInvocationRewriter would do if it encountered a super-call to a
          // moved implementation of an emulated method.  Equivalent to emitting the invokespecial
          // super call here and relying on CoreLibraryInvocationRewriter for the rest
          Class<?> emulatedImplementation =
              coreLibrarySupport.getCoreInterfaceRewritingTarget(
                  Opcodes.INVOKESPECIAL, superName, name, desc, /*itf=*/ false);
          if (emulatedImplementation != null && !emulatedImplementation.isInterface()) {
            receiverName = internalName(emulatedImplementation);
            owner = checkNotNull(coreLibrarySupport.getMoveTarget(receiverName, name, desc));
            calledMethodName = name;
          }
        }

        int slot = 0;
        stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
        Type neededType = Type.getMethodType(desc);
        for (Type arg : neededType.getArgumentTypes()) {
          stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
          slot += arg.getSize();
        }
        stubMethod.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            owner,
            calledMethodName,
            InterfaceDesugaring.companionDefaultMethodDescriptor(receiverName, desc),
            /*isInterface=*/ false);
        stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));

        stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
        stubMethod.visitEnd();
        return null; // don't visit the visited interface's default method
      } else if (shouldStubAsBridgeDefaultMethod(access, name, desc)) {
        recordIfInstanceMethod(access, name, desc);
        MethodVisitor stubMethod =
            DefaultMethodClassFixer.this.visitMethod(access, name, desc, (String) null, exceptions);
        // If we're visiting a bootclasspath interface then we most likely don't have the code.
        // That means we can't just copy the method bodies as we're trying to do below.
        if (isBootclasspathInterface) {
          // Synthesize a "bridge" method that calls the true implementation
          Method bridged = findBridgedMethod(name, desc);
          checkState(
              bridged != null,
              "TODO: Can't stub core interface bridge method %s.%s %s in %s",
              stubbedInterfaceName,
              name,
              desc,
              internalName);

          int slot = 0;
          stubMethod.visitVarInsn(Opcodes.ALOAD, slot++); // load the receiver
          Type neededType = Type.getType(bridged);
          Type[] neededArgTypes = neededType.getArgumentTypes();
          Type[] parameterTypes = Type.getArgumentTypes(desc);
          for (int i = 0; i < neededArgTypes.length; ++i) {
            Type arg = neededArgTypes[i];
            stubMethod.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), slot);
            if (!arg.equals(parameterTypes[i])) {
              checkState(
                  arg.getSort() == Type.ARRAY || arg.getSort() == Type.OBJECT,
                  "Can't cast parameter %s from in bridge for %s.%s%s to %s",
                  i,
                  stubbedInterfaceName,
                  name,
                  desc,
                  arg.getClassName());
              stubMethod.visitTypeInsn(Opcodes.CHECKCAST, arg.getInternalName());
            }
            slot += arg.getSize();
          }
          // Just call the bridged method directly on the visited class using invokevirtual
          stubMethod.visitMethodInsn(
              Opcodes.INVOKEVIRTUAL,
              internalName,
              name,
              neededType.getDescriptor(),
              /*isInterface=*/ false);
          stubMethod.visitInsn(neededType.getReturnType().getOpcode(Opcodes.IRETURN));

          stubMethod.visitMaxs(0, 0); // rely on class writer to compute these
          stubMethod.visitEnd();
          return null; // don't visit the visited interface's bridge method
        } else {
          // For bridges we just copy their bodies instead of going through the companion class.
          // Meanwhile, we also need to desugar the copied method bodies, so that any calls to
          // interface methods are correctly handled.
          return new InterfaceDesugaring.InterfaceInvocationRewriter(
              stubMethod,
              stubbedInterfaceName,
              bootclasspath,
              targetLoader,
              depsCollector,
              internalName);
        }
      } else {
        return null; // not a default or bridge method or the class already defines this method.
      }
    }

    /**
     * Returns a non-bridge interface method with given name that a method with the given descriptor
     * can bridge to, if any such method can be found.
     */
    @Nullable
    private Method findBridgedMethod(String name, String desc) {
      Type[] paramTypes = Type.getArgumentTypes(desc);
      Type returnType = Type.getReturnType(desc);
      Class<?> itf = loadFromInternal(stubbedInterfaceName);
      checkArgument(itf.isInterface(), "Should be an interface: %s", stubbedInterfaceName);

      // 1. Find the bridge method we're trying to implement
      Method bridge = null;
      for (Method m : itf.getDeclaredMethods()) {
        if (m.isBridge()
            && m.getName().equals(name)
            && Arrays.equals(paramTypes, Type.getArgumentTypes(m))
            && returnType.equals(Type.getReturnType(m))) {
          bridge = m;
          break;
        }
      }
      checkState(bridge != null, "Couldn't find bridge %s.%s %s", stubbedInterfaceName, name, desc);
      checkState(bridge.getParameterCount() == paramTypes.length);

      // 2. Try to find the method being bridged
      Method result = null;
      next_method:
      for (Method m : itf.getDeclaredMethods()) {
        if (m.isBridge() || Modifier.isStatic(m.getModifiers())) {
          continue;
        }
        if (!m.getName().equals(name)) {
          continue;
        }

        if (Arrays.equals(paramTypes, Type.getArgumentTypes(m))) {
          // All argument types match, only return type will differ: this is the method we want
          return m;
        } else if (m.getParameterCount() == bridge.getParameterCount()) {
          for (int i = 0; i < m.getParameterCount(); ++i) {
            if (!bridge.getParameterTypes()[i].isAssignableFrom(m.getParameterTypes()[i])) {
              continue next_method;
            }
          }

          // All of m's parameter types are subtypes of the bridge's parameter types (or primitives)
          if (result == null) {
            result = m;
          } else {
            // Bail if we find multiple methods that could be bridged
            return null;
          }
        }
      }
      return result;
    }
  }

  /**
   * Visitor for interfaces that recursively searches interfaces for default method declarations.
   */
  private class DefaultMethodFinder extends ClassVisitor {
    @SuppressWarnings("hiding")
    private ImmutableList<String> interfaces;

    private boolean found;

    public DefaultMethodFinder() {
      super(Opcodes.ASM9);
    }

    @Override
    public void visit(
        int version,
        int access,
        String name,
        String signature,
        String superName,
        String[] interfaces) {
      checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
      checkState(this.interfaces == null);
      this.interfaces = ImmutableList.copyOf(interfaces);
    }

    public boolean foundDefaultMethods() {
      return found;
    }

    @Override
    public void visitEnd() {
      if (!found) {
        found = defaultMethodsDefined(this.interfaces);
      }
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {
      if (!found && shouldStubAsDefaultMethod(access, name, desc)) {
        // Found a default method we're not ignoring (instanceMethods at this point contains methods
        // the top-level visited class implements itself).
        found = true;
      }
      return null; // we don't care about the actual code in these methods
    }
  }

  private class InstanceMethodRecorder extends ClassVisitor {

    private final boolean ignoreEmulatedMethods;

    private String className;

    public InstanceMethodRecorder(boolean ignoreEmulatedMethods) {
      super(Opcodes.ASM9);
      this.ignoreEmulatedMethods = ignoreEmulatedMethods;
    }

    @Override
    public void visit(
        int version,
        int access,
        String name,
        String signature,
        String superName,
        String[] interfaces) {
      checkArgument(!BitFlags.isInterface(access));
      className = name; // updated every time we start visiting another superclass
      super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {
      if (ignoreEmulatedMethods
          && !BitFlags.isStatic(access) // short-circuit
          && coreLibrarySupport.getCoreInterfaceRewritingTarget(
                  Opcodes.INVOKEVIRTUAL, className, name, desc, /*itf=*/ false)
              != null) {
        // *don't* record emulated core library method implementations in immediate subclasses of
        // emulated core library clasess so that they can be stubbed (since the inherited
        // implementation may be missing at runtime).
        return null;
      }
      recordIfInstanceMethod(access, name, desc);
      return null;
    }
  }

  /**
   * Detector to determine whether an interface needs to be initialized when it is loaded.
   *
   * <p>If the interface has a default method, and its <clinit> initializes any of its fields, then
   * this interface needs to be initialized.
   */
  private static class InterfaceInitializationNecessityDetector extends ClassVisitor {

    private final String internalName;
    private boolean hasFieldInitializedInClinit;
    private boolean hasDefaultMethods;

    public InterfaceInitializationNecessityDetector(String internalName) {
      super(Opcodes.ASM9);
      this.internalName = internalName;
    }

    public boolean needsToInitialize() {
      return hasDefaultMethods && hasFieldInitializedInClinit;
    }

    @Override
    public void visit(
        int version,
        int access,
        String name,
        String signature,
        String superName,
        String[] interfaces) {
      super.visit(version, access, name, signature, superName, interfaces);
      checkState(
          internalName.equals(name),
          "Inconsistent internal names: expected=%s, real=%s",
          internalName,
          name);
      checkArgument(
          BitFlags.isSet(access, Opcodes.ACC_INTERFACE),
          "This class visitor is only used for interfaces.");
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {
      if (!hasDefaultMethods) {
        hasDefaultMethods = isNonBridgeDefaultMethod(access);
      }
      if ("<clinit>".equals(name)) {
        return new MethodVisitor(Opcodes.ASM9) {
          @Override
          public void visitFieldInsn(int opcode, String owner, String name, String desc) {
            if (opcode == Opcodes.PUTSTATIC && internalName.equals(owner)) {
              hasFieldInitializedInClinit = true;
            }
          }
        };
      }
      return null; // Do not care about the code.
    }
  }

  /** Method visitor that changes super constructor calls to a new base class. */
  private static class ConstructorFixer extends MethodVisitor {

    private final String newSuperName;

    /**
     * This helps us find the very first constructor invocation we visit. We're looking for a
     * sequence ALOAD_0 ; INVOKESPECIAL java.lang.Object() while avoiding "new Object()" which
     * results in a NEW followed by the same INVOKESPECIAL for Object's constructor that we're
     * looking for. Note that anonymous inner class constructors store captured variables into
     * fields before calling super(), so we have to tolerate some unrelated logic, but since
     * Object's constructor is parameterless we thankfully don't have to worry about handwritten
     * code for constructor arguments.
     */
    private boolean looking = true;

    ConstructorFixer(MethodVisitor dest, String newSuperName) {
      super(Opcodes.ASM9, dest);
      this.newSuperName = newSuperName;
    }

    @Override
    public void visitMethodInsn(
        int opcode, String owner, String name, String descriptor, boolean isInterface) {
      if (looking
          && opcode == Opcodes.INVOKESPECIAL
          && "<init>".equals(name)
          && "java/lang/Object".equals(owner)) {
        // Call new base class's constructor instead of j.l.Object().  Note owner can be the
        // visited class for this() constructor calls, and we don't need to touch those here.
        owner = newSuperName;
      }
      // Since Object's constructor has no arguments it must be the very first INVOKE (because there
      // can't be any preceding logic to set up constructor arguments), so stop looking regardless
      // of whether we rewrote that INVOKE or not (but ignore inserted JaCoCo invocations).
      if (looking && (opcode != Opcodes.INVOKESTATIC || !"$jacocoInit".equals(name))) {
        looking = false;
      }
      super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }

    @Override
    public void visitTypeInsn(int opcode, String type) {
      if (opcode == Opcodes.NEW && "java/lang/Object".equals(type)) {
        // NEW java/lang/Object is followed by the very INVOKESPECIAL we're looking for, but we
        // don't want to rewrite it.  Since there can't be a super() call after this, just stop.
        looking = false;
      }
      super.visitTypeInsn(opcode, type);
    }
  }

  /** Comparator for classes and interfaces that compares by whether subtyping relationship. */
  enum SubtypeComparator implements Comparator<Class<?>> {
    /** Orders subtypes before supertypes and breaks ties lexicographically. */
    INSTANCE;

    @Override
    public int compare(Class<?> o1, Class<?> o2) {
      if (o1 == o2) {
        return 0;
      }
      // order subtypes before supertypes
      if (o1.isAssignableFrom(o2)) { // o1 is supertype of o2
        return 1; // we want o1 to come after o2
      }
      if (o2.isAssignableFrom(o1)) { // o2 is supertype of o1
        return -1; // we want o2 to come after o1
      }
      // o1 and o2 aren't comparable so arbitrarily impose lexicographical ordering
      return o1.getName().compareTo(o2.getName());
    }
  }
}
