// Package 
package gov.va.med.esr.common.report.data.impl;

// Java Classes
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Arrays;
import java.util.Collection;
import java.lang.reflect.Field;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;

// Library Classes
import org.hibernate.Query;
import org.hibernate.Session;

// Framework Classes
import gov.va.med.fw.report.ReportConfiguration;
import gov.va.med.fw.report.data.ReportDataException;
import gov.va.med.fw.report.data.QueryCriteria;
import gov.va.med.fw.util.Reflector;
import gov.va.med.fw.util.StringUtils;

// ESR Classes
import gov.va.med.esr.common.report.data.StandardReportCriteria;
import gov.va.med.esr.common.report.data.CommonCriteria;
import gov.va.med.esr.common.model.report.ReportSetup;
import gov.va.med.esr.common.model.report.ReportParameterSet;
import gov.va.med.esr.common.model.report.BaseReportLookupParameter;
import gov.va.med.esr.common.model.lookup.ReportDivision;
import gov.va.med.esr.common.model.lookup.ReportLetterFile;
import gov.va.med.esr.common.model.lookup.MessageType;
import gov.va.med.esr.common.model.lookup.ComLetterType;

/**
 * A reporting DAO that substitutes ReportParameterSet parameters into the query.  The QueryCriteria that is contained
 * within the ReportConfiguration must be of type StandardReportCriteria for the ReportParameterSet to be obtained.
 * <p/>
 * If the paramFields list is present, only those parameters will be processed.  Otherwise, if the paramFields list is
 * null, then all non-null fields in the ReportParameterSet will be processed.
 * <p/>
 * This class provides the ability to add custom "convert" methods that will convert a report parameter
 * into the value(s) that should be used in the query parameter.  These methods must be named
 * "convert<field_name>Param" (e.g. for the field "division", the conversion method name would be called
 * "convertDivisionParam").
 *
 * @author Andrew Pach
 */
public class ParamSubstitutionDAOImpl extends AbstractStandardReportDataDAOImpl
{
    /**
    * An instance of serialVersionUID 
    */
   private static final long serialVersionUID = -7503843209738315984L;

   // Map that contains parameter fields mapped to their query parameter names
    private static Map paramToQueryMap = new HashMap();

    // Optional list of parameter fields to use
    private List paramFields = null;

    /**
     * Default constructor.
     */
    public ParamSubstitutionDAOImpl()
    {
        super();

        // Initialize the report parameter to query parameter map.
        paramToQueryMap.put("fromDate", CommonCriteria.START_DATE);
        paramToQueryMap.put("toDate", CommonCriteria.END_DATE);
        paramToQueryMap.put("division", CommonCriteria.COM_LETTER_TYPE);
        paramToQueryMap.put("letterFile", CommonCriteria.MESSAGE_TYPE);
        paramToQueryMap.put("internalBadAddressReasons", CommonCriteria.BAD_ADDRESS_REASON);
        paramToQueryMap.put("internalRegistryTypes", CommonCriteria.REGISTRY_TYPE);
        paramToQueryMap.put("internalEnrollmentStatuses", CommonCriteria.ENROLLMENT_STATUSES);
        paramToQueryMap.put("internalMessageErrors", CommonCriteria.MESSAGE_ERROR);
        paramToQueryMap.put("internalMessageTypes", CommonCriteria.MESSAGE_TYPE);
        paramToQueryMap.put("daysBetweenUpdates", CommonCriteria.DAYS_BETWEEN_UPDATES);
    }
            
    public void preDataRetrieval( ReportConfiguration config ) throws ReportDataException {
        super.preDataRetrieval(config);
        
        QueryCriteria reportCriteria = config.getQueryCriteria();
        if( reportCriteria instanceof StandardReportCriteria ) {
           StandardReportCriteria criteria = (StandardReportCriteria)reportCriteria;
           ReportSetup setup = criteria.getReportSetup();
           ReportParameterSet parameters = setup != null ? setup.getParameterSet() : null;
           if( parameters == null ) {
                throw new ReportDataException("Missing report parameters in report " + config.getReportID() );
            }
           if(parameters.getAsOfDate() != null) {
               criteria.addCriterion(CommonCriteria.AS_OF_DATE,parameters.getAsOfDate());
           }
           if(parameters.getPhUnconfirmedDays() != null) {
               criteria.addCriterion(CommonCriteria.PH_UNCONFIRMED_DAYS,parameters.getPhUnconfirmedDays());
           }
           if(parameters.getDaysBetweenUpdates() != null) {
               criteria.addCriterion(CommonCriteria.DAYS_BETWEEN_UPDATES,parameters.getDaysBetweenUpdates());
           }
           if(parameters.getAddressUpdateDays() != null) {
               criteria.addCriterion(CommonCriteria.ADDRESS_UPDATE_DAYS,parameters.getAddressUpdateDays());
           }
        }
    }
    
