/*
 * 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.ec.util;

import org.kuali.kfs.module.ec.EffortConstants;
import org.kuali.kfs.module.ec.EffortPropertyConstants;
import org.kuali.kfs.module.ec.businessobject.EffortCertificationDetail;
import org.kuali.kfs.sys.DynamicCollectionComparator;
import org.kuali.kfs.sys.DynamicCollectionComparator.SortOrder;
import org.kuali.kfs.sys.ObjectUtil;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * grouping a set of detail lines. The class is implemented to manage: summary line and delegating line.
 */
public class DetailLineGroup {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DetailLineGroup.class);

    EffortCertificationDetail summaryDetailLine;
    EffortCertificationDetail delegateDetailLine;
    List<EffortCertificationDetail> detailLines;

    /**
     * Constructs a DetailLineGroup.java.
     */
    public DetailLineGroup() {
        this(null);
    }

    /**
     * Constructs a DetailLineGroup.java.
     *
     * @param detailLine the given detail line
     */
    public DetailLineGroup(EffortCertificationDetail newDetailLine) {
        detailLines = new ArrayList<EffortCertificationDetail>();
        summaryDetailLine = new EffortCertificationDetail();

        if (newDetailLine != null) {
            String groupId = getKeysAsString(newDetailLine);
            ObjectUtil.buildObject(summaryDetailLine, newDetailLine);
            summaryDetailLine.setGroupId(groupId);

            this.addNewLineIntoGroup(newDetailLine, groupId);
        }

        summaryDetailLine.setFinancialObjectCode(null);
        summaryDetailLine.setPositionNumber(null);
    }

    /**
     * update the effort percent of the delegate detail line if the effort on the summary line has been changed
     */
    public void updateDelegateDetailLineEffort() {
        Integer difference = this.getEffortPercentChanged();
        if (difference != 0) {
            Integer effortPercent = delegateDetailLine.getEffortCertificationUpdatedOverallPercent() + difference;
            delegateDetailLine.setEffortCertificationUpdatedOverallPercent(effortPercent);
        }
    }

    /**
     * update the effort percents of the detail lines if the effort on the summary line has been changed
     */
    public void updateDetailLineEffortPercent() {
        int totalDifference = this.getEffortPercentChanged();

        List<EffortCertificationDetail> detailLines = this.getDetailLines();
        DynamicCollectionComparator.sort(detailLines, SortOrder.DESC, EffortPropertyConstants.PERSISED_PAYROLL_AMOUNT);

        // restore the intial effort percents before update the detail lines
        for (EffortCertificationDetail detailLine : detailLines) {
            detailLine.setEffortCertificationUpdatedOverallPercent(detailLine.getPersistedEffortPercent());
        }

        for (EffortCertificationDetail detailLine : detailLines) {
            if (totalDifference == 0) {
                break;
            }

            int currentPercent = detailLine.getPersistedEffortPercent();
            int currentDifference = currentPercent + totalDifference;
            boolean needUpdateMultipleLines = (currentDifference < 0);

            int effortPercent = needUpdateMultipleLines ? 0 : currentDifference;
            detailLine.setEffortCertificationUpdatedOverallPercent(effortPercent);

            totalDifference = needUpdateMultipleLines ? currentDifference : 0;
        }
    }

    /**
     * update the payroll amounts of the detail lines if the payroll amount on the summary line has been changed
     */
    public void updateDetailLinePayrollAmount() {
        KualiDecimal totalDifference = this.getPayrollAmountChanged();
        if (totalDifference.isZero()) {
            return;
        }

        List<EffortCertificationDetail> detailLines = this.getDetailLines();
        DynamicCollectionComparator.sort(detailLines, SortOrder.DESC, EffortPropertyConstants.PERSISED_PAYROLL_AMOUNT);

        // restore the intial payroll amounts before update the detail lines
        for (EffortCertificationDetail detailLine : detailLines) {
            detailLine.setEffortCertificationPayrollAmount(detailLine.getPersistedPayrollAmount());
        }

        for (EffortCertificationDetail detailLine : detailLines) {
            if (totalDifference.isZero()) {
                break;
            }

            KualiDecimal currentAmount = detailLine.getPersistedPayrollAmount();
            KualiDecimal currentDifference = currentAmount.add(totalDifference);
            boolean needUpdateMultipleLines = currentDifference.isNegative();

            KualiDecimal payrollAmount = needUpdateMultipleLines ? KualiDecimal.ZERO : currentDifference;
            detailLine.setEffortCertificationPayrollAmount(payrollAmount);

            totalDifference = needUpdateMultipleLines ? currentDifference : KualiDecimal.ZERO;
        }
    }

    /**
     * group the given detail lines by the key fields
     *
     * @param detailLines the given detail lines
     * @param keyFields   the given key fields
     * @return the groups of detail lines
     */
    public static Map<String, DetailLineGroup> groupDetailLines(List<EffortCertificationDetail> detailLines) {
        Map<String, DetailLineGroup> detailLineGroupMap = new HashMap<String, DetailLineGroup>();

        for (EffortCertificationDetail line : detailLines) {
            String groupId = getKeysAsString(line);

            if (detailLineGroupMap.containsKey(groupId)) {
                DetailLineGroup group = detailLineGroupMap.get(groupId);
                group.addNewLineIntoGroup(line, groupId);
            } else {
                DetailLineGroup group = new DetailLineGroup(line);
                detailLineGroupMap.put(groupId, group);
            }
        }

        return detailLineGroupMap;
    }

    /**
     * concat the keys of the given detail line as a single string
     *
     * @param line the given detail line
     * @return a single string built from the keys of the given detail line
     */
    public static String getKeysAsString(EffortCertificationDetail line) {
        return ObjectUtil.concatPropertyAsString(line, EffortConstants.DETAIL_LINES_GROUPING_FILEDS);
    }

    /**
     * get the difference between the updated effort amount and the current effort amount
     *
     * @return the difference between the updated effort amount and the current effort amount
     */
    private Integer getEffortPercentChanged() {
        Integer currentEffortPercent = EffortCertificationDetail.getTotalPersistedEffortPercent(detailLines);
        Integer updatedEffortPercent = summaryDetailLine.getEffortCertificationUpdatedOverallPercent();

        return updatedEffortPercent - currentEffortPercent;
    }

    /**
     * get the difference between the updated payroll amount and the current payroll amount
     *
     * @return the difference between the updated payroll amount and the current payroll amount
     */
    private KualiDecimal getPayrollAmountChanged() {
        KualiDecimal currentAmount = EffortCertificationDetail.getTotalPersistedPayrollAmount(detailLines);
        KualiDecimal updatedAmount = summaryDetailLine.getEffortCertificationPayrollAmount();

        return updatedAmount.subtract(currentAmount);
    }

    /**
     * update the group when a new detail line is added
     *
     * @param line the new detail line
     */
    private void addNewLineIntoGroup(EffortCertificationDetail newDetailLine, String groupId) {
        if (detailLines.contains(newDetailLine)) {
            return;
        }

        newDetailLine.setGroupId(groupId);

        detailLines.add(newDetailLine);
        delegateDetailLine = this.getDetailLineWithMaxPayrollAmount(detailLines);

        this.updateSummaryDetailLineAmount();
    }

    /**
     * update the payroll amounts and effort percents based on current detail lines
     */
    private void updateSummaryDetailLineAmount() {
        Integer originalEffortPercent = EffortCertificationDetail.getTotalOriginalEffortPercent(detailLines);
        summaryDetailLine.setEffortCertificationCalculatedOverallPercent(originalEffortPercent);

        Integer effortPercent = EffortCertificationDetail.getTotalEffortPercent(detailLines);
        summaryDetailLine.setEffortCertificationUpdatedOverallPercent(effortPercent);

        Integer persistedEffortPercent = EffortCertificationDetail.getTotalPersistedEffortPercent(detailLines);
        summaryDetailLine.setPersistedEffortPercent(persistedEffortPercent);

        KualiDecimal originalPayrollAmount = EffortCertificationDetail.getTotalOriginalPayrollAmount(detailLines);
        summaryDetailLine.setEffortCertificationOriginalPayrollAmount(originalPayrollAmount);

        KualiDecimal payrollAmount = EffortCertificationDetail.getTotalPayrollAmount(detailLines);
        summaryDetailLine.setEffortCertificationPayrollAmount(payrollAmount);

        KualiDecimal persistedPayrollAmount = EffortCertificationDetail.getTotalPersistedPayrollAmount(detailLines);
        summaryDetailLine.setPersistedPayrollAmount(persistedPayrollAmount);
    }

    /**
     * find the detail lines that have max payroll amount
     *
     * @return the detail lines that have max payroll amount
     */
    private EffortCertificationDetail getDetailLineWithMaxPayrollAmount(List<EffortCertificationDetail> detailLines) {
        KualiDecimal maxAmount = null;
        EffortCertificationDetail detailLineWithMaxPayrollAmount = null;

        for (EffortCertificationDetail line : detailLines) {
            KualiDecimal currentAmount = line.getEffortCertificationOriginalPayrollAmount();

            if (detailLineWithMaxPayrollAmount == null) {
                maxAmount = currentAmount;
                detailLineWithMaxPayrollAmount = line;
                continue;
            }

            if (maxAmount.isLessThan(currentAmount)) {
                maxAmount = currentAmount;
                detailLineWithMaxPayrollAmount = line;
            }
        }

        return detailLineWithMaxPayrollAmount;
    }

    /**
     * Gets the summaryDetailLine attribute.
     *
     * @return Returns the summaryDetailLine.
     */
    public EffortCertificationDetail getSummaryDetailLine() {
        return summaryDetailLine;
    }

    /**
     * Sets the summaryDetailLine attribute value.
     *
     * @param summaryDetailLine The summaryDetailLine to set.
     */
    public void setSummaryDetailLine(EffortCertificationDetail summaryDetailLine) {
        this.summaryDetailLine = summaryDetailLine;
    }

    /**
     * Gets the detailLines attribute.
     *
     * @return Returns the detailLines.
     */
    public List<EffortCertificationDetail> getDetailLines() {
        return detailLines;
    }

    /**
     * Sets the detailLines attribute value.
     *
     * @param detailLines The detailLines to set.
     */
    public void setDetailLines(List<EffortCertificationDetail> detailLines) {
        this.detailLines = detailLines;
    }

    /**
     * Gets the delegateDetailLine attribute.
     *
     * @return Returns the delegateDetailLine.
     */
    public EffortCertificationDetail getDelegateDetailLine() {
        return delegateDetailLine;
    }

    /**
     * Sets the delegateDetailLine attribute value.
     *
     * @param delegateDetailLine The delegateDetailLine to set.
     */
    public void setDelegateDetailLine(EffortCertificationDetail delegateDetailLine) {
        this.delegateDetailLine = delegateDetailLine;
    }
}
