# /*******************************************************************************
# Copyright Intel Corporation.
# This software and the related documents are Intel copyrighted materials, and your use of them
# is governed by the express license under which they were provided to you (License).
# Unless the License provides otherwise, you may not use, modify, copy, publish, distribute, disclose
# or transmit this software or the related documents without Intel's prior written permission.
# This software and the related documents are provided as is, with no express or implied warranties,
# other than those that are expressly stated in the License.
#
# *******************************************************************************/

import json
import shutil
import os.path
import subprocess
import re
from typing import Dict, List
from modules.check import CheckSummary, CheckMetadataPy

# Agama-803.32 level-zero-gpu-version.
LEVEL_ZERO_MIN_VERSION = "1.3.27642"

def get_OS(data: Dict) -> str:
    return data["System"]["CheckResult"]


def get_gdb_base_dir():
    gdb_bin_path = shutil.which("gdb-oneapi")
    if gdb_bin_path:
        gdb_bin_dir = os.path.dirname(gdb_bin_path)
        return os.path.dirname(gdb_bin_dir)
    return None


def check_file_in_gdb_dir(json_node: Dict, rel_path: str, component_name: str) -> None:
    check_result = {"CheckResult": "Undefined", "CheckStatus": "PASS"}
    try:
        gdb_base_dir = get_gdb_base_dir()
        if gdb_base_dir and os.path.exists(os.path.join(gdb_base_dir, rel_path)):
            check_result["CheckResult"] = "Found"
            check_result["Message"] = f"{component_name} found."
            check_result["CheckStatus"] = "PASS"
        else:
            check_result["CheckResult"] = "Not Found"
            check_result["Message"] = f"{component_name} not found."
            check_result["CheckStatus"] = "FAIL"
    except Exception as error:
        check_result["CheckResult"] = "Error"
        check_result["CheckStatus"] = "ERROR"
        check_result["Message"] = f"Error while searching {component_name}: {error}"
    json_node.update({f"{component_name} exist": check_result})


def check_gdb_exist(json_node: Dict) -> None:
    check_file_in_gdb_dir(json_node, os.path.join("bin", "gdb-oneapi"), "Debugger")


def check_libipt_exist(json_node: Dict) -> None:
    check_file_in_gdb_dir(json_node, os.path.join("lib", "libipt.so"), "libipt")


def check_libiga_exist(json_node: Dict) -> None:
    check_file_in_gdb_dir(json_node, os.path.join("lib", "libiga64.so"), "libiga")


def check_linux_kernel_version(data: Dict, json_node: Dict) -> None:
    check_result_pass = {"CheckResult": "Supported", "CheckStatus": "PASS", "Command": "uname -r"}
    check_result_fail = {"CheckResult": "Not supported", "CheckStatus": "FAIL", "Command": "uname -r",
                         "Message": "This Linux kernel version is not supported."}
    linux_kernel_version = data["Release"]["CheckResult"].split(".")

    if len(linux_kernel_version) < 2:
        json_node.update({"Linux kernel version": check_result_fail})
        return

    try:
        linux_kernel_version[0] = int(linux_kernel_version[0])
        linux_kernel_version[1] = int(linux_kernel_version[1])
    except ValueError:
        json_node.update({"Linux kernel version": check_result_fail})
        return

    if linux_kernel_version[0] < 4 or (linux_kernel_version[0] == 4 and linux_kernel_version[1] < 14):
        json_node.update({"Linux kernel version": check_result_fail})
        return

    json_node.update({"Linux kernel version": check_result_pass})


