#include "cpu.h"
#include "common/io.h"
#include "common/processing.h"
#include "common/properties.h"
#include "common/mallocHelper.h"
#include "common/strutil.h"
#include "common/path.h"

#include <sys/sysinfo.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>

#define FF_CPUINFO_PATH "/proc/cpuinfo"

static double readTempFile(int dfd, const char* filename, FFstrbuf* buffer) {
    if (filename ? !ffReadFileBufferRelative(dfd, filename, buffer) : !ffReadFDBuffer(dfd, buffer)) {
        return FF_CPU_TEMP_UNSET;
    }

    double value = ffStrbufToDouble(buffer, FF_CPU_TEMP_UNSET); // millidegree Celsius
    if (value == FF_CPU_TEMP_UNSET) {
        return FF_CPU_TEMP_UNSET;
    }

    return value / 1000.;
}

static double parseTZDir(int dfd, FFstrbuf* buffer) {
    if (!ffReadFileBufferRelative(dfd, "type", buffer)) {
        return FF_CPU_TEMP_UNSET;
    }

    if (!ffStrbufStartsWithS(buffer, "cpu") &&
        !ffStrbufStartsWithS(buffer, "soc") &&
#if __x86_64__ || __i386__
        !ffStrbufEqualS(buffer, "x86_pkg_temp") &&
#endif
        true)
        return FF_CPU_TEMP_UNSET;

    return readTempFile(dfd, "temp", buffer);
}

static double parseHwmonDir(int dfd, FFstrbuf* buffer) {
    // https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface
    if (!ffReadFileBufferRelative(dfd, "name", buffer)) {
        return FF_CPU_TEMP_UNSET;
    }

    ffStrbufTrimRightSpace(buffer);

    if (
        !ffStrbufContainS(buffer, "cpu") &&
#if __x86_64__ || __i386__
        !ffStrbufEqualS(buffer, "k10temp") &&      // AMD
        !ffStrbufEqualS(buffer, "fam15h_power") && // AMD
        !ffStrbufEqualS(buffer, "coretemp") &&     // Intel
#endif
        true)
        return FF_CPU_TEMP_UNSET;

    return readTempFile(dfd, "temp1_input", buffer);
}

static double detectCPUTemp(const FFCPUOptions* options) {
    FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();

    if (options->tempSensor.length > 0) {
        FF_AUTO_CLOSE_FD int subfd = -1;
        const char* fileName = nullptr;
        if (ffStrbufStartsWithS(&options->tempSensor, "hwmon") && ffCharIsDigit(options->tempSensor.chars[strlen("hwmon")])) {
            FF_AUTO_CLOSE_FD int dfd = open("/sys/class/hwmon/", O_PATH | O_CLOEXEC);
            subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
            if (subfd >= 0) {
                fileName = "temp1_input";
            } else {
                subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_CLOEXEC);
            }
        } else if (ffStrbufStartsWithS(&options->tempSensor, "thermal_zone") && ffCharIsDigit(options->tempSensor.chars[strlen("thermal_zone")])) {
            FF_AUTO_CLOSE_FD int dfd = open("/sys/class/thermal/", O_PATH | O_CLOEXEC);
            subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
            if (subfd >= 0) {
                fileName = "temp";
            } else {
                subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_CLOEXEC);
            }
        } else if (ffStrbufStartsWithS(&options->tempSensor, "cputemp.") && ffCharIsDigit(options->tempSensor.chars[strlen("cputemp.")])) {
            FF_AUTO_CLOSE_FD int dfd = open("/sys/class/platform/", O_PATH | O_CLOEXEC);
            subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
            if (subfd >= 0) {
                fileName = "temp1_input";
            } else {
                subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_CLOEXEC);
            }
        } else if (ffIsAbsolutePath(options->tempSensor.chars)) {
            subfd = open(options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
            if (subfd >= 0) {
                fileName = "temp1_input";
            } else {
                subfd = open(options->tempSensor.chars, O_RDONLY | O_CLOEXEC);
            }
        }
        if (subfd < 0) {
            return FF_CPU_TEMP_UNSET;
        }

        return readTempFile(subfd, fileName, &buffer);
    }

    {
        FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/hwmon/");
        if (dirp) {
            int dfd = dirfd(dirp);

            struct dirent* entry;
            while ((entry = readdir(dirp)) != nullptr) {
                if (entry->d_name[0] == '.') {
                    continue;
                }

                FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
                if (subfd < 0) {
                    continue;
                }

                double result = parseHwmonDir(subfd, &buffer);
                if (result != FF_CPU_TEMP_UNSET) {
                    return result;
                }
            }
        }
    }
    {
        FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/thermal/");
        if (dirp) {
            int dfd = dirfd(dirp);
            struct dirent* entry;
            while ((entry = readdir(dirp)) != nullptr) {
                if (entry->d_name[0] == '.') {
                    continue;
                }
                if (!ffStrStartsWith(entry->d_name, "thermal_zone")) {
                    continue;
                }

                FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
                if (subfd < 0) {
                    continue;
                }

                double result = parseTZDir(subfd, &buffer);
                if (result != FF_CPU_TEMP_UNSET) {
                    return result;
                }
            }
        }
    }
    {
        FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/devices/platform/");
        if (dirp) {
            int dfd = dirfd(dirp);
            struct dirent* entry;
            while ((entry = readdir(dirp)) != nullptr) {
                if (entry->d_name[0] == '.') {
                    continue;
                }
                if (!ffStrStartsWith(entry->d_name, "cputemp.")) {
                    continue;
                }

                FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
                if (subfd < 0) {
                    continue;
                }

                double result = parseHwmonDir(subfd, &buffer);
                if (result != FF_CPU_TEMP_UNSET) {
                    return result;
                }
            }
        }
    }

    return FF_CPU_TEMP_UNSET;
}

