# Source code for moderngl_window

    """
    General helper functions aiding in the boostrapping of this library.
    """

    import argparse
    import logging
    import os
    import sys
    import time
    import weakref
    from pathlib import Path
    from typing import Any, Optional

    import moderngl

    from moderngl_window.conf import settings
    from moderngl_window.context.base import BaseWindow, WindowConfig
    from moderngl_window.timers.clock import Timer
    from moderngl_window.utils.keymaps import AZERTY, QWERTY, KeyMap, KeyMapFactory  # noqa
    from moderngl_window.utils.module_loading import import_string

    __version__ = "3.1.1"

    IGNORE_DIRS = [
        "__pycache__",
        "base",
    ]

    # Add new windows classes here to be recognized by the command line option --window
    WINDOW_CLASSES = [
        "glfw",
        "headless",
        "pygame2",
        "pyglet",
        "pyqt5",
        "pyside2",
        "sdl2",
        "tk",
    ]

    OPTIONS_TRUE = ["yes", "on", "true", "t", "y", "1"]
    OPTIONS_FALSE = ["no", "off", "false", "f", "n", "0"]
    OPTIONS_ALL = OPTIONS_TRUE + OPTIONS_FALSE

    # Quick and dirty debug logging setup by default
    # See: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial
    logger = logging.getLogger(__name__)

    def setup_basic_logging(level: int) -> None:
        """Set up basic logging

        Args:
            level (int): The log level
        """
        if level is None:
            return

        # Do not add a new handler if we already have one
        if not logger.handlers:
            logger.propagate = False
            logger.setLevel(level)
            ch = logging.StreamHandler()
            ch.setLevel(logging.DEBUG)
            ch.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
            logger.addHandler(ch)

    class ContextRefs:
        """Namespace for window/context references"""

        WINDOW: Optional[BaseWindow] = None
        CONTEXT: Optional[moderngl.Context] = None

    def activate_context(
        window: Optional[BaseWindow] = None, ctx: Optional[moderngl.Context] = None
    ) -> None:
        """
        Register the active window and context.
        If only a window is supplied the context is taken from the window.
        Only a context can also be passed in.

        Keyword Args:
            window (window): The window to activate
            ctx (moderngl.Context): The moderngl context to activate
        """
        ContextRefs.WINDOW = window
        ContextRefs.CONTEXT = ctx
        if ctx is None:
            ContextRefs.CONTEXT = window.ctx

    def window() -> BaseWindow:
        """Obtain the active window"""
        if ContextRefs.WINDOW:
            return ContextRefs.WINDOW

        raise ValueError("No active window and context. Call activate_window.")

    def ctx() -> moderngl.Context:
        """Obtain the active context"""
        if ContextRefs.CONTEXT:
            return ContextRefs.CONTEXT

        raise ValueError("No active window and context. Call activate_window.")

    def get_window_cls(window: str = "") -> type[BaseWindow]:
        """
        Attempt to obtain a window class using the full dotted
        python path. This can be used to import custom or modified
        window classes.

        Args:
            window (str): Name of the window

        Returns:
            A reference to the requested window class. Raises exception if not found.
        """
        logger.info("Attempting to load window class: %s", window)
        win = import_string(window)

        # assert issubclass(
        #     win, BaseWindow
        # ), f"{win} is not derived from moderngl_window.context.base.BaseWindow"
        return win

    def get_local_window_cls(window: Optional[str] = None) -> type[BaseWindow]:
        """
        Attempt to obtain a window class in the moderngl_window package
        using short window names such as ``pyglet`` or ``glfw``.

        Args:
            window (str): Name of the window

        Returns:
            A reference to the requested window class. Raises exception if not found.
        """
        window = os.environ.get("MODERNGL_WINDOW") or window
        if window is None:
            window = "pyglet"

        return get_window_cls("moderngl_window.context.{}.Window".format(window))

    def find_window_classes() -> list[str]:
        """
        Find available window packages
        Returns:
            A list of available window packages
        """
        # In some environments we cannot rely on introspection
        # and instead return a hardcoded list
        try:
            return [
                path.parts[-1]
                for path in Path(__file__).parent.joinpath("context").iterdir()
                if path.is_dir() and path.parts[-1] not in IGNORE_DIRS
            ]
        except Exception:
            return WINDOW_CLASSES

    def create_window_from_settings() -> BaseWindow:
        """
        Creates a window using configured values in :py:attr:`moderngl_window.conf.Settings.WINDOW`.
        This will also activate the window/context.

        Returns:
            The Window instance
        """
        window_cls = import_string(settings.WINDOW["class"])
        window = window_cls(**settings.WINDOW)

        assert isinstance(
            window, BaseWindow
        ), f"{type(window)} is not derived from moderngl_window.context.base.BaseWindow"
        activate_context(window=window)
        return window

    # --- The simple window config system ---

    def run_window_config(
        config_cls: type[WindowConfig], timer: Optional[Timer] = None, args: Any = None
    ) -> None:
        """
        Run an WindowConfig entering a blocking main loop

        Args:
            config_cls: The WindowConfig class to render
        Keyword Args:
            timer: A custom timer instance
            args: Override sys.args
        """
        config = create_window_config_instance(config_cls, timer=timer, args=args)
        run_window_config_instance(config)

    def create_window_config_instance(
        config_cls: type[WindowConfig], timer: Optional[Timer] = None, args: Any = None
    ) -> WindowConfig:
        """
        Create and initialize a instance of a WindowConfig class.
        Quite a bit of boilerplate is required to create a WindowConfig instance
        and this function aims to simplify that.

        Args:
            window_config: The WindowConfig class to create an instance of
        Keyword Args:
            kwargs: Arguments to pass to the WindowConfig constructor
        Returns:
            An instance of the WindowConfig class
        """
        setup_basic_logging(config_cls.log_level)
        parser = create_parser()
        config_cls.add_arguments(parser)
        values = parse_args(args=args, parser=parser)
        config_cls.argv = values
        window_cls = get_local_window_cls(values.window)

        # Calculate window size
        size = values.size or config_cls.window_size
        size = int(size[0] * values.size_mult), int(size[1] * values.size_mult)

        # Resolve cursor
        show_cursor = values.cursor
        if show_cursor is None:
            show_cursor = config_cls.cursor

        window = window_cls(
            title=config_cls.title,
            size=size,
            fullscreen=config_cls.fullscreen or values.fullscreen,
            resizable=(values.resizable if values.resizable is not None else config_cls.resizable),
            visible=config_cls.visible,
            gl_version=config_cls.gl_version,
            aspect_ratio=config_cls.aspect_ratio,
            vsync=values.vsync if values.vsync is not None else config_cls.vsync,
            samples=values.samples if values.samples is not None else config_cls.samples,
            cursor=show_cursor if show_cursor is not None else True,
            backend=values.backend,
            context_creation_func=config_cls.init_mgl_context,
        )
        window.print_context_info()
        activate_context(window=window)
        if timer is None:
            timer = Timer()
        config = config_cls(ctx=window.ctx, wnd=window, timer=timer)
        # Avoid the event assigning in the property setter for now
        # We want the even assigning to happen in WindowConfig.__init__
        # so users are free to assign them in their own __init__.
        window._config = weakref.ref(config)

        # Swap buffers once before staring the main loop.
        # This can trigger additional resize events reporting
        # a more accurate buffer size
        window.swap_buffers()
        window.set_default_viewport()
        return config

    def run_window_config_instance(config: WindowConfig) -> None:
        """
        Run an WindowConfig instance entering a blocking main loop.

        Args:
            window_config: The WindowConfig instance
        """
        window = config.wnd
        timer = config.timer

        timer.start()

        while not window.is_closing:
            current_time, delta = timer.next_frame()

            # Framerate  limit for hidden windows
            if not window.visible and config.hidden_window_framerate_limit > 0:
                expected_delta_time = 1.0 / config.hidden_window_framerate_limit
                sleep_time = expected_delta_time - delta
                if sleep_time > 0:
                    time.sleep(sleep_time)

            if config.clear_color is not None:
                window.clear(*config.clear_color)

            # Always bind the window framebuffer before calling render
            window.use()

            window.render(current_time, delta)

            if not window.is_closing:
                window.swap_buffers()

        _, duration = timer.stop()
        window.destroy()
        if duration > 0:
            logger.info("Duration: {0:.2f}s @ {1:.2f} FPS".format(duration, timer.fps_average))

    def create_parser() -> argparse.ArgumentParser:
        """Create an argparser parsing the standard arguments for WindowConfig"""
        parser = argparse.ArgumentParser()

        parser.add_argument(
            "-wnd",
            "--window",
            choices=find_window_classes(),
            help="Name for the window type to use",
        )
        parser.add_argument(
            "-fs",
            "--fullscreen",
            action="store_true",
            help="Open the window in fullscreen mode",
        )
        parser.add_argument(
            "-vs",
            "--vsync",
            type=valid_bool,
            help="Enable or disable vsync",
        )
        parser.add_argument(
            "-r",
            "--resizable",
            type=valid_bool,
            default=None,
            help="Enable/disable window resize",
        )
        parser.add_argument(
            "-hd",
            "--hidden",
            type=valid_bool,
            default=False,
            help="Start the window in hidden mode",
        )
        parser.add_argument(
            "-s",
            "--samples",
            type=int,
            help="Specify the desired number of samples to use for multisampling",
        )
        parser.add_argument(
            "-c",
            "--cursor",
            type=valid_bool,
            help="Enable or disable displaying the mouse cursor",
        )
        parser.add_argument(
            "--size",
            type=valid_window_size,
            help="Window size",
        )
        parser.add_argument(
            "--size_mult",
            type=valid_window_size_multiplier,
            default=1.0,
            help="Multiplier for the window size making it easy scale the window",
        )
        parser.add_argument(
            "--backend",
            help="Specify context backend. This is mostly used to enable EGL in headless mode",
        )
        return parser

    def parse_args(
        args: Optional[Any] = None, parser: Optional[argparse.ArgumentParser] = None
    ) -> argparse.Namespace:
        """Parse arguments from sys.argv

        Passing in your own argparser can be user to extend the parser.

        Keyword Args:
            args: override for sys.argv
            parser: Supply your own argparser instance
        """
        parser = parser or create_parser()
        return parser.parse_args(args or sys.argv[1:])

    # --- Validators ---

    def valid_bool(value: Optional[str]) -> Optional[bool]:
        """Validator for bool values"""
        if value is None:
            return None

        value = value.lower()
        if value in OPTIONS_TRUE:
            return True

        if value in OPTIONS_FALSE:
            return False

        raise argparse.ArgumentTypeError(f"Boolean value expected. Options: {OPTIONS_ALL}")

    def valid_window_size(value: str) -> tuple[int, int]:
        """
        Validator for window size parameter.

        Valid format is "[int]x[int]". For example "1920x1080".
        """
        try:
            width, height = value.split("x")
            return int(width), int(height)
        except ValueError:
            pass

        raise argparse.ArgumentTypeError(
            "Valid size format: int]x[int]. Example '1920x1080'",
        )

    def valid_window_size_multiplier(value: str) -> float:
        """Validates window size multiplier

        Must be an integer or float greater than 0
        """
        try:
            val = float(value)
            if val > 0:
                return val
        except ValueError:
            pass

        raise argparse.ArgumentTypeError(
            "Must be a positive int or float",
        )