/*
 Copyright (C) 2019 Quaternion Risk Management Ltd
 Copyright (C) 2022 Skandinaviska Enskilda Banken AB (publ)
 Copyright (C) 2025 Paolo D'Elia
 Copyright (C) 2026 Yassine Idyiahia

 This file is part of QuantLib, a free-software/open-source library
 for financial quantitative analysts and developers - http://quantlib.org/

 QuantLib is free software: you can redistribute it and/or modify it
 under the terms of the QuantLib license.  You should have received a
 copy of the license along with this program; if not, please email
 <quantlib-dev@lists.sf.net>. The license is also available online at
 <https://www.quantlib.org/license.shtml>.

 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 license for more details.
*/

#include <ql/errors.hpp>
#include <ql/quotes/simplequote.hpp>
#include <ql/pricingengines/blackdeltacalculator.hpp>
#include <ql/math/interpolations/cubicinterpolation.hpp>
#include <ql/math/interpolations/linearinterpolation.hpp>
#include <ql/termstructures/volatility/equityfx/blackvolsurfacedelta.hpp>
#include <ql/termstructures/volatility/interpolatedsmilesection.hpp>
#include <ql/termstructures/volatility/flatsmilesection.hpp>

using namespace std;

namespace QuantLib {

    BlackVolatilitySurfaceDelta::BlackVolatilitySurfaceDelta(
        Date referenceDate, const std::vector<Date>& dates, const std::vector<Real>& putDeltas,
        const std::vector<Real>& callDeltas, bool hasAtm, const Matrix& blackVolMatrix, const DayCounter& dayCounter,
        const Calendar& cal, const Handle<Quote>& spot, const Handle<YieldTermStructure>& domesticTS,
        const Handle<YieldTermStructure>& foreignTS, DeltaVolQuote::DeltaType deltaType, DeltaVolQuote::AtmType atmType,
        ext::optional<DeltaVolQuote::DeltaType> atmDeltaType, SmileInterpolationMethod im,
        bool flatStrikeExtrapolation, BlackVolTimeExtrapolation::Type timeExtrapolationType, const Period& switchTenor,
        DeltaVolQuote::DeltaType longTermDeltaType, DeltaVolQuote::AtmType longTermAtmType,
        ext::optional<DeltaVolQuote::DeltaType> longTermAtmDeltaType)
    : BlackVolatilityTermStructure(referenceDate, cal, Following, dayCounter), dates_(dates), times_(dates.size(), 0),
      putDeltas_(putDeltas), callDeltas_(callDeltas), hasAtm_(hasAtm), spot_(spot), domesticTS_(domesticTS),
      foreignTS_(foreignTS), deltaType_(deltaType), atmType_(atmType), atmDeltaType_(atmDeltaType ? *atmDeltaType : deltaType),
      interpolationMethod_(im), flatStrikeExtrapolation_(flatStrikeExtrapolation), timeExtrapolationType_(timeExtrapolationType),
      switchTenor_(switchTenor), longTermDeltaType_(longTermDeltaType), longTermAtmType_(longTermAtmType),
      longTermAtmDeltaType_(longTermAtmDeltaType ? *longTermAtmDeltaType : longTermDeltaType) {

        // set switch time
        switchTime_ = switchTenor_ == 0 * Days ? QL_MAX_REAL : timeFromReference(optionDateFromTenor(switchTenor));

        QL_REQUIRE(dates.size() > 1, "at least 1 date required");
        // this has already been done for dates
        for (Size i = 0; i < dates.size(); i++) {
            QL_REQUIRE(referenceDate < dates[i], "Dates must be greater than reference date");
            times_[i] = timeFromReference(dates[i]);
            if (i > 0) {
                QL_REQUIRE(times_[i] > times_[i - 1], "dates must be sorted unique!");
            }
        }

        // check size of matrix
        Size n = putDeltas.size() + (hasAtm ? 1 : 0) + callDeltas.size();
        QL_REQUIRE(n > 0, "Need at least one delta");
        QL_REQUIRE(blackVolMatrix.columns() == n, "Invalid number of columns in blackVolMatrix, got "
                                                    << blackVolMatrix.columns() << " but have " << n << " deltas");
        QL_REQUIRE(blackVolMatrix.rows() == dates.size(), "Invalid number of rows in blackVolMatrix, got "
                                                            << blackVolMatrix.rows() << " but have " << dates.size()
                                                            << " dates");

        // build interpolators for each delta
        // TODO: template this so it can be changed
        bool forceMonotoneVariance = false;
        for (Size i = 0; i < n; i++) {
            vector<Volatility> vols(dates.size());
            for (Size j = 0; j < dates.size(); j++) {
                vols[j] = blackVolMatrix[j][i];
            }

            // BlackVarianceCurve will make a local copy of vols and dates
            interpolators_.push_back(ext::make_shared<BlackVarianceCurve>(
                referenceDate, dates, vols, dayCounter, forceMonotoneVariance,
                timeExtrapolationType_));
        }

        // register
        registerWith(spot_);
        registerWith(domesticTS_);
        registerWith(foreignTS_);
    }

