// Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
//
// Licensed 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.tapestry5.internal.spring;

import org.apache.tapestry5.internal.AbstractContributionDef;
import org.apache.tapestry5.ioc.*;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.def.ContributionDef;
import org.apache.tapestry5.ioc.def.DecoratorDef;
import org.apache.tapestry5.ioc.def.ModuleDef;
import org.apache.tapestry5.ioc.def.ServiceDef;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.services.RegistryShutdownHub;
import org.apache.tapestry5.plastic.PlasticUtils;
import org.apache.tapestry5.spring.ApplicationContextCustomizer;
import org.apache.tapestry5.spring.SpringConstants;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.SpringVersion;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletContext;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A wrapper that converts a Spring {@link ApplicationContext} into a set of service definitions,
 * compatible with
 * Tapestry 5 IoC, for the beans defined in the context, as well as the context itself.
 */
public class SpringModuleDef implements ModuleDef
{
    static final String SERVICE_ID = "ApplicationContext";

    private final Map<String, ServiceDef> services = CollectionFactory.newMap();

    private final boolean compatibilityMode;

    private final AtomicBoolean applicationContextCreated = new AtomicBoolean(false);

    private final ServletContext servletContext;

    private ApplicationContext locateExternalContext()
    {
        ApplicationContext context = locateApplicationContext(servletContext);

        applicationContextCreated.set(true);

        return context;
    }

    /**
     * Invoked to obtain the Spring ApplicationContext, presumably stored in the ServletContext.
     * This method is only used in Tapestry 5.0 compatibility mode (in Tapestry 5.1 and above,
     * the default is for Tapestry to <em>create</em> the ApplicationContext).
     *
     * @param servletContext used to locate the ApplicationContext
     * @return the ApplicationContext itself
     * @throws RuntimeException if the ApplicationContext could not be located or is otherwise invalid
     * @since 5.2.0
     */
    protected ApplicationContext locateApplicationContext(ServletContext servletContext)
    {
        ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext);

        if (context == null)
        {
            throw new NullPointerException(
                    String
                            .format(
                                    "No Spring ApplicationContext stored in the ServletContext as attribute '%s'. "
                                            + "You should either re-enable Tapestry as the creator of the ApplicationContext, or "
                                            + "add a Spring ContextLoaderListener to web.xml.",
                                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE));
        }

