package gov.va.med.fw.conversion;

import gov.va.med.fw.util.StringUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Generic conversion utility to convert bean to bean properties.
 *
 * Limitations: Does not perform array/collection/map to flat or vice-versa. Full array to array or 
 * collection to collection or map to map works file.
 * 
 * @author Muddaiah Ranga
 * @version 3.0
 */
public class BeanConverter  
{
	protected static transient Log log = LogFactory.getLog(BeanConverter.class);
	protected static Map propertyDescriptorCache = new HashMap();
	
	static
	{
		BeanUtilsBean.setInstance(new BeanUtilsBean(new ConvertUtilsBean(), new PropertyUtilsBean()));
		ConvertUtilsBean convertUtils = BeanUtilsBean.getInstance().getConvertUtils();
		convertUtils.register(new DateConverter(null), java.util.Date.class);
	}

	/**
	 * Convert/copy from source bean to a target bean.
	 * 
	 * @param request the conversion input
	 * @param conversionService the conversion service
	 * 
	 * @throws ConversionException when threre is an error
	 */
	public static void convertBean(ConversionRequest request)
	throws ConversionException 
	{
		try
		{
			Object source = request.getSource();
			Object target = request.getTarget();
			Map sourceProp = request.getSourceProperties();
			Map targetProp = request.getTargetProperties();
		
			if(source == null) 
			{
				throw new ConversionException("Source bean is null");
			}
			if(target == null)
			{
				throw new ConversionException("Target bean is null");
			}
			if(sourceProp == null || sourceProp.isEmpty())
			{
				throw new ConversionException("Source property map is null or empty");
			}
			if(targetProp == null || targetProp.isEmpty())
			{
				throw new ConversionException("Target property map is null or empty");
			}

			PropertyUtilsBean propUtils = BeanUtilsBean.getInstance().getPropertyUtils();
			Iterator iter = sourceProp.keySet().iterator();
			while(iter.hasNext())
			{
				String key = (String)iter.next();
				String sourceKey = (String)sourceProp.get(key);
				String targetKey = (String)targetProp.get(key);

				if(StringUtils.isNotEmpty(sourceKey) && StringUtils.isNotEmpty(targetKey))
				{
					PropertyDescriptor propDesc = propUtils.getPropertyDescriptor(source,sourceKey);
					if(propDesc != null)
					{
						if(StringUtils.contains(targetKey,"[") || StringUtils.contains(targetKey,"]"))
						{
							log.warn("Not supported! - ignoring the individual array element conversion");
						}
						
						Object sourceValue = propUtils.getNestedProperty(source,sourceKey);
						if(sourceValue == null)
						{
							continue;
						}
						log.trace("Converting source value = " + sourceValue);
						log.trace("Converting target key   = " + targetKey);
						if(StringUtils.contains(targetKey,"."))
						{
							createNestedObject(target,targetKey,sourceValue);
						}
						//Copying/converting the source value to the target bean
						BeanUtils.copyProperty(target,targetKey,sourceValue);
					}
				}
			}
		}
		catch(Exception ex)
		{
			throw new ConversionException("Error occured while converting/copying source bean to target bean",ex);
		}

	}

	/**
	 * Creates nested objects if not already set in the target bean.
	 * 
	 * @param target the destination bean.
	 * @param nestedKey the nested target key (a.b.c...)
	 * @param incomingValue the 
	 * @throws Exception
	 */
	private static void createNestedObject(Object target, String nestedKey, Object incomingValue)
	throws Exception 
	{
		String nestedProperty = null;
		PropertyDescriptor prop = null;
		Class nestedClass = null;
		Object nestedObject = null;
		StringTokenizer tokens = new StringTokenizer(nestedKey, ".");
		
		PropertyUtilsBean propUtils = BeanUtilsBean.getInstance().getPropertyUtils();
		while(tokens.hasMoreTokens()) 
		{
			nestedProperty = tokens.nextToken();
			log.trace("Nested property: " + nestedProperty);
			try 
			{
				nestedObject = propUtils.getProperty(target,nestedProperty);
				if(nestedObject != null)
				{
					target = nestedObject;
					log.trace("Nested object is already set. Class name - " + nestedObject.getClass().getName());
					continue;
				}
			}
			catch(Exception e) 
			{
				// could not get property, for some reason or another (eg, not valid property, nested object is null, etc), just proceed to figure out why
				nestedObject = null;
			}
			prop = propUtils.getPropertyDescriptor(target,nestedProperty);
			
			//If the property type is array, return without doing anything.
			if(prop != null && prop.getPropertyType().isArray())
			{
				return;
			}
			 
			// ok, if we get here, nestedKey is a valid property on the target object that can be handled by subsequent logic
			// HOWEVER, we abort on this key if the target object does not have this nested object and the incoming value is  empty
			if(nestedObject == null && prop != null && (!isEmpty(incomingValue)))
			{
				nestedClass = prop.getPropertyType();
				nestedObject = createObject(nestedClass);
				executeMethod(prop.getWriteMethod(),target,nestedObject);	
			}

			// object now guaranteed to exist, so check for this
			//BeanUtils.registerDynamicConversionClass(prop);

			target = nestedObject;
		}
	}

	/**
	 * Create an instance of the specified class, assuming a public, no-argument constructor.
	 * @param clazz
	 * @return Object
	 * @throws RuntimeException If InstantiationException or IllegalAccessException are caught.
	 */
	public static Object createObject(Class clazz) 
	{
		try 
		{
			return clazz.newInstance(); 
		}
		catch(InstantiationException e) 
		{
			throw new RuntimeException("Unable to instantiate class " + (clazz == null ? "null" : clazz.getName()), e);
		}
		catch(IllegalAccessException e) 
		{
			throw new RuntimeException("Unable to access class " + (clazz == null ? "null" : clazz.getName()), e);
		}
	}
	
	public static Object createObject(String className)
	throws ClassNotFoundException
	{
		Class clazz = Class.forName(className);
		return BeanConverter.createObject(clazz);
	}
	
	private static void executeMethod(Method setMethod, Object dest, Object nestedObject) throws Exception 
	{
		Object[] args = {nestedObject};
		setMethod.invoke(dest, args);
	}
	
	private static boolean isEmpty(Object val) 
	{
		return (val != null) ? StringUtils.isEmpty(val.toString()) : true;
	}
}



