//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Sim/Fitting/ObjectiveMetricUtil.cpp
//! @brief     Implements ObjectiveMetric utilities.
//!
//! @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 "Sim/Fitting/ObjectiveMetricUtil.h"
#include "Sim/Fitting/ObjectiveMetric.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <sstream>

namespace {

const std::function<double(double)> l1_norm = [](double term) { return std::abs(term); };
const std::function<double(double)> l2_norm = [](double term) { return term * term; };

const std::map<std::string, std::function<std::unique_ptr<ObjectiveMetric>()>> metric_factory = {
    {"chi2", [] { return std::make_unique<Chi2Metric>(); }},
    {"poisson-like", [] { return std::make_unique<PoissonLikeMetric>(); }},
    {"log", [] { return std::make_unique<LogMetric>(); }},
    {"reldiff", [] { return std::make_unique<meanRelativeDifferenceMetric>(); }}};
// TODO restore rq4:
// https://jugit.fz-juelich.de/mlz/bornagain/-/issues/568
// {"rq4", [] { return std::make_unique<RQ4Metric>(); }}
const std::string default_metric_name = "log";

const std::map<std::string, std::function<double(double)>> norm_factory = {{"l1", l1_norm},
                                                                           {"l2", l2_norm}};
const std::string default_norm_name = "l2";

template <typename U> std::vector<std::string> keys(const std::map<std::string, U>& map)
{
    std::vector<std::string> result;
    result.reserve(map.size());
    for (auto& item : map)
        result.push_back(item.first);
    return result;
}

} // namespace

std::function<double(double)> ObjectiveMetricUtil::l1Norm()
{
    return l1_norm;
}

std::function<double(double)> ObjectiveMetricUtil::l2Norm()
{
    return l2_norm;
}

std::unique_ptr<ObjectiveMetric> ObjectiveMetricUtil::createMetric(const std::string& metric)
{
    return createMetric(metric, defaultNormName());
}

std::unique_ptr<ObjectiveMetric> ObjectiveMetricUtil::createMetric(std::string metric,
                                                                   std::string norm)
{
    std::transform(metric.begin(), metric.end(), metric.begin(), ::tolower);
    std::transform(norm.begin(), norm.end(), norm.begin(), ::tolower);
    const auto metric_iter = metric_factory.find(metric);
    const auto norm_iter = norm_factory.find(norm);
    if (metric_iter == metric_factory.end() || norm_iter == norm_factory.end()) {
        std::stringstream ss;
        ss << "Error in ObjectiveMetricUtil::createMetric: either metric (" << metric
           << ") or norm (" << norm << ") name is unknown.\n";
        ss << availableMetricOptions();
        throw std::runtime_error(ss.str());
    }

    auto result = metric_iter->second();
    result->setNorm(norm_iter->second);
    return result;
}

std::string ObjectiveMetricUtil::availableMetricOptions()
{
    std::stringstream ss;
    ss << "Available metrics:\n";
    for (auto& item : metricNames())
        ss << "\t" << item << "\n";
    ss << "default metric: " << defaultMetricName() << "\n";
    ss << "Available norms:\n";
    for (auto& item : normNames())
        ss << "\t" << item << "\n";
    ss << "default norm: " << defaultNormName() << "\n";
    return ss.str();
}

std::vector<std::string> ObjectiveMetricUtil::normNames()
{
    return keys(norm_factory);
}

std::vector<std::string> ObjectiveMetricUtil::metricNames()
{
    return keys(metric_factory);
}

std::string ObjectiveMetricUtil::defaultNormName()
{
    return default_norm_name;
}

std::string ObjectiveMetricUtil::defaultMetricName()
{
    return default_metric_name;
}
