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

package io.opentelemetry.javaagent.tooling.ignore;

import static java.util.logging.Level.FINE;

import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import io.opentelemetry.javaagent.bootstrap.PatchLogger;
import io.opentelemetry.javaagent.tooling.util.Trie;
import java.util.logging.Logger;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.matcher.ElementMatcher;

public class IgnoredClassLoadersMatcher extends ElementMatcher.Junction.AbstractBase<ClassLoader> {
  private static final Logger logger = Logger.getLogger(IgnoredClassLoadersMatcher.class.getName());

  /* Cache of class loader instance -> (true|false). True = skip instrumentation. False = safe to instrument. */
  private static final Cache<ClassLoader, Boolean> skipCache = Cache.weak();

  private final Trie<IgnoreAllow> ignoredClassLoaders;

  public IgnoredClassLoadersMatcher(Trie<IgnoreAllow> ignoredClassLoaders) {
    this.ignoredClassLoaders = ignoredClassLoaders;
  }

  @Override
  public boolean matches(ClassLoader cl) {
    if (cl == ClassLoadingStrategy.BOOTSTRAP_LOADER) {
      // Don't skip bootstrap loader
      return false;
    }

    String name = cl.getClass().getName();

    IgnoreAllow ignored = ignoredClassLoaders.getOrNull(name);
    if (ignored == IgnoreAllow.ALLOW) {
      return false;
    } else if (ignored == IgnoreAllow.IGNORE) {
      return true;
    }

    return skipCache.computeIfAbsent(
        cl,
        c -> {
          // when ClassloadingInstrumentation is active, checking delegatesToBootstrap() below is
          // not required, because ClassloadingInstrumentation forces all class loaders to load all
          // the classes in Constants.BOOTSTRAP_PACKAGE_PREFIXES directly from the bootstrap class
          // loader
          //
          // however, at this time we don't want to introduce the concept of a required
          // instrumentation, and we don't want to introduce the concept of the tooling code
          // depending on whether a particular instrumentation is active (mainly because this
          // particular use case doesn't seem to justify introducing either of these new concepts)
          return !delegatesToBootstrap(cl);
        });
  }

  /**
   * TODO: this turns out to be useless with OSGi: {@code
   * org.eclipse.osgi.internal.loader.BundleLoader#isRequestFromVM} returns {@code true} when class
   * loading is issued from this check and {@code false} for 'real' class loads. We should come up
   * with some sort of hack to avoid this problem.
   */
  private static boolean delegatesToBootstrap(ClassLoader loader) {
    boolean delegates = true;
    if (!loadsExpectedClass(loader, PatchLogger.class)) {
      logger.log(FINE, "loader {0} failed to delegate bootstrap agent class", loader);
      delegates = false;
    }
    return delegates;
  }

  private static boolean loadsExpectedClass(ClassLoader loader, Class<?> expectedClass) {
    try {
      return loader.loadClass(expectedClass.getName()) == expectedClass;
    } catch (ClassNotFoundException e) {
      return false;
    }
  }
}
