/*
 * 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.deltaspike.core.api.provider;

import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.AfterDeploymentValidation;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.BeforeShutdown;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.Extension;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.deltaspike.core.api.config.base.CoreBaseConfig;
import org.apache.deltaspike.core.util.ClassUtils;


/**
 * This class provides access to the {@link BeanManager} by registering the current {@link BeanManager} in an extension
 * and making it available via a singleton factory for the current application.
 *
 * <p>This is really handy when you need to access CDI functionality from places where no injection is available.</p>
 *
 * <p>If a simple but manual bean lookup is needed, it's easier to use the {@link BeanProvider}.</p>
 * 
 * <p>As soon as an application shuts down, the reference to the {@link BeanManager} is removed.</p>
 * 
 * <p>
 * Usage:

 * <pre>
 * BeanManager bm = BeanManagerProvider.getInstance().getBeanManager();</pre>
 * </p>
 * <p>
 * <b>Attention:</b> This approach is intended for use in user code at runtime. If BeanManagerProvider is used during
 * Container boot (in an Extension), non-portable behaviour results. During bootstrapping, an Extension shall
 * &#064;Inject BeanManager to get access to the underlying BeanManager (see e.g. {@link #cleanupFinalBeanManagers}).
 * This is the only way to guarantee that the right BeanManager is obtained in more complex Container scenarios.</p>
 */
public class BeanManagerProvider implements Extension
{
    private static final Logger  LOG = Logger.getLogger(BeanManagerProvider.class.getName());

    private static BeanManagerProvider bmpSingleton;

    /**
     * This data container is used for storing the BeanManager for each web application. This is needed in EAR or other
     * multi-webapp scenarios when the DeltaSpike classes (jars) are provided in a shared ClassLoader.
     */
    private static class BeanManagerInfo
    {
        /**
         * The BeanManager picked up via Extension loading.
         */
        private BeanManager loadTimeBm;

        /**
         * The final BeanManager. After the container did finally boot, we first try to resolve them from JNDI, and only
         * if we don't find any BM there we take the ones picked up at startup.
         */
        private BeanManager finalBm;

        /**
         * Whether the CDI Application has finally booted. Please note that this is only a nearby value as there is no
         * reliable event for this status in EE6.
         */
        private boolean booted;
    }

    /**
     * The BeanManagerInfo for the current ClassLoader.
     * 
     * <p><b>Attention:</b> This instance must only be used through the {@link #bmpSingleton} singleton!</p>
     */
    private volatile Map<ClassLoader, BeanManagerInfo> bmInfos = new ConcurrentHashMap<>();

    /**
     * Indicates whether the {@link BeanManagerProvider} has been initialized. Usually it's not necessary to call this
     * method in application code. It's useful e.g. for other frameworks to check if DeltaSpike and the CDI container in
     * general have been started.
     *
     * @return true if the BeanManagerProvider is ready to be used
     */
    public static boolean isActive()
    {
        // CDI#current delegation enabled, skip everything
        if (CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP)
        {
            return bmpSingleton != null;
        }

        return bmpSingleton != null && bmpSingleton.bmInfos.containsKey(ClassUtils.getClassLoader(null));
    }

    /**
     * Returns the current provider instance which provides access to the current {@link BeanManager}.
     *
     * @throws IllegalStateException if the {@link BeanManagerProvider} isn't ready to be used. That's the case if the
     *                               environment isn't configured properly and therefore the {@link AfterBeanDiscovery}
     *                               hasn't been called before this method gets called.
     * @return the singleton BeanManagerProvider
     */
    public static BeanManagerProvider getInstance()
    {
        if (bmpSingleton == null)
        {
            throw new IllegalStateException("No " + BeanManagerProvider.class.getName() + " in place! " +
                    "Please ensure that you configured the CDI implementation of your choice properly. " +
                    "If your setup is correct, please clear all caches and compiled artifacts.");
        }
        return bmpSingleton;
    }

    /**
     * It doesn't really matter which of the system events is used to obtain the BeanManager, but
     * {@link AfterBeanDiscovery} has been chosen since it allows all events which occur after the
     * {@link AfterBeanDiscovery} to use the {@link BeanManagerProvider}.
     *
     * @param afterBeanDiscovery event which we don't actually use ;)
     * @param beanManager        the BeanManager we store and make available.
     */
    public void setBeanManager(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager)
    {
        setBeanManagerProvider(this);

        // CDI#current delegation enabled, skip everything
        if (CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP &&
            resolveBeanManagerViaStaticHelper() != null)
        {
            return;
        }

        BeanManagerInfo bmi = getBeanManagerInfo(ClassUtils.getClassLoader(null));
        bmi.loadTimeBm = beanManager;
    }

