/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.esr.ui.common.action;

// Java Classes
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;

// Libraries Classes
import org.apache.commons.beanutils.NestedNullException;
import org.apache.commons.beanutils.PropertyUtilsBean;

// Framework Classes
import gov.va.med.fw.model.AbstractEntity;
import gov.va.med.fw.model.AbstractKeyedEntity;
import gov.va.med.fw.model.EntityKey;
import gov.va.med.fw.model.AbstractVersionedEntity;
import gov.va.med.fw.model.lookup.AbstractLookup;
import gov.va.med.fw.model.lookup.Lookup;
import gov.va.med.fw.security.UserPrincipal;
import gov.va.med.fw.service.ServiceException;
import gov.va.med.fw.util.Reflector;
import gov.va.med.fw.util.StringUtils;

// ESR Classes
import gov.va.med.esr.common.model.ee.Eligibility;
import gov.va.med.esr.common.model.financials.IncomeTestStatus;

import gov.va.med.esr.service.UserAdminService;
import gov.va.med.esr.service.impl.ChangeEvent;
import gov.va.med.esr.service.impl.HistoricalInfo;
import gov.va.med.esr.ui.common.beans.EntityHistory;
import gov.va.med.esr.ui.common.beans.FieldHistoryInfo;
import gov.va.med.esr.ui.common.beans.FormattingTableRow;
import gov.va.med.esr.ui.common.util.JspUtils;

/**
 * @author DNS   KATIKM
 *
 */
public abstract class AbstractHistoryAction extends PersonAbstractAction {
	public static final String NULL_TEXT = "";
	public static final String IS_NOT_NULL_YES_NO = "isNotNullYesorNo";
	public static final String IS_NOT_NULL_TRUE_FALSE = "isNotNullTrueorFalse";
	public static final String TEXT_YES = "Yes";
	public static final String TEXT_NO = "No";
	public static final String TEXT_NO_DATA = "No Data";
	public static final String TEXT_TRUE = "True";
	public static final String TEXT_FALSE = "False";
	public static final String NO_MAPPING = "NO_MAPPING";
	public static final String THIS_OBJECT = "*this"; 
	
	public String formatCurrency(BigDecimal value) {
		return "$" + value==null ? "" : NumberFormat.getCurrencyInstance().format(value.doubleValue());
	}
	public String formatCurrency(Integer value) {
		return "$" + value==null ? "" : NumberFormat.getCurrencyInstance().format(value.doubleValue());
	}
	
	public String getUserName(String userId){
		try {
			if (getUserAdminService() == null) {
				UserAdminService service = (UserAdminService) getBean("userAdminService");
				setUserAdminService(service);
			}
			
			if (getUserAdminService() != null) {
				Lookup userLookup = getUserAdminService().getByCode(userId);
				return userLookup == null ? userId : userLookup.getDescription();
			}
		} catch (ServiceException e) {
			e.printStackTrace();
		}
		return userId;
	}
	/**
	 * Get historical info for the selected change event
	 * @param histConfig
	 * @param changeEvent
	 * @return
	 * @throws ServiceException
	 */	
	protected HistoricalInfo getHistoricalInfo(HistoryConfiguration histConfig, 
			ChangeEvent changeEvent) throws ServiceException {
		
		Object[] objArray = new Object[1];
		objArray[0] = changeEvent;
		Object retValue = invokeService(objArray, histConfig.getServiceName(),histConfig.getHistoryMethodName());

		if (retValue != null) {
			return (HistoricalInfo) retValue;
		}
		else
			return null;	
	}
	
	
	/**
	 * Get Change events for the given Entity Key 
	 * (Invokes the configured service method to get the Change events)
	 * @param histConfig
	 * @param key
	 * @return
	 * @throws ServiceException
	 */ 	
	protected Set getChangeEvents(HistoryConfiguration histConfig, EntityKey key) throws ServiceException
    {
        // Calculate the number of parameters to pass into the service method
        int numParameters = (key == null ? 0 : 1) + histConfig.getParameterValues().size();
        Object[] objArray = null;
        if (numParameters > 0)
        {
            objArray = new Object[numParameters];
        }

        // Add the optional key to the list of parameters
        int paramCount = 0;
        if (key != null && objArray != null)
        {
            objArray[paramCount] = key;
            paramCount++;
        }

        // Add additional parameters to the list if specified
		for (int i=paramCount; i < numParameters; i++)
        {
			if (objArray != null) {
				objArray[i] = histConfig.getParameterValues().get(paramCount-1);
			}
		}

        // Invoke the service
        Object retValue = invokeService(objArray, histConfig.getServiceName(),histConfig.getChangeEventsMethodName());

		if (retValue != null)
        {
			return (Set)retValue;
		}
		else
        {
        	return new HashSet();
		}
	}
	/**
	 * Gets the predefined parameter values from the session
	 * @param request
	 * @param histConfig
	 * @return HistoryConfiguration
	 */
	protected HistoryConfiguration updateParameterValues(HttpServletRequest request, HistoryConfiguration histConfig) {
		List parameterNames = histConfig.getParameterNames();

		if (parameterNames != null && parameterNames.size() > 0) {
			List parameterValues = new ArrayList ();

			for (int i=0; i<parameterNames.size(); i++) {
				Object obj = request.getSession().getAttribute((String)parameterNames.get(i));
				parameterValues.add(obj);
			}
			
			histConfig.setParameterValues(parameterValues);
		}
		return histConfig;
	}

