//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2019-2026 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt).  If not, see
// <https://www.gnu.org/licenses/>.
//
#include "App.h"
#include "CityHash.h"
#include "Json/Json.h"
#include "Logger.h"
#include "Util.h"

#include "bitcoin/hash.h"

// below headers are for getN*Processors, etc.
#if defined(Q_OS_DARWIN)
#  include <sys/types.h>
#  include <sys/sysctl.h>
#  include <mach/mach.h>
#  include <mach/mach_time.h>
#elif defined(Q_OS_LINUX)
#  include <array>
#  include <fstream>
#  include <locale>
#  include <sstream>
#  include <strings.h>
#  include <time.h>
#  include <unistd.h>
#elif defined(Q_OS_WINDOWS)
#  define WIN32_LEAN_AND_MEAN 1
#  include <windows.h>
#  include <psapi.h>
#  include <io.h>              // for _write(), _read(), _pipe(), _close()
#  include <fcntl.h>           // for O_BINARY, O_TEXT
#  include <errno.h>           // for errno
#endif

#if defined(Q_OS_UNIX)
#  include <unistd.h>          // for write(), read(), pipe(), close()
#  if __has_include(<sys/time.h>) && __has_include(<sys/resource.h>) // POSIX includes for setrlimit/getrlimit
#    include <sys/time.h>      // for setrlimit related stuff
#    include <sys/resource.h>  // for setrlimit related stuff
#    define HAS_SETRLIMIT
#  endif
#endif

#include <QRegularExpression>
#include <QHostAddress>

#include <cctype>
#include <cstddef>             // for std::byte, offsetof()
#include <cstring>             // for strerror
#include <iostream>
#include <mutex>
#include <thread>
#include <utility>

namespace Util {
    QString basename(const QString &s) {
        const QRegularExpression re("[\\/]");
        auto toks = s.split(re);
        return toks.last();
    }

#if defined(Q_OS_LINUX)
    static int64_t getAbsTimeNS() noexcept
    {
        struct timespec ts;
        // Note: CLOCK_MONOTONIC does *not* include the time spent suspended. If we want that, then we can Use
        // CLOCK_BOOTTIME here for that.
        if (clock_gettime(CLOCK_MONOTONIC, &ts)) {
            ts = {0, 0};
            // We can't do a Warning() or Error() here because that would cause infinite recursion.
            // This is an unlikely and also pretty fatal situation, though, so we must warn.
            // Also we will use these noexcept functions here to preserve our noexcept-ness
            using namespace AsyncSignalSafe;
            writeStdErr(SBuf("Fatal: clock_gettime for CLOCK_MONOTONIC returned error status: ", std::strerror(errno)));
        }
        return int64_t(ts.tv_sec * 1000000000LL) + int64_t(ts.tv_nsec);
    }
    static int64_t absT0 = getAbsTimeNS();
    qint64 getTimeNS() noexcept {
        const auto now = getAbsTimeNS();
        return now - absT0;
    }
    qint64 getTime() noexcept {
        return getTimeNS()/1000000LL;
    }
    bool isClockSteady() noexcept { return true; }
#elif defined(Q_OS_WINDOWS)
    // Windows lacks a decent high resolution clock source on some C++ implementations (such as MinGW). So we
    // query the OS's QPC mechanism, which, on Windows 7+ is very fast to query and guaranteed to be accurate, and also
    // monotocic ("steady").
    static int64_t getAbsTimeNS() noexcept
    {
        static __int64 freq = 0;
        __int64 ct, factor;

        if (!freq) {
            QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
        }
        QueryPerformanceCounter((LARGE_INTEGER *)&ct);   // reads the current time (in system units)
        factor = 1000000000LL/freq;
        if (factor <= 0) factor = 1;
        return int64_t(ct * factor);
    }
    static qint64 absT0 = qint64(getAbsTimeNS()); // initializes static data inside getAbsTimeNS() once at startup in main thread.
    qint64 getTimeNS() noexcept {
        const auto now = getAbsTimeNS();
        return now - absT0;
    }
    qint64 getTime() noexcept {
        return getTimeNS()/1000000LL;
    }
    bool isClockSteady() noexcept { return true; }
#else
    // MacOS or generic platform (on MacOS with clang this happens to be very accurate)
    static const auto t0 = std::chrono::high_resolution_clock::now();
    qint64 getTime() noexcept {
        const auto now = std::chrono::high_resolution_clock::now();
        return std::chrono::duration_cast<std::chrono::milliseconds>(now - t0).count();
    }
    qint64 getTimeNS() noexcept {
        const auto now = std::chrono::high_resolution_clock::now();
        return std::chrono::duration_cast<std::chrono::nanoseconds>(now - t0).count();
    }
    bool isClockSteady() noexcept {
        return std::chrono::high_resolution_clock::is_steady;
    }
#endif

