/*
 * 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.camel.quarkus.core.deployment;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.processor.BuildExtension;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Overridable;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.maven.dependency.ArtifactKey;
import io.quarkus.runtime.RuntimeValue;
import io.smallrye.common.annotation.Identifier;
import org.apache.camel.impl.converter.BaseTypeConverterRegistry;
import org.apache.camel.quarkus.core.CamelCapabilities;
import org.apache.camel.quarkus.core.CamelConfig;
import org.apache.camel.quarkus.core.CamelConfigFlags;
import org.apache.camel.quarkus.core.CamelProducers;
import org.apache.camel.quarkus.core.CamelRecorder;
import org.apache.camel.quarkus.core.FastFactoryFinderResolver.Builder;
import org.apache.camel.quarkus.core.deployment.spi.CamelComponentNameResolverBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelFactoryFinderResolverBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelModelJAXBContextFactoryBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelModelReifierFactoryBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelModelToXMLDumperBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelModelToYAMLDumperBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelPackageScanClassBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelPackageScanClassResolverBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelRoutesBuilderClassBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelServiceBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelServiceDestination;
import org.apache.camel.quarkus.core.deployment.spi.CamelServiceFilter;
import org.apache.camel.quarkus.core.deployment.spi.CamelServiceFilterBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelServicePatternBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelTypeConverterLoaderBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelTypeConverterRegistryBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.ContainerBeansBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.RoutesBuilderClassExcludeBuildItem;
import org.apache.camel.quarkus.core.deployment.util.CamelSupport;
import org.apache.camel.quarkus.core.deployment.util.PathFilter;
import org.apache.camel.quarkus.core.util.FileUtils;
import org.apache.camel.spi.TypeConverterLoader;
import org.apache.camel.spi.TypeConverterRegistry;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.camel.quarkus.core.CamelCapabilities.CLOUD_EVENTS;

class CamelProcessor {
    private static final Logger LOGGER = LoggerFactory.getLogger(CamelProcessor.class);

    private static final DotName ROUTES_BUILDER_TYPE = DotName.createSimple(
            "org.apache.camel.RoutesBuilder");
    private static final DotName ROUTE_BUILDER_TYPE = DotName.createSimple(
            "org.apache.camel.builder.RouteBuilder");
    private static final DotName LAMBDA_ROUTE_BUILDER_TYPE = DotName.createSimple(
            "org.apache.camel.builder.LambdaRouteBuilder");
    private static final DotName LAMBDA_ENDPOINT_ROUTE_BUILDER_TYPE = DotName.createSimple(
            "org.apache.camel.builder.endpoint.LambdaEndpointRouteBuilder");
    private static final DotName ADVICE_WITH_ROUTE_BUILDER_TYPE = DotName.createSimple(
            "org.apache.camel.builder.AdviceWithRouteBuilder");
    private static final DotName DATA_FORMAT_TYPE = DotName.createSimple(
            "org.apache.camel.spi.DataFormat");
    private static final DotName LANGUAGE_TYPE = DotName.createSimple(
            "org.apache.camel.spi.Language");
    private static final DotName COMPONENT_TYPE = DotName.createSimple(
            "org.apache.camel.Component");
    private static final DotName PRODUCER_TYPE = DotName.createSimple(
            "org.apache.camel.Producer");
    private static final DotName PREDICATE_TYPE = DotName.createSimple(
            "org.apache.camel.Predicate");
    private static final DotName CONVERTER_TYPE = DotName.createSimple(
            "org.apache.camel.Converter");

    private static final Set<DotName> UNREMOVABLE_BEANS_TYPES = CamelSupport.setOf(
            ROUTES_BUILDER_TYPE,
            LAMBDA_ROUTE_BUILDER_TYPE,
            LAMBDA_ENDPOINT_ROUTE_BUILDER_TYPE,
            DATA_FORMAT_TYPE,
            LANGUAGE_TYPE,
            COMPONENT_TYPE,
            PRODUCER_TYPE,
            PREDICATE_TYPE);

    @BuildStep
    BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem containerBeans(
            BeanRegistrationPhaseBuildItem beanRegistrationPhase,
            BuildProducer<ContainerBeansBuildItem> containerBeans) {

        containerBeans.produce(
                new ContainerBeansBuildItem(beanRegistrationPhase.getContext().get(BuildExtension.Key.BEANS)));

        // method using BeanRegistrationPhaseBuildItem should return a BeanConfiguratorBuildItem
        // otherwise the build step may be processed at the wrong time.
        return new BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem();
    }

    /*
     * Configure filters for core services.
     */
    @BuildStep
    void coreServiceFilter(BuildProducer<CamelServiceFilterBuildItem> filterBuildItems) {
        filterBuildItems.produce(
                new CamelServiceFilterBuildItem(CamelServiceFilter.forService("properties-component-factory")));

        // The reactive executor is programmatically configured by an extension or
        // a default implementation is provided by this processor thus we can safely
        // prevent loading of this service.
        filterBuildItems.produce(
                new CamelServiceFilterBuildItem(CamelServiceFilter.forService("reactive-executor")));
    }

    @BuildStep
    void coreServicePatterns(BuildProducer<CamelServicePatternBuildItem> services) {
        services.produce(new CamelServicePatternBuildItem(
                CamelServiceDestination.REGISTRY,
                true,
                "META-INF/services/org/apache/camel/component/*",
                "META-INF/services/org/apache/camel/language/constant",
                "META-INF/services/org/apache/camel/language/file",
                "META-INF/services/org/apache/camel/language/header",
                "META-INF/services/org/apache/camel/language/ref",
                "META-INF/services/org/apache/camel/language/simple"));

        services.produce(new CamelServicePatternBuildItem(
                CamelServiceDestination.DISCOVERY,
                true,
                "META-INF/services/org/apache/camel/*",
                "META-INF/services/org/apache/camel/routes-loader/*",
                "META-INF/services/org/apache/camel/resource-resolver/*",
                "META-INF/services/org/apache/camel/invoke-on-header/*",
                "META-INF/services/org/apache/camel/management/*",
                "META-INF/services/org/apache/camel/model/*",
                "META-INF/services/org/apache/camel/configurer/*",
                "META-INF/services/org/apache/camel/language/*",
                "META-INF/services/org/apache/camel/dataformat/*",
                "META-INF/services/org/apache/camel/send-dynamic/*",
                "META-INF/services/org/apache/camel/urifactory/*",
                "META-INF/services/org/apache/camel/properties-function/*",
                "META-INF/services/org/apache/camel/health-check/*",
                "META-INF/services/org/apache/camel/periodic-task/*",
                "META-INF/services/org/apache/camel/transformer/*"));
    }

    @BuildStep
    void userServicePatterns(
            CamelConfig camelConfig,
            BuildProducer<CamelServicePatternBuildItem> services) {

        camelConfig.service.discovery.includePatterns.ifPresent(list -> services.produce(new CamelServicePatternBuildItem(
                CamelServiceDestination.DISCOVERY,
                true,
                list)));

        camelConfig.service.discovery.excludePatterns.ifPresent(list -> services.produce(new CamelServicePatternBuildItem(
                CamelServiceDestination.DISCOVERY,
                false,
                list)));

        camelConfig.service.registry.includePatterns.ifPresent(list -> services.produce(new CamelServicePatternBuildItem(
                CamelServiceDestination.REGISTRY,
                true,
                list)));

        camelConfig.service.registry.excludePatterns.ifPresent(list -> services.produce(new CamelServicePatternBuildItem(
                CamelServiceDestination.REGISTRY,
                false,
                list)));
    }

    @BuildStep
    CamelServicePatternBuildItem conditionalCloudEventsTransformerServiceExcludePattern(Capabilities capabilities) {
        // Exclude cloudevents transformers unless optional camel-quarkus-cloudevents is present
        if (capabilities.isMissing(CLOUD_EVENTS)) {
            return new CamelServicePatternBuildItem(
                    CamelServiceDestination.DISCOVERY,
                    false,
                    "META-INF/services/org/apache/camel/transformer/*cloudevents*");
        }
        return null;
    }

    @BuildStep
    void camelServices(
            ApplicationArchivesBuildItem applicationArchives,
            List<CamelServicePatternBuildItem> servicePatterns,
            BuildProducer<CamelServiceBuildItem> camelServices) {

        final PathFilter pathFilter = servicePatterns.stream()
                .filter(patterns -> patterns.getDestination() == CamelServiceDestination.DISCOVERY)
                .collect(
                        PathFilter.Builder::new,
                        (builder, patterns) -> builder.patterns(patterns.isInclude(), patterns.getPatterns()),
                        PathFilter.Builder::combine)
                .build();
        CamelSupport.services(applicationArchives, pathFilter)
                .forEach(camelServices::produce);

        if (LOGGER.isDebugEnabled()) {
            debugCamelServiceInclusion(applicationArchives, servicePatterns);
        }
    }

    /*
     * Discover {@link TypeConverterLoader}.
     */
    @SuppressWarnings("unchecked")
    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep
    CamelTypeConverterRegistryBuildItem typeConverterRegistry(
            CamelRecorder recorder,
            ApplicationArchivesBuildItem applicationArchives,
            List<CamelTypeConverterLoaderBuildItem> additionalLoaders,
            CombinedIndexBuildItem combinedIndex,
            BuildProducer<UnremovableBeanBuildItem> unremovableBean) {

        IndexView index = combinedIndex.getIndex();

        RuntimeValue<TypeConverterRegistry> typeConverterRegistry = recorder.createTypeConverterRegistry();

        //
        // This should be simplified by searching for classes implementing TypeConverterLoader but that
        // would lead to have org.apache.camel.impl.converter.AnnotationTypeConverterLoader taken into
        // account even if it should not.
        //
        final ClassLoader TCCL = Thread.currentThread().getContextClassLoader();

        for (ApplicationArchive archive : applicationArchives.getAllApplicationArchives()) {
            for (Path root : archive.getRootDirectories()) {
                Path path = root.resolve(BaseTypeConverterRegistry.META_INF_SERVICES_TYPE_CONVERTER_LOADER);
                if (!Files.isRegularFile(path)) {
                    continue;
                }

                try {
                    Files.readAllLines(path, StandardCharsets.UTF_8).stream()
                            .map(String::trim)
                            .filter(l -> !l.isEmpty())
                            .filter(l -> !l.startsWith("#"))
                            .map(l -> (Class<? extends TypeConverterLoader>) CamelSupport.loadClass(l, TCCL))
                            .forEach(loader -> recorder.addTypeConverterLoader(typeConverterRegistry, loader));
                } catch (IOException e) {
                    throw new RuntimeException("Error discovering TypeConverterLoader", e);
                }
            }
        }

        Set<String> internalConverters = new HashSet<>();
        //ignore all @converters from org.apache.camel:camel-* dependencies
        for (ApplicationArchive archive : applicationArchives.getAllApplicationArchives()) {
            ArtifactKey artifactKey = archive.getKey();
            if (artifactKey != null && "org.apache.camel".equals(artifactKey.getGroupId())
                    && artifactKey.getArtifactId().startsWith("camel-")) {
                internalConverters.addAll(archive.getIndex().getAnnotations(CONVERTER_TYPE)
                        .stream().filter(a -> a.target().kind() == AnnotationTarget.Kind.CLASS)
                        .map(a -> a.target().asClass().name().toString())
                        .collect(Collectors.toSet()));
            }
        }

        Set<Class<?>> convertersClasses = index
                .getAnnotations(CONVERTER_TYPE)
                .stream().filter(a -> a.target().kind() == AnnotationTarget.Kind.CLASS &&
                        (a.value("generateBulkLoader") == null || !a.value("generateBulkLoader").asBoolean()) &&
                        (a.value("generateLoader") == null || !a.value("generateLoader").asBoolean()))
                .map(a -> a.target().asClass().name().toString())
                .filter(s -> !internalConverters.contains(s))
                .map(s -> CamelSupport.loadClass(s, TCCL))
                .collect(Collectors.toSet());

        recorder.loadAnnotatedConverters(typeConverterRegistry, convertersClasses);

        // Enable @Converter annotated classes to be CDI beans
        unremovableBean.produce(UnremovableBeanBuildItem.beanClassAnnotation(CONVERTER_TYPE));

        //
        // User can register loaders by providing a CamelTypeConverterLoaderBuildItem that can be used to
        // provide additional TypeConverter or override default converters discovered by the previous step.
        //
        for (CamelTypeConverterLoaderBuildItem item : additionalLoaders) {
            recorder.addTypeConverterLoader(typeConverterRegistry, item.getValue());
        }

        return new CamelTypeConverterRegistryBuildItem(typeConverterRegistry);
    }

    @Overridable
    @BuildStep
    @Record(value = ExecutionTime.STATIC_INIT, optional = true)
    public CamelModelJAXBContextFactoryBuildItem createJaxbContextFactory(CamelRecorder recorder) {
        return new CamelModelJAXBContextFactoryBuildItem(recorder.newDisabledModelJAXBContextFactory());
    }

    @Overridable
    @BuildStep
    @Record(value = ExecutionTime.STATIC_INIT, optional = true)
    public CamelModelToXMLDumperBuildItem createModelToXMLDumper(CamelRecorder recorder) {
        return new CamelModelToXMLDumperBuildItem(recorder.newDisabledModelToXMLDumper());
    }

    @Overridable
    @BuildStep
    @Record(value = ExecutionTime.STATIC_INIT, optional = true)
    public CamelModelToYAMLDumperBuildItem createModelToYAMLDumper(CamelRecorder recorder) {
        return new CamelModelToYAMLDumperBuildItem(recorder.newDisabledModelToYAMLDumper());
    }

    @BuildStep
    @Record(ExecutionTime.STATIC_INIT)
    void disableXmlReifiers(CamelRecorder recorder, Capabilities capabilities) {
        if (!capabilities.isCapabilityWithPrefixPresent(CamelCapabilities.XML)) {
            LOGGER.debug("Camel XML capability not detected, disable XML reifiers");
            recorder.disableXmlReifiers();
        }
    }

    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep
    CamelFactoryFinderResolverBuildItem factoryFinderResolver(
            CamelRecorder recorder,
            List<CamelServiceBuildItem> camelServices) {
        final ClassLoader TCCL = Thread.currentThread().getContextClassLoader();

        RuntimeValue<Builder> builder = recorder.factoryFinderResolverBuilder();

        camelServices.forEach(service -> {
            recorder.factoryFinderResolverEntry(
                    builder,
                    FileUtils.nixifyPath(service.path),
                    CamelSupport.loadClass(service.type, TCCL));
        });

        return new CamelFactoryFinderResolverBuildItem(recorder.factoryFinderResolver(builder));
    }

    @BuildStep
    void unremovableBeans(
            BuildProducer<AdditionalBeanBuildItem> beanProducer,
            BuildProducer<UnremovableBeanBuildItem> unremovableBeans) {

        beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(CamelProducers.class));

        unremovableBeans.produce(UnremovableBeanBuildItem.targetWithAnnotation(DotName.createSimple(Identifier.class)));
        unremovableBeans.produce(new UnremovableBeanBuildItem(
                b -> b.getTypes().stream().map(Type::name).anyMatch(UNREMOVABLE_BEANS_TYPES::contains)));
    }

    @BuildStep(onlyIf = { CamelConfigFlags.RoutesDiscoveryEnabled.class })
    public List<CamelRoutesBuilderClassBuildItem> discoverRoutesBuilderClassNames(
            CombinedIndexBuildItem combinedIndex,
            CamelConfig camelConfig,
            List<RoutesBuilderClassExcludeBuildItem> routesBuilderClassExcludes) {

        final IndexView index = combinedIndex.getIndex();

        Set<ClassInfo> allKnownImplementors = new HashSet<>();
        allKnownImplementors.addAll(index.getAllKnownImplementors(ROUTES_BUILDER_TYPE));
        allKnownImplementors.addAll(index.getAllKnownSubclasses(ROUTE_BUILDER_TYPE));
        allKnownImplementors.addAll(index.getAllKnownSubclasses(ADVICE_WITH_ROUTE_BUILDER_TYPE));

        final Predicate<DotName> pathFilter = new PathFilter.Builder()
                .exclude(
                        routesBuilderClassExcludes.stream()
                                .map(RoutesBuilderClassExcludeBuildItem::getPattern)
                                .collect(Collectors.toList()))
                .exclude(camelConfig.routesDiscovery.excludePatterns)
                .include(camelConfig.routesDiscovery.includePatterns)
                .build().asDotNamePredicate();

        return allKnownImplementors
                .stream()
                // public and non-abstract
                .filter(ci -> ((ci.flags() & (Modifier.ABSTRACT | Modifier.PUBLIC)) == Modifier.PUBLIC))
                .map(ClassInfo::name)
                .filter(pathFilter)
                .map(CamelRoutesBuilderClassBuildItem::new)
                .collect(Collectors.toList());
    }

    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep
    CamelComponentNameResolverBuildItem componentNameResolver(
            ApplicationArchivesBuildItem applicationArchives,
            CamelConfig camelConfig,
            CamelRecorder recorder) {

        PathFilter pathFilter = new PathFilter.Builder()
                .include("META-INF/services/org/apache/camel/component/*")
                .exclude(camelConfig.service.registry.excludePatterns)
                .build();

        Set<String> componentNames = CamelSupport.services(applicationArchives, pathFilter)
                .map(CamelServiceBuildItem::getName)
                .collect(Collectors.collectingAndThen(Collectors.toUnmodifiableSet(), TreeSet::new));

        return new CamelComponentNameResolverBuildItem(recorder.createComponentNameResolver(componentNames));
    }

    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep
    CamelPackageScanClassResolverBuildItem packageScanClassResolver(
            List<CamelPackageScanClassBuildItem> camelPackageScanClassBuildItems,
            CamelRecorder recorder) {
        Set<? extends Class<?>> packageScanClassCache = camelPackageScanClassBuildItems.stream()
                .map(CamelPackageScanClassBuildItem::getClassNames)
                .flatMap(Set::stream)
                .map(className -> CamelSupport.loadClass(className, Thread.currentThread().getContextClassLoader()))
                .collect(Collectors.toUnmodifiableSet());

        return new CamelPackageScanClassResolverBuildItem(recorder.createPackageScanClassResolver(packageScanClassCache));

    }

    @BuildStep
    NativeImageResourceBuildItem initResources() {
        return new NativeImageResourceBuildItem(
                "META-INF/services/org/apache/camel/bean-processor-factory",
                "META-INF/services/org/apache/camel/rest-registry-factory");
    }

    @Overridable
    @BuildStep
    @Record(value = ExecutionTime.STATIC_INIT, optional = true)
    CamelModelReifierFactoryBuildItem modelReifierFactory(
            CamelRecorder recorder) {

        return new CamelModelReifierFactoryBuildItem(recorder.modelReifierFactory());
    }

    /**
     * Useful for identifying Camel services that are potentially not covered by inclusion patterns
     */
    private void debugCamelServiceInclusion(ApplicationArchivesBuildItem applicationArchives,
            List<CamelServicePatternBuildItem> servicePatterns) {
        PathFilter.Builder pathBuilder = new PathFilter.Builder();
        servicePatterns.forEach(camelServicePatternBuildItem -> {
            camelServicePatternBuildItem.getPatterns().forEach(pathBuilder::include);
        });

        PathFilter filter = pathBuilder.build();
        HashSet<String> missingServiceIncludes = new HashSet<>();

        for (ApplicationArchive archive : applicationArchives.getAllApplicationArchives()) {
            for (Path root : archive.getRootDirectories()) {
                final Path resourcePath = root.resolve("META-INF/services/org/apache/camel");

                if (!Files.isDirectory(resourcePath)) {
                    continue;
                }

                try (Stream<Path> files = Files.walk(resourcePath)) {
                    files.filter(Files::isRegularFile).forEach(file -> {
                        Path key = root.relativize(file);
                        if (!filter.asPathPredicate().test(key)) {
                            missingServiceIncludes.add(key.toString());
                        }
                    });
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        if (!missingServiceIncludes.isEmpty()) {
            // Note this is only partly reliable info as some include patterns are provided elsewhere independently of camel-quarkus-core
            LOGGER.debug("Found potential missing service include patterns for the following paths:");
            missingServiceIncludes.forEach(path -> {
                LOGGER.debug("Missing service include path: {}", path);
            });
        }
    }
}
