#include "cpu.h"
#include "common/windows/registry.h"
#include "common/windows/nt.h"
#include "common/mallocHelper.h"
#include "common/smbiosHelper.h"

#include <windows.h>
#include "common/windows/perflib_.h"
#include <wchar.h>

static inline void ffPerfCloseQueryHandle(HANDLE* phQuery)
{
    if (*phQuery != NULL)
    {
        PerfCloseQueryHandle(*phQuery);
        *phQuery = NULL;
    }
}

const char* detectThermalTemp(const FFCPUOptions* options, double* result)
{
    struct FFPerfQuerySpec
    {
        PERF_COUNTER_IDENTIFIER Identifier;
        WCHAR Name[16];
    } querySpec = {
        .Identifier = {
            // Thermal Zone Information
            // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\_V2Providers\{383487a6-3676-4870-a4e7-d45b30c35629}\{52bc5412-dac2-449c-8bc2-96443888fe6b}
            .CounterSetGuid = { 0x52bc5412, 0xdac2, 0x449c, {0x8b, 0xc2, 0x96, 0x44, 0x38, 0x88, 0xfe, 0x6b} },
            .Size = sizeof(querySpec),
            .CounterId = PERF_WILDCARD_COUNTER,
            .InstanceId = PERF_WILDCARD_COUNTER,
        },
        .Name = L"\\_TZ.CPUZ", // The standard(?) instance name for CPU temperature in the thermal provider
    };

    if (options->tempSensor.length > 0)
    {
        int written = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, options->tempSensor.chars, (int) options->tempSensor.length, querySpec.Name, (int)(ARRAY_SIZE(querySpec.Name) - 1));
        if (written == 0)
            return "Invalid temp sensor string";
        querySpec.Name[written] = L'\0';
    }

    DWORD dataSize = 0;
    if (PerfEnumerateCounterSetInstances(NULL, &querySpec.Identifier.CounterSetGuid, NULL, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY)
        return "PerfEnumerateCounterSetInstances() failed";

    if (dataSize <= sizeof(PERF_INSTANCE_HEADER))
        return "No `Thermal Zone Information` instances found";

    {
        FF_AUTO_FREE PERF_INSTANCE_HEADER* const pHead = malloc(dataSize);
        if (PerfEnumerateCounterSetInstances(NULL, &querySpec.Identifier.CounterSetGuid, pHead, dataSize, &dataSize) != ERROR_SUCCESS)
            return "PerfEnumerateCounterSetInstances() failed to get instance headers";

        PERF_INSTANCE_HEADER* pInstanceHeader = pHead;
        while (1)
        {
            const wchar_t* instanceName = (const wchar_t*)((BYTE*)pInstanceHeader + sizeof(*pInstanceHeader));
            if (wcscmp(instanceName, querySpec.Name) == 0)
                break;

            dataSize -= pInstanceHeader->Size;
            if (dataSize == 0)
                break;
            pInstanceHeader = (PERF_INSTANCE_HEADER*)((BYTE*)pInstanceHeader + pInstanceHeader->Size);
        }

        if (dataSize == 0)
        {
            if (options->tempSensor.length > 0)
                return "Unable to find CPU sensor";

            const wchar_t* instanceName = (const wchar_t*)((BYTE*)pHead + sizeof(*pHead));
            wcscpy(querySpec.Name, instanceName); // Use the first instance name if the specific one is not found
        }
    }

    __attribute__((__cleanup__(ffPerfCloseQueryHandle)))
    HANDLE hQuery = NULL;

    if (PerfOpenQueryHandle(NULL, &hQuery) != ERROR_SUCCESS)
        return "PerfOpenQueryHandle() failed";

    if (PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)) != ERROR_SUCCESS)
        return "PerfAddCounters() failed";

    if (querySpec.Identifier.Status != ERROR_SUCCESS)
        return "PerfAddCounters() reports invalid identifier";

    if (PerfQueryCounterData(hQuery, NULL, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY)
        return "PerfQueryCounterData(NULL) failed";

    if (dataSize <= sizeof(PERF_DATA_HEADER) + sizeof(PERF_COUNTER_HEADER)) // PERF_ERROR_RETURN, should not happen
        return "instance doesn't exist";

    FF_AUTO_FREE PERF_DATA_HEADER* const pDataHeader = malloc(dataSize);

    if (PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize) != ERROR_SUCCESS)
        return "PerfQueryCounterData(pDataHeader) failed";

    PERF_COUNTER_HEADER* pCounterHeader = (PERF_COUNTER_HEADER*)(pDataHeader + 1);
    if (pCounterHeader->dwType != PERF_MULTIPLE_COUNTERS)
        return "Invalid counter type";

    PERF_MULTI_COUNTERS* pMultiCounters = (PERF_MULTI_COUNTERS*)(pCounterHeader + 1);
    PERF_COUNTER_DATA* pCounterData = (PERF_COUNTER_DATA*)((BYTE*)pMultiCounters + pMultiCounters->dwSize);

    for (ULONG iCounter = 0; iCounter != pMultiCounters->dwCounters; iCounter++)
    {
        if (pCounterData->dwDataSize == sizeof(int32_t))
        {
            DWORD* pCounterIds = (DWORD*)(pMultiCounters + 1);
            int32_t value = *(int32_t*)(pCounterData + 1);
            if (value == 0)
                return "Temperature data is zero";

            switch (pCounterIds[iCounter]) {
            case 0: // Temperature
                *result = value - 273;
                break;
            case 3: // High Precision Temperature
                *result = value / 10.0 - 273;
                break;
            }
        }

        pCounterData = (PERF_COUNTER_DATA*)((BYTE*)pCounterData + pCounterData->dwSize);
    }

    return NULL;
}