def check_i915_debug(json_node: Dict) -> None:
    check_result = {"CheckResult": "i915 debug", "CheckStatus": "PASS", "Message": "i915 debug is enabled."}

    # Find out which GPUs are installed
    try:
        ls_ret = subprocess.run(["sycl-ls"], capture_output=True, text=True, check=True)
    except:
        check_result["CheckStatus"] = "ERROR"
        check_result["Message"] = "Failed to run 'sycl-ls' command. Make sure Intel(R) oneAPI Compiler is installed."
        json_node.update({"i915 debug": check_result})
        return

    dev_ids = re.findall(r"(Intel\(R\).*Data.*Center.*GPU.*Flex|Intel.*Arc.*A.*Graphics|Intel.*Data.*Center.*GPU.*Max.*1100|Intel.*Data.*Center.*GPU.*Max.*1550|Intel.*Data.*Center.*GPU.*Max.*1350)", ls_ret.stdout)

    if len(dev_ids) == 0:
        check_result["Message"] = "No devices found that support debugging of GPU offload code."
        check_result["CheckStatus"] = "ERROR"
        json_node.update({"i915 debug": check_result})
        return

    # Check i915 prelim_enable_eu_debug settings
    find_out = subprocess.check_output("find /sys/devices | grep prelim_enable_eu_debug", shell=True).decode()
    lines = find_out.split('\n')
    if len(lines) == 0:
        check_result["Message"] = "Could not find any i915 devices."
        check_result["CheckStatus"] = "ERROR"
        json_node.update({"i915 debug": check_result})
        return

    # All GPUs should have prelim_enable_eu_debug set to 1
    prelim_debug_enabled = True
    for line in lines:
        enabled = subprocess.run(["cat", line], capture_output=True, text=True).stdout
        if "0" in enabled:
            prelim_debug_enabled = False

    # If not set, then check kernel parameters from GRUB2
    # TODO: i915.debug_eu will be deprecated and removed in the future
    if not prelim_debug_enabled:
        # Get kernel parameters in use
        cmdline_out = subprocess.run(["cat", "/proc/cmdline"], capture_output=True, text=True).stdout
        if "i915.debug_eu=1" not in cmdline_out:
            check_result["Message"] = "i915.debug_eu=1 not found in kernel parameters.\n" \
                "  HINT: Add i915.debug_eu=1 parameter to GRUB_CMDLINE_LINUX_DEFAULT" \
                " in /etc/default/grub, rebuild GRUB and reboot.\n" \
                "Alternatively set prelim_enable_eu_debug to 1 for each GPU using sysfs.\n" \
                "  HINT: You can find the GPU's with command: 'find /sys/devices | grep prelim_enable_eu_debug'.\n" \
                "  HINT: Set the value with command: 'echo 1 | sudo tee <sysfs_path>/prelim_enable_eu_debug'.\n" \
                "  HINT: List all i915 kernel parameters: 'sudo grep . /sys/module/i915/parameters/*'"
            check_result["CheckStatus"] = "ERROR"

    json_node.update({"i915 debug": check_result})


def is_require_version(first):
    """ Returns true when detected version is greater or equal to minimum
    required version set globally using LEVEL_ZERO_MIN_VERSION."""
    try:
        first_split = first.split(".")
        second_split = LEVEL_ZERO_MIN_VERSION.split(".")
        assert(len(first_split) == 3 and len(second_split) == 3)

        if first_split[0] < second_split[0]:
            return False
        else:
            if first_split[1] < second_split[1]:
                return False
            else:
                if first_split[2] < second_split[2]:
                    return False
        return True
    except:
        # Fail the test in case Version comparison is failed.
        return False


def check_level_zero_version(json_node: Dict) -> None:
    check_result = {"CheckResult": "Debugger level_zero version check", "CheckStatus": "PASS", "Message": "level_zero minimum required Version is installed."}

    try:
        """Return GPU devices shown in sycl-ls command."""
        ls_ret = subprocess.run(["sycl-ls"], capture_output=True, text=True, check=True)
        devices_available = str(ls_ret.stdout)
        assert("ext_oneapi_level_zero" in devices_available)
    except:
        check_result["CheckStatus"] = "ERROR"
        check_result["Message"] = "Failed to run 'sycl-ls' command for checking level-zero version."
        json_node.update({"Debugger level_zero version check ": check_result})
        return

    finddev = re.findall(r'\[(.*)\]', devices_available)
    total_devices = devices_available.count("ext_oneapi_level_zero")

    devlist = re.findall(r'\[(.*)\]', devices_available)
    current_ver = "[0]"

    for dev in devlist:
        if "ext_oneapi_level_zero" in dev and "UHD Graphics" not in dev:
            current_ver = re.findall(r'ext_oneapi_level_zero.*\[(.*)', dev)
            if not is_require_version(current_ver[0]):
                check_result["CheckStatus"] = "ERROR"
                check_result["Message"] = f"For Intel GPU Debugging Level-Zero version should be >= {LEVEL_ZERO_MIN_VERSION}."
                json_node.update({"Debugger level_zero version check ": check_result})
                return


def check_gdb_env_vars(json_node: Dict) -> None:
    check_result = {"CheckResult": "Env variables", "CheckStatus": "PASS",
                    "Message": "Environmental variables correct."}

    l0_dbg_enabled = os.getenv('ZET_ENABLE_PROGRAM_DEBUGGING')

    if l0_dbg_enabled != '1':
        check_result["Message"] = "Environmental variable ZET_ENABLE_PROGRAM_DEBUGGING=1 is not set."
        check_result["CheckStatus"] = "ERROR"

    json_node.update({"Env variables": check_result})


