// Package 
package gov.va.med.esr.ui.report.action;

// Java Classes
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

// Library Classes
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionMessage;
import net.mlw.vlh.ValueList;

// Framework Classes
import gov.va.med.fw.ui.struts.ValueListActionUtils;
import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.report.ReportExportedType;

// ESR Classes
import gov.va.med.esr.service.UnknownLookupCodeException;
import gov.va.med.esr.service.UnknownLookupTypeException;
import gov.va.med.esr.ui.common.util.DateUtils;
import gov.va.med.esr.ui.ApplicationConstants;
import gov.va.med.esr.common.model.lookup.StandardReport;
import gov.va.med.esr.common.model.lookup.ReportType;
import gov.va.med.esr.common.model.report.SimpleCompletedReport;
import gov.va.med.esr.common.model.report.ReportSetupLite;

/**
 * Abstract Action for filtered reports.
 *
 * @author Andrew Pach
 * @version 3.0
 */
public abstract class ReportFilterAction extends ReportAction
{
    /**
     * Gets the value list table Id.
     *
     * @return the table id.
     */
    public abstract String getTableId();

    /**
     * Gets the value list adapter.
     *
     * @return the adapter.
     */
    public abstract String getValueListAdapter();

    /**
     * Displays the report data.
     *
     * @param mapping Struts action mapping for this action
     * @param form Struts form bean for this action
     * @param request The Http Request
     * @param response The Http Response
     *
     * @return A struts action forward for where we will go next.
     * @throws Exception If there are any errors during processing.
     */
    public ActionForward displayReportData(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        // Note that we should only be able to apply a filter once all results have already been
        // retrieved or when no
        ReportFilterForm filterForm = (ReportFilterForm)form;

        // Don't display the filter when we are refreshing the data in some way.
        filterForm.setDisplayFilter(false);

        // Set whether a filter was applied on the form
        filterForm.setFilterApplied(filterForm.getContainsFilterCriteria());

        // Get and store the value list information
        ValueList valueList =
            ValueListActionUtils.getValueList(form, request, getTableId(), getValueListAdapter());
        ValueListActionUtils.setValueList(request, valueList, "vlhlist");

        // Cache the report filters ONLY when no filters are present.  Otherwise, our new filters will be
        // working on previously filtered data.
        if (!filterForm.getContainsFilterCriteria())
        {
            cacheReportFilters(ValueListActionUtils.getCachedResults(request.getSession(), getTableId()),
                request);
        }

        // Process any info or error messages in session
        processMessagesInSession(request);

        // Display the reports
        return mapping.findForward(DISPLAY_DATA);
    }

    /**
     * Applies a report filter.
     *
     * @param mapping Struts action mapping for this action
     * @param form Struts form bean for this action
     * @param request The Http Request
     * @param response The Http Response
     *
     * @return A struts action forward for where we will go next.
     * @throws Exception If there are any errors during processing.
     */
    public ActionForward applyFilter(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        // Note that we should only be able to apply a filter once all results have already been
        // retrieved or when no
        ReportFilterForm filterForm = (ReportFilterForm)form;

        // Validate the filter and add error messages as needed.
        // Only refetch the data when we have valid filter information or if the data is already cached.
        if (!validateFilter(request, filterForm))
        {
            if (ValueListActionUtils.isCachedResults(request.getSession(), getTableId()))
            {
                // Re-wrap the valuelist results to make them accessible from the UI
                ValueList valueList = ValueListActionUtils
                    .getValueList(form, request, getTableId(), getValueListAdapter());
                ValueListActionUtils.setValueList(request, valueList, "vlhlist");
            }

            // Display the filter when there are validation errors.
            filterForm.setDisplayFilter(true);

            // Refresh the existing page without re-quering the data
            return mapping.findForward(DISPLAY_DATA);
        }
        else
        {
            // Refresh the data
            return refreshData(mapping, form, request, response);
        }
    }

    /**
     * Resets the reports filter.
     *
     * @param mapping Struts action mapping for this action
     * @param form Struts form bean for this action
     * @param request The Http Request
     * @param response The Http Response
     *
     * @return A struts action forward for where we will go next.
     * @throws Exception If there are any errors during processing.
     */
    public ActionForward resetFilter(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        // Reset the filter values
        ReportFilterForm filterForm = (ReportFilterForm)form;
        filterForm.resetFilter();

        // Refresh the data
        return refreshData(mapping, form, request, response);
    }

