# pylint: disable=no-member
"""Hardware driver related utilities

Everything in this module should rely on /proc or /sys only, no executable calls
"""

import os
import re
from typing import Dict, Iterable, List

from lutris.util import cache_single
from lutris.util.graphics.glxinfo import GlxInfo
from lutris.util.log import logger
from lutris.util.system import read_process_output

MIN_RECOMMENDED_NVIDIA_DRIVER = 515


@cache_single
def get_nvidia_driver_info() -> Dict[str, str]:
    """Return information about Nvidia drivers"""
    version_file_path = "/proc/driver/nvidia/version"

    def read_from_proc() -> Dict[str, str]:
        try:
            if not os.path.exists(version_file_path):
                return {}
            with open(version_file_path, encoding="utf-8") as version_file:
                content = version_file.readlines()
        except PermissionError:
            # MAC systems (selinux, apparmor) may block access to files in /proc.
            # If this happens, we may still be able to retrieve the info by
            # other means, but need additional validation.
            logger.info("Could not access %s. Falling back to glxinfo.", version_file_path)
            return {}
        except OSError as e:
            logger.warning(
                "Unexpected error when accessing %s. Falling back to glxinfo.",
                version_file_path,
                exc_info=e,
            )
            return {}

        try:
            nvrm_version = content[0].split(": ")[1].strip().split()
            if "Open" in nvrm_version:
                return {
                    "vendor": nvrm_version[0],
                    "platform": nvrm_version[1],
                    "arch": nvrm_version[6],
                    "version": nvrm_version[7],
                }
            return {
                "vendor": nvrm_version[0],
                "platform": nvrm_version[1],
                "arch": nvrm_version[2],
                "version": nvrm_version[5],
                "date": " ".join(nvrm_version[6:]),
            }
        except IndexError as ex:
            logger.warning("Unable to parse %s. Falling back to glxinfo: %s", version_file_path, ex)
            return {}

    def invoke_glxinfo() -> Dict[str, str]:
        glx_info = GlxInfo()
        platform = read_process_output(["uname", "-s"])
        arch = read_process_output(["uname", "-m"])
        vendor = glx_info.opengl_vendor  # type: ignore[attr-defined]
        if "nvidia" not in vendor.lower():
            raise RuntimeError("Expected NVIDIA vendor information, received %s." % vendor)
        return {
            "vendor": vendor,
            "platform": platform,
            "arch": arch,
            "version": glx_info.opengl_version.rsplit(maxsplit=1)[-1],  # type: ignore[attr-defined]
        }

    try:
        from_proc = read_from_proc()
        if from_proc:
            return from_proc
    except Exception as ex:
        logger.exception("Unable to read from '%s': %s", version_file_path, ex)

    return invoke_glxinfo()


def get_nvidia_gpu_ids() -> List[str]:
    """Return the list of Nvidia GPUs"""
    gpus_dir = "/proc/driver/nvidia/gpus"
    try:
        return os.listdir(gpus_dir)
    except PermissionError:
        logger.info("Permission denied to %s. Using lspci instead.", gpus_dir)
    except OSError as e:
        logger.warning("Unexpected error accessing %s. Using lspci instead.", gpus_dir, exc_info=e)
    # 10de is NVIDIA's vendor ID, 0300 gets you video controllers.
    values = read_process_output(["lspci", "-D", "-n", "-d", "10de::0300"]).splitlines()
    return [line.split(maxsplit=1)[0] for line in values]


def get_nvidia_gpu_info(gpu_id: str) -> Dict[str, str]:
    """Return details about a GPU"""
    gpu_info_file = f"/proc/driver/nvidia/gpus/{gpu_id}/information"
    try:
        with open(gpu_info_file, encoding="utf-8") as info_file:
            content = info_file.readlines()
    except PermissionError:
        logger.info("Permission denied to %s. Detecting with lspci.", gpu_info_file)
    except OSError as e:
        logger.warning(
            "Unexpected error accessing %s. Detecting with lspci",
            gpu_info_file,
            exc_info=e,
        )
    else:
        info = {}
        for line in content:
            key, value = line.split(":", 1)
            info[key] = value.strip()
        return info
    return get_lspci_nvidia_gpu_info(gpu_id)


def get_lspci_nvidia_gpu_info(gpu_id: str) -> Dict[str, str]:
    lspci_data = read_process_output(["lspci", "-v", "-s", gpu_id])
    model_info = re.search(r"NVIDIA Corporation \w+ \[(.+?)\]", lspci_data)
    if model_info:
        model = model_info.group(1)
    else:
        logger.error("Could not detect NVIDIA GPU model.")
        model = "Unknown"
    irq_info = re.search("IRQ ([0-9]+)", lspci_data)
    if irq_info:
        irq = irq_info.group(1)
    else:
        logger.error("Could not detect GPU IRQ information.")
        irq = None

    info = {
        "Model": f"NVIDIA {model}",
        "IRQ": str(irq),
        "Bus Location": str(gpu_id),
    }
    for line in lspci_data.splitlines():
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        info[key.strip()] = value.strip()
    return info


def is_nvidia() -> bool:
    """Return true if the Nvidia drivers are currently in use.

    Note: This function may not detect use of the nouveau drivers.
    """

    try:
        return os.path.exists("/proc/driver/nvidia")
    except OSError:
        logger.info("Could not determine whether /proc/driver/nvidia exists. Falling back to alternative method")
    try:
        with open("/proc/modules", encoding="utf-8") as f:
            modules = f.read()
        return bool(re.search(r"^nvidia ", modules, flags=re.MULTILINE))
    except OSError:
        logger.error("Could not access /proc/modules to find the Nvidia drivers. Nvidia card may not be detected.")
    glx_info = GlxInfo()
    return "NVIDIA" in glx_info.opengl_vendor  # type: ignore[attr-defined]


def get_gpu_cards() -> Iterable[str]:
    """Return GPUs connected to the system"""
    if not os.path.exists("/sys/class/drm"):
        logger.error("No GPU available on this system!")
        return

    try:
        cardlist = os.listdir("/sys/class/drm/")
    except PermissionError:
        logger.error("Your system does not allow reading from /sys/class/drm, no GPU detected.")
        return

    for cardname in cardlist:
        if re.match(r"^card\d$", cardname):
            yield cardname


def get_gpu_info(card: str) -> Dict[str, str]:
    """Return information about a GPU"""
    infos = {"DRIVER": "", "PCI_ID": "", "PCI_SUBSYS_ID": ""}
    try:
        with open(f"/sys/class/drm/{card}/device/uevent", encoding="utf-8") as card_uevent:
            content = card_uevent.readlines()
    except FileNotFoundError:
        logger.error("Unable to read driver information for card %s", card)
        return infos
    for line in content:
        key, value = line.split("=", 1)
        infos[key] = value.strip()
    return infos


def is_amd() -> bool:
    """Return true if the system uses the AMD driver"""
    for card in get_gpu_cards():
        if get_gpu_info(card)["DRIVER"] == "amdgpu":
            return True
    return False


def is_outdated() -> bool:
    if not is_nvidia():
        return False
    driver_info = get_nvidia_driver_info()
    driver_version = driver_info["version"]
    if not driver_version:
        logger.error("Failed to get Nvidia version")
        return True
    try:
        major_version = int(driver_version.split(".")[0])
    except (IndexError, ValueError) as ex:
        logger.exception("Failed to parse Nvidia version: %s", ex)
        return True
    return major_version < MIN_RECOMMENDED_NVIDIA_DRIVER