    protected List convert(HttpServletRequest request, HistoryConfiguration histConfig, HistoricalInfo historicalInfo) 
    throws ServiceException {
        AbstractVersionedEntity current = historicalInfo.getCurrentVersion();
        AbstractVersionedEntity previous = historicalInfo.getPreviousVersion();
        
        //Do not process deleted entities
        if (current != null && current.isDeleted().booleanValue()) {
            current = null;
        }
        if (previous != null && previous.isDeleted().booleanValue()) {
            previous = null;
        }        

    	return convert(histConfig.getDisplayPropertyMap(), 
                histConfig.getDisplayObjectMap(), current, previous);        
    }

    /**
     * Converts the keyed entity data for the specified map and creates the
     * historyInfo objects for display
     * @param propMap
     * @param current
     * @param previous
     * @return
     * @throws ServiceException
     */
    protected List convert(Map propMap, Map displayObjectMap, AbstractVersionedEntity current, AbstractVersionedEntity previous)
        throws ServiceException{
    	
    	if (propMap == null || propMap.size() == 0) 
    	{
    		if (logger.isDebugEnabled()) {
    			logger.debug("Property Map is not configured for History Action");
    		}
    		return new ArrayList ();
    	}
    	
    	Set keys = propMap.keySet();
    	List list = new ArrayList (propMap.size());
    	PropertyUtilsBean propUtils = new PropertyUtilsBean();

    	for (Iterator iter=keys.iterator(); iter.hasNext();)
    	{
    		String key = (String) iter.next();
    		String value = (String) propMap.get(key);
    		
    		//If no mapping is specified create an empty object this entity separator
     		if (StringUtils.isEmpty(value)) {

                FormattingTableRow infoBean = new FormattingTableRow(key);
                list.add(infoBean);
    		}
    		else {
    			Object curValue = null;
    			Object prevValue = null;
    			
	    		//Use BeanUtils to get the property value from current and previous	    		
		    	curValue = getNestedProperty(propUtils,current,value);	    		
		    	prevValue = getNestedProperty(propUtils,previous,value);
	    			    		
	    		String curValueAsString = null;
	    		String prevValueAsString = null;
	    		
	    		Map objectProperties = (Map) displayObjectMap.get(key);
	    		
	    		//Both the objects are null and it has object proprties defined
	    		if (curValue == null && prevValue == null && objectProperties != null) {
	    			// Force color change ?? Do we require the color change ??
                    FormattingTableRow tr = new FormattingTableRow();
                    tr.setForceColorChange(true);
		    		list.add(tr);
		    		list.addAll(createEmptyFieldHistoryObjects(objectProperties));
	    			continue;
	    		}
	    		
	    		//Use the not null object to find the type of method to call for conversion
	    		Object typeCheck = curValue == null ? prevValue : curValue;
	    		
	    		if (typeCheck instanceof Set) {
	    			//process sets
	    		    list.addAll(convert(objectProperties, displayObjectMap, (Set) curValue, (Set) prevValue));
	    		    continue;
	    		}
	    		else if (typeCheck instanceof Map && objectProperties != null) {
	    			//process Maps
	    		    list.addAll(convert(objectProperties, (Map) curValue, (Map) prevValue));
	    		    continue;
	    		}
	    		else if (typeCheck instanceof AbstractKeyedEntity && objectProperties != null) {
	    			//process abstract keyed entity (BOM Objects)
	    			list.addAll(convert(objectProperties, curValue, prevValue));
					continue;
	    		}
	    		else if (typeCheck instanceof String) {
	    			curValueAsString = (String) curValue;
	    			prevValueAsString = (String) prevValue;
	    			
	    		}else
	    		{
	    			curValueAsString = JspUtils.displayValue(curValue,NULL_TEXT,null);
	    			prevValueAsString = JspUtils.displayValue(prevValue,NULL_TEXT,null);
	    		}
	    		
	    		//create the history infor bean and add to the list only if it has
	    		//individual prperty key is specified
	    		if (objectProperties == null)
	    			list.add(new FieldHistoryInfo(key,curValueAsString,prevValueAsString));	    		
    		}
    	}    	    	
    	return list;
    }
    	