def check_gdb_processes(json_node: Dict) -> None:
    check_result = {"CheckResult": "Gdb processes", "CheckStatus": "PASS"}

    gdb_oneapi = subprocess.check_output("ps aux | grep gdb-oneapi", shell=True).decode()
    gdbserver_ze = subprocess.check_output("ps aux | grep gdbserver-ze", shell=True).decode()

    # If gdb-oneapi process is found, then it's safe to assume, user is
    # running debug session and there can also be gdbserver-ze processes.
    # If not found, then check if there are hanging gdbserver-ze processes
    # and let the user know those should be killed.
    gdboneapi_lines = gdb_oneapi.split('\n')
    del gdboneapi_lines[-1]
    gdb_count = 0
    # Only count gdb-oneapi lines without "grep"
    for line in gdboneapi_lines:
        if line.find("grep") == -1:
            gdb_count += 1

    # If there are no gdb-oneapi processes, check gdbserver-ze processes
    if gdb_count == 0:
        gdbserver_lines = gdbserver_ze.split('\n')
        del gdbserver_lines[-1]
        for line in gdbserver_lines:
            if line.find("grep") == -1:
                check_result["Message"] = "Some gdbserver-ze processes are left running. Kill them with command 'killall gdbserver-ze' and try again."
                check_result["CheckStatus"] = "WARNING"
                break

    json_node.update({"Gdb processes": check_result})


def check_compiler(json_node: Dict) -> None:
    check_result = {"CheckResult": "Compiler", "CheckStatus": "PASS"}
    icx_path = shutil.which("icx")
    if icx_path is None:
        check_result["Message"] = "Intel(R) oneAPI Compiler is not installed."
        check_result["CheckStatus"] = "ERROR"

    json_node.update({"Compiler": check_result})


def check_multi_gpu_supported(json_node: Dict) -> None:
    check_result = {"CheckResult": "Debugger multi-device check", "CheckStatus": "PASS", "Message": "Requirement for setting ZE_AFFINTY_MASK enrionment variable."}

    try:
        """Return GPU devices shown in sycl-ls command."""
        ls_ret = subprocess.run(["sycl-ls"], capture_output=True, text=True, check=True)
        devices_available = str(ls_ret.stdout)
        assert("ext_oneapi_level_zero" in devices_available)
    except:
        check_result["CheckStatus"] = "ERROR"
        check_result["Message"] = "Failed to run 'sycl-ls' command for checking multi-device machine."
        json_node.update({"Debugger multi-device support check ": check_result})
        return

    dev_ids = re.findall(r"Intel\(R\).*Data.*Center.*GPU.*Flex", ls_ret.stdout)

    # Multi-devices are not supported on ATSM.
    if len(dev_ids) > 1:
        check_result["CheckStatus"] = "ERROR"
        check_result["Message"] = f"Multi-device is not supported.  So use filter ZE_AFFINITY_MASK to filter a device."
        json_node.update({"Debugger multi-device check ": check_result})
        return


def run_debugger_check(data: dict) -> CheckSummary:
    result_json = {"CheckResult": {}}
    os_info = data["base_system_check"]["CheckResult"]["Operating system information"]["CheckResult"]
    if get_OS(os_info) == "Linux":
        check_multi_gpu_supported(result_json["CheckResult"])
        check_level_zero_version(result_json["CheckResult"])
        check_linux_kernel_version(os_info, result_json["CheckResult"])
        check_gdb_exist(result_json["CheckResult"])
        check_libipt_exist(result_json["CheckResult"])
        check_libiga_exist(result_json["CheckResult"])
        check_compiler(result_json["CheckResult"])
        check_i915_debug(result_json["CheckResult"])
        check_gdb_env_vars(result_json["CheckResult"])
        check_gdb_processes(result_json["CheckResult"])
    else:
        result_json["CheckResult"] = {
            "Error message": {
                "CheckResult": "",
                "CheckStatus": "ERROR",
                "Message": "This check works on linux only."
            }
        }

    check_summary = CheckSummary(
        result=json.dumps(result_json, indent=4)
    )
    return check_summary


def get_api_version() -> str:
    return "0.2"


def get_check_list() -> List[CheckMetadataPy]:
    someCheck = CheckMetadataPy(
        name="debugger_sys_check",
        type="Data",
        groups="debugger,gdb,sys_check",
        descr="This check verifies if the environment is ready to use gdb (Intel(R) Distribution for GDB*).",
        dataReq="{\"base_system_check\": 2}",
        merit=60,
        timeout=10,
        version=2,
        run="run_debugger_check"
    )
    return [someCheck]


## 313c8ce82
