package org.jboss.seam.ioc.spring;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.contexts.Lifecycle;
import org.jboss.seam.init.Initialization;
import org.jboss.seam.ioc.IoCComponent;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.util.ClassUtils;

/**
 * An extension of Component that allows spring to provide the base instance for a seam component.
 *
 * @author youngm
 */
public class SpringComponent extends IoCComponent
{
    private static final String SPRING_COMPONENT_NAME_MAP = "org.jboss.seam.SpringComponentNameMap";

    public static final String DESTRUCTION_CALLBACK_NAME_PREFIX = IoCComponent.class.getName()
    + ".DESTRUCTION_CALLBACK.";

    private BeanFactory beanfactory;

    private Boolean interceptionEnabled;

    private String springBeanName;

    private static final ThreadLocal<ObjectFactory> objectFactory = new ThreadLocal<ObjectFactory>();

    public static ObjectFactory getObjectFactory()
    {
        return objectFactory.get();
    }

    public static void setObjectFactory(ObjectFactory bean)
    {
        objectFactory.set(bean);
    }

    /**
     * Utility to add a SpringComponent to the seam component ApplicationContext.
     *
     * @param componentName the seam component name to use
     * @param springBeanName the spring bean name to map to this seam component
     * @param beanClassName the seam beanClass to use
     * @param scopeType the scope of this component
     * @param beanFactory the beanfactory this spring bean exists in
     * @param intercept the InterceptorTyp to force the bean to use. Will override any annotations on the bean.
     */
    public static void addSpringComponent(String componentName, String springBeanName, String beanClassName,
            ScopeType scopeType, BeanFactory beanFactory, Boolean intercept)
    {
        // mock the application context
        // TODO reuse
        boolean unmockApplication = false;
        if (!Contexts.isApplicationContextActive())
        {
            Lifecycle.mockApplication();
            unmockApplication = true;
        }
        try
        {
            if (Component.forName(componentName) != null)
            {
                throw new IllegalStateException("Cannot add spring component to seam with name: " + componentName
                        + ".  There is already a seam component with that name.");
            }
            Map<String, String> springComponentNameMap = getSpringComponentNameMap();
            // Add an entry to the spring+seam name association map
            springComponentNameMap.put(springBeanName, componentName);
            Class beanClass = ClassUtils.forName(beanClassName);
            // Add the component to seam
            Contexts.getApplicationContext().set(
                    componentName + Initialization.COMPONENT_SUFFIX,
                    new SpringComponent(beanClass, componentName, springBeanName, scopeType, beanFactory,
                            intercept));
        }
        catch (ClassNotFoundException e)
        {
            throw new FatalBeanException("Error", e);
        }
        finally
        {
            if (unmockApplication)
            {
                Lifecycle.unmockApplication();
            }
        }
    }

    @SuppressWarnings("unchecked")
    private static Map<String, String> getSpringComponentNameMap()
    {
        if (Contexts.getApplicationContext().get(SPRING_COMPONENT_NAME_MAP) == null)
        {
            Contexts.getApplicationContext().set(SPRING_COMPONENT_NAME_MAP, new HashMap<String, String>());
        }
        return (Map<String, String>) Contexts.getApplicationContext().get(SPRING_COMPONENT_NAME_MAP);
    }

    /**
     * Just like Component.forName() but mocks the applicationContext and you provide it with the spring bean name
     * instead of the seam component name.
     *
     * @param springBeanName the spring bean name.
     * @return the SpringComponent mapped to that spring bean name.
     */
    public static SpringComponent forSpringBeanName(String springBeanName)
    {
        // TODO reuse
        boolean unmockApplication = false;
        if (!Contexts.isApplicationContextActive())
        {
            Lifecycle.mockApplication();
            unmockApplication = true;
        }
        try
        {
            return (SpringComponent) Component.forName(getSpringComponentNameMap().get(springBeanName));
        }
        finally
        {
            if (unmockApplication)
            {
                Lifecycle.unmockApplication();
            }
        }
    }

    /**
     * Creates a Spring Seam Component given a beanFactory.
     *
     * @param clazz the seam beanClass to use
     * @param componentName component name
     * @param springBeanName the spring bean name
     * @param scope component scope
     * @param factory the beanfactory this spring component should use
     * @param intercept the interception type
     */
    public SpringComponent(Class clazz, String componentName, String springBeanName, ScopeType scope,
            BeanFactory factory, Boolean intercept)
    {
        super(clazz, componentName, scope);
        this.springBeanName = springBeanName;
        this.beanfactory = factory;
        this.interceptionEnabled = intercept;
    }

    @Override
    protected String getIoCName()
    {
        return "Spring";
    }

    @Override
    protected Object instantiateIoCBean() throws Exception
    {
        ObjectFactory objectFactory = getObjectFactory();
        if (objectFactory == null)
        {
            return beanfactory.getBean(springBeanName);
        }
        setObjectFactory(null);
        return objectFactory.getObject();
    }

    /**
     * Calls the spring destroy callback when seam destroys the component
     *
     * @see org.jboss.seam.Component#callDestroyMethod(Object)
     */
    @Override
    public void destroy(Object instance)
    {
        super.destroy(instance);
        // Cannot call the callback on a STATELESS bean because we have no way of storing it.
        if (getScope() != ScopeType.STATELESS)
        {
            Runnable callback = (Runnable) getScope().getContext().get(DESTRUCTION_CALLBACK_NAME_PREFIX + getName());
            if (callback != null)
            {
                callback.run();
            }
        }
    }

    /**
     * Registers a destruction callback with this bean.
     *
     * @param name bean name
     * @param destroy the destroy to set
     */
    public void registerDestroyCallback(String name, Runnable destroy)
    {
        // Not sure yet how to register a stateless bean's Destruction callback.
        if (getScope() != ScopeType.STATELESS)
        {
            getScope().getContext().set(DESTRUCTION_CALLBACK_NAME_PREFIX + name, destroy);
        }
    }

    /**
     * Overrides Components inject to unwrap all of the spring AOP layers so that fields can be injected into this bean.
     *
     * @see org.jboss.seam.Component#inject(java.lang.Object, boolean)
     */
    @Override
    public void inject(Object bean, boolean enforceRequired)
    {
        if (bean instanceof Advised)
        {
            try
            {
                inject(((Advised) bean).getTargetSource().getTarget(), enforceRequired);
            }
            catch (RuntimeException e)
            {
                throw e;
            }
            catch (Exception e)
            {
                throw new RuntimeException(e);
            }
        }
        super.inject(bean, enforceRequired);
    }

    /**
     * Use the InterceptionType override if available otherwise use the annotation or seam default.
     *
     * @see org.jboss.seam.Component#isInterceptionEnabled()
     */
    @Override
    public boolean isInterceptionEnabled()
    {
        if (interceptionEnabled == null)
        {
            return super.isInterceptionEnabled();
        }
        return interceptionEnabled;
    }
    
    @Override
    protected void checkSynchronizedForComponentType() {}
    
    @Override
    protected void checkPersistenceContextForComponentType() {}
    
    /**
     * Ignore PersistenceContextAttributes if scope is stateless
     */
    @Override    
    public List<BijectedAttribute> getPersistenceContextAttributes()
    {
       if(getScope().equals(ScopeType.STATELESS)) {
          return Collections.emptyList();          
       }
       return super.getPersistenceContextAttributes();
    }
}
