#if !defined(spp_memory_h_guard)
#define spp_memory_h_guard

#include <cstdint>
#include <cstring>
#include <cstdlib>

#if defined(_WIN32) || defined(__CYGWIN__)
#define SPP_WIN
#endif

#ifdef SPP_WIN
#include <windows.h>
#include <Psapi.h>
#undef min
#undef max
#elif defined(__linux__)
#include <sys/types.h>
#include <sys/sysinfo.h>
#elif defined(__FreeBSD__)
#include <paths.h>
#include <fcntl.h>
#include <kvm.h>
#include <unistd.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#endif

namespace spp {
uint64_t GetSystemMemory();
uint64_t GetTotalMemoryUsed();
uint64_t GetProcessMemoryUsed();
uint64_t GetPhysicalMemory();

uint64_t GetSystemMemory() {
#ifdef SPP_WIN
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(MEMORYSTATUSEX);
    GlobalMemoryStatusEx(&memInfo);
    return static_cast<uint64_t>(memInfo.ullTotalPageFile);
#elif defined(__linux__)
    struct sysinfo memInfo;
    sysinfo(&memInfo);
    auto totalVirtualMem = memInfo.totalram;

    totalVirtualMem += memInfo.totalswap;
    totalVirtualMem *= memInfo.mem_unit;
    return static_cast<uint64_t>(totalVirtualMem);
#elif defined(__FreeBSD__)
    kvm_t *kd;
    u_int pageCnt;
    size_t pageCntLen = sizeof(pageCnt);
    u_int pageSize;
    struct kvm_swap kswap;
    uint64_t totalVirtualMem;

    pageSize = static_cast<u_int>(getpagesize());

    sysctlbyname("vm.stats.vm.v_page_count", &pageCnt, &pageCntLen, NULL, 0);
    totalVirtualMem = pageCnt * pageSize;

    kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open");
    kvm_getswapinfo(kd, &kswap, 1, 0);
    kvm_close(kd);
    totalVirtualMem += kswap.ksw_total * pageSize;

    return totalVirtualMem;
#else
    return 0;
#endif
}

uint64_t GetTotalMemoryUsed() {
#ifdef SPP_WIN
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(MEMORYSTATUSEX);
    GlobalMemoryStatusEx(&memInfo);
    return static_cast<uint64_t>(memInfo.ullTotalPageFile - memInfo.ullAvailPageFile);
#elif defined(__linux__)
    struct sysinfo memInfo;
    sysinfo(&memInfo);
    auto virtualMemUsed = memInfo.totalram - memInfo.freeram;

    virtualMemUsed += memInfo.totalswap - memInfo.freeswap;
    virtualMemUsed *= memInfo.mem_unit;

    return static_cast<uint64_t>(virtualMemUsed);
#elif defined(__FreeBSD__)
    kvm_t *kd;
    u_int pageSize;
    u_int pageCnt, freeCnt;
    size_t pageCntLen = sizeof(pageCnt);
    size_t freeCntLen = sizeof(freeCnt);
    struct kvm_swap kswap;
    uint64_t virtualMemUsed;

    pageSize = static_cast<u_int>(getpagesize());

    sysctlbyname("vm.stats.vm.v_page_count", &pageCnt, &pageCntLen, NULL, 0);
    sysctlbyname("vm.stats.vm.v_free_count", &freeCnt, &freeCntLen, NULL, 0);
    virtualMemUsed = (pageCnt - freeCnt) * pageSize;

    kd = kvm_open(NULL, _PATH_DEVNULL, NULL, O_RDONLY, "kvm_open");
    kvm_getswapinfo(kd, &kswap, 1, 0);
    kvm_close(kd);
    virtualMemUsed += kswap.ksw_used * pageSize;

    return virtualMemUsed;
#else
    return 0;
#endif
}

uint64_t GetProcessMemoryUsed() {
#ifdef SPP_WIN
    PROCESS_MEMORY_COUNTERS_EX pmc;
    GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PPROCESS_MEMORY_COUNTERS>(&pmc), sizeof(pmc));
    return static_cast<uint64_t>(pmc.PrivateUsage);
#elif defined(__linux__)
    auto parseLine = [](char* line) -> int {
        auto i = strlen(line);

        while (*line < '0' || *line > '9') {
            line++;
        }

        line[i - 3] = '\0';
        i = atoi(line);
        return i;
    };

    auto file = fopen("/proc/self/status", "r");
    auto result = -1;
    char line[128];

    while (fgets(line, 128, file) != nullptr) {
        if (strncmp(line, "VmSize:", 7) == 0) {
            result = parseLine(line);
            break;
        }
    }

    fclose(file);
    return static_cast<uint64_t>(result) * 1024;
#elif defined(__FreeBSD__)
    struct kinfo_proc info;
    size_t infoLen = sizeof(info);
    int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};

    sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &infoLen, NULL, 0);
    return static_cast<uint64_t>(info.ki_rssize * getpagesize());
#else
    return 0;
#endif
}

uint64_t GetPhysicalMemory() {
#ifdef SPP_WIN
    MEMORYSTATUSEX memInfo;
    memInfo.dwLength = sizeof(MEMORYSTATUSEX);
    GlobalMemoryStatusEx(&memInfo);
    return static_cast<uint64_t>(memInfo.ullTotalPhys);
#elif defined(__linux__)
    struct sysinfo memInfo;
    sysinfo(&memInfo);

    auto totalPhysMem = memInfo.totalram;

    totalPhysMem *= memInfo.mem_unit;
    return static_cast<uint64_t>(totalPhysMem);
#elif defined(__FreeBSD__)
    u_long physMem;
    size_t physMemLen = sizeof(physMem);
    int mib[] = {CTL_HW, HW_PHYSMEM};

    sysctl(mib, sizeof(mib) / sizeof(*mib), &physMem, &physMemLen, NULL, 0);
    return physMem;
#else
    return 0;
#endif
}

}  // namespace spp

#endif  // spp_memory_h_guard