    /**
     * Refreshes the data without resetting the filter.  This action entry point is different from
     * displayReportData in that it will clear the previously cached results first.
     *
     * @param mapping Struts action mapping for this action
     * @param form Struts form bean for this action
     * @param request The Http Request
     * @param response The Http Response
     *
     * @return A struts action forward for where we will go next.
     * @throws Exception If there are any errors during processing.
     */
    public ActionForward refreshData(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        // Clear the cached results.  This will force a new search.
        ValueListActionUtils.clearCachedResults(request.getSession(), getTableId());

        // Place any error or info messages in the session
        placeMessagesInSession(request);

        // Display the reports (data will be refreshed)
        return mapping.findForward(DISPLAY_UPDATED_DATA);
    }

    /**
     * Places any error or info messages in the session.  This is needed when forwarding to an ActionForward whose
     * redirect="true".
     *
     * @param request The HttpServletRequest.
     */
    protected void placeMessagesInSession(HttpServletRequest request)
    {
        String value = getValueFromParamOrAtttributeKey(request, INFO_MESSAGE);
        if (value != null)
        {
            setMessageInSession(request.getSession(), value, false);
        }
        value = getValueFromParamOrAtttributeKey(request, ERROR_MESSAGE);
        if (value != null)
        {
            setMessageInSession(request.getSession(), value, true);
        }
    }

    /**
     * Places any error or info messages in the session.  This is needed when forwarding to an ActionForward whose
     * redirect="true".
     *
     * @param request The HttpServletRequest.
     */
    protected void processMessagesInSession(HttpServletRequest request)
    {
        // Process the informational messages
        String value = getMessageFromSession(request.getSession(), false);
        setMessageInSession(request.getSession(), null, false);
        processMessage(request, INFO_MESSAGE, value);

        // Process the error messages
        value = getMessageFromSession(request.getSession(), true);
        processMessage(request, ERROR_MESSAGE, value);
        setMessageInSession(request.getSession(), null, true);
    }

    /**
     * Caches the report filters in session.
     *
     * @param reportList The list of reports.  This must be either a list of one of the following: SimpleCompletedReport
     * or ReportSetupLite.
     * @param request the HttpServletRequest.
     */
    protected void cacheReportFilters(List reportList, HttpServletRequest request)
    {
        // Sort out the various filters in their own maps.
        // Putting them into maps will eliminate duplicates since the map key's are the displayable values.
        Map reportTypeMap = new HashMap();
        Map reportIdMap = new HashMap();
        Map reportTitleMap = new HashMap();
        Map fileTypeMap = new HashMap();
        for (Iterator iterator = reportList.iterator(); iterator.hasNext();)
        {
            Object report = (Object)iterator.next();
            StandardReport standardReport = getStandardReport(report);
            if (standardReport != null)
            {
                // Process the report title (i.e. the standard report description)
                if ((StringUtils.isNotEmpty(standardReport.getDescription())) &&
                    (StringUtils.isNotEmpty(standardReport.getCode())))
                {
                    reportTitleMap.put(standardReport.getDescription(), standardReport);
                }

                // Process the report Id (i.e. the standard report code)
                if (StringUtils.isNotEmpty(standardReport.getCode()))
                {
                    reportIdMap.put(standardReport.getCode(), standardReport);
                }

                // Process the report type
                ReportType reportType = standardReport.getType();
                if ((reportType != null) && (StringUtils.isNotEmpty(reportType.getDescription())) &&
                    (StringUtils.isNotEmpty(reportType.getCode())))
                {
                    reportTypeMap.put(reportType.getCode(), reportType);
                }

                // Process the file type
                ReportExportedType fileType = getReportExportedType(report);
                if ((fileType != null) && (StringUtils.isNotEmpty(fileType.getDescription())) &&
                    (StringUtils.isNotEmpty(fileType.getCode())))
                {
                    fileTypeMap.put(fileType.getCode(), fileType);
                }
            }
        }

        // Create lists for each filter with duplicates removed
        List reportTypeList = new ArrayList(reportTypeMap.values());
        List reportIdList = new ArrayList(reportIdMap.values());
        List reportTitleList = new ArrayList(reportTitleMap.values());
        List fileTypeList = new ArrayList(fileTypeMap.values());
        
        //Cache the report titles (Standard reports sorted by description)
        Collections.sort(reportTitleList, StandardReport.getNameComparator());
        request.getSession().setAttribute(REPORT_TITLE_LIST, reportTitleList);

        //Cache the report Ids (Standard reports sorted by code)
        Collections.sort(reportIdList, StandardReport.getCodeComparator());
        request.getSession().setAttribute(REPORT_ID_LIST, reportIdList);

        // Cache the report types (Report Type sorted by description)
        Collections.sort(reportTypeList, ReportType.getReportTypeNameComparator());
        request.getSession().setAttribute(REPORT_TYPE_LIST, reportTypeList);

        // Cache the file types (Report Exported Type sorted by description)
        Collections.sort(fileTypeList, ReportExportedType.getNameComparator());
        request.getSession().setAttribute(FILE_TYPE_LIST, fileTypeList);

      }

