/*
 * 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.ld.businessobject.lookup;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.gl.Constant;
import org.kuali.kfs.integration.ld.businessobject.inquiry.AbstractPositionDataDetailsInquirableImpl;
import org.kuali.kfs.kns.lookup.AbstractLookupableHelperServiceImpl;
import org.kuali.kfs.kns.lookup.HtmlData;
import org.kuali.kfs.kns.lookup.HtmlData.AnchorHtmlData;
import org.kuali.kfs.krad.lookup.CollectionIncomplete;
import org.kuali.kfs.krad.util.BeanPropertyComparator;
import org.kuali.kfs.module.ld.businessobject.EmployeeFunding;
import org.kuali.kfs.module.ld.businessobject.LaborLedgerPendingEntry;
import org.kuali.kfs.module.ld.businessobject.inquiry.EmployeeFundingInquirableImpl;
import org.kuali.kfs.module.ld.businessobject.inquiry.PositionDataDetailsInquirableImpl;
import org.kuali.kfs.module.ld.service.LaborInquiryOptionsService;
import org.kuali.kfs.module.ld.service.LaborLedgerBalanceService;
import org.kuali.kfs.module.ld.service.LaborLedgerPendingEntryService;
import org.kuali.kfs.module.ld.util.DebitCreditUtil;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.ObjectUtil;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.krad.bo.BusinessObject;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static org.kuali.kfs.module.ld.LaborConstants.BalanceInquiries.EMPLOYEE_FUNDING_EXPENSE_OBJECT_TYPE_CODE;
import static org.kuali.kfs.module.ld.LaborConstants.BalanceInquiries.EMPLOYEE_FUNDING_NORMAL_OP_EXPENSE_OBJECT_TYPE_CODE;

/**
 * The EmployeeFundingLookupableHelperServiceImpl class is the front-end for all Employee Funding balance inquiry processing.
 */
public class EmployeeFundingLookupableHelperServiceImpl extends AbstractLookupableHelperServiceImpl {
    private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory.getLog(EmployeeFundingLookupableHelperServiceImpl.class);

    private LaborLedgerBalanceService laborLedgerBalanceService;
    private LaborInquiryOptionsService laborInquiryOptionsService;
    private LaborLedgerPendingEntryService laborLedgerPendingEntryService;

    /**
     * @see org.kuali.rice.kns.lookup.Lookupable#getInquiryUrl(org.kuali.rice.krad.bo.BusinessObject, java.lang.String)
     */
    @Override
    public HtmlData getInquiryUrl(BusinessObject bo, String propertyName) {
        if (KFSPropertyConstants.POSITION_NUMBER.equals(propertyName)) {
            EmployeeFunding employeeFunding = (EmployeeFunding) bo;
            AbstractPositionDataDetailsInquirableImpl positionDataDetailsInquirable = new PositionDataDetailsInquirableImpl();

            Map<String, String> fieldValues = new HashMap<String, String>();
            fieldValues.put(propertyName, employeeFunding.getPositionNumber());

            BusinessObject positionData = positionDataDetailsInquirable.getBusinessObject(fieldValues);

            return positionData == null ? new AnchorHtmlData(KFSConstants.EMPTY_STRING, KFSConstants.EMPTY_STRING) : positionDataDetailsInquirable.getInquiryUrl(positionData, propertyName);
        }

        return (new EmployeeFundingInquirableImpl()).getInquiryUrl(bo, propertyName);
    }

