// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details.

#if defined(__APPLE__)
#include <mach-o/dyld.h>
#endif

#ifdef _WIN32
#include <windows.h>
#else
#include <pwd.h>
#include <sys/time.h>
#include <wordexp.h>
#endif

#include <sys/stat.h>
#include <unistd.h>
#include <utf8proc/utf8proc.h>

#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>

#include <hilti/rt/backtrace.h>
#include <hilti/rt/filesystem.h>
#include <hilti/rt/util.h>

#include <hilti/base/logger.h>
#include <hilti/base/util.h>

using namespace hilti;
using namespace hilti::util;

void util::detail::internalError(const std::string& s) { logger().internalError(s); }

void util::cannotBeReached() { hilti::logger().internalError("code is executing that should not be reachable"); }

std::vector<std::string> util::split(std::string_view s, std::string_view delim) {
    std::vector<std::string> l;

    while ( true ) {
        size_t p = s.find(delim);

        if ( p == std::string::npos )
            break;

        l.emplace_back(s.substr(0, p));

        s = s.substr(p + delim.size(), std::string::npos);
    }

    l.emplace_back(s);
    return l;
}

std::pair<std::string, std::string> util::split1(std::string_view s, std::string_view delim) {
    if ( auto i = s.find(delim); i != std::string::npos )
        return std::make_pair(std::string(s.substr(0, i)), std::string(s.substr(i + delim.size())));

    return std::make_pair(std::string(s), "");
}

std::pair<std::string, std::string> util::rsplit1(std::string_view s, std::string_view delim) {
    if ( auto i = s.rfind(delim); i != std::string::npos )
        return std::make_pair(std::string(s.substr(0, i)), std::string(s.substr(i + delim.size())));

    return std::make_pair("", std::string(s));
}

Result<std::vector<std::string>> util::splitShellUnsafe(std::string_view s) {
#if defined(_MSC_VER)
    // Shell-style word splitting with support for single/double quotes,
    // backslash escaping, command substitution (backticks), and basic
    // variable expansion ($VAR).
    if ( s.empty() )
        return {std::vector<std::string>{}};

    std::vector<std::string> result;
    std::string current;
    bool in_single_quote = false;
    bool in_double_quote = false;
    bool escape_next = false;

    for ( size_t i = 0; i < s.size(); ++i ) {
        char c = s[i];

        if ( escape_next ) {
            current += c;
            escape_next = false;
            continue;
        }

        if ( c == '\\' && ! in_single_quote ) {
            escape_next = true;
            continue;
        }

        if ( c == '\'' && ! in_double_quote ) {
            in_single_quote = ! in_single_quote;
            continue;
        }

        if ( c == '"' && ! in_single_quote ) {
            in_double_quote = ! in_double_quote;
            continue;
        }

        if ( c == '`' && ! in_single_quote && ! in_double_quote ) {
            // Skip command substitution content (cannot execute on Windows).
            auto end = s.find('`', i + 1);
            if ( end != std::string::npos )
                i = end;
            continue;
        }

        if ( c == '$' && ! in_single_quote ) {
            size_t start = i + 1;
            size_t end = start;
            while ( end < s.size() && (std::isalnum(s[end]) || s[end] == '_') )
                ++end;

            auto varname = s.substr(start, end - start);
            if ( auto* val = ::getenv(std::string(varname).c_str()) )
                current += val;
            i = end - 1;
            continue;
        }

        if ( std::isspace(static_cast<unsigned char>(c)) && ! in_single_quote && ! in_double_quote ) {
            if ( ! current.empty() ) {
                result.push_back(std::move(current));
                current.clear();
            }
            continue;
        }

        current += c;
    }

    if ( ! current.empty() )
        result.push_back(std::move(current));

    return {std::move(result)};
#else
    // On FreeBSD running `wordexp` on an empty string errors with
    // `WRDE_SYNTAX`; construct the result by hand.
    if ( s.empty() )
        return {std::vector<std::string>{}};

    wordexp_t we{};
    auto _ = util::scope_exit([&]() { wordfree(&we); });

    if ( wordexp(std::string(s).c_str(), &we, WRDE_UNDEF) != 0 )
        return result::Error("could not split string");

    std::vector<std::string> result{we.we_wordv, we.we_wordv + we.we_wordc};

    return {std::move(result)};
#endif
}

