/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.tem.service.impl;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.tem.TemConstants;
import org.kuali.kfs.module.tem.TemConstants.PerDiemParameter;
import org.kuali.kfs.module.tem.TemConstants.TravelAuthorizationParameters;
import org.kuali.kfs.module.tem.TemConstants.TravelDocTypes;
import org.kuali.kfs.module.tem.TemConstants.TravelParameters;
import org.kuali.kfs.module.tem.TemConstants.TravelReimbursementParameters;
import org.kuali.kfs.module.tem.TemParameterConstants;
import org.kuali.kfs.module.tem.batch.PerDiemLoadStep;
import org.kuali.kfs.module.tem.batch.businessobject.MealBreakDownStrategy;
import org.kuali.kfs.module.tem.businessobject.AccountingDistribution;
import org.kuali.kfs.module.tem.businessobject.ExpenseTypeObjectCode;
import org.kuali.kfs.module.tem.businessobject.MileageRate;
import org.kuali.kfs.module.tem.businessobject.PerDiem;
import org.kuali.kfs.module.tem.businessobject.PerDiemExpense;
import org.kuali.kfs.module.tem.businessobject.TemExpense;
import org.kuali.kfs.module.tem.businessobject.TravelerDetail;
import org.kuali.kfs.module.tem.businessobject.TripType;
import org.kuali.kfs.module.tem.dataaccess.PerDiemDao;
import org.kuali.kfs.module.tem.dataaccess.TravelDocumentDao;
import org.kuali.kfs.module.tem.document.TravelAuthorizationDocument;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.module.tem.document.TravelReimbursementDocument;
import org.kuali.kfs.module.tem.document.web.struts.TravelFormBase;
import org.kuali.kfs.module.tem.service.PerDiemService;
import org.kuali.kfs.module.tem.service.TemExpenseService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.module.tem.util.ExpenseUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
import org.kuali.kfs.sys.util.KfsDateUtils;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.location.api.state.State;
import org.kuali.rice.location.api.state.StateService;
import org.springframework.transaction.annotation.Transactional;

import java.sql.Date;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.kuali.kfs.module.tem.TemConstants.TravelReimbursementParameters.LODGING_OBJECT_CODE;
import static org.kuali.kfs.module.tem.TemConstants.TravelReimbursementParameters.PER_DIEM_OBJECT_CODE;

/**
 * implement the service method calls defined in PerDiemService
 */
@Transactional
public class PerDiemServiceImpl extends ExpenseServiceBase implements PerDiemService, TemExpenseService {

    private static Logger LOG = Logger.getLogger(PerDiemServiceImpl.class);

    protected DateTimeService dateTimeService;
    protected ParameterService parameterService;
    protected BusinessObjectService businessObjectService;
    protected StateService stateService;
    protected PerDiemDao perDiemDao;
    protected Map<String, MealBreakDownStrategy> mealBreakDownStrategies;
    protected String allStateCodes;
    protected TravelDocumentDao travelDocumentDao;
    protected TravelExpenseService travelExpenseService;

