/*
 Copyright (C) 2018 Quaternion Risk Management Ltd
 All rights reserved.

 This file is part of ORE, a free-software/open-source library
 for transparent pricing and risk analysis - http://opensourcerisk.org

 ORE is free software: you can redistribute it and/or modify it
 under the terms of the Modified BSD License.  You should have received a
 copy of the license along with this program.
 The license is also available online at <http://opensourcerisk.org>

 This program is distributed on the basis that it will form a useful
 contribution to risk analytics and model standardisation, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
*/

#include "utilities.hpp"

#include "toplevelfixture.hpp"
#include <boost/test/unit_test.hpp>
#include <ql/cashflows/cpicoupon.hpp>
#include <ql/cashflows/cpicouponpricer.hpp>
#include <ql/cashflows/indexedcashflow.hpp>
#include <ql/experimental/inflation/cpicapfloorengines.hpp>
#include <ql/experimental/inflation/cpicapfloortermpricesurface.hpp>
#include <ql/indexes/ibor/gbplibor.hpp>
#include <ql/indexes/inflation/euhicp.hpp>
#include <ql/indexes/inflation/ukrpi.hpp>
#include <ql/instruments/bonds/cpibond.hpp>
#include <ql/instruments/cpicapfloor.hpp>
#include <ql/instruments/cpiswap.hpp>
#include <ql/instruments/zerocouponinflationswap.hpp>
#include <ql/math/interpolations/bilinearinterpolation.hpp>
#include <ql/pricingengines/blackformula.hpp>
#include <ql/pricingengines/bond/discountingbondengine.hpp>
#include <ql/pricingengines/swap/discountingswapengine.hpp>
#include <ql/termstructures/bootstraphelper.hpp>
#include <ql/termstructures/inflation/inflationhelpers.hpp>
#include <ql/termstructures/inflation/piecewisezeroinflationcurve.hpp>
#include <ql/termstructures/volatility/inflation/constantcpivolatility.hpp>
#include <ql/termstructures/yield/flatforward.hpp>
#include <ql/termstructures/yield/zerocurve.hpp>
#include <ql/time/calendars/unitedkingdom.hpp>
#include <ql/time/daycounters/actual365fixed.hpp>
#include <ql/time/daycounters/actualactual.hpp>
#include <ql/types.hpp>
#include <qle/pricingengines/cpiblackcapfloorengine.hpp>
#include <qle/termstructures/interpolatedcpivolatilitysurface.hpp>
#include <qle/termstructures/strippedcpivolatilitystructure.hpp>
#include <qle/time/yearcounter.hpp>

using namespace QuantLib;
using namespace boost::unit_test_framework;
using namespace std;

namespace {
struct Datum {
    Date date;
    Rate rate;
};

BOOST_FIXTURE_TEST_SUITE(QuantExtTestSuite, qle::test::TopLevelFixture)

BOOST_AUTO_TEST_SUITE(CPICapFloorTest)

template <class T, class U, class I>
std::vector<boost::shared_ptr<BootstrapHelper<T> > > makeHelpers(Datum iiData[], Size N, const boost::shared_ptr<I>& ii,
                                                                 const Period& observationLag, const Calendar& calendar,
                                                                 const BusinessDayConvention& bdc, const DayCounter& dc,
                                                                 Handle<YieldTermStructure> yts) {

    std::vector<boost::shared_ptr<BootstrapHelper<T> > > instruments;
    for (Size i = 0; i < N; i++) {
        Date maturity = iiData[i].date;
        Handle<Quote> quote(boost::shared_ptr<Quote>(new SimpleQuote(iiData[i].rate / 100.0)));
        boost::shared_ptr<BootstrapHelper<T> > anInstrument(
            new U(quote, observationLag, maturity, calendar, bdc, dc, ii, yts));
        instruments.push_back(anInstrument);
    }

    return instruments;
}

// Copy of the test setup from QuantLib/test-suite/inflationcpicapfloor.cpp
namespace {
struct CommonVars {
    // common data

    Size length;
    Date startDate;
    Rate baseZeroRate;
    Real volatility;

    Frequency frequency;
    std::vector<Real> nominals;
    Calendar calendar;
    BusinessDayConvention convention;
    Natural fixingDays;
    Date evaluationDate;
    Natural settlementDays;
    Date settlement;
    Period observationLag, contractObservationLag;
    CPI::InterpolationType contractObservationInterpolation;
    DayCounter dcZCIIS, dcNominal;
    std::vector<Date> zciisD;
    std::vector<Rate> zciisR;
    boost::shared_ptr<UKRPI> ii;
    RelinkableHandle<ZeroInflationIndex> hii;
    Size zciisDataLength;

    RelinkableHandle<YieldTermStructure> nominalUK;
    RelinkableHandle<ZeroInflationTermStructure> cpiUK;
    RelinkableHandle<ZeroInflationTermStructure> hcpi;

    vector<Rate> cStrikesUK;
    vector<Rate> fStrikesUK;
    vector<Period> cfMaturitiesUK;
    boost::shared_ptr<Matrix> cPriceUK;
    boost::shared_ptr<Matrix> fPriceUK;

    boost::shared_ptr<CPICapFloorTermPriceSurface> cpiCFsurfUK;

    // cleanup

    SavedSettings backup;