static void detectNumaNodes(FFCPUResult* cpu) {
    FF_AUTO_CLOSE_DIR DIR* dir = opendir("/sys/devices/system/node/");
    if (!dir) {
        return;
    }

    struct dirent* entry;
    while ((entry = readdir(dir)) != nullptr) {
        if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) {
            continue;
        }
        if (ffStrStartsWith(entry->d_name, "node") && ffCharIsDigit(entry->d_name[strlen("node")])) {
            cpu->numaNodes++;
        }
    }
}

#ifdef __ANDROID__
    #include "common/settings.h"

static void detectQualcomm(FFCPUResult* cpu) {
    // https://en.wikipedia.org/wiki/List_of_Qualcomm_Snapdragon_systems_on_chips

    assert(cpu->name.length >= 2);
    uint32_t code = (uint32_t) strtoul(cpu->name.chars + 2, nullptr, 10);
    const char* name = nullptr;

    switch (code) {
        case 8975:
            name = "8 Elite Extreme Gen 6";
            break;
        case 8950:
            name = "8 Elite Gen 6";
            break;
        case 8845:
            name = "8 Gen 5";
            break;
        case 8850:
            name = "8 Elite Gen 5";
            break;
        case 8735:
            name = "8s Gen 4";
            break;
        case 8750:
            name = "8 Elite";
            break;
        case 8635:
            name = "8s Gen 3";
            break;
        case 8650:
            name = "8 Gen 3";
            break;
        case 8550:
            name = "8 Gen 2";
            break;
        case 8475:
            name = "8+ Gen 1";
            break;
        case 8425:
            name = "8+ Gen 1 4G";
            break;
        case 8450:
            name = "8 Gen 1";
            break;
        case 7750:
            name = "7 Gen 4";
            break;
        case 7675:
            name = "7+ Gen 3";
            break;
        case 7635:
            name = "7s Gen 3";
            break;
        case 7550:
            name = "7 Gen 3";
            break;
        case 7475:
            name = "7+ Gen 2";
            break;
        case 7435:
            name = "7s Gen 2";
            break;
        case 7450:
            name = "7 Gen 1";
            break;
        case 6650:
            name = "6 Gen 4";
            break;
        case 6375:
            name = "6s Gen 3";
            break;
        case 6475:
            name = "6 Gen 3";
            break;
        case 6225:
            name = "6s 4G Gen 2";
            break;
        case 6115:
            name = "6s 4G Gen 1";
            break;
        case 6450:
            name = "6 Gen 1";
            break;
        case 4850:
            name = "4 Gen 5";
            break;
        case 4635:
            name = "4s Gen 2";
            break;
        case 4450:
            name = "4 Gen 2";
            break;
        case 4375:
            name = "4 Gen 1";
            break;
    }

    if (name) {
        char str[32];
        ffStrCopy(str, cpu->name.chars, sizeof(str));
        ffStrbufSetF(&cpu->name, "Qualcomm Snapdragon %s [%s]", name, str);
        return;
    }
}

static void detectMediaTek(FFCPUResult* cpu) {
    // https://en.wikipedia.org/wiki/List_of_MediaTek_systems_on_chips

    assert(cpu->name.length >= 2);
    uint32_t code = (uint32_t) strtoul(cpu->name.chars + 2, nullptr, 10);
    const char* name = nullptr;

    switch (code) // The SOC code of MTK Dimensity series is full of mess
    {
        case 6995:
            name = "9600";
            break;
        case 6993:
            name = "9500";
            break;
        case 6991:
            name = "9400";
            break;
        case 6989:
        case 8796:
            name = "9300";
            break;
        case 6985:
            name = "9200";
            break;
        case 6983:
        case 8798:
            name = "9000";
            break;

        case 6899:
            name = "8400";
            break;
        case 6897:
        case 8792:
            name = "8300";
            break;
        case 6896:
            name = "8200";
            break;
        case 8795:
            name = "8100";
            break;
        case 6895:
            name = "8000";
            break;
    }

    if (name) {
        char str[32];
        ffStrCopy(str, cpu->name.chars, sizeof(str));
        ffStrbufSetF(&cpu->name, "MediaTek Dimensity %s [%s]", name, str);
        return;
    }
}