    /**
     * Gets the standard report from the passed in object.  The object should be of one of the following types:
     * SimpleCompletedReport or ReportSetupLite.  Otherwise an IllegalArgumentException will be thrown.
     *
     * @param report the report
     *
     * @return the standard report
     */
    private StandardReport getStandardReport(Object report)
    {
        // Get the standard report from the various object instances
        StandardReport standardReport = null;
        if (report instanceof SimpleCompletedReport)
        {
            standardReport = ((SimpleCompletedReport)report).getStandardReport();
        }
        else
        {
            if (report instanceof ReportSetupLite)
            {
                standardReport = ((ReportSetupLite)report).getReport();
            }
            else
            {
                throw new IllegalArgumentException("Unable to get a standard report from object of type: " +
                    report.getClass().getName());
            }
        }

        // Return the standard report
        return standardReport;
    }

    /**
     * Gets the report exported type from the passed in object.  The object should be of one of the following types:
     * SimpleCompletedReport or ReportSetupLite.  Otherwise an IllegalArgumentException will be thrown.
     *
     * @param report the report
     *
     * @return the report exported type
     */
    private ReportExportedType getReportExportedType(Object report)
    {
        // Get the standard report from the various object instances
        ReportExportedType reportExportedType = null;
        if (report instanceof SimpleCompletedReport)
        {
            reportExportedType = ((SimpleCompletedReport)report).getFileType();
        }
        else
        {
            if (report instanceof ReportSetupLite)
            {
                reportExportedType = ((ReportSetupLite)report).getFileType();
            }
            else
            {
                throw new IllegalArgumentException("Unable to get a report exported type from object of type: " +
                    report.getClass().getName());
            }
        }

        // Return the standard report
        return reportExportedType;
    }