    private List convert(Map propMap, Map current, Map previous) throws ServiceException{
    	//TODO implement 
		List historyList = new ArrayList ();
    	if (propMap == null || propMap.size() == 0) 
    		return historyList;

		return historyList;
    }

    private List convert(Map propMap, Object current, Object previous) throws ServiceException{
    	List historyList = new ArrayList ();
    	if (propMap == null || propMap.size() == 0) 
    		return historyList;
    	
    	Set keys = propMap.keySet();
    	PropertyUtilsBean propUtils = new PropertyUtilsBean();
    	
    	for (Iterator iter=keys.iterator(); iter.hasNext();)
    	{
    		String key = (String) iter.next();
    		String value = (String) propMap.get(key);
    		
    		//If no mapping is specified create an empty object
     		if (StringUtils.isEmpty(value)) {
     			//Create 2nd level title
     			FormattingTableRow infoBean = new FormattingTableRow(key,true);
     			historyList.add(infoBean);
    		}
    		else {
    			Object curValue = null;
    			Object prevValue = null;
    			
	    		//Use BeanUtils to get the property value from current and previous	    		
		    	curValue = getNestedProperty(propUtils,current,value);	    		
		    	prevValue = getNestedProperty(propUtils,previous,value);
		    			    		
	    		String curValueAsString = null;
	    		String prevValueAsString = null;
	    		
	    		if (curValue instanceof String) {
	    			curValueAsString = (String) curValue;
	    			prevValueAsString = (String) prevValue;
	    		}else
	    		{
	    			curValueAsString = JspUtils.displayValue(curValue,NULL_TEXT,null);
	    			prevValueAsString = JspUtils.displayValue(prevValue,NULL_TEXT,null);
	    		}
	    		FieldHistoryInfo infoBean = 
	    			new FieldHistoryInfo(key,curValueAsString,prevValueAsString);
	    		historyList.add(infoBean);	    		
    		}
    	}
	    return historyList;
    }
    
