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

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.gl.Constant;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.OJBUtility;
import org.kuali.kfs.gl.batch.service.AccountBalanceCalculator;
import org.kuali.kfs.gl.businessobject.AccountBalance;
import org.kuali.kfs.gl.businessobject.TransientBalanceInquiryAttributes;
import org.kuali.kfs.gl.businessobject.inquiry.AccountBalanceInquirableImpl;
import org.kuali.kfs.gl.service.AccountBalanceService;
import org.kuali.kfs.kns.lookup.HtmlData;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.businessobject.SystemOptions;
import org.kuali.kfs.sys.service.OptionsService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.krad.bo.BusinessObject;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A class to support Account Balance lookups
 */
public class AccountBalanceLookupableHelperServiceImpl extends AbstractGeneralLedgerLookupableHelperServiceImpl {

    private AccountBalanceCalculator postAccountBalance;
    private AccountBalanceService accountBalanceService;
    private OptionsService optionsService;

    /**
     * Returns the url for the account balance inquiry
     *
     * @param bo           the business object with a property that an inquiry drill down url is being asked for
     * @param propertyName the property of that bo that the inquiry drill down url is being asked for
     * @return the URL for the inquiry
     * @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) {
        return (new AccountBalanceInquirableImpl()).getInquiryUrl(bo, propertyName);
    }

    /**
     * Given a map of fieldValues, actually searches for the appropriate account balance records to return
     *
     * @param fieldValues a map of keys for the search
     * @return a List of AccountBalance records that match the search criteria
     * @see org.kuali.rice.kns.lookup.Lookupable#getSearchResults(java.util.Map)
     * <p>
     * KRAD Conversion: Lookupable modifies the search results based on the fields consolidated.
     * But all field definitions are in data dictionary.
     */
    @Override
    public List<? extends BusinessObject> getSearchResults(Map<String, String> fieldValues) {
        setBackLocation((String) fieldValues.get(KFSConstants.BACK_LOCATION));
        setDocFormKey((String) fieldValues.get(KFSConstants.DOC_FORM_KEY));

        Collection searchResultsCollection = null;

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

        // KFSMI-410: added one more node for consolidationOption
        String consolidationOption = (String) fieldValues.get(GeneralLedgerConstants.DummyBusinessObject.CONSOLIDATION_OPTION);
        // test if the consolidation option is selected or not
        boolean isConsolidated = isConsolidationSelected(fieldValues);

        // get the search result collection
        // KFSMI-410: added one more node for consolidationOption
        if (consolidationOption.equals(Constant.EXCLUDE_SUBACCOUNTS)) {
            fieldValues.put(Constant.SUB_ACCOUNT_OPTION, KFSConstants.getDashSubAccountNumber());
            isConsolidated = false;
        }

        if (isConsolidated) {
            Iterator availableBalanceIterator = accountBalanceService.findConsolidatedAvailableAccountBalance(fieldValues);
            searchResultsCollection = buildConsolidedAvailableBalanceCollection(availableBalanceIterator);
        } else {
            Iterator availableBalanceIterator = accountBalanceService.findAvailableAccountBalance(fieldValues);
            searchResultsCollection = buildDetailedAvailableBalanceCollection(availableBalanceIterator);
        }

        // update search results according to the selected pending entry option
        updateByPendingLedgerEntry(searchResultsCollection, fieldValues, pendingEntryOption, isConsolidated, false);

        // Put the search related stuff in the objects
        for (Iterator iter = searchResultsCollection.iterator(); iter.hasNext(); ) {
            AccountBalance ab = (AccountBalance) iter.next();
            TransientBalanceInquiryAttributes dbo = ab.getDummyBusinessObject();
            dbo.setConsolidationOption(consolidationOption);
            dbo.setPendingEntryOption(pendingEntryOption);
        }

        // get the actual size of all qualified search results
        Integer recordCount = accountBalanceService.getAvailableAccountBalanceCount(fieldValues, isConsolidated);
        Long actualSize = OJBUtility.getResultActualSize(searchResultsCollection, recordCount, fieldValues, new AccountBalance());

        return this.buildSearchResultList(searchResultsCollection, actualSize);
    }