    qint64 getTimeMicros() noexcept {
        return getTimeNS()/1000LL;
    }

    double getTimeSecs() noexcept {
        return double(getTime()) / 1e3;
    }

    bool VoidFuncOnObjectNoThrow(const QObject *obj, const std::function<void()> & lambda, int timeout_ms)
    {
        try {
            LambdaOnObject<void>(obj, lambda, timeout_ms);
            return true;
        } catch (const Exception &) {}
        return false;
    }

#if defined(Q_OS_DARWIN)
    unsigned getNVirtualProcessors()
    {
        static std::atomic<unsigned> nVProcs = 0;
        if (!nVProcs) {
            int a = 0;
            size_t b = sizeof(a);
            if (0 == sysctlbyname("hw.ncpu",&a, &b, nullptr, 0)) {
                nVProcs = unsigned(a); // this returns virtual CPUs which isn't always what we want..
            }
        }
        return nVProcs.load() ? nVProcs.load() : 1;
    }

    unsigned getNPhysicalProcessors()
    {
        static std::atomic<unsigned> nProcs = 0;
        if (!nProcs) {
            int a = 0;
            size_t b = sizeof(a);
            if (0 == sysctlbyname("hw.physicalcpu",&a,&b,nullptr,0)) {
                nProcs = unsigned(a);
            }
        }
        return nProcs.load() ? nProcs.load() : 1;
    }
#elif defined(Q_OS_LINUX)
    unsigned getNVirtualProcessors() { return std::thread::hardware_concurrency(); }
    unsigned getNPhysicalProcessors() {
        static std::atomic<unsigned> nProcs = 0;
        if (!nProcs) {
            nProcs = unsigned(sysconf(_SC_NPROCESSORS_ONLN));
        }
        return nProcs.load() ? nProcs.load() : 1;
    }
#elif defined(Q_OS_WINDOWS)
    unsigned getNVirtualProcessors()
    {
        static std::atomic_uint nProcs = 0;
        if (auto val = nProcs.load()) return val;

        SYSTEM_INFO system_info = {};
        GetSystemInfo(&system_info);
        const auto nVirtProc = static_cast<unsigned>(system_info.dwNumberOfProcessors);
        return nProcs = std::max(nVirtProc, 1u);
    }
    unsigned getNPhysicalProcessors()
    {
        static std::atomic_uint nProcs = 0;
        if (auto val = nProcs.load()) return val;

        // from: https://stackoverflow.com/questions/150355/programmatically-find-the-number-of-cores-on-a-machine
        DWORD length = 0;
        auto res = GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &length);
        if (res || GetLastError() != ERROR_INSUFFICIENT_BUFFER)
            return getNVirtualProcessors(); // fallback

        const std::size_t align = alignof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX);
        auto buffer = std::make_unique_for_overwrite<std::byte[]>(size_t(length) + align);
        uintptr_t ptrval = reinterpret_cast<uintptr_t>(buffer.get());
        if (const auto rem = ptrval % align; rem) ptrval += align - rem; // ensure alignment
        PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX info =
            reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>(reinterpret_cast<std::byte *>(ptrval));

        res = GetLogicalProcessorInformationEx(RelationProcessorCore, info, &length);
        if (!res)
            return getNVirtualProcessors(); // fallback

        unsigned nPhysProc = 0;
        DWORD offset = 0;
        const std::byte *buf = reinterpret_cast<std::byte *>(info);
        while (offset < length) {
            const std::byte *punaligned = buf + offset + offsetof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, Size);
            decltype(std::declval<SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>().Size) len{};
            std::memcpy(&len, punaligned, sizeof(len));
            if (!len) break; // prevent infinite loops
            offset += len;
            ++nPhysProc;
        }
        return nProcs = std::max(nPhysProc, 1u);
    }
