/* -*- mode: c++ -*-
 * Copyright 2025 Dracal Technologies Inc. All rights reserved.
 */

#ifdef __clang__
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif

#include <dracal/common/fs.hpp>
#include <dracal/common/log.hpp>
#include <dracal/common/platform.hpp>
#include <dracal/common/string.hpp>

#include <algorithm>
#include <cctype>
#include <codecvt>
#include <locale>
#include <random>
#include <ranges>
#include <string>

#if defined(DRACAL_HAS_SPAN)
    #include <span>
#endif

namespace dracal::str {

bool starts_with(const std::string &str, const std::string &prefix) noexcept {
    const size_t result = str.rfind(prefix, 0);
    return result != std::string::npos;
}

bool contains(const std::string &str, const std::string &substring) noexcept {
    const size_t result = str.find(substring);
    return result != std::string::npos;
}

bool contains(const std::string &str, const std::vector<std::string> &substrings) noexcept {
    if (substrings.empty()) {
        return true;
    }

    return std::any_of(substrings.cbegin(), substrings.cend(),
                       [&](const std::string &substr) -> bool { return contains(str, substr); });
}

bool ends_with(const std::string &str, const std::string &postfix) noexcept {
    const size_t result = str.find(postfix, str.size() - postfix.size());
    return result != std::string::npos;
}

std::string to_lower(std::string str) {
    std::transform(str.begin(), str.end(), str.begin(), [](const unsigned char c) { return std::tolower(c); });
    return str;
}

std::string to_lower(std::string str, const int pos) {
    auto index = static_cast<unsigned int>(pos);

    if (str.size() <= index) {
        return str;
    }

    str[index] = static_cast<char>(std::tolower(static_cast<unsigned char>(str[index])));
    return str;
}

std::string to_lower(std::string str, const unsigned int pos) {
    if (str.size() <= pos) {
        return str;
    }

    str[pos] = static_cast<char>(std::tolower(static_cast<unsigned char>(str[pos])));
    return str;
}

std::string to_upper(std::string str) {
    std::transform(str.begin(), str.end(), str.begin(), [](const unsigned char c) { return std::toupper(c); });
    return str;
}

std::string to_upper(std::string str, const int pos) {
    auto index = static_cast<unsigned int>(pos);

    if (str.size() <= index) {
        return str;
    }

    str[index] = static_cast<char>(std::toupper(static_cast<unsigned char>(str[index])));
    return str;
}

std::string to_upper(std::string str, const unsigned int pos) {
    if (str.size() <= pos) {
        return str;
    }

    str[pos] = static_cast<char>(std::toupper(static_cast<unsigned char>(str[pos])));
    return str;
}

bool is_alpha(const std::string &str) {
    if (str.empty()) {
        return false;
    }
    return std::all_of(str.begin(), str.end(), [](const unsigned char c) { return std::isalpha(c); });
}

bool is_alpha(const std::string &str, const int pos) {
    const auto index = static_cast<unsigned int>(pos);

    if (str.size() <= index) {
        return false;
    }

    return std::isalpha(static_cast<unsigned char>(str[index]));
}

bool is_alpha(const std::string &str, const unsigned int pos) {
    if (str.size() <= pos) {
        return false;
    }

    return std::isalpha(static_cast<unsigned char>(str[pos]));
}

bool is_numeric(const std::string &str) {
    if (str.empty()) {
        return false;
    }
    return std::all_of(str.begin(), str.end(), [](const unsigned char c) { return std::isdigit(c); });
}

bool is_numeric(const std::string &str, const int pos) {
    const auto index = static_cast<unsigned int>(pos);

    if (str.size() <= index) {
        return false;
    }

    return std::isdigit(static_cast<unsigned char>(str[index]));
}

bool is_numeric(const std::string &str, const unsigned int pos) {
    if (str.size() <= pos) {
        return false;
    }

    return std::isdigit(static_cast<unsigned char>(str[pos]));
}

bool is_alpha_numeric(const std::string &str) {
    if (str.empty()) {
        return false;
    }
    return std::all_of(str.begin(), str.end(), [](const unsigned char c) { return std::isalnum(c); });
}

bool is_alpha_numeric(const std::string &str, const int pos) {
    const auto index = static_cast<unsigned int>(pos);

    if (str.size() <= index) {
        return false;
    }

    return std::isalnum(static_cast<unsigned char>(str[index]));
}

bool is_alpha_numeric(const std::string &str, const unsigned int pos) {
    if (str.size() <= pos) {
        return false;
    }

    return std::isalnum(static_cast<unsigned char>(str[pos]));
}

std::string replace(const std::string &str, const std::string &from, const std::string &to) {
    if (from.empty()) {
        return str;
    }

    size_t pos = 0;
    std::string result = str;

    while ((pos = result.find(from, pos)) != std::string::npos) {
        result.replace(pos, from.length(), to);
        pos += to.length();
    }

    return result;
}

void trim_left_inplace(std::string &str) {
    str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](const unsigned char ch) -> bool {
                  return !static_cast<bool>(std::isspace(ch));
              }));
}

