// Copyright 2016 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.devtools.build.android.desugar.io.BitFlags;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypePath;

/**
 * Visitor that tries to ensures bytecode version <= 51 (Java 7) and that throws if it sees default
 * or static interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
 * <p>The class version will 52 iff static interface method from the bootclasspath is invoked.
 * This is mostly ensure that the generated bytecode is valid.
 */
public class Java7Compatibility extends ClassVisitor {

  @Nullable private final ClassReaderFactory factory;
  @Nullable private final ClassReaderFactory bootclasspathReader;

  private boolean isInterface;
  private String internalName;
  private int access;
  private String signature;
  private String superName;
  private String[] interfaces;

  public Java7Compatibility(
      ClassVisitor cv, ClassReaderFactory factory, ClassReaderFactory bootclasspathReader) {
    super(Opcodes.ASM6, cv);
    this.factory = factory;
    this.bootclasspathReader = bootclasspathReader;
  }

  @Override
  public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName,
      String[] interfaces) {
    internalName = name;
    this.access = access;
    this.signature = signature;
    this.superName = superName;
    this.interfaces = interfaces;
    isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
    super.visit(
        Math.min(version, Opcodes.V1_7),
        access,
        name,
        signature,
        superName,
        interfaces);
  }

  @Override
  public MethodVisitor visitMethod(
      int access, String name, String desc, String signature, String[] exceptions) {
    // Remove bridge default methods in interfaces; javac generates them again for implementing
    // classes anyways.
    if (isInterface
        && (access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC))
            == Opcodes.ACC_BRIDGE) {
      return null;
    }
    if (isInterface
        && "$jacocoInit".equals(name)
        && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC)) {
      // Drop static interface method that Jacoco generates--we'll inline it into the static
      // initializer instead
      return null;
    }
    checkArgument(!isInterface
        || BitFlags.isSet(access, Opcodes.ACC_ABSTRACT)
        || "<clinit>".equals(name),
        "Interface %s defines non-abstract method %s%s, which is not supported",
        internalName, name, desc);
    MethodVisitor result =
        new UpdateBytecodeVersionIfNecessary(
            super.visitMethod(access, name, desc, signature, exceptions));

    return (isInterface && "<clinit>".equals(name)) ? new InlineJacocoInit(result) : result;
  }

  @Override
  public void visitInnerClass(String name, String outerName, String innerName, int access) {
    // Drop MethodHandles$Lookup inner class information--it shouldn't be needed anymore.  Proguard
    // complains about this even though the inner class information is never used.
    if (!"java/lang/invoke/MethodHandles$Lookup".equals(name)) {
      super.visitInnerClass(name, outerName, innerName, access);
    }
  }

  /** This will rewrite class version to 52 if it sees invokestatic on an interface. */
  private class UpdateBytecodeVersionIfNecessary extends MethodVisitor {

    boolean updated = false;

    public UpdateBytecodeVersionIfNecessary(MethodVisitor methodVisitor) {
      super(Opcodes.ASM6, methodVisitor);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
      if (itf && opcode == Opcodes.INVOKESTATIC) {
        checkNotNull(bootclasspathReader);
        checkState(
            bootclasspathReader.isKnown(owner),
            "%s contains invocation of static interface method that is "
                + "not in the bootclasspath. Owner: %s, name: %s, desc: %s.",
            Java7Compatibility.this.internalName,
            owner,
            name,
            desc);
        if (!updated) {
          Java7Compatibility.this.cv.visit(
              Opcodes.V1_8,
              Java7Compatibility.this.access,
              Java7Compatibility.this.internalName,
              Java7Compatibility.this.signature,
              Java7Compatibility.this.superName,
              Java7Compatibility.this.interfaces);
          updated = true;
        }
      }
      super.visitMethodInsn(opcode, owner, name, desc, itf);
    }
  }

  private class InlineJacocoInit extends MethodVisitor {
    public InlineJacocoInit(MethodVisitor dest) {
      super(Opcodes.ASM6, dest);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
      if (opcode == Opcodes.INVOKESTATIC
          && "$jacocoInit".equals(name)
          && internalName.equals(owner)) {
        checkNotNull(factory);
        ClassReader bytecode = checkNotNull(factory.readIfKnown(internalName),
            "Couldn't load interface %s to inline $jacocoInit()", internalName);
        InlineOneMethod copier = new InlineOneMethod("$jacocoInit", this);
        bytecode.accept(copier, ClassReader.SKIP_DEBUG /* we're copying generated code anyway */);
      } else {
        super.visitMethodInsn(opcode, owner, name, desc, itf);
      }
    }
  }

  private static class InlineOneMethod extends ClassVisitor {

    private final String methodName;
    private final MethodVisitor dest;
    private int copied = 0;

    public InlineOneMethod(String methodName, MethodVisitor dest) {
      super(Opcodes.ASM6);
      this.methodName = methodName;
      this.dest = dest;
    }

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

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {
      if (name.equals(methodName)) {
        checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc);
        ++copied;
        return new InlineMethodBody(dest);
      }
      return null;
    }
  }

  private static class InlineMethodBody extends MethodVisitor {
    private final MethodVisitor dest;

    public InlineMethodBody(MethodVisitor dest) {
      // We'll set the destination visitor in visitCode() to reduce the risk of copying anything
      // we didn't mean to copy
      super(Opcodes.ASM6, (MethodVisitor) null);
      this.dest = dest;
    }

    @Override
    public void visitParameter(String name, int access) {
    }

    @Override
    public AnnotationVisitor visitAnnotationDefault() {
      throw new IllegalStateException("Don't use to copy annotation attributes");
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
      return null;
    }

    @Override
    public AnnotationVisitor visitTypeAnnotation(
        int typeRef, TypePath typePath, String desc, boolean visible) {
      return null;
    }

    @Override
    public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
      return null;
    }

    @Override
    public void visitAttribute(Attribute attr) {
      mv = null; // don't care about anything but the code attribute
    }

    @Override
    public void visitCode() {
      // Start copying instructions but don't call super.visitCode() since dest is already in the
      // middle of visiting another method
      mv = dest;
    }

    @Override
    public void visitInsn(int opcode) {
      switch (opcode) {
        case Opcodes.IRETURN:
        case Opcodes.LRETURN:
        case Opcodes.FRETURN:
        case Opcodes.DRETURN:
        case Opcodes.ARETURN:
        case Opcodes.RETURN:
          checkState(mv != null, "Encountered a second return it would seem: %s", opcode);
          mv = null; // Done: we don't expect anything to follow
          return;
        default:
          super.visitInsn(opcode);
      }
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
      throw new UnsupportedOperationException(
          "We don't support inlining methods with locals: " + opcode + " " + var);
    }

    @Override
    public void visitLocalVariable(
        String name, String desc, String signature, Label start, Label end, int index) {
      throw new UnsupportedOperationException(
          "We don't support inlining methods with locals: " + name + ": " + desc);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
      // Drop this, since dest will get more instructions and will need to recompute stack size.
      // This does indicate the end of visiting bytecode instructions, so defensively reset mv.
      mv = null;
    }
  }
}