// 7.5
typedef struct FFSmbiosProcessorInfo
{
    FFSmbiosHeader Header;

    uint8_t SocketDesignation; // string
    uint8_t ProcessorType; // enum
    uint8_t ProcessorFamily; // enum
    uint8_t ProcessorManufacturer; // string
    uint64_t ProcessorID; // varies
    uint8_t ProcessorVersion; // string
    uint8_t Voltage; // varies
    uint16_t ExternalClock; // varies
    uint16_t MaxSpeed; // varies
    uint16_t CurrentSpeed; // varies
    uint8_t Status; // varies
    uint8_t ProcessorUpgrade; // enum

    // 2.1+
    uint16_t L1CacheHandle; // varies
    uint16_t L2CacheHandle; // varies
    uint16_t L3CacheHandle; // varies

    // 2.3+
    uint8_t SerialNumber; // string
    uint8_t AssertTag; // string
    uint8_t PartNumber; // string

    // 2.5+
    uint8_t CoreCount; // varies
    uint8_t CoreEnabled; // varies
    uint8_t ThreadCount; // varies
    uint16_t ProcessorCharacteristics; // bit field

    // 2.6+
    uint16_t ProcessorFamily2; // enum

    // 3.0+
    uint16_t CoreCount2; // varies
    uint16_t CoreEnabled2; // varies
    uint16_t ThreadCount2; // varies

    // 3.6+
    uint16_t ThreadEnabled; // varies
} __attribute__((__packed__)) FFSmbiosProcessorInfo;

static_assert(offsetof(FFSmbiosProcessorInfo, ThreadEnabled) == 0x30,
    "FFSmbiosProcessorInfo: Wrong struct alignment");

static const char* detectMaxSpeedBySmbios(FFCPUResult* cpu)
{
    const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable();
    if (!smbiosTable)
        return "Failed to get SMBIOS data";

    const FFSmbiosProcessorInfo* data = (const FFSmbiosProcessorInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_PROCESSOR_INFO];

    if (!data)
        return "Processor information is not found in SMBIOS data";

    while (data->ProcessorType != 0x03 /*Central Processor*/ || (data->Status & 0b00000111) != 1 /*Enabled*/)
    {
        data = (const FFSmbiosProcessorInfo*) ffSmbiosNextEntry(&data->Header);
        if (data->Header.Type != FF_SMBIOS_TYPE_PROCESSOR_INFO)
            return "No active CPU is found in SMBIOS data";
    }

    uint32_t speed = data->MaxSpeed;
    // Sometimes SMBIOS reports invalid value. We assume that max speed is small than 2x of base
    if (speed < cpu->frequencyBase || speed > cpu->frequencyBase * 2)
        return "Possible invalid CPU max speed in SMBIOS data. See #800";

    cpu->frequencyMax = speed;

    return NULL;
}