static void detectExynos(FFCPUResult* cpu) {
    // https://en.wikipedia.org/wiki/Exynos

    assert(cpu->name.length > 3);
    uint32_t code = (uint32_t) strtoul(cpu->name.chars + 3, nullptr, 10);
    const char* name = nullptr;

    switch (code) {
        case 9975:
            name = "2700";
            break;
        case 9965:
            name = "2600";
            break;
        case 9955:
            name = "2500";
            break;
        case 9945:
            name = "2400";
            break;
        // No 2300
        case 9925:
            name = "2200";
            break;
        case 9840:
            name = "2100";
            break;

        case 8865:
            name = "1680";
            break;
        case 8855:
            name = "1580";
            break;
        case 8845:
            name = "1480";
            break;
        case 8835:
            name = "1380";
            break;
        case 8535:
            name = "1330";
            break;
        case 8825:
            name = "1280";
            break;
        case 9815:
            name = "1080";
            break;

        case 9830:
            name = "990";
            break;
        case 9630:
            name = "980";
            break;

        case 8805:
            name = "880";
            break;
        case 3830:
            name = "850";
            break;
    }

    if (name) {
        char str[32];
        ffStrCopy(str, cpu->name.chars, sizeof(str));
        ffStrbufSetF(&cpu->name, "Samsung Exynos %s [%s]", name, str);
        return;
    }
}

static void detectAndroid(FFCPUResult* cpu) {
    if (cpu->name.length == 0) {
        if (ffSettingsGetAndroidProperty("ro.soc.model", &cpu->name)) {
            ffStrbufClear(&cpu->vendor); // We usually detect the vendor of CPU core as ARM, but instead we want the vendor of SOC
        }
    }
    if (cpu->vendor.length == 0) {
        if (!ffSettingsGetAndroidProperty("ro.soc.manufacturer", &cpu->vendor)) {
            if (!ffSettingsGetAndroidProperty("ro.product.product.manufacturer", &cpu->vendor)) {
                if (!ffSettingsGetAndroidProperty("ro.product.vendor.manufacturer", &cpu->vendor)) {
                    if (ffSettingsGetAndroidProperty("ro.mediatek.platform", &cpu->name)) {
                        ffStrbufSetStatic(&cpu->vendor, "MediaTek");
                    }
                }
            }
        }
    }

    if (ffStrbufEqualS(&cpu->vendor, "QTI")) {
        ffStrbufSetStatic(&cpu->vendor, "Qualcomm");
    } else if (ffStrbufIgnCaseEqualS(&cpu->vendor, "MediaTek")) { // sometimes "Mediatek"
        ffStrbufSetStatic(&cpu->vendor, "MediaTek");
    } else if (cpu->vendor.length > 0) {
        cpu->vendor.chars[0] = (char) toupper(cpu->vendor.chars[0]);
    }

    if (ffStrbufEqualS(&cpu->vendor, "Qualcomm") && ffStrbufStartsWithS(&cpu->name, "SM")) {
        detectQualcomm(cpu);
    } else if (ffStrbufEqualS(&cpu->vendor, "MediaTek") && ffStrbufStartsWithS(&cpu->name, "MT")) {
        detectMediaTek(cpu);
    } else if (ffStrbufEqualS(&cpu->vendor, "Samsung") && ffStrbufStartsWithS(&cpu->name, "s5e")) {
        cpu->name.chars[0] = 'S';
        cpu->name.chars[2] = 'E';
        detectExynos(cpu);
    }
}
#endif

#if __arm__ || __aarch64__
    #include "cpu_arm.h"