void trim_right_inplace(std::string &str) {
    str.erase(std::find_if(str.rbegin(), str.rend(),
                           [](const unsigned char ch) -> bool { return !static_cast<bool>(std::isspace(ch)); })
                  .base(),
              str.end());
}

void trim_inplace(std::string &str) {
    trim_left_inplace(str);
    trim_right_inplace(str);
}

std::string trim_left(const std::string &str) {
    std::string temp = str;
    trim_left_inplace(temp);
    return temp;
}

void trim_prefix_inplace(std::string &str, const std::string &prefix) {
    if (prefix.empty()) {
        return;
    }

    if (!starts_with(str, prefix)) {
        return;
    }

    str.erase(0, prefix.size());
}

void trim_before_prefix_inplace(std::string &str, const std::string &prefix) {
    if (prefix.empty()) {
        return;
    }

    if (!contains(str, prefix)) {
        return;
    }

    const size_t pos = str.find(prefix, 0);

    if (pos < 1) {
        return;
    }

    str.erase(0, pos);
}

void trim_postfix_inplace(std::string &str, const std::string &postfix) {
    if (postfix.empty()) {
        return;
    }

    if (!ends_with(str, postfix)) {
        return;
    }

    str.erase(str.size() - postfix.size(), postfix.size());
}

void erase_null_terminators_inplace(std::string &str) {
    str.erase(std::remove_if(str.begin(), str.end(), [](const unsigned char x) { return x == '\0'; }), str.end());
}

std::string trim_right(const std::string &str) {
    std::string temp = str;
    trim_right_inplace(temp);
    return temp;
}

std::string trim(const std::string &str) {
    std::string temp = str;
    trim_inplace(temp);
    return temp;
}

std::string trim_prefix(const std::string &str, const std::string &prefix) {
    std::string temp = str;
    trim_prefix_inplace(temp, prefix);
    return temp;
}

std::string trim_before_prefix(const std::string &str, const std::string &prefix) {
    std::string temp = str;
    trim_before_prefix_inplace(temp, prefix);
    return temp;
}

std::string trim_postfix(const std::string &str, const std::string &postfix) {
    std::string temp = str;
    trim_postfix_inplace(temp, postfix);
    return temp;
}

std::string trim_extension(const std::string &filename, const bool keep_path) {
    const auto tmp = fs::path(filename);

    if (!keep_path) {
        return trim_postfix(tmp.filename().string(), tmp.extension().string());
    }

    return trim_postfix(filename, tmp.extension().string());
}

std::string trim_all_after_first(const std::string &str, const std::string &delimiter) {
    if (str.empty()) {
        return str;
    }

    if (delimiter.empty()) {
        return str;
    }

    const size_t pos = str.find(delimiter);
    if (pos == std::string::npos) {
        return str;
    }

    return str.substr(0, pos + delimiter.size());
}

std::string trim_all_before_last(const std::string &str, const std::string &delimiter) {
    if (str.empty()) {
        return str;
    }

    if (delimiter.empty()) {
        return str;
    }

    std::string trimmed_path = str;
    const size_t pos = trimmed_path.rfind(delimiter); // Use rfind instead of find
    if (pos != std::string::npos) {
        trimmed_path = trimmed_path.substr(pos + delimiter.size());
    }
    return trimmed_path;
}

std::string erase_null_terminators(std::string str) {
    erase_null_terminators_inplace(str);
    return str;
}

std::string prepend_prefix(const std::string &str, const std::string &prefix, const bool only_if_missing) {
    if (prefix.empty()) {
        return str;
    }

    if (!only_if_missing) {
        return prefix + str;
    }

    if (starts_with(str, prefix)) {
        return str;
    }

    return prefix + str;
}

std::string append_postfix(const std::string &str, const std::string &postfix, const bool only_if_missing) {
    if (postfix.empty()) {
        return str;
    }

    if (!only_if_missing) {
        return str + postfix;
    }

    if (ends_with(str, postfix)) {
        return str;
    }

    return str + postfix;
}

std::vector<std::string> split(const std::string &str, const std::string &delims, const bool keep_empty_parts) {
    if (str.empty()) {
        return {};
    }

    if (delims.empty()) {
        return {str};
    }

    size_t start = 0;
    size_t end = 0;
    std::string token;
    std::vector<std::string> result;

    while ((end = str.find(delims, start)) != std::string::npos) {
        if (start != end) {
            token = str.substr(start, end - start);
            result.push_back(token);
        } else if (keep_empty_parts) {
            result.push_back("");
        }

        start = end + delims.length();
    }

    const std::string last = str.substr(start);

    if (last.empty() && keep_empty_parts) {
        result.push_back("");
    }

    if (!last.empty()) {
        result.push_back(last);
    }

    return result;
}

