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

import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.math.BigDecimal;

/**
 * Utility class that provides methods to allocate money into a number of targets according to certain policies.
 * Note: It's assumed that the currency we use here is US dollar.
 */
public class KualiDecimalUtils {

    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiDecimalUtils.class);

    /**
     * Allocates evenly a sum of money amongst a number of targets.
     * Note:
     * 1. This method assumes that the divisor is a positive integer. Validation of the the passed in parameters shall be the caller's responsibility.
     * 2. If the sum can't be evenly divided due to limited accuracy, the last few elements will be adjusted (each by 1c) to reflect the round-up difference.
     *
     * @param amount  the total amount to allocate
     * @param divisor the number of targets to allocate to
     * @return an array of allocated amounts
     */
    public static KualiDecimal[] allocateByQuantity(KualiDecimal amount, int divisor) {

        if (amount == null || divisor == 0) {
            return amount == null ? null : new KualiDecimal[]{amount};
        }

        // calculate evenly divided amount
        KualiDecimal dividedAmount = amount.divide(new KualiDecimal(divisor));

        // set the allocated amount into array
        KualiDecimal[] amountsArray = new KualiDecimal[divisor];
        for (int i = 0; i < divisor; i++) {
            amountsArray[i] = dividedAmount;
        }

        // compute the difference between the original total amount and the allocated total amount, due to round up error in division
        KualiDecimal allocatedAmount = dividedAmount.multiply(new KualiDecimal(divisor));
        KualiDecimal reminderAmount = amount.subtract(allocatedAmount);

        /* Note:
         * We choose to distribute 1c to each of the last N elements, instead of putting the total difference into the last one element, because:
         * 1. This way the allocation is more even; otherwise the last element may end up taking too much or too little (even negative).
         * For ex, suppose amount = $99, and divisor = 10000. So the first 9999 elements would each get 1c, while the last one would get -99c,
         * if we dump all of the roundup error into the last element.
         * 2. The round up error for each element is less than 1c (in fact, it's less than 0.5c, as KualiDecimal uses ROUND_HALF_UP),
         * thus the total error is less than 1c * divisor, which means we can safely distribute 1c to the last N elements, where N < divisor.
         */

        // since KualiDecimal has 2 digits after decimal point, multiply it by 100 guarantees that we get an integer number of cents
        int n = reminderAmount.abs().multiply(new KualiDecimal(100)).intValue();
        KualiDecimal cent = reminderAmount.isPositive() ? new KualiDecimal(0.01) : new KualiDecimal(-0.01);

        // compensate the difference by offsetting the last N elements, each by 1c; here N = reminderAmount * 100
        for (int i = divisor - 1; i >= divisor - n; i--) {
            amountsArray[i] = dividedAmount.add(cent);
        }

        return amountsArray;
    }

    /**
     * Allocates by ratio a sum of money amongst a number of targets.
     * Note:
     * 1. This method assumes that 0 < ratio < 1 and all ratios sum up to 1. Validation of the the passed in parameters shall be the caller's responsibility.
     * 2. If the allocated amount doesn't sum up to exactly the total amount due to limited accuracy, the last few elements will be adjusted (each by 1c) to reflect the round-up difference.
     *
     * @param amount the total amount to allocate
     * @param ratios an array of ratios according to which the total amount is to be allocated
     * @return an array of allocated amounts
     */
    public static KualiDecimal[] allocateByRatio(KualiDecimal amount, double[] ratios) {

        if (ratios == null || ratios.length == 0 || amount == null) {
            return amount == null ? null : new KualiDecimal[]{amount};
        }

        // allocate amounts into array according to the ratios
        KualiDecimal[] amountsArray = new KualiDecimal[ratios.length];
        KualiDecimal allocatedAmount = KualiDecimal.ZERO;
        for (int i = 0; i < ratios.length; i++) {
            KualiDecimal currAmount = new KualiDecimal(amount.bigDecimalValue().multiply(new BigDecimal(ratios[i])));
            amountsArray[i] = currAmount;
            allocatedAmount = allocatedAmount.add(currAmount);
        }

        // compute the difference between the original total amount and the allocated total amount, due to round up error in division
        KualiDecimal reminderAmount = amount.subtract(allocatedAmount);

        /* Note:
         * We choose to distribute 1c to each of the last N elements, instead of putting the total difference into the last one element, because:
         * 1. This way the allocation is more even; otherwise the last element may end up taking too much or too little (even negative).
         * For ex, suppose amount = $99, and ratio[0-9999] = 0.0001. So the first 9999 elements would each get 1c, while the last one would get -99c,
         * if we dump all of the roundup error into the last element.
         * 2. The round up error for each element is less than 1c (in fact, it's less than 0.5c, as KualiDecimal uses ROUND_HALF_UP),
         * thus the total error is less than 1c * ratio.length, which means we can safely distribute 1c to the last N elements, where N < ratio.length.
         */

        // since KualiDecimal has 2 digits after decimal point, multiply it by 100 guarantees that we get an integer number of cents
        int n = reminderAmount.abs().multiply(new KualiDecimal(100)).intValue();
        KualiDecimal cent = reminderAmount.isPositive() ? new KualiDecimal(0.01) : new KualiDecimal(-0.01);

        // If the ratios sum up to 1, then we should have N < ratios.length;
        // However, if that's not the case, it's possible that N > ratio.length; in this case,
        // the best we can do is to chop N to length to avoid ArrayIndexOutOfBoundary exception
        n = n > ratios.length ? ratios.length : n;

        // compensate the difference by offsetting the last N elements, each by 1c; here N = reminderAmount * 100
        for (int i = ratios.length - 1; i >= ratios.length - n; i--) {
            amountsArray[i] = amountsArray[i].add(cent);
        }

        return amountsArray;
    }

    /**
     * Makes sure no null pointer exception occurs on fields that can accurately be null when multiplying. If either field are null
     * the value is returned.
     *
     * @param value
     * @param multiplier
     * @return
     */
    public static KualiDecimal safeMultiply(KualiDecimal value, double multiplier) {
        if (value == null) {
            return value;
        } else {
            return new KualiDecimal(value.bigDecimalValue().multiply(new BigDecimal(multiplier)));
        }
    }

    /**
     * Makes sure no null pointer exception occurs on fields that can accurately be null when subtracting. If either field are null
     * the value is returned.
     *
     * @param value
     * @param subtrahend
     * @return
     */
    public static KualiDecimal safeSubtract(KualiDecimal value, KualiDecimal subtrahend) {
        if (subtrahend == null || value == null) {
            return value;
        }

        return value.subtract(subtrahend);
    }
}
