/*
 * Copyright 2020 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.corelibadapter;

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

import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.android.desugar.io.BootClassPathDigest;
import com.google.devtools.build.android.desugar.langmodel.ClassName;
import com.google.devtools.build.android.desugar.langmodel.MemberUseKind;
import com.google.devtools.build.android.desugar.langmodel.MethodDeclInfo;
import com.google.devtools.build.android.desugar.langmodel.MethodInvocationSite;
import com.google.devtools.build.android.desugar.langmodel.MethodKey;
import com.google.devtools.build.android.desugar.typehierarchy.HierarchicalMethodKey;
import com.google.devtools.build.android.desugar.typehierarchy.HierarchicalMethodQuery;
import com.google.devtools.build.android.desugar.typehierarchy.TypeHierarchy;
import java.util.Optional;
import org.objectweb.asm.Type;

/**
 * Static utilities that serve conversions between desugar-shadowed platform types and their
 * desugared-mirrored counterparts.
 */
public class ShadowedApiAdapterHelper {

  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  private ShadowedApiAdapterHelper() {}

  /**
   * Returns {@code true} if the desugar tool transforms given invocation site in an inline
   * strategy, i.e. inserting the parameter type conversion instructions before the give invocation
   * site.
   *
   * @param verbatimInvocationSite The invocation site parsed directly from the desugar input jar.
   *     No in-process label, such as "__desugar__/", is attached to this invocation site.
   * @param typeHierarchy The type hierarchy context of for this query API.
   * @param bootClassPathDigest The boot class path context used for complication.
   * @param enclosingMethod The method that holds the invocation instruction.
   */
  static boolean shouldUseInlineTypeConversion(
      MethodInvocationSite verbatimInvocationSite,
      TypeHierarchy typeHierarchy,
      BootClassPathDigest bootClassPathDigest,
      MethodDeclInfo enclosingMethod) {
    if (verbatimInvocationSite.invocationKind() != MemberUseKind.INVOKESPECIAL) {
      return false;
    }

    // invokespecial on a private method in the same class.
    if (verbatimInvocationSite.owner().equals(enclosingMethod.owner())) {
      return false;
    }

    // Absent of desugar-shadowed type in the method header.
    if (verbatimInvocationSite.method().getHeaderTypeNameSet().stream()
        .noneMatch(ClassName::isDesugarShadowedType)) {
      return false;
    }

    if (verbatimInvocationSite.isConstructorInvocation()) {
      return bootClassPathDigest.containsType(verbatimInvocationSite.owner());
    }

    // Upon on a super call, trace to the adjusted owner with code.
    ClassName adjustedGrossOwner = verbatimInvocationSite.owner();
    HierarchicalMethodQuery verbatimMethod =
        HierarchicalMethodKey.from(verbatimInvocationSite.method()).inTypeHierarchy(typeHierarchy);
    if (!verbatimMethod.isPresent()) {
      HierarchicalMethodKey resolvedMethod = verbatimMethod.getFirstBaseClassMethod();
      if (resolvedMethod == null) {
        logger.atSevere().log("Missing base method lookup: %s", verbatimInvocationSite);
      } else {
        adjustedGrossOwner = resolvedMethod.owner().type();
      }
    }
    return adjustedGrossOwner.isAndroidDomainType()
        && bootClassPathDigest.containsType(adjustedGrossOwner);
  }

  /**
   * Returns {@code true} if the desugar tool transforms given invocation site in an adapter
   * strategy, that is to replace the original invocation with its corresponding adapter method.
   *
   * @param verbatimInvocationSite The invocation site parsed directly from the desugar input jar.
   *     No in-process label, such as "__desugar__/", is attached to this invocation site.
   */
  static boolean shouldUseApiTypeAdapter(
      MethodInvocationSite verbatimInvocationSite, BootClassPathDigest bootClassPathDigest) {
    return verbatimInvocationSite.invocationKind() != MemberUseKind.INVOKESPECIAL
        && verbatimInvocationSite.owner().isAndroidDomainType()
        && bootClassPathDigest.containsType(verbatimInvocationSite.owner())
        && verbatimInvocationSite.method().getHeaderTypeNameSet().stream()
            .anyMatch(ClassName::isDesugarShadowedType);
  }

