//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Fit/Kernel/MinimizerFactory.cpp
//! @brief     Implements class MinimizerFactory.
//!
//! @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 "Fit/Kernel/MinimizerFactory.h"
#include "Fit/Suite/GSLLevenbergMarquardtMinimizer.h"
#include "Fit/Suite/GSLMultiMinimizer.h"
#include "Fit/Suite/GeneticMinimizer.h"
#include "Fit/Suite/Minuit2Minimizer.h"
#include "Fit/Suite/SimAnMinimizer.h"
#include "Fit/Tool/MinimizerUtil.h"
#include <boost/format.hpp>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>

namespace {

class MinimizerCatalog {
public:
    MinimizerCatalog();

    std::string toString() const;

    std::vector<std::string> minimizerNames() const;

    std::vector<std::string> algorithmNames(const std::string& minimizerName) const;

    std::vector<std::string> algorithmDescriptions(const std::string& minimizerName) const;

    const MinimizerInfo& minimizerInfo(const std::string& minimizerName) const;

private:
    void addMinimizerInfo(MinimizerInfo&& info);
    std::vector<MinimizerInfo> m_minimizers;
};

MinimizerCatalog::MinimizerCatalog()
{
    addMinimizerInfo(MinimizerInfo::buildMinuit2Info());
    addMinimizerInfo(MinimizerInfo::buildGSLMultiMinInfo());
    addMinimizerInfo(MinimizerInfo::buildGSLLMAInfo());
    addMinimizerInfo(MinimizerInfo::buildGSLSimAnInfo());
    addMinimizerInfo(MinimizerInfo::buildGeneticInfo());
}

//! Returns multiline string representing catalog content.
std::string MinimizerCatalog::toString() const
{
    const int text_width = 80;
    std::ostringstream result;

    result << std::string(text_width, '-') << "\n";
    result << boost::format("%-15s|%-65s\n") % "Minimizer" % " Algorithms";
    result << std::string(text_width, '-') << "\n";

    for (const auto& info : m_minimizers) {
        result << boost::format("%-15s| %-64s\n") % info.name()
                      % mumufit::utils::toString(info.algorithmNames(), " ");
    }
    return result.str();
}

std::vector<std::string> MinimizerCatalog::minimizerNames() const
{
    std::vector<std::string> result;
    for (const auto& info : m_minimizers)
        result.push_back(info.name());

    return result;
}

//! Returns list of algorithms defined for the minimizer with a given name.
std::vector<std::string> MinimizerCatalog::algorithmNames(const std::string& minimizerName) const
{
    return minimizerInfo(minimizerName).algorithmNames();
}

//! Returns list of algorithm's descriptions for the minimizer with a given name    .
std::vector<std::string>
MinimizerCatalog::algorithmDescriptions(const std::string& minimizerName) const
{
    return minimizerInfo(minimizerName).algorithmDescriptions();
}

//! Returns info for minimizer with given name.
const MinimizerInfo& MinimizerCatalog::minimizerInfo(const std::string& minimizerName) const
{
    for (const auto& info : m_minimizers)
        if (info.name() == minimizerName)
            return info;

    throw std::runtime_error("MinimizerCatalog::minimizerInfo -> Error. "
                             "No minimizer with the name '"
                             + minimizerName + "'");
}

//! Adds minimizer info to the catalog.
void MinimizerCatalog::addMinimizerInfo(MinimizerInfo&& info)
{
    m_minimizers.emplace_back(std::move(info));
}

} // namespace


IMinimizer* MinimizerFactory::createMinimizer(const std::string& minimizerName,
                                              const std::string& algorithmType,
                                              const std::string& optionString)
{
    IMinimizer* result(nullptr);

    if (minimizerName == "Minuit2")
        result = new Minuit2Minimizer(algorithmType);

    else if (minimizerName == "GSLLMA")
        result = new GSLLevenbergMarquardtMinimizer();


    else if (minimizerName == "GSLSimAn")
        result = new SimAnMinimizer();


    else if (minimizerName == "GSLMultiMin")
        result = new GSLMultiMinimizer(algorithmType);


    else if (minimizerName == "Genetic")
        result = new GeneticMinimizer();


    if (!result) {
        std::ostringstream ostr;
        ostr << "MinimizerFactory::MinimizerFactory -> Error! Cannot create minimizer for given "
                "collection name '"
             << minimizerName << "' or algorithm '" << algorithmType << "'" << std::endl;
        ostr << "Possible names are:" << std::endl;

        static MinimizerCatalog c;
        ostr << c.toString();
        throw std::runtime_error(ostr.str());
    }

    if (!optionString.empty())
        result->setOptions(optionString);

    return result;
}

void MinimizerFactory::printCatalog()
{
    std::cout << catalogToString() << std::endl;
}

//! Returns multi-line string representing catalog content: minimizer names and list of their
//! algorithms.

std::string MinimizerFactory::catalogToString()
{
    static MinimizerCatalog c;
    return c.toString();
}

//! Returns multi-line string representing detailed catalog content:
//! minimizer names, list of their algorithms and description, list of minimizer options.

std::string MinimizerFactory::catalogDetailsToString()
{
    static MinimizerCatalog c;

    const int text_width = 80;
    std::ostringstream result;
    const std::string fmt("%-20s| %-65s\n");

    for (const auto& minimizerName : c.minimizerNames()) {
        // general info
        const MinimizerInfo& info = c.minimizerInfo(minimizerName);
        result << std::string(text_width, '-') << "\n";
        result << boost::format(fmt) % info.name() % info.description();
        result << std::string(text_width, '-') << "\n";

        // algorithm names and description
        result << "\nAlgorithm names\n";
        auto algorithmNames = info.algorithmNames();
        auto algorithmDescription = info.algorithmDescriptions();
        for (size_t i = 0; i < algorithmNames.size(); ++i)
            result << boost::format(fmt) % algorithmNames[i] % algorithmDescription[i];
        if (algorithmNames.size() > 1)
            result << boost::format(fmt) % "Default algorithm" % info.algorithmName();

        // list of minimizer options
        std::unique_ptr<IMinimizer> minimizer(createMinimizer(minimizerName));
        if (auto* rootMinimizer = dynamic_cast<MinimizerAdapter*>(minimizer.get())) {
            result << "\nOptions\n";
            for (const auto& option : rootMinimizer->options()) {
                std::ostringstream opt;
                opt << std::setw(5) << std::left << option->value_str() << option->description();
                result << boost::format(fmt) % option->name() % opt.str();
            }
        }

        result << "\n";
    }

    return result.str();
}

std::vector<std::string> MinimizerFactory::algorithmNames(const std::string& minimizerName)
{
    static MinimizerCatalog c;
    return c.algorithmNames(minimizerName);
}

std::vector<std::string> MinimizerFactory::algorithmDescriptions(const std::string& minimizerName)
{
    static MinimizerCatalog c;
    return c.algorithmDescriptions(minimizerName);
}
