// Copyright 2019 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.nest;

import static com.google.devtools.build.android.desugar.langmodel.LangModelHelper.isCrossMateRefInNest;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.android.desugar.langmodel.ClassName;
import com.google.devtools.build.android.desugar.langmodel.FieldInstrVisitor;
import com.google.devtools.build.android.desugar.langmodel.FieldKey;
import com.google.devtools.build.android.desugar.langmodel.LangModelHelper;
import com.google.devtools.build.android.desugar.langmodel.MemberUseKind;
import com.google.devtools.build.android.desugar.langmodel.MethodInstrVisitor;
import com.google.devtools.build.android.desugar.langmodel.MethodKey;
import org.objectweb.asm.Opcodes;
import javax.annotation.Nullable;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/** A visitor that convert direct access to field/method with access through bridges. */
public final class NestBridgeRefConverter extends MethodVisitor {

  private final MethodKey enclosingMethodKey;
  private final NestDigest nestDigest;
  private final FieldAccessToBridgeRedirector directFieldAccessReplacer;
  private final MethodToBridgeRedirector methodToBridgeRedirector;

  NestBridgeRefConverter(
      @Nullable MethodVisitor methodVisitor, MethodKey methodKey, NestDigest nestDigest) {
    super(Opcodes.ASM9, methodVisitor);
    this.enclosingMethodKey = methodKey;
    this.nestDigest = nestDigest;

    directFieldAccessReplacer = new FieldAccessToBridgeRedirector();
    methodToBridgeRedirector = new MethodToBridgeRedirector(nestDigest);
  }

