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

import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.MapFactoryBean;
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.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionValueResolver;
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;

/**
 * Allow a factory bean of type Map 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 "sourceMap" property to inherit a list of elements 
 * from its parent bean.  This means that a child bean will have a "sourceMap"
 * property that is a combination of its own list and its parent bean's list.
 * 
 * @author DNS   LEV, DNS   CHENJ2
 */
public class InheritedMapFactoryBean extends MapFactoryBean 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;
	
	/**
	 * A default constructor
	 */
	public InheritedMapFactoryBean() {
		super();
	}

	/**
	 * @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;
	}
	
	/**
	 * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance()
	 */
	protected Object createInstance() {
		
		// Create an instance of a Map
		Map beans = (Map)super.createInstance();
		
		// Only combine a list to its parent list if a flag is set
		if( this.inherited ) {

			Map parentMap = 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
			parentMap = getParentMap(confFactory, parentBeanName);

			beans = this.populateMap( beans, parentMap ); 
		}
		return beans;
	}

	/**
	 * Get a parent's BeanDefinition, recurse through the parent bean factory chain
	 * until it's found.  Then get the parent bean's map depending on whether it's abstract.
	 * 
	 * @param confFactory
	 * @param parentBeanName
	 * @return A map that this bean is meant to create
	 */
	private Map getParentMap( ConfigurableListableBeanFactory confFactory, 
									  String parentBeanName ) {
		
		BeanDefinition def = null;
		BeanFactory parentFactory = null;
		Map parentMap = 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 && !def.isAbstract() ) {
			// A parent bean is not abstract so create it to get a list of elements
			Object obj = confFactory.getBean( parentBeanName, this.getObjectType() );
			parentMap = obj instanceof Map ? (Map)obj : null;
		}
		else if (def != null) {
			// A parent bean is abstract so only instantiate a parent bean's "sourceMap" property values
			MutablePropertyValues values = def.getPropertyValues();
			PropertyValue value = values.getPropertyValue( "sourceMap" );
			
			// Get a property value of a property "sourceMap". In this case, spring returns a ManagedMap
			Object propValue = value.getValue();
			Map valueMap = propValue instanceof Map ? (Map)propValue : null;
			
			// Instantiate a list to hold value of a parent bean's "sourceList" property
			parentMap = this.resolveListElement( value, valueMap, (AbstractBeanFactory)confFactory );
		}
		return parentMap;
	}

	/** Populates a source map with elements from a target
	 * map.
	 *  
	 * @param source A source map to populate
	 * @param target A target map to retrieve data
	 * @return A merge list
	 */
	private Map populateMap( Map source, Map target ) {

		Map beans = source;
		if( source != null && target != null ) {
			source.putAll( target );
		}
		return beans;
	}

	/** Resolves a list of properties of a property of type Map.  This method resolves
	 * a bean definition, and a bean reference in an element map of a Map property
	 * @param value A property value containing property name and its value map
	 * @param valueMap A property's value map
	 * @param factory A factory containing this bean
	 * @return A resolved map of elements
	 */
	private Map resolveListElement( PropertyValue value, Map valueMap, AbstractBeanFactory factory ) {
		
		Map resolvedMap = new HashMap();
		Iterator i = valueMap != null ? valueMap.entrySet().iterator() : null;
		
		while( factory != null && value != null && i != null && i.hasNext() ) {
			
			Map.Entry entry = (Map.Entry)i.next();
			Object entryKey = entry.getKey();
			Object entryValue = entry.getValue();
			
			if( entryValue instanceof BeanDefinitionHolder ) {
				BeanDefinitionHolder holder = (BeanDefinitionHolder)entryValue;

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