    // setup
    CommonVars() : nominals(1, 1000000) {
        // option variables
        frequency = Annual;
        // usual setup
        volatility = 0.01;
        length = 7;
        calendar = UnitedKingdom();
        convention = ModifiedFollowing;
        Date today(1, June, 2010);
        evaluationDate = calendar.adjust(today);
        Settings::instance().evaluationDate() = evaluationDate;
        settlementDays = 0;
        fixingDays = 0;
        settlement = calendar.advance(today, settlementDays, Days);
        startDate = settlement;
        dcZCIIS = ActualActual();
        dcNominal = ActualActual();

        // uk rpi index
        //      fixing data
        Date from(1, July, 2007);
        Date to(1, June, 2010);
        Schedule rpiSchedule = MakeSchedule()
                                   .from(from)
                                   .to(to)
                                   .withTenor(1 * Months)
                                   .withCalendar(UnitedKingdom())
                                   .withConvention(ModifiedFollowing);
        Real fixData[] = { 206.1, 207.3, 208.0, 208.9, 209.7, 210.9, 209.8, 211.4, 212.1, 214.0, 215.1, 216.8, //  2008
                           216.5, 217.2, 218.4, 217.7, 216.0, 212.9, 210.1, 211.4, 211.3, 211.5, 212.8, 213.4, //  2009
                           213.4, 214.4, 215.3, 216.0, 216.6, 218.0, 217.9, 219.2, 220.7, 222.8, -999,  -999,  //  2010
                           -999 };

        // link from cpi index to cpi TS
        bool interp = false; // this MUST be false because the observation lag is only 2 months
                             // for ZCIIS; but not for contract if the contract uses a bigger lag.
        ii = boost::make_shared<UKRPI>(interp, hcpi);
        for (Size i = 0; i < rpiSchedule.size(); i++) {
            ii->addFixing(rpiSchedule[i], fixData[i], true); // force overwrite in case multiple use
        };

        Datum nominalData[] = { { Date(2, June, 2010), 0.499997 },
                                { Date(3, June, 2010), 0.524992 },
                                { Date(8, June, 2010), 0.524974 },
                                { Date(15, June, 2010), 0.549942 },
                                { Date(22, June, 2010), 0.549913 },
                                { Date(1, July, 2010), 0.574864 },
                                { Date(2, August, 2010), 0.624668 },
                                { Date(1, September, 2010), 0.724338 },
                                { Date(16, September, 2010), 0.769461 },
                                { Date(1, December, 2010), 0.997501 },
                                //{ Date( 16, December, 2010), 0.838164 },
                                { Date(17, March, 2011), 0.916996 },
                                { Date(16, June, 2011), 0.984339 },
                                { Date(22, September, 2011), 1.06085 },
                                { Date(22, December, 2011), 1.141788 },
                                { Date(1, June, 2012), 1.504426 },
                                { Date(3, June, 2013), 1.92064 },
                                { Date(2, June, 2014), 2.290824 },
                                { Date(1, June, 2015), 2.614394 },
                                { Date(1, June, 2016), 2.887445 },
                                { Date(1, June, 2017), 3.122128 },
                                { Date(1, June, 2018), 3.322511 },
                                { Date(3, June, 2019), 3.483997 },
                                { Date(1, June, 2020), 3.616896 },
                                { Date(1, June, 2022), 3.8281 },
                                { Date(2, June, 2025), 4.0341 },
                                { Date(3, June, 2030), 4.070854 },
                                { Date(1, June, 2035), 4.023202 },
                                { Date(1, June, 2040), 3.954748 },
                                { Date(1, June, 2050), 3.870953 },
                                { Date(1, June, 2060), 3.85298 },
                                { Date(2, June, 2070), 3.757542 },
                                { Date(3, June, 2080), 3.651379 } };
        const Size nominalDataLength = 33 - 1;

        std::vector<Date> nomD;
        std::vector<Rate> nomR;
        for (Size i = 0; i < nominalDataLength; i++) {
            nomD.push_back(nominalData[i].date);
            nomR.push_back(nominalData[i].rate / 100.0);
        }
        boost::shared_ptr<YieldTermStructure> nominalTS =
            boost::make_shared<InterpolatedZeroCurve<Linear> >(nomD, nomR, dcNominal);

        nominalUK.linkTo(nominalTS);

        // now build the zero inflation curve
        observationLag = Period(2, Months);
        contractObservationLag = Period(3, Months);
        contractObservationInterpolation = CPI::Flat;

        Datum zciisData[] = {
            { Date(1, June, 2011), 3.087 }, { Date(1, June, 2012), 3.12 },  { Date(1, June, 2013), 3.059 },
            { Date(1, June, 2014), 3.11 },  { Date(1, June, 2015), 3.15 },  { Date(1, June, 2016), 3.207 },
            { Date(1, June, 2017), 3.253 }, { Date(1, June, 2018), 3.288 }, { Date(1, June, 2019), 3.314 },
            { Date(1, June, 2020), 3.401 }, { Date(1, June, 2022), 3.458 }, { Date(1, June, 2025), 3.52 },
            { Date(1, June, 2030), 3.655 }, { Date(1, June, 2035), 3.668 }, { Date(1, June, 2040), 3.695 },
            { Date(1, June, 2050), 3.634 }, { Date(1, June, 2060), 3.629 },
        };
        zciisDataLength = 17;
        for (Size i = 0; i < zciisDataLength; i++) {
            zciisD.push_back(zciisData[i].date);
            zciisR.push_back(zciisData[i].rate);
        }

        // now build the helpers ...
        std::vector<boost::shared_ptr<BootstrapHelper<ZeroInflationTermStructure> > > helpers =
            makeHelpers<ZeroInflationTermStructure, ZeroCouponInflationSwapHelper, ZeroInflationIndex>(
                zciisData, zciisDataLength, ii, observationLag, calendar, convention, dcZCIIS,
                Handle<YieldTermStructure>(nominalTS));

        // we can use historical or first ZCIIS for this
        // we know historical is WAY off market-implied, so use market implied flat.
        baseZeroRate = zciisData[0].rate / 100.0;
        boost::shared_ptr<PiecewiseZeroInflationCurve<Linear>> pCPIts(
            new PiecewiseZeroInflationCurve<Linear>(evaluationDate, calendar, dcZCIIS, observationLag, ii->frequency(),
                                                    ii->interpolated(), baseZeroRate, helpers));
        pCPIts->recalculate();
        cpiUK.linkTo(pCPIts);
        hii.linkTo(ii);

        // make sure that the index has the latest zero inflation term structure
        hcpi.linkTo(pCPIts);

        // cpi CF price surf data
        Period cfMat[] = { 3 * Years, 5 * Years, 7 * Years, 10 * Years, 15 * Years, 20 * Years, 30 * Years };
        Real cStrike[] = { 0.03, 0.04, 0.05, 0.06 };
        Real fStrike[] = { -0.01, 0, 0.01, 0.02 };
        Size ncStrikes = 4, nfStrikes = 4, ncfMaturities = 7;

        Real cPrice[7][4] = { { 227.6, 100.27, 38.8, 14.94 },    { 345.32, 127.9, 40.59, 14.11 },
                              { 477.95, 170.19, 50.62, 16.88 },  { 757.81, 303.95, 107.62, 43.61 },
                              { 1140.73, 481.89, 168.4, 63.65 }, { 1537.6, 607.72, 172.27, 54.87 },
                              { 2211.67, 839.24, 184.75, 45.03 } };
        Real fPrice[7][4] = { { 15.62, 28.38, 53.61, 104.6 },   { 21.45, 36.73, 66.66, 129.6 },
                              { 24.45, 42.08, 77.04, 152.24 },  { 39.25, 63.52, 109.2, 203.44 },
                              { 36.82, 63.62, 116.97, 232.73 }, { 39.7, 67.47, 121.79, 238.56 },
                              { 41.48, 73.9, 139.75, 286.75 } };

        // now load the data into vector and Matrix classes
        cStrikesUK.clear();
        fStrikesUK.clear();
        cfMaturitiesUK.clear();
        for (Size i = 0; i < ncStrikes; i++)
            cStrikesUK.push_back(cStrike[i]);
        for (Size i = 0; i < nfStrikes; i++)
            fStrikesUK.push_back(fStrike[i]);
        for (Size i = 0; i < ncfMaturities; i++)
            cfMaturitiesUK.push_back(cfMat[i]);
        cPriceUK = boost::make_shared<Matrix>(ncStrikes, ncfMaturities);
        fPriceUK = boost::make_shared<Matrix>(nfStrikes, ncfMaturities);
        for (Size i = 0; i < ncStrikes; i++) {
            for (Size j = 0; j < ncfMaturities; j++) {
                (*cPriceUK)[i][j] = cPrice[j][i] / 10000.0;
            }
        }
        for (Size i = 0; i < nfStrikes; i++) {
            for (Size j = 0; j < ncfMaturities; j++) {
                (*fPriceUK)[i][j] = fPrice[j][i] / 10000.0;
            }
        }

        Real nominal = 1.0;
        boost::shared_ptr<InterpolatedCPICapFloorTermPriceSurface<Bilinear> > intplCpiCFsurfUK(
            new InterpolatedCPICapFloorTermPriceSurface<Bilinear>(
                nominal, baseZeroRate, observationLag, calendar, convention, dcZCIIS, hii, nominalUK, cStrikesUK,
                fStrikesUK, cfMaturitiesUK, *(cPriceUK), *(fPriceUK)));

        cpiCFsurfUK = intplCpiCFsurfUK;
    }
};

class FlatZeroInflationTermStructure : public ZeroInflationTermStructure {
public:
    FlatZeroInflationTermStructure(const Date& referenceDate, const Calendar& calendar, const DayCounter& dayCounter,
                                   Rate zeroRate, const Period& observationLag, Frequency frequency, bool indexIsInterp,
                                   const Handle<YieldTermStructure>& ts)
        : ZeroInflationTermStructure(referenceDate, calendar, dayCounter, zeroRate, observationLag, frequency,
                                     indexIsInterp),
          zeroRate_(zeroRate) {}