    /**
     * Builds the query by getting the query by name and substituting parameters.
     *
     * @see gov.va.med.fw.report.data.hibernate.HibernateReportDataDAO#buildQuery(gov.va.med.fw.report.ReportConfiguration)
     */
    protected Query buildQuery(ReportConfiguration config, Session session ) throws ReportDataException
    {
        // Get the query whose parameters need to be substituted
        Query query = this.getNamedQuery( session );
        List queryNamedParams = Arrays.asList(query.getNamedParameters());

        QueryCriteria reportCriteria = config.getQueryCriteria();
        if (reportCriteria instanceof StandardReportCriteria)
        {
            // Get the report parameter set
            StandardReportCriteria criteria = (StandardReportCriteria)reportCriteria;
            ReportSetup setup = criteria.getReportSetup();
            ReportParameterSet parameters = setup != null ? setup.getParameterSet() : null;
            if (parameters == null)
            {
                throw new ReportDataException("Missing report parameters in report " + config.getReportID());
            }

            // Get the field to value map
            Map fieldToValueMap = getFieldToValueMap(parameters);

            // Determine which fields to use.  If the param fields are null, then use all fields
            List paramFieldsToUse = getParamFields();
            if (paramFieldsToUse == null)
            {
                paramFieldsToUse = new ArrayList(fieldToValueMap.keySet());
            }

            // Process each field
            for (Iterator iterator = paramFieldsToUse.iterator(); iterator.hasNext();)
            {
                String fieldName = (String)iterator.next();
                Object fieldValue = fieldToValueMap.get(fieldName);

                // Treat empty collections as null values as well
                if ((fieldValue != null) && (Collection.class.isAssignableFrom(fieldValue.getClass())))
                {
                    Collection col = (Collection)fieldValue;
                    if (col.size() == 0)
                    {
                        fieldValue = null;
                    }
                }

                // Only process fields whose values are not null (either because the field didn't exist
                // or because the parameter value wasn't present)
                if (fieldValue != null)
                {
                    // Get the query parameter
                    String queryParam = (String)getParamToQueryMap().get(fieldName);

                    // If the query parameter wasn't found, see if the parameter is the field name
                    if (queryParam == null)
                    {
                        if (queryNamedParams.contains(fieldName))
                        {
                            queryParam = fieldName;
                        }
                    }

                    // If we have a query parameter, process it
                    processQueryParameter(query, fieldName, fieldValue, queryParam);
                }
            }
        }

        // Return the query
        return query;
    }

    /**
     * Gets the list of report parameter set fields and their values in a map
     *
     * @param parameters The report setup parameters
     *
     * @return A map of field names to their values.
     */
    protected Map getFieldToValueMap(ReportParameterSet parameters)
    {
        // Initialize the field to value map
        Map fieldToValueMap = new HashMap();

        // Get all fields (including private) and ensure they are accessible
        Field[] fields = null;
        fields = ReportParameterSet.class.getDeclaredFields();
        AccessibleObject.setAccessible(fields, true); // would fail with SecurityManager

        // Get all the field values and store the fields and their values in the map
        try
        {
            for (int i = 0; i < fields.length; i++)
            {
                Field field = fields[i];
                fieldToValueMap.put(field.getName(), field.get(parameters));
            }
        }
        catch (Exception ex)
        {
            throw new RuntimeException("Unable to retrieve field property values from ReportParameterSet.", ex);
        }

        // Return the field to value map
        return fieldToValueMap;
    }

