//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Base/Py/PyFmt.cpp
//! @brief     Implements functions from namespace pyfmt.
//!
//! @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/Py/PyFmt.h"
#include "Base/Const/Units.h" // printDegrees
#include "Base/Math/Numeric.h"
#include "Base/Util/Assert.h"
#include "Base/Util/StringUtil.h"
#include <iomanip>
#include <numbers>

using std::numbers::pi;

std::string Py::Fmt::printImportedSymbols(const std::string& code)
{
    std::vector<std::string> to_declare;
    for (const std::string key : {"deg"})
        if (code.find("*" + key) != std::string::npos)
            to_declare.push_back(key);
    for (const std::string key : {"angstrom", "nm", "nm2", "nm3", "micrometer"})
        if (code.find("*" + key) != std::string::npos || code.find("/" + key) != std::string::npos)
            to_declare.push_back(key);
    for (const std::string key : {"R3"})
        if (code.find(key) != std::string::npos)
            to_declare.push_back(key);
    return "from bornagain import " + Base::String::join(to_declare, ", ") + "\n";
}

std::string Py::Fmt::printInt(int value)
{
    return std::to_string(value);
}

std::string Py::Fmt::printBool(double value)
{
    return value ? "True" : "False";
}

std::string Py::Fmt::printDouble(double input)
{
    std::ostringstream inter;
    inter << std::setprecision(15);
    if (std::abs(input) < std::numeric_limits<double>::epsilon()) {
        inter << "0.0";
        return inter.str();
    }
    inter << input;
    if (inter.str().find('e') == std::string::npos && inter.str().find('.') == std::string::npos)
        inter << ".0";
    return inter.str();
}

//! prints double as an integer, if possible within standard accuracy
std::string Py::Fmt::printLightDouble(double input)
{
    std::ostringstream inter;
    int ival = std::lround(input);
    if (std::abs(input - ival) < 1e-15)
        inter << ival;
    else {
        inter << std::setprecision(15);
        if (std::abs(input) < std::numeric_limits<double>::epsilon())
            return "0.0";
        inter << input;
        if (inter.str().find('e') == std::string::npos
            && inter.str().find('.') == std::string::npos)
            inter << ".0";
    }
    return inter.str();
}

std::string Py::Fmt::printNm(double input, int pow)
{
    std::ostringstream inter;
    inter << printLightDouble(input);

    if (pow > 0)
        inter << "*nm";
    else if (pow < 0)
        inter << "/nm";

    if (std::abs(pow) >= 2)
        inter << printInt(std::abs(pow));

    return inter.str();
}

// 1.000000e7 -> 1e7
std::string Py::Fmt::printScientificDouble(double input)
{
    std::ostringstream inter;
    inter << std::scientific;
    inter << input;

    std::string::size_type pos = inter.str().find('e');
    if (pos == std::string::npos)
        return inter.str();

    std::string part1 = inter.str().substr(0, pos);
    std::string part2 = inter.str().substr(pos, std::string::npos);

    part1.erase(part1.find_last_not_of('0') + 1, std::string::npos);
    if (part1.back() == '.')
        part1 += "0";

    return part1 + part2;
}

std::string Py::Fmt::printDegrees(double input)
{
    std::ostringstream inter;
    inter << printLightDouble(Units::rad2deg(input)) << "*deg";
    return inter.str();
}

std::string Py::Fmt::printValue(double value, const std::string& units)
{
    if (units == "rad")
        return printDegrees(value);
    if (units == "nm")
        return printNm(value, 1);
    if (units == "nm^2")
        return printNm(value, 2);
    if (units == "nm^3")
        return printNm(value, 3);
    if (units == "1/nm")
        return printNm(value, -1);
    if (units.empty())
        return printDouble(value);
    ASSERT_NEVER;
}

std::string Py::Fmt::printValue(std::variant<double, int> value, const std::string& units /*= ""*/)
{
    if (std::holds_alternative<int>(value)) {
        ASSERT(units.empty()); // int with units is not supported. Implement when necessary.
        return printInt(std::get<int>(value));
    }

    return printValue(std::get<double>(value), units);
}

std::string Py::Fmt::printString(const std::string& value)
{
    std::ostringstream result;
    result << "\"" << value << "\"";
    return result.str();
}

std::string Py::Fmt::printArguments(const std::vector<std::pair<double, std::string>>& arguments)
{
    std::vector<std::string> args;
    args.reserve(arguments.size());
    for (const auto& argument : arguments)
        args.push_back(Py::Fmt::printValue(argument.first, argument.second));
    return Base::String::join(args, ", ");
}

std::string Py::Fmt::printFunction(const std::string& name,
                                   const std::vector<std::pair<double, std::string>>& arguments)
{
    return name + "(" + printArguments(arguments) + ")";
}

std::string Py::Fmt::printFunction(const std::string& name, double value, const std::string& unit)
{
    return printFunction(name, {{value, unit}});
}

std::string Py::Fmt::printFunction(const std::string& name, double value1, const std::string& unit1,
                                   double value2, const std::string& unit2)
{
    return printFunction(name, {{value1, unit1}, {value2, unit2}});
}

std::string Py::Fmt::printKvector(const R3 value)
{
    std::ostringstream result;
    result << "R3(" << printDouble(value.x()) << ", " << printDouble(value.y()) << ", "
           << printDouble(value.z()) << ")";
    return result.str();
}

std::string Py::Fmt::indent(size_t width)
{
    return std::string(width, ' ');
}
