/*
 * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.strata.pricer.impl.rate;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.OptionalDouble;

import com.opengamma.strata.basics.date.DayCount;
import com.opengamma.strata.basics.date.HolidayCalendar;
import com.opengamma.strata.basics.index.OvernightIndex;
import com.opengamma.strata.basics.index.OvernightIndexObservation;
import com.opengamma.strata.collect.timeseries.LocalDateDoubleTimeSeries;
import com.opengamma.strata.market.explain.ExplainKey;
import com.opengamma.strata.market.explain.ExplainMapBuilder;
import com.opengamma.strata.market.sensitivity.PointSensitivityBuilder;
import com.opengamma.strata.pricer.PricingException;
import com.opengamma.strata.pricer.rate.OvernightIndexRates;
import com.opengamma.strata.pricer.rate.RateComputationFn;
import com.opengamma.strata.pricer.rate.RatesProvider;
import com.opengamma.strata.product.rate.OvernightAveragedRateComputation;

/**
* Rate computation implementation for a rate based on a single overnight index that is arithmetically averaged.
* <p>
* The rate already fixed are retrieved from the time series of the {@link RatesProvider}.
* The rate in the future and not in the cut-off period are computed by approximation.
* The rate in the cut-off period (already fixed or forward) are added.
* <p>
* Reference: Overnight Indexes related products, OpenGamma documentation 29, version 1.1, March 2013.
*/
public class ApproxForwardOvernightAveragedRateComputationFn
    implements RateComputationFn<OvernightAveragedRateComputation> {

  /**
   * Default implementation.
   */
  public static final ApproxForwardOvernightAveragedRateComputationFn DEFAULT =
      new ApproxForwardOvernightAveragedRateComputationFn();

  /**
   * Creates an instance.
   */
  public ApproxForwardOvernightAveragedRateComputationFn() {
  }

  //-------------------------------------------------------------------------
  @Override
  public double rate(
      OvernightAveragedRateComputation computation,
      LocalDate startDate,
      LocalDate endDate,
      RatesProvider provider) {

    OvernightIndexRates rates = provider.overnightIndexRates(computation.getIndex());
    LocalDate valuationDate = rates.getValuationDate();
    LocalDate startFixingDate = computation.getStartDate();
    LocalDate startPublicationDate = computation.calculatePublicationFromFixing(startFixingDate);
    // No fixing to analyze. Go directly to approximation and cut-off.
    if (valuationDate.isBefore(startPublicationDate)) {
      return rateForward(computation, rates);
    }
    ObservationDetails details = new ObservationDetails(computation, rates);
    return details.calculateRate();
  }

  @Override
  public PointSensitivityBuilder rateSensitivity(
      OvernightAveragedRateComputation computation,
      LocalDate startDate,
      LocalDate endDate,
      RatesProvider provider) {

    OvernightIndexRates rates = provider.overnightIndexRates(computation.getIndex());
    LocalDate valuationDate = rates.getValuationDate();
    LocalDate startFixingDate = computation.getStartDate();
    LocalDate startPublicationDate = computation.calculatePublicationFromFixing(startFixingDate);
    // No fixing to analyze. Go directly to approximation and cut-off.
    if (valuationDate.isBefore(startPublicationDate)) {
      return rateForwardSensitivity(computation, rates);
    }
    ObservationDetails details = new ObservationDetails(computation, rates);
    return details.calculateRateSensitivity();
  }

  @Override
  public double explainRate(
      OvernightAveragedRateComputation computation,
      LocalDate startDate,
      LocalDate endDate,
      RatesProvider provider,
      ExplainMapBuilder builder) {

    double rate = rate(computation, startDate, endDate, provider);
    builder.put(ExplainKey.COMBINED_RATE, rate);
    return rate;
  }

  //-------------------------------------------------------------------------
  // Compute the approximated rate in the case where the whole period is forward.
  // There is no need to compute overnight periods, except for the cut-off period.
  private double rateForward(OvernightAveragedRateComputation computation, OvernightIndexRates rates) {
    OvernightIndex index = computation.getIndex();
    HolidayCalendar calendar = computation.getFixingCalendar();
    LocalDate startFixingDate = computation.getStartDate();
    LocalDate endFixingDateP1 = computation.getEndDate();
    LocalDate endFixingDate = calendar.previous(endFixingDateP1);
    LocalDate onRateEndDate = computation.calculateMaturityFromFixing(endFixingDate);
    LocalDate onRateStartDate = computation.calculateEffectiveFromFixing(startFixingDate);
    LocalDate onRateNoCutOffEndDate = onRateEndDate;
    int cutoffOffset = computation.getRateCutOffDays() > 1 ? computation.getRateCutOffDays() : 1;
    double accumulatedInterest = 0.0d;
    double accrualFactorTotal = index.getDayCount().yearFraction(onRateStartDate, onRateEndDate);
    if (cutoffOffset > 1) { // Cut-off period
      LocalDate currentFixingDate = endFixingDate;
      OvernightIndexObservation lastIndexObs = null;
      double cutOffAccrualFactorTotal = 0d;
      for (int i = 1; i < cutoffOffset; i++) {
        currentFixingDate = calendar.previous(currentFixingDate);
        lastIndexObs = computation.observeOn(currentFixingDate);
        onRateNoCutOffEndDate = lastIndexObs.getMaturityDate();
        cutOffAccrualFactorTotal += lastIndexObs.getYearFraction();
      }
      double forwardRateCutOff = rates.rate(lastIndexObs);
      accumulatedInterest += cutOffAccrualFactorTotal * forwardRateCutOff;
    }
    // Approximated part
    accumulatedInterest += approximatedInterest(computation.observeOn(onRateStartDate), onRateNoCutOffEndDate, rates);
    // final rate
    return accumulatedInterest / accrualFactorTotal;
  }

  private PointSensitivityBuilder rateForwardSensitivity(
      OvernightAveragedRateComputation computation,
      OvernightIndexRates rates) {

    OvernightIndex index = computation.getIndex();
    HolidayCalendar calendar = computation.getFixingCalendar();
    LocalDate startFixingDate = computation.getStartDate();
    LocalDate endFixingDateP1 = computation.getEndDate();
    LocalDate endFixingDate = calendar.previous(endFixingDateP1);
    LocalDate onRateEndDate = computation.calculateMaturityFromFixing(endFixingDate);
    LocalDate onRateStartDate = computation.calculateEffectiveFromFixing(startFixingDate);
    LocalDate lastNonCutOffMatDate = onRateEndDate;
    int cutoffOffset = computation.getRateCutOffDays() > 1 ? computation.getRateCutOffDays() : 1;
    PointSensitivityBuilder combinedPointSensitivityBuilder = PointSensitivityBuilder.none();
    double accrualFactorTotal = index.getDayCount().yearFraction(onRateStartDate, onRateEndDate);
    if (cutoffOffset > 1) { // Cut-off period
      List<Double> noCutOffAccrualFactorList = new ArrayList<>();
      LocalDate currentFixingDate = endFixingDateP1;
      LocalDate cutOffEffectiveDate;
      for (int i = 0; i < cutoffOffset; i++) {
        currentFixingDate = calendar.previous(currentFixingDate);
        cutOffEffectiveDate = computation.calculateEffectiveFromFixing(currentFixingDate);
        lastNonCutOffMatDate = computation.calculateMaturityFromEffective(cutOffEffectiveDate);
        double accrualFactor = index.getDayCount().yearFraction(cutOffEffectiveDate, lastNonCutOffMatDate);
        noCutOffAccrualFactorList.add(accrualFactor);
      }
      OvernightIndexObservation lastIndexObs = computation.observeOn(currentFixingDate);
      PointSensitivityBuilder forwardRateCutOffSensitivity = rates.ratePointSensitivity(lastIndexObs);
      double totalAccrualFactor = 0.0;
      for (int i = 0; i < cutoffOffset - 1; i++) {
        totalAccrualFactor += noCutOffAccrualFactorList.get(i);
      }
      forwardRateCutOffSensitivity = forwardRateCutOffSensitivity.multipliedBy(totalAccrualFactor);
      combinedPointSensitivityBuilder = combinedPointSensitivityBuilder.combinedWith(forwardRateCutOffSensitivity);
    }
    // Approximated part
    OvernightIndexObservation indexObs = computation.observeOn(onRateStartDate);
    PointSensitivityBuilder approximatedInterestAndSensitivity =
        approximatedInterestSensitivity(indexObs, lastNonCutOffMatDate, rates);
    combinedPointSensitivityBuilder = combinedPointSensitivityBuilder.combinedWith(approximatedInterestAndSensitivity);
    combinedPointSensitivityBuilder = combinedPointSensitivityBuilder.multipliedBy(1.0 / accrualFactorTotal);
    // final rate
    return combinedPointSensitivityBuilder;
  }

  // Compute the accrued interest on a given period by approximation
  private static double approximatedInterest(
      OvernightIndexObservation observation,
      LocalDate endDate,
      OvernightIndexRates rates) {

    DayCount dayCount = observation.getIndex().getDayCount();
    double remainingFixingAccrualFactor = dayCount.yearFraction(observation.getEffectiveDate(), endDate);
    double forwardRate = rates.periodRate(observation, endDate);
    return Math.log(1.0 + forwardRate * remainingFixingAccrualFactor);
  }

  // Compute the accrued interest sensitivity on a given period by approximation
  private static PointSensitivityBuilder approximatedInterestSensitivity(
      OvernightIndexObservation observation,
      LocalDate endDate,
      OvernightIndexRates rates) {

    DayCount dayCount = observation.getIndex().getDayCount();
    double remainingFixingAccrualFactor = dayCount.yearFraction(observation.getEffectiveDate(), endDate);
    double forwardRate = rates.periodRate(observation, endDate);
    PointSensitivityBuilder forwardRateSensitivity = rates.periodRatePointSensitivity(observation, endDate);
    double rateExp = 1.0 + forwardRate * remainingFixingAccrualFactor;
    forwardRateSensitivity = forwardRateSensitivity.multipliedBy(remainingFixingAccrualFactor / rateExp);
    return forwardRateSensitivity;
  }

  //-------------------------------------------------------------------------
  // Internal class representing all the details related to the computation
  private static final class ObservationDetails {
    // The list below are created in the constructor and never modified after.
    private final OvernightIndexRates rates;
    private final List<OvernightIndexObservation> observations;  // one observation per fixing date
    private int fixedPeriod; // Note this is mutable
    private final double accrualFactorTotal;
    private final int nbPeriods;
    private final OvernightIndex index;
    private final int cutoffOffset;

    // Construct all the details related to the observation: fixing dates, publication dates, start and end dates, 
    // accrual factors, number of already fixed ON rates.
    private ObservationDetails(OvernightAveragedRateComputation computation, OvernightIndexRates rates) {
      this.index = computation.getIndex();
      this.rates = rates;
      LocalDate startFixingDate = computation.getStartDate();
      LocalDate endFixingDateP1 = computation.getEndDate();
      this.cutoffOffset = computation.getRateCutOffDays() > 1 ? computation.getRateCutOffDays() : 1;
      double accrualFactorAccumulated = 0d;
      // find all observations in the period
      LocalDate currentFixing = startFixingDate;
      List<OvernightIndexObservation> indexObsList = new ArrayList<>();
      while (currentFixing.isBefore(endFixingDateP1)) {
        OvernightIndexObservation indexObs = computation.observeOn(currentFixing);
        indexObsList.add(indexObs);
        currentFixing = computation.getFixingCalendar().next(currentFixing);
        accrualFactorAccumulated += indexObs.getYearFraction();
      }
      this.accrualFactorTotal = accrualFactorAccumulated;
      this.nbPeriods = indexObsList.size();
      // dealing with cut-off by replacing observations with ones where fixing/publication locked
      // within cut-off, the effective/maturity dates of each observation have to stay the same
      for (int i = 0; i < cutoffOffset - 1; i++) {
        OvernightIndexObservation fixingIndexObs = indexObsList.get(nbPeriods - cutoffOffset);
        OvernightIndexObservation cutoffIndexObs = indexObsList.get(nbPeriods - 1 - i);
        OvernightIndexObservation updatedIndexObs = cutoffIndexObs.toBuilder()
            .fixingDate(fixingIndexObs.getFixingDate())
            .publicationDate(fixingIndexObs.getPublicationDate())
            .build();
        indexObsList.set(nbPeriods - 1 - i, updatedIndexObs);
      }
      this.observations = Collections.unmodifiableList(indexObsList);
    }

    // Accumulated rate - publication strictly before valuation date: try accessing fixing time-series.
    // fixedPeriod is altered by this method.
    private double pastAccumulation() {
      double accumulatedInterest = 0.0d;
      LocalDateDoubleTimeSeries indexFixingDateSeries = rates.getFixings();
      while ((fixedPeriod < nbPeriods) &&
          rates.getValuationDate().isAfter(observations.get(fixedPeriod).getPublicationDate())) {
        OvernightIndexObservation obs = observations.get(fixedPeriod);
        accumulatedInterest += obs.getYearFraction() *
            checkedFixing(obs.getFixingDate(), indexFixingDateSeries, index);
        fixedPeriod++;
      }
      return accumulatedInterest;
    }

    // Accumulated rate - publication on valuation: Check if a fixing is available on current date.
    // fixedPeriod is altered by this method.
    private double valuationDateAccumulation() {
      double accumulatedInterest = 0.0d;
      LocalDateDoubleTimeSeries indexFixingDateSeries = rates.getFixings();
      boolean ratePresent = true;
      while (ratePresent && fixedPeriod < nbPeriods &&
          rates.getValuationDate().isEqual(observations.get(fixedPeriod).getPublicationDate())) {
        OvernightIndexObservation obs = observations.get(fixedPeriod);
        OptionalDouble fixedRate = indexFixingDateSeries.get(obs.getFixingDate());
        if (fixedRate.isPresent()) {
          accumulatedInterest += obs.getYearFraction() * fixedRate.getAsDouble();
          fixedPeriod++;
        } else {
          ratePresent = false;
        }
      }
      return accumulatedInterest;
    }

    //  Accumulated rate - approximated forward rates if not all fixed and not part of cutoff
    private double approximatedForwardAccumulation() {
      int nbPeriodNotCutOff = nbPeriods - cutoffOffset + 1;
      if (fixedPeriod < nbPeriodNotCutOff) {
        LocalDate endDateApprox = observations.get(nbPeriodNotCutOff - 1).getMaturityDate();
        return approximatedInterest(observations.get(fixedPeriod), endDateApprox, rates);
      }
      return 0.0d;
    }

    //  Accumulated rate sensitivity - approximated forward rates if not all fixed and not part of cutoff
    private PointSensitivityBuilder approximatedForwardAccumulationSensitivity() {
      int nbPeriodNotCutOff = nbPeriods - cutoffOffset + 1;
      if (fixedPeriod < nbPeriodNotCutOff) {
        LocalDate endDateApprox = observations.get(nbPeriodNotCutOff - 1).getMaturityDate();
        return approximatedInterestSensitivity(observations.get(fixedPeriod), endDateApprox, rates);
      }
      return PointSensitivityBuilder.none();
    }

    // Accumulated rate - cutoff part if not fixed
    private double cutOffAccumulation() {
      double accumulatedInterest = 0.0d;
      int nbPeriodNotCutOff = nbPeriods - cutoffOffset + 1;
      for (int i = Math.max(fixedPeriod, nbPeriodNotCutOff); i < nbPeriods; i++) {
        OvernightIndexObservation obs = observations.get(i);
        double forwardRate = rates.rate(obs);
        accumulatedInterest += obs.getYearFraction() * forwardRate;
      }
      return accumulatedInterest;
    }

    // Accumulated rate sensitivity - cutoff part if not fixed
    private PointSensitivityBuilder cutOffAccumulationSensitivity() {
      PointSensitivityBuilder combinedPointSensitivityBuilder = PointSensitivityBuilder.none();
      int nbPeriodNotCutOff = nbPeriods - cutoffOffset + 1;
      for (int i = Math.max(fixedPeriod, nbPeriodNotCutOff); i < nbPeriods; i++) {
        OvernightIndexObservation obs = observations.get(i);
        PointSensitivityBuilder forwardRateSensitivity = rates.ratePointSensitivity(obs)
            .multipliedBy(obs.getYearFraction());
        combinedPointSensitivityBuilder = combinedPointSensitivityBuilder.combinedWith(forwardRateSensitivity);
      }
      return combinedPointSensitivityBuilder;
    }

    // Calculate the total rate.
    private double calculateRate() {
      return (pastAccumulation() + valuationDateAccumulation() +
          approximatedForwardAccumulation() + cutOffAccumulation()) / accrualFactorTotal;
    }

    // Calculate the total rate sensitivity.
    private PointSensitivityBuilder calculateRateSensitivity() {
      // call these methods to ensure mutable fixedPeriod variable is updated
      pastAccumulation();
      valuationDateAccumulation();
      // calculate sensitivity
      PointSensitivityBuilder combinedPointSensitivity = approximatedForwardAccumulationSensitivity();
      PointSensitivityBuilder cutOffAccumulationSensitivity = cutOffAccumulationSensitivity();
      combinedPointSensitivity = combinedPointSensitivity.combinedWith(cutOffAccumulationSensitivity);
      combinedPointSensitivity = combinedPointSensitivity.multipliedBy(1.0d / accrualFactorTotal);
      return combinedPointSensitivity;
    }

    // Check that the fixing is present. Throws an exception if not and return the rate as double.
    private static double checkedFixing(
        LocalDate currentFixingTs,
        LocalDateDoubleTimeSeries indexFixingDateSeries,
        OvernightIndex index) {

      OptionalDouble fixedRate = indexFixingDateSeries.get(currentFixingTs);
      return fixedRate.orElseThrow(() -> new PricingException(
          "Could not get fixing value of index " + index.getName() + " for date " + currentFixingTs));
    }
  }

}