#else
    unsigned getNVirtualProcessors() { return std::thread::hardware_concurrency(); }
    unsigned getNPhysicalProcessors() { return std::thread::hardware_concurrency(); }
#endif

    QByteArray ParseHexFast(const QByteArray &hex, bool checkDigits)
    {
        const int size = hex.size();
        QByteArray ret(size / 2, Qt::Initialization::Uninitialized);
        if (size % 2) [[unlikely]] {
            // bad / not hex because not even number of chars.
            ret.clear();
            return ret;
        }
        const char *d = hex.constData(), * const dend = d + size;
        uint8_t c1, c2;
        for (char *out = ret.data(); d < dend; d += 2, ++out) {
            constexpr uint8_t offset_A = 'A' - 0xa,
                              offset_a = 'a' - 0xa,
                              offset_0 = '0';
            // slightly unrolled loop, does 2 chars at a time
            c1 = uint8_t(d[0]);
            c2 = uint8_t(d[1]);

            // c1
            if (c1 <= '9') // this is the most likely for any random digit, so we check this first
                c1 -= offset_0;
            else if (c1 >= 'a') // next, we anticipate lcase, so we do this check first
                c1 -= offset_a;
            else // c1 >= 'A'
                c1 -= offset_A;
            // c2
            if (c2 <= '9') // this is the most likely for any random digit, so we check this first
                c2 -= offset_0;
            else if (c2 >= 'a') // next, we anticipate lcase, so we do this check first
                c2 -= offset_a;
            else // c2 >= 'A'
                c2 -= offset_A;


            // The below is slowish... we can just accept bad hex data as 'corrupt' ...
            // checkDigit = false allows us to skip this check, making this function >5x faster!
            if (checkDigits && (c1 > 0xf || c2 > 0xf)) [[unlikely]] { // ensure data was actually in range
                ret.clear();
                break;
            }
            *out = char((c1 << 4) | c2);
        }
        return ret;
    }

    QByteArray ToHexFast(const QByteArray &ba)
    {
        QByteArray ret(ba.size()*2, Qt::Initialization::Uninitialized);
        if (!ToHexFastInPlace(ba, ret.data(), size_t(ret.size())))
            ret.clear();
        return ret;
    }
    bool ToHexFastInPlace(const QByteArray &ba, char *out, size_t bufsz)
    {
        static const char hexmap[513] =
            "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
            "303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"
            "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"
            "909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
            "c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef"
            "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";

        const int size = ba.size();
        if (bufsz < size_t(size*2))
            return false;
        const uint8_t *cur = reinterpret_cast<const uint8_t *>(ba.constData()), * const end = cur + size;
        for (const char *nibbles; cur < end; ++cur, out += 2) {
            nibbles = &hexmap[*cur * 2];
            out[0] = nibbles[0];
            out[1] = nibbles[1];
        }
        return true;
    }

    bool IsValidHex(const QByteArray &s)
    {
        return s.size() % 2 == 0 && std::all_of(s.begin(), s.end(), [](char const c) { return std::isxdigit(c); });
    }


    namespace {
        /// Stores a hash seed that we will use for our hash tables.
        /// There really should only be one of these globally.
        class HashSeed {
            uint64_t seed;
        public:
            /// seeds 'seed' from QRandomGenerator
            HashSeed() {
                auto gen = QRandomGenerator::global();
                if (!gen) {
                    Warning() << "App-global random number generator is null! Seeding hash seed with current time. FIXME!";
                    seed = uint64_t(getTimeNS());
                } else {
                    seed = uint64_t(gen->generate64());
                }
            }
            template <std::integral IntType>
            IntType get() const { return static_cast<IntType>(seed); }
        };

        /// app-global hash seed -- initialized before we enter main()
        const HashSeed hashSeed;
    } // namespace (anonymous)

    uint32_t hashData32(const ByteView &bv) noexcept
    {
        // bitcoin::MurmurHash3 is not marked noexcept but it will never throw -- it does not allocate and
        // just uses basic arithmetic ops on the data in-place.
        return bitcoin::MurmurHash3(hashSeed.get<uint32_t>(), bv.ucharData(), bv.size());
    }
    uint64_t hashData64(const ByteView &bv) noexcept
    {
        // CityHash::CityHash64WithSeed is not marked noexcept but it will never throw -- it does not allocate and
        // just uses basic arithmetic ops on the data in-place.
        return uint64_t(CityHash::CityHash64WithSeed(bv.charData(), bv.size(), hashSeed.get<CityHash::uint64>()));
    }

    MemUsage getProcessMemoryUsage()
    {
#if defined(Q_OS_WINDOWS)
        PROCESS_MEMORY_COUNTERS_EX pmc;
        GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc));
        return { std::size_t{pmc.WorkingSetSize}, std::size_t{pmc.PrivateUsage} };
