/*
 * 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.langmodel;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import org.objectweb.asm.Type;

/** The key to index a class or interface method or constructor. */
@AutoValue
public abstract class MethodKey extends ClassMemberKey<MethodKey> {

  private static final Splitter COLON_SPLITTER = Splitter.on(":");

  /** The factory method for {@link MethodKey}. */
  public static MethodKey create(ClassName ownerClass, String name, String descriptor) {
    checkState(
        descriptor.isEmpty() // Allows empty descriptor for non-overloaded methods.
            || descriptor.startsWith("("),
        "Expected a method descriptor. Actual: (%s#%s:%s)",
        ownerClass,
        name,
        descriptor);
    return new AutoValue_MethodKey(ownerClass, name, descriptor);
  }

  public final String encode() {
    return ownerName() + ":" + name() + ":" + descriptor();
  }

  public static MethodKey decode(String textFormat) {
    List<String> payloads = COLON_SPLITTER.splitToList(textFormat);
    checkState(payloads.size() == 3, "Invalid text format for method key: Actual(%s)", textFormat);
    return MethodKey.create(ClassName.create(payloads.get(0)), payloads.get(1), payloads.get(2));
  }

  public static MethodKey fromProto(MethodId methodId) {
    return create(ClassName.create(methodId.getOwner()), methodId.getName(), methodId.getDesc());
  }

  public MethodId toMethodIdProto() {
    return MethodId.newBuilder()
        .setOwner(ownerName())
        .setName(name())
        .setDesc(descriptor())
        .build();
  }

  /** The return type of a method. */
  public Type getReturnType() {
    return Type.getReturnType(descriptor());
  }

  /** The return type of a method. */
  public ClassName getReturnTypeName() {
    return ClassName.create(Type.getReturnType(descriptor()));
  }

  /** The formal parameter types of a method. */
  public Type[] getArgumentTypeArray() {
    return Type.getArgumentTypes(descriptor());
  }

  /** The formal parameter types of a method. */
  public ImmutableList<Type> getArgumentTypes() {
    return ImmutableList.copyOf(getArgumentTypeArray());
  }

  /** The formal parameter type names of a method. */
  public ImmutableList<ClassName> getArgumentTypeNames() {
    return getArgumentTypes().stream().map(ClassName::create).collect(toImmutableList());
  }

  /**
   * Returns the set of all types referenced in the method header, i.e. all types in the method
   * return type and argument types.
   */
  public ImmutableSet<ClassName> getHeaderTypeNameSet() {
    return ImmutableSet.<ClassName>builder()
        .add(getReturnTypeName())
        .addAll(getArgumentTypeNames())
        .build();
  }

  public MethodKey toAdapterMethodForArgsAndReturnTypes(
      boolean fromStaticOrigin, int invocationSiteHashCode) {
    checkState(
        !isConstructor(), "Argument type adapter for constructor is not supported: %s. ", this);
    return MethodKey.create(
            owner().typeAdapterOwner(invocationSiteHashCode),
            name(),
            fromStaticOrigin ? descriptor() : instanceMethodToStaticDescriptor())
        .acceptTypeMapper(ClassName.SHADOWED_TO_MIRRORED_TYPE_MAPPER);
  }

  /** The synthetic constructor for a private constructor. */
  public final MethodKey bridgeOfConstructor(ClassName nestCompanion) {
    checkState(isConstructor(), "Expect to use for a constructor but is %s", this);
    Type companionClassType = nestCompanion.toAsmObjectType();
    ImmutableList<Type> argumentTypes = getArgumentTypes();
    ImmutableList<Type> bridgeConstructorArgTypes =
        ImmutableList.<Type>builder().addAll(argumentTypes).add(companionClassType).build();
    return create(
        owner(),
        name(),
        Type.getMethodDescriptor(getReturnType(), bridgeConstructorArgTypes.toArray(new Type[0])));
  }

