import asyncio
import importlib
import fnmatch

import carb
import carb.settings

import omni.appwindow
import omni.ext
import omni.gpu_foundation_factory
import omni.kit.app
import omni.kit.renderer.bind

RENDERER_ENABLED_SETTINGS_PATH = "/renderer/enabled"
CHECKER_SETTINGS_PATH = "exts/omni.kit.compatibility_checker/"

def escape_for_fnmatch(s: str) -> str:
    return s.replace("[", "[[]")

def get_gpus_list_from_device(device):
    gpus_list = omni.gpu_foundation_factory.get_gpus_list(device)
    try:
        gpus_list.remove("Microsoft Basic Render Driver")
    except ValueError:
        pass
    return gpus_list

def get_driver_version_from_device(device):
    driver_version = omni.gpu_foundation_factory.get_driver_version(device)
    return driver_version

def check_supported_gpu(gpus_list, supported_gpus):
    gpu_good = False
    for gpu in gpus_list:
        for supported_gpu in supported_gpus:
            if fnmatch.fnmatch(escape_for_fnmatch(gpu), supported_gpu):
                gpu_good = True
                break
    return gpu_good

def check_driver_version(driver_version, driver_blacklist_ranges):
    for driver_blacklist_range in driver_blacklist_ranges:
        version_from = [int(i) for i in driver_blacklist_range[0].split(".")]
        version_to = [int(i) for i in driver_blacklist_range[1].split(".")]

        def is_driver_in_range(version, version_from, version_to):
            version_int = version[0] * 100 + version[1]
            version_from_int = version_from[0] * 100 + version_from[1]
            version_to_int = version_to[0] * 100 + version_to[1]

            if version_int < version_from_int or version_int >= version_to_int:
                return False

            return True

        if is_driver_in_range(driver_version, version_from, version_to):
            return False, driver_blacklist_range
    return True, []

def check_os_version(min_os_version):
    os_good = True
    os_version = omni.gpu_foundation_factory.get_os_build_number()
    if os_version < min_os_version:
        os_good = False
    return os_good, os_version

class Extension(omni.ext.IExt):
    def __init__(self):
        super().__init__()
        pass

    def _check_rtx_renderer_state(self):
        if self._module_viewport:
            enabled_renderers = self._settings.get(RENDERER_ENABLED_SETTINGS_PATH).lower().split(',')
            rtx_enabled = 'rtx' in enabled_renderers

            # Viewport 1.5 doesn't have methods to return attached context, but it also doesn't
            # quite work with multiple contexts out of the box, so we just assume default context.
            # In VP 2.0, the hope is to have better communication protocol with HydraEngine
            # so there will be no need for this whole compatibility checker anyway.
            usd_context = omni.usd.get_context("")
            attached_hydra_engines = usd_context.get_attached_hydra_engine_names()

            if rtx_enabled and ("rtx" not in attached_hydra_engines):
                return False

        return True

    async def _compatibility_check(self):
        # Wait a couple of frames to make sure all subsystems are properly initialized
        await omni.kit.app.get_app().next_update_async()
        await omni.kit.app.get_app().next_update_async()

        rtx_renderer_good = self._check_rtx_renderer_state()
        if rtx_renderer_good is True:
            # Early exit if renderer is OK, no need to check driver, GPU and OS version in this case
            return

        # Renderer wasn't initialized properly
        message = "RTX engine creation failed. RTX renderers in viewport will be disabled. Please make sure selected GPU is RTX-capable, and GPU drivers meet requirements."
        carb.log_error(message)
        if self._module_notification_manager is not None:
            def open_log():
                log_file_path = self._settings.get("/log/file")
                import webbrowser
                webbrowser.open(log_file_path)
            dismiss_button = self._module_notification_manager.NotificationButtonInfo("Dismiss", None)
            show_log_file_button = self._module_notification_manager.NotificationButtonInfo("Open log", open_log)
            self._module_notification_manager.post_notification(message, hide_after_timeout=False, button_infos=[dismiss_button, show_log_file_button])


        platform_info = omni.kit.app.get_app().get_platform_info()
        platform_settings_path = CHECKER_SETTINGS_PATH + platform_info['platform'] + "/"

        min_os_version = self._settings.get(platform_settings_path + "minOsVersion")
        driver_blacklist_ranges = self._settings.get(platform_settings_path + "driverBlacklistRanges")
        supported_gpus = self._settings.get(CHECKER_SETTINGS_PATH + "supportedGpus")

        self._renderer = omni.kit.renderer.bind.acquire_renderer_interface()
        device = self._renderer.get_graphics_device(omni.appwindow.get_default_app_window())

        if supported_gpus is None:
            carb.log_warn("Supported GPUs list is empty, skipping GPU compatibility check!")
        else:
            gpus_list = get_gpus_list_from_device(device)
            gpu_good = check_supported_gpu(gpus_list, supported_gpus)
            if gpu_good is not True:
                message = "Available GPUs are not supported. List of available GPUs:\n%s" % (gpus_list)
                carb.log_error(message)
                if self._module_notification_manager is not None:
                    self._module_notification_manager.post_notification(message, hide_after_timeout=False)

        driver_good = True
        if driver_blacklist_ranges is None:
            carb.log_warn("Supported drivers list is empty, skipping GPU compatibility check!")
        else:
            driver_version = get_driver_version_from_device(device)
            driver_good, driver_blacklist_range = check_driver_version(driver_version, driver_blacklist_ranges)
            if driver_good is not True:
                message = "Driver version %d.%d is not supported. Driver is in blacklisted range %s-%s. Reason for blacklisting: %s." % (
                    driver_version[0],
                    driver_version[1],
                    driver_blacklist_range[0],
                    driver_blacklist_range[1],
                    driver_blacklist_range[2]
                )
                carb.log_error(message)
                if self._module_notification_manager is not None:
                    self._module_notification_manager.post_notification(message, hide_after_timeout=False)

        os_good, os_version = check_os_version(min_os_version)
        if os_good is not True:
            message = "Please update the OS. Minimum required OS build number for %s is %d, current version is %d." % (platform_info['platform'], min_os_version, os_version)
            carb.log_error(message)
            if self._module_notification_manager is not None:
                self._module_notification_manager.post_notification(message, hide_after_timeout=False)


    def on_startup(self):
        self._settings = carb.settings.get_settings()
        try:
            self._module_viewport = importlib.import_module('omni.kit.viewport_legacy')
        except ImportError:
            self._module_viewport = None
        try:
            self._module_notification_manager = importlib.import_module('omni.kit.notification_manager')
        except ImportError:
            self._module_notification_manager = None
        asyncio.ensure_future(self._compatibility_check())

    def on_shutdown(self):
        self._settings = None
