#include "gpu.h"
#include "common/io.h"
#include "common/properties.h"
#include "common/memrchr.h"

#include <stdlib.h>
#ifdef __FreeBSD__
    #include <paths.h>
    #ifndef _PATH_LOCALBASE
        #define _PATH_LOCALBASE "/usr/local"
    #endif
#elif __OpenBSD__
    #define _PATH_LOCALBASE "/usr/local"
#elif __NetBSD__
    #define _PATH_LOCALBASE "/usr/pkg"
#endif

#if FF_HAVE_EMBEDDED_PCIIDS
    #include "fastfetch_pciids.c.inc"
#endif
#if FF_HAVE_EMBEDDED_AMDGPUIDS
    #include "fastfetch_amdgpuids.c.inc"
#endif

#define FF_STR_INDIR(x) #x
#define FF_STR(x) FF_STR_INDIR(x)

static const FFstrbuf* loadPciIds() {
    static FFstrbuf pciids;

    if (pciids.chars) {
        return &pciids;
    }
    ffStrbufInit(&pciids);

#ifdef FF_CUSTOM_PCI_IDS_PATH

    ffReadFileBuffer(FF_STR(FF_CUSTOM_PCI_IDS_PATH), &pciids);

#else // FF_CUSTOM_PCI_IDS_PATH

    #if __linux__
    ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/share/hwdata/pci.ids", &pciids);
    if (pciids.length == 0) {
        ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/share/misc/pci.ids", &pciids); // debian?
        if (pciids.length == 0) {
            ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/local/share/hwdata/pci.ids", &pciids);
        }
    }
    #elif __OpenBSD__ || __FreeBSD__ || __NetBSD__
    ffReadFileBuffer(_PATH_LOCALBASE "/share/hwdata/pci.ids", &pciids);
    if (pciids.length == 0) {
        ffReadFileBuffer(_PATH_LOCALBASE "/share/pciids/pci.ids", &pciids);
    }
    #elif __sun
    ffReadFileBuffer(FASTFETCH_TARGET_DIR_ROOT "/usr/share/hwdata/pci.ids", &pciids);
    #elif __HAIKU__
    ffReadFileBuffer(FASTFETCH_TARGET_DIR_ROOT "/system/data/hwdata/pci.ids", &pciids);
    #endif

#endif // FF_CUSTOM_PCI_IDS_PATH

    return &pciids;
}

static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
    if (content->length) {
        char buffer[32];

        // Search for vendor
        uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x  ", vendor);
        char* start = (char*) memmem(content->chars, content->length, buffer, len);
        char* end = content->chars + content->length;
        if (start) {
            start += len;
            end = memchr(start, '\n', (uint32_t) (end - start));
            if (!end) {
                end = content->chars + content->length;
            }
            if (!gpu->vendor.length) {
                ffStrbufSetNS(&gpu->vendor, (uint32_t) (end - start), start);
            }

            start = end;     // point to '\n' of vendor
            end = start + 1; // point to start of devices
            // find the start of next vendor
            while (end[0] == '\t' || end[0] == '#') {
                end = strchr(end, '\n');
                if (!end) {
                    end = content->chars + content->length;
                    break;
                } else {
                    end++;
                }
            }

            // Search for device
            len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n\t%04x  ", device);
            start = memmem(start, (size_t) (end - start), buffer, len);
            if (start) {
                start += len;
                end = memchr(start, '\n', (uint32_t) (end - start));
                if (!end) {
                    end = content->chars + content->length;
                }

                char* closingBracket = end - 1;
                if (*closingBracket == ']') {
                    char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start));
                    if (openingBracket) {
                        openingBracket++;
                        ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket);
                    }
                }
                if (!gpu->name.length) {
                    ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start);
                }
            }
        }
    }

    if (!gpu->name.length) {
        const char* subclassStr;
        switch (subclass) {
            case 0 /*PCI_CLASS_DISPLAY_VGA*/:
                subclassStr = " (VGA compatible)";
                break;
            case 1 /*PCI_CLASS_DISPLAY_XGA*/:
                subclassStr = " (XGA compatible)";
                break;
            case 2 /*PCI_CLASS_DISPLAY_3D*/:
                subclassStr = " (3D)";
                break;
            default:
                subclassStr = "";
                break;
        }

        ffStrbufSetF(&gpu->name, "%s Device %04X%s", gpu->vendor.length ? gpu->vendor.chars : "Unknown", device, subclassStr);
    }
}

