#include "gpu.h"
#include "common/debug.h"
#include "detection/vulkan/vulkan.h"
#include "detection/opencl/opencl.h"
#include "detection/opengl/opengl.h"
#include "modules/opengl/opengl.h"

const char* FF_GPU_VENDOR_NAME_APPLE = "Apple";
const char* FF_GPU_VENDOR_NAME_AMD = "AMD";
const char* FF_GPU_VENDOR_NAME_INTEL = "Intel";
const char* FF_GPU_VENDOR_NAME_NVIDIA = "NVIDIA";
const char* FF_GPU_VENDOR_NAME_MTHREADS = "Moore Threads";
const char* FF_GPU_VENDOR_NAME_QUALCOMM = "Qualcomm";
const char* FF_GPU_VENDOR_NAME_MTK = "MTK";
const char* FF_GPU_VENDOR_NAME_VMWARE = "VMware";
const char* FF_GPU_VENDOR_NAME_PARALLELS = "Parallels";
const char* FF_GPU_VENDOR_NAME_MICROSOFT = "Microsoft";
const char* FF_GPU_VENDOR_NAME_REDHAT = "RedHat";
const char* FF_GPU_VENDOR_NAME_ORACLE = "Oracle";
const char* FF_GPU_VENDOR_NAME_BROADCOM = "Broadcom";
const char* FF_GPU_VENDOR_NAME_LOONGSON = "Loongson";
const char* FF_GPU_VENDOR_NAME_JINGJIA_MICRO = "Jingjia Micro";
const char* FF_GPU_VENDOR_NAME_HUAWEI = "Huawei";
const char* FF_GPU_VENDOR_NAME_ZHAOXIN = "Zhaoxin";
const char* FF_GPU_VENDOR_NAME_QEMU = "QEMU";

const char* ffGPUGetVendorString(unsigned vendorId) {
    // https://devicehunt.com/all-pci-vendors
    switch (vendorId) {
        case 0x106b:
            return FF_GPU_VENDOR_NAME_APPLE;
        case 0x1002:
        case 0x1022:
        case 0x1dd8:
            return FF_GPU_VENDOR_NAME_AMD;
        case 0x8086:
        case 0x8087:
        case 0x03e7:
            return FF_GPU_VENDOR_NAME_INTEL;
        case 0x0955:
        case 0x10de:
        case 0x12d2:
            return FF_GPU_VENDOR_NAME_NVIDIA;
        case 0x1ed5:
            return FF_GPU_VENDOR_NAME_MTHREADS;
        case 0x17cb:
        case 0x5143:
            return FF_GPU_VENDOR_NAME_QUALCOMM;
        case 0x14c3:
            return FF_GPU_VENDOR_NAME_MTK;
        case 0x15ad:
            return FF_GPU_VENDOR_NAME_VMWARE;
        case 0x1af4:
            return FF_GPU_VENDOR_NAME_REDHAT;
        case 0x1ab8:
        case 0x05404c42: // PD
            return FF_GPU_VENDOR_NAME_PARALLELS;
        case 0x1414:
            return FF_GPU_VENDOR_NAME_MICROSOFT;
        case 0x108e:
            return FF_GPU_VENDOR_NAME_ORACLE;
        case 0x182f:
        case 0x14e4:
            return FF_GPU_VENDOR_NAME_BROADCOM;
        case 0x0014:
            return FF_GPU_VENDOR_NAME_LOONGSON;
        case 0x0731:
            return FF_GPU_VENDOR_NAME_JINGJIA_MICRO;
        case 0x19e5:
            return FF_GPU_VENDOR_NAME_HUAWEI;
        case 0x1d17:
            return FF_GPU_VENDOR_NAME_ZHAOXIN;
        case 0x1234: // https://admin.pci-ids.ucw.cz/read/PC/1234
            return FF_GPU_VENDOR_NAME_QEMU;
        default:
            return nullptr;
    }
}

