package gov.va.med.fw.conversion;

import gov.va.med.fw.model.AbstractEntity;
import gov.va.med.fw.model.lookup.AbstractLookup;
import gov.va.med.fw.service.AbstractComponent;
import gov.va.med.fw.util.Reflector;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.DynaBean;
import org.apache.commons.beanutils.DynaProperty;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang.Validate;

/**
 * Reference copies the source object to target object. If the either one of the 
 * includePropertyList and excludePropertyList are not provided copies all the 
 * readable properties of source object to the writable properties of the target object.
 * 
 * @author Muddaiah
 * @version 3.0
 */
public class CopyServiceImpl extends AbstractComponent implements CopyService
{
    private List commonExcludePropertyList;
    private Map includePropertyMap;
    private Map excludePropertyMap;
    private Map complexPropertyMap;
    private Map classNameAliasMap;

    /**
     * Reference copies the source object to target object. If the either one of the 
     * includePropertyList and excludePropertyList are not provided copies all the 
     * readable properties of source object to the writable properties of the target object.
     * 
     * @param source source object
     * @param target target object
     */
    public void copy(AbstractEntity source, AbstractEntity target)
    throws CopyServiceException
    {
        Validate.notNull(source, "Source object must not be null");
        Validate.notNull(target, "Target object must not be null");
        
        PropertyUtilsBean propUtils = new PropertyUtilsBean();
        copy(source, target, propUtils);
    }

    /**
     * This creates a new instance of the source object
     * and copies all the specified fileds
     */    
    public AbstractEntity getCopy(AbstractEntity source) throws CopyServiceException
    {
        Validate.notNull(source, "Source object must not be null"); 
        //Lookups are not copied
        if (AbstractLookup.class.isAssignableFrom(source.getClass())) {
            return source;
        }
        PropertyUtilsBean propUtils = new PropertyUtilsBean();
        return (AbstractEntity) getCopy(source, propUtils);
    }

    protected Object getCopy(Object source, PropertyUtilsBean propUtils) throws CopyServiceException
    {
        if (source == null) {
            return null;
        }
        
        Object target = null;
        try {
            target = source.getClass().newInstance();
            
            //Copy the specified properties (based on Includes or Excludes)
            copy(source, target, propUtils);
            
            //If the complex properties are specified copy them recursively
            List complexProperties = getComplexPropertyList(target.getClass().getName());
            copyComplexProperties(source, target, complexProperties, propUtils);
            return target;            
        }
        catch (Exception e) {
            throw new CopyServiceException("Could not create a new instance",e);
        }        
    }
    
    protected void copyComplexProperties(Object source, Object target, List properties, 
            PropertyUtilsBean propUtils) throws Exception{
        
        //check if complex properties are defined 
        if (properties == null || properties.size() == 0) {
            return;
        }
        
        //process each property
        for (int i=0; i<properties.size(); i++) {
            String propName = (String) properties.get(i);
            
            //get the property Value and find the type
            Object propValue = null;
            try {
                propValue = propUtils.getSimpleProperty(source, propName);
            }catch (Exception e) {
                throw new CopyServiceException("getSimpleProperty failed class: " + 
                    source.getClass().getName() + " property: " + propName);                
            }
            
            //If the property value is null continue to process next 
            if (propValue == null) {
                continue;
            }
            Class propType = propValue.getClass();
            //find the type and create copy of the collection or entity
            if (Map.class.isAssignableFrom(propType)) {
                //Handle Map
                //TODO define standrad methods based on keys in the BOM Objects
                Map map = (Map)propValue;
                for (Iterator j = map.keySet().iterator(); j.hasNext();) {
                    Object itemKey = j.next();
                    Object itemValue = map.get(itemKey);
                    
                    //Skip null property Values
                    if (itemValue == null) 
                        continue;
                    
                    //make a copy and add to the target object
                    Object itemCopy = getCopy(itemValue, propUtils);
                    //TODO More generic and configuration based need to be developed
                    String addMethod = getAddMethodName(itemCopy);
                    //Execute Reflection code to set the property need to pass and Key value
                    Object[] values = new Object[] {itemKey, itemCopy};
                    executeMethod(target, addMethod, values);
                }                
            }
            //Process Set/List/...
            else if (Collection.class.isAssignableFrom(propType)) {
                
                for (Iterator j=((Collection)propValue).iterator(); j.hasNext();) {
                    Object item = j.next();
                    //make a copy and add to the target object
                    Object itemCopy = getCopy(item, propUtils);
                    //TODO More generic and configuration based need to be developed
                    String addMethod = getAddMethodName(item);
                    //Execute Reflection code to set the property
                    executeMethod(target, addMethod, new Object[] {itemCopy});
                }
            }
            else {
                Object propCopy = getCopy(propValue,propUtils);
                propUtils.setSimpleProperty(target,propName,propCopy);
            }
        }
    }    
    