    ext::shared_ptr<SmileSection> BlackVolatilitySurfaceDelta::blackVolSmile(Time t) const {

        Real spot = spot_->value();
        DiscountFactor dDiscount = domesticTS_->discount(t);
        DiscountFactor fDiscount = foreignTS_->discount(t);
        Real sqrtT = sqrt(t);

        DeltaVolQuote::AtmType at;
        DeltaVolQuote::DeltaType dt;
        DeltaVolQuote::DeltaType atmDt;
        if (t < switchTime_ && !close_enough(t, switchTime_)) {
            at = atmType_;
            dt = deltaType_;
            atmDt = atmDeltaType_;
        } else {
            at = longTermAtmType_;
            dt = longTermDeltaType_;
            atmDt = longTermAtmDeltaType_;
        }

        // Store smile section in map. Use strikes as key and vols as values for automatic sorting by strike.
        // If we have already have a strike from a previous delta, we do not overwrite it.
        auto comp = [](Real a, Real b) { return !close(a, b) && a < b; };
        map<Real, Real, decltype(comp)> smileSection(comp);
        Size i = 0;
        Real atmLevel = 1.0; // set atmLevel to 1.0 in case hasAtm_ is false

        for (Real delta : putDeltas_) {
            Real vol = interpolators_.at(i)->blackVol(t, 1, true);
            try {
                BlackDeltaCalculator bdc(Option::Put, dt, spot, dDiscount, fDiscount, vol * sqrtT);
                Real strike = bdc.strikeFromDelta(delta);
                if (smileSection.count(strike) == 0)
                    smileSection[strike] = vol;
            } catch (const std::exception& e) {
                QL_FAIL("BlackVolatilitySurfaceDelta: Error during calculating put strike at delta " << delta << ": "
                                                                                                    << e.what());
            }
            i++;
        }
        if (hasAtm_) {
            Real vol = interpolators_.at(i)->blackVol(t, 1, true);
            atmLevel = vol;
            try {
                BlackDeltaCalculator bdc(Option::Put, atmDt, spot, dDiscount, fDiscount, vol * sqrtT);
                Real strike = bdc.atmStrike(at);
                if (smileSection.count(strike) == 0)
                    smileSection[strike] = vol;
            } catch (const std::exception& e) {
                QL_FAIL("BlackVolatilitySurfaceDelta: Error during calculating atm strike: " << e.what());
            }
            i++;
        }
        for (Real delta : callDeltas_) {
            Real vol = interpolators_.at(i)->blackVol(t, 1, true);
            try {
                BlackDeltaCalculator bdc(Option::Call, dt, spot, dDiscount, fDiscount, vol * sqrtT);
                Real strike = bdc.strikeFromDelta(delta);
                if (smileSection.count(strike) == 0)
                    smileSection[strike] = vol;
            } catch (const std::exception& e) {
                QL_FAIL("BlackVolatilitySurfaceDelta: Error during calculating call strike at delta " << delta << ": "
                                                                                                    << e.what());
            }
            i++;
        }

        // sort and extract to vectors
        vector<Real> strikes;
        strikes.reserve(smileSection.size());
        vector<Real> stdDevs;
        stdDevs.reserve(smileSection.size());
        for (const auto& kv : smileSection) {
            strikes.push_back(kv.first);
            stdDevs.push_back(kv.second * sqrtT);
        }

        // now build smile from strikes and vols
        QL_REQUIRE(!stdDevs.empty(),
                "BlackVolatilitySurfaceDelta::blackVolSmile(" << t << "): no strikes given, this is unexpected.");
        if (stdDevs.size() == 1) {
            // handle the situation that we only have one strike (might occur for e.g. t=0)
            return ext::make_shared<FlatSmileSection>(t, stdDevs.front() / sqrtT, dayCounter());
        } else {
            // we have at least two strikes
            if (interpolationMethod_ == SmileInterpolationMethod::Linear)
                return ext::make_shared<InterpolatedSmileSection<Linear>>(t, strikes, stdDevs, atmLevel, Linear(), dayCounter(), 
                                                                          VolatilityType::ShiftedLognormal, 0.0, flatStrikeExtrapolation_);
            else if (interpolationMethod_ == SmileInterpolationMethod::NaturalCubic)
                return ext::make_shared<InterpolatedSmileSection<Cubic>>(t, strikes, stdDevs, atmLevel, Cubic(CubicInterpolation::Kruger), 
                                                                         dayCounter(), VolatilityType::ShiftedLognormal, 0.0, 
                                                                         flatStrikeExtrapolation_);
            else if (interpolationMethod_ == SmileInterpolationMethod::FinancialCubic)
                return ext::make_shared<InterpolatedSmileSection<Cubic>>(t, 
                        strikes, 
                        stdDevs, 
                        atmLevel, 
                        Cubic(CubicInterpolation::Kruger, true, CubicInterpolation::SecondDerivative, 0.0,
                                    CubicInterpolation::FirstDerivative),
                        dayCounter(),
                        VolatilityType::ShiftedLognormal, 
                        0.0, 
                        flatStrikeExtrapolation_);
            else if (interpolationMethod_ == SmileInterpolationMethod::CubicSpline)
                return ext::make_shared<InterpolatedSmileSection<Cubic>>(t, strikes, stdDevs, atmLevel, Cubic(CubicInterpolation::Spline),
                                                                         dayCounter(), VolatilityType::ShiftedLognormal, 0.0, 
                                                                         flatStrikeExtrapolation_);
            else {
                QL_FAIL("Invalid method " << (int)interpolationMethod_);
            }
        }
    }

    ext::shared_ptr<SmileSection> BlackVolatilitySurfaceDelta::blackVolSmile(const Date& d) const {
        return blackVolSmile(timeFromReference(d));
    }

    Real BlackVolatilitySurfaceDelta::atmLevel(Time t) const {
        return spot_->value() * foreignTS_->discount(t) / domesticTS_->discount(t); // TODO
    }

    Volatility BlackVolatilitySurfaceDelta::blackVolImpl(Time t, Real strike) const {
        // If asked for strike == 0, just return the ATM value.
        Time tme =
            (t > times_.back() && timeExtrapolationType_ == BlackVolTimeExtrapolation::FlatVolatility) ? times_.back() : t;

        if (strike == 0 || strike == Null<Real>()) {
            if (hasAtm_) {
                // ask the ATM interpolator directly
                return interpolators_[putDeltas_.size()]->blackVol(tme, Null<Real>(), true);
            } else {
                // set strike to be fwd and we will return ATMF
                strike = atmLevel(tme);
            }
        }
        return blackVolSmile(tme)->volatility(strike);
    }

} // namespace QuantLib
