/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

package org.apache.ignite.examples.computegrid.montecarlo;

import java.util.Arrays;
import java.util.Random;

/**
 * This class abstracts out the calculation of risk for a credit portfolio.
 */
@SuppressWarnings({"FloatingPointEquality"})
public class CreditRiskManager {
    /**
     * Default randomizer with normal distribution.
     * Note that since every JVM on the cluster will have its own random
     * generator (independently initialized) the Monte-Carlo simulation
     * will be slightly skewed when performed on the ignite cluster due to skewed
     * normal distribution of the sub-jobs comparing to execution on the
     * local node only with single random generator. Real-life applications
     * may want to provide its own implementation of distributed random
     * generator.
     */
    private static Random rndGen = new Random();

    /**
     * Calculates credit risk for a given credit portfolio. This calculation uses
     * Monte-Carlo Simulation to produce risk value.
     *
     * @param portfolio Credit portfolio.
     * @param horizon Forecast horizon (in days).
     * @param num Number of Monte-Carlo iterations.
     * @param percentile Cutoff level.
     * @return Credit risk value, i.e. the minimal amount that creditor has to
     *      have available to cover possible defaults.
     */
    public double calculateCreditRiskMonteCarlo(Credit[] portfolio, int horizon, int num, double percentile) {
        System.out.println(">>> Calculating credit risk for portfolio [size=" + portfolio.length + ", horizon=" +
            horizon + ", percentile=" + percentile + ", iterations=" + num + "] <<<");

        long start = System.currentTimeMillis();

        double[] losses = calculateLosses(portfolio, horizon, num);

        Arrays.sort(losses);

        double[] lossProbs = new double[losses.length];

        // Count variational numbers.
        // Every next one either has the same value or previous one plus probability of loss.
        for (int i = 0; i < losses.length; i++)
            if (i == 0)
                // First time it's just a probability of first value.
                lossProbs[i] = getLossProbability(losses, 0);
            else if (losses[i] != losses[i - 1])
                // Probability of this loss plus previous one.
                lossProbs[i] = getLossProbability(losses, i) + lossProbs[i - 1];
            else
                // The same loss the same probability.
                lossProbs[i] = lossProbs[i - 1];

        // Count percentile.
        double crdRisk = 0;

        for (int i = 0; i < lossProbs.length; i++)
            if (lossProbs[i] > percentile) {
                crdRisk = losses[i - 1];

                break;
            }

        System.out.println(">>> Finished calculating portfolio risk [risk=" + crdRisk +
            ", time=" + (System.currentTimeMillis() - start) + "ms]");

        return crdRisk;
    }

    /**
     * Calculates losses for the given credit portfolio using Monte-Carlo Simulation.
     * Simulates probability of default only.
     *
     * @param portfolio Credit portfolio.
     * @param horizon Forecast horizon.
     * @param num Number of Monte-Carlo iterations.
     * @return Losses array simulated by Monte Carlo method.
     */
    private double[] calculateLosses(Credit[] portfolio, int horizon, int num) {
        double[] losses = new double[num];

        // Count losses using Monte-Carlo method. We generate random probability of default,
        // if it exceeds certain credit default value we count losses - otherwise count income.
        for (int i = 0; i < num; i++)
            for (Credit crd : portfolio) {
                int remDays = Math.min(crd.getRemainingTerm(), horizon);

                if (rndGen.nextDouble() >= 1 - crd.getDefaultProbability(remDays))
                    // (1 + 'r' * min(H, W) / 365) * S.
                    // Where W is a horizon, H is a remaining crediting term, 'r' is an annual credit rate,
                    // S is a remaining credit amount.
                    losses[i] += (1 + crd.getAnnualRate() * Math.min(horizon, crd.getRemainingTerm()) / 365)
                        * crd.getRemainingAmount();
                else
                    // - 'r' * min(H,W) / 365 * S
                    // Where W is a horizon, H is a remaining crediting term, 'r' is a annual credit rate,
                    // S is a remaining credit amount.
                    losses[i] -= crd.getAnnualRate() * Math.min(horizon, crd.getRemainingTerm()) / 365 *
                        crd.getRemainingAmount();
            }

        return losses;
    }

    /**
     * Calculates probability of certain loss in array of losses.
     *
     * @param losses Array of losses.
     * @param i Index of certain loss in array.
     * @return Probability of loss with given index.
     */
    private double getLossProbability(double[] losses, int i) {
        double cnt = 0;
        double loss = losses[i];

        for (double tmp : losses)
            if (loss == tmp)
                cnt++;

        return cnt / losses.length;
    }
}