std::string util::replace(const std::string& s, const std::string& o, const std::string& n) {
    if ( o.empty() )
        return s;

    auto x = s;

    size_t i = 0;
    while ( (i = x.find(o, i)) != std::string::npos ) {
        x.replace(i, o.length(), n);
        i += n.length();
    }

    return x;
}

std::string util::tolower(const std::string& s) {
    std::string t = s;
    std::ranges::transform(t, t.begin(), ::tolower);
    return t;
}

std::string util::toupper(const std::string& s) {
    std::string t = s;
    std::ranges::transform(t, t.begin(), ::toupper);
    return t;
}

std::string util::rtrim(const std::string& s) {
    auto t = s;

    // We require `std::reverse_iterator::base` here which in e.g., gcc-10.2.1
    // is not implemented for the range equivalent of the code
    // (`borrowed_iterator_t` over a `reverse_view`). Stick to the non-ranges
    // version for now.
    // NOLINTNEXTLINE(modernize-use-ranges)
    auto it = std::find_if(t.rbegin(), t.rend(), [](char c) { return ! std::isspace(c); });
    t.erase(it.base(), t.end());

    return t;
}

std::string util::ltrim(const std::string& s) {
    auto t = s;
    t.erase(t.begin(), std::ranges::find_if(t, [](char c) { return ! std::isspace(c); }));
    return t;
}

std::string util::trim(const std::string& s) { return ltrim(rtrim(s)); }

uint64_t util::hash(const std::string& str) { return util::hash(str.data(), str.size()); }

uint64_t util::hash(const char* data, size_t len) { return std::hash<std::string_view>()(std::string_view(data, len)); }