    /**
     * @see org.kuali.rice.kns.lookup.Lookupable#gfetSearchResults(java.util.Map)
     */
    @Override
    public List<? extends BusinessObject> getSearchResults(Map<String, String> fieldValues) {
        setBackLocation(fieldValues.get(KFSConstants.BACK_LOCATION));
        setDocFormKey(fieldValues.get(KFSConstants.DOC_FORM_KEY));

        boolean showBlankLine = this.showBlankLines(fieldValues);
        fieldValues.remove(Constant.BLANK_LINE_OPTION);

        // get the pending entry option. This method must be prior to the get search results
        String pendingEntryOption = laborInquiryOptionsService.getSelectedPendingEntryOption(fieldValues);

        // test if the consolidation option is selected or not
        boolean isConsolidated = false;

        Collection<EmployeeFunding> searchResultsCollection = laborLedgerBalanceService.findEmployeeFundingWithCSFTracker(fieldValues, isConsolidated);

        if (!showBlankLine) {
            Collection<EmployeeFunding> tempSearchResultsCollection = new ArrayList<EmployeeFunding>();
            for (EmployeeFunding employeeFunding : searchResultsCollection) {
                // KFSCNTRB-1534- Properly group CSF Items and show all CSF items that should be shown
                if (employeeFunding.getCurrentAmount().isNonZero() ||
                    employeeFunding.getOutstandingEncumbrance().isNonZero() ||
                    (employeeFunding.getCsfAmount() != null && employeeFunding.getCsfAmount().isNonZero())) {
                    tempSearchResultsCollection.add(employeeFunding);
                }
            }
            searchResultsCollection = tempSearchResultsCollection;
        }

        // update search results according to the selected pending entry option
        updateByPendingLedgerEntry(searchResultsCollection, fieldValues, pendingEntryOption, isConsolidated);
        searchResultsCollection = consolidateObjectTypeCode(searchResultsCollection);
        // get the actual size of all qualified search results
        Long actualSize = new Long(searchResultsCollection.size());

        return this.buildSearchResultList(searchResultsCollection, actualSize);
    }

    /**
     * Filter search results to consolidate them if the only difference is object type code.
     *
     * @param searchResultsCollection
     * @return
     */
    private Collection<EmployeeFunding> consolidateObjectTypeCode(Collection<EmployeeFunding> searchResultsCollection) {
        // KFSCNTRB-1534- Properly group CSF Items and show all CSF items that should be shown
        Collection<EmployeeFunding> ret = new ArrayList<EmployeeFunding>(searchResultsCollection.size());
        for (EmployeeFunding empFunding : searchResultsCollection) {
            EmployeeFunding temp = findEmployeeFunding(ret, empFunding);
            if (temp == null) {
                ret.add(empFunding);
            } else {
                /*we need to act on `temp' because that's the one from the collection.*/
                temp.setCsfFullTimeEmploymentQuantity(add(temp.getCsfFullTimeEmploymentQuantity(), empFunding.getCsfFullTimeEmploymentQuantity()));
                temp.setCsfAmount(add(temp.getCsfAmount(), empFunding.getCsfAmount()));
                temp.setCurrentAmount(add(temp.getCurrentAmount(), empFunding.getCurrentAmount()));
                temp.setOutstandingEncumbrance(add(temp.getOutstandingEncumbrance(), empFunding.getOutstandingEncumbrance()));
                temp.setTotalAmount(add(temp.getTotalAmount(), empFunding.getTotalAmount()));
            }
        }
        return ret;
    }