#elif defined(Q_OS_LINUX)
        MemUsage ret;
        std::ifstream file("/proc/self/status", std::ios_base::in);
        if (!file) return ret;
        file.imbue(std::locale::classic());
        std::array<char, 256> buf;
        buf[0] = 0;
        // sizes are in kB
        while (file.getline(buf.data(), buf.size()) && (ret.phys == 0 || ret.virt == 0)) {
            if (strncasecmp(buf.data(), "VmSize:", 7) == 0) {
                std::istringstream is(buf.data() + 7);
                is.imbue(std::locale::classic());
                is >> std::skipws >> ret.virt;
                ret.virt *= std::size_t(1024);
            } else if (strncasecmp(buf.data(), "VmRSS:", 6) == 0) {
                std::istringstream is(buf.data() + 6);
                is.imbue(std::locale::classic());
                is >> std::skipws >> ret.phys;
                ret.phys *= std::size_t(1024);
            }
        }
        return ret;
#elif defined(Q_OS_DARWIN)
        struct task_basic_info t_info;
        mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;

        if (KERN_SUCCESS != task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count)) {
            return {};
        }
        return { std::size_t{t_info.resident_size}, std::size_t{t_info.virtual_size} };
#else
        return {};
#endif
    }

    uint64_t getAvailablePhysicalRAM()
    {
        uint64_t ret = 2048u * 1024u * 1024u; // just return 2GB, even if it's wrong, for unknown platforms
#if defined(Q_OS_WINDOWS)
        MEMORYSTATUSEX statex;
        statex.dwLength = sizeof(statex);
        GlobalMemoryStatusEx(&statex);
        ret = static_cast<uint64_t>(statex.ullAvailPhys);
#elif defined(Q_OS_DARWIN)
        // can't easily query memory on darwin, just take 1/2 of physical memory
        char buf[8];
        size_t bufsz = 8;
        static_assert(sizeof(uint64_t) == 8);
        if ( 0 == ::sysctlbyname("hw.memsize", buf, &bufsz, nullptr, 0) ) {
            switch (bufsz) {
            case 4: { uint32_t tmp; std::memcpy(&tmp, buf, 4); ret = tmp; ret /= uint64_t(2); break; }
            case 8: { std::memcpy(&ret, buf, 8); ret /= uint64_t(2); break; }
            default: qWarning() << "Failed to query physical RAM, kernel returned unexpected bufsize: " << bufsz;
            }
        }
#elif defined(Q_OS_LINUX)
        std::ifstream file("/proc/meminfo", std::ios_base::in);
        if (!file) return ret;
        file.imbue(std::locale::classic());
        std::array<char, 256> buf;
        buf[0] = 0;
        // sizes are in KiB
        while (file.getline(buf.data(), buf.size())) {
            if (strncasecmp(buf.data(), "MemAvailable:", 13) == 0) {
                std::istringstream is(buf.data() + 13);
                is.imbue(std::locale::classic());
                uint64_t tmp = 0;
                is >> std::skipws >> tmp;
                tmp *= uint64_t(1024);
                if (tmp > 0) ret = tmp;
                break;
            }
        }
#endif
        return ret;
    }

    uint64_t getTotalPhysicalRAM()
    {
        uint64_t ret = 2048u * 1024u * 1024u; // just return 2GB, even if it's wrong, for unknown platforms
#if defined(Q_OS_WINDOWS)
        MEMORYSTATUSEX statex;
        statex.dwLength = sizeof(statex);
        GlobalMemoryStatusEx(&statex);
        ret = static_cast<uint64_t>(statex.ullTotalPhys);
#elif defined(Q_OS_DARWIN)
        char buf[8];
        size_t bufsz = 8;
        static_assert(sizeof(uint64_t) == 8);
        if ( 0 == ::sysctlbyname("hw.memsize", buf, &bufsz, nullptr, 0) ) {
            switch (bufsz) {
            case 4: { uint32_t tmp; std::memcpy(&tmp, buf, 4); ret = tmp; break; }
            case 8: { std::memcpy(&ret, buf, 8); break; }
            default: qWarning() << "Failed to query physical RAM, kernel returned unexpected bufsize: " << bufsz;
            }
        }
#elif defined(Q_OS_LINUX)
        std::ifstream file("/proc/meminfo", std::ios_base::in);
        if (!file) return ret;
        file.imbue(std::locale::classic());
        std::array<char, 256> buf;
        buf[0] = 0;
        // sizes are in KiB
        while (file.getline(buf.data(), buf.size())) {
            if (strncasecmp(buf.data(), "MemTotal:", 9) == 0) {
                std::istringstream is(buf.data() + 9);
                is.imbue(std::locale::classic());
                uint64_t tmp = 0;
                is >> std::skipws >> tmp;
                tmp *= uint64_t(1024);
                if (tmp > 0) ret = tmp;
                break;
            }
        }
#endif
        return ret;
    }

    namespace AsyncSignalSafe {
        namespace {
#if defined(Q_OS_WIN)
            auto writeFD = ::_write; // Windows API docs say to use this function, since write() is deprecated
            auto readFD  = ::_read;  // Windows API docs say to use this function, since read() is deprecated
            auto closeFD = ::_close; // Windows API docs say to use this function, since close() is deprecated
            inline constexpr std::array<char, 3> NL{"\r\n"};
#elif defined(Q_OS_UNIX)
            auto writeFD = ::write;
            auto readFD  = ::read;
            auto closeFD = ::close;
            inline constexpr std::array<char, 2> NL{"\n"};
#else
            // no-op on unknown platform (this platform would use the cond variable and doesn't need read/close/pipe)
            auto writeFD = [](int, const void *, size_t n) { return int(n); };
            inline constexpr std::array<char, 1> NL{0};
#endif
        }
        void writeStdErr(const std::string_view &sv, bool wrnl) noexcept {
            constexpr int stderr_fd = 2; /* this is the case on all platforms */
            writeFD(stderr_fd, sv.data(), sv.length());
            if (wrnl && NL.size() > 1)
                writeFD(stderr_fd, NL.data(), NL.size()-1);
        }
#if defined(Q_OS_WIN) || defined(Q_OS_UNIX)
        Sem::Pipe::Pipe() {
            const int res =
#           ifdef Q_OS_WIN
                ::_pipe(fds, 32 /* bufsize */, O_BINARY);
#           else
                ::pipe(fds);
#           endif
            if (res != 0)
                throw InternalError(QString("Failed to create a Cond::Pipe: (%1) %2").arg(errno).arg(std::strerror(errno)));
        }
        Sem::Pipe::~Pipe() { closeFD(fds[0]), closeFD(fds[1]); }
        std::optional<SBuf<>> Sem::acquire() noexcept {
            std::optional<SBuf<>> ret;
            char c;
            if (const int res = readFD(p.fds[0], &c, 1); res != 1)
                ret.emplace("Sem::acquire: readFD returned ", res);
            return ret;
        }
        std::optional<SBuf<>> Sem::release() noexcept {
            std::optional<SBuf<>> ret;
            const char c = 0;
            if (const int res = writeFD(p.fds[1], &c, 1); res != 1)
                ret.emplace("Sem::release: writeFD returned ", res);
            return ret;
        }
#else
        // fallback to emulated -- use std C++ condition variable which is not technically
        // guaranteed async signal safe, but for all pratical purposes it's safe enough as a fallback.
        std::optional<SBuf<>> Sem::acquire() noexcept {
            std::mutex dummy; // hack, but works
            std::unique_lock l(dummy);
            p.cond.wait(l);
            return std::nullopt;
        }
        std::optional<SBuf<>> Sem::release() noexcept {
            p.cond.notify_one();
            return std::nullopt;
        }
#endif // defined(Q_OS_WIN) || defined(Q_OS_UNIX)
    } // end namespace AsyncSignalSafe

    MaxOpenFilesResult raiseMaxOpenFilesToHardLimit()
    {
#ifdef HAS_SETRLIMIT
        MaxOpenFilesResult ret;
        struct rlimit rl;
        auto get = [&rl, &ret] {
            if (getrlimit(RLIMIT_NOFILE, &rl)) {
                ret.status = ret.Error;
                ret.errMsg = QString("getrlimit: ") + std::strerror(errno);
                return false;
            }
            return true;
        };
        // first get the current limits
        if (!get())
            return ret;
        // paranoia
        if (long(rl.rlim_cur) < 0 || long(rl.rlim_max) < 0) {
            ret.status = ret.Error;
            ret.errMsg = "getrlimit reports limits are negative";
        }
        // more paranoia
        if (rl.rlim_cur > rl.rlim_max) {
            ret.status = ret.Error;
            ret.errMsg = "soft limit > hard limit (this shouldn't happen)";
        }
        // save value
        ret.oldLimit = long(rl.rlim_cur);
        if (rl.rlim_cur != rl.rlim_max) { // if not at hard limit, raise it
            // set to max
            rl.rlim_cur = rl.rlim_max;
            if (setrlimit(RLIMIT_NOFILE, &rl)) {
                ret.status = ret.Error;
                ret.errMsg = QString("setrlimit: ") + std::strerror(errno);
                return ret;
            }
        }
        // get the new limits again
        if (!get())
            return ret;
        // save value, indicate success
        ret.newLimit = long(rl.rlim_cur);
        ret.status = ret.Ok;

        return ret;
#else
        // On Windows this call is not even needed -- our use of Qt uses the Win32 API directly which has a limit
        // of 16.7 million for the handle tables.
        return {MaxOpenFilesResult::NotRelevant};
#endif
    }

    QPair<QString, quint16> ParseHostPortPair(const QString &s, bool allowImplicitLoopback)
    {
        constexpr auto parsePort = [](const QString & portStr) -> quint16 {
            bool ok;
            quint16 port = portStr.toUShort(&ok);
            if (!ok || port == 0)
                throw BadArgs(QString("Bad port: %1").arg(portStr));
            return port;
        };
        auto toks = s.split(":");
        constexpr const char *msg1 = "Malformed host:port spec. Please specify a string of the form <host>:<port>";
        if (const auto len = toks.length(); len < 2) {
            if (allowImplicitLoopback && len == 1)
                // this option allows bare port number with the implicit ipv4 127.0.0.1 -- try that (may throw if bad port number)
                return QPair<QString, quint16>{QHostAddress(QHostAddress::LocalHost).toString(), parsePort(toks.front())};
            throw BadArgs(msg1);
        }
        QString portStr = toks.last();
        toks.removeLast(); // pop off port
        QString hostStr = toks.join(':'); // rejoin on ':' in case it was IPv6 which is full of colons
        if (hostStr.isEmpty())
            throw BadArgs(msg1);
        if (toks.length() > 1 && hostStr.length() > 2 && hostStr.front() == QChar('[') && hostStr.back() == QChar(']'))
            hostStr = hostStr.mid(1, hostStr.length()-2); // pop off leading and trailing [] for ipv6, if present
        return {hostStr, parsePort(portStr)};
    }

    std::pair<double, QString> ScaleBytes(uint64_t bytes, std::string_view baseByteUnitLabel)
    {
        double dataSize = bytes;
        if (dataSize > 1e3) { baseByteUnitLabel = "KB"; dataSize /= 1e3; }
        if (dataSize > 1e3) { baseByteUnitLabel = "MB"; dataSize /= 1e3; }
        if (dataSize > 1e3) { baseByteUnitLabel = "GB"; dataSize /= 1e3; }
        if (dataSize > 1e3) { baseByteUnitLabel = "TB"; dataSize /= 1e3; }
        if (dataSize > 1e3) { baseByteUnitLabel = "PB"; dataSize /= 1e3; }
        if (dataSize > 1e3) { baseByteUnitLabel = "EB"; dataSize /= 1e3; }
        return {dataSize, QString::fromUtf8(baseByteUnitLabel.data(), baseByteUnitLabel.size())};
    }

    QString RenderHostPortPair(const QHostAddress &addr, quint16 port)
    {
        QString ret = addr.toString();
        if (!ret.isEmpty()) {
            if (addr.protocol() == QAbstractSocket::IPv6Protocol && ret.front() != QChar('[') && ret.back() != QChar(']')) {
                ret.insert(0, QChar('['));
                ret.append(QChar(']'));
            }
            ret.append(QStringLiteral(":%1").arg(port));
        }
        return ret;
    }

    namespace ThreadName {
        namespace {
            QString & GetMutable() {
                static thread_local QString threadName;
                return threadName;
            }
        } // namespace
        const QString & Get() { return GetMutable(); }
        void Set(const QString &name) { GetMutable() = name; }
    } // namespace ThreadName


    ThreadInterrupt::operator bool() const { return flag.load(std::memory_order_acquire); }

    void ThreadInterrupt::reset() { flag.store(false, std::memory_order_release); }

    void ThreadInterrupt::operator()()
    {
        {
            std::unique_lock l(mut);
            flag.store(true, std::memory_order_release);
        }
        cond.notify_all();
    }

    bool ThreadInterrupt::wait(std::optional<std::chrono::milliseconds> rel_time) const
    {
        const auto predicate = [this] { return this->operator bool(); };
        std::unique_lock lock(mut);
        if (predicate()) {
            return true;
        } else if (rel_time) {
            return cond.wait_for(lock, *rel_time, predicate);
        } else {
            cond.wait(lock, predicate);
            return predicate(); // should always be true here
        }
    }

    size_t GetWindowsObjectCount()
    {
#if defined(Q_OS_WINDOWS)
        return GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) + GetGuiResources(GetCurrentProcess(), GR_USEROBJECTS);