  @Override
  public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
    FieldKey fieldKey = FieldKey.create(ClassName.create(owner), name, descriptor);
    MemberUseKind useKind = MemberUseKind.fromValue(opcode);
    if (isCrossMateRefInNest(fieldKey, enclosingMethodKey)
        && nestDigest.hasAnyUse(fieldKey, useKind)) {
      fieldKey.accept(useKind, directFieldAccessReplacer, mv);
      return;
    }
    super.visitFieldInsn(opcode, owner, name, descriptor);
  }

  @Override
  public void visitMethodInsn(
      int opcode, String owner, String name, String descriptor, boolean isInterface) {
    MethodKey methodKey = MethodKey.create(ClassName.create(owner), name, descriptor);
    MemberUseKind useKind = MemberUseKind.fromValue(opcode);

    // Desugar invokevirtual on private instance methods within the same class.
    if (!isInterface
        && useKind == MemberUseKind.INVOKEVIRTUAL
        && methodKey.owner().equals(enclosingMethodKey.owner())
        && nestDigest.isPrivateInstanceMethod(methodKey)) {
      super.visitMethodInsn(Opcodes.INVOKESPECIAL, owner, name, descriptor, isInterface);
      return;
    }

    if ((isInterface || isCrossMateRefInNest(methodKey, enclosingMethodKey))
        && nestDigest.hasAnyUse(methodKey, useKind)) {
      methodKey.accept(useKind, isInterface, methodToBridgeRedirector, mv);
      return;
    }
    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
  }

  /** A visitor that re-directs the invocation of a method to that of the method's bridge. */
  static class MethodToBridgeRedirector
      implements MethodInstrVisitor<MethodKey, MethodKey, MethodVisitor> {

    private final NestDigest nestDigest;

    MethodToBridgeRedirector(NestDigest nestDigest) {
      this.nestDigest = nestDigest;
    }

    @Override
    public MethodKey visitInvokeVirtual(MethodKey methodKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = methodKey.bridgeOfClassInstanceMethod();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          /* isInterface= */ false);
      return methodKey;
    }

    @Override
    public MethodKey visitInvokeSpecial(MethodKey methodKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = methodKey.bridgeOfClassInstanceMethod();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          /* isInterface= */ false);
      return methodKey;
    }

    @Override
    public MethodKey visitConstructorInvokeSpecial(MethodKey methodKey, MethodVisitor mv) {
      ClassName nestCompanion = nestDigest.nestCompanion(ClassName.create(methodKey.ownerName()));
      MethodKey constructorBridge = methodKey.bridgeOfConstructor(nestCompanion);
      mv.visitInsn(Opcodes.ACONST_NULL);
      mv.visitMethodInsn(
          Opcodes.INVOKESPECIAL,
          constructorBridge.ownerName(),
          constructorBridge.name(),
          constructorBridge.descriptor(),
          /* isInterface= */ false);
      return methodKey;
    }

    @Override
    public MethodKey visitInterfaceInvokeSpecial(MethodKey methodKey, MethodVisitor mv) {
      MethodKey methodBridge = methodKey.substituteOfInterfaceInstanceMethod();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          methodBridge.ownerName(),
          methodBridge.name(),
          methodBridge.descriptor(),
          /* isInterface= */ true);
      return methodKey;
    }

    @Override
    public MethodKey visitInvokeStatic(MethodKey methodKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = methodKey.bridgeOfClassStaticMethod();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          /* isInterface= */ false);
      return methodKey;
    }

    @Override
    public MethodKey visitInterfaceInvokeStatic(MethodKey methodKey, MethodVisitor mv) {
      final MethodKey methodBridge = methodKey.substituteOfInterfaceStaticMethod();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          methodBridge.ownerName(),
          methodBridge.name(),
          methodBridge.descriptor(),
          /* isInterface= */ true);
      return methodKey;
    }

    @Override
    public MethodKey visitInvokeInterface(MethodKey methodKey, MethodVisitor mv) {
      final MethodKey methodBridge = methodKey.substituteOfInterfaceInstanceMethod();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          methodBridge.ownerName(),
          methodBridge.name(),
          methodBridge.descriptor(),
          /* isInterface= */ true);
      return methodKey;
    }

    @Override
    public MethodKey visitInvokeDynamic(MethodKey methodKey, MethodVisitor mv) {
      throw new UnsupportedOperationException();
    }
  }

  /** A visitor that re-directs field access with bridge method invocation. */
  static class FieldAccessToBridgeRedirector
      implements FieldInstrVisitor<MethodKey, FieldKey, MethodVisitor> {

    @Override
    public MethodKey visitGetStatic(FieldKey fieldKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = fieldKey.bridgeOfStaticRead();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          false);
      return bridgeMethodKey;
    }

    @Override
    public MethodKey visitPutStatic(FieldKey fieldKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = fieldKey.bridgeOfStaticWrite();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          false);
      // The bridge method for an instance field writer pushes the new field value to its invoker
      // operand stack, we emit a pop instruction to be consistent with putfield instruction which
      // consumes the updated field value on the operand stack.
      mv.visitInsn(
          LangModelHelper.getTypeSizeAlignedPopOpcode(ImmutableList.of(fieldKey.getFieldType())));
      return bridgeMethodKey;
    }

    @Override
    public MethodKey visitGetField(FieldKey fieldKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = fieldKey.bridgeOfInstanceRead();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          false);
      return bridgeMethodKey;
    }

    @Override
    public MethodKey visitPutField(FieldKey fieldKey, MethodVisitor mv) {
      MethodKey bridgeMethodKey = fieldKey.bridgeOfInstanceWrite();
      mv.visitMethodInsn(
          Opcodes.INVOKESTATIC,
          bridgeMethodKey.ownerName(),
          bridgeMethodKey.name(),
          bridgeMethodKey.descriptor(),
          /* isInterface= */ false);
      // The bridge method for an instance field writer pushes the new field value to its invoker
      // operand stack, we emit a pop instruction to be consistent with putfield instruction which
      // consumes the updated field value on the operand stack.
      mv.visitInsn(
          LangModelHelper.getTypeSizeAlignedPopOpcode(ImmutableList.of(fieldKey.getFieldType())));
      return bridgeMethodKey;
    }
  }
}
