/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.javaagent.tooling.muzzle;

import static net.bytebuddy.description.method.MethodDescription.CONSTRUCTOR_INTERNAL_NAME;

import io.opentelemetry.javaagent.tooling.muzzle.references.ClassRef;
import io.opentelemetry.javaagent.tooling.muzzle.references.FieldRef;
import io.opentelemetry.javaagent.tooling.muzzle.references.Flag.ManifestationFlag;
import io.opentelemetry.javaagent.tooling.muzzle.references.Flag.OwnershipFlag;
import io.opentelemetry.javaagent.tooling.muzzle.references.Flag.VisibilityFlag;
import io.opentelemetry.javaagent.tooling.muzzle.references.MethodRef;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodDescription.InDefinedShape;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.pool.TypePool.Resolution;

/** This class provides a common interface for {@link ClassRef} and {@link TypeDescription}. */
interface HelperReferenceWrapper {
  boolean isAbstract();

  /**
   * Returns true if the wrapped type extends any class other than {@link Object} or implements any
   * interface.
   */
  boolean hasSuperTypes();

  /**
   * Returns an iterable containing the wrapped type's super class (if exists) and implemented
   * interfaces.
   */
  Stream<HelperReferenceWrapper> getSuperTypes();

  /** Returns an iterable with all non-private, non-static methods declared in the wrapped type. */
  Stream<Method> getMethods();

  /** Returns an iterable with all non-private fields declared in the wrapped type. */
  Stream<Field> getFields();

  final class Method {
    private final boolean isAbstract;
    private final String declaringClass;
    private final String name;
    private final String descriptor;

    public Method(boolean isAbstract, String declaringClass, String name, String descriptor) {
      this.isAbstract = isAbstract;
      this.declaringClass = declaringClass;
      this.name = name;
      this.descriptor = descriptor;
    }

    public boolean isAbstract() {
      return isAbstract;
    }

    public String getDeclaringClass() {
      return declaringClass;
    }

    public String getName() {
      return name;
    }

    public String getDescriptor() {
      return descriptor;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
      if (obj == this) {
        return true;
      }
      if (!(obj instanceof Method)) {
        return false;
      }
      Method other = (Method) obj;
      return Objects.equals(name, other.name) && Objects.equals(descriptor, other.descriptor);
    }

    @Override
    public int hashCode() {
      return Objects.hash(name, descriptor);
    }

    @Override
    public String toString() {
      return "Method{"
          + "isAbstract="
          + isAbstract
          + ", declaringClass='"
          + declaringClass
          + '\''
          + ", name='"
          + name
          + '\''
          + ", descriptor='"
          + descriptor
          + '\''
          + '}';
    }
  }

  final class Field {
    private final String name;
    private final String descriptor;

    public Field(String name, String descriptor) {
      this.name = name;
      this.descriptor = descriptor;
    }

    public String getName() {
      return name;
    }

    public String getDescriptor() {
      return descriptor;
    }

    @Override
    public boolean equals(@Nullable Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      Field field = (Field) o;
      return Objects.equals(name, field.name) && Objects.equals(descriptor, field.descriptor);
    }

    @Override
    public int hashCode() {
      return Objects.hash(name, descriptor);
    }

    @Override
    public String toString() {
      return "Field{" + "name='" + name + '\'' + ", descriptor='" + descriptor + '\'' + '}';
    }
  }

  class Factory {
    private final TypePool classpathPool;
    private final Map<String, ClassRef> helperReferences;
    private final HelperClassPredicate helperClassPredicate;

    public Factory(
        TypePool classpathPool,
        Map<String, ClassRef> helperReferences,
        HelperClassPredicate helperClassPredicate) {
      this.classpathPool = classpathPool;
      this.helperReferences = helperReferences;
      this.helperClassPredicate = helperClassPredicate;
    }

    public HelperReferenceWrapper create(ClassRef reference) {
      return new ReferenceType(reference);
    }

    private HelperReferenceWrapper create(String className) {
      // Looking up an injected helper is recorded in AgentCachingPoolStrategy as a failed resolve
      // because injected helper can't be found by the TypePool that is used here. If that helper is
      // defined before it gets evicted there will be a stack trace about resource not found which
      // is flagged as a transformation failure that makes tests fail.
      if (!helperClassPredicate.isHelperClass(className)) {
        Resolution resolution = classpathPool.describe(className);
        if (resolution.isResolved()) {
          return new ClasspathType(resolution.resolve());
        }
      }
      // checking helper references is needed when one helper class A extends another helper class B
      // and the subclass A also implements a library interface C
      // B needs to be resolved as part of checking that A implements all required methods of C
      // but B cannot be resolved on the classpath, so B needs to be resolved from helper references
      if (helperReferences.containsKey(className)) {
        return new ReferenceType(helperReferences.get(className));
      }
      throw new IllegalStateException("Missing class " + className);
    }

