//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Base/Util/StringUtil.cpp
//! @brief     Implements a few helper functions.
//!
//! @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/Util/StringUtil.h"
#include "Base/Util/Assert.h"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <charconv>
#include <chrono>
#include <iomanip>
#include <iterator>
#include <regex>
#ifdef BA_CPP20
#include <ranges>
#endif // BA_CPP20
#include <sstream>

//! Returns token vector obtained by splitting string at delimiters.
std::vector<std::string> Base::String::split(const std::string& text, const std::string& delimiter)
{
    if (text.empty())
        return {};
    std::vector<std::string> result;
    size_t pos = 0;
    while (pos != std::string::npos) {
        size_t next_pos = text.find(delimiter, pos);
        if (next_pos == std::string::npos) {
            result.push_back(text.substr(pos));
            break;
        }
        result.push_back(text.substr(pos, next_pos - pos));
        pos = next_pos + delimiter.length();
    }
    return result;
}

void Base::String::replaceItemsFromString(std::string& text, const std::vector<std::string>& items,
                                          const std::string& replacement)
{
    for (const auto& item : items) {
        size_t pos = 0;
        while ((pos = text.find(item, pos)) != std::string::npos) {
            text.replace(pos, item.length(), replacement);
            pos += replacement.length();
        }
    }
}

std::string Base::String::join(const std::vector<std::string>& joinable, const std::string& joint)
{
    std::string result;
    size_t n = joinable.size();
    if (n == 0)
        return result;
    for (size_t i = 0; i < n - 1; ++i)
        result += joinable[i] + joint;
    result += joinable[n - 1];
    return result;
}

std::string Base::String::to_lower(std::string text)
{

#ifdef BA_CPP20
    std::ranges::transform(text, text.begin(), [](unsigned char c) { return std::tolower(c); });
#else
    std::transform(text.begin(), text.end(), text.begin(),
                   [](unsigned char c) { return std::tolower(c); });
#endif // BA_CPP20

    return text;
}

bool Base::String::to_int(const std::string& str, int* result)
{
    const char* first = str.data() + str.find_first_not_of(' ');
    const char* last = str.data() + str.size();
    int _result = 0;
    auto [p, ec] = std::from_chars(first, last, _result);

    if (ec != std::errc())
        return false;

    if (p != last) {
        // not all was consumed. Check whether only space characters left
        const size_t pos = p - str.data();
        const auto hasNonSpaceLeft = str.find_first_not_of(' ', pos) != std::string::npos;
        if (hasNonSpaceLeft)
            return false;
    }

    if (result != nullptr)
        *result = _result;
    return true;
}

bool Base::String::to_double(const std::string& str, double* result)
{
    errno = 0;
    char* end{};
    const char* p = str.c_str();
    *result = std::strtod(p, &end);
    if (end == p) // no digits found
        return false;
    if (errno != 0) {
        errno = 0;
        return false;
    }
    return true;
}

std::string Base::String::trim(const std::string& str, const std::string& whitespace)
{
    const auto strBegin = str.find_first_not_of(whitespace);

    if (strBegin == std::string::npos)
        return "";

    const auto strEnd = str.find_last_not_of(whitespace);
    const auto strRange = strEnd - strBegin + 1;

    return str.substr(strBegin, strRange);
}

std::string Base::String::trimFront(const std::string& str, const std::string& whitespace)
{
    const auto strBegin = str.find_first_not_of(whitespace);

    if (strBegin == std::string::npos)
        return "";

    return str.substr(strBegin);
}

bool Base::String::startsWith(const std::string& str, const std::string& substr)
{
    return str.rfind(substr, 0) == 0;
}

std::vector<int> Base::String::expandNumberList(const std::string& pattern)
{
    std::vector<int> result;

    for (const std::string& word : split(trim(pattern), ",")) {

        std::vector<std::string> parts = split(trim(word), "-");
        if (parts.empty())
            throw std::runtime_error("invalid number list");
        if (parts.size() > 2)
            throw std::runtime_error("invalid number list");
        int i0;
        bool ok = to_int(parts[0], &i0);
        if (!ok)
            throw std::runtime_error("invalid number list");
        if (parts.size() == 1)
            result.push_back(i0);
        else {
            ASSERT(parts.size() == 2);
            int i1;
            ok = to_int(parts[1], &i1);
            if (!ok)
                throw std::runtime_error("invalid number list");
            for (int j = i0; j <= i1; ++j)
                result.push_back(j);
        }
    }

    return result;
}

std::vector<double> Base::String::parse_doubles(const std::string& str)
{
    std::vector<double> result;
    std::istringstream iss(str);
    iss.imbue(std::locale::classic());
    std::copy(std::istream_iterator<double>(iss), std::istream_iterator<double>(),
              back_inserter(result));
    if (result.empty()) {
        std::string out = str;
        if (out.size() > 10) {
            out.resize(10, ' ');
            out += " ...";
        }
        throw std::runtime_error("Found '" + out + "' while expecting a floating-point number");
    }
    return result;
}