std::string util::uitoa_n(uint64_t value, unsigned int base, int n) {
    static char dig[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    assert(base <= strlen(dig));

    std::string s;

    do {
        s.append(1, dig[value % base]);
        value /= base;
    } while ( value && (n < 0 || s.size() < static_cast<size_t>(n) - 1) );

    return s;
}

bool util::endsWith(const std::string& s, const std::string& suffix) {
    size_t i = s.rfind(suffix);

    if ( i == std::string::npos )
        return false;

    return (i == (s.length() - suffix.size()));
}

hilti::Result<hilti::rt::filesystem::path> util::findInPaths(const hilti::rt::filesystem::path& file,
                                                             const std::vector<hilti::rt::filesystem::path>& paths) {
    if ( file.is_absolute() ) {
        if ( hilti::rt::filesystem::exists(file) )
            return file;

        return hilti::result::Error(fmt("absolute path %s does not exist", file));
    }

    for ( const auto& d : paths ) {
        auto p = d / file;
        if ( hilti::rt::filesystem::exists(p) )
            return p;
    }

    return hilti::result::Error(fmt("%s not found", file));
}

hilti::rt::filesystem::path util::currentExecutable() {
    std::string error = "could not determine path of current executable";

    std::error_code ec;

#if defined(__APPLE__)
    // Get the length of the path.
    uint32_t path_len = 0;
    _NSGetExecutablePath(nullptr, &path_len);

    // Get the actual value.
    std::string path_(path_len, '\0');
    if ( _NSGetExecutablePath(path_.data(), &path_len) != 0 )
        // We already determined above that the path fits into the buffer, this
        // should never fail.
        rt::internalError(error);

    auto path = rt::filesystem::path(std::move(path_));

    if ( auto is_symlink = rt::filesystem::is_symlink(path, ec); ! ec && is_symlink )
        if ( auto p = rt::filesystem::read_symlink(path, ec); ! ec )
            path = p;

    return path;

#elif defined(_WIN32)
    char buf[MAX_PATH];
    DWORD len = GetModuleFileNameA(nullptr, buf, MAX_PATH);
    if ( len > 0 && len < MAX_PATH )
        return rt::filesystem::path(std::string(buf, len));

#elif defined(__FreeBSD__)
    const auto* exe = "/proc/curproc/file";

    if ( ! rt::filesystem::exists(exe, ec) )
        error += ": /proc needs to be mounted";

    if ( auto path = rt::filesystem::read_symlink(exe, ec); ! ec )
        return path;

#elif defined(__linux__)
    if ( auto path = rt::filesystem::read_symlink("/proc/self/exe", ec); ! ec )
        return path;

#endif

    rt::internalError(error);
}

void util::abortWithBacktrace() {
    std::cerr << "\n--- Aborting" << '\n';
    auto bt = hilti::rt::Backtrace().backtrace();
    for ( const auto& f : *bt )
        std::cerr << f << '\n';
    abort();
}

double util::currentTime() {
#ifdef _WIN32
    FILETIME ft;
    GetSystemTimeAsFileTime(&ft);
    ULARGE_INTEGER ull;
    ull.LowPart = ft.dwLowDateTime;
    ull.HighPart = ft.dwHighDateTime;
    // FILETIME is in 100ns intervals since 1601-01-01; convert to Unix epoch seconds.
    return static_cast<double>(ull.QuadPart - 116444736000000000ULL) / 1e7;
#else
    struct timeval tv{};
    gettimeofday(&tv, nullptr);
    return static_cast<double>(tv.tv_sec) + (static_cast<double>(tv.tv_usec) / 1e6);
#endif
}

std::string util::toIdentifier(std::string s) {
    if ( s.empty() )
        return s;

    if ( ! isdigit(s[0]) && std::ranges::all_of(s, [](auto c) { return std::isalnum(c) || c == '_'; }) )
        // Fast-path: no special-characters, no leading digits.
        return s;

    auto buffer_size = (s.size() * 3) + 1;
    char* buffer = reinterpret_cast<char*>(alloca(buffer_size)); // max possible size of modified string
    char* p = buffer;

    if ( isdigit(s[0]) )
        // do not start with a digit
        *p++ = '_';

    for ( auto c : s ) {
        switch ( c ) {
            case ':':
            case '<':
            case '>':
            case ',':
            case '.':
            case ' ':
            case '-':
            case '\'':
            case '"':
            case '%': *p++ = '_'; break;

            default: {
                if ( std::isalnum(c) || c == '_' )
                    *p++ = c;
                else {
                    static const char* const hex = "0123456789abcdef";
                    *p++ = 'x';
                    *p++ = hex[c >> 4U];
                    *p++ = hex[c % 0x0f];
                }

                break;
            }
        }
    }

    assert(p < buffer + buffer_size);
    return std::string(buffer, p - buffer);
}

std::string util::prefixParts(const std::string& in, const std::string& prefix, const std::string& include_tag) {
    if ( in.empty() )
        return "";

    auto x = split(in, " ") | std::views::transform([&](auto s) {
                 if ( s.empty() )
                     return std::string();

                 if ( include_tag.size() ) {
                     auto x = split(s, "!");
                     if ( x.size() == 3 ) {
                         if ( x[1] != include_tag )
                             return std::string();

                         s = x[2];
                     }
                 }

                 if ( auto x = trim(s); ! util::startsWith(s, "-") )
                     return prefix + x;
                 else
                     return x;
             });

    return join(std::move(x) | std::views::filter([](const auto& s) -> bool { return s.size(); }), " ");
}

std::vector<std::string> util::flattenParts(const std::vector<std::string>& in) {
    std::vector<std::string> out;

    for ( const auto& i : in ) {
        for ( auto s : util::split(i) ) {
            s = util::trim(s);
            if ( s.empty() )
                continue;

            out.push_back(std::move(s));
        }
    }

    return out;
}

std::optional<hilti::rt::filesystem::path> util::cacheDirectory(const hilti::Configuration& configuration) {
    // If we are executing from the build directory, the cache is also located
    // there; else it lives in a versioned folder in the user's `$HOME/.cache/spicy`.
    if ( configuration.uses_build_directory )
        return configuration.build_directory / "cache" / "spicy";

    if ( auto* spicy_cache = ::getenv("SPICY_CACHE") )
        return spicy_cache;

    const char* homedir = getenv("HOME");

#ifdef _WIN32
    if ( homedir == nullptr )
        homedir = getenv("USERPROFILE");
#else
    if ( homedir == nullptr ) {
        auto* pwuid = getpwuid(getuid());
        if ( ! pwuid )
            return {};

        homedir = pwuid->pw_dir;
    }
#endif

    if ( homedir )
        return rt::filesystem::path(rt::filesystem::path(homedir) / ".cache" / "spicy" / configuration.version_string);

    return {};
}