static bool normalizeVendorName(FFGPUResult* gpu) {
    if (ffStrbufContainS(&gpu->name, "Apple")) {
        ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_APPLE);
        gpu->type = FF_GPU_TYPE_INTEGRATED;
    } else if (ffStrbufContainS(&gpu->name, "Intel")) {
        ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_INTEL);
    } else if (ffStrbufContainS(&gpu->name, "AMD") || ffStrbufContainS(&gpu->name, "ATI")) {
        ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_AMD);
    } else if (ffStrbufContainS(&gpu->name, "NVIDIA")) {
        ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_NVIDIA);
    } else if (ffStrbufContainS(&gpu->name, "MTT")) {
        ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_MTHREADS);
    } else {
        return false;
    }

    return true;
}

const char* detectByOpenGL(FFlist* gpus) {
    FF_DEBUG("Starting OpenGL GPU detection fallback");

    FFOpenGLResult result;
    ffStrbufInit(&result.version);
    ffStrbufInit(&result.renderer);
    ffStrbufInit(&result.vendor);
    ffStrbufInit(&result.slv);
    ffStrbufInit(&result.library);

    [[gnu::cleanup(ffDestroyOpenGLOptions)]] FFOpenGLOptions options;
    ffInitOpenGLOptions(&options);
    const char* error = ffDetectOpenGL(&options, &result);
    FF_DEBUG("OpenGL detection returns: %s", error ?: "success");

    if (!error) {
        FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus);
        gpu->type = FF_GPU_TYPE_UNKNOWN;
        ffStrbufInitMove(&gpu->vendor, &result.vendor);
        ffStrbufInitMove(&gpu->name, &result.renderer);
        ffStrbufInit(&gpu->driver);
        ffStrbufInitF(&gpu->platformApi, "OpenGL %s", result.version.chars);
        ffStrbufInit(&gpu->memoryType);
        gpu->index = FF_GPU_INDEX_UNSET;
        gpu->temperature = FF_GPU_TEMP_UNSET;
        gpu->coreCount = FF_GPU_CORE_COUNT_UNSET;
        gpu->frequency = FF_GPU_FREQUENCY_UNSET;
        gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET;
        gpu->dedicated = gpu->shared = (FFGPUMemory) { 0, 0 };
        gpu->deviceId = 0;
        gpu->pcieSpeed = FF_GPU_PCIE_SPEED_UNSET;

        FF_DEBUG("OpenGL reported renderer='%s', vendor='%s', version='%s'",
            gpu->name.chars,
            gpu->vendor.chars,
            result.version.chars);

        normalizeVendorName(gpu);

        FF_DEBUG("OpenGL fallback produced GPU: name='%s', vendor='%s', type=%u",
            gpu->name.chars,
            gpu->vendor.chars,
            gpu->type);
    }

    ffStrbufDestroy(&result.version);
    ffStrbufDestroy(&result.renderer);
    ffStrbufDestroy(&result.vendor);
    ffStrbufDestroy(&result.slv);
    ffStrbufDestroy(&result.library);
    return error;
}

#if defined(FF_HAVE_EGL) || __has_include(<EGL/egl.h>)
    #include <EGL/egl.h>
    #include <EGL/eglext.h>

    #if EGL_EXT_device_base && EGL_EXT_device_persistent_id && EGL_EXT_device_query_name

        #define FF_HAVE_EGL_EXT 1
        #include <inttypes.h>

        #include "common/library.h"
        #include "common/strutil.h"
        #include "common/io.h"