#else
        return 0;
#endif
    }

} // end namespace Util

Log::Log() {}

Log::Log(Color c)
{
    setColor(c);
}

Log::Log(const char *fmt...)
    :  s()
{
    va_list ap;
    va_start(ap,fmt);
    str = QString::vasprintf(fmt,ap);
    va_end(ap);
    s.setString(&str, QIODevice::WriteOnly|QIODevice::Append);
}

Log::~Log()
{
    if (doprt) {
        App *ourApp = app();
        if (ourApp && !ourApp->options) [[unlikely]]
            ourApp = nullptr; // spurious Qt message -- ourApp not yet fully constructed.
        using LTS = Options::LogTimestampMode;
        const LTS ltsMode = !ourApp ? Options::defaultLogTimeStampMode : ourApp->options->logTimestampMode;
        s.flush(); // does nothing probably..
        // [timestamp]
        // Note: we always want to log the timestamp, even in syslog mode.
        // This is because if logging from a thread, log lines may be out-of-order.
        // The timestamp is the only record of the actual order in which things
        // occurred. Currently the timestamp is to 4 decimal places (hundreds of micros) in Uptime mode only.
        // We do offer LogTimestampMode::None for users really wishing to suppress timestamp logging.
        QString tsStr;
        switch (ltsMode) {
        case LTS::None:
            break;
        case LTS::Uptime: {
            const auto unow = Util::getTimeNS()/1000LL;
            tsStr = QString::asprintf("[%lld.%04d] ", unow/1000000LL, int((unow/100LL)%10000));
        }
            break;
        case LTS::UTC:
        case LTS::Local: {
            const auto now = ltsMode == LTS::UTC ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime();
            tsStr = now.toString(u"[yyyy-MM-dd hh:mm:ss.zzz] ");
        }
            break;
        }
        // /[timestamp]
        QString thrdStr;
        if (QThread *th = QThread::currentThread(); th && ourApp && th != ourApp->thread()) {
            QString thrdName = Util::ThreadName::Get(); /* We must use an internal name. THIS IS UNSAFE --> th->objectName(); */
            if (thrdName.trimmed().isEmpty()) thrdName = QString::asprintf("%p", reinterpret_cast<void *>(QThread::currentThreadId()));
            thrdStr = QStringLiteral("<%1> ").arg(thrdName);
        }

        Logger *logger = ourApp ? ourApp->logger() : nullptr;

        QString theString = tsStr + thrdStr + (logger && logger->isaTTY() ? colorize(str, color) : str);

        if (logger) {
            emit logger->log(level, theString);
        } else {
            // logger not active yet; just print to console for now..
            static std::mutex mut;
            {
                const auto bytes = theString.toUtf8();
                std::unique_lock g(mut);
                std::fwrite(bytes.constData(), 1, bytes.size(), stderr);
                std::fwrite("\n", 1, 1, stderr);
                std::fflush(stderr);
            }
            // Fatal should signal a quit even here
            if (level == Logger::Level::Fatal && qApp) {
                QTimer::singleShot(0, qApp, []{ qApp->quit(); });
            }
        }
    }
}