static void detectArmName(FFstrbuf* cpuinfo, FFCPUResult* cpu, uint32_t implId) {
    char* line = nullptr;
    size_t len = 0;
    uint32_t lastPartId = UINT32_MAX;
    while (ffStrbufGetline(&line, &len, cpuinfo)) {
        if (!ffStrStartsWith(line, "CPU part\t: ")) {
            continue;
        }
        uint32_t partId = (uint32_t) strtoul(line + strlen("CPU part\t: "), nullptr, 16);
        const char* name = nullptr;
        switch (implId) {
            case 0x41:
                name = armPartId2name(partId);
                break;
            case 0x42:
                name = brcmPartId2name(partId);
                break;
            case 0x43:
                name = caviumPartId2name(partId);
                break;
            case 0x44:
                name = decPartId2name(partId);
                break;
            case 0x46:
                name = fujitsuPartId2name(partId);
                break;
            case 0x48:
                name = hisiPartId2name(partId);
                break;
            case 0x4e:
                name = nvidiaPartId2name(partId);
                break;
            case 0x50:
                name = apmPartId2name(partId);
                break;
            case 0x51:
                name = qcomPartId2name(partId);
                break;
            case 0x53:
                name = samsungPartId2name(partId);
                break;
            case 0x56:
                name = marvellPartId2name(partId);
                break;
            case 0x61:
                if (partId == 0) {
                    // https://github.com/Dr-Noob/cpufetch/issues/213#issuecomment-1927782105
                    ffStrbufSetStatic(&cpu->name, "Virtualized Apple Silicon");
                    ffStrbufGetlineRestore(&line, &len, cpuinfo);
                    return;
                }
                name = applePartId2name(partId);
                break;
            case 0x66:
                name = faradayPartId2name(partId);
                break;
            case 0x69:
                name = intelPartId2name(partId);
                break;
            case 0x6d:
                name = msPartId2name(partId);
                break;
            case 0x70:
                name = ftPartId2name(partId);
                break;
            case 0xc0:
                name = amperePartId2name(partId);
                break;
        }
        if (lastPartId != partId) {
            if (lastPartId != UINT32_MAX) {
                ffStrbufAppendC(&cpu->name, '+');
            }
            if (name) {
                ffStrbufAppendS(&cpu->name, name);
            } else if (partId) {
                ffStrbufAppendF(&cpu->name, "%s-%X", cpu->vendor.chars, partId);
            } else {
                ffStrbufAppend(&cpu->name, &cpu->vendor);
            }
            lastPartId = partId;
        }
    }
}
#endif

static const char* parseCpuInfo(
    FFstrbuf* cpuinfo,
    FFCPUResult* cpu,
    [[maybe_unused]] FFstrbuf* physicalCoresBuffer,
    [[maybe_unused]] FFstrbuf* cpuMHz,
    [[maybe_unused]] FFstrbuf* cpuIsa,
    [[maybe_unused]] FFstrbuf* cpuUarch,
    [[maybe_unused]] FFstrbuf* cpuImplementer) {
    char* line = nullptr;
    size_t len = 0;

    while (ffStrbufGetline(&line, &len, cpuinfo)) {
        // Stop after reasonable information is acquired
        if ((*line == '\0' || *line == '\n') && cpu->name.length > 0) {
            ffStrbufGetlineRestore(&line, &len, cpuinfo);
            break;
        }

        (void) (
// arm64 doesn't have "model name"; arm32 does have "model name" but its value is not useful.
// "Hardware" should always be used in this case
#if __x86_64__ || __i386__
            (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) ||
            (cpu->vendor.length == 0 && ffParsePropLine(line, "vendor_id :", &cpu->vendor)) ||
            (physicalCoresBuffer->length == 0 && ffParsePropLine(line, "cpu cores :", physicalCoresBuffer)) ||
            (cpuMHz->length == 0 && ffParsePropLine(line, "cpu MHz :", cpuMHz)) ||
#elif __arm__ || __aarch64__
            (cpuImplementer->length == 0 && ffParsePropLine(line, "CPU implementer :", cpuImplementer)) ||
            (cpu->name.length == 0 && ffParsePropLine(line, "Hardware :", &cpu->name)) || // For Android devices
#elif __powerpc__ || __powerpc
            (cpuMHz->length == 0 && ffParsePropLine(line, "clock :", cpuMHz)) ||
            (cpu->name.length == 0 && ffParsePropLine(line, "cpu :", &cpu->name)) ||
#elif __mips__ || __mips
            (cpu->name.length == 0 && ffParsePropLine(line, "cpu model :", &cpu->name)) ||
#elif __loongarch__
            (cpu->name.length == 0 && ffParsePropLine(line, "Model Name :", &cpu->name)) ||
            (cpuMHz->length == 0 && ffParsePropLine(line, "CPU MHz :", cpuMHz)) ||
#elif __riscv__ || __riscv
            (cpuIsa->length == 0 && ffParsePropLine(line, "isa :", cpuIsa)) ||
            (cpuUarch->length == 0 && ffParsePropLine(line, "uarch :", cpuUarch)) ||
#elif __s390x__
            (cpu->name.length == 0 && ffParsePropLine(line, "machine :", &cpu->name)) ||
            (cpu->vendor.length == 0 && ffParsePropLine(line, "vendor_id :", &cpu->vendor)) ||
            (cpuMHz->length == 0 && ffParsePropLine(line, "cpu MHz static :", cpuMHz)) ||
#elif __ia64__
            (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) ||
            (cpu->vendor.length == 0 && ffParsePropLine(line, "vendor :", &cpu->vendor)) ||
            (cpuMHz->length == 0 && ffParsePropLine(line, "cpu MHz :", cpuMHz)) ||
#elif __hppa__
            (cpu->name.length == 0 && ffParsePropLine(line, "cpu :", &cpu->name)) ||
#elif __sh__
            (cpu->name.length == 0 && ffParsePropLine(line, "cpu type :", &cpu->name)) ||
#else
            (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) ||
            (cpu->name.length == 0 && ffParsePropLine(line, "model :", &cpu->name)) ||
            (cpu->name.length == 0 && ffParsePropLine(line, "cpu model :", &cpu->name)) ||
            (cpu->name.length == 0 && ffParsePropLine(line, "hardware :", &cpu->name)) ||
            (cpu->name.length == 0 && ffParsePropLine(line, "processor :", &cpu->name)) ||
#endif

            false);
    }

    return nullptr;
}