    /**
     * Validates the filter data and adds error messages as needed.
     *
     * @param request The HttpServletRequest.
     * @param form The completed reports form.
     *
     * @return True if the filters are valid or false if not.
     * @throws gov.va.med.esr.service.UnknownLookupCodeException if the lookup code for the Report Id is invalid (should
     * never happen).
     * @throws gov.va.med.esr.service.UnknownLookupTypeException if the lookup type for the Report Id is invalid (should
     * never happen).
     */
    protected boolean validateFilter(HttpServletRequest request, ReportFilterForm form)
        throws UnknownLookupCodeException, UnknownLookupTypeException
    {
        // Assume filter is valid
        boolean valid = true;

        // Validate dates
        Date fromDate = null;
        Date toDate = null;
        if (StringUtils.isNotEmpty(form.getDateFrom()))
        {
            try
            {
                fromDate = DateUtils.getDate(form.getDateFrom());
            }
            catch (Exception e)
            {
                addActionMessageForField(request,
                    new ActionMessage(ApplicationConstants.MessageKeys.ERRORS_DATE, form.getDateFrom()),
                    LABEL_DATE_FILTER);
                valid = false;
            }
        }
        if (StringUtils.isNotEmpty(form.getDateTo()))
        {
            try
            {
                toDate = DateUtils.getDate(form.getDateTo());
            }
            catch (Exception e)
            {
                addActionMessageForField(request,
                    new ActionMessage(ApplicationConstants.MessageKeys.ERRORS_DATE, form.getDateTo()),
                    LABEL_DATE_FILTER);
                valid = false;
            }
        }

        // Ensure dates are within range
        if ((fromDate != null) && (toDate != null) && (toDate.getTime() < fromDate.getTime()))
        {
            addActionMessageForField(request,
                new ActionMessage(ERROR_INVALID_START_END_DATES, form.getDateTo(), form.getDateFrom()),
                LABEL_DATE_FILTER);
            valid = false;
        }

        // Ensure the Report Id and the Report Title are consistent if both entered.
        if ((StringUtils.isNotEmpty(form.getReportId())) && (StringUtils.isNotEmpty(form.getReportTitle())) &&
            (!form.getReportId().equals(form.getReportTitle())))
        {
            addActionMessageForField(request, ERROR_REPORT_ID_AND_TITLE_MUST_MATCH, LABEL_REPORT_ID);
            addActionMessageForField(request, ERROR_REPORT_ID_AND_TITLE_MUST_MATCH, LABEL_REPORT_TITLE);
            valid = false;
        }
        else
        {
            // Ensure the Report Id and/or the Report Title are consistent with the report type if both entered.
            // This filter should go AFTER the report Id and Title consistency check.
            if (((StringUtils.isNotEmpty(form.getReportId())) || (StringUtils.isNotEmpty(form.getReportTitle()))) &&
                (StringUtils.isNotEmpty(form.getReportType())))
            {
                // Get the report Id from either the Id or Title since both use the same code at this point.
                boolean reportIdSpecified = false;
                boolean reportTitleSpecified = false;
                String reportId = form.getReportId();
                if (StringUtils.isNotEmpty(form.getReportId()))
                {
                    reportIdSpecified = true;
                    reportId = form.getReportId();
                }
                if (StringUtils.isNotEmpty(form.getReportTitle()))
                {
                    reportTitleSpecified = true;
                    reportId = form.getReportTitle();
                }

                // Determine if the Report Id matches the Report Type
                StandardReport standardReport = getLookupService().getStandardReportByCode(reportId);
                ReportType reportType = standardReport.getType();
                if (!reportType.getCode().equals(form.getReportType()))
                {
                    // Add the error messages to the appropriate fields.
                    if (reportIdSpecified)
                    {
                        addActionMessageForField(request, ERROR_REPORT_ID_AND_TYPE_MUST_MATCH, LABEL_REPORT_ID);
                    }
                    if (reportTitleSpecified)
                    {
                        addActionMessageForField(request, ERROR_REPORT_ID_AND_TYPE_MUST_MATCH, LABEL_REPORT_TITLE);
                    }
                    addActionMessageForField(request, ERROR_REPORT_ID_AND_TYPE_MUST_MATCH, LABEL_REPORT_TYPE);
                    valid = false;
                }
            }
        }
        // Return whether the filter is valid or not
        return valid;
    }

    /**
     * Sets an error message bundle key to display an error message to the user.
     *
     * @param session The HttpSession
     * @param messageBundleKey The resource bundle key for the message to be displayed.
     * @param errorMessage If true, an error message will be produced.  Otherwise, an information message will be
     * displayed.
     */
    protected void setMessageInSession(HttpSession session, String messageBundleKey, boolean errorMessage)
    {
        session.setAttribute(errorMessage ? ERROR_MESSAGE_SESSION_KEY : INFO_MESSAGE_SESSION_KEY, messageBundleKey);
    }

    /**
     * Gets the error message bundle key for displaying an error message to the user.
     *
     * @param session The HttpSession
     * @param errorMessage If true, the error message will be returned.  Otherwise, the informational message will
     * be returned.
     *
     * @return The resource bundle key for the message to be displayed.
     */
    protected String getMessageFromSession(HttpSession session, boolean errorMessage)
    {
        return (String)session.getAttribute(errorMessage ? ERROR_MESSAGE_SESSION_KEY : INFO_MESSAGE_SESSION_KEY);
    }
}
