/*
 * Copyright (c) 2012, 2018, Werner Keil, Anatole Tresch and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 * Contributors: @atsticks, @keilw
 */
package org.javamoney.calc.common;

import org.javamoney.calc.CalculationContext;

import java.math.BigDecimal;
import java.util.Objects;

import javax.money.MonetaryAmount;
import javax.money.MonetaryException;
import javax.money.MonetaryOperator;

/**
 * The formula for the future value of a growing annuity is used to calculate the future amount of a
 * series of cash flows, or payments, that grow at a proportionate rate. A growing annuity may
 * sometimes be referred to as an increasing annuity.
 *
 * <b>Example of FV of Growing Annuity</b>
 *
 * An example of the future value of a growing annuity formula would be an individual who is paid
 * biweekly and decides to save one of her extra paychecks per year. One of her net paychecks
 * amounts to $2,000 for the first year and she expects to receive a 5% raise on her net pay every
 * year. For this example, we will use 5% on her net pay and not involve taxes and other adjustments
 * in order to hold all other things constant. In an account that has a yield of 3% per year, she
 * would like to calculate her savings balance after 5 years.
 *
 * The growth rate in this example would be the 5% increase per year, the initial cash flow or
 * payment would be $2,000, the number of periods would be 5 years, and rate per period would be 3%.
 * Using these variables in the future value of growing annuity formula would show
 *
 * <i>Example</i>
 *
 * After solving this equation, the amount after the 5th cash flow would be $11,700.75.
 *
 * @author Anatole Tresch
 * @see <a href="http://www.financeformulas.net/Future-Value-of-Growing-Annuity.html">http://www.financeformulas.net/Future-Value-of-Growing-Annuity.html</a>
 */
public final class FutureValueGrowingAnnuity implements MonetaryOperator {
    /**
     * The discount rate, not null.
     */
    private final Rate discountRate;
    /**
     * The growth rate, not null.
     */
    private final Rate growthRate;
    /**
     * the target periods, &gt;= 0.
     */
    private final int periods;

    /**
     * Constructor.
     *
     * @param discountRate The discount rate, not null.
     * @param growthRate   The growth rate, not null.
     * @param periods      the target periods, &gt;= 0.
     */
    private FutureValueGrowingAnnuity(Rate discountRate, Rate growthRate, int periods) {
        this.discountRate = Objects.requireNonNull(discountRate);
        this.growthRate = Objects.requireNonNull(growthRate);
        if(discountRate.equals(growthRate)){
            throw new MonetaryException("Discount rate and growth rate cannot be the same.");
        }
        if (periods < 0) {
            throw new MonetaryException("Periods < 0");
        }
        this.periods = periods;
    }

    /**
     * Gets discount rate.
     *
     * @return the discount rate
     */
    public Rate getDiscountRate() {
        return discountRate;
    }

    /**
     * Gets growth rate.
     *
     * @return the growth rate
     */
    public Rate getGrowthRate() {
        return growthRate;
    }

    /**
     * Gets periods.
     *
     * @return the periods
     */
    public int getPeriods() {
        return periods;
    }

    /**
     * Access a MonetaryOperator for calculation.
     *
     * @param discountRate The discount rate, not null.
     * @param growthRate   The growth rate, not null.
     * @param periods      the target periods, &gt;= 0.
     * @return the operator, never null.
     */
    public static FutureValueGrowingAnnuity of(Rate discountRate, Rate growthRate, int periods) {
        return new FutureValueGrowingAnnuity(discountRate, growthRate, periods);
    }

    /**
     * Performs the calculation.
     *
     * @param firstPayment the first payment
     * @param discountRate The rate perperiod, not null. If the rate is less than 0, the result will be zero.
     * @param growthRate   The growth rate, not null.
     * @param periods      the target periods, &gt;= 0.
     * @return the resulting amount, never null.
     */
    public static MonetaryAmount calculate(MonetaryAmount firstPayment, Rate discountRate, Rate growthRate,
                                           int periods) {
        if(discountRate.equals(growthRate)){
            throw new MonetaryException("Discount rate and growth rate cannot be the same.");
        }
        if(discountRate.get().signum()<0){
            return firstPayment.getFactory().setNumber(0.0d).create();
        }
        BigDecimal num = CalculationContext.one().add(discountRate.get()).pow(periods)
                .subtract(CalculationContext.one().add(growthRate.get()).pow(periods));
        BigDecimal denum = discountRate.get().subtract(growthRate.get());
        return firstPayment.multiply(num.divide(denum, CalculationContext.mathContext()));
    }

    @Override
    public MonetaryAmount apply(MonetaryAmount firstPayment) {
        return calculate(firstPayment, discountRate, growthRate, periods);
    }

    @Override
    public String toString() {
        return "FutureValueGrowingAnnuity{" +
                "discountRate=" + discountRate +
                ", growthRate=" + growthRate +
                ", periods=" + periods +
                '}';
    }
}