    Date maxDate() const { return Date::maxDate(); }
    // Base date consistent with observation lag, interpolation and frequency
    Date baseDate() const {
        Date base = referenceDate() - observationLag();
        if (!indexIsInterpolated()) {
            std::pair<Date, Date> ips = inflationPeriod(base, frequency());
            base = ips.first;
        }
        return base;
    }

private:
    Rate zeroRateImpl(Time t) const { return zeroRate_; }
    Real zeroRate_;
};

} // namespace

// additional test cases from here

BOOST_AUTO_TEST_CASE(testVolatilitySurface) {
    CommonVars common;

    Handle<YieldTermStructure> nominalTS = common.nominalUK;

    // this engine is used to imply the volatility that reproduces a quoted price
    boost::shared_ptr<QuantExt::CPIBlackCapFloorEngine> blackEngine =
        boost::make_shared<QuantExt::CPIBlackCapFloorEngine>(nominalTS,
                                                             QuantLib::Handle<QuantLib::CPIVolatilitySurface>());

    // wrap the pointer into a handle
    Handle<CPICapFloorTermPriceSurface> cpiPriceSurfaceHandle(common.cpiCFsurfUK);

    // initialize the vol surface, taking the price surface as an input and running the vol imply calculations
    QuantExt::PriceQuotePreference type = QuantExt::CapFloor;
    boost::shared_ptr<QuantExt::StrippedCPIVolatilitySurface<QuantLib::Bilinear> > cpiVolSurface =
        boost::make_shared<QuantExt::StrippedCPIVolatilitySurface<QuantLib::Bilinear> >(type, cpiPriceSurfaceHandle,
                                                                                common.ii, blackEngine);

    // attach the implied vol surface to the engine
    blackEngine->setVolatility(QuantLib::Handle<QuantLib::CPIVolatilitySurface>(cpiVolSurface));

    // reprice and check that we recover the quotes

    Real nominal = 1.0;

    Date startDate = Settings::instance().evaluationDate();
    Calendar fixCalendar = UnitedKingdom(), payCalendar = UnitedKingdom();
    BusinessDayConvention fixConvention(Unadjusted), payConvention(ModifiedFollowing);
    Real baseCPI = common.hii->fixing(fixCalendar.adjust(startDate - common.observationLag, fixConvention));
    CPI::InterpolationType observationInterpolation = CPI::AsIndex;

    Handle<CPICapFloorTermPriceSurface> cpiCFsurfUKh(common.cpiCFsurfUK);
    boost::shared_ptr<PricingEngine> engine(new InterpolatingCPICapFloorEngine(cpiCFsurfUKh));

    for (Size i = 0; i < common.cStrikesUK.size(); i++) {
        Rate strike = common.cStrikesUK[i];
        for (Size j = 0; j < common.cfMaturitiesUK.size(); j++) {
            Period maturity = common.cfMaturitiesUK[j];
            Date maturityDate = startDate + maturity;

            CPICapFloor aCap(Option::Call, nominal, startDate, baseCPI, maturityDate, fixCalendar, fixConvention,
                             payCalendar, payConvention, strike, common.hii, common.observationLag,
                             observationInterpolation);

            aCap.setPricingEngine(engine);

            Real cached = (*common.cPriceUK)[i][j] * 10000;
            Real npv1 = aCap.NPV() * 10000;
            // QL_REQUIRE(fabs(cached - npv1) < 1e-10,
            //            "InterpolatingCPICapFloorEngine does not reproduce cached price: " << cached << " vs " <<
            //            npv1);
            BOOST_CHECK_SMALL(fabs(cached - npv1), 1e-10);

            aCap.setPricingEngine(blackEngine);
            Real npv2 = aCap.NPV() * 10000;
            // QL_REQUIRE(fabs(cached - npv2) < 0.1,
            //            "CPIBlackCapFloorEngine does not reproduce cached price: " << cached << " vs " << npv2);
            BOOST_CHECK_SMALL(fabs(cached - npv2), 1e-5); // depends on the default solver tolerance 1e-8

            BOOST_TEST_MESSAGE("Cap " << fixed << std::setprecision(2) << strike << " " << setw(3) << maturity
                                      << ":  cached " << setw(7) << cached << "  QL " << setw(8) << npv1 << "  QLE "
                                      << setw(8) << npv2 << "  diff " << setw(8) << npv2 - npv1);
        }
    }

    for (Size i = 0; i < common.fStrikesUK.size(); i++) {
        Rate strike = common.fStrikesUK[i];
        //       fStrikesUK.push_back(fStrike[i]);
        for (Size j = 0; j < common.cfMaturitiesUK.size(); j++) {
            Period maturity = common.cfMaturitiesUK[j];
            Date maturityDate = startDate + maturity;

            CPICapFloor aFloor(Option::Put, nominal, startDate, baseCPI, maturityDate, fixCalendar, fixConvention,
                               payCalendar, payConvention, strike, common.hii, common.observationLag,
                               observationInterpolation);

            aFloor.setPricingEngine(engine);

            Real cached = (*common.fPriceUK)[i][j] * 10000;
            Real npv1 = aFloor.NPV() * 10000;
            // QL_REQUIRE(fabs(cached - npv1) < 1e-10,
            //            "InterpolatingCPICapFloorEngine does not reproduce cached price: " << cached << " vs " <<
            //            npv1);
            BOOST_CHECK_SMALL(fabs(cached - npv1), 1e-10);

            aFloor.setPricingEngine(blackEngine);
            Real npv2 = aFloor.NPV() * 10000;
            // QL_REQUIRE(fabs(cached - npv2) < 0.1,
            //            "CPIBlackCapFloorEngine does not reproduce cached price: " << cached << " vs " << npv2);
            BOOST_CHECK_SMALL(fabs(cached - npv2), 1e-5); // depends on the default solver tolerance 1e-8

            BOOST_TEST_MESSAGE("Floor " << fixed << std::setprecision(2) << strike << " " << setw(3) << maturity
                                        << ":  cached " << setw(7) << cached << "  QL " << setw(8) << npv1 << "  QLE "
                                        << setw(8) << npv2 << "  diff " << setw(8) << npv2 - npv1);
        }
    }
}

BOOST_AUTO_TEST_CASE(testPutCallParity) {

    CommonVars common;

    Handle<YieldTermStructure> nominalTS = common.nominalUK;

    // this engine is used to imply the volatility that reproduces a quoted price
    boost::shared_ptr<QuantExt::CPIBlackCapFloorEngine> blackEngine =
        boost::make_shared<QuantExt::CPIBlackCapFloorEngine>(nominalTS,
                                                             QuantLib::Handle<QuantLib::CPIVolatilitySurface>());

    // wrap the pointer into a handle
    Handle<CPICapFloorTermPriceSurface> cpiPriceSurfaceHandle(common.cpiCFsurfUK);

    // initialize the vol surface, taking the price surface as an input and running the vol imply calculations
    QuantExt::PriceQuotePreference type = QuantExt::CapFloor;
    boost::shared_ptr<QuantExt::StrippedCPIVolatilitySurface<QuantLib::Bilinear> > cpiVolSurface =
        boost::make_shared<QuantExt::StrippedCPIVolatilitySurface<QuantLib::Bilinear> >(type, cpiPriceSurfaceHandle,
                                                                                common.ii, blackEngine);

    // attach the implied vol surface to the engine
    blackEngine->setVolatility(QuantLib::Handle<QuantLib::CPIVolatilitySurface>(cpiVolSurface));

    Period mat[] = { 3 * Years,  4 * Years,  5 * Years,  6 * Years,  7 * Years,  8 * Years, 9 * Years,
                     10 * Years, 12 * Years, 15 * Years, 20 * Years, 25 * Years, 30 * Years };
    Size nMat = 13;

    Real strike[] = { 0.0, 0.005, 0.01, 0.015, 0.02, 0.025, 0.03, 0.035, 0.04, 0.045, 0.05 };
    Size nStrike = 11;

    Real nominal = 1.0;

    Date startDate = Settings::instance().evaluationDate();
    Calendar fixCalendar = UnitedKingdom(), payCalendar = UnitedKingdom();
    BusinessDayConvention fixConvention(Unadjusted), payConvention(ModifiedFollowing);
    Real baseCPI = common.hii->fixing(fixCalendar.adjust(startDate - common.observationLag, fixConvention));
    CPI::InterpolationType observationInterpolation = CPI::AsIndex;

    Date effectiveStart = startDate - common.observationLag;
    if (!common.ii->interpolated()) {
        std::pair<Date, Date> ips = inflationPeriod(effectiveStart, common.ii->frequency());
        effectiveStart = ips.first;
    }

    for (Size i = 0; i < nStrike; ++i) {

        for (Size j = 0; j < nMat; ++j) {
            Date maturityDate = startDate + mat[j];

            CPICapFloor aCap(Option::Call, nominal, startDate, baseCPI, maturityDate, fixCalendar, fixConvention,
                             payCalendar, payConvention, strike[i], common.hii, common.observationLag,
                             observationInterpolation);
            aCap.setPricingEngine(blackEngine);

            CPICapFloor aFloor(Option::Put, nominal, startDate, baseCPI, maturityDate, fixCalendar, fixConvention,
                               payCalendar, payConvention, strike[i], common.hii, common.observationLag,
                               observationInterpolation);
            aFloor.setPricingEngine(blackEngine);

            Real capPrice = aCap.NPV() * 10000;
            Real floorPrice = aFloor.NPV() * 10000;

            // build CPI leg price manually
            Date fixingDate = maturityDate - common.observationLag;
            Date effectiveMaturity = fixingDate;
            if (!common.ii->interpolated()) {
                std::pair<Date, Date> ipm = inflationPeriod(effectiveMaturity, common.ii->frequency());
                effectiveMaturity = ipm.first;
            }
            Real timeFromStart =
                common.ii->zeroInflationTermStructure()->dayCounter().yearFraction(effectiveStart, effectiveMaturity);
            Real K = pow(1.0 + strike[i], timeFromStart);
            Real F = common.ii->fixing(effectiveMaturity) / baseCPI;
            DiscountFactor disc = nominalTS->discount(maturityDate);
            Real cpilegPrice = disc * (F - K) * 10000;

            Real parity = capPrice - floorPrice - cpilegPrice;
            BOOST_TEST_MESSAGE(std::setprecision(3) << fixed << "strike=" << strike[i] << " mat=" << mat[j] << " cap="
                                                    << capPrice << " floor=" << floorPrice << " cpileg=" << cpilegPrice
                                                    << " parity=cap-floor-cpileg=" << parity);

            // FIXME: investigate why the parity check yields ~bp upfront error in some cases
            BOOST_CHECK_SMALL(fabs(parity), 1.1); // bp upfront parity error
        }
    }
}

BOOST_AUTO_TEST_CASE(testInterpolatedVolatilitySurface) {
    CommonVars common;

    Handle<YieldTermStructure> nominalTS = common.nominalUK;

    // this engine is used to imply the volatility that reproduces a quoted price
    boost::shared_ptr<QuantExt::CPIBlackCapFloorEngine> blackEngine =
        boost::make_shared<QuantExt::CPIBlackCapFloorEngine>(nominalTS,
                                                             QuantLib::Handle<QuantLib::CPIVolatilitySurface>());

    // wrap the pointer into a handle
    Handle<CPICapFloorTermPriceSurface> cpiPriceSurfaceHandle(common.cpiCFsurfUK);

    // initialize the vol surface, taking the price surface as an input and running the vol imply calculations
    QuantExt::PriceQuotePreference type = QuantExt::CapFloor;
    boost::shared_ptr<QuantExt::StrippedCPIVolatilitySurface<QuantLib::Bilinear> > cpiVolSurface =
        boost::make_shared<QuantExt::StrippedCPIVolatilitySurface<QuantLib::Bilinear> >(type, cpiPriceSurfaceHandle,
                                                                                common.ii, blackEngine);

    std::vector<Period> optionTenors = cpiVolSurface->maturities();
    std::vector<Real> strikes = cpiVolSurface->strikes();
    vector<vector<Handle<Quote> > > quotes(optionTenors.size(),
                                           vector<Handle<Quote> >(strikes.size(), Handle<Quote>()));
    for (Size i = 0; i < optionTenors.size(); ++i) {
        for (Size j = 0; j < strikes.size(); ++j) {
            Real vol = cpiVolSurface->volatility(optionTenors[i], strikes[j]);
            boost::shared_ptr<SimpleQuote> q(new SimpleQuote(vol));
            quotes[i][j] = Handle<Quote>(q);
        }
    }
    boost::shared_ptr<QuantExt::InterpolatedCPIVolatilitySurface<Bilinear> > interpolatedCpiVol =
        boost::make_shared<QuantExt::InterpolatedCPIVolatilitySurface<Bilinear> >(
            optionTenors, strikes, quotes, common.hii.currentLink(), cpiVolSurface->settlementDays(),
            cpiVolSurface->calendar(), cpiVolSurface->businessDayConvention(), cpiVolSurface->dayCounter(),
            cpiVolSurface->observationLag());

    for (Size i = 0; i < optionTenors.size(); ++i) {
        Date d = cpiVolSurface->optionDateFromTenor(optionTenors[i]);
        for (Size j = 0; j < strikes.size(); ++j) {
            Real vol1 = cpiVolSurface->volatility(d, strikes[j]);
            Real vol2 = interpolatedCpiVol->volatility(d, strikes[j]);
            BOOST_CHECK_SMALL(fabs(vol1 - vol2), 1.0e-10);
        }
    }
}

BOOST_AUTO_TEST_CASE(testSimpleCapFloor) {
    CommonVars common;

    Real rate = 0.03;
    Real inflationRate = 0.02;
    Real inflationBlackVol = 0.05;
    BusinessDayConvention bdc = Unadjusted;
    DayCounter dc = ActualActual();
    Period observationLag = 3 * Months; // EUHICPXT Caps/Swaps
    Handle<YieldTermStructure> discountCurve(boost::make_shared<FlatForward>(common.evaluationDate, rate, dc));
    RelinkableHandle<ZeroInflationTermStructure> inflationCurve;
    Handle<ZeroInflationIndex> index(boost::make_shared<EUHICPXT>(false, inflationCurve));
    // Make sure we use the correct index publication frequency and interpolation, consistent with the index we want to
    // project. We therefore create the index first, then the term structure, then relink the curve. Otherwise time
    // calculations will be inconsistent, and ATM strikes do not generate equal put/call or cap/floor prices.
    boost::shared_ptr<ZeroInflationTermStructure> inflationCurvePtr =
        boost::make_shared<FlatZeroInflationTermStructure>(common.evaluationDate, index->fixingCalendar(), dc,
                                                           inflationRate, observationLag, index->frequency(),
                                                           index->interpolated(), discountCurve);
    inflationCurve.linkTo(inflationCurvePtr);
    // Make sure we use the same observation lag as in the inflation curve, and same index publication frequency and
    // interpolation. The vol surface observation lag is used in the engine for lag difference calculations compared to
    // the instrument's lag.
    Handle<CPIVolatilitySurface> inflationVol(boost::make_shared<ConstantCPIVolatility>(
        inflationBlackVol, 0, inflationCurve->calendar(), bdc, dc, inflationCurve->observationLag(),
        inflationCurve->frequency(), inflationCurve->indexIsInterpolated()));

    boost::shared_ptr<PricingEngine> engine =
        boost::make_shared<QuantExt::CPIBlackCapFloorEngine>(discountCurve, inflationVol);

    Real nominal = 10000.0;
    Date start = common.evaluationDate;
    Date end = start + 10 * Years;
    Real baseCPI = 100.0;
    Calendar fixCalendar = index->fixingCalendar();
    Calendar payCalendar = index->fixingCalendar();

    Rate capStrike = 0.03;
    CPICapFloor atmCap(Option::Call, nominal, start, baseCPI, end, fixCalendar, bdc, payCalendar, bdc, inflationRate,
                       index, observationLag);
    atmCap.setPricingEngine(engine);
    CPICapFloor cap(Option::Call, nominal, start, baseCPI, end, fixCalendar, bdc, payCalendar, bdc, capStrike, index,
                    observationLag);
    cap.setPricingEngine(engine);

    Rate floorStrike = 0.01;
    CPICapFloor atmFloor(Option::Put, nominal, start, baseCPI, end, fixCalendar, bdc, fixCalendar, bdc, inflationRate,
                         index, observationLag);
    atmFloor.setPricingEngine(engine);
    CPICapFloor floor(Option::Put, nominal, start, baseCPI, end, fixCalendar, bdc, fixCalendar, bdc, floorStrike, index,
                      observationLag);
    floor.setPricingEngine(engine);

    DayCounter cpiDayCounter = QuantExt::YearCounter();

    // use base CPI as base fixing
    index->addFixing(inflationCurve->baseDate(), baseCPI);

    // Do cap and floor match at the money?
    BOOST_CHECK_CLOSE(atmCap.NPV(), atmFloor.NPV(), 1e-8);

    // Pedestrian CPI Cap/Floor valuation assuming baseCPI = base fixing
    Real t = 10.0; // time to maturity is 10 (base to effective maturity)
    Real stdDev = inflationBlackVol * sqrt(t);
    Real forward = pow(1.0 + inflationRate, t);
    Real floorStrikePrice = pow(1.0 + floorStrike, t);
    Real capStrikePrice = pow(1.0 + capStrike, t);
    Real discount = exp(-rate * t);
    Real expectedCapNPV = nominal * blackFormula(Option::Call, capStrikePrice, forward, stdDev, discount);
    Real expectedFloorNPV = nominal * blackFormula(Option::Put, floorStrikePrice, forward, stdDev, discount);

    BOOST_TEST_MESSAGE("CPI Cap NPV " << cap.NPV() << " " << expectedCapNPV);
    BOOST_TEST_MESSAGE("CPI Floor NPV " << floor.NPV() << " " << expectedFloorNPV);

    BOOST_CHECK_CLOSE(cap.NPV(), expectedCapNPV, 0.01);     // relative difference of 0.01%
    BOOST_CHECK_CLOSE(floor.NPV(), expectedFloorNPV, 0.01); // relative difference of 0.01%
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()

} // namespace


//
// Created by Hamza El-Kebir on 6/25/21.
//

#ifndef LODESTAR_CONTINUOUSSAMPLEDEKF_HPP
#define LODESTAR_CONTINUOUSSAMPLEDEKF_HPP

#include <Eigen/Dense>
#include <Eigen/QR>
#include <functional>
#include <type_traits>

#include "Lodestar/primitives/integrators/IntegratorsEnum.hpp"
#include "Lodestar/primitives/integrators/BogackiShampine.hpp"
#include "Lodestar/primitives/integrators/RungeKuttaFehlberg45.hpp"
#include "Lodestar/primitives/integrators/RungeKuttaFehlberg78.hpp"

#include <iostream>

namespace ls {
    namespace filter {
        class ContinuousSampledEKF {
        public:
            using integrators = ls::primitives::IntegratorsEnum;

