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

import org.springframework.beans.BeanMetadataAttribute;
import org.springframework.beans.BeanMetadataAttributeAccessor;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.support.AbstractBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

import org.springframework.config.java.internal.util.Constants;
import org.springframework.config.java.naming.BeanNamingStrategy;
import org.springframework.config.java.naming.MethodNameStrategy;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;


/** TODO: Eliminate as part of SJC-209 */
public class DefaultJavaConfigBeanFactory extends DefaultListableBeanFactory implements JavaConfigBeanFactory {

    /**
     * Defaults to.{@link MethodNameStrategy}
     *
     * @see  #setBeanNamingStrategy(BeanNamingStrategy) to override
     */
    BeanNamingStrategy beanNamingStrategy = new MethodNameStrategy();

    public DefaultJavaConfigBeanFactory(ConfigurableListableBeanFactory externalBeanFactory,
                                        BeanFactoryProvider beanFactoryProvider) {
        super(externalBeanFactory);

        // ensure that all BeanPostProcessors, etc registered with the external factory propagate
        // locally
        this.copyConfigurationFrom(externalBeanFactory);

        // look for and process any @ExternalValue-annotated fields in @Configuration class beans
        // this is registered with the parent (public) bean factory because that is typically the
        // only place @Configuration beans get registered.  A reference to 'this' is given to the
        // BPP so it can find the ValueSource instance that is registered locally.
        this.getParentBeanFactory().addBeanPostProcessor(new ExternalValueInjectingBeanPostProcessor(this));

        beanFactoryProvider.registerBeanDefinition(this);
    }

    @Override
    public boolean isCurrentlyInCreation(String beanName) {
        if (super.isCurrentlyInCreation(beanName))
            return true;

        if (this.getParentBeanFactory() != null)
            return this.getParentBeanFactory().isCurrentlyInCreation(beanName);

        return super.isCurrentlyInCreation(beanName);
    }

    /**
     * Overridden to exploit covariant return type.
     */
    @Override
    public DefaultListableBeanFactory getParentBeanFactory() {
        return (DefaultListableBeanFactory) super.getParentBeanFactory();
    }

    /**
     * Register a singleton object with this bean factory.  Because no {@link BeanVisibility} is specified,
     * register this bean as {@link BeanVisibility#HIDDEN hidden} by default.  Note that this is the opposite
     * of the behavior of {@link #registerBeanDefinition(String, BeanDefinition)}, which registers beans as public
     * by default.  The reason for this is that certain singletons are registered automatically by the container
     * (messageSource, applicationEventMulticaster, etc).  If these are automatically made public (and thus delegated
     * to the parent bean factory), a conflict occurs because the parent bean factory already contains singletons
     * by the same name.  Bottom line: if a singleton needs to be hidden it must be done explicitly.
     *
     * @see #registerSingleton(String, Object, BeanVisibility)
     * @see #registerBeanDefinition(String, BeanDefinition)
     * @see #registerBeanDefinition(String, BeanDefinition, BeanVisibility)
     */
    @Override
    public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
        registerSingleton(beanName, singletonObject, BeanVisibility.HIDDEN);
    }

    public void registerSingleton(String beanName, Object bean, BeanVisibility visibility) {
        switch (visibility) {
            case HIDDEN:
                super.registerSingleton(beanName, bean);
                break;
            case PUBLIC:
                getParentBeanFactory().registerSingleton(beanName, bean);
                break;
        }
    }

    /**
     * Register a bean definition. No explicit {@link BeanVisibility} is defined; default to
     * {@link BeanVisibility#PUBLIC public}.
     *
     * @see #registerBeanDefinition(String, BeanDefinition, BeanVisibility)
     */
    @Override
    public void registerBeanDefinition(String beanName, BeanDefinition beanDef) throws BeanDefinitionStoreException {
        if(beanDef.getAttribute(Constants.JAVA_CONFIG_IGNORE) != null)
            this.registerBeanDefinition(beanName, beanDef, BeanVisibility.HIDDEN);
        else
            this.registerBeanDefinition(beanName, beanDef, BeanVisibility.PUBLIC);

    }

    /**
     * Register <var>beanDef</var> with name <var>beanName</var>.  If <var>visibility</var> is PUBLIC
     * the bean definition registration will be delegated to the parent bean factory.  If HIDDEN, will
     * be registered locally with this BeanFactory.
     *
     * @see #registerBeanDefinition(String, BeanDefinition) for default behavior when no visibility is specified
     */
    public void registerBeanDefinition(String beanName, BeanDefinition beanDef, BeanVisibility visibility) {
        // note that this bean definition was added by JavaConfig (as opposed to XML, etc)
        ((BeanMetadataAttributeAccessor) beanDef).addMetadataAttribute(
            new BeanMetadataAttribute(Constants.JAVA_CONFIG_PKG, true));

        switch (visibility) {
            case HIDDEN:
                super.registerBeanDefinition(beanName, beanDef);
                break;
            case PUBLIC:
                getParentBeanFactory().registerBeanDefinition(beanName, beanDef);
                break;
        }
    }

    /**
     * Register a bean alias. No explicit {@link BeanVisibility} is defined, default to
     * {@link BeanVisibility#PUBLIC public}.
     *
     * @see #registerAlias(String, String, BeanVisibility)
     * @see #registerBeanDefinition(String, BeanDefinition)
     */
    @Override
    public void registerAlias(String beanName, String alias) {
        this.registerAlias(beanName, alias, BeanVisibility.PUBLIC);
    }

    /**
     * @see #registerAlias(String, String)
     * @see #registerBeanDefinition(String, BeanDefinition, BeanVisibility)
     */
    public void registerAlias(String beanName, String alias, BeanVisibility visibility) {
        switch (visibility) {
            case HIDDEN:
                super.registerAlias(beanName, alias);
                break;
            case PUBLIC:
                getParentBeanFactory().registerAlias(beanName, alias);
                break;
        }
    }

    public boolean containsBeanDefinition(String beanName, BeanVisibility visibility) {
        switch (visibility) {
            case HIDDEN:
                return containsBeanDefinition(beanName);
            case PUBLIC:
                return getParentBeanFactory().containsBeanDefinition(beanName);
            default:
                throw new IllegalArgumentException();
        }
    }