    private final class ReferenceType implements HelperReferenceWrapper {
      private final ClassRef reference;

      private ReferenceType(ClassRef reference) {
        this.reference = reference;
      }

      @Override
      public boolean isAbstract() {
        return reference.getFlags().contains(ManifestationFlag.ABSTRACT);
      }

      @Override
      public boolean hasSuperTypes() {
        return hasActualSuperType() || reference.getInterfaceNames().size() > 0;
      }

      @Override
      public Stream<HelperReferenceWrapper> getSuperTypes() {
        Stream<HelperReferenceWrapper> superClass = Stream.empty();
        if (hasActualSuperType()) {
          superClass = Stream.of(Factory.this.create(reference.getSuperClassName()));
        }

        Stream<HelperReferenceWrapper> interfaces =
            reference.getInterfaceNames().stream().map(Factory.this::create);

        return Stream.concat(superClass, interfaces);
      }

      private boolean hasActualSuperType() {
        return reference.getSuperClassName() != null;
      }

      @Override
      public Stream<Method> getMethods() {
        return reference.getMethods().stream().filter(this::isOverrideable).map(this::toMethod);
      }

      private boolean isOverrideable(MethodRef method) {
        return !(method.getFlags().contains(OwnershipFlag.STATIC)
            || method.getFlags().contains(VisibilityFlag.PRIVATE)
            || CONSTRUCTOR_INTERNAL_NAME.equals(method.getName()));
      }

      private Method toMethod(MethodRef method) {
        return new Method(
            method.getFlags().contains(ManifestationFlag.ABSTRACT),
            reference.getClassName(),
            method.getName(),
            method.getDescriptor());
      }

      @Override
      public Stream<Field> getFields() {
        return reference.getFields().stream()
            .filter(this::isDeclaredAndNotPrivate)
            .map(this::toField);
      }

      private boolean isDeclaredAndNotPrivate(FieldRef field) {
        return field.isDeclared() && !field.getFlags().contains(VisibilityFlag.PRIVATE);
      }

      private Field toField(FieldRef field) {
        return new Field(field.getName(), field.getDescriptor());
      }
    }

    private static final class ClasspathType implements HelperReferenceWrapper {
      private final TypeDescription type;

      private ClasspathType(TypeDescription type) {
        this.type = type;
      }

      @Override
      public boolean isAbstract() {
        return type.isAbstract();
      }

      @Override
      public boolean hasSuperTypes() {
        return hasActualSuperType() || type.getInterfaces().size() > 0;
      }

      private boolean hasActualSuperType() {
        return type.getSuperClass() != null;
      }

      @Override
      public Stream<HelperReferenceWrapper> getSuperTypes() {
        Stream<HelperReferenceWrapper> superClass = Stream.empty();
        if (hasActualSuperType()) {
          superClass = Stream.of(new ClasspathType(type.getSuperClass().asErasure()));
        }

        Stream<HelperReferenceWrapper> interfaces =
            type.getInterfaces().asErasures().stream().map(ClasspathType::new);

        return Stream.concat(superClass, interfaces);
      }

      @Override
      public Stream<Method> getMethods() {
        return type.getDeclaredMethods().stream()
            .filter(ClasspathType::isOverrideable)
            .map(this::toMethod);
      }

      private static boolean isOverrideable(InDefinedShape method) {
        return !(method.isStatic() || method.isPrivate() || method.isConstructor());
      }

      private Method toMethod(InDefinedShape method) {
        return new Method(
            method.isAbstract(), type.getName(), method.getInternalName(), method.getDescriptor());
      }

      @Override
      public Stream<Field> getFields() {
        return type.getDeclaredFields().stream()
            .filter(ClasspathType::isNotPrivate)
            .map(ClasspathType::toField);
      }

      private static boolean isNotPrivate(FieldDescription.InDefinedShape field) {
        return !field.isPrivate();
      }

      private static Field toField(FieldDescription.InDefinedShape field) {
        return new Field(field.getName(), field.getDescriptor());
      }
    }
  }
}
