/*
 * 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.beanutils.BeanUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.service.ObjectCodeService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.krad.service.BusinessObjectService;
import org.kuali.kfs.module.tem.TemConstants.ExpenseType;
import org.kuali.kfs.module.tem.businessobject.AccountingDistribution;
import org.kuali.kfs.module.tem.businessobject.TemDistributionAccountingLine;
import org.kuali.kfs.module.tem.businessobject.TemSourceAccountingLine;
import org.kuali.kfs.module.tem.businessobject.TravelerDetail;
import org.kuali.kfs.module.tem.businessobject.TripType;
import org.kuali.kfs.module.tem.document.TravelDocument;
import org.kuali.kfs.module.tem.document.TravelReimbursementDocument;
import org.kuali.kfs.module.tem.document.service.TravelDocumentService;
import org.kuali.kfs.module.tem.document.web.bean.AccountingLineDistributionKey;
import org.kuali.kfs.module.tem.service.AccountingDistributionService;
import org.kuali.kfs.module.tem.service.TravelExpenseService;
import org.kuali.kfs.module.tem.util.AccountingDistributionComparator;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Accounting Distribution Service Implementation
 */

/**
 * This class...
 */
public class AccountingDistributionServiceImpl implements AccountingDistributionService {

    protected static Logger LOG = Logger.getLogger(AccountingDistributionServiceImpl.class);

    protected BusinessObjectService businessObjectService;
    protected ObjectCodeService objectCodeService;
    protected TravelDocumentService travelDocumentService;
    protected ParameterService parameterService;

