//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Tests/Functional/Fitting/FittingTests.cpp
//! @brief     Defines classes from FitObjectiveTest family.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Base/Axis/Scale.h"
#include "Base/Const/Units.h"
#include "Device/Beam/Beam.h"
#include "Device/Data/Datafield.h"
#include "Device/Detector/SphericalDetector.h"
#include "Device/Mask/Rectangle.h"
#include "Fit/Kernel/Minimizer.h"
#include "Sample/Multilayer/Sample.h"
#include "Sample/StandardSample/ExemplarySamples.h"
#include "Sim/Fitting/FitObjective.h"
#include "Sim/Simulation/OffspecSimulation.h"
#include "Sim/Simulation/ScatteringSimulation.h"
#include "Sim/Simulation/SpecularSimulation.h"
#include "Tests/Functional/Fitting/SimfitTestPlan.h"
#include "Tests/GTestWrapper/google_test.h"
#include "Tests/SimFactory/MakeSimulations.h"

using mumufit::Parameter;
using Units::deg;
using Units::nm;

bool runFitTest(const std::string& minimizer_name, const std::string& algorithm_name,
                const SimfitTestPlan& plan, const std::string& options = "")
{
    mumufit::Minimizer minimizer;
    minimizer.setMinimizer(minimizer_name, algorithm_name, options);
    return plan.checkMinimizer(minimizer);
}

//  ************************************************************************************************
//  sample builders
//  ************************************************************************************************

const auto build_CylBA = [](const mumufit::Parameters& params) -> Sample* {
    return ExemplarySamples::createCylindersInBA(params["height"].value(),
                                                 params["radius"].value());
};

const auto build_AlternatingLayers = [](const mumufit::Parameters& params) -> Sample* {
    return ExemplarySamples::createPlainMultiLayerBySLD(10, params["ti_thickness"].value());
};

const auto build_Resonator = [](const mumufit::Parameters& params) -> Sample* {
    return ExemplarySamples::createResonator(params["ti_thickness"].value());
};

//  ************************************************************************************************
//  simulation builders
//  ************************************************************************************************

const auto build_CylBA_MiniGISAS =
    [](const mumufit::Parameters& params) -> std::unique_ptr<ISimulation> {
    return test::makeSimulation::MiniGISAS(*build_CylBA(params));
};

const auto build_CylBA_Masked =
    [](const mumufit::Parameters& params) -> std::unique_ptr<ISimulation> {
    Beam beam(Beam(1, 1.0 * Units::angstrom, 0.2 * deg));

    SphericalDetector detector(20, -1.2 * deg, 1.2 * deg, 18, -1 * deg, 1 * deg);
    // TODO restore detector.setRegionOfInterest(5.0, 6.0, 15.0, 12.0);
    detector.addMask(Rectangle(0.0, 0.0, 2.0, 2.0), true);

    std::unique_ptr<ScatteringSimulation> result(
        new ScatteringSimulation(beam, *build_CylBA(params), detector));

    return result;
};

const auto build_AlternatingLayers_Specular =
    [](const mumufit::Parameters& params) -> std::unique_ptr<ISimulation> {
    return test::makeSimulation::BasicSpecular(*build_AlternatingLayers(params), false);
};

const auto build_AlternatingLayers_SpecularQ =
    [](const mumufit::Parameters& params) -> std::unique_ptr<ISimulation> {
    return test::makeSimulation::BasicSpecular(*build_AlternatingLayers(params), true);
};

const auto build_Resonator_Offspec =
    [](const mumufit::Parameters& params) -> std::unique_ptr<ISimulation> {
    return test::makeSimulation::MiniOffspec(*build_Resonator(params));
};

//  ************************************************************************************************
//  test plans
//  ************************************************************************************************

//! Large tolerance on expected parameter values to help stocastic minimizers to converge fatser.
const SimfitTestPlan cylindersInBAEasyPlan(
    build_CylBA_MiniGISAS, false,
    {{Parameter("height", 4.5 * nm, AttLimits::limited(4.0, 6.0), 0.1), 5.0 * nm, 0.1},
     {Parameter("radius", 5.5 * nm, AttLimits::limited(4.0, 6.0), 0.1), 5.0 * nm, 0.1}});

const SimfitTestPlan
    cylindersInBAResidualPlan(build_CylBA_MiniGISAS, true,
                              {{Parameter("height", 4.5 * nm, AttLimits::limitless()), 5.0 * nm},
                               {Parameter("radius", 5.5 * nm, AttLimits::limitless()), 5.0 * nm}});