    /**
     * This method builds the available account balance collection based on the input iterator
     *
     * @param iterator the iterator of search results of account balance
     * @return the account balance collection
     */
    private Collection buildConsolidedAvailableBalanceCollection(Iterator iterator) {
        Collection balanceCollection = new ArrayList();

        // build available balance collection throught analyzing the input iterator
        while (iterator.hasNext()) {
            Object avaiableAccountBalance = iterator.next();

            if (avaiableAccountBalance.getClass().isArray()) {
                int i = 0;
                Object[] array = (Object[]) avaiableAccountBalance;
                AccountBalance accountBalance = new AccountBalance();

                accountBalance.setUniversityFiscalYear(new Integer(array[i++].toString()));
                accountBalance.setChartOfAccountsCode(array[i++].toString());

                accountBalance.setAccountNumber(array[i++].toString());
                accountBalance.setSubAccountNumber(Constant.CONSOLIDATED_SUB_ACCOUNT_NUMBER);

                accountBalance.setObjectCode(array[i++].toString());
                accountBalance.setSubObjectCode(Constant.CONSOLIDATED_SUB_OBJECT_CODE);

                String objectTypeCode = array[i++].toString();
                accountBalance.getFinancialObject().setFinancialObjectTypeCode(objectTypeCode);

                KualiDecimal budgetAmount = new KualiDecimal(array[i++].toString());
                accountBalance.setCurrentBudgetLineBalanceAmount(budgetAmount);

                KualiDecimal actualsAmount = new KualiDecimal(array[i++].toString());
                accountBalance.setAccountLineActualsBalanceAmount(actualsAmount);

                KualiDecimal encumbranceAmount = new KualiDecimal(array[i].toString());
                accountBalance.setAccountLineEncumbranceBalanceAmount(encumbranceAmount);

                KualiDecimal variance = calculateVariance(accountBalance);
                accountBalance.getDummyBusinessObject().setGenericAmount(variance);

                balanceCollection.add(accountBalance);
            }
        }
        return balanceCollection;
    }

    /**
     * This method builds the available account balance collection based on the input collection
     *
     * @param collection a collection of account balance entries
     * @return the account balance collection
     */
    private Collection buildDetailedAvailableBalanceCollection(Iterator iterator) {
        Collection balanceCollection = new ArrayList();

        // build available balance collection throught analyzing the iterator above
        while (iterator.hasNext()) {
            AccountBalance accountBalance = (AccountBalance) iterator.next();

            if (accountBalance.getDummyBusinessObject() == null) {
                accountBalance.setDummyBusinessObject(new TransientBalanceInquiryAttributes());
            }

            KualiDecimal variance = calculateVariance(accountBalance);
            accountBalance.getDummyBusinessObject().setGenericAmount(variance);

            balanceCollection.add(accountBalance);
        }
        return balanceCollection;
    }

    /**
     * This method calculates the variance of current budget balance, actuals balance and encumbrance balance
     *
     * @param balance an account balance entry
     */
    private KualiDecimal calculateVariance(AccountBalance balance) {

        KualiDecimal variance = new KualiDecimal(0.0);
        KualiDecimal budgetAmount = balance.getCurrentBudgetLineBalanceAmount();
        KualiDecimal actualsAmount = balance.getAccountLineActualsBalanceAmount();
        KualiDecimal encumbranceAmount = balance.getAccountLineEncumbranceBalanceAmount();

        // determine if the object type code is one of the given codes
        if (ObjectUtils.isNull(balance.getFinancialObject()) || StringUtils.isBlank(balance.getFinancialObject().getFinancialObjectTypeCode())) {
            balance.refreshReferenceObject("financialObject"); // refresh if we need to...
        }
        ObjectCode financialObject = balance.getFinancialObject();
        String objectTypeCode = (financialObject == null) ? Constant.EMPTY_STRING : financialObject.getFinancialObjectTypeCode();

        SystemOptions options = getOptionsService().getOptions(balance.getUniversityFiscalYear());
        if (ObjectUtils.isNull(options)) {
            options = getOptionsService().getCurrentYearOptions();
        }
        String[] objectTypeCodeList = new String[3];
        objectTypeCodeList[0] = options.getFinObjTypeExpendNotExpCode();
        objectTypeCodeList[1] = options.getFinObjTypeExpNotExpendCode();
        objectTypeCodeList[2] = options.getFinObjTypeExpenditureexpCd();

        boolean isObjectTypeCodeInList = ArrayUtils.contains(objectTypeCodeList, objectTypeCode);

        // calculate the variance based on the object type code of the balance
        if (isObjectTypeCodeInList) {
            variance = budgetAmount.subtract(actualsAmount);
            variance = variance.subtract(encumbranceAmount);
        } else {
            variance = actualsAmount.subtract(budgetAmount);
        }
        return variance;
    }