    @SuppressWarnings("deprecation")
    @Override
    public List<TemSourceAccountingLine> distributionToSouceAccountingLines(List<TemDistributionAccountingLine> distributionAccountingLines, List<AccountingDistribution> accountingDistributionList, KualiDecimal sourceAccountingLinesTotal, KualiDecimal expenseLimit) {
        List<TemSourceAccountingLine> sourceAccountingList = new ArrayList<TemSourceAccountingLine>();
        Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();
        KualiDecimal total = KualiDecimal.ZERO;
        int distributionTargetCount = 0;
        boolean useExpenseLimit = false;
        for (AccountingDistribution accountDistribution : accountingDistributionList) {
            if (accountDistribution.getSelected()) {
                total = total.add(accountDistribution.getRemainingAmount());
                distributionTargetCount += 1;
            }
        }

        if (expenseLimit != null && expenseLimit.isPositive()) {
            KualiDecimal expenseLimitTotal = new KualiDecimal(expenseLimit.bigDecimalValue());
            // do we have any accounting line amount to subtract from the expense limit?
            if (sourceAccountingLinesTotal != null && sourceAccountingLinesTotal.isGreaterThan(KualiDecimal.ZERO)) {
                expenseLimitTotal = expenseLimitTotal.subtract(sourceAccountingLinesTotal);
            }
            if (expenseLimitTotal.isLessThan(total)) {
                total = expenseLimitTotal;
                useExpenseLimit = true;
            }
        }

        if (total.isGreaterThan(KualiDecimal.ZERO)) {
            for (AccountingDistribution accountingDistribution : accountingDistributionList) {
                List<TemSourceAccountingLine> tempSourceAccountingList = new ArrayList<TemSourceAccountingLine>();
                if (accountingDistribution.getSelected()) {
                    for (TemDistributionAccountingLine accountingLine : distributionAccountingLines) {
                        TemSourceAccountingLine newLine = new TemSourceAccountingLine();
                        try {
                            BeanUtils.copyProperties(newLine, accountingLine);
                        } catch (IllegalAccessException ex) {
                            ex.printStackTrace();
                        } catch (InvocationTargetException ex) {
                            ex.printStackTrace();
                        }
                        BigDecimal distributionAmount = (distributionTargetCount > 1) ? accountingDistribution.getRemainingAmount().bigDecimalValue() : total.bigDecimalValue();
                        BigDecimal product = accountingLine.getAccountLinePercent().multiply(distributionAmount);
                        product = product.divide(new BigDecimal(100), 5, RoundingMode.HALF_UP);
                        BigDecimal lineAmount = product.divide(total.bigDecimalValue(), 5, RoundingMode.HALF_UP);

                        newLine.setAmount(new KualiDecimal(product));
                        newLine.setCardType(accountingDistribution.getCardType());
                        Map<String, Object> fieldValues = new HashMap<String, Object>();
                        fieldValues.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, accountingDistribution.getObjectCode());
                        fieldValues.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, newLine.getChartOfAccountsCode());
                        fieldValues.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear());
                        ObjectCode objCode = getBusinessObjectService().findByPrimaryKey(ObjectCode.class, fieldValues);
                        newLine.setObjectCode(objCode);
                        newLine.setFinancialObjectCode(accountingDistribution.getObjectCode());
                        tempSourceAccountingList.add(newLine);
                    }
                    if (useExpenseLimit) {
                        sourceAccountingList.addAll(tempSourceAccountingList); //we just adjusted the accounting lines for the expense...let's not readjust
                    } else {
                        sourceAccountingList.addAll(adjustValues(tempSourceAccountingList, accountingDistribution.getRemainingAmount()));
                    }
                }
            }
        }
        return sourceAccountingList;
    }

    /**
     * This method will adjust the last accounting line to make the amounts sum up to the total.
     *
     * @param sourceAccountingList
     * @param total
     * @return
     */
    protected List<TemSourceAccountingLine> adjustValues(List<TemSourceAccountingLine> sourceAccountingList, KualiDecimal total) {
        KualiDecimal totalAmount = KualiDecimal.ZERO;
        for (TemSourceAccountingLine newLine : sourceAccountingList) {
            totalAmount = totalAmount.add(newLine.getAmount());
        }
        TemSourceAccountingLine line = sourceAccountingList.get(sourceAccountingList.size() - 1);
        KualiDecimal remainderAmount = total.subtract(totalAmount);
        if (remainderAmount.isPositive()) {
            line.setAmount(line.getAmount().subtract(remainderAmount));
        } else if (remainderAmount.isNegative()) {
            line.setAmount(line.getAmount().add(remainderAmount));
        }

        return sourceAccountingList;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.AccountingDistributionService#distributionToDistributionAccountingLine(java.util.List)
     */
    @Override
    public TemDistributionAccountingLine distributionToDistributionAccountingLine(List<AccountingDistribution> accountingDistributionList) {
        KualiDecimal distributionTotal = KualiDecimal.ZERO;
        for (AccountingDistribution accountingDistribution : accountingDistributionList) {
            if (accountingDistribution.getSelected()) {
                distributionTotal = distributionTotal.add(accountingDistribution.getRemainingAmount());
            }
        }
        TemDistributionAccountingLine newLine = new TemDistributionAccountingLine();
        newLine.setAmount(distributionTotal);
        return newLine;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.AccountingDistributionService#createDistributions(org.kuali.kfs.module.tem.document.TravelDocument)
     */
    @Override
    public List<AccountingDistribution> createDistributions(TravelDocument travelDocument) {
        List<AccountingDistribution> documentDistribution = new ArrayList<AccountingDistribution>();
        Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();

        for (ExpenseType expense : EnumSet.allOf(ExpenseType.class)) {
            Map<String, AccountingDistribution> newDistributionMap = getTravelExpenseService().getExpenseServiceByType(expense).getAccountingDistribution(travelDocument);
            addMergeDistributionMap(distributionMap, newDistributionMap);
        }
        subtractMergeDistributionMap(distributionMap, accountingLinesToDistributionMap(travelDocument));

        for (String distribution : distributionMap.keySet()) {
            if (!distributionMap.get(distribution).getSubTotal().equals(KualiDecimal.ZERO)) {  // don't include distributions of 0.00
                documentDistribution.add(distributionMap.get(distribution));
            }
        }
        Collections.sort(documentDistribution, new AccountingDistributionComparator());

        return documentDistribution;
    }

    protected Map<String, AccountingDistribution> accountingLinesToDistributionMap(TravelDocument travelDocument) {
        Map<String, AccountingDistribution> distributionMap = new HashMap<String, AccountingDistribution>();
        for (TemSourceAccountingLine accountingLine : (List<TemSourceAccountingLine>) travelDocument.getSourceAccountingLines()) {
            AccountingDistribution distribution = null;
            String key = accountingLine.getObjectCode().getCode() + "-" + accountingLine.getCardType();
            if (distributionMap.containsKey(key)) {
                distributionMap.get(key).setSubTotal(distributionMap.get(key).getSubTotal().add(accountingLine.getAmount()));
            } else {
                distribution = new AccountingDistribution();
                distribution.setObjectCode(accountingLine.getObjectCode().getCode());
                distribution.setSubTotal(accountingLine.getAmount());
                distributionMap.put(key, distribution);
            }
        }

        return distributionMap;
    }

    protected void addMergeDistributionMap(Map<String, AccountingDistribution> destinationMap, Map<String, AccountingDistribution> originMap) {
        for (String key : originMap.keySet()) {
            if (destinationMap.containsKey(key)) {
                destinationMap.get(key).setSubTotal(destinationMap.get(key).getSubTotal().add(originMap.get(key).getSubTotal()));
                destinationMap.get(key).setRemainingAmount(destinationMap.get(key).getRemainingAmount().add(originMap.get(key).getRemainingAmount()));
            } else {
                destinationMap.put(key, originMap.get(key));
            }
        }
    }

    protected void subtractMergeDistributionMap(Map<String, AccountingDistribution> destinationMap, Map<String, AccountingDistribution> originMap) {
        Iterator<String> it = originMap.keySet().iterator();
        while (it.hasNext()) {
            String key = it.next();
            if (destinationMap.containsKey(key)) {
                destinationMap.get(key).setRemainingAmount(destinationMap.get(key).getRemainingAmount().subtract(originMap.get(key).getSubTotal()));
            }
        }
    }

    public String getObjectCodeFrom(final TravelDocument travelDocument, String paramName) {
        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);
    }

    protected AccountingDistribution retrieveDistributionFor(final List<AccountingDistribution> distros, ObjectCode objectCode) {
        if (objectCode != null) {
            AccountingDistribution retval = retrieveDistributionFor(distros, objectCode.getCode());
            retval.setObjectCode(objectCode.getCode());
            retval.setObjectCodeName(objectCode.getName());

            return retval;
        }

        return new AccountingDistribution();
    }

    protected AccountingDistribution retrieveDistributionFor(final List<AccountingDistribution> distros, final String code) {
        if (distros != null && code != null) {
            for (final AccountingDistribution distribution : distros) {
                LOG.debug("comparing " + code + " to " + distribution.getObjectCode());
                if (distribution.getObjectCode().equals(code)) {
                    return distribution;
                }
            }
        }

        return new AccountingDistribution();
    }

    /**
     * @see org.kuali.kfs.module.tem.document.service.AccountingDistributionService#buildDistribution(TravelDocument)
     */
    @Override
    public List<AccountingDistribution> buildDistributionFrom(final TravelDocument travelDocument) {
        List<AccountingDistribution> distributions = createDistributions(travelDocument);
        return distributions;
    }

    @Override
    public KualiDecimal getTotalAmount(List<TemDistributionAccountingLine> lines) {
        KualiDecimal total = KualiDecimal.ZERO;

        for (TemDistributionAccountingLine line : lines) {
            total = total.add(line.getAmount());
        }
        return total;
    }

    @Override
    public BigDecimal getTotalPercent(List<TemDistributionAccountingLine> lines) {
        BigDecimal total = new BigDecimal(0);

        for (TemDistributionAccountingLine line : lines) {
            total = total.add(line.getAccountLinePercent());
        }
        return total;
    }

    /**
     * @see org.kuali.kfs.module.tem.service.AccountingDistributionService#calculateAccountingLineDistributionPercent(java.util.List)
     */
    @Override
    public Map<AccountingLineDistributionKey, KualiDecimal> calculateAccountingLineDistributionPercent(List<SourceAccountingLine> accountingLine) {
        Map<AccountingLineDistributionKey, KualiDecimal> distributionMap = new HashMap<AccountingLineDistributionKey, KualiDecimal>();

        //calculate the total from the accounting line
        KualiDecimal total = KualiDecimal.ZERO;
        for (SourceAccountingLine sourceLine : accountingLine) {
            total = total.add(sourceLine.getAmount());
        }

        //calculate the distribution on each of the accounting line distribution keys
        AccountingLineDistributionKey key;
        KualiDecimal factor;
        for (SourceAccountingLine sourceLine : accountingLine) {
            key = new AccountingLineDistributionKey(sourceLine);

            factor = sourceLine.getAmount().divide(total);
            if (distributionMap.containsKey(key)) {
                //accumulate the stored percentage to the calculated; though this is probably very unlikely
                factor = distributionMap.get(key).add(factor);
            }
            //store the final distribution to the map
            distributionMap.put(key, factor);
        }

        return distributionMap;
    }

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

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

    public void setTravelDocumentService(final TravelDocumentService travelDocumentService) {
        this.travelDocumentService = travelDocumentService;
    }

    protected TravelDocumentService getTravelDocumentService() {
        return travelDocumentService;
    }

    /**
     * Gets the objectCodeService attribute.
     *
     * @return Returns the objectCodeService.
     */
    public ObjectCodeService getObjectCodeService() {
        return objectCodeService;
    }

    /**
     * Sets the objectCodeService attribute value.
     *
     * @param objectCodeService The objectCodeService to set.
     */
    public void setObjectCodeService(ObjectCodeService objectCodeService) {
        this.objectCodeService = objectCodeService;
    }

    public BusinessObjectService getBusinessObjectService() {
        return businessObjectService;
    }

    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
        this.businessObjectService = businessObjectService;
    }

    public TravelExpenseService getTravelExpenseService() {
        return SpringContext.getBean(TravelExpenseService.class);
    }

}