/* static */
QString Log::colorString(Color c) {
    const char *suffix = "[0m"; // normal
    switch(c) {
    case Black: suffix = "[30m"; break;
    case Red: suffix = "[31m"; break;
    case Green: suffix = "[32m"; break;
    case Yellow: suffix = "[33m"; break;
    case Blue: suffix = "[34m"; break;
    case Magenta: suffix = "[35m"; break;
    case Cyan: suffix = "[36m"; break;
    case White: suffix = "[37m"; break;
    case BrightBlack: suffix = "[30;1m"; break;
    case BrightRed: suffix = "[31;1m"; break;
    case BrightGreen: suffix = "[32;1m"; break;
    case BrightYellow: suffix = "[33;1m"; break;
    case BrightBlue: suffix = "[34;1m"; break;
    case BrightMagenta: suffix = "[35;1m"; break;
    case BrightCyan: suffix = "[36;1m"; break;
    case BrightWhite: suffix = "[37;1m"; break;

    default:
        // will just use normal
        break;
    }
    static const char prefix[2] = { 033, 0 }; // esc 033 in octal
    return QString::asprintf("%s%s", prefix, suffix);
}

QString Log::colorize(const QString &str, Color c) {
    QString colorStr = useColor && c != Normal ? colorString(c) : "";
    QString normalStr = useColor && c != Normal ? colorString(Normal) : "";
    return colorStr + str + normalStr;
}