static uint32_t getFrequency(int policyFd, const char* cpuinfoFileName, const char* scalingFileName, FFstrbuf* buffer) {
    bool ok = ffReadFileBufferRelative(policyFd, cpuinfoFileName, buffer);
    if (ok) {
        return (uint32_t) (ffStrbufToUInt(buffer, 0) / 1000);
    }

    if (scalingFileName) {
        ok = ffReadFileBufferRelative(policyFd, scalingFileName, buffer);
        if (ok) {
            return (uint32_t) (ffStrbufToUInt(buffer, 0) / 1000);
        }
    }

    return 0;
}

static bool detectFrequency(FFCPUResult* cpu, const FFCPUOptions* options) {
    const char* basePath = "/sys/devices/system/cpu/cpufreq/";
    FF_AUTO_CLOSE_DIR DIR* dir = opendir(basePath);
    if (!dir) {
        return false;
    }
    int freqFd = dirfd(dir);

    FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();

    struct dirent* entry;
    while ((entry = readdir(dir)) != nullptr) {
        if (ffStrStartsWith(entry->d_name, "policy") && ffCharIsDigit(entry->d_name[strlen("policy")])) {
            FF_AUTO_CLOSE_FD int policyFd = openat(freqFd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_PATH);
            if (policyFd < 0) {
                continue;
            }

            uint32_t fmax = getFrequency(policyFd, "cpuinfo_max_freq", "scaling_max_freq", &buffer);
            if (fmax == 0) {
                continue;
            }

            if (cpu->frequencyMax >= fmax) {
                if (!options->showPeCoreCount) {
                    continue;
                }
            } else {
                cpu->frequencyMax = fmax;
            }

            uint32_t fbase = getFrequency(policyFd, "base_frequency", nullptr, &buffer);
            if (fbase > 0) {
                cpu->frequencyBase = cpu->frequencyBase > fbase ? cpu->frequencyBase : fbase;
            }

            if (options->showPeCoreCount) {
                uint32_t freq = fbase == 0 ? fmax : fbase; // seems base frequencies are more stable
                uint32_t ifreq = 0;
                while (cpu->coreTypes[ifreq].freq != freq && cpu->coreTypes[ifreq].freq > 0) {
                    ++ifreq;
                }
                if (cpu->coreTypes[ifreq].freq == 0) {
                    cpu->coreTypes[ifreq].freq = freq;
                }
                if (ffReadFileBufferRelative(policyFd, "affected_cpus", &buffer)) {
                    cpu->coreTypes[ifreq].count += ffStrbufCountC(&buffer, ' ') + 1;
                }
            }
        }
    }
    return true;
}

#if __i386__ || __x86_64__

[[maybe_unused]] static uint16_t getPackageCount(FFstrbuf* cpuinfo) {
    const char* p = cpuinfo->chars;
    uint64_t low = 0, high = 0;

    while ((p = memmem(p, cpuinfo->length - (uint32_t) (p - cpuinfo->chars), "\nphysical id\t:", strlen("\nphysical id\t:")))) {
        p += strlen("\nphysical id\t:");
        char* pend;
        unsigned long long id = strtoul(p, &pend, 10);
        if (__builtin_expect(id > 64, false)) { // Do 129-socket boards exist?
            high |= 1ULL << (id - 64);
        } else {
            low |= 1ULL << id;
        }
        p = pend;
    }

    return (uint16_t) (__builtin_popcountll(low) + __builtin_popcountll(high));
}

[[maybe_unused]] static const char* detectCPUX86(const FFCPUOptions* options, FFCPUResult* cpu) {
    FF_STRBUF_AUTO_DESTROY cpuinfo = ffStrbufCreateA(PROC_FILE_BUFFSIZ);
    if (!ffReadFileBuffer(FF_CPUINFO_PATH, &cpuinfo) || cpuinfo.length == 0) {
        return "ffReadFileBuffer(\"" FF_CPUINFO_PATH "\") failed";
    }

    FF_STRBUF_AUTO_DESTROY physicalCoresBuffer = ffStrbufCreate();
    FF_STRBUF_AUTO_DESTROY cpuMHz = ffStrbufCreate();
    const char* error = parseCpuInfo(&cpuinfo, cpu, &physicalCoresBuffer, &cpuMHz, nullptr, nullptr, nullptr);
    if (error) {
        return error;
    }

    cpu->coresLogical = (uint16_t) get_nprocs_conf();
    cpu->coresOnline = (uint16_t) get_nprocs();
    cpu->packages = getPackageCount(&cpuinfo);
    cpu->coresPhysical = (uint16_t) ffStrbufToUInt(&physicalCoresBuffer, 0); // physical cores in single package
    if (cpu->coresPhysical > 0 && cpu->packages > 1) {
        cpu->coresPhysical *= cpu->packages;
    }

    // Ref https://github.com/fastfetch-cli/fastfetch/issues/1194#issuecomment-2295058252
    ffCPUDetectByCpuid(cpu);
    if (!detectFrequency(cpu, options) || cpu->frequencyBase == 0) {
        cpu->frequencyBase = (uint32_t) ffStrbufToUInt(&cpuMHz, 0);
    }

    detectNumaNodes(cpu);

    return nullptr;
}

