/********************************************************************
 * Copyright © 2005 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.fw.service.config;

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

import org.apache.commons.collections.ListUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ListFactoryBean;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.ChildBeanDefinition;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.support.BeanDefinitionValueResolver;
import org.springframework.beans.factory.support.AbstractBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;

/**
 * Allow a factory bean of type List to inherit properties from its parent bean.
 * By default, this bean works as a normal Spring bean that override its parent 
 * properties instead of inheritting them.  Setting a "inherited" flag to true 
 * in this bean allows a "sourceList" property to inherit a list of elements 
 * from its parent bean.  This means that a child bean will have a "sourceList"
 * property that is a combination of its own list and its parent bean's list.
 * 
 * @author DNS   LEV, DNS   CHENJ2
 */
public class InheritedListFactoryBean extends ListFactoryBean implements BeanFactoryAware, BeanNameAware {
	
	/**
	 * An instance of factory
	 */
	private BeanFactory factory = null;
	
	/**
	 * An instance of name
	 */
	private String name = null;
	
	/**
	 * An instance of inherited
	 */
	private boolean inherited = false;
	
	/**
	 * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance()
	 */
	protected Object createInstance() {
		
		// Create an instance of a list
		List beans = (List)super.createInstance();
		
		// Only combine a list to its parent list if a flag is set
		if( this.inherited ) {
			List parentList = null;
			
			// Cast to a configurable factory bean to get a bean dinition
			ConfigurableListableBeanFactory confFactory = 
				factory instanceof ConfigurableListableBeanFactory ? (ConfigurableListableBeanFactory)factory : null;
				
			// Get a bean's definition to obtains its parent bean's definition
			BeanDefinition def = confFactory != null ? confFactory.getBeanDefinition( name ) : null;
			
			// Get a parent bean's name
			ChildBeanDefinition childDef = def instanceof ChildBeanDefinition ? (ChildBeanDefinition)def : null;
			String parentBeanName = childDef != null ? childDef.getParentName() : null;
			
			// Get a parent bean's definition, then get the parent bean list
			parentList = getParentList(confFactory, parentBeanName);

			beans = this.populateList( beans, parentList ); 
		}
		return beans;
	}
	
	
	/**
	 * Get a parent's BeanDefinition, recurse through the parent bean factory chain
	 * until it's found.  Then get the parent bean's list depending on whether it's abstract.
	 * 
	 * @param confFactory
	 * @param parentBeanName
	 * @return List A list that this factory bean is meant to create
	 */
	private List getParentList( ConfigurableListableBeanFactory confFactory, 
										 String parentBeanName ) {
		
		BeanDefinition def = null;
		BeanFactory parentFactory = null;
		List parentList = null;
		
		// Get a parent bean's definition
		if (parentBeanName != null) {
			while (confFactory != null) {
				try {
					def = confFactory.getBeanDefinition(parentBeanName);					
					break;
				} 
				catch (NoSuchBeanDefinitionException ex) {
					// if it is not found in the current bean factory, look in
					// the parent bean factory
					parentFactory = confFactory.getParentBeanFactory();
					confFactory = parentFactory instanceof ConfigurableListableBeanFactory ? 
										(ConfigurableListableBeanFactory) parentFactory : null;
				}
			}
			
			if (def == null) {
				// parent bean definition not found in any of the bean factories
				throw new NoSuchBeanDefinitionException(parentBeanName,
					"Parent bean name not found in current or any parent BeanFactories");
			}
		}

		if (def != null) {
			if (!def.isAbstract()) {
				// A parent bean is not abstract so create it to get a list
				// of elements
				Object obj = confFactory.getBean(parentBeanName, this.getObjectType());
				parentList = obj instanceof List ? (List) obj : null;
			} 
			else {
				// A parent bean is abstract so only instantiate a parent
				// bean's "sourceList" property values
				MutablePropertyValues values = def.getPropertyValues();
				PropertyValue value = values.getPropertyValue("sourceList");

				// Get a property value of a property "sourceList". In this
				// case, spring returns a ManagedList
				Object propValue = value.getValue();
				List valueList = propValue instanceof List ? (List) propValue : null;

				// Instantiate a list to hold value of a parent bean's
				// "sourceList" property
				parentList = this.resolveListElement(value, valueList,
						(AbstractBeanFactory) confFactory);
			}
		}
		
		return parentList;
	}
	
	/** Populates a source list with elements from a target
	 * list.  If a target list contains an element of type
	 * List, add all elements to a source list.
	 * 
	 * @param source A source list to populate
	 * @param target A target list to retrieve data
	 * @return A merge list
	 */
	private List populateList( List source, List target ) {

		List beans = source;
		if( source != null ) {
			Iterator itr = target != null ? target.iterator() : null;
			Object obj = null;
			while(itr != null && itr.hasNext()) {
				obj = itr.next();
				if(obj instanceof List) {
					source = ListUtils.sum( source, (List) obj);
				} 
				else {
					source.add(obj);
				}
			}
		}
		return beans;
	}
	
	/** Resolves a list of properties of a property of type List.  This method resolves
	 * a bean definition, and a bean reference in an element list of a List property
	 * @param value A property value containing property name and its value list
	 * @param valueList A property's value list
	 * @param factory A factory containing this bean
	 * @return A resolved element list
	 */
	private List resolveListElement( PropertyValue value, List valueList, AbstractBeanFactory factory ) {
		
		List resolvedList = new ArrayList();
		Iterator i = valueList != null ? valueList.iterator() : null;
		
		while( factory != null && value != null && i != null && i.hasNext() ) {
			Object obj = i.next();
			if( obj instanceof BeanDefinitionHolder ) {
				BeanDefinitionHolder holder = (BeanDefinitionHolder)obj;

				BeanDefinition beanDef = (BeanDefinition)holder.getBeanDefinition();
				String beanName = holder.getBeanName();
				BeanDefinitionValueResolver resolver = 
					new BeanDefinitionValueResolver( factory,
																beanName != null ? beanName : "(inner-bean)",
																beanDef );
				obj = resolver.resolveValueIfNecessary( value.getName(), obj );
			}
			else if( obj instanceof RuntimeBeanReference ) {
				String beanName = ((RuntimeBeanReference)obj).getBeanName();
				obj = factory.getBean( beanName );
			}
			resolvedList.add( obj );
		}
		return resolvedList;
	}

	/**
	 * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
	 */
	public void setBeanFactory(BeanFactory factory) throws BeansException {
		this.factory = factory;
	}

	/**
	 * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String)
	 */
	public void setBeanName(String name) {
		this.name = name;
	}

	/**
	 * @param flag The flag to indicate the property sourceList is inheritted.
	 */
	public void setInherited(boolean flag) {
		this.inherited = flag;
	}
}