            template<ls::primitives::IntegratorsEnum TIntegrator = ls::primitives::IntegratorsEnum::RungeKuttaFehlberg45, typename TScalar = double, int TStateDim = Eigen::Dynamic, int TInputDim = Eigen::Dynamic, int TOutputDim = Eigen::Dynamic>
            struct EKFParameters {
                const ls::primitives::IntegratorsEnum kIntegrator = TIntegrator;
                using scalar_type = TScalar;
                const int kStateDim = TStateDim;
                const int kInputDim = TInputDim;
                const int kOutputDim = TOutputDim;

                typedef Eigen::Matrix<TScalar, TStateDim, TStateDim> TDStateMatrix;
                typedef Eigen::Matrix<TScalar, TStateDim, 1> TDStateVector;
                typedef Eigen::Matrix<TScalar, TStateDim, TInputDim> TDInputMatrix;
                typedef Eigen::Matrix<TScalar, TInputDim, 1> TDInputVector;
                typedef Eigen::Matrix<TScalar, TOutputDim, TStateDim> TDOutputMatrix;
                typedef Eigen::Matrix<TScalar, TOutputDim, 1> TDOutputVector;
                typedef Eigen::Matrix<TScalar, TOutputDim, TInputDim> TDFeedforwardMatrix;

                typedef Eigen::Matrix<TScalar, TStateDim, TOutputDim> TDKalmanGain;

                typedef TDStateMatrix TDProcessCovarianceMatrix;
                typedef Eigen::Matrix<TScalar, TOutputDim, TOutputDim> TDObservationCovarianceMatrix;

                typedef ls::primitives::IntegratorsEnum TDIntegratorsEnum;

                typedef std::function<TDStateVector(TScalar, const TDStateVector &, const TDInputVector &)> TDOde; //! Ordinary differential equation: f(t, x, u)
                typedef std::function<TDStateMatrix(TScalar, const TDStateMatrix &, const TDStateVector &,
                                                    const TDInputVector &)> TDPOde; //! Matrix ordinary differential equation: f(t, P, x, u)
                typedef std::function<TDStateMatrix(const TDStateVector &, const TDInputVector &)> TDStateJacobian; //! Jacobian state matrix: f(x, u)
                typedef std::function<TDOutputMatrix(const TDStateVector &)> TDOutputJacobian; //! Jacobian output matrix: f(x)
                typedef std::function<TDOutputVector(const TDStateVector &)> TDOutputMap; //! Output map: f(x)

                typedef std::function<TDStateVector(TScalar, const TDStateVector &)> TDOdeFunc;
                typedef std::function<TDStateMatrix(TScalar, const TDStateMatrix &)> TDMatrixOdeFunc;

                void initEquations(const TDOde &ordinaryDiffEq, const TDStateJacobian &stateJacobianEq,
                                   const TDOutputJacobian &outputJacobianEq, const TDOutputMap &outputMapEq)
                {
                    ode = ordinaryDiffEq;
                    stateJacobian = stateJacobianEq;
                    outputJacobian = outputJacobianEq;
                    outputMap = outputMapEq;

                    Pdot = [&](TScalar t, const TDStateMatrix &P, const TDStateVector &x, const TDInputVector &u) {
                        const auto F = stateJacobian(x, u);
                        std::cout << "F at t = " << t << ": " << F << std::endl;
                        std::cout << "P at t = " << t << ": " << P << std::endl;
                        std::cout << "Pdot at t = " << t << ": " << F * P + P * F.transpose() + Q << std::endl;

                        // NOTE: The return value must be explicitly cast as type TDStateMatrix, otherwise the copy
                        // constructor in Eigen fails.
                        TDStateMatrix ret;
                        ret = F * P + P * F.transpose() + Q;

                        return ret;
                    };
                }

                void
                initParams(const TDStateVector &x0, const TDStateMatrix &P0 = TDStateMatrix::Zero(), TScalar t0 = 0)
                {
                    xhat = x0;
                    P = P0;
                    time = t0;
                }

                TDOde ode;
                TDStateJacobian stateJacobian;
                TDOutputJacobian outputJacobian;
                TDOutputMap outputMap;

                TDProcessCovarianceMatrix Q;
                TDObservationCovarianceMatrix R;
                TDStateMatrix P;

                TDStateVector xhat;
                TScalar time;

                TDPOde Pdot;

                // TODO: Add template parameter for integration scheme;
                //  Add initialization and prediction routines;
                //  Add time member variable;
                //  Implement unit tests.

                template<TDIntegratorsEnum T_TIntegrator = TIntegrator>
                typename std::enable_if<T_TIntegrator == TDIntegratorsEnum::BogackiShampine, void>::type
                predict(TDStateVector &xout, TDStateMatrix &Pout, const TDStateVector &x, const TDInputVector &u,
                        const TScalar h)
                {
                    xout = x;
                    auto f = std::bind(ode, std::placeholders::_1, std::placeholders::_2, u);

                    // FIXME: Make x time-dependent.
                    Pout = P;
                    auto F = std::bind(Pdot, std::placeholders::_1, std::placeholders::_2, x, u);
                    TScalar t = 0;

                    ls::primitives::BogackiShampine<TDStateVector, TScalar>::integrateSimple(f, t, xout, h, 1);

                    t = 0;
                    ls::primitives::BogackiShampine<TDStateMatrix, TScalar>::integrateSimple(F, t, Pout, h, 1);
                }