static const char* detectNCores(FFCPUResult* cpu)
{
    DWORD length = 0;
    GetLogicalProcessorInformationEx(RelationAll, NULL, &length);
    if (length == 0)
        return "GetLogicalProcessorInformationEx(RelationAll, NULL, &length) failed";

    SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* FF_AUTO_FREE
        pProcessorInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)malloc(length);

    if (!pProcessorInfo || !GetLogicalProcessorInformationEx(RelationAll, pProcessorInfo, &length))
        return "GetLogicalProcessorInformationEx(RelationAll, pProcessorInfo, &length) failed";

    for(
        SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* ptr = pProcessorInfo;
        (uint8_t*)ptr < ((uint8_t*)pProcessorInfo) + length;
        ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*)(((uint8_t*)ptr) + ptr->Size)
    )
    {
        if (ptr->Relationship == RelationGroup)
        {
            for (uint32_t index = 0; index < ptr->Group.ActiveGroupCount; ++index)
            {
                cpu->coresOnline += ptr->Group.GroupInfo[index].ActiveProcessorCount;
                cpu->coresLogical += ptr->Group.GroupInfo[index].MaximumProcessorCount;
            }
        }
        else if (ptr->Relationship == RelationProcessorCore)
            ++cpu->coresPhysical;
        else if (ptr->Relationship == RelationProcessorPackage)
            ++cpu->packages;
        else if (ptr->Relationship == RelationNumaNode)
            ++cpu->numaNodes;
    }

    return NULL;
}

static const char* detectByRegistry(FFCPUResult* cpu)
{
    FF_HKEY_AUTO_DESTROY hKey = NULL;
    if(!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", &hKey, NULL))
        return "ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L\"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\", &hKey, NULL) failed";

    ffRegReadStrbuf(hKey, L"ProcessorNameString", &cpu->name, NULL);
    if (ffRegReadStrbuf(hKey, L"VendorIdentifier", &cpu->vendor, NULL))
        ffStrbufTrimRightSpace(&cpu->vendor);

    if (cpu->coresLogical == 0)
    {
        FF_HKEY_AUTO_DESTROY hProcsKey = NULL;
        if (ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor", &hProcsKey, NULL))
        {
            uint32_t cores;
            if (ffRegGetNSubKeys(hProcsKey, &cores, NULL))
                cpu->coresOnline = cpu->coresPhysical = cpu->coresLogical = (uint16_t) cores;
        }
    }

    uint32_t mhz;
    if(ffRegReadUint(hKey, L"~MHz", &mhz, NULL))
        cpu->frequencyBase = mhz;

    return NULL;
}

static const char* detectCoreTypes(FFCPUResult* cpu)
{
    FF_AUTO_FREE PROCESSOR_POWER_INFORMATION* pinfo = calloc(cpu->coresLogical, sizeof(PROCESSOR_POWER_INFORMATION));
    if (!NT_SUCCESS(NtPowerInformation(ProcessorInformation, NULL, 0, pinfo, (ULONG) sizeof(PROCESSOR_POWER_INFORMATION) * cpu->coresLogical)))
        return "NtPowerInformation(ProcessorInformation, NULL, 0, pinfo, size) failed";

    for (uint32_t icore = 0; icore < cpu->coresLogical && pinfo[icore].MhzLimit; ++icore)
    {
        uint32_t ifreq = 0;
        while (cpu->coreTypes[ifreq].freq != pinfo[icore].MhzLimit && cpu->coreTypes[ifreq].freq > 0)
            ++ifreq;
        if (cpu->coreTypes[ifreq].freq == 0)
            cpu->coreTypes[ifreq].freq = pinfo[icore].MhzLimit;
        ++cpu->coreTypes[ifreq].count;
    }

    if (cpu->frequencyBase == 0)
        cpu->frequencyBase = pinfo->MaxMhz;
    return NULL;
}

const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu)
{
    detectNCores(cpu);

    const char* error = detectByRegistry(cpu);
    if (error)
        return error;

    ffCPUDetectByCpuid(cpu);
    if (options->showPeCoreCount) detectCoreTypes(cpu);

    if (cpu->frequencyMax == 0)
        detectMaxSpeedBySmbios(cpu);

    if(options->temp)
        detectThermalTemp(options, &cpu->temperature);

    return NULL;
}
