/*
 * 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.internal.factory.support;


import static java.lang.String.format;
import static org.springframework.config.java.internal.factory.BeanVisibility.PUBLIC;
import static org.springframework.config.java.internal.factory.BeanVisibility.visibilityOf;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.springframework.util.StringUtils.hasText;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.beans.BeanMetadataAttribute;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
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.BeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.config.java.annotation.Bean;
import org.springframework.config.java.annotation.Configuration;
import org.springframework.config.java.annotation.Lazy;
import org.springframework.config.java.annotation.Meta;
import org.springframework.config.java.annotation.Primary;
import org.springframework.config.java.internal.factory.BeanFactoryProvider;
import org.springframework.config.java.internal.factory.BeanVisibility;
import org.springframework.config.java.internal.factory.JavaConfigBeanFactory;
import org.springframework.config.java.internal.model.AutoBeanMethod;
import org.springframework.config.java.internal.model.BeanMethod;
import org.springframework.config.java.internal.model.ConfigurationClass;
import org.springframework.config.java.internal.model.ConfigurationModel;
import org.springframework.config.java.internal.model.MalformedJavaConfigurationException;
import org.springframework.config.java.internal.model.UsageError;
import org.springframework.config.java.internal.util.Constants;
import org.springframework.config.java.model.ModelClass;
import org.springframework.config.java.plugin.ConfigurationPlugin;
import org.springframework.config.java.plugin.Plugin;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;


/**
 * Renders a given {@link ConfigurationModel} as bean definitions to be registered on-the-fly with a
 * given {@link BeanDefinitionRegistry}. Modeled after the {@link BeanDefinitionReader} hierarchy,
 * but could not extend directly as {@link ConfigurationModel} is not a {@link Resource}
 *
 * @author  Chris Beams
 */
public class ConfigurationModelBeanDefinitionReader {

    private static final Log logger = LogFactory.getLog(ConfigurationModelBeanDefinitionReader.class);

    private static final String TARGET_NAME_PREFIX = "scopedTarget.";

    private final JavaConfigBeanFactory beanFactory;


    public ConfigurationModelBeanDefinitionReader(JavaConfigBeanFactory beanFactory) { this.beanFactory = beanFactory; }

    /**
     * Number of bean definitions generated.
     *
     * @param   model
     *
     * @return  number of bean definitions generated
     */
    public int loadBeanDefinitions(ConfigurationModel model) {
        int initialBeanDefCount = beanFactory.getBeanDefinitionCount();

        for (ConfigurationClass configClass : model.getAllConfigurationClasses())
            loadBeanDefinitionsForConfigurationClass(configClass);

        return beanFactory.getBeanDefinitionCount() - initialBeanDefCount;
    }