    /**
     * Searches the given collection for an element that is equal to the given exhibit for purposes of consolidation.
     *
     * @param coll    The collection to search for a like element.
     * @param exhibit The search criteria.
     * @return The element from the collection that matches the exhibit or null if 1) no items match or
     * the 2) exhibit is null.
     */
    private static EmployeeFunding findEmployeeFunding(Collection<EmployeeFunding> coll, EmployeeFunding exhibit) {
        if (exhibit == null) {
            return null;
        }

        for (EmployeeFunding temp : coll) {
            if (temp == null) {
                continue;
            }

            if (ObjectUtils.notEqual(temp.getEmplid(), exhibit.getEmplid())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getUniversityFiscalYear(), exhibit.getUniversityFiscalYear())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getChartOfAccountsCode(), exhibit.getChartOfAccountsCode())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getAccountNumber(), exhibit.getAccountNumber())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getSubAccountNumber(), exhibit.getSubAccountNumber())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getFinancialObjectCode(), exhibit.getFinancialObjectCode())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getFinancialSubObjectCode(), exhibit.getFinancialSubObjectCode())) {
                continue;
            }
            if (ObjectUtils.notEqual(temp.getPositionNumber(), exhibit.getPositionNumber())) {
                continue;
            }
            return temp;
        }
        /*no items in the collection match the exhibit.*/
        return null;
    }

    /**
     * Adds two KualiDecimal objects in a null-safe way. If one of them is null, the other is returned, otherwise,
     * a new KualiDecimal containing their sum is returned.
     *
     * @param one The first KualiDecimal to add.
     * @param two The second KualiDecimal to add.
     * @return The sum of the two KualiDecimals.
     */
    private static KualiDecimal add(KualiDecimal one, KualiDecimal two) {
        if (one == null) {
            return two;
        }
        if (two == null) {
            return one;
        }
        return one.add(two);
    }

    /**
     * Adds two BigDecimal objects. If one of them is null, the other is returned, otherwise,
     * a new BigDecimal containing their sum is returned.
     *
     * @param one The first BigDecimal to add.
     * @param two The second BigDecimal to add.
     * @return The sum of the two BigDecimals.
     */
    private static BigDecimal add(BigDecimal one, BigDecimal two) {
        if (one == null) {
            return two;
        }
        if (two == null) {
            return one;
        }
        return one.add(two);
    }

    private boolean showBlankLines(Map fieldValues) {
        String pendingEntryOption = (String) fieldValues.get(Constant.BLANK_LINE_OPTION);
        return Constant.SHOW_BLANK_LINE.equals(pendingEntryOption) ? true : false;
    }

    /**
     * build the serach result list from the given collection and the number of all qualified search results
     *
     * @param searchResultsCollection the given search results, which may be a subset of the qualified search results
     * @param actualSize              the number of all qualified search results
     * @return the serach result list with the given results and actual size
     */
    protected List buildSearchResultList(Collection searchResultsCollection, Long actualSize) {
        CollectionIncomplete results = new CollectionIncomplete(searchResultsCollection, actualSize);

        // sort list if default sort column given
        List searchResults = results;
        List defaultSortColumns = getDefaultSortColumns();
        if (defaultSortColumns.size() > 0) {
            Collections.sort(results, new BeanPropertyComparator(defaultSortColumns, true));
        }
        return searchResults;
    }

    /**
     * @see org.kuali.kfs.module.ld.service.LaborInquiryOptionsService#updateByPendingLedgerEntry(java.util.Collection,
     * java.util.Map, java.lang.String, boolean)
     */
    public void updateByPendingLedgerEntry(Collection entryCollection, Map fieldValues, String pendingEntryOption, boolean isConsolidated) {

        // determine if search results need to be updated by pending ledger entries
        if (Constant.ALL_PENDING_ENTRY.equals(pendingEntryOption)) {
            updateEntryCollection(entryCollection, fieldValues, false, isConsolidated);
        } else if (Constant.APPROVED_PENDING_ENTRY.equals(pendingEntryOption)) {
            updateEntryCollection(entryCollection, fieldValues, true, isConsolidated);
        }
    }

    /**
     * @see org.kuali.kfs.module.ld.service.LaborInquiryOptionsService#updateEntryCollection(java.util.Collection, java.util.Map,
     * boolean, boolean)
     */
    public void updateEntryCollection(Collection entryCollection, Map fieldValues, boolean isApproved, boolean isConsolidated) {
        // go through the pending entries to update the balance collection
        Iterator<LaborLedgerPendingEntry> pendingEntryIterator = laborLedgerPendingEntryService.findPendingLedgerEntriesForLedgerBalance(fieldValues, isApproved);

        while (pendingEntryIterator.hasNext()) {
            LaborLedgerPendingEntry pendingEntry = pendingEntryIterator.next();

            if (!isEmployeeFunding(pendingEntry)) {
                continue;
            }

            // if consolidated, change the following fields into the default values for consolidation
            if (isConsolidated) {
                pendingEntry.setSubAccountNumber(Constant.CONSOLIDATED_SUB_ACCOUNT_NUMBER);
                pendingEntry.setFinancialSubObjectCode(Constant.CONSOLIDATED_SUB_OBJECT_CODE);
                pendingEntry.setFinancialObjectTypeCode(Constant.CONSOLIDATED_OBJECT_TYPE_CODE);
            }

            EmployeeFunding ledgerBalance = (EmployeeFunding) laborLedgerBalanceService.findLedgerBalance(entryCollection, pendingEntry, getKeyList());
            if (ledgerBalance == null) {
                ledgerBalance = new EmployeeFunding();
                ObjectUtil.buildObject(ledgerBalance, pendingEntry);
                entryCollection.add(ledgerBalance);
            } else {
                laborLedgerBalanceService.updateLedgerBalance(ledgerBalance, pendingEntry);
            }
            updateAmount(ledgerBalance, pendingEntry);
        }
    }

    /**
     * update the amount of the given employee funding with the given pending entry
     *
     * @param employeeFunding the given employee funding
     * @param pendingEntry    the given pending entry
     */
    private void updateAmount(EmployeeFunding employeeFunding, LaborLedgerPendingEntry pendingEntry) {
        String balanceTypeCode = pendingEntry.getFinancialBalanceTypeCode();
        String debitCreditCode = pendingEntry.getTransactionDebitCreditCode();
        KualiDecimal amount = DebitCreditUtil.getNumericAmount(pendingEntry.getTransactionLedgerEntryAmount(), pendingEntry.getTransactionDebitCreditCode());

        if (StringUtils.equals(balanceTypeCode, KFSConstants.BALANCE_TYPE_ACTUAL)) {
            employeeFunding.setCurrentAmount(amount.add(employeeFunding.getCurrentAmount()));
        } else if (StringUtils.equals(balanceTypeCode, KFSConstants.BALANCE_TYPE_INTERNAL_ENCUMBRANCE)) {
            employeeFunding.setOutstandingEncumbrance(amount.add(employeeFunding.getOutstandingEncumbrance()));
        }
    }

    /**
     * determine whether the given pending entry is qualified to be processed as an employee funding
     *
     * @param pendingEntry the given pending entry
     * @return true if the given pending entry is qualified to be processed as an employee funding; otherwise, false
     */
    private boolean isEmployeeFunding(LaborLedgerPendingEntry pendingEntry) {
        String balanceTypeCode = pendingEntry.getFinancialBalanceTypeCode();

        if (StringUtils.equals(balanceTypeCode, KFSConstants.BALANCE_TYPE_ACTUAL)) {
            String objectTypeCode = pendingEntry.getFinancialObjectTypeCode();
            String[] objectTypeCodes = {EMPLOYEE_FUNDING_EXPENSE_OBJECT_TYPE_CODE, EMPLOYEE_FUNDING_NORMAL_OP_EXPENSE_OBJECT_TYPE_CODE};

            return ArrayUtils.contains(objectTypeCodes, objectTypeCode) ? true : false;
        }

        if (StringUtils.equals(balanceTypeCode, KFSConstants.BALANCE_TYPE_INTERNAL_ENCUMBRANCE)) {
            return true;
        }

        return false;
    }

    /**
     * construct the primary key list of the business object
     *
     * @return the primary key list of the business object
     */
    private List<String> getKeyList() {
        List<String> keyList = new ArrayList<String>();
        keyList.add(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR);
        keyList.add(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
        keyList.add(KFSPropertyConstants.ACCOUNT_NUMBER);
        keyList.add(KFSPropertyConstants.SUB_ACCOUNT_NUMBER);
        keyList.add(KFSPropertyConstants.FINANCIAL_OBJECT_CODE);
        keyList.add(KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE);
        keyList.add(KFSPropertyConstants.FINANCIAL_OBJECT_TYPE_CODE);
        keyList.add(KFSPropertyConstants.POSITION_NUMBER);
        keyList.add(KFSPropertyConstants.EMPLID);
        return keyList;
    }

    /**
     * Sets the laborLedgerBalanceService attribute value.
     *
     * @param laborLedgerBalanceService The laborLedgerBalanceService to set.
     */
    public void setLaborLedgerBalanceService(LaborLedgerBalanceService laborLedgerBalanceService) {
        this.laborLedgerBalanceService = laborLedgerBalanceService;
    }

    /**
     * Sets the laborInquiryOptionsService attribute value.
     *
     * @param laborInquiryOptionsService The laborInquiryOptionsService to set.
     */
    public void setLaborInquiryOptionsService(LaborInquiryOptionsService laborInquiryOptionsService) {
        this.laborInquiryOptionsService = laborInquiryOptionsService;
    }

    /**
     * Sets the laborLedgerPendingEntryService attribute value.
     *
     * @param laborLedgerPendingEntryService The laborLedgerPendingEntryService to set.
     */
    public void setLaborLedgerPendingEntryService(LaborLedgerPendingEntryService laborLedgerPendingEntryService) {
        this.laborLedgerPendingEntryService = laborLedgerPendingEntryService;
    }
}