    List<PerDiem> persistedPerDiems;


    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#breakDownMealsIncidental(java.util.List)
     */
    @Override
    public <T extends PerDiem> void breakDownMealsIncidental(List<T> perDiemList) {
        for (T perDiem : perDiemList) {
            this.breakDownMealsIncidental(perDiem);
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#breakDownMealsIncidental(org.kuali.kfs.module.tem.businessobject.PerDiem)
     */
    @Override
    public <T extends PerDiem> void breakDownMealsIncidental(T perDiem) {
        String conusIndicator = perDiem.getConusIndicator();
        if (this.getMealBreakDownStrategies().containsKey(conusIndicator)) {
            MealBreakDownStrategy mealBreakDownStrategy = this.getMealBreakDownStrategies().get(conusIndicator);

            mealBreakDownStrategy.breakDown(perDiem);
        } else {
            throw new RuntimeException("Fail to meal break down strategy for the CONUS indicator: " + conusIndicator);
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#retrieveActivePerDiem()
     */
    public <T extends PerDiem> List<T> retrieveInactivePerDiem() {
        Map<String, Object> fieldValues = new HashMap<String, Object>();
        fieldValues.put(KFSPropertyConstants.ACTIVE, Boolean.FALSE);

        return (List<T>) this.getBusinessObjectService().findMatching(PerDiem.class, fieldValues);
    }


    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#updateTripType(java.util.List)
     */
    @Override
    public <T extends PerDiem> void updateTripType(List<T> perDiemList) {
        for (T perDiem : perDiemList) {
            this.updateTripType(perDiem);
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#updateTripType(org.kuali.kfs.module.tem.businessobject.PerDiem)
     */
    @Override
    public <T extends PerDiem> void updateTripType(T perDiem) {
        String region = perDiem.getPrimaryDestination().getRegion().getRegionCode();
        String institutionState = this.getInstitutionState();

        String tripTypeCode = this.getInternationalTripTypeCode();
        if (StringUtils.equals(region, institutionState)) {
            tripTypeCode = this.getInStateTripTypeCode();
        } else if (getAllStateCodes().contains(";" + region.toUpperCase() + ";")) {
            tripTypeCode = this.getOutStateTripTypeCode();
        }

        perDiem.getPrimaryDestination().getRegion().setTripTypeCode(tripTypeCode);
    }

    /**
     * get institution state code defined as an application parameter
     *
     * @return institution state code
     */
    protected String getInstitutionState() {
        String institutionState = this.getParameterService().getParameterValueAsString(PerDiemLoadStep.class, PerDiemParameter.INSTITUTION_STATE_PARAM_NAME);

        return institutionState;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#retrievePreviousPerDiem(org.kuali.kfs.module.tem.businessobject.PerDiem)
     */
    @Override
    public <T extends PerDiem> List<PerDiem> retrievePreviousPerDiem(T perDiem) {

        return (List<PerDiem>) perDiemDao.findSimilarPerDiems(perDiem);
    }


    /**
     * check whether the given per diem exists in the database
     *
     * @param perDiem the given per diem
     * @return true if the given per diem exists in the database
     */
    @Override
    public <T extends PerDiem> boolean hasExistingPerDiem(T perDiem) {
        if (ObjectUtils.isNull(persistedPerDiems)) {
            persistedPerDiems = (List<PerDiem>) businessObjectService.findAll(PerDiem.class);
        }


        boolean retval = persistedPerDiems.contains(perDiem);

        return retval;

    }

    /**
     * get out state trip type code defined as an application parameter
     *
     * @return out state trip type code
     */
    protected String getOutStateTripTypeCode() {
        String outStateTripTypeCode = this.getParameterService().getParameterValueAsString(PerDiemLoadStep.class, PerDiemParameter.OUT_STATE_TRIP_TYPE_CODE_PARAM_NAME);

        return outStateTripTypeCode;
    }

    /**
     * get in state trip type code defined as an application parameter
     *
     * @return in state trip type code
     */
    protected String getInStateTripTypeCode() {
        String inStateTripTypeCode = this.getParameterService().getParameterValueAsString(PerDiemLoadStep.class, PerDiemParameter.IN_STATE_TRIP_TYPE_CODE_PARAM_NAME);

        return inStateTripTypeCode;
    }

    /**
     * get international trip type code defined as an application parameter
     *
     * @return international trip type code
     */
    protected String getInternationalTripTypeCode() {
        String internationalTripTypeCode = this.getParameterService().getParameterValueAsString(PerDiemLoadStep.class, PerDiemParameter.INTERNATIONAL_TRIP_TYPE_CODE_PARAM_NAME);

        return internationalTripTypeCode;
    }

    /**
     * Gets the parameterService attribute.
     *
     * @return Returns the parameterService.
     */
    @Override
    public ParameterService getParameterService() {
        return parameterService;
    }

    /**
     * Sets the parameterService attribute value.
     *
     * @param parameterService The parameterService to set.
     */
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    /**
     * Gets the businessObjectService attribute.
     *
     * @return Returns the businessObjectService.
     */
    @Override
    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    /**
     * Sets the businessObjectService attribute value.
     *
     * @param businessObjectService The businessObjectService to set.
     */
    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    /**
     * Gets the dateTimeService attribute.
     *
     * @return Returns the dateTimeService.
     */
    public DateTimeService getDateTimeService() {
        return dateTimeService;
    }

    /**
     * Sets the dateTimeService attribute value.
     *
     * @param dateTimeService The dateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    /**
     * Gets the perDiemDao attribute.
     *
     * @return Returns the perDiemDao
     */

    public PerDiemDao getPerDiemDao() {
        return perDiemDao;
    }

    /**
     * Sets the perDiemDao attribute.
     *
     * @param perDiemDao The perDiemDao to set.
     */
    public void setPerDiemDao(PerDiemDao perDiemDao) {
        this.perDiemDao = perDiemDao;
    }

    /**
     * Gets the mealBreakDownStrategies attribute.
     *
     * @return Returns the mealBreakDownStrategies.
     */
    public Map<String, MealBreakDownStrategy> getMealBreakDownStrategies() {
        return mealBreakDownStrategies;
    }

    /**
     * Sets the mealBreakDownStrategies attribute value.
     *
     * @param mealBreakDownStrategies The mealBreakDownStrategies to set.
     */
    public void setMealBreakDownStrategies(Map<String, MealBreakDownStrategy> mealBreakDownStrategies) {
        this.mealBreakDownStrategies = mealBreakDownStrategies;
    }

    /**
     * Gets the stateService attribute.
     *
     * @return Returns the stateService.
     */
    public StateService getStateService() {
        return stateService;
    }

    /**
     * Sets the stateService attribute value.
     *
     * @param stateService The stateService to set.
     */
    public void setStateService(StateService stateService) {
        this.stateService = stateService;
    }

    /**
     * Sets the allStateCodes attribute value.
     *
     * @param allStateCodes The allStateCodes to set.
     */
    public void setAllStateCodes(String allStateCodes) {
        this.allStateCodes = allStateCodes;
    }

    /**
     * Gets the allStateCodes attribute.
     *
     * @return Returns the allStateCodes.
     */
    public String getAllStateCodes() {

        if (StringUtils.isEmpty(allStateCodes)) {
            final List<State> codes = getStateService().findAllStatesInCountry(KFSConstants.COUNTRY_CODE_UNITED_STATES);

            final StringBuffer sb = new StringBuffer();
            sb.append(";").append(KFSConstants.COUNTRY_CODE_UNITED_STATES.toUpperCase()).append(";");
            for (final State state : codes) {
                if (state.isActive()) {
                    sb.append(state.getCode().toUpperCase()).append(";");
                }
            }

            allStateCodes = sb.toString();
        }

        return allStateCodes;
    }

    public String getObjectCodeFrom(final TravelDocument travelDocument, String paramName) {
        if (travelDocument instanceof TravelReimbursementDocument) {
            final String parameterValue = getParameterService().getParameterValueAsString(TravelReimbursementDocument.class, paramName);
            String paramSearchStr = "";
            TravelerDetail traveler = travelDocument.getTraveler();
            if (traveler != null) {
                paramSearchStr += traveler.getTravelerTypeCode() + "=";
            }
            TripType tripType = travelDocument.getTripType();
            if (tripType != null) {
                paramSearchStr += tripType.getCode() + "=";
            }

            final int searchIdx = parameterValue.indexOf(paramSearchStr);

            if (searchIdx == -1) {
                return null;
            }

            final int endIdx = parameterValue.indexOf(";", searchIdx);
            if (endIdx == -1) {
                return parameterValue.substring(searchIdx + paramSearchStr.length());
            }

            return parameterValue.substring(searchIdx + paramSearchStr.length(), endIdx);
        } else {
            if (travelDocument.getTripType() != null) {
                return travelDocument.getTripType().getEncumbranceObjCode();
            }
            return null;
        }
    }

    @Override
    public Map<String, AccountingDistribution> getAccountingDistribution(TravelDocument document) {
        Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();
        if (document.getPerDiemExpenses() != null
            && document.getPerDiemExpenses().size() > 0) {
            String defaultChartCode = ExpenseUtils.getDefaultChartCode(document);
            String perDiemCode = getObjectCodeFrom(document, PER_DIEM_OBJECT_CODE);
            if (document instanceof TravelAuthorizationDocument) {
                if (document.getTripType() != null) {
                    perDiemCode = document.getTripType().getEncumbranceObjCode();
                } else {
                    perDiemCode = null;
                }
            }
            LOG.debug("Looking up Object Code for chart = " + defaultChartCode + " perDiemObjectCode = " + perDiemCode);
            final ObjectCode perDiemObjCode = getObjectCodeService().getByPrimaryIdForCurrentYear(defaultChartCode, perDiemCode);
            LOG.debug("Got per diem object code " + perDiemObjCode);

            // Per Diem
            AccountingDistribution accountingDistribution = new AccountingDistribution();

            if (perDiemObjCode != null) {
                String key = perDiemObjCode.getCode() + "-" + document.getDefaultCardTypeCode();

                for (PerDiemExpense expense : document.getPerDiemExpenses()) {
                    if (!expense.getPersonal()) {
                        if (!distributionMap.containsKey(key)) {
                            accountingDistribution.setCardType(document.getDefaultCardTypeCode());
                            accountingDistribution.setObjectCode(perDiemObjCode.getCode());
                            accountingDistribution.setObjectCodeName(perDiemObjCode.getName());
                            distributionMap.put(key, accountingDistribution);
                        }
                        distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().add(expense.getMealsAndIncidentals()));
                        distributionMap.get(key).setRemainingAmount(distributionMap.get(key).getRemainingAmount().add(expense.getMealsAndIncidentals()));
                        LOG.debug("Set perdiem distribution subtotal to " + accountingDistribution.getSubTotal());
                    }
                }

                if (document.getPerDiemAdjustment() != null
                    && document.getPerDiemAdjustment().isPositive()) {
                    distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().subtract(document.getPerDiemAdjustment()));
                    distributionMap.get(key).setRemainingAmount(distributionMap.get(key).getRemainingAmount().subtract(document.getPerDiemAdjustment()));
                }
                distributeLodging(distributionMap, document);
                distributeMileage(distributionMap, document);
            } else {
                LOG.error("PerDiemObjCode is null!");
            }
        }

        return distributionMap;
    }

    protected void distributeLodging(Map<String, AccountingDistribution> distributionMap, final TravelDocument document) {
        String defaultChartCode = ExpenseUtils.getDefaultChartCode(document);
        String lodgingCode = getObjectCodeFrom(document, LODGING_OBJECT_CODE);
        if (document instanceof TravelAuthorizationDocument) {
            if (document.getTripType() != null) {
                lodgingCode = document.getTripType().getEncumbranceObjCode();
            } else {
                lodgingCode = null;
            }
        }
        LOG.debug("Looking up Object Code for chart = " + defaultChartCode + " lodgingCode = " + lodgingCode);
        final ObjectCode lodgingObjCode = getObjectCodeService().getByPrimaryIdForCurrentYear(defaultChartCode, lodgingCode);
        LOG.debug("Got lodging object code " + lodgingObjCode);

        AccountingDistribution accountingDistribution = new AccountingDistribution();
        String key = lodgingObjCode.getCode() + "-" + document.getDefaultCardTypeCode();

        if (document.getPerDiemExpenses() != null) {
            for (PerDiemExpense expense : document.getPerDiemExpenses()) {
                if (!distributionMap.containsKey(key)) {
                    accountingDistribution.setCardType(document.getDefaultCardTypeCode());
                    accountingDistribution.setObjectCode(lodgingObjCode.getCode());
                    accountingDistribution.setObjectCodeName(lodgingObjCode.getName());
                    distributionMap.put(key, accountingDistribution);
                }
                distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().add(expense.getLodgingTotal()));
                distributionMap.get(key).setRemainingAmount(distributionMap.get(key).getRemainingAmount().add(expense.getLodgingTotal()));
            }
        }
    }

    protected void distributeMileage(Map<String, AccountingDistribution> distributionMap, TravelDocument document) {
        String defaultChartCode = ExpenseUtils.getDefaultChartCode(document);

        AccountingDistribution accountingDistribution = new AccountingDistribution();

        if (document.getPerDiemExpenses() != null) {
            for (PerDiemExpense expense : document.getPerDiemExpenses()) {
                if (!StringUtils.isBlank(expense.getMileageRateExpenseTypeCode())) {
                    String mileageCode = null;
                    if (document instanceof TravelAuthorizationDocument && document.getTripType() != null) {
                        mileageCode = document.getTripType().getEncumbranceObjCode();
                    } else {
                        final String travelerTypeCode = (ObjectUtils.isNull(document.getTraveler())) ? null : document.getTraveler().getTravelerTypeCode();
                        final ExpenseTypeObjectCode expenseTypeObjectCode = getTravelExpenseService().getExpenseType(expense.getMileageRateExpenseTypeCode(), document.getDocumentTypeName(), document.getTripTypeCode(), travelerTypeCode);
                        if (expenseTypeObjectCode != null) {
                            mileageCode = expenseTypeObjectCode.getFinancialObjectCode();
                        }
                    }
                    LOG.debug("Looking up Object Code for chart = " + defaultChartCode + " mileageCode = " + mileageCode);
                    final ObjectCode mileageObjCode = getObjectCodeService().getByPrimaryIdForCurrentYear(defaultChartCode, mileageCode);
                    LOG.debug("Got mileage object code " + mileageObjCode);
                    String key = mileageObjCode.getCode() + "-" + document.getDefaultCardTypeCode();

                    if (!distributionMap.containsKey(key)) {
                        accountingDistribution.setCardType(document.getDefaultCardTypeCode());
                        accountingDistribution.setObjectCode(mileageObjCode.getCode());
                        accountingDistribution.setObjectCodeName(mileageObjCode.getName());
                        distributionMap.put(key, accountingDistribution);
                    }
                    distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().add(expense.getMileageTotal()));
                    distributionMap.get(key).setRemainingAmount(distributionMap.get(key).getRemainingAmount().add(expense.getMileageTotal()));
                }
            }
        }
    }

    /**
     * @see org.kuali.kfs.module.tem.service.impl.ExpenseServiceBase#getAllExpenseTotal(org.kuali.kfs.module.tem.document.TravelDocument, boolean)
     */
    @Override
    public KualiDecimal getAllExpenseTotal(TravelDocument document, boolean includeNonReimbursable) {
        KualiDecimal total = KualiDecimal.ZERO;

        for (PerDiemExpense expense : document.getPerDiemExpenses()) {
            if ((expense.getPersonal().booleanValue() && includeNonReimbursable)
                || !expense.getPersonal().booleanValue()) {
                total = total.add(expense.getDailyTotalForDocument(document));
            }
        }

        return total;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.impl.ExpenseServiceBase#getNonReimbursableExpenseTotal(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public KualiDecimal getNonReimbursableExpenseTotal(TravelDocument document) {
        // This is because for per diem, when the personal checkbox is checked the amount is already excluded from the total.
        return KualiDecimal.ZERO;
    }

    @Override
    public KualiDecimal getMealsAndIncidentalsGrandTotal(TravelDocument travelDocument) {
        KualiDecimal mealsAndIncidentalsTotal = KualiDecimal.ZERO;
        for (PerDiemExpense expense : travelDocument.getPerDiemExpenses()) {
            mealsAndIncidentalsTotal = mealsAndIncidentalsTotal.add(expense.getMealsAndIncidentals());
        }
        return mealsAndIncidentalsTotal;
    }

    @Override
    public KualiDecimal getLodgingGrandTotal(TravelDocument travelDocument) {
        KualiDecimal lodgingTotal = KualiDecimal.ZERO;
        for (PerDiemExpense perDiemExpense : travelDocument.getPerDiemExpenses()) {
            if (!perDiemExpense.getPersonal()) {
                lodgingTotal = lodgingTotal.add(perDiemExpense.getLodgingTotal());
            }
        }
        return lodgingTotal;
    }

    @Override
    public KualiDecimal getMileageTotalGrandTotal(TravelDocument travelDocument) {
        KualiDecimal mileageTotal = KualiDecimal.ZERO;
        for (PerDiemExpense perDiemExpense : travelDocument.getPerDiemExpenses()) {
            mileageTotal = mileageTotal.add(perDiemExpense.getMileageTotal());
        }
        return mileageTotal;
    }

    @Override
    public KualiDecimal getDailyTotalGrandTotal(TravelDocument travelDocument) {
        KualiDecimal dailyTotal = KualiDecimal.ZERO;
        for (PerDiemExpense perDiemExpense : travelDocument.getPerDiemExpenses()) {
            dailyTotal = dailyTotal.add(perDiemExpense.getDailyTotal());
        }
        return dailyTotal;
    }

    @Override
    public Integer getMilesGrandTotal(TravelDocument travelDocument) {
        Integer milesTotal = 0;
        for (PerDiemExpense perDiemExpense : travelDocument.getPerDiemExpenses()) {
            milesTotal = milesTotal + perDiemExpense.getMiles();
        }
        return milesTotal;
    }

    /**
     * Determines if per diem is handling lodging based on KFS-TEM / Document / PER_DIEM_CATEGORIES
     *
     * @see org.kuali.kfs.module.tem.service.PerDiemService#isPerDiemHandlingLodging()
     */
    @Override
    public boolean isPerDiemHandlingLodging() {
        final Collection<String> perDiemCategoryValues = getParameterService().getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class, TemConstants.TravelParameters.PER_DIEM_CATEGORIES);
        for (String perDiemCategoryValue : perDiemCategoryValues) {
            final String[] keyValueSplit = perDiemCategoryValue.split("=");
            if (keyValueSplit.length == 2) {
                if (TemConstants.PerDiemType.lodging.name().equalsIgnoreCase(keyValueSplit[0])) {
                    return KFSConstants.ParameterValues.YES.equalsIgnoreCase(keyValueSplit[1]);
                }
            }
        }
        return false;
    }

    /**
     * Uses travelDocumentDao to look up per diem records (TravelDocumentDao handles the the date wierdnesses of per diem date)
     *
     * @see org.kuali.kfs.module.tem.service.PerDiemService#getPerDiem(int, java.sql.Date, java.sql.Date)
     */
    @Override
    public PerDiem getPerDiem(int primaryDestinationId, java.sql.Timestamp perDiemDate, java.sql.Date effectiveDate) {
        final List<PerDiem> possiblePerDiems = getTravelDocumentDao().findEffectivePerDiems(primaryDestinationId, effectiveDate);
        Date date = KfsDateUtils.clearTimeFields(new Date(perDiemDate.getTime()));

        if (possiblePerDiems.isEmpty()) {
            return null;
        }
        if (possiblePerDiems.size() == 1) {
            return possiblePerDiems.get(0);
        }

        Collections.sort(possiblePerDiems, new PerDiemComparator());
        PerDiem foundPerDiem = null;
        for (PerDiem perDiem : possiblePerDiems) {
            if (isOnOrAfterSeasonBegin(perDiem.getSeasonBeginMonthAndDay(), perDiemDate)) {
                foundPerDiem = perDiem;
            }
        }
        if (foundPerDiem == null) {
            // no found per diem, so let's take the *last* one of the list (because years are circular and so the last of the list represents the beginning of the year; see KFSTP-926 for a discussion of this)
            foundPerDiem = possiblePerDiems.get(possiblePerDiems.size() - 1);
        }

        return foundPerDiem;
    }

    /**
     * Comparator to help us sort per diem records by season begin month/day
     */
    protected class PerDiemComparator implements Comparator<PerDiem> {
        /**
         * next compare method I write will use patty and selma, I promise
         * Sorts the season begin month/days such that earlier dates are chosen before later dates
         *
         * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
         */
        @Override
        public int compare(PerDiem viola, PerDiem sebastian) {
            if (StringUtils.isBlank(viola.getSeasonBeginMonthAndDay())) {
                if (StringUtils.isBlank(sebastian.getSeasonBeginMonthAndDay())) {
                    return 0;
                }
                return 1; // sebastian has a value - choose sebastian
            }
            if (StringUtils.isBlank(sebastian.getSeasonBeginMonthAndDay())) {
                return -1; // viola has a value but not sebastian - choose viola
            }

            final String[] violaSeasonBegin = viola.getSeasonBeginMonthAndDay().split("/");
            final String[] sebastianSeasonBegin = sebastian.getSeasonBeginMonthAndDay().split("/");

            final int violaBeginMonth = Integer.parseInt(violaSeasonBegin[0]);
            final int sebastianBeginMonth = Integer.parseInt(sebastianSeasonBegin[0]);
            if (violaBeginMonth != sebastianBeginMonth) {
                return violaBeginMonth - sebastianBeginMonth;
            }

            final int violaBeginDay = Integer.parseInt(violaSeasonBegin[1]);
            final int sebastianBeginDay = Integer.parseInt(sebastianSeasonBegin[1]);
            if (violaBeginDay != sebastianBeginDay) {
                return violaBeginDay - sebastianBeginDay;
            }
            return 0;
        }
    }

    /**
     * Determines if the given date happens on or after the given season begin month/day for the year of the given date
     *
     * @param seasonBegin the season begin month/day to check
     * @param d           the date to check if on or after season begin
     * @return true if the given date is on or after the season begin date, false otherwise
     */
    protected boolean isOnOrAfterSeasonBegin(String seasonBegin, java.sql.Timestamp d) {
        if (StringUtils.isBlank(seasonBegin)) {
            return true; // no season begin/end?  Well...then we're after that, I should think
        }

        Calendar dCal = Calendar.getInstance();
        dCal.setTime(d);
        final int year = dCal.get(Calendar.YEAR);

        Calendar seasonBeginCal = getSeasonBeginMonthDayCalendar(seasonBegin, year);

        if (KfsDateUtils.isSameDay(dCal, seasonBeginCal)) { // let's see if they're on the same day, regardless of time
            return true;
        }
        if (dCal.after(seasonBeginCal)) { // now that we know they're not on the same day, time isn't such a big deal
            return true;
        }
        return false;
    }

    /**
     * Given a season begin month/day and a year, returns a Calendar representing the date
     *
     * @param seasonBegin the season begin month/day
     * @param year        the year to set for the calendar
     * @return the Calendar from the given date information
     */
    protected Calendar getSeasonBeginMonthDayCalendar(String seasonBegin, int year) {
        final String[] seasonBeginMonthDay = seasonBegin.split("/");
        Calendar seasonBeginCal = Calendar.getInstance();
        seasonBeginCal.set(Calendar.MONTH, Integer.parseInt(seasonBeginMonthDay[0]) - 1);
        seasonBeginCal.set(Calendar.DATE, Integer.parseInt(seasonBeginMonthDay[1]));
        seasonBeginCal.set(Calendar.YEAR, year);
        return seasonBeginCal;
    }


    @Override
    public void processExpense(TravelDocument travelDocument, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
        //do nothing
    }

    @Override
    public void updateExpense(TravelDocument travelDocument) {
        //do nothing
    }

    @Override
    public void calculateDistributionTotals(TravelDocument document, Map<String, AccountingDistribution> distributionMap, List<? extends TemExpense> expenses) {
        //not used for PerDiem
    }

    @Override
    public List<? extends TemExpense> getExpenseDetails(TravelDocument document) {
        //not used for PerDiem
        return null;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.PerDiemService#setPerDiemCategories(org.kuali.kfs.module.tem.document.web.struts.TravelFormBase)
     */
    @Override
    public void setPerDiemCategoriesAndBreakdown(TravelFormBase form) {
        Collection<String> perDiemCats = getParameterService().getParameterValuesAsString(TemParameterConstants.TEM_DOCUMENT.class, TravelParameters.PER_DIEM_CATEGORIES);
        form.parsePerDiemCategories(perDiemCats);

        //default to TA
        Boolean showPerDiemBreakdown = parameterService.getParameterValueAsBoolean(TravelAuthorizationDocument.class, TravelAuthorizationParameters.PER_DIEM_AMOUNT_EDITABLE_IND);
        if (TravelDocTypes.TRAVEL_REIMBURSEMENT_DOCUMENT.equals(form.getDocTypeName())) {
            showPerDiemBreakdown = parameterService.getParameterValueAsBoolean(TravelReimbursementDocument.class, TravelReimbursementParameters.PER_DIEM_AMOUNT_EDITABLE_IND);
        }
        form.setShowPerDiemBreakdown(showPerDiemBreakdown);
    }

    /**
     * Determines if:
     * <ul>
     * <li>A current mileage rate for the KFS-TEM / Document / PER_DIEM_MILEAGE_RATE_EXPENSE_TYPE_CODE is available; if it is not, then per diem cannot be created
     * </ul>
     *
     * @param form the form with the document on it, which may help in making such a decision
     */
    @Override
    public boolean isMileageRateAvailableForAllPerDiem(TravelDocument doc) {
        final String defaultPerDiemMileageRate = getDefaultPerDiemMileageRateExpenseType();
        if (StringUtils.isBlank(defaultPerDiemMileageRate)) {
            return false;
        }
        if (StringUtils.isBlank(doc.getTripTypeCode()) || doc.getTripBegin() == null || doc.getTripEnd() == null) {
            return true; // we can't create per diem when trip begin or end are blank anyhow - but we shouldn't get the error
        }
        // now we need to loop through each day from begin date to end date to see if there is a validate mileage rate record for it
        Calendar currDay = Calendar.getInstance();
        currDay.setTime(KfsDateUtils.clearTimeFields(doc.getTripBegin()));
        Calendar lastDay = Calendar.getInstance();
        lastDay.setTime(KfsDateUtils.clearTimeFields(doc.getTripEnd()));

        while (currDay.before(lastDay) || currDay.equals(lastDay)) {
            java.sql.Date effectiveDay = doc.getEffectiveDateForPerDiem(new java.sql.Timestamp(currDay.getTimeInMillis()));
            final MileageRate currDayMileageRate = getMileageRateService().findMileageRateByExpenseTypeCodeAndDate(defaultPerDiemMileageRate, effectiveDay);
            if (currDayMileageRate == null) {
                return false;
            }
            currDay.add(Calendar.DATE, 1);
        }
        // we're good
        return true;
    }

    /**
     * Does the parameter look-up so that validations don't have to
     *
     * @see org.kuali.kfs.module.tem.service.PerDiemService#getDefaultPerDiemMileageRateExpenseType()
     */
    @Override
    public String getDefaultPerDiemMileageRateExpenseType() {
        final String defaultPerDiemMileageRate = getParameterService().getParameterValueAsString(TemParameterConstants.TEM_DOCUMENT.class, TemConstants.TravelParameters.PER_DIEM_MILEAGE_RATE_EXPENSE_TYPE_CODE, KFSConstants.EMPTY_STRING);
        return defaultPerDiemMileageRate;
    }

    public TravelDocumentDao getTravelDocumentDao() {
        return travelDocumentDao;
    }

    public void setTravelDocumentDao(TravelDocumentDao travelDocumentDao) {
        this.travelDocumentDao = travelDocumentDao;
    }

    public TravelExpenseService getTravelExpenseService() {
        return travelExpenseService;
    }

    public void setTravelExpenseService(TravelExpenseService travelExpenseService) {
        this.travelExpenseService = travelExpenseService;
    }
}
