/*
 * Copyright 2002-2008 the original author or authors.
 *
 * 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.springframework.config.java.context;

import static java.lang.String.format;
import static org.springframework.util.ClassUtils.getDefaultClassLoader;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanMetadataAttribute;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;

import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.internal.factory.TypeSafeBeanFactoryUtils;
import org.springframework.config.java.internal.model.ConfigurationClass;
import org.springframework.config.java.internal.process.InternalConfigurationPostProcessor;
import org.springframework.config.java.process.ConfigurationPostProcessor;

import org.springframework.util.Assert;
import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;

import java.io.IOException;

import java.util.ArrayList;


/**
 * JavaConfig ApplicationContext implementation for use in the web tier. May be supplied as the
 * {@literal commandClass} parameter to Spring MVC's DispatcherServlet
 *
 * <p>This class is almost wholly copy-and-pasted from {@link JavaConfigApplicationContext}. Because
 * it the two classes must maintain mutually exclusive ancestry, achieving reuse is quite
 * challenging. Consider a code-generation approach for JCWAC? See SJC-139</p>
 *
 * @author  Chris Beams
 * @see     JavaConfigApplicationContext
 * @see     org.springframework.web.context.WebApplicationContext
 * @see     org.springframework.web.servlet.DispatcherServlet
 */
public class JavaConfigWebApplicationContext extends AbstractRefreshableWebApplicationContext
                                             implements ConfigurableJavaConfigApplicationContext {

    private Log log = LogFactory.getLog(getClass());

    private final ClassPathScanningConfigurationProvider scanner = new ClassPathScanningConfigurationProvider(this);

    private ArrayList<Class<?>> configClasses = new ArrayList<Class<?>>();

    private ArrayList<String> basePackages = new ArrayList<String>();

    @Override
    protected void prepareRefresh() {
        super.prepareRefresh();
        initConfigLocations();
    }

    @Override
    protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
        BeanFactoryPostProcessor bfpp = new InternalConfigurationPostProcessor(this, null, new DefaultBeanFactoryProvider());
        bfpp.postProcessBeanFactory(beanFactory);
        super.invokeBeanFactoryPostProcessors(beanFactory);
    }

    protected void initConfigLocations() {
        Assert.notEmpty(getConfigLocations(),
                        "Zero configLocations present. Was the 'contextConfigLocations' " +
                        "context-param set properly in web.xml?");

        log.trace("JavaConfigWebApplicationContext.initConfigLocations(): number of locations: " + getConfigLocations().length);

        for (String location : getConfigLocations()) {
            log.trace("JavaConfigWebApplicationContext.initConfigLocations(): now initializing location: " + location);
            try {
                Class<?> cz = getDefaultClassLoader().loadClass(location);
                if (cz.isAnnotationPresent(Configuration.class)) {
                    configClasses.add(cz);
                } else {
                    String message =
                        "[%s] is not a valid configuration class. "
                        + "Perhaps you forgot to annotate your bean creation methods with @Bean?";
                    log.warn(format(message, cz));
                }
            } catch (ClassNotFoundException ex) {
                log.info(format("location [%s] does not appear to be a class.  Adding to list of base packages to scan.", location));
                basePackages.add(location);
            }
        }
    }

    /**
     * Loads any specified {@link Configuration @Configuration} classes as bean
     * definitions within this context's BeanFactory for later processing by
     * {@link ConfigurationPostProcessor}
     *
     * @see  #JavaConfigApplicationContext(Class...)
     * @see  #addConfigClass(Class)
     */
    @Override
    protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException, BeansException {
        for (Class<?> configClass : configClasses)
            loadBeanDefinitionForConfigurationClass(beanFactory, configClass);

        for (String basePackage : basePackages)
            loadBeanDefinitionsForBasePackage(beanFactory, basePackage);
    }

    private void loadBeanDefinitionForConfigurationClass(DefaultListableBeanFactory beanFactory, Class<?> configClass) {
        // TODO: {naming strategy} should end in # mark?
        String configBeanName = configClass.getName();
        RootBeanDefinition configBeanDef = new RootBeanDefinition();
        configBeanDef.setBeanClassName(configBeanName);
        configBeanDef.addMetadataAttribute(new BeanMetadataAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS, true));
        beanFactory.registerBeanDefinition(configBeanName, configBeanDef);
    }

    private void loadBeanDefinitionsForBasePackage(DefaultListableBeanFactory beanFactory, String basePackage) {
        for (BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) {
            String configBeanName = beanDef.getBeanClassName(); // TODO: {naming strategy}
            ((AbstractBeanDefinition) beanDef).addMetadataAttribute(
                new BeanMetadataAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS, true));
            beanFactory.registerBeanDefinition(configBeanName, beanDef);
        }
    }

    public <T> T getBean(Class<T> type) {
        return TypeSafeBeanFactoryUtils.getBean(this.getBeanFactory(), type);
    }

    public <T> T getBean(Class<T> type, String beanName) {
        return TypeSafeBeanFactoryUtils.getBean(this.getBeanFactory(), type, beanName);
    }

    public void addConfigClass(Class<?> cls) {
        String[] configLocations = getConfigLocations();
        int nLocations = (configLocations == null) ? 0 : configLocations.length;
        String[] newConfigLocations = new String[nLocations + 1];
        for (int i = 0; i < nLocations; i++)
            newConfigLocations[i] = configLocations[i];

        newConfigLocations[newConfigLocations.length - 1] = cls.getName();
        this.setConfigLocations(newConfigLocations);
    }

    /**
     * @throws UnsupportedOperationException
     */
    public void addBasePackage(String basePackage) {
        throw new UnsupportedOperationException();
    }

}