#if FF_HAVE_EMBEDDED_PCIIDS
static inline int pciDeviceCmp(const uint16_t* key, const FFPciDevice* element) {
    return (int) *key - (int) element->id;
}

static bool loadPciidsInc(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
    for (const FFPciVendor* pvendor = ffPciVendors; pvendor->name; pvendor++) {
        if (pvendor->id != vendor) {
            continue;
        }

        if (!gpu->vendor.length) {
            ffStrbufSetS(&gpu->vendor, pvendor->name);
        }

        const FFPciDevice* pdevice = (const FFPciDevice*) bsearch(&device, pvendor->devices, pvendor->nDevices, sizeof(*pdevice), (void*) pciDeviceCmp);

        if (pdevice) {
            uint32_t nameLen = (uint32_t) strlen(pdevice->name);
            const char* closingBracket = pdevice->name + nameLen - 1;
            if (*closingBracket == ']') {
                const char* openingBracket = memrchr(pdevice->name, '[', nameLen - 1);
                if (openingBracket) {
                    openingBracket++;
                    ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket);
                }
            }
            if (!gpu->name.length) {
                ffStrbufSetNS(&gpu->name, nameLen, pdevice->name);
            }
            return true;
        }

        if (!gpu->name.length) {
            const char* subclassStr;
            switch (subclass) {
                case 0 /*PCI_CLASS_DISPLAY_VGA*/:
                    subclassStr = " (VGA compatible)";
                    break;
                case 1 /*PCI_CLASS_DISPLAY_XGA*/:
                    subclassStr = " (XGA compatible)";
                    break;
                case 2 /*PCI_CLASS_DISPLAY_3D*/:
                    subclassStr = " (3D)";
                    break;
                default:
                    subclassStr = "";
                    break;
            }

            ffStrbufSetF(&gpu->name, "%s Device %04X%s", gpu->vendor.length ? gpu->vendor.chars : "Unknown", device, subclassStr);
        }
        return true;
    }
    return false;
}
#endif

void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
    if (vendor == 0x1234 && device == 0x1111 && subclass == 0) { // Not exist in pci.ids
        ffStrbufSetStatic(&gpu->name, "Virtual Video Controller");
        return;
    }

#if FF_HAVE_EMBEDDED_PCIIDS
    bool ok = loadPciidsInc(subclass, vendor, device, gpu);
    if (ok) {
        return;
    }
#endif
    return parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu);
}

#if FF_HAVE_EMBEDDED_AMDGPUIDS
static inline int amdGpuCmp(const uint32_t* key, const FFArmGpuProduct* element) {
    // Maximum value of *key is 0x00FFFFFF. `(int) *key` should never overflow
    return (int) *key - (int) element->id;
}

static bool loadAmdGpuIdsInc(uint16_t deviceId, uint8_t revision, FFGPUResult* gpu) {
    uint32_t key = (deviceId << 8u) | revision;
    FFArmGpuProduct* product = bsearch(&key, ffAmdGpuProducts, ARRAY_SIZE(ffAmdGpuProducts), sizeof(*ffAmdGpuProducts), (void*) amdGpuCmp);
    if (product) {
        ffStrbufSetS(&gpu->name, product->name);
        return true;
    }
    return false;
}
#endif

static void parseAmdGpuIdsFile(uint16_t deviceId, uint8_t revision, FFGPUResult* gpu) {
    char query[32];
    snprintf(query, ARRAY_SIZE(query), "%X,\t%X,", (unsigned) deviceId, (unsigned) revision);
#ifdef FF_CUSTOM_AMDGPU_IDS_PATH
    ffParsePropFile(FF_STR(FF_CUSTOM_AMDGPU_IDS_PATH), query, &gpu->name);
#else
    ffParsePropFileData("libdrm/amdgpu.ids", query, &gpu->name);
#endif
}

void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu) {
#if FF_HAVE_EMBEDDED_AMDGPUIDS
    bool ok = loadAmdGpuIdsInc(deviceId, revisionId, gpu);
    if (ok) {
        return;
    }
#endif
    return parseAmdGpuIdsFile(deviceId, revisionId, gpu);
}
