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

package io.opentelemetry.javaagent.tooling.muzzle;

import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.muzzle.references.ClassRef;
import io.opentelemetry.javaagent.tooling.muzzle.references.FieldRef;
import io.opentelemetry.javaagent.tooling.muzzle.references.Flag;
import io.opentelemetry.javaagent.tooling.muzzle.references.MethodRef;
import io.opentelemetry.javaagent.tooling.muzzle.references.Source;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Collections;
import org.objectweb.asm.Type;

/**
 * A mismatch between a {@link ClassRef} and a runtime class.
 *
 * <p>This class' {@link #toString()} returns a human-readable description of the mismatch along
 * with the first source code location of the reference which caused the mismatch.
 */
public abstract class Mismatch {
  /** Instrumentation sources which caused the mismatch. */
  private final Collection<Source> mismatchSources;

  private Mismatch(Collection<Source> mismatchSources) {
    this.mismatchSources = mismatchSources;
  }

  @Override
  public String toString() {
    if (mismatchSources.size() > 0) {
      return mismatchSources.iterator().next().toString() + " " + getMismatchDetails();
    } else {
      return "<no-source> " + getMismatchDetails();
    }
  }

  /** Human-readable string describing the mismatch. */
  abstract String getMismatchDetails();

  public static class MissingClass extends Mismatch {
    private final String className;

    public MissingClass(ClassRef classRef) {
      super(classRef.getSources());
      this.className = classRef.getClassName();
    }

    public MissingClass(ClassRef classRef, String className) {
      super(classRef.getSources());
      this.className = className;
    }

    @Override
    String getMismatchDetails() {
      return "Missing class " + className;
    }
  }

  public static class MissingFlag extends Mismatch {
    private final Flag expectedFlag;
    private final String classMethodOrFieldDesc;
    private final int foundAccess;

    public MissingFlag(
        Collection<Source> sources,
        String classMethodOrFieldDesc,
        Flag expectedFlag,
        int foundAccess) {
      super(sources);
      this.classMethodOrFieldDesc = classMethodOrFieldDesc;
      this.expectedFlag = expectedFlag;
      this.foundAccess = foundAccess;
    }

    @Override
    String getMismatchDetails() {
      return classMethodOrFieldDesc + " requires flag " + expectedFlag + " found " + foundAccess;
    }
  }

  public static class MissingField extends Mismatch {
    private final String className;
    private final String fieldName;
    private final String fieldDescriptor;

    MissingField(ClassRef classRef, FieldRef fieldRef) {
      super(fieldRef.getSources());
      this.className = classRef.getClassName();
      this.fieldName = fieldRef.getName();
      this.fieldDescriptor = fieldRef.getDescriptor();
    }

    MissingField(ClassRef classRef, HelperReferenceWrapper.Field field) {
      super(classRef.getSources());
      this.className = classRef.getClassName();
      this.fieldName = field.getName();
      this.fieldDescriptor = field.getDescriptor();
    }

    @Override
    String getMismatchDetails() {
      return "Missing field "
          + Type.getType(fieldDescriptor).getClassName()
          + " "
          + fieldName
          + " in class "
          + className;
    }
  }

  public static class MissingMethod extends Mismatch {
    private final String className;
    private final String methodName;
    private final String methodDescriptor;

    public MissingMethod(ClassRef classRef, MethodRef methodRef) {
      super(methodRef.getSources());
      this.className = classRef.getClassName();
      this.methodName = methodRef.getName();
      this.methodDescriptor = methodRef.getDescriptor();
    }

    public MissingMethod(ClassRef classRef, HelperReferenceWrapper.Method method) {
      super(classRef.getSources());
      this.className = method.getDeclaringClass();
      this.methodName = method.getName();
      this.methodDescriptor = method.getDescriptor();
    }

    @Override
    String getMismatchDetails() {
      return "Missing method " + className + "#" + methodName + methodDescriptor;
    }
  }

  /** Fallback mismatch in case an unexpected exception occurs during reference checking. */
  public static class ReferenceCheckError extends Mismatch {
    private final Exception referenceCheckException;
    private final ClassRef referenceBeingChecked;
    private final ClassLoader classLoaderBeingChecked;

    public ReferenceCheckError(
        Exception e, ClassRef referenceBeingChecked, ClassLoader classLoaderBeingChecked) {
      super(Collections.emptyList());
      referenceCheckException = e;
      this.referenceBeingChecked = referenceBeingChecked;
      this.classLoaderBeingChecked = classLoaderBeingChecked;
    }

    @Override
    String getMismatchDetails() {
      StringWriter sw = new StringWriter();
      sw.write("Failed to generate reference check for: ");
      sw.write(referenceBeingChecked.toString());
      sw.write(" on class loader ");
      sw.write(classLoaderBeingChecked.toString());
      sw.write("\n");
      // add exception message and stack trace
      PrintWriter pw = new PrintWriter(sw);
      referenceCheckException.printStackTrace(pw);
      return sw.toString();
    }
  }

  /**
   * Represents failure of some class loader to satisfy {@link
   * InstrumentationModule#classLoaderMatcher()}.
   */
  public static class InstrumentationModuleClassLoaderMismatch extends Mismatch {

    public InstrumentationModuleClassLoaderMismatch() {
      super(Collections.emptyList());
    }

    @Override
    String getMismatchDetails() {
      return "InstrumentationModule class loader check";
    }
  }

  /**
   * Represents failure to inject {@link InstrumentationModuleMuzzle#getMuzzleHelperClassNames()}
   * into some class loader.
   */
  public static class HelperClassesInjectionError extends Mismatch {

    public HelperClassesInjectionError() {
      super(Collections.emptyList());
    }

    @Override
    String getMismatchDetails() {
      return "Failed to inject helper classes. Are Helpers being injected in the correct order?";
    }
  }
}