template <> Log & Log::operator<<(const Color &c) { setColor(c); return *this; }

Debug::~Debug()
{
    level = Logger::Level::Debug;
    doprt = isEnabled();
    if (!doprt) return;
    if (!colorOverridden) color = Cyan;
    str = QStringLiteral("(Debug) ") + str;
}

bool Debug::forceEnable = false;

bool Debug::isEnabled() {
    auto ourApp = app();
    return forceEnable || !ourApp || !ourApp->options || ourApp->options->verboseDebug;
}


Trace::~Trace()
{
    level = Logger::Level::Debug;
    doprt = isEnabled();
    if (!doprt) return;
    if (!colorOverridden) color = Green;
    str = QStringLiteral("(Trace) ") + str;
}

bool Trace::forceEnable = false;

bool Trace::isEnabled() {
    auto ourApp = app();
    return forceEnable
            || (ourApp && ourApp->options && ourApp->options->verboseTrace && ourApp->options->verboseDebug); // both trace and debug must be on
}

Error::~Error()
{
    level = Logger::Level::Critical;
    if (!colorOverridden) color = BrightRed;
}


Warning::~Warning()
{
    level = Logger::Level::Warning;
    if (!colorOverridden) color = Yellow;
}

Alert::~Alert()
{
    level = Logger::Level::Alert;
    if (!colorOverridden) color = BrightMagenta;
}

Fatal::~Fatal()
{
    level = Logger::Level::Fatal;
    str = QString("FATAL: ") + str;
    if (!colorOverridden) color = BrightRed;
}


#ifdef ENABLE_TESTS
#endif
