/FileExtraction/Java_unseen/crisis-economics_CRISIS/CRISIS/src/eu/crisis_economics/abm/markets/clearing/IncrementalLoanDistributionAlgorithm.java
/*
 * This file is part of CRISIS, an economics simulator.
 * 
 * Copyright (C) 2015 John Kieran Phillips
 *
 * CRISIS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * CRISIS 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with CRISIS.  If not, see <http://www.gnu.org/licenses/>.
 */
package eu.crisis_economics.abm.markets.clearing;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import java.util.Map.Entry;

import org.apache.commons.collections15.keyvalue.MultiKey;

import eu.crisis_economics.abm.bank.Bank;
import eu.crisis_economics.abm.contracts.Contract;
import eu.crisis_economics.abm.contracts.InsufficientFundsException;
import eu.crisis_economics.abm.contracts.loans.Borrower;
import eu.crisis_economics.abm.contracts.loans.FixedRateMortgage;
import eu.crisis_economics.abm.contracts.loans.Lender;
import eu.crisis_economics.abm.contracts.loans.LenderInsufficientFundsException;
import eu.crisis_economics.abm.contracts.loans.Loan;
import eu.crisis_economics.abm.contracts.loans.LoanFactory;
import eu.crisis_economics.abm.markets.clearing.heterogeneous.ResourceDistributionAlgorithm;
import eu.crisis_economics.utilities.Pair;

/**
  * @author bochmann 
  * @author phillips (legacy compat.)
  */