#else

static const char* detectPhysicalCores(FFCPUResult* cpu) {
    int dfd = open("/sys/devices/system/cpu/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (dfd < 0) {
        return "open(\"/sys/devices/system/cpu/\") failed";
    }

    FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd);
    if (!dir) {
        close(dfd);
        return "fdopendir(dfd) failed";
    }

    uint64_t pkgLow = 0, pkgHigh = 0;

    struct dirent* entry;
    FF_LIST_AUTO_DESTROY cpuList = ffListCreate();
    while ((entry = readdir(dir)) != nullptr) {
        if (entry->d_type != DT_DIR || !ffStrStartsWith(entry->d_name, "cpu") || !ffCharIsDigit(entry->d_name[strlen("cpu")])) {
            continue;
        }

        FF_AUTO_CLOSE_FD int cpuxfd = openat(dirfd(dir), entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
        if (cpuxfd < 0) {
            continue;
        }

        char buf[128];

        // Check if the directory contains a file named "topology/physical_package_id"
        // that lists the physical package id of the CPU.

        ssize_t len = ffReadFileDataRelative(cpuxfd, "topology/physical_package_id", sizeof(buf) - 1, buf);
        if (len > 0) {
            buf[len] = '\0';
            unsigned long long id = strtoull(buf, nullptr, 10);
            if (__builtin_expect(id < 128, true)) {
                // Do 129-socket boards exist?
                if (__builtin_expect(id >= 64, false)) {
                    pkgHigh |= 1ULL << (id - 64);
                } else {
                    pkgLow |= 1ULL << id;
                }
            }
        }

        // Check if the directory contains a file named "topology/core_cpus_list"
        // that lists the logical cores in the same physical core.

        len = ffReadFileDataRelative(cpuxfd, "topology/core_cpus_list", sizeof(buf) - 1, buf);
        if (len > 0) {
            buf[len] = '\0'; // low[-high, low-high, ...]

            char* pend;
            // We assume that the different physical cores exposes different logical core ids,
            // so that the first `low` is always different between different physical cores.
            uint32_t coreId = (uint32_t) strtoul(buf, &pend, 10);
            if (pend == buf) {
                break;
            }

            bool found = false;
            FF_LIST_FOR_EACH (uint32_t, id, cpuList) {
                if (*id == coreId) {
                    // This core is already counted
                    found = true;
                    break;
                }
            }
            if (!found) {
                *FF_LIST_ADD(uint32_t, cpuList) = coreId;
            }
        }
    }

    cpu->coresPhysical = (uint16_t) cpuList.length;
    cpu->packages = (uint16_t) (__builtin_popcountll(pkgLow) + __builtin_popcountll(pkgHigh));
    return nullptr;
}

[[maybe_unused]] static bool detectSnapdragonX(FFstrbuf* name, const char* model) {
    // SoC models are enumerated in the form of `x<gen>[e|p]<part><rev>`, e.g. `x1e80100`,
    // which is marketed as `X1E-80-100`, where `e` stands for Elite and `p` for Plus.
    // https://en.wikipedia.org/wiki/List_of_Qualcomm_Snapdragon_systems_on_chips#Snapdragon_X_series
    const char* prefix;
    uint32_t prefixLen;

    if (ffStrStartsWith(model, "x1e")) {
        prefix = "Qualcomm Snapdragon X Elite";
        prefixLen = 3;
    } else if (ffStrStartsWith(model, "x1p")) {
        prefix = "Qualcomm Snapdragon X Plus";
        prefixLen = 3;
    } else if (ffStrStartsWith(model, "x2e")) {
        // Only X2E-94-100 and X2E-96-100 are branded as Extreme
        prefix = model[3] == '9' ? "Qualcomm Snapdragon X2 Elite Extreme" : "Qualcomm Snapdragon X2 Elite";
        prefixLen = 3;
    } else if (ffStrStartsWith(model, "x2p")) {
        prefix = "Qualcomm Snapdragon X2 Plus";
        prefixLen = 3;
    } else if (ffStrStartsWith(model, "x1")) {
        prefix = "Qualcomm Snapdragon X";
        prefixLen = 2;
    } else {
        return false;
    }

    uint32_t length = (uint32_t) strlen(model);
    bool splitCode = length - prefixLen == 5;

    ffStrbufSetF(name, "%s X", prefix);
    for (uint32_t i = 1; i < length; ++i) {
        if (i == prefixLen || (splitCode && i == length - 3)) {
            ffStrbufAppendC(name, '-');
        }
        ffStrbufAppendC(name, (char) toupper(model[i]));
    }
    return true;
}