    protected void copy(Object source, Object target, PropertyUtilsBean propUtils)
    throws CopyServiceException
    {    
       try
        {
            List includePropertyList = getIncludePropertyList(source.getClass().getName());
            List excludePropertyList = getExcludePropertyList(source.getClass().getName());
            
            if((includePropertyList != null && !includePropertyList.isEmpty()) &&
               (excludePropertyList != null && !excludePropertyList.isEmpty()))
            {
                throw new CopyServiceException("Both include and exclude properties must not be specified.");
            }
            if(includePropertyList != null && !includePropertyList.isEmpty())
            {
                copyIncludes(source,target,includePropertyList,propUtils);
            }
            else {
                Map sourcePropertyMap = getReadableProperties(source,propUtils);
                if(excludePropertyList != null && !excludePropertyList.isEmpty())
                {
                    copyExcludes(sourcePropertyMap,target,excludePropertyList,propUtils);
                }
                else
                {
                    copyAll(sourcePropertyMap,target,propUtils);
                }
            }
        }
        catch(CopyServiceException copyEx)
        {
            throw copyEx;
        }
        catch(Exception ex)
        {
            throw new CopyServiceException("Error while copying the properties.",ex);
        }
    }
    
    /**
     * Getter method for common exclude properties. None on the properties in this list will be copied
     * for all the copy calls and all the bean classes.
     */
    public List getCommonExcludePropertyList() 
    {
        return commonExcludePropertyList != null ? commonExcludePropertyList : new ArrayList(); 
    }
    
    /**
     * Setter method for common exclude properties. None on the properties in this list will be copied
     * for all the copy calls and all the bean classes.
     */
    public void setCommonExcludePropertyList(List commonExcludePropertyList) 
    {
        if(commonExcludePropertyList == null) 
        {
            commonExcludePropertyList = new ArrayList();
        }
        this.commonExcludePropertyList = commonExcludePropertyList;
    }
    
    /**
     * Setter method for exclude properties. None on the properties in this list will be copied
     * for a specific bean type. The map contains class name as the key and value is the list of 
     * properties not to be copied for that class name.
     */
    public void setExcludePropertyMap(Map excludePropertyMap) 
    {
        this.excludePropertyMap = excludePropertyMap;
    }

    /**
     * Setter method for include properties. Only the properties in this list will be copied
     * for a specific bean type. The map contains class name as the key and value is the list of 
     * properties to be copied for that class name.
     */
    public void setIncludePropertyMap(Map includePropertyMap) 
    {
        this.includePropertyMap = includePropertyMap;
    }
    
    /**
     * Getter method for exclude properties. None on the properties in this list will be copied
     * for a specific bean type.
     */
    public List getExcludePropertyList(String className)
    {
        List exList = this.excludePropertyMap != null ? (List)this.excludePropertyMap.get(className) : null;
        return exList;
    }

    /**
     * Setter method for include properties. Only the properties in this list will be copied
     * for a specific bean type.
     */
    public List getIncludePropertyList(String className) 
    {
        return this.includePropertyMap != null ? (List)this.includePropertyMap.get(className) : null;
    }
    
    public List getComplexPropertyList(String className) {
        return this.complexPropertyMap != null ? (List) this.complexPropertyMap.get(className) : null;
    }

    public void setComplexPropertyMap(Map complexPropertyMap) {
        this.complexPropertyMap = complexPropertyMap;
    }

    /**
     * Copies all the accessible properties only in the includePropertyList list.
     */
    protected void copyIncludes(Map sourcePropertyMap,Object target,List includePropertyList,PropertyUtilsBean propUtils)
    throws Exception
    {
        for(Iterator iter = includePropertyList.iterator(); iter.hasNext();)
        {
            String key = (String)iter.next();
            this.setSimpleProperty(target,key,sourcePropertyMap.get(key),propUtils);
        }
    }
    /**
     * Copies all the accessible properties only in the includePropertyList list.
     */
    protected void copyIncludes(Object source, Object target,List includePropertyList,PropertyUtilsBean propUtils)
    throws Exception
    {
        for(Iterator iter = includePropertyList.iterator(); iter.hasNext();)
        {
            String key = (String)iter.next();
            this.setSimpleProperty(target,key,propUtils.getNestedProperty(source,key),propUtils);
        }
    }    
    
    /**
     * Copies all the accessible properties except the excludePropertyList list.
     */
    protected void copyExcludes(Map sourcePropertyMap,Object target,List excludePropertyList,PropertyUtilsBean propUtils)
    throws Exception
    {
        String key = null;
        for(Iterator iter = sourcePropertyMap.keySet().iterator(); iter.hasNext();)
        {
            key = (String)iter.next();
            if(excludePropertyList.contains(key) || getCommonExcludePropertyList().contains(key))
            {
                continue;
            }
            this.setSimpleProperty(target,key,sourcePropertyMap.get(key),propUtils);
        }
    }
    
    /**
     * Copies all the accessible properties.
     */
    protected void copyAll(Map sourcePropertyMap,Object target,PropertyUtilsBean propUtils)
    throws Exception
    {       
        String key = null;
        for(Iterator iter = sourcePropertyMap.keySet().iterator(); iter.hasNext();)
        {
            key = (String)iter.next();
            if(getCommonExcludePropertyList().contains(key))
            {
                continue;
            }
            this.setSimpleProperty(target,key,sourcePropertyMap.get(key),propUtils);
        }
    }