    /**
     * The active {@link BeanManager} for the current application (current {@link ClassLoader}). This method will throw
     * an {@link IllegalStateException} if the BeanManager cannot be found.
     *
     * @return the current BeanManager, never <code>null</code>
     *
     * @throws IllegalStateException if the BeanManager cannot be found
     */
    public BeanManager getBeanManager()
    {
        // CDI#current delegation enabled, skip everything
        if (CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP)
        {
            BeanManager bm = resolveBeanManagerViaStaticHelper();
            if (bm != null)
            {
                return bm;
            }
        }

        BeanManagerInfo bmi = getBeanManagerInfo(ClassUtils.getClassLoader(null));

        if (!bmi.booted)
        {
            // This is a workaround for some containers with messed up EAR handling.
            // Those containers might boot up with the shared ear ClassLoader
            // and later run the WARs with their own child ClassLoaders.
            if (bmi.loadTimeBm == null)
            {
                BeanManagerInfo parentBmi = getParentBeanManagerInfo(ClassUtils.getClassLoader(null));
                if (parentBmi != null)
                {
                    bmi.loadTimeBm = parentBmi.loadTimeBm;
                }
            }
        }

        BeanManager result = bmi.finalBm;

        if (result == null)
        {
            synchronized (bmi)
            {
                result = bmi.finalBm;
                if (result == null)
                {
                    // first we look for a BeanManager from JNDI
                    result = resolveBeanManagerViaJndi();

                    if (result == null)
                    {
                        // if none found, we take the one we got from the Extension loading
                        result = bmi.loadTimeBm;
                    }

                    if (result == null)
                    {
                        throw new IllegalStateException("Unable to find BeanManager. " +
                                "Please ensure that you configured the CDI implementation of your choice properly.");
                    }

                    // store the resolved BeanManager in the result cache until #cleanupFinalBeanManagers gets called
                    // -> afterwards the next call of #getBeanManager will trigger the final lookup
                    bmi.finalBm = result;
                }
            }
        }

        return result;
    }

    /**
     * By cleaning the final BeanManager map after the deployment gets validated, premature loading of information from
     * JNDI is prevented in cases where the container might not be fully setup yet.
     *
     * This might happen if the BeanManagerProvider is used in an extension during CDI bootstrap. This should be
     * generally avoided. Instead, an injected BeanManager should be used in Extensions and propagated using setters.
     *
     * In EARs with multiple webapps, each WAR might get a different Extension. This depends on the container used.
     */
    public void cleanupFinalBeanManagers(@Observes AfterDeploymentValidation adv)
    {
        // CDI#current delegation enabled, skip everything
        if (CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP &&
            resolveBeanManagerViaStaticHelper() != null)
        {
            return;
        }

        for (BeanManagerInfo bmi : bmpSingleton.bmInfos.values())
        {
            bmi.finalBm = null;
            bmi.booted = true;

            /*possible issue with >weld< based servers:
            if #getBeanManager gets called in a custom AfterDeploymentValidation observer >after< this observer,
            the wrong bean-manager might get stored (not deterministic due to the unspecified order of observers).
            finally a bean-manager for a single bda will be stored and returned (which isn't the bm exposed via jndi).*/
        }
    }

    /**
     * Cleanup on container shutdown.
     *
     * @param beforeShutdown CDI shutdown event
     */
    public void cleanupStoredBeanManagerOnShutdown(@Observes BeforeShutdown beforeShutdown)
    {
        // CDI#current delegation enabled, skip everything
        if (CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP)
        {
            return;
        }

        if (bmpSingleton == null)
        {
            // this happens if there has been a failure at startup
            return;
        }

        bmpSingleton.bmInfos.remove(ClassUtils.getClassLoader(null));
    }

    /**
     * Get the BeanManager from the JNDI registry.
     *
     * @return current {@link BeanManager} which is provided via JNDI
     */
    private BeanManager resolveBeanManagerViaJndi()
    {
        try
        {
            // this location is specified in JSR-299 and must be
            // supported in all certified EE environments
            return (BeanManager) new InitialContext().lookup("java:comp/BeanManager");
        }
        catch (NamingException e)
        {
            //workaround didn't work -> return null
            return null;
        }
    }

    private BeanManager resolveBeanManagerViaStaticHelper()
    {
        try
        {
            return CDI.current().getBeanManager();
        }
        catch (Throwable t)
        {
            LOG.log(Level.FINEST, "failed to delegate bean-manager lookup -> fallback to default.", t);
            return null;
        }
    }

    /**
     * Get or create the BeanManagerInfo for the given ClassLoader.
     */
    private BeanManagerInfo getBeanManagerInfo(ClassLoader cl)
    {
        BeanManagerInfo bmi = bmpSingleton.bmInfos.get(cl);

        if (bmi == null)
        {
            synchronized (this)
            {
                bmi = bmpSingleton.bmInfos.get(cl);
                if (bmi == null)
                {
                    bmi = new BeanManagerInfo();
                    bmpSingleton.bmInfos.put(cl, bmi);
                    if (cl.getParent() != null && !bmpSingleton.bmInfos.containsKey(cl.getParent()))
                    {
                        bmpSingleton.bmInfos.put(cl.getParent(), bmi);
                    }
                }
            }
        }

        return bmi;
    }

    /**
     * This function exists to prevent findbugs from complaining about setting a static member from a non-static
     * function.
     *
     * @param beanManagerProvider the bean-manager-provider which should be used if there isn't an existing provider
     *
     * @return the first BeanManagerProvider
     */
    private static BeanManagerProvider setBeanManagerProvider(BeanManagerProvider beanManagerProvider)
    {
        if (bmpSingleton == null)
        {
            bmpSingleton = beanManagerProvider;
        }

        return bmpSingleton;
    }

    /**
     * This method recurses into the parent ClassLoaders and checks whether a BeanManagerInfo for it exists.
     *
     * @return the BeanManagerInfo of the parent ClassLoader hierarchy if any exists, or <code>null</code> if there is
     *         no {@link BeanManagerInfo} for the ClassLoaders in the hierarchy.
     */
    private BeanManagerInfo getParentBeanManagerInfo(ClassLoader classLoader)
    {
        ClassLoader parentClassLoader = classLoader.getParent();
        if (parentClassLoader == null)
        {
            return null;
        }

        BeanManagerInfo bmi = getBeanManagerInfo(parentClassLoader);
        if (bmi == null)
        {
            // recursive call up to the root ClassLoader
            bmi = getParentBeanManagerInfo(parentClassLoader);
        }

        return bmi;
    }

}