    private List convert(Map propMap, Map displayObjectMap, Set currentSet, Set previousSet) throws ServiceException{
    	//Validate.notNull(currentSet,"current set can't be null");
		//Validate.notNull(previousSet,"previous set can't be null");
		List historyList = new ArrayList ();
    	
    	List historyObjects = processSets(currentSet, previousSet);

        FormattingTableRow trDisable = new FormattingTableRow();
        trDisable.setDisableColorChange(new Boolean(true));
        historyList.add(trDisable);

    	for (int i=0; i<historyObjects.size(); i++) {
    		//Add an empty line for seperation between objects
    		//TODO make this to change color instead of empty line
    		if (i > 0)
            {
                // Force color change
                FormattingTableRow tr = new FormattingTableRow();
                tr.setForceColorChange(true);
                historyList.add(tr);
            }

    		EntityHistory entityHistory = (EntityHistory)historyObjects.get(i);
    		Object current = entityHistory.getCurrent();
    		Object previous = entityHistory.getPrevious();
    		
    		//If some common properties are specified use them
        	if (propMap != null && propMap.size() > 0) 
        		historyList.addAll(convert(propMap, current, previous));
	   		
			//Get Object specific map if exists
			String className = getClassName(current, previous);
			Map objectPropMap = (Map) displayObjectMap.get(className);
			if (objectPropMap != null && objectPropMap.size() > 0) {
				historyList.addAll(convert(objectPropMap, current, previous));
	    	}
    	}

        FormattingTableRow trEnable = new FormattingTableRow();
        trEnable.setDisableColorChange(new Boolean(false));
        historyList.add(trEnable);

    	return historyList;
    }
    
    private String getClassName(Object obj1, Object obj2) {
    	if (obj1 != null) {
    		return obj1.getClass().getName();
    	}
    	else if (obj2 != null) {
    		return obj2.getClass().getName();
    	}
    	else
    	{
    		return null;
    	}
    }
	/**
	 * Get the configured history information - 
	 * service name and access method names for change event and history
	 * @param beanId
	 * @return HistoryConfiguration
	 */
	protected HistoryConfiguration getHistoryConfiguration(String beanId) throws ServiceException{
		return (HistoryConfiguration) getBean(beanId);
	}
	
    /**
     * Gets the nested property value using bean utils 
     * @param propUtils
     * @param source
     * @param nestedPropName (. separated)
     * @return
     * @throws ServiceException
     */
    private Object getNestedProperty(PropertyUtilsBean propUtils, Object source, String nestedPropName) 
    throws ServiceException {
    	if (StringUtils.isEmpty(nestedPropName) || nestedPropName.equals(NO_MAPPING)) {
    		return null;
    	}
    	else if (nestedPropName.equals(IS_NOT_NULL_YES_NO)) {
    		return source == null ? TEXT_NO : TEXT_YES;
    	}
    	else if (nestedPropName.equals(IS_NOT_NULL_TRUE_FALSE)) {
    		return source == null ? TEXT_FALSE : TEXT_TRUE;
    	}
    	
    	//Check for the existance of function call
    	String methodName = null;
    	String propertyName = nestedPropName;
    	if (nestedPropName.indexOf('(') > -1) {
    		//extract methodName and the property Name
    		methodName = nestedPropName.substring(0,nestedPropName.indexOf('('));
    		propertyName = nestedPropName.substring(
    			nestedPropName.indexOf('(')+1,nestedPropName.indexOf(')'));
    	}

    	try {
        	if (source == null) {
        		return null;
        	}
        	else {
        		//Executes a custome method on this class if specified in the mapping
        			Object propertyValue = THIS_OBJECT.equals(propertyName) ? source :
        					propUtils.getNestedProperty(source,propertyName);
        			if (methodName != null)        			
        				return executeMethod(methodName,propertyValue);
        			return propertyValue;
        	}
    	}
    	catch (NestedNullException np) {
    		//Ignore all nullpointer exceptions
    		return null;
    	}catch (NullPointerException npe) {
    		//Ignore all nullpointer exceptions
    		return null;
    	}catch (Exception ex) {
    		throw new ServiceException ("Conversion to HistoryInfo failed property:" + 
    				nestedPropName + "class " + source.getClass().getName(),ex);
    	}
    }
    