const SimfitTestPlan specularPlan(
    build_AlternatingLayers_Specular, false,
    {{Parameter("ti_thickness", 5.0 * nm, AttLimits::limited(1.0 * nm, 7.0 * nm), 0.1), 3.0 * nm}});

const SimfitTestPlan specularPlanQ(
    build_AlternatingLayers_SpecularQ, false,
    {{Parameter("ti_thickness", 5.0 * nm, AttLimits::limited(1.0 * nm, 7.0 * nm), 0.1), 3.0 * nm}});

const SimfitTestPlan offspecPlan(build_Resonator_Offspec, false,
                                 {{Parameter("ti_thickness", 12.0 * nm,
                                             AttLimits::limited(11.5 * nm, 14.0 * nm), 0.1 * nm),
                                   13.0 * nm}});

class MultipleSpecPlan : public SimfitTestPlan {
public:
    MultipleSpecPlan(const simulation_builder_t& sim_builder, bool residual_based,
                     const std::vector<ParameterReference>& parameter_references)
        : SimfitTestPlan(sim_builder, residual_based, parameter_references)
    {
    }
    std::unique_ptr<FitObjective> createFitObjective() const override
    {
        std::unique_ptr<FitObjective> result(new FitObjective);
        result->addFitPair(m_sim_builder, *fakeData(), 0.5);
        result->addFitPair(m_sim_builder, *fakeData(), 0.5);
        result->initPrint(1);
        return result;
    }
};

const MultipleSpecPlan multispecPlan(
    build_AlternatingLayers_SpecularQ, false,
    {{Parameter("ti_thickness", 5.0 * nm, AttLimits::limited(1.0 * nm, 7.0 * nm), 0.1), 3.0 * nm}});

//  ************************************************************************************************
//  tests
//  ************************************************************************************************

TEST(Fitting, MigradCylindersInBA)
{
    SimfitTestPlan plan(
        build_CylBA_MiniGISAS, false,
        {{Parameter("height", 4.5 * nm, AttLimits::lowerLimited(0.01), 0.01), 5.0 * nm},
         {Parameter("radius", 5.5 * nm, AttLimits::lowerLimited(0.01), 0.01), 5.0 * nm}});
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", plan));
}

TEST(Fitting, MigradResidualCylindersInBA)
{
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", cylindersInBAResidualPlan));
}

TEST(Fitting, BfgsCylindersInBA)
{
    EXPECT_TRUE(runFitTest("GSLMultiMin", "BFGS2", cylindersInBAEasyPlan));
}

TEST(Fitting, SteepestDescentCylindersInBA)
{
    EXPECT_TRUE(runFitTest("GSLMultiMin", "SteepestDescent", cylindersInBAEasyPlan));
}

TEST(Fitting, FumuliCylindersInBA)
{
    EXPECT_TRUE(runFitTest("Minuit2", "Fumili", cylindersInBAResidualPlan));
}

TEST(Fitting, LevenbergMarquardtCylindersInBA)
{
    EXPECT_TRUE(runFitTest("GSLLMA", "", cylindersInBAResidualPlan));
}

TEST(Fitting, SimAnCylindersInBA)
{
    EXPECT_TRUE(runFitTest("GSLSimAn", "", cylindersInBAEasyPlan,
                           "IterationsAtTemp=5;MaxIterations=10;t_min=1.0"));
}

TEST(Fitting, GeneticCylindersInBA)
{
    EXPECT_TRUE(runFitTest("Genetic", "", cylindersInBAEasyPlan, "MaxIterations=1;RandomSeed=1"));
}

TEST(Fitting, RectDetectorFit)
{
    SimfitTestPlan plan(
        build_CylBA_Masked, false,
        {{Parameter("height", 4.5 * nm, AttLimits::limited(4.0, 6.0), 0.01), 5.0 * nm},
         {Parameter("radius", 5.5 * nm, AttLimits::limited(4.0, 6.0), 0.01), 5.0 * nm}});
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", plan, "Strategy=2"));
}

TEST(Fitting, SpecularFit)
{
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", specularPlan));
}

TEST(Fitting, SpecularFitTestQ)
{
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", specularPlanQ));
}

TEST(Fitting, MultipleSpecFitting)
{
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", multispecPlan));
}

TEST(Fitting, OffspecFit)
{
    EXPECT_TRUE(runFitTest("Minuit2", "Migrad", offspecPlan));
}