    /**
     * Extract the readable properties from the given object.
     */
    private Map getReadableProperties(Object source,PropertyUtilsBean propUtils)
    throws CopyServiceException
    {
        Map map = null;
        try
        {
            map = propUtils.describe(source);
        }
        catch(Exception ex)
        {
            throw new CopyServiceException("Error while extracting the source properties.",ex);
        }
        return map;
    }
    
    /**
     * Set the value of the specified simple property of the specified bean,
     * with no type conversions.
     *
     * @param bean Bean whose property is to be modified
     * @param name Name of the property to be modified
     * @param value Value to which the property should be set
     * @param propUtils the property utils object
     *
     * @exception IllegalAccessException if the caller does not have
     *  access to the property accessor method
     * @exception IllegalArgumentException if <code>bean</code> or
     *  <code>name</code> is null
     * @exception IllegalArgumentException if the property name is
     *  nested or indexed
     * @exception InvocationTargetException if the property accessor method
     *  throws an exception
     * @exception NoSuchMethodException if an accessor method for this
     *  propety cannot be found
     */
    private void setSimpleProperty(Object bean, String name, Object value,PropertyUtilsBean propUtils)
    throws IllegalAccessException, InvocationTargetException,NoSuchMethodException 
    {
        Validate.notNull(bean,"No bean specified");
        Validate.notNull(name,"No name specified");

        //If there is no setter method for the property, do nothing
        if(!propUtils.isWriteable(bean,name))
        {
            if(logger.isDebugEnabled())
            {
                logger.debug("No setter method for property '" + name + "' in class '" + bean.getClass().getName());
            }
            return;
        }

        //Validate the syntax of the property name
        if (name.indexOf(PropertyUtils.NESTED_DELIM) >= 0) 
        {
            throw new IllegalArgumentException("Nested property names are not allowed");
        } 
        else if (name.indexOf(PropertyUtils.INDEXED_DELIM) >= 0) 
        {
            throw new IllegalArgumentException("Indexed property names are not allowed");
        } 
        else if (name.indexOf(PropertyUtils.MAPPED_DELIM) >= 0) 
        {
            throw new IllegalArgumentException("Mapped property names are not allowed");
        }

        //Handle DynaBean instances specially
        if (bean instanceof DynaBean) 
        {
            DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name);
            if(descriptor == null) 
            {
                throw new NoSuchMethodException("Unknown property '" + name + "'");
            }
            ((DynaBean) bean).set(name, value);
            return;
        }

        //Retrieve the property setter method for the specified property
        PropertyDescriptor descriptor = propUtils.getPropertyDescriptor(bean,name);
        if(descriptor == null) 
        {
            throw new NoSuchMethodException("Unknown property '" + name + "'");
        }
        descriptor.getWriteMethod();
        Method writeMethod = descriptor.getWriteMethod();
        if(writeMethod == null) 
        {
            throw new NoSuchMethodException("Property '" + name + "' has no setter method");
        }

        //Call the property setter method
        Object values[] = new Object[1];
        values[0] = value;
        
        writeMethod.invoke(bean,values);
    }
    
    //Generic execute method with multiple parameters    
    protected Object executeMethod(Object targetObj, String methodName, Object[] params) throws Exception
    {
        if (targetObj != null && methodName != null && params != null && params[0] != null)
        {
            Class currentClass = targetObj.getClass();
            // If the user passed in a class instead of an object, use that
            if (Class.class.isAssignableFrom(targetObj.getClass())) {
                currentClass = (Class)targetObj;
            }

            while (currentClass != null) {
                try  {
                    // Attempt to find the method on the current class
                    Method method = Reflector.findMethod(currentClass, methodName, Reflector.typesOf(params));

                    // If found, invoke the method
                    return method.invoke(targetObj, params);
                    
                } catch (IllegalArgumentException e){
                    throw e;
                } catch (Exception ex) {
                    // Get the parent of the current class
                    currentClass = currentClass.getSuperclass();
                }
            }
  
            throw new NoSuchMethodException("Unable to find a matching '" + methodName + "' method in the '" +
                targetObj.getClass().getName() + "' class hierarchy.");
        } else {
            return null;
        }        
    }
    
    /**
     * Method to generate addMethodName based on Type
     * @param obj
     * @return
     */
    protected String getAddMethodName(Object obj) {
        String className = obj.getClass().getName();
        className = getAliasName(className);
        return "add"+ className.substring(className.lastIndexOf('.')+1);
    }
    
    protected String getAliasName(String className) {
        String alias = null;
        if (classNameAliasMap != null) {
            alias = (String) classNameAliasMap.get(className);
        }
        return alias == null ? className : alias;
    }

    public Map getClassNameAliasMap() {
        return classNameAliasMap;
    }

    public void setClassNameAliasMap(Map classNameAliasMap) {
        this.classNameAliasMap = classNameAliasMap;
    }
    
}