    /**
     * Updates the collection of entries that will be applied to the results of the inquiry
     *
     * @param entryCollection     a collection of balance entries
     * @param fieldValues         the map containing the search fields and values
     * @param isApproved          flag whether the approved entries or all entries will be processed
     * @param isConsolidated      flag whether the results are consolidated or not
     * @param isCostShareExcluded flag whether the user selects to see the results with cost share subaccount
     * @see org.kuali.module.gl.web.lookupable.AbstractGLLookupableImpl#updateEntryCollection(java.util.Collection, java.util.Map,
     * boolean, boolean, boolean)
     */
    @Override
    protected void updateEntryCollection(Collection entryCollection, Map fieldValues, boolean isApproved, boolean isConsolidated, boolean isCostShareExcluded) {

        // convert the field names of balance object into corresponding ones of pending entry object
        Map pendingEntryFieldValues = BusinessObjectFieldConverter.convertToTransactionFieldValues(fieldValues);

        // go through the pending entries to update the balance collection
        Iterator pendingEntryIterator = getGeneralLedgerPendingEntryService().findPendingLedgerEntriesForAccountBalance(pendingEntryFieldValues, isApproved);
        while (pendingEntryIterator.hasNext()) {
            GeneralLedgerPendingEntry pendingEntry = (GeneralLedgerPendingEntry) pendingEntryIterator.next();

            if (isCostShareExcluded) {
                if (ObjectUtils.isNotNull(pendingEntry.getSubAccount()) && ObjectUtils.isNotNull(pendingEntry.getSubAccount().getA21SubAccount())) {
                    if (KFSConstants.SubAccountType.COST_SHARE.equals(pendingEntry.getSubAccount().getA21SubAccount().getSubAccountTypeCode())) {
                        // Don't process this one
                        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);
            }

            AccountBalance accountBalance = postAccountBalance.findAccountBalance(entryCollection, pendingEntry);
            postAccountBalance.updateAccountBalance(pendingEntry, accountBalance);

            // recalculate the variance after pending entries are combined into account balances
            if (accountBalance.getDummyBusinessObject() == null) {
                accountBalance.setDummyBusinessObject(new TransientBalanceInquiryAttributes());
            }
            KualiDecimal variance = calculateVariance(accountBalance);
            accountBalance.getDummyBusinessObject().setGenericAmount(variance);
        }
    }

    /**
     * Sets the postAccountBalance attribute value.
     *
     * @param postAccountBalance The postAccountBalance to set.
     */
    public void setPostAccountBalance(AccountBalanceCalculator postAccountBalance) {
        this.postAccountBalance = postAccountBalance;
    }

    /**
     * Sets the accountBalanceService attribute value.
     *
     * @param accountBalanceService The accountBalanceService to set.
     */
    public void setAccountBalanceService(AccountBalanceService accountBalanceService) {
        this.accountBalanceService = accountBalanceService;
    }

    /**
     * Sets the optionsService attribute value
     *
     * @param optionsService The optionsService to set.
     */
    public void setOptionsService(OptionsService optionsService) {
        this.optionsService = optionsService;
    }

    /**
     * Gets the optionsService attribute.
     *
     * @return Returns the optionsService.
     */
    public OptionsService getOptionsService() {
        return optionsService;
    }
}