//    @Override
//    public boolean containsBeanDefinition(String beanName) {
//        return super.containsBeanDefinition(beanName)
//            || getParentBeanFactory().containsBeanDefinition(beanName);
//    }

    public BeanDefinition getBeanDefinition(String beanName, BeanVisibility visibility) {
        switch (visibility) {
            case HIDDEN:
                return getBeanDefinition(beanName);
            case PUBLIC:
                return getParentBeanFactory().getBeanDefinition(beanName);
            default:
                throw new IllegalArgumentException();
        }
    }

//    @Override
//    public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
//        if(super.containsBeanDefinition(beanName))
//            return super.getBeanDefinition(beanName);
//
//        return getParentBeanFactory().getBeanDefinition(beanName);
//    }


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

    public BeanNamingStrategy getBeanNamingStrategy() { return beanNamingStrategy; }

    /**
     * Ensure that all externally visible {@link BeanPostProcessor BeanPostProcessors} are applied
     * to hidden beans. BPP instances from the parent (external) bean factory are merged with those
     * from this (internal) bean factory, taking care to avoid duplication. In duplication cases,
     * the parent BPP takes precedence.
     */
    @Override
    @SuppressWarnings("unchecked")
    public List getBeanPostProcessors() {
        LinkedHashSet mergedBeanPostProcessors = new LinkedHashSet();
        mergedBeanPostProcessors.addAll(getParentBeanFactory().getBeanPostProcessors());
        mergedBeanPostProcessors.addAll(super.getBeanPostProcessors());
        return new ArrayList(mergedBeanPostProcessors);
    }

    @Override
    protected boolean hasInstantiationAwareBeanPostProcessors() {
        return super.hasInstantiationAwareBeanPostProcessors()
                   || parentHasInstantiationAwareBeanPostProcessors();
    }

    /**
     * Duplicates logic in {@link AbstractBeanFactory#addBeanPostProcessor(BeanPostProcessor)}.
     * Ideally would call {@link AbstractBeanFactory#hasInstantiationAwareBeanPostProcessors()},
     * however this method is protected.
     *
     * @see  AbstractBeanFactory#addBeanPostProcessor(BeanPostProcessor)
     */
    private boolean parentHasInstantiationAwareBeanPostProcessors() {
        for (Object bpp : getParentBeanFactory().getBeanPostProcessors())
            if (bpp instanceof InstantiationAwareBeanPostProcessor)
                return true;

        return false;
    }

    @Override
    protected boolean hasDestructionAwareBeanPostProcessors() {
        return super.hasDestructionAwareBeanPostProcessors()
                   || parentHasDestructionAwareBeanPostProcessors();
    }

    /**
     * TODO: JAVADOC
     *
     * @see  #parentHasInstantiationAwareBeanPostProcessors()
     * @see  AbstractBeanFactory#addBeanPostProcessor(BeanPostProcessor)
     */
    private boolean parentHasDestructionAwareBeanPostProcessors() {
        for (Object bpp : getParentBeanFactory().getBeanPostProcessors())
            if (bpp instanceof DestructionAwareBeanPostProcessor)
                return true;

        return false;
    }

}