  /** The synthetic bridge method for a private static method in a class. */
  public final MethodKey bridgeOfClassStaticMethod() {
    checkState(!isConstructor(), "Expect a non-constructor method but is a constructor %s", this);
    return create(owner(), nameWithSuffix("bridge"), descriptor());
  }

  /** The synthetic bridge method for a private instance method in a class. */
  public final MethodKey bridgeOfClassInstanceMethod() {
    return create(owner(), nameWithSuffix("bridge"), this.instanceMethodToStaticDescriptor());
  }

  /** The substitute method for a private static method in an interface. */
  public final MethodKey substituteOfInterfaceStaticMethod() {
    checkState(!isConstructor(), "Expect a non-constructor: %s", this);
    return create(owner(), name(), descriptor());
  }

  /** The substitute method for a private instance method in an interface. */
  public final MethodKey substituteOfInterfaceInstanceMethod() {
    return create(owner(), name(), this.instanceMethodToStaticDescriptor());
  }

  /** The descriptor of the static version of a given instance method. */
  public final String instanceMethodToStaticDescriptor() {
    checkState(!isConstructor(), "Expect a Non-constructor method: %s", this);
    ImmutableList<Type> argumentTypes = getArgumentTypes();
    ImmutableList<Type> bridgeMethodArgTypes =
        ImmutableList.<Type>builder().add(ownerAsmObjectType()).addAll(argumentTypes).build();
    return Type.getMethodDescriptor(getReturnType(), bridgeMethodArgTypes.toArray(new Type[0]));
  }

  @Override
  public MethodKey acceptTypeMapper(TypeMapper typeMapper) {
    return MethodKey.create(typeMapper.map(owner()), name(), typeMapper.mapDesc(descriptor()));
  }

  /**
   * Accepts a {@link MethodInstrVisitor} that visits all kinds of method invocation instructions.
   */
  public final <R, P> R accept(
      MemberUseKind methodInstrOpcode,
      boolean isInterface,
      MethodInstrVisitor<R, ? super MethodKey, P> visitor,
      P param) {
    if (isConstructor()) {
      checkState(methodInstrOpcode == MemberUseKind.INVOKESPECIAL);
      return visitor.visitConstructorInvokeSpecial(this, param);
    } else if (isInterface) {
      switch (methodInstrOpcode) {
        case INVOKESPECIAL:
          // Super call to an non-abstract instance interface method.
          return visitor.visitInterfaceInvokeSpecial(this, param);
        case INVOKESTATIC:
          return visitor.visitInterfaceInvokeStatic(this, param);
        case INVOKEINTERFACE:
          return visitor.visitInvokeInterface(this, param);
        default:
          throw new AssertionError(
              String.format(
                  "Unexpected Opcode: (%s) invoked on Interface Method(%s).\n"
                      + "Opcode Reference: {INVOKEVIRTUAL(182), INVOKESPECIAL(183),"
                      + " INVOKESTATIC(184), INVOKEINTERFACE(185), INVOKEDYNAMIC(186)}",
                  methodInstrOpcode, this));
      }
    } else {
      switch (methodInstrOpcode) {
        case INVOKEVIRTUAL:
          return visitor.visitInvokeVirtual(this, param);
        case INVOKESPECIAL:
          return visitor.visitInvokeSpecial(this, param);
        case INVOKESTATIC:
          return visitor.visitInvokeStatic(this, param);
        case INVOKEDYNAMIC:
          return visitor.visitInvokeDynamic(this, param);
        default:
          throw new AssertionError(
              String.format(
                  "Unexpected Opcode: (%s) invoked on class Method(%s).\n"
                      + "Opcode Reference: {INVOKEVIRTUAL(182), INVOKESPECIAL(183),"
                      + " INVOKESTATIC(184), INVOKEINTERFACE(185), INVOKEDYNAMIC(186)}",
                  methodInstrOpcode, this));
      }
    }
  }
}