[[maybe_unused]] static void parseIsa(FFstrbuf* cpuIsa) {
    // Always use the last part of the ISA string. Ref: #590 #1204
    ffStrbufSubstrAfterLastC(cpuIsa, ' ');

    if (ffStrbufStartsWithS(cpuIsa, "rv")) {
        // RISC-V ISA string example: "rv64imafdch_zicsr_zifencei".
        // The _z parts are not important for CPU showcasing, so we remove them.
        if (ffStrbufContainC(cpuIsa, '_')) {
            ffStrbufSubstrBeforeFirstC(cpuIsa, '_');
        }
        // Then we replace "imafd" with "g" since "g" is a shorthand.
        if (ffStrbufContainS(cpuIsa, "imafd")) {
            // Remove 4 of the 5 characters and replace the remaining one with "g".
            ffStrbufRemoveSubstr(cpuIsa, 4, 8);
            cpuIsa->chars[4] = 'g';
        }
        // The final ISA output of the above example is "rv64gch".
    }
}

[[maybe_unused]] static void detectSocName(FFCPUResult* cpu) {
    if (cpu->name.length > 0) {
        return;
    }

    // [x-vendor,x-model\0]*N
    char content[512];
    ssize_t length = ffReadFileData("/sys/firmware/devicetree/base/compatible", ARRAY_SIZE(content), content);
    if (length < 4) {
        return; // v,m\0
    }

    if (content[length - 1] != '\0') {
        return; // must end with \0
    }

    --length;

    char* vendor = nullptr;
    char* model = nullptr;

    for (char* p; length > 0; length = p ? (ssize_t) (p - content) - 1 : 0) {
        p = memrchr(content, '\0', (size_t) length);

        vendor = p /* first entry */ ? p + 1 : content;

        size_t partLen = (size_t) (length - (vendor - content));
        if (partLen < 3) {
            continue;
        }

        char* comma = memchr(vendor, ',', partLen);
        if (!comma) {
            continue;
        }

        size_t vendorLen = (size_t) (comma - vendor);
        if (vendorLen == 0) {
            continue;
        }

        model = comma + 1;
        size_t modelLen = (size_t) (partLen - (size_t) (model - vendor));
        if (modelLen == 0) {
            continue;
        }

        if ((modelLen >= strlen("-platform") && ffStrEndsWith(model, "-platform")) ||
            (modelLen >= strlen("-soc") && ffStrEndsWith(model, "-soc"))) {
            continue;
        }

        *comma = '\0';
        break;
    }

    if (!length) {
        return;
    }

    if (false) {
    }
    #if __aarch64__
    else if (ffStrEquals(vendor, "apple")) {
        // https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/boot/dts/apple
        if (model[0] == 't') {
            uint32_t deviceId = (uint32_t) strtoul(model + 1, nullptr, 10);
            ffStrbufSetStatic(&cpu->name, ffCPUAppleCodeToName(deviceId));

            if (!cpu->name.length) {
                ffStrbufSetS(&cpu->name, "Apple Silicon ");
                ffStrbufAppendS(&cpu->name, model);
            }
        } else {
            ffStrbufSetS(&cpu->name, model);
        }

        ffStrbufSetStatic(&cpu->vendor, "Apple");
    }
    #endif
    else if (ffStrEquals(vendor, "qcom")) {
        // https://elixir.bootlin.com/linux/latest/source/arch/arm64/boot/dts/qcom
        if (!detectSnapdragonX(&cpu->name, model)) {
            if (ffStrStartsWith(model, "sc")) {
                const char* code = model + 2;
                uint32_t deviceId = (uint32_t) strtoul(code, nullptr, 10);
                ffStrbufSetStatic(&cpu->name, ffCPUQualcommCodeToName(deviceId));
                if (!cpu->name.length) {
                    ffStrbufAppendS(&cpu->name, "Qualcomm Snapdragon SC");
                    ffStrbufAppendS(&cpu->name, code);
                }
            } else {
                ffStrbufSetS(&cpu->name, model);
            }
        }

        ffStrbufSetStatic(&cpu->vendor, "Qualcomm");
    } else if (ffStrEquals(vendor, "brcm")) {
        // Raspberry Pi
        ffStrbufSetStatic(&cpu->vendor, "Broadcom");
        for (const char* p = model; *p; ++p) {
            ffStrbufAppendC(&cpu->name, (char) toupper(*p));
        }
    } else if (ffStrEquals(vendor, "thead")) {
        // Lichee Pi?
        ffStrbufSetStatic(&cpu->vendor, "T-Head");
        for (const char* p = model; *p; ++p) {
            ffStrbufAppendC(&cpu->name, (char) toupper(*p));
        }
    } else {
        ffStrbufSetS(&cpu->name, model);
        ffStrbufSetS(&cpu->vendor, vendor);
        cpu->vendor.chars[0] = (char) toupper(vendor[0]);
    }
}

    #ifdef __loongarch__