    private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) {
        loadBeanDefinitionsForDeclaringClass(configClass.getDeclaringClass());

        doLoadBeanDefinitionForConfigurationClass(configClass);

        for (BeanMethod beanMethod : configClass.getBeanMethods())
            loadBeanDefinitionsForBeanMethod(configClass, beanMethod);

        for (AutoBeanMethod autoBeanMethod : configClass.getAutoBeanMethods())
            loadBeanDefinitionsForAutoBeanMethod(autoBeanMethod);

        for (Annotation pluginAnnotation : configClass.getPluginAnnotations())
            loadBeanDefinitionsForPluginAnnotation(pluginAnnotation);
    }

    @SuppressWarnings("unchecked")
    private void loadBeanDefinitionsForPluginAnnotation(Annotation pluginAnnotation) {
        // there is a fixed assumption that in order for this annotation to have
        // been registered in the first place, it must be meta-annotated with @Plugin
        // assert this as an invariant now
        Class<?> pluginAnnoClass = pluginAnnotation.getClass();
        Plugin pluginMetadata = findAnnotation(pluginAnnoClass, Plugin.class);
        Assert.isTrue(pluginMetadata != null,
                      format("%s annotation is not annotated as a @Plugin", pluginAnnoClass));

        Class<? extends ConfigurationPlugin> handler = pluginMetadata.handler();

        try {
            Constructor<? extends ConfigurationPlugin> noArgConstructor = handler.getDeclaredConstructor();
            noArgConstructor.setAccessible(true);
            ConfigurationPlugin pluginHandler =  noArgConstructor.newInstance();
            pluginHandler.handle(pluginAnnotation, beanFactory);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * The instance of the configuration class itself must be registered as a bean definition
     */
    private void doLoadBeanDefinitionForConfigurationClass(ConfigurationClass configClass) {

        Configuration metadata = configClass.getMetadata();
        
        if (metadata.checkRequired() == true) {
            RootBeanDefinition requiredAnnotationPostProcessor = new RootBeanDefinition();
            Class<?> beanClass = RequiredAnnotationBeanPostProcessor.class;
            String beanName = beanClass.getName() + "#0";
            requiredAnnotationPostProcessor.setBeanClass(beanClass);
            requiredAnnotationPostProcessor.setResourceDescription("ensures @Required methods have been invoked");
            beanFactory.registerBeanDefinition(beanName, requiredAnnotationPostProcessor, BeanVisibility.PUBLIC);
        }

        GenericBeanDefinition configBeanDef;
        configBeanDef = new GenericBeanDefinition();
        configBeanDef.setBeanClassName(configClass.getName());

        // mark this bean def with metadata indicating that it is a configuration bean
        configBeanDef.addMetadataAttribute(new BeanMetadataAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS, true));

        String configBeanId = configClass.getId();

        // consider the case where it's already been defined (probably in XML)
        // and potentially has PropertyValues and ConstructorArgs)
        if (beanFactory.containsBeanDefinition(configBeanId, PUBLIC)) {
            if (logger.isInfoEnabled())
                logger.info(format("Copying property and constructor arg values from existing bean definition for "
                                   + "@Configuration class %s to new bean definition", configBeanId));
            AbstractBeanDefinition existing = (AbstractBeanDefinition)beanFactory.getBeanDefinition(configBeanId, PUBLIC);
            configBeanDef.setPropertyValues(existing.getPropertyValues());
            configBeanDef.setConstructorArgumentValues(existing.getConstructorArgumentValues());
            configBeanDef.setResource(existing.getResource());
        }

        if (logger.isInfoEnabled())
            logger.info(format("Registering %s bean definition for @Configuration class %s", PUBLIC, configBeanId));

        beanFactory.registerBeanDefinition(configBeanId, configBeanDef, PUBLIC);
    }

    private void loadBeanDefinitionsForBeanMethod(ConfigurationClass configClass, BeanMethod beanMethod) {
        RootBeanDefinition beanDef = new RootBeanDefinition();
        beanDef.setFactoryBeanName(configClass.getId());
        beanDef.setFactoryMethodName(beanMethod.getName());

        Bean metadata = beanMethod.getMetadata();
        Configuration defaults = configClass.getMetadata();

        // consider scoping
        beanDef.setScope(metadata.scope());

        // consider autowiring
        if (metadata.autowire() != AnnotationUtils.getDefaultValue(Bean.class, "autowire"))
            beanDef.setAutowireMode(metadata.autowire().value());
        else if (defaults.defaultAutowire() != AnnotationUtils.getDefaultValue(Configuration.class, "defaultAutowire"))
            beanDef.setAutowireMode(defaults.defaultAutowire().value());

        String beanName = beanFactory.getBeanNamingStrategy().getBeanName(beanMethod);

        // has this already been overriden (i.e.: via XML)?
        if (containsBeanDefinitionIncludingAncestry(beanFactory, beanName)) {
            BeanDefinition existingBeanDef = getBeanDefinitionIncludingAncestry(beanFactory, beanName);

            // is the existing bean definition one that was created by JavaConfig?
            if (existingBeanDef.getAttribute(Constants.JAVA_CONFIG_PKG) == null) {
                // no -> then it's an external override, probably XML

                // ensure that overriding is ok
                if (metadata.allowOverriding() == false) {
                    UsageError error = configClass.new IllegalBeanOverrideError(null, beanMethod);
                    throw new MalformedJavaConfigurationException(error);
                }

                // overriding is legal, return immediately
                logger.info(format("Skipping loading bean definition for %s: a definition for bean '%s' already exists. "
                                   + "This is likely due to an override in XML.",
                                   beanMethod, beanName));
                return;
            }
        }

        // propagate this bean's 'role' attribute
        beanDef.setRole(metadata.role());

        // consider aliases
        for (String alias : metadata.aliases())
            beanFactory.registerAlias(beanName, alias, PUBLIC);

        // is this bean marked as primary for disambiguation?
        if (metadata.primary() == Primary.TRUE)
            beanDef.setPrimary(true);

        // is this bean lazily instantiated?
        if ((metadata.lazy() == Lazy.TRUE)
                || ((metadata.lazy() == Lazy.UNSPECIFIED) && (defaults.defaultLazy() == Lazy.TRUE)))
            beanDef.setLazyInit(true);

        // does this bean have a custom init-method specified?
        String initMethodName = metadata.initMethodName();
        if (hasText(initMethodName))
            beanDef.setInitMethodName(initMethodName);

        // does this bean have a custom destroy-method specified?
        String destroyMethodName = metadata.destroyMethodName();
        if (hasText(destroyMethodName))
            beanDef.setDestroyMethodName(destroyMethodName);

        // is this method annotated with @ScopedProxy?
        if (beanMethod.isScopedProxy()) {
            RootBeanDefinition targetDef = beanDef;

            // Create a scoped proxy definition for the original bean name,
            // "hiding" the target bean in an internal target definition.
            String targetBeanName = ConfigurationModelBeanDefinitionReader.resolveHiddenScopedProxyBeanName(beanName);
            RootBeanDefinition scopedProxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
            scopedProxyDefinition.getPropertyValues().addPropertyValue("targetBeanName", targetBeanName);

            // transfer relevant attributes from original bean to scoped-proxy bean
            scopedProxyDefinition.setScope(beanMethod.getMetadata().scope());

            if (beanMethod.getScopedProxyMetadata().proxyTargetClass())
                targetDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
            // ScopedFactoryBean's "proxyTargetClass" default is TRUE, so we
            // don't need to set it explicitly here.
            else
                scopedProxyDefinition.getPropertyValues().addPropertyValue("proxyTargetClass", Boolean.FALSE);

            // The target bean should be ignored in favor of the scoped proxy.
            targetDef.setAutowireCandidate(false);

            // Register the target bean as separate bean in the factory
            beanFactory.registerBeanDefinition(targetBeanName, targetDef, PUBLIC);

            // replace the original bean definition with the target one
            beanDef = scopedProxyDefinition;
        }

        // does this bean method have any @Meta annotations?
        for (Meta meta : metadata.meta())
            beanDef.addMetadataAttribute(new BeanMetadataAttribute(meta.key(), meta.value()));

        if(metadata.dependsOn().length > 0)
            beanDef.setDependsOn(metadata.dependsOn());

        BeanVisibility visibility = visibilityOf(beanMethod.getModifiers());
        logger.info(format("Registering %s bean definition for @Bean method %s.%s()",
                           visibility, configClass.getName(), beanName));
        beanFactory.registerBeanDefinition(beanName, beanDef, visibility);
    }

    private static boolean containsBeanDefinitionIncludingAncestry(JavaConfigBeanFactory beanFactory, String beanName) {
        try {
            getBeanDefinitionIncludingAncestry(beanFactory, beanName);
            return true;
        } catch (NoSuchBeanDefinitionException ex) {
            return false;
        }
    }

    private static BeanDefinition getBeanDefinitionIncludingAncestry(JavaConfigBeanFactory beanFactory,
                                                                     String beanName) {
        ConfigurableListableBeanFactory bf = beanFactory;
        do {
            if (bf.containsBeanDefinition(beanName))
                return bf.getBeanDefinition(beanName);

            BeanFactory parent = bf.getParentBeanFactory();
            if (parent == null) {
                bf = null;
            } else if (parent instanceof ConfigurableListableBeanFactory) {
                bf = (ConfigurableListableBeanFactory) parent;
            } else if (parent instanceof AbstractApplicationContext) {
                bf = ((AbstractApplicationContext) parent).getBeanFactory();
            } else {
                throw new IllegalStateException("unknown parent type: " + parent.getClass().getName());
            }
        } while (bf != null);

        throw new NoSuchBeanDefinitionException(format("No bean definition matching name '%s' "
                                                       + "could be found in %s or its ancestry",
                                                       beanName, beanFactory));
    }


    private void loadBeanDefinitionsForAutoBeanMethod(AutoBeanMethod method) {
        ModelClass returnType = method.getReturnType();

        RootBeanDefinition beanDef = new RootBeanDefinition();
        beanDef.setBeanClassName(returnType.getName());
        beanDef.setAutowireMode(method.getMetadata().autowire().value());

        beanFactory.registerBeanDefinition(method.getName(), beanDef, PUBLIC);
    }

    private void loadBeanDefinitionsForDeclaringClass(ConfigurationClass declaringClass) {
        if (declaringClass == null)
            return;

        logger.info(format("Found declaring class [%s] on configClass [%s]", declaringClass, declaringClass));

        BeanFactory parentBF;
        String factoryName = BeanFactoryProvider.BEAN_NAME;

        if (beanFactory.containsBean(factoryName))
            parentBF = (BeanFactory) beanFactory.getBean(factoryName, new Object[] { declaringClass.getName() });
        else
            parentBF = new DefaultListableBeanFactory();

        beanFactory.getParentBeanFactory().setParentBeanFactory(parentBF);
    }

    /**
     * Return the <i>hidden</i> name based on a scoped proxy bean name.
     *
     * @param   originalBeanName  the scope proxy bean name as declared in the
     *                            Configuration-annotated class
     *
     * @return  the internally-used <i>hidden</i> bean name
     */
    public static String resolveHiddenScopedProxyBeanName(String originalBeanName) {
        Assert.hasText(originalBeanName);
        return TARGET_NAME_PREFIX.concat(originalBeanName);
    }


}