                template<TDIntegratorsEnum T_TIntegrator = TIntegrator>
                typename std::enable_if<T_TIntegrator == TDIntegratorsEnum::RungeKuttaFehlberg45, void>::type
                predict(TDStateVector &xout, TDStateMatrix &Pout, const TDStateVector &x, const TDInputVector &u,
                        const TScalar h)
                {
                    xout = x;
                    auto f = std::bind(ode, std::placeholders::_1, std::placeholders::_2, u);

                    // FIXME: Make x time-dependent.
                    Pout = P;
                    auto F = std::bind(Pdot, std::placeholders::_1, std::placeholders::_2, x, u);
                    TScalar t = 0;

                    ls::primitives::RungeKuttaFehlberg45<TDStateVector, TScalar>::integrateSimple(f, t, xout, h, 1);

                    t = 0;
                    ls::primitives::RungeKuttaFehlberg45<TDStateMatrix, TScalar>::integrateSimple(F, t, Pout, h, 1);
                }

                template<TDIntegratorsEnum T_TIntegrator = TIntegrator>
                typename std::enable_if<T_TIntegrator == TDIntegratorsEnum::RungeKuttaFehlberg78, void>::type
                predict(TDStateVector &xout, TDStateMatrix &Pout, const TDStateVector &x, const TDInputVector &u,
                        const TScalar h)
                {
                    xout = x;
                    auto f = std::bind(ode, std::placeholders::_1, std::placeholders::_2, u);
                    std::function<TDStateVector(TScalar, const TDStateVector &)> func = [&](TScalar t, const TDStateVector &x)
                    {
                        return ode(t, x, u);
                    };

                    // FIXME: Make x time-dependent.
                    Pout = P;
                    auto F = std::bind(Pdot, std::placeholders::_1, std::placeholders::_2, x, u);
                    std::function<TDStateMatrix (TScalar, const TDStateMatrix &)> Func = [&](TScalar t, const TDStateMatrix &P)
                    {
                        return Pdot(t, P, x, u);
                    };
                    TScalar t = 0;

                    ls::primitives::RungeKuttaFehlberg78<TDStateVector, TScalar>::integrateSimple(func, t, xout, h, 1);

                    t = 0;
                    ls::primitives::RungeKuttaFehlberg78<TDStateMatrix, TScalar>::integrateSimple(Func, t, Pout, h, 1);
                }

                void update(TDStateVector &x, const TDOutputVector &z)
                {
                    const TDOutputMatrix H = outputJacobian(x);
                    const TDKalmanGain K = P * H.transpose() * (H * P * H.transpose() + R).completeOrthogonalDecomposition().pseudoInverse();
                    x = x + K * (z - outputMap(x));
                    P = (TDStateMatrix::Identity() - K * H) * P;
                }

                void step(const TDInputVector &u, const TDOutputVector &z, const TScalar h)
                {
                    TDStateVector xnew;
                    TDStateMatrix Pnew;

                    predict(xnew, Pnew, xhat, u, h);
                    xhat = xnew;
                    P = Pnew;

                    std::cout << "xnew " << xnew << std::endl;
                    std::cout << "Pnew " << Pnew << std::endl;

                    update(xhat, z);

                    time += h;
                }

                void setCovarianceMatrices(const TDProcessCovarianceMatrix &QMatrix, const TDObservationCovarianceMatrix &RMatrix)
                {
                    Q = QMatrix;
                    R = RMatrix;
                }

                const TDStateVector &getStateEstimate() const
                {
                    return xhat;
                }

                TScalar getCurrentTime() const
                {
                    return time;
                }
            };
        };
    }
}

#endif //LODESTAR_CONTINUOUSSAMPLEDEKF_HPP


// ------------------------------------------------------------------
// Deformable Convolutional Networks
// Copyright (c) 2015 Microsoft
// Licensed under The MIT License
// Modified from MATLAB Faster R-CNN (https://github.com/shaoqingren/faster_rcnn)
// ------------------------------------------------------------------
#include <cuda_runtime.h>
#include <boost/geometry.hpp>
#include <iostream>
#include <vector>

#define CUDA_CHECK(condition)                               \
  /* Code block avoids redefinition of cudaError_t error */ \
  do {                                                      \
    cudaError_t error = condition;                          \
    if (error != cudaSuccess) {                             \
      std::cout << cudaGetErrorString(error) << std::endl;  \
    }                                                       \
  } while (0)

#define DIVUP(m, n) ((m) / (n) + ((m) % (n) > 0))
int const threadsPerBlock = sizeof(unsigned long long) * 8;

template<typename DType> __device__ inline DType devIoU(DType const *const a, DType const *const b) {
  DType left = max(a[0], b[0]), right = min(a[2], b[2]);
  DType top = max(a[1], b[1]), bottom = min(a[3], b[3]);
  DType width = max(right - left + 1, 0.f), height = max(bottom - top + 1, 0.f);
  DType interS = width * height;
  DType Sa = (a[2] - a[0] + 1) * (a[3] - a[1] + 1);
  DType Sb = (b[2] - b[0] + 1) * (b[3] - b[1] + 1);
  return interS / (Sa + Sb - interS);
}

template<typename DType, int BLOCK_THREADS>
__global__ void nms_kernel(const int n_boxes, const DType nms_overlap_thresh, const DType *dev_boxes,
                           unsigned long long *dev_mask) {
  const int row_start = blockIdx.y;
  const int col_start = blockIdx.x;

  // if (row_start > col_start) return;

  const int row_size = min(n_boxes - row_start * BLOCK_THREADS, BLOCK_THREADS);
  const int col_size = min(n_boxes - col_start * BLOCK_THREADS, BLOCK_THREADS);

  __shared__ DType block_boxes[BLOCK_THREADS * 5];
  if (threadIdx.x < col_size) {
#pragma unroll
    for (int i = 0; i < 5; ++i) {
      block_boxes[threadIdx.x * 5 + i] = dev_boxes[(BLOCK_THREADS * col_start + threadIdx.x) * 5 + i];
    }
  }
  __syncthreads();

  if (threadIdx.x < row_size) {
    const int cur_box_idx = BLOCK_THREADS * row_start + threadIdx.x;
    const DType *cur_box = dev_boxes + cur_box_idx * 5;
    unsigned long long t = 0;
    int start = 0;
    if (row_start == col_start) {
      start = threadIdx.x + 1;
    }
    for (int i = start; i < col_size; i++) {
      if (devIoU(cur_box, block_boxes + i * 5) > nms_overlap_thresh) {
        t |= 1ULL << i;
      }
    }
    const int col_blocks = DIVUP(n_boxes, BLOCK_THREADS);
    dev_mask[cur_box_idx * col_blocks + col_start] = t;
  }
}

void _set_device(int device_id) {
  int current_device;
  CUDA_CHECK(cudaGetDevice(&current_device));
  if (current_device == device_id) {
    return;
  }
  // The call to cudaSetDevice must come before any calls to Get, which
  // may perform initialization using the GPU.
  CUDA_CHECK(cudaSetDevice(device_id));
}

template<typename DType, int BLOCK_THREADS>
int _nms_gpu(int *keep_out, const DType *boxes_host, int boxes_num, int boxes_dim, DType nms_overlap_thresh, int device_id) {
  _set_device(device_id);

  DType *boxes_dev = NULL;
  unsigned long long *mask_dev = NULL;

  const int col_blocks = DIVUP(boxes_num, BLOCK_THREADS);

  CUDA_CHECK(cudaMalloc(&boxes_dev, boxes_num * boxes_dim * sizeof(DType)));
  CUDA_CHECK(cudaMemcpy(boxes_dev, boxes_host, boxes_num * boxes_dim * sizeof(DType), cudaMemcpyHostToDevice));

  CUDA_CHECK(cudaMalloc(&mask_dev, boxes_num * col_blocks * sizeof(unsigned long long)));

  dim3 blocks(DIVUP(boxes_num, BLOCK_THREADS), DIVUP(boxes_num, BLOCK_THREADS));
  dim3 threads(BLOCK_THREADS);
  nms_kernel<DType, BLOCK_THREADS><<<blocks, threads>>>(boxes_num, nms_overlap_thresh, boxes_dev, mask_dev);

  std::vector<unsigned long long> mask_host(boxes_num * col_blocks);
  CUDA_CHECK(cudaMemcpy(&mask_host[0], mask_dev, sizeof(unsigned long long) * boxes_num * col_blocks, cudaMemcpyDeviceToHost));

  std::vector<unsigned long long> remv(col_blocks);
  memset(&remv[0], 0, sizeof(unsigned long long) * col_blocks);

  int num_to_keep = 0;
  for (int i = 0; i < boxes_num; i++) {
    int nblock = i / BLOCK_THREADS;
    int inblock = i % BLOCK_THREADS;

    if (!(remv[nblock] & (1ULL << inblock))) {
      keep_out[num_to_keep++] = i;
      unsigned long long *p = &mask_host[0] + i * col_blocks;
      for (int j = nblock; j < col_blocks; j++) {
        remv[j] |= p[j];
      }
    }
  }

  CUDA_CHECK(cudaFree(boxes_dev));
  CUDA_CHECK(cudaFree(mask_dev));
  return num_to_keep;
}

// template<>
template int _nms_gpu<float, threadsPerBlock>(int *keep_out, const float *boxes_host, int boxes_num, int boxes_dim,
                                              float nms_overlap_thresh, int device_id);
// template<>
template int _nms_gpu<double, threadsPerBlock>(int *keep_out, const double *boxes_host, int boxes_num, int boxes_dim,
                                               double nms_overlap_thresh, int device_id);


/*
 * This file is subject to the terms and conditions defined in
 * file 'LICENSE', which is part of this source code package.
 *
 * Partialy financiated as part of the protocol between UTAustin I Portugal - UTA-P.
 * [2017] - [2018] University of Minho, Filipe Costa Oliveira
 * All Rights Reserved.
 */

#ifndef SRC_SUPERCELL_H__
#define SRC_SUPERCELL_H__

/* BEGIN BOOST */
#include <boost/iostreams/device/mapped_file.hpp> // for mmap
#include <boost/iostreams/stream.hpp>             // for stream
#include <boost/format.hpp>
#include <boost/bind.hpp>
/* END BOOST */

#include <math.h>                        // for fabs, ceil, pow, M_PI
#include <algorithm>                     // for max_element, min_element
#include <cassert>                       // for assert
#include <cstdio>                        // for perror, size_t, NULL, SEEK_END
#include <iomanip>                       // for operator<<, setfill, setw
#include <iostream>                      // for operator<<, basic_ostream
#include <iterator>                      // for distance
#include <limits>                        // for numeric_limits
#include <string>                        // for allocator, char_traits, to_s...
#include <vector>                        // for vector, vector<>::iterator
#include <iosfwd>                    // for string

#include <opencv2/core/hal/interface.h>  // for CV_8UC1, CV_32F, CV_32FC1
#include <opencv2/imgproc/imgproc_c.h>   // for cvGetSpatialMoment
#include <opencv2/imgproc/types_c.h>     // for ::CV_THRESH_BINARY, CvMoments
#include <opencv2/core.hpp>              // for minMaxLoc, normalize, Exception
#include <opencv2/core/base.hpp>         // for NormTypes::NORM_MINMAX, Code...
#include <opencv2/core/cvstd.hpp>        // for Ptr
#include <opencv2/core/cvstd.inl.hpp>    // for operator<<, String::String
#include <opencv2/core/ptr.inl.hpp>      // for Ptr::operator->, Ptr::Ptr<T>
#include <opencv2/core/version.hpp>      // for CV_MAJOR_VERSION
#include <opencv2/features2d.hpp>        // for SimpleBlobDetector::Params
#include <opencv2/imgcodecs.hpp>         // for imwrite
#include <opencv2/imgproc.hpp>           // for Canny, boundingRect, drawCon...
#include <opencv2/video/tracking.hpp>    // for findTransformECC, ::MOTION_E...
#include <opencv2/core/mat.hpp>      // for Mat
#include <opencv2/core/mat.inl.hpp>  // for Mat::~Mat
#include <opencv2/core/matx.hpp>     // for Vec4d
#include <opencv2/core/types.hpp>    // for Point3d, Point, Rect, Point2d
#include <opencv2/opencv.hpp>

#include <QtWidgets>
#include <QObject>

#include "image_bounds.hpp"
#include "base_cell.hpp"
#include "base_bin.hpp"
#include "unit_cell.hpp"
#include "application_log.hpp"
#include "cv_polygon.hpp"
#include "treemodel.h"

class SuperCell : public BaseCell{
  Q_OBJECT
  private:
      //Atom properties
  TreeModel *atom_info_fields_model = nullptr;

