/*
 * 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 org.springframework.beans.BeanMetadataAttribute;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
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.naming.BeanNamingStrategy;
import org.springframework.config.java.naming.MethodNameStrategy;
import org.springframework.config.java.process.ConfigurationPostProcessor;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractRefreshableApplicationContext;

import java.io.IOException;

import java.util.ArrayList;


/**
 * Implementation of {@link ApplicationContext} that accepts {@link Configuration @Configuration}
 * class literals and/or base package paths containing {@link Configuration @Configuration} classes.
 * This feature enables users to avoid XML entirely and go "pure Java" with their configurations.
 *
 * <p>As with many {@link ApplicationContext} implementations, a number of constructors are
 * provided, most of which cause a {@link #refresh()} immediately for convenience. Others, such as
 * {@link #JavaConfigApplicationContext()} do not, and leave the context "open for configuration"
 * such that additional methods such as {@link #addConfigClass(Class)},
 * {@link #setParent(ApplicationContext)}, etc may be called. See individual constructors for more
 * detail.</p>
 *
 * @author  Chris Beams
 * @see     JavaConfigWebApplicationContext for use in web applications
 */
public class JavaConfigApplicationContext extends AbstractRefreshableApplicationContext
    implements ConfigurableJavaConfigApplicationContext {

    private final ClassPathScanningConfigurationProvider scanner = new ClassPathScanningConfigurationProvider(this);

    // TODO: should be LinkedHashSet?
    private final ArrayList<Class<?>> configClasses = new ArrayList<Class<?>>();

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

    private BeanNamingStrategy beanNamingStrategy = new MethodNameStrategy();

    /** context is configurable until refresh() is called. */
    private boolean openForConfiguration = true;

    /** TODO: JAVADOC */
    public JavaConfigApplicationContext() { }

    public JavaConfigApplicationContext(Class<?>... classes) {
        for(Class<?> cls : classes)
            addConfigClass(cls);
        refresh();
    }

    public JavaConfigApplicationContext(String... basePackages) {
        for(String pkg : basePackages)
            addBasePackage(pkg);
        
        refresh();
    }

    /**
     * Create a new context with <var>parent</var> as the parent. Requires an explicit call to
     * refresh().
     *
     * @param  parent
     */
    public JavaConfigApplicationContext(JavaConfigApplicationContext parent) {
        super(parent);
    }

    public JavaConfigApplicationContext(Class<?>[] classes, String[] basePackages) {
        for(Class<?> cls : classes)
            addConfigClass(cls);
        
        for(String basePackage : basePackages)
            addBasePackage(basePackage);
        
        refresh();
    }

    public JavaConfigApplicationContext(ApplicationContext parent, Class<?>... classes) {
        super(parent);
        for(Class<?> cls : classes)
            addConfigClass(cls);
        refresh();
    }

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

    @Override
    protected void finishRefresh() {
        super.finishRefresh();
        openForConfiguration = false;
    }

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

    @Override
    public void setParent(ApplicationContext parent) {
        assertOpenForConfiguration("setParent");
        super.setParent(parent);
    }

    public void setBeanNamingStrategy(BeanNamingStrategy namingStrategy) {
        this.beanNamingStrategy = namingStrategy;
    }

    public BeanNamingStrategy getBeanNamingStrategy() {
        return beanNamingStrategy;
    }

    public void addBasePackage(String basePackage) {
        assertOpenForConfiguration("addBasePackage");
        basePackages.add(basePackage);
    }

    public void addConfigClass(Class<?> cls) {
        assertOpenForConfiguration("addConfigClass");
        configClasses.add(cls);
    }

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

    private void assertOpenForConfiguration(String attemptedMethod) {
        if (!openForConfiguration)
            throw new IllegalStateException(format(
                    "context is closed for configuration. %s() must be called "
                    + "before refresh(), consider using the no-arg constructor for %s",
                    attemptedMethod, this.getClass().getSimpleName()));
    }

}