  /**
   * Returns {@code true} if the current method overrides a platform API with desugar-shadowed types
   * and should emit an overriding bridge method for the integrity of method dynamic dispatching.
   */
  static boolean shouldEmitApiOverridingBridge(
      MethodDeclInfo methodDeclInfo,
      TypeHierarchy typeHierarchy,
      BootClassPathDigest bootClassPathDigest) {
    if (bootClassPathDigest.containsType(methodDeclInfo.owner())
        || methodDeclInfo.methodKey().isConstructor()
        || methodDeclInfo.isStaticMethod()
        || methodDeclInfo.isPrivateAccess()
        || methodDeclInfo.headerTypeNameSet().stream()
            .noneMatch(ClassName::isDesugarShadowedType)) {
      return false;
    }

    HierarchicalMethodKey baseMethod =
        HierarchicalMethodKey.from(methodDeclInfo.methodKey())
            .inTypeHierarchy(typeHierarchy)
            .getFirstBaseClassMethod();

    boolean queryResult =
        baseMethod != null
            && baseMethod.owner().type().isAndroidDomainType()
            && bootClassPathDigest.containsType(baseMethod.owner().type());
    if (queryResult) {
      logger.atInfo().log(
          "----> Shadowed Method Overriding Bridge eligible for %s due to base method %s",
          methodDeclInfo.methodKey(), baseMethod.toMethodKey());
    }
    return queryResult;
  }

  /**
   * Returns an optional {@link MethodInvocationSite}, present if the given {@link ClassName} is
   * eligible for transforming a desugar-mirrored type to a desugar-shadowed platform type.
   */
  static Optional<MethodInvocationSite> anyMirroredToBuiltinTypeConversion(ClassName className) {
    return className.isDesugarMirroredType()
        ? Optional.of(
            MethodInvocationSite.builder()
                .setInvocationKind(MemberUseKind.INVOKESTATIC)
                .setMethod(
                    MethodKey.create(
                        className.mirroredToShadowed().typeConverterOwner(),
                        "to",
                        Type.getMethodDescriptor(
                            className.mirroredToShadowed().toAsmObjectType(),
                            className.toAsmObjectType())))
                .setIsInterface(false)
                .build())
        : Optional.empty();
  }

  /**
   * Returns an {@link MethodInvocationSite} that serves transforming a {@code
   * shadowedTypeName}-represented type to its desugar-mirrored counterpart.
   */
  public static MethodInvocationSite shadowedToMirroredTypeConversionSite(
      ClassName shadowedTypeName) {
    checkArgument(
        shadowedTypeName.isDesugarShadowedType(),
        "Expected desugar-shadowed type: Actual (%s)",
        shadowedTypeName);
    return MethodInvocationSite.builder()
        .setInvocationKind(MemberUseKind.INVOKESTATIC)
        .setMethod(
            MethodKey.create(
                shadowedTypeName.typeConverterOwner(),
                "from",
                Type.getMethodDescriptor(
                    shadowedTypeName.shadowedToMirrored().toAsmObjectType(),
                    shadowedTypeName.toAsmObjectType())))
        .setIsInterface(false)
        .build();
  }

  /**
   * Returns an {@link MethodInvocationSite} that serves transforming a {@code
   * mirroredTypeName}-represented type to its desugar-shadowed counterpart.
   */
  static MethodInvocationSite mirroredToShadowedTypeConversionSite(ClassName mirroredTypeName) {
    checkArgument(
        mirroredTypeName.isDesugarMirroredType(),
        "Expected mirrored type: Actual (%s)",
        mirroredTypeName);
    return MethodInvocationSite.builder()
        .setInvocationKind(MemberUseKind.INVOKESTATIC)
        .setMethod(
            MethodKey.create(
                mirroredTypeName.mirroredToShadowed().typeConverterOwner(),
                "to",
                Type.getMethodDescriptor(
                    mirroredTypeName.mirroredToShadowed().toAsmObjectType(),
                    mirroredTypeName.toAsmObjectType())))
        .setIsInterface(false)
        .build();
  }

  /**
   * Returns an {@link MethodInvocationSite} that serves as an adapter between desugar-mirrored
   * invocations and desugar-shadowed invocations.
   */
  static MethodInvocationSite getAdapterInvocationSite(MethodInvocationSite methodInvocationSite) {
    return MethodInvocationSite.builder()
        .setInvocationKind(MemberUseKind.INVOKESTATIC)
        .setMethod(
            methodInvocationSite
                .method()
                .toAdapterMethodForArgsAndReturnTypes(
                    methodInvocationSite.isStaticInvocation(), methodInvocationSite.hashCode()))
        .setIsInterface(false)
        .build();
  }
}