    /** Private Class methods **/

    cv::Point2d op_Point2d_padding ( cv::Point2d point, const double padd_x, const double  padd_y );
    cv::Point3d op_Point3d_padding ( cv::Point3d point,  cv::Point3d padd  );
    cv::Point3d op_Point3d_a_plus_b_times_c ( cv::Point3d point, cv::Point3d b, cv::Point3d c );

    bool op_Point3d_z_in_range ( cv::Point3d point,  const double bot_limit, const double top_limit );
    bool op_Point3d_xy_in_range ( cv::Point3d point,  const double x_bot_limit, const double x_top_limit , const double y_bot_limit, const double y_top_limit );
    cv::Point3d op_Mat3d_dot_Point3d ( cv::Mat mat , cv::Point3d point );

    /* Base dir path */
    boost::filesystem::path base_bin_start_dir_path;
    bool _flag_base_bin_start_dir_path = false;

    boost::filesystem::path base_bin_output_dir_path;
    bool _flag_base_bin_output_dir_path = false;

    /* Loggers */
    ApplicationLog::ApplicationLog* logger = nullptr;
    bool _flag_logger = false;

    UnitCell* unit_cell = nullptr;
    bool _flag_unit_cell = false;

    ImageBounds* image_bounds = nullptr;
    bool _flag_image_bounds = false;
    bool _flag_calculate_ab_cell_limits_from_image_bounds = false;

    /** supercell exclusive **/
    double a_min_size_nm = 0.0f;
    bool _flag_a_min_size_nm = false;
    double b_min_size_nm = 0.0f;
    bool _flag_b_min_size_nm = false;
    double c_min_size_nm = 0.0f;
    bool _flag_c_min_size_nm = false;
    bool _flag_min_size_nm = false;

    int expand_factor_a = 1;
    int expand_factor_b = 1;
    int expand_factor_c = 1;
    bool _flag_expand_factor = false;
    bool _flag_expand_factor_a = false;
    bool _flag_expand_factor_b = false;
    bool _flag_expand_factor_c = false;
    bool _flag_auto_calculate_expand_factor = true;
    bool _flag_enable_orientation = true;

    bool update_length_parameters_from_expand_factor();
  protected:

  public:
    SuperCell( Chem_Database *chem_db );
    SuperCell( UnitCell* cell );
    bool clean_for_re_run();
    bool set_base_bin_start_dir_path( boost::filesystem::path base_dir );

    TreeModel *get_atom_info_fields_model(){ return atom_info_fields_model; }

    bool update_from_unit_cell( bool create_orientation_matrix = true );
    bool update_angle_parameters_from_unit_cell();
    bool calculate_expand_factor();
    bool create_atoms_from_unit_cell();
    bool create_fractional_positions_atoms();
    bool orientate_atoms_from_matrix( bool create_matrix = true );
    bool remove_z_out_of_range_atoms();
    bool remove_xy_out_of_range_atoms();
    bool remove_xy_out_of_range_atoms_from_image_bounds();
    bool generate_super_cell_file();
    bool generate_xyz_file();

    /* getters */
    int get_expand_factor_a(){ return expand_factor_a; }
    int get_expand_factor_b(){ return expand_factor_b; }
    int get_expand_factor_c(){ return expand_factor_c; }

    /* setters */
    bool set_unit_cell( UnitCell* cell );
    bool set_image_bounds( ImageBounds* image_bounds );

    bool set_calculate_ab_cell_limits_from_image_bounds( bool value );

    //overide set length from basecell
    bool set_length_a_Nanometers( double a );
    bool set_length_b_Nanometers( double b );
    bool set_length_c_Nanometers( double c );

    bool set_expand_factor_abc( int factor_a, int factor_b, int factor_c );
    bool set_expand_factor_a( int factor_a );
    bool set_expand_factor_b( int factor_b );
    bool set_expand_factor_c( int factor_c );
    bool set_flag_auto_calculate_expand_factor( bool value );
    bool set_flag_enable_orientation( bool value );

    /* Loggers */
    bool set_application_logger( ApplicationLog::ApplicationLog* logger );
    void print_var_state();
    // friend std::ostream& operator<< (std::ostream& stream, const SuperCell::SuperCell& image);
    virtual std::ostream& output(std::ostream& stream) const;

    signals:
      void expand_factor_changed();

};

#endif


#define BOOST_THREAD_PROVIDES_FUTURE

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <stdlib.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
#include <tuple>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <array>
#include <vector>
#include <complex>
#include <boost/thread.hpp>
#include <boost/thread/future.hpp>
#include <random>
#include <math.h>
#include <utility>
#include <algorithm>
#include <boost/hana/tuple.hpp>
#include <boost/hana/zip.hpp>
#include <boost/hana/for_each.hpp>
#include <boost/hana/ext/std/tuple.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include <boost/optional.hpp>
#include <boost/memory_order.hpp>
#include <boost/atomic/atomic.hpp>

const std::string g_infoPath   = "../src/config.xml";
const std::string g_vertexPath = "../src/vertex.vs";
const std::string g_fragmentPath = "../src/fragment.fs";

template<typename T>
struct complexVec2
{
  using type = complexVec2<T>;
  std::complex<T> a, b, c;

  auto operator*(const std::complex<T> &coeff) const
  {
    return type{coeff*a, coeff*b};
  }

  auto operator+(const type &other) const
  {
    return type{other.a+a, other.b+b};
  }
};

template<typename T>
struct complexVec3
{
  using type = complexVec3<T>;
  std::complex<T> a, b, c;

  auto operator*(const std::complex<T> &coeff) const
  {
    return type{coeff*a, coeff*b, coeff*c};
  }

  auto operator+(const type &other) const
  {
    return type{other.a+a, other.b+b, other.c+c};
  }

  auto operator-(const type &other) const
  {
    return type{a-other.a, b-other.b, c-other.c};
  }
};

struct vertex
{
  glm::vec4 position;

  vertex()
  { }

  vertex(float x, float y, float z, float w) : position(x,y,z,w)
  { }
};

struct wave
{
  float time, constant, windForce;
  glm::vec2 dir;

  wave()
  { }

  wave(float time_, float A, float V, glm::vec2 direction) :
    time(time_),
    constant(A),
    windForce(V*V/9.8),
    dir(direction)
  { }

};

struct simInfo
{
  wave wav;
  size_t len, gridlen, numThreads = 2;
  float displacementScale;

  simInfo()
  { }

  simInfo(size_t gridlen_, size_t len_, wave wav_, float dispScale = 1.0f) : wav(wav_), len(len_), gridlen(gridlen_), displacementScale(dispScale)
  { }