const char* detectByEglext(FFlist* result) {
    FF_LIBRARY_LOAD_MESSAGE(egl, "libEGL" FF_LIBRARY_EXTENSION, 1);
    FF_LIBRARY_LOAD_SYMBOL_MESSAGE(egl, eglGetProcAddress);

    PFNEGLQUERYDEVICESEXTPROC ffeglQueryDevicesEXT = (PFNEGLQUERYDEVICESEXTPROC) ffeglGetProcAddress("eglQueryDevicesEXT");
    if (!ffeglQueryDevicesEXT) {
        return "eglQueryDevicesEXT is unavailable";
    }
    PFNEGLQUERYDEVICESTRINGEXTPROC ffeglQueryDeviceStringEXT = (PFNEGLQUERYDEVICESTRINGEXTPROC) ffeglGetProcAddress("eglQueryDeviceStringEXT");
    if (!ffeglQueryDeviceStringEXT) {
        return "eglQueryDeviceStringEXT is unavailable";
    }
    PFNEGLQUERYDEVICEBINARYEXTPROC ffeglQueryDeviceBinaryEXT = (PFNEGLQUERYDEVICEBINARYEXTPROC) ffeglGetProcAddress("eglQueryDeviceBinaryEXT");
    if (!ffeglQueryDeviceBinaryEXT) {
        return "eglQueryDeviceBinaryEXT is unavailable";
    }

    FF_DEBUG("Loaded EGL library and required symbols");

    FF_SUPPRESS_IO();

    EGLDeviceEXT devices[16];
    EGLint numDevices;
    if (ffeglQueryDevicesEXT(ARRAY_SIZE(devices), devices, &numDevices) == EGL_TRUE) {
        FF_DEBUG("eglQueryDevicesEXT() returned %d device(s)", numDevices);
        for (EGLint i = 0; i < numDevices; i++) {
            const EGLDeviceEXT device = devices[i];
            if (device == (EGLDeviceEXT) EGL_NO_DEVICE_EXT || device == (EGLDeviceEXT) EGL_BAD_DEVICE_EXT) {
                FF_DEBUG("eglQueryDevicesEXT() returned invalid device at index %d", i);
                continue;
            }
            const char* exts = (const char*) ffeglQueryDeviceStringEXT(device, EGL_EXTENSIONS);
            FF_DEBUG("eglQueryDeviceStringEXT() returned extensions for device %d: %s", i, exts);
            if (exts && ffStrContains(exts, "EGL_MESA_device_software")) {
                FF_DEBUG("eglQueryDeviceStringEXT() returned software device at index %d, skipping", i);
                continue;
            }

            const char* name = ffeglQueryDeviceStringEXT(device, EGL_RENDERER_EXT);
            if (!name) {
                FF_DEBUG("eglQueryDeviceStringEXT() returned nullptr name for device %d, skipping", i);
                continue;
            }

            FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *result);
            ffStrbufInitS(&gpu->name, name);
            ffStrbufInit(&gpu->vendor);
            ffStrbufInitS(&gpu->driver, ffeglQueryDeviceStringEXT(device, EGL_DRIVER_NAME_EXT));
            ffStrbufInitF(&gpu->platformApi, "egl-ext");
            ffStrbufInit(&gpu->memoryType);
            gpu->index = FF_GPU_INDEX_UNSET;
            gpu->temperature = FF_GPU_TEMP_UNSET;
            gpu->coreCount = FF_GPU_CORE_COUNT_UNSET;
            gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET;
            gpu->type = FF_GPU_TYPE_UNKNOWN;
            gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET;
            gpu->deviceId = 0;
            gpu->frequency = FF_GPU_FREQUENCY_UNSET;
            gpu->pcieSpeed = FF_GPU_PCIE_SPEED_UNSET;

            ffeglQueryDeviceBinaryEXT(device, EGL_DEVICE_UUID_EXT, sizeof(gpu->deviceId), &gpu->deviceId, nullptr);

            if (!normalizeVendorName(gpu)) {
                ffStrbufSetS(&gpu->vendor, ffeglQueryDeviceStringEXT(device, EGL_VENDOR));
            }

            FF_DEBUG("Detected GPU %d: vendor='%s', name='%s', driver='%s', id='%16" PRIX64 "'", i, gpu->vendor.chars, gpu->name.chars, gpu->driver.chars, gpu->deviceId);
        }
    } else {
        FF_DEBUG("eglQueryDevicesEXT() returned EGL_FALSE");
    }

    return nullptr;
}
    #endif

#endif