    /**
     * Processes an individual query parameter
     * @param query The query
     * @param fieldName The field name
     * @param fieldValue The field value
     * @param queryParam The named query parameter
     */
    protected void processQueryParameter(Query query, String fieldName, Object fieldValue, String queryParam)
    {
        // Only process this parameter if all fields are present
        if ((query != null) && (StringUtils.isNotBlank(fieldName)) && (fieldValue != null) &&
            (StringUtils.isNotBlank(queryParam)))
        {
            // See if there is a custom parameter processor and execute it
            Object[] methodParams = new Object[]{fieldValue};
            String customMethodName = "convert" + String.valueOf(fieldName.charAt(0)).toUpperCase() +
                (fieldName.length() > 1 ? fieldName.substring(1, fieldName.length()) : "") + "Param";
            Object updatedFieldValue = null;
            try
            {
                updatedFieldValue = executeMethod(this, customMethodName, methodParams);
            }
            catch (Exception ex)
            {
                throw new RuntimeException("Exception thrown executing custom method: " + customMethodName, ex);
            }

            // If a parameter converter was found, use the value it returned
            if (updatedFieldValue != null)
            {
                fieldValue = updatedFieldValue;
            }

            // Set the value on the query
            if (Collection.class.isAssignableFrom(fieldValue.getClass()))
            {
                // Ensure the collection contains at least 1 entry
                Collection queryCollection = (Collection)fieldValue;
                if (queryCollection.size() > 0)
                {
                    // See if the collection contains lookups
                    Object element = queryCollection.iterator().next();
                    if (BaseReportLookupParameter.class.isAssignableFrom(element.getClass()))
                    {
                        // Create a new query parameter collection of the lookup codes
                        Collection lookupCollection = new ArrayList();
                        for (Iterator iterator = queryCollection.iterator(); iterator.hasNext();)
                        {
                            BaseReportLookupParameter baseReportLookupParameter =
                                (BaseReportLookupParameter)iterator.next();
                            lookupCollection.add(baseReportLookupParameter.getLookup().getCode());
                        }
                        queryCollection = lookupCollection;
                    }
                    query.setParameterList(queryParam, queryCollection);
                }
            }
            else
            {
                query.setParameter(queryParam, fieldValue);
            }
        }
    }

    /**
     * Converts a division parameter to a list of letter type codes
     * @param fieldValue the ReportDivision
     * @return The list of letter type codes
     */
    protected List convertDivisionParam(Object fieldValue)
    {
        // Typecast field value
        ReportDivision division = (ReportDivision)fieldValue;

        ArrayList comLetterTypeList = new ArrayList();
        if ((division.getCode().equals(ReportDivision.ENR.getCode())) ||
            (division.getCode().equals(ReportDivision.ENR_AND_IVM.getCode())))
        {
            comLetterTypeList.add(ComLetterType.CODE_ENROLLMENT.getCode());
        }
        if ((division.getCode().equals(ReportDivision.IVM.getCode())) ||
            (division.getCode().equals(ReportDivision.ENR_AND_IVM.getCode())))
        {
            comLetterTypeList.add(ComLetterType.CODE_IVM.getCode());
        }

        if (comLetterTypeList.size() <= 0)
        {
            throw new RuntimeException("No support for division code: " + division.getCode());
        }

        // Return the message types
        return comLetterTypeList;
    }

    /**
     * Converts a ReportLetterFile code into a list of MessageType codes
     * @param fieldValue the ReportLetterFile
     * @return The list of message type codes
     */
    protected List convertLetterFileParam(Object fieldValue)
    {
        // Typecast field value
        ReportLetterFile letterFile = (ReportLetterFile)fieldValue;

        ArrayList messageTypeList = new ArrayList();
        if ((letterFile.getCode().equals(ReportLetterFile.ADDRESS.getCode())) ||
            (letterFile.getCode().equals(ReportLetterFile.ALL.getCode())))
        {
            messageTypeList.add(MessageType.CODE_COM_ADDRESS_UPDATE.getCode());
        }
        if ((letterFile.getCode().equals(ReportLetterFile.CODE_1_REJECT.getCode())) ||
            (letterFile.getCode().equals(ReportLetterFile.ALL.getCode())))
        {
            messageTypeList.add(MessageType.CODE_COM_REJECT.getCode());
        }
        if ((letterFile.getCode().equals(ReportLetterFile.ERROR.getCode())) ||
            (letterFile.getCode().equals(ReportLetterFile.ALL.getCode())))
        {
            messageTypeList.add(MessageType.CODE_COM_ERROR.getCode());
        }

        if (messageTypeList.size() <= 0)
        {
            throw new RuntimeException("No support for letter file: " + letterFile.getCode());
        }

        // Return the message types
        return messageTypeList;
    }

    /**
     * Executes a method on the target object passing in the params.
     *
     * @param targetObj the target object to execute the method on
     * @param methodName the method name of the method to invoke
     * @param params the parameters that will be passed to the method
     *
     * @return The return value of the method or null if no method could be found.
     * @throws Exception if any problems were encountered
     */
    protected Object executeMethod(Object targetObj, String methodName, Object[] params) throws Exception
    {
        if (targetObj != null && methodName != null && params != 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 (NoSuchMethodException nsmf)
                {
                    currentClass = currentClass.getSuperclass();
                }
            }
        }

        // If a method couldn't be found, return null.
        return null;
    }

    protected static Map getParamToQueryMap()
    {
        return paramToQueryMap;
    }

    protected static void setParamToQueryMap(Map paramToQueryMap)
    {
        ParamSubstitutionDAOImpl.paramToQueryMap = paramToQueryMap;
    }

    public List getParamFields()
    {
        return paramFields;
    }

    public void setParamFields(List paramFields)
    {
        this.paramFields = paramFields;
    }
}