[[maybe_unused]] static uint16_t getLoongarchPropCount(FFstrbuf* cpuinfo, const char* key) {
    const char* p = cpuinfo->chars;
    uint64_t low = 0, high = 0;
    uint32_t keylen = (uint32_t) strlen(key);

    while ((p = memmem(p, cpuinfo->length - (uint32_t) (p - cpuinfo->chars), key, keylen))) {
        p += keylen;
        char* pend;
        unsigned long id = strtoul(p, &pend, 10);
        if (__builtin_expect(id > 64, false)) {
            high |= 1UL << (id - 64);
        } else {
            low |= 1UL << id;
        }
        p = pend;
    }

    return (uint16_t) (__builtin_popcountll(low) + __builtin_popcountll(high));
}
    #endif

[[maybe_unused]] static const char* detectCPUOthers(const FFCPUOptions* options, FFCPUResult* cpu) {
    cpu->coresLogical = (uint16_t) get_nprocs_conf();
    cpu->coresOnline = (uint16_t) get_nprocs();

    #if __ANDROID__
    detectAndroid(cpu);
    #elif !__powerpc__ && !__powerpc
    detectSocName(cpu);
    #endif

    detectFrequency(cpu, options);

    if (cpu->name.length == 0) {
        FF_STRBUF_AUTO_DESTROY cpuinfo = ffStrbufCreateA(PROC_FILE_BUFFSIZ);
        if (!ffReadFileBuffer(FF_CPUINFO_PATH, &cpuinfo) || cpuinfo.length == 0) {
            return "ffReadFileBuffer(\"" FF_CPUINFO_PATH "\") failed";
        }

        FF_STRBUF_AUTO_DESTROY cpuMHz = ffStrbufCreate();
        FF_STRBUF_AUTO_DESTROY cpuIsa = ffStrbufCreate();
        FF_STRBUF_AUTO_DESTROY cpuUarch = ffStrbufCreate();
        FF_STRBUF_AUTO_DESTROY cpuImplementerStr = ffStrbufCreate();

        const char* error = parseCpuInfo(&cpuinfo, cpu, nullptr, &cpuMHz, &cpuIsa, &cpuUarch, &cpuImplementerStr);
        if (error) {
            return error;
        }

        if (cpu->frequencyBase == 0) {
            cpu->frequencyBase = (uint32_t) ffStrbufToUInt(&cpuMHz, 0);
        }

    #if __arm__ || __aarch64__
        uint32_t cpuImplementer = (uint32_t) strtoul(cpuImplementerStr.chars, nullptr, 16);
        ffStrbufSetStatic(&cpu->vendor, hwImplId2Vendor(cpuImplementer));

        if (cpu->name.length == 0) {
            detectArmName(&cpuinfo, cpu, cpuImplementer);
        }
    #elif __riscv__ || __riscv
        if (cpu->name.length == 0) {
            if (cpuUarch.length > 0) {
                if (cpu->name.length > 0) {
                    ffStrbufAppendC(&cpu->name, ' ');
                }
                ffStrbufAppend(&cpu->name, &cpuUarch);
            }

            if (cpuIsa.length > 0) {
                parseIsa(&cpuIsa);
                if (cpu->name.length > 0) {
                    ffStrbufAppendC(&cpu->name, ' ');
                }
                ffStrbufAppend(&cpu->name, &cpuIsa);
            }
        }
    #elif __loongarch__
        cpu->packages = getLoongarchPropCount(&cpuinfo, "\npackage\t\t\t:");
        cpu->coresPhysical = getLoongarchPropCount(&cpuinfo, "\ncore\t\t\t:");
        if (cpu->packages > 1) {
            cpu->coresPhysical *= cpu->packages;
        }
    #elif __s390x__
        if (cpu->name.length) {
            ffStrbufPrependS(&cpu->name, "Machine ");
        }
    #endif
    }

    if (cpu->coresPhysical == 0) {
        detectPhysicalCores(cpu);
    }

    ffCPUDetectByCpuid(cpu);
    detectNumaNodes(cpu);

    return nullptr;
}
#endif

const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) {
    cpu->temperature = options->temp ? detectCPUTemp(options) : FF_CPU_TEMP_UNSET;

#if __x86_64__ || __i386__
    return detectCPUX86(options, cpu);
#else
    return detectCPUOthers(options, cpu);
#endif
}
