/*
 * 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 javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.AfterDeploymentValidation;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.BeforeShutdown;
import javax.enterprise.inject.spi.Extension;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.lang.reflect.Method;
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());

    //for CDI 1.1+ delegation
    private static final Method CDI_CURRENT_METHOD;
    private static final Method CDI_CURRENT_BEAN_MANAGER_METHOD;

    private static BeanManagerProvider bmpSingleton;

    static
    {
        Class cdiClass = ClassUtils.tryToLoadClassForName("javax.enterprise.inject.spi.CDI");

        Method resolvedCdiCurrentMethod = null;
        Method resolvedCdiBeanManagerMethod = null;
        //only init methods if a cdi 1.1+ container is available and the delegation-mode isn't deactivated.
        //deactivation is e.g. useful if owb is used in "parallel mode" in a weld-based server.
        if (cdiClass != null && !CoreBaseConfig.BeanManagerIntegration.DELEGATE_LOOKUP)
        {

            try
            {
                resolvedCdiCurrentMethod = cdiClass.getDeclaredMethod("current");
                resolvedCdiBeanManagerMethod = cdiClass.getDeclaredMethod("getBeanManager");
            }
            catch (Exception e)
            {
                LOG.log(Level.SEVERE, "Couldn't get method from " + cdiClass.getName(), e);
            }
        }

        //null if no init happened e.g. due to CDI 1.0 or deactivated delegation-mode
        CDI_CURRENT_METHOD = resolvedCdiCurrentMethod;
        CDI_CURRENT_BEAN_MANAGER_METHOD = resolvedCdiBeanManagerMethod;
    }

    /**
     * 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<ClassLoader, BeanManagerInfo>();

    /**
     * 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()
    {
        return bmpSingleton != 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()
    {
        /*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 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);

        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()
    {
        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();

                    // needs to be here to get a better performance and
                    // even in ee7 containers we can't rely on that lookup as a primary strategy
                    // (esp. in case of EAR based applications)
                    if (result == null)
                    {
                        result = resolveBeanManagerViaStaticHelper();
                    }

                    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)
    {
        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;
        }

        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()
    {
        if (CDI_CURRENT_METHOD != null && CDI_CURRENT_BEAN_MANAGER_METHOD != null)
        {
            try
            {
                Object cdiCurrentObject = CDI_CURRENT_METHOD.invoke(null);
                return (BeanManager) CDI_CURRENT_BEAN_MANAGER_METHOD.invoke(cdiCurrentObject);
            }
            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;
    }

}