  simInfo(const std::string &filename)
  {
    boost::property_tree::ptree settings;
    boost::property_tree::xml_parser::read_xml(filename, settings);
    wav = wave(
      settings.get<float>("simInfo.wave.time"),
      settings.get<float>("simInfo.wave.phillipsConstant"),
      settings.get<float>("simInfo.wave.windSpeed"),
      glm::vec2(settings.get<float>("simInfo.wave.windDirectionX"),
      settings.get<float>("simInfo.wave.windDirectionY"))
    );

    displacementScale = settings.get<float>("simInfo.displacementScale");
    gridlen = settings.get<unsigned int>("simInfo.gridLength");
    len = settings.get<unsigned int>("simInfo.simulationLength");
  }
};

class amplitudeGenerator
{
  std::mt19937 gen_;
  std::normal_distribution<> dist_;
  wave wav_;
public:
  using complex = std::complex<float>;
  using vec2 = complexVec2<float>;

  struct fftVertex
  {
    vec2 displacementCoefficient;
    complex h0;
    complex h0Conj;
    float dispersion;

    template<typename T>
    fftVertex(const wave &wav, T&& waveF, float rand0, float rand1, float x, float y)
    {
      auto k = std::max(sqrtf(x*x+y*y), 0.00001f);
      h0 = (1/sqrtf(2)) * (complex(rand0, rand1)) * sqrtf(waveF(x,y));
      h0Conj = std::conj((1/sqrtf(2)) * (complex(rand0, rand1)) * sqrtf(waveF(-x,-y)));
      dispersion = int(sqrt(9.8*k) / ((2*M_PI)/wav.time)) * ((2*M_PI)/wav.time);
      displacementCoefficient = vec2{complex(x*(1/k)),complex(y*(1/k))}*complex(0,-1.0f);
      //std::cout << " factor1squared " << wavXY << '\n';
    }
  };

  amplitudeGenerator(wave wav) : gen_(std::random_device()()), dist_(0,1), wav_(wav)
  { }

  auto newAmp(float x, float y)
  {
    const auto phillips = [this](float x, float y)
    {
      auto k = sqrtf(x*x + y*y);
      if (k < 0.000001) return 0.0f;
      //std::cout << "( k " << k;
      auto cosineFactorSqrt = wav_.dir[0]*x/k + wav_.dir[1]*y/k;
      //std::cout << " cosineFactorSqrt " << k << " )";
      return wav_.constant * (exp(-1/(k*wav_.windForce*k*wav_.windForce))/(k*k*k*k)) * cosineFactorSqrt * cosineFactorSqrt;
    };

    return fftVertex(wav_, phillips, dist_(gen_), dist_(gen_), x, y);
  }
};

struct plane
{
  GLuint buf;
  GLuint obj;
  size_t count;
  std::vector<amplitudeGenerator::fftVertex> amplitudeCoefficients;

  ~plane()
  {
    // glDeleteBuffers(1, &buf);
    // glDeleteVertexArrays(1, &obj);
  }
};

void GLAPIENTRY msgCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam )
{
  //std::cout << message << '\n';
}

auto constructPlane(const simInfo &info)
{
  float scaleK = (2*M_PI)/info.len;
  float scalePoints = info.len/info.gridlen;
  amplitudeGenerator ampGen(info.wav);
  std::vector<vertex> verts;
  std::vector<amplitudeGenerator::fftVertex> amplitudeCoefficients;

  const auto newPoint = [&verts, &amplitudeCoefficients, &ampGen, &info](auto x, auto y, auto index)
  {
    verts.emplace_back(x, 0.0f, y, index);
  };

  for(auto i = -float{float(info.gridlen)/2}; i < float{info.gridlen/2}; i++)
  {
    for(auto j = -float{info.gridlen/2}; j < float{info.gridlen/2}; j++)
    {
      auto ind = amplitudeCoefficients.size();
      if((info.gridlen/2 - j > 0.001) && (info.gridlen/2 - j > 0.001))
      {
        newPoint((i)*scalePoints, (j)*scalePoints, ind); // base
        newPoint((i)*scalePoints, (j+1.0f)*scalePoints, ind+1); // one column over
        newPoint((i+1.0f)*scalePoints, (j)*scalePoints, ind+info.gridlen); // one row over
        newPoint((i+1.0f)*scalePoints, (j+1.0f)*scalePoints, ind+1+info.gridlen); // diagonal
        newPoint((i)*scalePoints, (j+1.0f)*scalePoints, ind+1); // as above
        newPoint((i+1.0f)*scalePoints, (j)*scalePoints, ind+info.gridlen); // as below
      }

      amplitudeCoefficients.emplace_back(ampGen.newAmp(i*scaleK,j*scaleK)); // gridlen*gridlen amplitudes
    }
  }

  GLuint buf, obj;
  glGenBuffers(1, &buf);
  glGenVertexArrays(1, &obj);

  std::cout << "size " << sizeof(vertex) << "\n";

  glBindBuffer(GL_ARRAY_BUFFER, buf);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertex)*verts.size(), verts.data(), GL_STATIC_DRAW);

  glBindVertexArray(obj);
  glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(vertex), (void*)0);
  glEnableVertexAttribArray(0);

  return plane{buf, obj, info.gridlen*info.gridlen*6, amplitudeCoefficients};
}

template<typename T, typename U, typename V>
void ditfft(const T src, U dest, V &&mutator, size_t N, int s, const size_t srcStride = 1, const size_t destStride = 1)
{
  using valtype = typename std::complex<float>;
  auto halfN = N/2;
  if (halfN == 0)
  {
    *dest = mutator(*src);
    //std::cout << "dest: " << *dest << '\n';
  } else {
    ditfft(src, dest, mutator, halfN, 2*s, srcStride, destStride);
    ditfft(src+s*srcStride, dest+halfN*destStride, mutator, halfN, 2*s, srcStride, destStride);
    for(size_t i = 0; i < halfN; i++)
    {
      auto kInd = (dest+i*destStride);
      auto coeffInd = (dest+(i+halfN)*destStride);
      auto copy = *kInd;
      auto expRes = std::exp(valtype(0.0f, -2*M_PI*i/N));
      //auto expRes = valtype(cos(-6.283185*i/N), sin(-6.283185*i/N)); // e^ix

      *kInd = copy + (*coeffInd)*expRes;
      *coeffInd = copy - (*coeffInd)*expRes;
      //std::cout << "copy: " << copy << " expres: " << expRes << '\n';
    }
  }
}

class fftContext
{
  struct taskControlBlock
  {
    boost::atomic<int> isNew;
  };

  struct task
  {
    const simInfo *info;
    plane *target;
    std::vector<complexVec3<float>> *output;
    double time;
  };

  std::vector<boost::thread> threads_;
  std::vector<complexVec3<float>> horizontalFfts_;
  std::vector<taskControlBlock> taskControlBlocks_;
  boost::optional<task> task_;
  boost::atomic<int> firstStageCount_;
  boost::atomic<int> secondStageCount_;
  boost::mutex mtx_;
  size_t numThreads_;
public:
  fftContext(const fftContext &) = delete;

  fftContext()
  { }

  fftContext(const simInfo &info) :
    threads_(),
    horizontalFfts_(info.gridlen*info.gridlen),
    taskControlBlocks_(info.numThreads),
    task_(boost::none),
    firstStageCount_(),
    secondStageCount_(),
    mtx_(),
    numThreads_(info.numThreads)
  {
    for(auto j = size_t{0}; j < info.numThreads; j++)
    {
      threads_.emplace_back([this, j]()
      {
        while(true)
        {
          auto val = taskControlBlocks_[j].isNew.exchange(0);
          if(val == 0)
          {
            continue;
          }

          // bad bad bad
          if(!task_) // task is null?
          {
            continue; // skip
          }

          auto &task = task_.get();
          auto info = *(task.info);

          for(auto i = size_t{0}; i < info.gridlen/numThreads_; i++) // generate inner ffts over x's
          {
            auto mult = std::complex<float>(pow(-1.0f, i*numThreads_+j), 0);
            ditfft(
              task.target->amplitudeCoefficients.begin() + (i*numThreads_+j)*info.gridlen,
              horizontalFfts_.begin() + (i*numThreads_+j)*info.gridlen,
              [this, info, &mult, &task](const auto &vertex)
                {
                auto coefficient =
                  mult*(std::exp(std::complex<float>(0, vertex.dispersion*task.time))*vertex.h0 +
                  std::exp(std::complex<float>(0, -vertex.dispersion*task.time))*vertex.h0Conj);
                auto vectorComponent = vertex.displacementCoefficient*coefficient*info.displacementScale;
                return complexVec3<float>
                {
                  vectorComponent.a,
                  vectorComponent.b,
                  coefficient
                };
            }, info.gridlen, 1);
          }

          firstStageCount_.fetch_add(1); // increment our horizontal fft counter

          while(firstStageCount_.load() < numThreads_) // all of our horizontal ffts have been completed
          { }

          for(auto i = size_t{0}; i < info.gridlen/numThreads_; i++) // update output
          {
            auto mult = std::complex<float>(pow(-1.0f, i*numThreads_ + j), 0);
            // fft varies over z
            ditfft(horizontalFfts_.begin() + i*numThreads_ + j, task.output->begin() + i*numThreads_ + j, [&mult](const auto &item){
              return item*mult;
            }, info.gridlen, 1, info.gridlen, info.gridlen);
          }

          secondStageCount_.fetch_add(1); // finished
        }
      });

      threads_[j].detach();
    }
  }

