package org.infinispan.cdi.common.util;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import javax.naming.InitialContext;
import javax.naming.NamingException;

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.Extension;


/**
 * <p>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>
 * <p>This is really handy if you like 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 BeanManagerProvider}.</p>
 * <p>As soon as an application shuts down, the reference to the {@link BeanManager} will be removed.<p>
 * <p>Usage:</p>
 * <pre>
 * BeanManager bm = BeanManagerProvider.getInstance().getBeanManager();
 *
 * </pre>
 *
 * <p><b>Attention:</b> This method is intended for being used in user code at runtime.
 * If this method gets 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 to get the right
 * BeanManager 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 = null;

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

        /**
         * The final BeanManagers.
         * 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 volatile BeanManager finalBm = null;

        /**
         * 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 = false;
    }

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

    /**
     * Returns if the {@link BeanManagerProvider} has been initialized.
     * Usually it isn't needed to call this method in application code.
     * It's e.g. useful for other frameworks to check if DeltaSpike and the CDI container in general have been started.
     *
     * @return true if the bean-manager-provider is ready to be used
     */
    public static boolean isActive()
    {
        return bmpSingleton != null;
    }

    /**
     * Allows to get 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 be called before this method gets called.
     * @return the singleton BeanManagerProvider
     */
    public static BeanManagerProvider getInstance()
    {
        /*X TODO Java-EE5 support needs to be discussed
        if (bmpSingleton == null)
        {
            // workaround for some Java-EE5 environments in combination with a special
            // StartupBroadcaster for bootstrapping CDI

            // CodiStartupBroadcaster.broadcastStartup();
            // here bmp might not be null (depends on the broadcasters)
        }
        */

        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 basically doesn't matter which of the system events we use,
     * but basically we use the {@link AfterBeanDiscovery} event since it allows to use the
     * {@link BeanManagerProvider} for all events which occur after the {@link AfterBeanDiscovery} event.
     *
     * @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);

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

    /**
     * The active {@link BeanManager} for the current application (/{@link ClassLoader}). This method will throw an
     * {@link IllegalStateException} if the BeanManager cannot be found.
     *
     * @return the current bean-manager, never <code>null</code>
     * @throws IllegalStateException if the BeanManager cannot be found
     */
    public BeanManager getBeanManager()
    {
        BeanManagerInfo bmi = getBeanManagerInfo(getClassLoader(null));

        // warn the user if he tries to use the BeanManager before container startup
        if (!bmi.booted)
        {
            LOG.warning("When using the BeanManager to retrieve Beans before the Container is started," +
                    " non-portable behaviour results!");
        }

        if (bmi.finalBm == null)
        {
            synchronized (this)
            {
                if (bmi.finalBm == null)
                {
                    // first we look for a BeanManager from JNDI
                    BeanManager 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 bmi.finalBm;
    }

    /**
     * Detect the right ClassLoader.
     * The lookup order is determined by:
     * <ol>
     * <li>ContextClassLoader of the current Thread</li>
     * <li>ClassLoader of the given Object 'o'</li>
     * <li>ClassLoader of this very ClassUtils class</li>
     * </ol>
     *
     * @param o if not <code>null</code> it may get used to detect the classloader.
     * @return The {@link ClassLoader} which should get used to create new instances
     */
    public static ClassLoader getClassLoader(Object o) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        if (loader == null && o != null) {
            loader = o.getClass().getClassLoader();
        }
        if (loader == null) {
            loader = BeanManagerProvider.class.getClassLoader();
        }
        return loader;
    }

    /**
     * By cleaning the final BeanManager map after the Deployment got Validated,
     * we prevent premature loading of information from JNDI in cases where the
     * container might not be fully setup yet.
     *
     * This might happen if someone uses the BeanManagerProvider during Extension
     * startup.
     */
    public void cleanupFinalBeanManagers(@Observes AfterDeploymentValidation adv)
    {
        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)
    {
        if (bmpSingleton == null)
        {
            // this happens if there has been a failure at startup
            return;
        }

        ClassLoader classLoader = getClassLoader(null);
        bmpSingleton.bmInfos.remove(classLoader);

        //X TODO this might not be enough as there might be
        //X ClassLoaders used during Weld startup which are not the TCCL...
    }

    /**
     * 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;
        }
    }

    /**
     * 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);
                }
            }
        }

        return bmi;
    }

    /**
     * This function exists to prevent findbugs to complain 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;
    }
}