public final class IncrementalLoanDistributionAlgorithm
   implements ResourceDistributionAlgorithm<Borrower, Lender> {
   
   public IncrementalLoanDistributionAlgorithm() { }   // Stateless
   
   /** 
     * Establish a new {@link Loan} contract. At present, commercial loans
     * are implemented as {@link FixedRateMortgage} contracts.
     */
   private static Loan setupLoanContract(
      Borrower borrower,
      Lender lender,
      final double loanSize,
      final double interestRate
      ) {
      try {
         return LoanFactory.createFixedRateMortgage(
            borrower, lender, loanSize, interestRate, 1);
      } catch (LenderInsufficientFundsException fundingException) {
         fundingException.printStackTrace();
      }
      return null;
   }
   
   /** 
     * Extend a the principal of an existing {@link Loan} {@link Contract}.
     * The argument to this method should be strictly positive, or else
     * no action is taken.
     */
   static void extendExistingLoan(
      Loan existingLoan,
      final double postitiveAmount
      ) {
      if(postitiveAmount <= 0.) return;
      try {
         existingLoan.extendLoan(postitiveAmount);
      } catch(InsufficientFundsException fundingException) {
         fundingException.printStackTrace();
      }
   }
   
   @Override
   public Pair<Double, Double> distributeResources(
      final Map<String, Borrower> marketConsumers,
      final Map<String, Lender> marketSuppliers,
      final Map<Pair<String, String>, Pair<Double, Double>> desiredResourceExchanges
      ) {
      Map<Pair<String, String>, Double> interestRates = 
         new HashMap<Pair<String, String>, Double>();
      Map<String, Double> 
         loanDemands = new HashMap<String, Double>(),
         loanSupplies = new HashMap<String, Double>();
      for(Entry<Pair<String, String>, Pair<Double, Double>> record : 
         desiredResourceExchanges.entrySet()) {
         interestRates.put(record.getKey(), record.getValue().getSecond());
         final String
            borrowerID = record.getKey().getFirst(),
            lenderID = record.getKey().getSecond();
         final double
            desiredTrade = record.getValue().getFirst();
         if(!loanDemands.containsKey(borrowerID))
            loanDemands.put(borrowerID, 0.);
         if(!loanSupplies.containsKey(lenderID))
            loanSupplies.put(lenderID, 0.);
         loanDemands.put(borrowerID, loanDemands.get(borrowerID) + desiredTrade);
         loanSupplies.put(lenderID, loanSupplies.get(lenderID) + desiredTrade);
      }
      return this.distributeLoans(
         marketConsumers, marketSuppliers, loanDemands, loanSupplies, interestRates);
   }
   
   private Pair<Double, Double> distributeLoans(
      final Map<String, Borrower> borrowers,
      final Map<String, Lender> lenders,
      final Map<String, Double> loanDemands,
      final Map<String, Double> loanSupplies,
      final Map<Pair<String, String>, Double> interestRates
      ) {
      
      // collect amount demanded by each firm at the current interest rate
      Map<String,Double> updatedLoanDemand = new HashMap<String,Double>();
      for(String key : loanDemands.keySet())
         updatedLoanDemand.put(key, 0.);
      
      Map<String,Double> updatedLoanSupply = new HashMap<String, Double>();
      Map<String, Double> loanSuppliesCopy = new HashMap<String, Double>();
      for(String key : loanSupplies.keySet()) {
         updatedLoanSupply.put(key, 0.);
         loanSuppliesCopy.put(key, loanSupplies.get(key));
      }
   
   /** 
     * Aggregation for loans. Loans between the same participant pair (the
     * same lender-borrower pair) will be combined, by extension of the loan
     * principal, until the cash aggreation threshold LOAN_AGGREGATION_THRESHOLD
     * is reached. At this point, a new loan contract will instead be created.
     */
   class LoanAggregator {
      private static final double LOAN_AGGREGATION_THRESHOLD = 5.e7;
      private Map<MultiKey<String>, Stack<Loan>> newLoanContracts = 
         new HashMap<MultiKey<String>, Stack<Loan>>();
      // Establish a new effective loan, either (a) by creating a loan contract, or
      // (b) by extending the principal sum of an existing loan.
      void establishNew(
         String borrowerID,
         String lenderID,
         double principalValue
         ) {
         MultiKey<String> contractKey = new MultiKey<String>(borrowerID, lenderID);
         Stack<Loan> existingLoans = newLoanContracts.get(contractKey);
         if(existingLoans == null) {
            existingLoans = new Stack<Loan>();
            newLoanContracts.put(contractKey, existingLoans);
            existingLoans.push(createNewLoan(borrowerID, lenderID, principalValue));
         }
         else if (existingLoans.peek().getLoanPrincipalValue() < LOAN_AGGREGATION_THRESHOLD) {
            extendExistingLoan(existingLoans.peek(), principalValue);
         } 
         else
            existingLoans.push(createNewLoan(borrowerID, lenderID, principalValue));
      }
      
      // Create a new loan contract.
      private Loan createNewLoan(
         String borrowerID,
         String lenderID,
         double principalValue
         ) {
         return setupLoanContract(
            borrowers.get(borrowerID),
            lenders.get(lenderID),
            principalValue,
            interestRates.get(new Pair<String, String>(borrowerID, lenderID))
            );
      }
   }
   LoanAggregator loanAggregator = new LoanAggregator();
   
   final Iterator<Entry<String, Double>> firmIter = loanDemands.entrySet().iterator();
   while (firmIter.hasNext()) {
       Entry<String, Double> firm = firmIter.next();
       final Iterator<Entry<String, Double>> bankIter = loanSuppliesCopy.entrySet().iterator();
       while (bankIter.hasNext()) { 
           Entry<String, Double> bank = bankIter.next();
           // leverage circle in case lender == borrower's depositor - try same bank again
           while (updatedLoanDemand.get(firm.getKey())<firm.getValue()&&updatedLoanSupply.get(bank.getKey())<bank.getValue()) { 
               double reserves = ((Bank) lenders.get(bank.getKey())).getCashReserveValue();
               if (reserves>0.0) {
                   double increment  = Math.min(reserves,Math.min(firm.getValue()-updatedLoanDemand.get(firm.getKey()),bank.getValue()-updatedLoanSupply.get(bank.getKey())));
                   updatedLoanDemand.put(firm.getKey(),updatedLoanDemand.get(firm.getKey())+increment);
                   updatedLoanSupply.put(bank.getKey(),updatedLoanSupply.get(bank.getKey())+increment);
                   loanSupplies.put(bank.getKey(), loanSupplies.get(bank.getKey()) - increment); //legacy
                   loanAggregator.establishNew(firm.getKey(), bank.getKey(), increment);
               } else {
                   // leverage circle in case lender != borrower's depositor - try all (other) banks
                   final Iterator<Entry<String, Double>> otherBankIter = loanSuppliesCopy.entrySet().iterator();
                   while (otherBankIter.hasNext()) { 
                       Entry<String, Double> otherBank = otherBankIter.next();
                       while (updatedLoanDemand.get(firm.getKey())<firm.getValue()&&updatedLoanSupply.get(otherBank.getKey())<otherBank.getValue()) { 
                           reserves = ((Bank) lenders.get(otherBank.getKey())).getCashReserveValue();
                           if (reserves>0.0) {
                               double increment  = Math.min(reserves,Math.min(firm.getValue()-updatedLoanDemand.get(firm.getKey()),otherBank.getValue()-updatedLoanSupply.get(otherBank.getKey())));
                               updatedLoanDemand.put(firm.getKey(),updatedLoanDemand.get(firm.getKey())+increment);
                               updatedLoanSupply.put(otherBank.getKey(),updatedLoanSupply.get(otherBank.getKey())+increment);
                               loanSupplies.put(otherBank.getKey(), loanSupplies.get(otherBank.getKey()) - increment); //legacy
                               loanAggregator.establishNew(firm.getKey(), otherBank.getKey(), increment);
                           } else break;
                       }
                       if (updatedLoanDemand.get(firm.getKey())>=firm.getValue()) 
                           break; // otherBank loop shortcut
                   }
                   // in case cash is not flowing back to any bank, bank has to handle cash flow exception
                   double increment = firm.getValue()-updatedLoanDemand.get(firm.getKey());
                   if (increment > 0) {
                       updatedLoanDemand.put(firm.getKey(),updatedLoanDemand.get(firm.getKey())+increment);
                       updatedLoanSupply.put(bank.getKey(),updatedLoanSupply.get(bank.getKey())+increment);
                       loanSupplies.put(bank.getKey(), loanSupplies.get(bank.getKey()) - increment); //legacy
                       loanAggregator.establishNew(firm.getKey(), bank.getKey(), increment);
                   }
                   break; // no point of trying same bank again
               }
           }
           if (updatedLoanDemand.get(firm.getKey())>=firm.getValue()) 
               break; // bank loop shortcut
         }
      }
      
      double
         effectiveLoan = 0.,
         tradeWeightedInterestRate = 0.;
      int numberOfLoansCreated = 0;
      for(Stack<Loan> loans : loanAggregator.newLoanContracts.values()) {
         for(Loan loan : loans) {
            effectiveLoan += loan.getLoanPrincipalValue();
            tradeWeightedInterestRate += loan.getLoanPrincipalValue() * loan.getInterestRate();
         }
         numberOfLoansCreated += loans.size();
      }
      System.out.println("Number of loans created: " + numberOfLoansCreated + ".");
      loanAggregator.newLoanContracts.clear();
      return new Pair<Double, Double>(effectiveLoan, tradeWeightedInterestRate/effectiveLoan);
   }
}