std::string join(const std::vector<std::string> &list, const std::string &delims, const bool add_delims_at_end) {
    return join_from(list, delims, 0, add_delims_at_end);
}

std::string join_from(const std::vector<std::string> &list, const std::string &delims, unsigned int start,
                      const bool add_delims_at_end) {
    if (start >= list.size()) {
        return "";
    }

    auto str = fmt::format("{}", fmt::join(list.begin() + start, list.end(), delims));

    if (add_delims_at_end) {
        str += delims;
    }

    return str;
}

#if defined(DRACAL_WINDOWS)
std::string convert(const std::wstring &wstr) {
    // TL;DR Marked as deprecated in C++17 but it will be kept until C++26.
    // https://stackoverflow.com/questions/4804298/how-to-convert-wstring-into-string
    // https://stackoverflow.com/questions/2573834/c-convert-string-or-char-to-wstring-or-wchar-t/18597384#18597384
    // https://en.cppreference.com/w/cpp/locale/wstring_convert : std::wstring_convert assumes ownership of the
    // conversion facet, and cannot use a facet managed by a locale.
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    return converter.to_bytes(wstr);
}

std::wstring convert(const std::string &str) {
    // TL;DR Marked as deprecated in C++17 but it will be kept until C++26.
    // https://stackoverflow.com/questions/4804298/how-to-convert-wstring-into-string
    // https://stackoverflow.com/questions/2573834/c-convert-string-or-char-to-wstring-or-wchar-t/18597384#18597384
    // https://en.cppreference.com/w/cpp/locale/wstring_convert : std::wstring_convert assumes ownership of the
    // conversion facet, and cannot use a facet managed by a locale.
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
    return converter.from_bytes(str);
}
#endif

#if defined(DRACAL_HAS_SPAN)
std::string convect_utf16le_to_utf8(const std::span<const unsigned char> &data) {
    if (data.size() % 2 != 0) {
        dracal_error("Invalid utf16le data. The size({}) must be even.", data.size());
        return {};
    }

    std::u16string utf16str;
    utf16str.reserve(data.size() / 2);

    std::ranges::for_each(std::views::iota(size_t{0}, data.size() - 1) |
                              std::views::filter([](const size_t i) { return i % 2 == 0; }),
                          [&data, &utf16str](const auto i) {
                              const char16_t c = data[i] | (data[i + 1] << 8);
                              utf16str.push_back(c);
                          });

    // TL;DR Marked as deprecated in C++17 but it will be kept until C++26.
    // https://stackoverflow.com/questions/4804298/how-to-convert-wstring-into-string
    // https://stackoverflow.com/questions/2573834/c-convert-string-or-char-to-wstring-or-wchar-t/18597384#18597384
    // https://en.cppreference.com/w/cpp/locale/wstring_convert : std::wstring_convert assumes ownership of the
    // conversion facet, and cannot use a facet managed by a locale.
    try {
        std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
        return convert.to_bytes(utf16str);
    } catch (const std::range_error &error) {
        dracal_warn("Failed to convert UTF-16LE to UTF-8: {}", error.what());
        return {};
    } catch (...) {
        dracal_warn("Unexpected exception during UTF-16LE to UTF-8 conversion");
        return {};
    }
}
#endif

std::string extract_executable_name(const char *raw_executable_name) {
    return str::trim_extension(fs::path(raw_executable_name).filename().string());
}

std::string uuid_v4() {
    static std::random_device rd;
    static std::mt19937_64 gen(rd());
    static std::uniform_int_distribution<uint64_t> dis;
    const uint64_t random1 = dis(gen);
    const uint64_t random2 = dis(gen);

    const uint32_t time_low = random1 & 0xFFFFFFFF;
    const uint16_t time_mid = (random1 >> 32) & 0xFFFF;
    const uint16_t time_hi_version = ((random1 >> 48) & 0x0FFF) | 0x4000;
    const uint8_t clk_seq_hi = ((random2 >> 8) & 0x3F) | 0x80;
    const uint8_t clk_seq_low = random2 & 0xFF;
    const uint64_t node = (random2 >> 16) & 0xFFFFFFFFFFFF;

    return fmt::format("{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{:012x}", time_low, time_mid, time_hi_version, clk_seq_hi,
                       clk_seq_low, node);
}

double to_double(const std::string &s, double default_val) noexcept {
    try {
        return std::stod(s);
    } catch (...) {
        return default_val;
    }
}

bool try_to_double(const std::string &s, double &out) noexcept {
    try {
        out = std::stod(s);
        return true;
    } catch (...) {
        return false;
    }
}

float to_float(const std::string &s, float default_val) noexcept {
    try {
        return std::stof(s);
    } catch (...) {
        return default_val;
    }
}

bool try_to_float(const std::string &s, float &out) noexcept {
    try {
        out = std::stof(s);
        return true;
    } catch (...) {
        return false;
    }
}

} // namespace dracal::str

#ifdef __clang__
    #pragma clang diagnostic pop
#endif