/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.kafka.connect.runtime.isolation;

import org.apache.kafka.common.config.provider.ConfigProvider;
import org.apache.kafka.connect.connector.policy.ConnectorClientConfigOverridePolicy;
import org.apache.kafka.connect.rest.ConnectRestExtension;
import org.apache.kafka.connect.sink.SinkConnector;
import org.apache.kafka.connect.source.SourceConnector;
import org.apache.kafka.connect.storage.Converter;
import org.apache.kafka.connect.storage.HeaderConverter;
import org.apache.kafka.connect.transforms.Transformation;
import org.apache.kafka.connect.transforms.predicates.Predicate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
import java.util.SortedSet;
import java.util.TreeSet;

import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;

/**
 * A {@link PluginScanner} implementation which uses reflection and {@link ServiceLoader} to discover plugins.
 * <p>This implements the legacy discovery strategy, which uses a combination of reflection and service loading in
 * order to discover plugins. Specifically, a plugin appears in the scan result if all the following conditions are true:
 * <ul>
 *     <li>The class and direct dependencies can be loaded</li>
 *     <li>The class is concrete</li>
 *     <li>The class is public</li>
 *     <li>The class has a no-args constructor</li>
 *     <li>The no-args constructor is public</li>
 *     <li>Static initialization of the class completes without throwing an exception</li>
 *     <li>The no-args constructor completes without throwing an exception</li>
 *     <li>One of the following is true:
 *         <ul>
 *             <li>Is a subclass of {@link SinkConnector}, {@link SourceConnector}, {@link Converter},
 *             {@link HeaderConverter}, {@link Transformation}, or {@link Predicate}</li>
 *             <li>Is a subclass of {@link ConfigProvider}, {@link ConnectRestExtension}, or
 *             {@link ConnectorClientConfigOverridePolicy}, and has a {@link ServiceLoader} compatible
 *             manifest file or module declaration</li>
 *         </ul>
 *     </li>
 * </ul>
 * <p>Note: This scanner has a runtime proportional to the number of overall classes in the passed-in
 * {@link PluginSource} objects, which may be significant for plugins with large dependencies. For a more performant
 * implementation, consider using {@link ServiceLoaderScanner} and follow migration instructions for
 * <a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-898%3A+Modernize+Connect+plugin+discovery">KIP-898</a>.
 */
public class ReflectionScanner extends PluginScanner {

    private static final Logger log = LoggerFactory.getLogger(ReflectionScanner.class);

    private static <T> String versionFor(Class<? extends T> pluginKlass) throws ReflectiveOperationException {
        T pluginImpl = pluginKlass.getDeclaredConstructor().newInstance();
        return versionFor(pluginImpl);
    }

    @Override
    protected PluginScanResult scanPlugins(PluginSource source) {
        ClassGraph classGraphBuilder = new ClassGraph()
                .overrideClassLoaders(classLoaderOrder(source))
                .enableExternalClasses()
                .enableClassInfo();
        try (ScanResult classGraph = classGraphBuilder.scan()) {
            return new PluginScanResult(
                  getPluginDesc(classGraph, PluginType.SINK, source),
                  getPluginDesc(classGraph, PluginType.SOURCE, source),
                  getPluginDesc(classGraph, PluginType.CONVERTER, source),
                  getPluginDesc(classGraph, PluginType.HEADER_CONVERTER, source),
                  getTransformationPluginDesc(source, classGraph),
                  getPredicatePluginDesc(source, classGraph),
                  getServiceLoaderPluginDesc(PluginType.CONFIGPROVIDER, source),
                  getServiceLoaderPluginDesc(PluginType.REST_EXTENSION, source),
                  getServiceLoaderPluginDesc(PluginType.CONNECTOR_CLIENT_CONFIG_OVERRIDE_POLICY, source)
          );
        }
    }

    @SuppressWarnings({"unchecked"})
    private SortedSet<PluginDesc<Predicate<?>>> getPredicatePluginDesc(PluginSource source, ScanResult classGraph) {
        return (SortedSet<PluginDesc<Predicate<?>>>) (SortedSet<?>) getPluginDesc(classGraph, PluginType.PREDICATE, source);
    }

    @SuppressWarnings({"unchecked"})
    private SortedSet<PluginDesc<Transformation<?>>> getTransformationPluginDesc(PluginSource source, ScanResult classGraph) {
        return (SortedSet<PluginDesc<Transformation<?>>>) (SortedSet<?>) getPluginDesc(classGraph, PluginType.TRANSFORMATION, source);
    }

    private ClassLoader[] classLoaderOrder(PluginSource source) {
        // Classgraph will first scan all the class URLs from the provided classloader chain and use said chain during classloading.
        // We compute and provide the classloader chain starting from the isolated PluginClassLoader to ensure that it adheres
        // to the child first delegation model used in connect. In addition, classgraph can fail to find URLs from the
        // application classloader as it uses an illegal reflections access. Providing the entire chain of classloaders
        // which included the application classloader forces classpath URLs to be scanned separately.
        List<ClassLoader> classLoaderOrder = new ArrayList<>();
        ClassLoader cl = source.loader();
        while (cl != null) {
            classLoaderOrder.add(cl);
            cl = cl.getParent();
        }
        return classLoaderOrder.toArray(new ClassLoader[0]);
    }

    @SuppressWarnings({"unchecked"})
    private <T> SortedSet<PluginDesc<T>> getPluginDesc(
            ScanResult classGraph,
            PluginType type,
            PluginSource source
    ) {
        ClassInfoList plugins;
        Class<T> klass = (Class<T>) type.superClass();
        try {
            if (klass.isInterface()) {
                plugins = classGraph.getClassesImplementing(klass.getName());
            } else {
                plugins = classGraph.getSubclasses(klass.getName());
            }
        } catch (Exception e) {
            log.debug("Reflections scanner could not find any {} in {} for URLs: {}",
                    type, source, source.urls(), e);
            return Collections.emptySortedSet();
        }

        SortedSet<PluginDesc<T>> result = new TreeSet<>();
        for (Class<? extends T> pluginKlass : plugins.getStandardClasses().loadClasses(klass, true)) {
            if (!PluginUtils.isConcrete(pluginKlass)) {
                log.debug("Skipping {} in {} as it is not concrete implementation", pluginKlass, source);
                continue;
            }
            if (pluginKlass.getClassLoader() != source.loader()) {
                log.debug("{} from other classloader {} is visible from {}, excluding to prevent isolated loading",
                        pluginKlass, pluginKlass.getClassLoader(), source);
                continue;
            }
            try (LoaderSwap loaderSwap = withClassLoader(source.loader())) {
                result.add(pluginDesc(pluginKlass, versionFor(pluginKlass), type, source));
            } catch (ReflectiveOperationException | LinkageError e) {
                log.error("Failed to discover {} in {}: Unable to instantiate {}{}",
                        type.simpleName(), source, pluginKlass.getSimpleName(),
                        reflectiveErrorDescription(e), e);
            }
        }
        return result;
    }
}