const char* ffDetectGPU(const FFGPUOptions* options, FFlist* result) {
    FF_DEBUG("Starting GPU detection with method=%d", (int) options->detectionMethod);

    if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_PCI) {
        FF_DEBUG("Trying PCI/native GPU detection");
        const char* error = ffDetectGPUImpl(options, result);
        if (!error && result->length > 0) {
            FF_DEBUG("PCI/native GPU detection succeeded with %u GPU(s)", result->length);
            return nullptr;
        }

        FF_DEBUG("PCI/native GPU detection did not produce results (error=%s, gpuCount=%u)",
            error ?: "none",
            result->length);
    }
    if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_VULKAN) {
        FF_DEBUG("Trying Vulkan GPU detection fallback");
        FFVulkanResult* vulkan = ffDetectVulkan();
        if (!vulkan->error && vulkan->gpus.length > 0) {
            FF_DEBUG("Vulkan detection succeeded with %u GPU(s)", vulkan->gpus.length);
            ffListDestroy(result);
            ffListInitMove(result, &vulkan->gpus);

#ifdef __ANDROID__
            double ffGPUDetectTempFromTZ(void);
            if (options->temp && result->length == 1) {
                FF_DEBUG("Applying Android thermal-zone temperature to single Vulkan GPU");
                FF_LIST_GET(FFGPUResult, *result, 0)->temperature = ffGPUDetectTempFromTZ();
            }
#endif

            return nullptr;
        }

        FF_DEBUG("Vulkan detection did not produce results (error=%s, gpuCount=%u)",
            vulkan->error ?: "none",
            vulkan->gpus.length);
    }
    if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_OPENCL) {
        FF_DEBUG("Trying OpenCL GPU detection fallback");
        FFOpenCLResult* opencl = ffDetectOpenCL();
        if (!opencl->error && opencl->gpus.length > 0) {
            FF_DEBUG("OpenCL detection succeeded with %u GPU(s)", opencl->gpus.length);
            ffListDestroy(result);
            ffListInitMove(result, &opencl->gpus);
            return nullptr;
        }

        FF_DEBUG("OpenCL detection did not produce results (error=%s, gpuCount=%u)",
            opencl->error ?: "none",
            opencl->gpus.length);
    }
    if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_EGL_EXT) {
        #if FF_HAVE_EGL_EXT
        FF_DEBUG("Trying EGL_EXT GPU detection fallback");
        const char* error = detectByEglext(result);
        if (!error && result->length > 0) {
            FF_DEBUG("EGL_EXT GPU detection succeeded with %u GPU(s)", result->length);
            return nullptr;
        }

        FF_DEBUG("EGL_EXT GPU detection did not produce results (error=%s, gpuCount=%u)",
            error ?: "none",
            result->length);
        #else
        FF_DEBUG("EGL_EXT GPU detection is unavailable");
        #endif
    }
    if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_OPENGL) {
        FF_DEBUG("Trying OpenGL GPU detection fallback");
        const char* error = detectByOpenGL(result);
        if (error == nullptr) {
            FF_DEBUG("OpenGL fallback succeeded with %u GPU(s)", result->length);
            return nullptr;
        }

        FF_DEBUG("OpenGL fallback failed: %s", error);
    }

    FF_DEBUG("GPU detection failed in all enabled backends");
    return "GPU detection failed";
}

bool ffGPUDetectTypeByVendorAndName(FFGPUResult* gpu) {
    if (gpu->type != FF_GPU_TYPE_UNKNOWN) {
        return true;
    }

    FF_DEBUG("Using fallback GPU type detection");
    if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_NVIDIA) {
        if (ffStrbufStartsWithIgnCaseS(&gpu->name, "GeForce") ||
            ffStrbufStartsWithIgnCaseS(&gpu->name, "Quadro") ||
            ffStrbufStartsWithIgnCaseS(&gpu->name, "Tesla")) {
            gpu->type = FF_GPU_TYPE_DISCRETE;
        }
    } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_MTHREADS) {
        if (ffStrbufStartsWithIgnCaseS(&gpu->name, "MTT ")) { gpu->type = FF_GPU_TYPE_DISCRETE; }
    } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_INTEL) {
        // 0000:00:02.0 is reserved for Intel integrated graphics
        gpu->type = gpu->deviceId == ffGPUPciAddr2Id(0, 0, 2, 0) ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE;
    } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_VMWARE || gpu->vendor.chars == FF_GPU_VENDOR_NAME_PARALLELS) {
        // Virtualized GPUs
        gpu->type = FF_GPU_TYPE_INTEGRATED;
    }

    if (gpu->type != FF_GPU_TYPE_UNKNOWN) {
        FF_DEBUG("Determined GPU type based on vendor (%s) and name: %u", gpu->vendor.chars, gpu->type);
        return true;
    }

    return false;
}