    /**
     * Generic code that calls a method on the action class to 
     * convert/format a property value
     * @param methodName
     * @param propertyValue
     * @return 
     * @throws InvocationTargetException
     */
    private Object executeMethod(String methodName, Object propertyValue) throws Exception
    {
    	if (propertyValue != null && methodName != null)
        {
            Object[] params = new Object[] { propertyValue };
            Class currentClass = this.getClass();
            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(this, params);
                }
                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 '" +
                this.getClass().getName() + "' class hierarchy.");
        }
    	else
        {
            return propertyValue;
        }
    }
    
    /**
     * Match the entity objects from current to previous sets and
     * return the list of EntityHistory objects
     * @param current
     * @param previous
     * @return
     */
    private List processSets(Set current, Set previous) {
        return matchSets(current, previous);
    }

    /**
     * Match the entity objects from current to previous sets and
     * return the list of EntityHistory objects
     * @param current
     * @param previous
     * @return
     */
    private List processMaps(Map current, Map previous) {
    	Set allKeySet = new HashSet(current.keySet());
    	allKeySet.addAll(previous.keySet());
    	List list = new ArrayList ();
    	
    	for (Iterator i=allKeySet.iterator(); i.hasNext();) {
    		Object key = i.next();
       		//get the matchig elements from both the sets (based on key)  		 
    		Object objPrevious = previous.get(key);
    		Object objCurrent = current.get(key);
    		list.add(new EntityHistory(objCurrent,objPrevious));
    	}
    	//return the list
    	return list;
    }    

    private boolean match(IncomeTestStatus statusSrc, IncomeTestStatus statusTarget) {
    	if (statusSrc.getType().getIdentifier().equals(
    			statusTarget.getType().getIdentifier())) {
    		return true;
    	}
    	else
    		return false;
    }    
    /**
     * 
     * @param eligibilitySrc
     * @param eligibilityTarget
     * @return
     */
    private boolean match(Eligibility eligibilitySrc, Eligibility eligibilityTarget) {
    	//compare types
    	if (eligibilitySrc.getType() != null && eligibilityTarget.getType() != null) {
	    	if (eligibilitySrc.getType().getIdentifier().equals(
	    			eligibilityTarget.getType().getIdentifier())) {
	    		return true;
	    	}
    	}
    	//compare factors    	
    	else if (eligibilitySrc.getFactor() != null && eligibilityTarget.getFactor() != null){
	    	if (eligibilitySrc.getFactor().getIdentifier().equals(
	    			eligibilityTarget.getFactor().getIdentifier())) {
	    		return true;
	    	}
    	}
    	return false;
    }
    
    private boolean match(AbstractKeyedEntity src, AbstractKeyedEntity target) {
        if (src.getEntityKey().getKeyValue().equals(
                target.getEntityKey().getKeyValue())) 
        {
            return true;
        }
        else
            return false;
    }
    
    private boolean match(AbstractLookup src, AbstractLookup target) {
        if (src.getIdentifier().equals(
                target.getIdentifier()) && 
            src.getClass().getName().equals(target.getClass().getName())) 
        {
            return true;
        }
        else
            return false;
    }
    
    public boolean match(Object src, Object target) {
    	if (src instanceof Eligibility)
    		return match((Eligibility) src, (Eligibility) target);
    	else if (src instanceof IncomeTestStatus)
    		return match((IncomeTestStatus) src, (IncomeTestStatus) target);
        else if (src instanceof AbstractKeyedEntity)
            return match((AbstractKeyedEntity)src, (AbstractKeyedEntity) target);
        else if (src instanceof AbstractLookup)
            return match((AbstractLookup)src, (AbstractLookup) target);
        else
            return src.equals(target) ? true : false;
    }    
    /**
     * Creates empty list for properties where both current and previous objects are null
     * @param propMap
     * @return
     */
    private List createEmptyFieldHistoryObjects(Map propMap) {
    	List historyList = new ArrayList ();
    	if (propMap == null || propMap.size() == 0) 
    		return historyList;
    	
    	Set keys = propMap.keySet();
    	
    	for (Iterator iter=keys.iterator(); iter.hasNext();)
    	{
    		String key = (String) iter.next();
    		historyList.add(new FieldHistoryInfo(key,null,null));
    	}
    	return historyList;
    }
}