        return context;
    }

    public SpringModuleDef(ServletContext servletContext)
    {
        this.servletContext = servletContext;

        compatibilityMode = Boolean.parseBoolean(servletContext
                .getInitParameter(SpringConstants.USE_EXTERNAL_SPRING_CONTEXT));

        final ApplicationContext externalContext = compatibilityMode ? locateExternalContext()
                : null;

        if (compatibilityMode)
            addServiceDefsForSpringBeans(externalContext);

        ServiceDef applicationContextServiceDef = new ServiceDef()
        {
            @Override
            public ObjectCreator createServiceCreator(final ServiceBuilderResources resources)
            {
                if (compatibilityMode)
                    return new StaticObjectCreator(externalContext,
                            "externally configured Spring ApplicationContext");

                ApplicationContextCustomizer customizer = resources.getService(
                        "ApplicationContextCustomizer", ApplicationContextCustomizer.class);

                return constructObjectCreatorForApplicationContext(resources, customizer);
            }

            @Override
            public String getServiceId()
            {
                return SERVICE_ID;
            }

            @Override
            public Set<Class> getMarkers()
            {
                return Collections.emptySet();
            }

            @Override
            public Class getServiceInterface()
            {
                return compatibilityMode ? externalContext.getClass()
                        : ConfigurableWebApplicationContext.class;
            }

            @Override
            public String getServiceScope()
            {
                return ScopeConstants.DEFAULT;
            }

            @Override
            public boolean isEagerLoad()
            {
                return false;
            }
        };

        services.put(SERVICE_ID, applicationContextServiceDef);
    }

    private void addServiceDefsForSpringBeans(ApplicationContext context)
    {
        for (final String beanName : context.getBeanDefinitionNames())
        {
            String trueName = beanName.startsWith("&") ? beanName.substring(1) : beanName;

            services.put(trueName, new SpringBeanServiceDef(trueName, context));
        }
    }

    private ObjectCreator constructObjectCreatorForApplicationContext(
            final ServiceBuilderResources resources, @Primary
    ApplicationContextCustomizer customizer)
    {
        final CustomizingContextLoader loader = new CustomizingContextLoader(customizer);

        final Runnable shutdownListener = new Runnable()
        {
            @Override
            public void run()
            {
                loader.closeWebApplicationContext(servletContext);
            }
        };

        final RegistryShutdownHub shutdownHub = resources.getService(RegistryShutdownHub.class);

        return new ObjectCreator()
        {
            @Override
            public Object createObject()
            {
                return resources.getTracker().invoke(
                        "Creating Spring ApplicationContext via ContextLoader",
                        new Invokable<Object>()
                        {
                            @Override
                            public Object invoke()
                            {
                                resources.getLogger().info(
                                        String.format("Starting Spring (version %s)", SpringVersion
                                                .getVersion()));

                                WebApplicationContext context = loader
                                        .initWebApplicationContext(servletContext);

                                shutdownHub.addRegistryShutdownListener(shutdownListener);

                                applicationContextCreated.set(true);

                                return context;
                            }
                        });
            }

            @Override
            public String toString()
            {
                return "ObjectCreator for Spring ApplicationContext";
            }
        };
    }

    @Override
    public Class getBuilderClass()
    {
        return null;
    }

    /**
     * Returns a contribution, "SpringBean", to the MasterObjectProvider service. It is ordered
     * after the built-in
     * contributions.
     */
    @Override
    public Set<ContributionDef> getContributionDefs()
    {
        ContributionDef def = createContributionToMasterObjectProvider();

        return CollectionFactory.newSet(def);
    }

    private ContributionDef createContributionToMasterObjectProvider()
    {

        ContributionDef def = new AbstractContributionDef()
        {
            @Override
            public String getServiceId()
            {
                return "MasterObjectProvider";
            }

            @Override
            public void contribute(ModuleBuilderSource moduleSource, ServiceResources resources,
                                   OrderedConfiguration configuration)
            {
                final OperationTracker tracker = resources.getTracker();

                final ApplicationContext context = resources.getService(SERVICE_ID,
                        ApplicationContext.class);

                final ObjectProvider springBeanProvider = new ObjectProvider()
                {
                    @Override
                    public <T> T provide(Class<T> objectType,
                                         AnnotationProvider annotationProvider, ObjectLocator locator)
                    {

                        Map beanMap = context.getBeansOfType(objectType);

                        switch (beanMap.size())
                        {
                            case 0:
                                return null;

                            case 1:

                                Object bean = beanMap.values().iterator().next();

                                return objectType.cast(bean);

                            default:

                                String message = String
                                        .format(
                                                "Spring context contains %d beans assignable to type %s: %s.",
                                                beanMap.size(), PlasticUtils.toTypeName(objectType), InternalUtils
                                                .joinSorted(beanMap.keySet()));

                                throw new IllegalArgumentException(message);
                        }
                    }
                };

                final ObjectProvider springBeanProviderInvoker = new ObjectProvider()
                {
                    @Override
                    public <T> T provide(final Class<T> objectType,
                                         final AnnotationProvider annotationProvider, final ObjectLocator locator)
                    {
                        return tracker.invoke(
                                "Resolving dependency by searching Spring ApplicationContext",
                                new Invokable<T>()
                                {
                                    @Override
                                    public T invoke()
                                    {
                                        return springBeanProvider.provide(objectType,
                                                annotationProvider, locator);
                                    }
                                });
                    }
                };

                ObjectProvider outerCheck = new ObjectProvider()
                {
                    @Override
                    public <T> T provide(Class<T> objectType,
                                         AnnotationProvider annotationProvider, ObjectLocator locator)
                    {
                        // I think the following line is the only reason we put the
                        // SpringBeanProvider here,
                        // rather than in SpringModule.

                        if (!applicationContextCreated.get())
                            return null;

                        return springBeanProviderInvoker.provide(objectType, annotationProvider,
                                locator);
                    }
                };


                configuration.add("SpringBean", outerCheck, "after:AnnotationBasedContributions", "after:ServiceOverride");
            }
        };

        return def;
    }

    /**
     * Returns an empty set.
     */
    @Override
    public Set<DecoratorDef> getDecoratorDefs()
    {
        return Collections.emptySet();
    }

    @Override
    public String getLoggerName()
    {
        return SpringModuleDef.class.getName();
    }

    @Override
    public ServiceDef getServiceDef(String serviceId)
    {
        return services.get(serviceId);
    }

    @Override
    public Set<String> getServiceIds()
    {
        return services.keySet();
    }
}
