/*
 * 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.main;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Overridable;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.QuarkusMain;
import org.apache.camel.CamelContext;
import org.apache.camel.main.RoutesCollector;
import org.apache.camel.quarkus.core.CamelConfig;
import org.apache.camel.quarkus.core.CamelRecorder;
import org.apache.camel.quarkus.core.CamelRuntime;
import org.apache.camel.quarkus.core.deployment.CamelContextProcessor.EventBridgeEnabled;
import org.apache.camel.quarkus.core.deployment.main.spi.CamelMainBuildItem;
import org.apache.camel.quarkus.core.deployment.main.spi.CamelMainListenerBuildItem;
import org.apache.camel.quarkus.core.deployment.main.spi.CamelRoutesCollectorBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelContextBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelRoutesBuilderClassBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelRoutesLoaderBuildItems;
import org.apache.camel.quarkus.core.deployment.spi.CamelRuntimeBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.CamelRuntimeTaskBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.ContainerBeansBuildItem;
import org.apache.camel.quarkus.core.deployment.spi.RuntimeCamelContextCustomizerBuildItem;
import org.apache.camel.quarkus.main.CamelMain;
import org.apache.camel.quarkus.main.CamelMainApplication;
import org.apache.camel.quarkus.main.CamelMainConfig;
import org.apache.camel.quarkus.main.CamelMainProducers;
import org.apache.camel.quarkus.main.CamelMainRecorder;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;

public class CamelMainProcessor {

    @BuildStep
    void unremovableBeans(BuildProducer<AdditionalBeanBuildItem> beanProducer) {
        beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(CamelMainProducers.class));
    }

    @BuildStep
    @Record(value = ExecutionTime.STATIC_INIT, optional = true)
    public CamelRoutesLoaderBuildItems.Registry routesLoader(CamelConfig config, CamelRecorder recorder) {
        return config.routesDiscovery.enabled
                ? new CamelRoutesLoaderBuildItems.Registry(recorder.newDefaultRegistryRoutesLoader())
                : new CamelRoutesLoaderBuildItems.Registry(recorder.newDisabledRegistryRoutesLoader());
    }

    @Overridable
    @BuildStep
    @Record(value = ExecutionTime.STATIC_INIT, optional = true)
    public CamelRoutesCollectorBuildItem routesCollector(
            CamelMainRecorder recorder,
            CamelRoutesLoaderBuildItems.Registry registryRoutesLoader) {
        RuntimeValue<RoutesCollector> routesCollector = recorder.newRoutesCollector(registryRoutesLoader.getLoader());
        return new CamelRoutesCollectorBuildItem(routesCollector);
    }

    /**
     * This build step is responsible to assemble a camel-main instance.
     *
     * @param  beanContainer        a reference to a fully initialized CDI bean container
     * @param  containerBeans       a list of bean known by the CDI container used to filter out auto-discovered routes from
     *                              those known by the CDI container.
     * @param  recorder             the recorder.
     * @param  context              a reference to a {@link CamelContext}.
     * @param  routesCollector      a reference to a {@link org.apache.camel.main.RoutesCollector}.
     * @param  routesBuilderClasses a list of known {@link org.apache.camel.RoutesBuilder} classes.
     * @param  listeners            a list of known {@link org.apache.camel.main.MainListener} instances.
     * @return                      a build item holding a {@link CamelMain} instance.
     */
    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep
    CamelMainBuildItem main(
            BeanContainerBuildItem beanContainer,
            ContainerBeansBuildItem containerBeans,
            CamelMainRecorder recorder,
            CamelContextBuildItem context,
            CamelRoutesCollectorBuildItem routesCollector,
            List<CamelRoutesBuilderClassBuildItem> routesBuilderClasses,
            List<CamelMainListenerBuildItem> listeners,
            CamelMainConfig config) {

        RuntimeValue<CamelMain> main = recorder.createCamelMain(
                context.getCamelContext(),
                routesCollector.getValue(),
                beanContainer.getValue(),
                config.arguments.onUnknown);

        for (CamelRoutesBuilderClassBuildItem item : routesBuilderClasses) {
            // don't add routes builders that are known by the container
            if (containerBeans.getClasses().contains(item.getDotName())) {
                continue;
            }

            recorder.addRoutesBuilder(main, item.getDotName().toString());
        }

        for (CamelMainListenerBuildItem listener : listeners) {
            recorder.addListener(main, listener.getListener());
        }

        return new CamelMainBuildItem(main);
    }

    /**
     * This build step is responsible to assemble a camel-main based {@link org.apache.camel.quarkus.core.CamelRuntime}.
     * <p>
     * This implementation leverages camel-main which brings advanced auto-configuration capabilities such as:
     * <ul>
     * <li>auto-configure components/languages/data-formats through properties.
     * <li>auto-configure {@link CamelContext} traits like:
     * <ul>
     * <li>REST
     * <li>Circuit Breaker
     * <li>Health
     * </ul>
     * <li>take control of the application life-cycle and initiates Quarkus shutdown according to some conditions as
     * example
     * after having processed a certain number of messages..
     * </ul>
     *
     * @param  index           a reference to a {@link IndexView}
     * @param  beanContainer   a reference to a fully initialized CDI bean container
     * @param  recorder        the recorder.
     * @param  main            a reference to a {@link CamelMain}.
     * @param  customizers     a list of {@link org.apache.camel.spi.CamelContextCustomizer} that will be
     *                         executed before starting the {@link CamelContext} at {@link ExecutionTime#RUNTIME_INIT}.
     * @param  runtimeTasks    a placeholder to ensure all the runtime task are properly are done.
     * @param  camelMainConfig a {@link CamelMainConfig}
     * @return                 a build item holding a {@link CamelRuntime} instance.
     */
    @BuildStep
    @Record(value = ExecutionTime.RUNTIME_INIT)
    /*
     * @Consume(SyntheticBeansRuntimeInitBuildItem.class) makes sure that camel-main starts after the ArC container is
     * fully initialized. This is required as under the hoods the camel registry may look-up beans from the
     * container thus we need it to be fully initialized to avoid unexpected behaviors.
     */
    @Consume(SyntheticBeansRuntimeInitBuildItem.class)
    CamelRuntimeBuildItem runtime(
            CombinedIndexBuildItem index,
            BeanContainerBuildItem beanContainer,
            CamelMainRecorder recorder,
            CamelMainBuildItem main,
            List<RuntimeCamelContextCustomizerBuildItem> customizers,
            List<CamelRuntimeTaskBuildItem> runtimeTasks,
            CamelMainConfig camelMainConfig) {

        // Run the customizer before starting the context to give a last chance
        // to amend the Camel Context setup.
        //
        // Note that the customizer are actually materialized as main listeners
        // and bound to MainListener::afterConfigure so they are be executed as
        // last step in the Camel Context configuration process by Camel Main.
        recorder.customizeContext(
                main.getInstance(),
                customizers.stream().map(RuntimeCamelContextCustomizerBuildItem::get).collect(Collectors.toList()));

        return new CamelRuntimeBuildItem(
                recorder.createRuntime(
                        beanContainer.getValue(),
                        main.getInstance(),
                        camelMainConfig.shutdown.timeout.toMillis()),
                index.getIndex().getAnnotations(DotName.createSimple(QuarkusMain.class.getName())).isEmpty());
    }

    /**
     * Registers Camel Main CDI event bridges if quarkus.camel.event-bridge.enabled=true and if
     * the relevant events have CDI observers configured for them.
     *
     * @param beanDiscovery build item containing the results of bean discovery
     * @param main          build item containing the CamelMain instance
     * @param recorder      the CamelContext recorder instance
     */
    @Record(ExecutionTime.STATIC_INIT)
    @BuildStep(onlyIf = EventBridgeEnabled.class)
    public void registerCamelMainEventBridge(
            BeanDiscoveryFinishedBuildItem beanDiscovery,
            CamelMainBuildItem main,
            CamelMainRecorder recorder) {

        Set<String> observedMainEvents = beanDiscovery.getObservers()
                .stream()
                .map(observerInfo -> observerInfo.getObservedType().name().toString())
                .filter(observedType -> observedType.startsWith("org.apache.camel.quarkus.main.events"))
                .collect(Collectors.collectingAndThen(Collectors.toUnmodifiableSet(), HashSet::new));

        if (!observedMainEvents.isEmpty()) {
            recorder.registerCamelMainEventBridge(main.getInstance(), observedMainEvents);
        }
    }

    @BuildStep
    AdditionalIndexedClassesBuildItem indexCamelMainApplication() {
        // Required for launching CamelMain based applications from the IDE
        return new AdditionalIndexedClassesBuildItem(CamelMainApplication.class.getName());
    }
}