  auto fft(const simInfo &info, plane &target, std::vector<complexVec3<float>> &output, double time)
  {
    if(horizontalFfts_.size() < info.gridlen*info.gridlen)
    {
      static constexpr auto zero = std::complex<float>(0,0);
      horizontalFfts_.resize(info.gridlen*info.gridlen, complexVec3<float>{zero,zero,zero});
    }

    boost::lock_guard<boost::mutex> lock(mtx_); // wait until this fft is done before running another
    task_ = task{&info, &target, &output, time};

    firstStageCount_.store(0);
    secondStageCount_.store(0);

    for(auto &block : taskControlBlocks_)
    {
      block.isNew.store(1);
    }

    while(secondStageCount_.load() < numThreads_) // halt on calling thread until the fft is complete
    {}
  }
};

class fftwaterProgram
{
  simInfo simInfo_;
  GLuint program_ = 0;
  GLuint heightmap_ = 0;
  GLuint heightmaptbo_ = 0;
  std::vector<complexVec3<float>> complexMapCpu_;
  std::vector<glm::vec3> heightMapCpu_;
  GLint timeLoc_;
  GLint viewLoc_;
  GLint projLoc_;
  GLint modelLoc_;
  fftContext *fft_;

  template<typename... Ts>
 auto compile(const Ts&... paths)
 {
   program_ = glCreateProgram();

   const auto enums = std::array<unsigned int, 5>{GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_GEOMETRY_SHADER};
   const auto emptyStr = std::string("");
   auto shaders = std::array<GLuint, 5>{0,0,0,0,0};

   std::ifstream input;
   input.exceptions(std::ifstream::failbit|std::ifstream::badbit);

   auto ind = size_t{0};
   boost::hana::for_each(std::tie(paths...), [this, &input, &enums, &shaders, &ind, &emptyStr](const auto& path)
   {
       std::string src;
       auto type = enums[ind];
       decltype(auto) shader = shaders[ind++];

       try
       {
         if(!(path==emptyStr))
         {
           input.open(path);
           std::stringstream stream;
           stream << input.rdbuf();
           src = stream.str();
           input.close();

           const char* srcCStr = src.c_str();
           shader = glCreateShader(type);
           glShaderSource(shader, 1, &srcCStr, NULL);
           glCompileShader(shader);

           GLchar log[1024];
           GLint success;

           glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
           if(!success)
           {
             glGetShaderInfoLog(shader, 1024, NULL, log);
             std::cout << "shader compilation error: " << log << '\n';
           }
           glAttachShader(program_, shader);
         } else {
           return;
         }
       }
       catch(std::ifstream::failure err)
       {
         std::cout << "error reading shader source(s)\n";
       }
   });

   glLinkProgram(program_);
   glUseProgram(program_);

   timeLoc_ = glGetUniformLocation(program_, "time");
   viewLoc_ = glGetUniformLocation(program_, "view");
   modelLoc_ = glGetUniformLocation(program_, "model");
   projLoc_ = glGetUniformLocation(program_, "proj");

   for(auto& shader : shaders)
   {
     glDeleteShader(shader);
   }
 }

public:
  fftwaterProgram()
  {
  }

  template<typename... Ts>
  auto recompile(const Ts&... paths)
  {
    glDeleteProgram(program_);
    compile(paths...);
  }

  auto draw(const glm::mat4 &proj, const glm::mat4 &view, const glm::mat4 &model, double time, plane &toDraw)
  {
    fft_->fft(simInfo_, toDraw, complexMapCpu_, time);

    auto signs = std::array<float,2>{-1.0, 1.0f};

    for(auto i = size_t{0}; i < complexMapCpu_.size(); i++)
    {
      //std::cout << "x: " << i/simInfo_.gridlen << " value: " << complexMapCpu_[i].real() << "\n";
      heightMapCpu_[i] = glm::vec3(
        complexMapCpu_[i].c.real()*signs[(i/simInfo_.gridlen)%2==0],
        complexMapCpu_[i].a.real()*signs[(i/simInfo_.gridlen)%2==0],
        complexMapCpu_[i].b.real()*signs[(i/simInfo_.gridlen)%2==0]
      );
    }

    // heightMapCpu_[x_*x_*4-1] = 50.0f;
    // heightMapCpu_[x_*x_*4-1] = 50.0f;

    glBindBuffer(GL_TEXTURE_BUFFER, heightmaptbo_);
    glBufferData(GL_TEXTURE_BUFFER, sizeof(decltype(heightMapCpu_)::value_type)*heightMapCpu_.size(), heightMapCpu_.data(), GL_STREAM_DRAW);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_BUFFER, heightmap_);
    glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, heightmaptbo_);

    glUseProgram(program_);
    glUniform1f(timeLoc_, time);
    glUniformMatrix4fv(viewLoc_, 1, GL_FALSE, &view[0][0]);
    glUniformMatrix4fv(projLoc_, 1, GL_FALSE,  &proj[0][0]);
    glUniformMatrix4fv(modelLoc_, 1, GL_FALSE,  &model[0][0]);

    glBindVertexArray(toDraw.obj);
    glDrawArrays(GL_TRIANGLES, 0, toDraw.count);
  }

  template<typename... Ts>
  fftwaterProgram(const simInfo &info, fftContext& fft, const Ts&... paths) :
   simInfo_(info),
   program_(0),
   heightmap_(0),
   heightmaptbo_(0),
   complexMapCpu_(info.gridlen*info.gridlen),
   heightMapCpu_(info.gridlen*info.gridlen),
   timeLoc_(0),
   viewLoc_(0),
   projLoc_(0),
   modelLoc_(0),
   fft_(&fft)
  {
    glGenBuffers(1, &heightmaptbo_);
    glBindBuffer(GL_TEXTURE_BUFFER, heightmaptbo_);
    glBufferData(GL_TEXTURE_BUFFER, sizeof(float)*heightMapCpu_.size(), nullptr, GL_STREAM_DRAW);

    glGenTextures(1, &heightmap_);
    glBindTexture(GL_TEXTURE_BUFFER, heightmap_);
    glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, heightmaptbo_);

    compile(paths...);
  }

  fftwaterProgram(fftwaterProgram &&other) :
    simInfo_(other.simInfo_),
    program_(other.program_),
    heightmap_(other.heightmap_),
    heightmaptbo_(other.heightmaptbo_),
    heightMapCpu_(std::move(other.heightMapCpu_)),
    timeLoc_(other.timeLoc_),
    viewLoc_(other.viewLoc_),
    projLoc_(other.projLoc_),
    modelLoc_(other.modelLoc_),
    fft_(other.fft_)
  {
    other.program_ = 0;
    other.heightmap_ = 0;
    other.heightmaptbo_ = 0;
  }

  auto operator=(fftwaterProgram &&other)
  {
    if(this != &other)
    {
      glDeleteBuffers(1, &heightmaptbo_);
      glDeleteTextures(1, &heightmap_);
      glDeleteProgram(program_);

      heightmaptbo_ = 0;
      heightmap_ = 0;
      program_ = 0;

      std::swap(simInfo_, other.simInfo_);
      std::swap(program_, other.program_);
      std::swap(heightmap_, other.heightmap_);
      std::swap(heightmaptbo_, other.heightmaptbo_);
      std::swap(complexMapCpu_, other.complexMapCpu_);
      std::swap(heightMapCpu_, other.heightMapCpu_);
      std::swap(timeLoc_, other.timeLoc_);
      std::swap(viewLoc_, other.viewLoc_);
      std::swap(projLoc_, other.projLoc_);
      std::swap(modelLoc_, other.modelLoc_);
      std::swap(fft_, other.fft_);
    }
  }

  ~fftwaterProgram()
  {
    glDeleteBuffers(1, &heightmaptbo_);
    glDeleteTextures(1, &heightmap_);
    glDeleteProgram(program_);

    heightmaptbo_ = 0;
    heightmap_ = 0;
    program_ = 0;
  }
};

fftContext fft{simInfo(g_infoPath)};
fftwaterProgram g_prog;

int main()
{
  glfwSetErrorCallback([](auto err, const auto* desc){ std::cout << "Error: " << desc << '\n'; });

  // glfw init
  if(!glfwInit())
  {
    std::cout << "glfw failed to initialize\n";
    std::exit(1);
  }

  // context init
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
  auto window = glfwCreateWindow(640, 480, "FFT Water", NULL, NULL);
  if (!window)
  {
    std::cout << "window/glcontext failed to initialize\n";
    std::exit(1);
  }

  glfwMakeContextCurrent(window);

  // glew init
  auto err = glewInit();
  if(GLEW_OK != err)
  {
    std::cout << "glew failed to init: " << glewGetErrorString(err) << '\n';
    std::exit(1);
  }

  // gl init
  glEnable(GL_DEBUG_OUTPUT);
  glDebugMessageCallback(msgCallback, 0);
  glEnable(GL_DEPTH_TEST);

  // fft program setup
  auto sim = simInfo{g_infoPath};
  g_prog = fftwaterProgram(sim, fft, g_vertexPath, g_fragmentPath);

  glfwSetKeyCallback(window, [](auto window, auto key, auto scancode, auto action, auto mods){
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
      glfwSetWindowShouldClose(window, GLFW_TRUE);
    if(key == GLFW_KEY_F4)
    {
      g_prog = fftwaterProgram(simInfo{g_infoPath}, fft, g_vertexPath, g_fragmentPath);
      std::cout << "f4 received\n";
    }

  });

  // pre loop declarations/actions
  int width, height;
  glm::mat4 view, proj;
  auto plane = constructPlane(sim);

  view = glm::lookAt(glm::vec3(0,7,0), glm::vec3(10,0,10), glm::vec3(0,1,0));

  glfwSwapInterval(1);
  while(!glfwWindowShouldClose(window))
  {
    glfwGetFramebufferSize(window, &width, &height);
    glViewport(0, 0, width, height);
    proj = glm::perspective(1.57f, float(width)/float(height), 0.1f, 7000.0f);

    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    g_prog.draw(proj, view, glm::mat4(), glfwGetTime(), plane);

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glfwTerminate();
  std::exit(0);
  return 0;
}