#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: System utilities.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Utilities for a PyGame game.
#
# @Script: system.py
# @Date Created: 17 Apr, 2024
# @Last Modified: 14 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------

"""
    Modules that are loaded will always be loaded globally.
    Some will only be loaded based on conditions, but made global.
    Thus, they can be checked for in globals() and be easily accessible anywhere.
    Even if a module were loaded and unloaded, it would remain in cache, so unloading it is not really worth it.
    Imports will be loaded as the program needs them, thus reducing the initial memory consumption.
"""

# Although they won't be reloaded and the imports will be skipped if called again,
# avoid wasting processing time by keeping out of import_dependencies()
from pkg_resources import working_set
from subprocess import check_call, CalledProcessError
from sys import executable


def install_dependencies(dependencies:dict):
    """
    Tries to install and import any missing dependencies from list.
    """

    # Adapted from Serge Stroobandt on https://stackoverflow.com/questions/46419607/how-to-automatically-install-required-packages-from-a-python-script-as-necessary

    libsInst = {
        pkg.key for pkg in working_set                                                  # Find required and installed libraries.
    }

    libsMiss = dependencies - libsInst                                                  # Remove the already installed libraries from the dependency list.

    try:
        if len(libsMiss) != 0:
            check_call([executable, "-m", "pip", "install", "--upgrade", "pip"])        # Ensure pip is up-to-date.
            for library in libsMiss:
                check_call([executable, "-m", "pip", "install", library])               # Install all missing libraries.

    except CalledProcessError:
        print("Error | CANNOT FIND OR INSTALL DEPENDENCY:")
        print(library)
        raise SystemExit

install_dependencies({"icecream", "pygame"})

from icecream import ic
from pygame.version import vernum
from pygame.display import get_driver
from os import path
from sys import version as pyver
from platform import uname
from json import load as jload


DISPLAY = {}
PLATFORM = {"libraries": {}}
PLATFORM["kernel"] = uname().system                                                     # `system` is a variable of uname()
PLATFORM["libraries"]["pygame"] = vernum

# Python version cleanup.
versionStr = ""

for c in pyver:
    if c == " ":
        break
    else:
        versionStr = versionStr + c

versionTuple = tuple(versionStr.split("."))

versionTuple = (int(versionTuple[0]), int(versionTuple[1]), int(versionTuple[2]))

PLATFORM["libraries"]["python"] = versionTuple

# GCC version cleanup.
versionStr = ""
startFlag = False

for c in pyver:
    if startFlag:
        if c not in ["G", "C", "]"]:
            versionStr = versionStr + c
    elif c == "[":
        startFlag = True

PLATFORM["libraries"]["GCC"] = versionStr

del pyver, versionStr, startFlag, versionTuple

# Fetch compatability settings.
DIRWORKING = path.dirname(__file__) + "/.."

with open(DIRWORKING + "/config/features.json") as f:
    FEATURES = jload(f)

# All features in file default to is_active: false
# Enable supported features.

for feature in FEATURES.items():
    is_active = False

    for lib in feature[1]["libs"].items():
        if PLATFORM["libraries"][lib[0]] > tuple(lib[1]["ver_implemented"]):
            is_active = True

    FEATURES[feature[0]]["is_active"] = is_active

class Compat():
    def linux():
        # Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate

        """
        if "environ" not in globals():                                                                      # Import if not already.
            global environ
            from os import environ

        try:
            if environ["WAYLAND_DISPLAY"] != "" and globals["XDG_SESSION_TYPE"] != "x11":                   # Check for environment variable present on Wayland systems.
                print("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.")

        except KeyError:                                                                                    # Wayland keys won't exist on X11 systems (mostly).
            install_dependencies({"Xlib"})
            global display
            from Xlib import display
            global randr
            from Xlib.ext import randr

            displayGlobal = display.Display()                                                           # Get every display thing. Maybe a WM object?
            default_screen = displayGlobal.get_default_screen()                                         # Find default display for some reason.
            info = displayGlobal.screen(default_screen)                                                 # Use this as a point to get information about all screens.

            displayConfigs = randr.get_screen_resources(info.root)                                      # Get every screen's possible configurations.
            configsActive = set()                                                                       # Create an empty set to add configurations to.

            for config in displayConfigs.crtcs:                                                         # For every display configuration, determine if it's active and add its identifier to the configurations in use.
                crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp)
                if crtc_info.mode:
                    configsActive.add(crtc_info.mode)

            for config in displayConfigs.modes:
                if config.id in configsActive:                                                          # For every active display mode, figure out its framerate.
                    framerate = config.dot_clock / (config.h_total * config.v_total)
        """

        if get_driver() == "wayland":
                ic("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.")
        elif get_driver() == "x11":
            install_dependencies({"Xlib"})
            global display
            from Xlib import display
            global randr
            from Xlib.ext import randr

            displayGlobal = display.Display()                                                           # Get every display thing. Maybe a WM object?
            default_screen = displayGlobal.get_default_screen()                                         # Find default display for some reason.
            info = displayGlobal.screen(default_screen)                                                 # Use this as a point to get information about all screens.

            displayConfigs = randr.get_screen_resources(info.root)                                      # Get every screen's possible configurations.
            configsActive = set()                                                                       # Create an empty set to add configurations to.

            for config in displayConfigs.crtcs:                                                         # For every display configuration, determine if it's active and add its identifier to the configurations in use.
                crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp)
                if crtc_info.mode:
                    configsActive.add(crtc_info.mode)

            framerate = 30

            # With multiple displays, get the highest framerate that any one monitor can do.
            for config in displayConfigs.modes:
                if config.id in configsActive:                                                          # For every active display mode, figure out its framerate.
                    framerateDisplay = (config.dot_clock / (config.h_total * config.v_total))
                    if framerateDisplay > framerate:
                        framerate = framerateDisplay
            
            return framerate

    def nt():
        # Cannot use pygame.display.get_driver for windows or macos in pygame < v2

        # Adapted from Anurag Uniyal on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate

        install_dependencies({"pywin32"})

        if "EnumDisplayDevices" not in globals():                                                       # Import if not already.
            global EnumDisplayDevices
            from win32api import EnumDisplayDevices
        if "EnumDisplaySettings" not in globals():                                                      # Import if not already.
            global EnumDisplaySettings
            from win32api import EnumDisplaySettings
        
        display_info = EnumDisplayDevices()                                                             # Returns PyDISPLAY_DEVICE object

        display_settings = EnumDisplaySettings(display_info.DeviceName, -1)                             # Picks display that seems to have highest framerate?...
        return getattr(display_settings, "DisplayFrequency")

    def darwin():
        # Cannot use pygame.display.get_driver for windows or macos in pygame < v2

        # Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate

        install_dependencies({"Appkit"})

        if "NSScreen" not in globals():                                                                 # Import if not already.
            global NSScreen
            from AppKit import NSScreen

        for screen in NSScreen.screens():
            if framerate < screen.maximumFramesPerSecond():                                             # I the framerate of this display is higher, set its as the new maximum rate.
                framerate = screen.maximumFramesPerSecond()
        
        return framerate

def update_framerate():
    """
    Used for mock vsync to support Pygame < v2.0.0
    """

    if FEATURES["matchcase"]["is_active"]:
        match PLATFORM["kernel"]:
            case "Linux":
                framerate = Compat.linux()
            case "Windows":
                framerate = Compat.nt()
            case "Darwin":
                framerate = Compat.darwin()

    elif PLATFORM["kernel"] == "Linux":
        framerate = Compat.linux()

    elif PLATFORM["kernel"] == "Windows":
        framerate = Compat.nt()

    elif PLATFORM["kernel"] == "Darwin":                                       # I'm trusting that the info I found for MacOS does actually still work this way.
        framerate = Compat.darwin()

    else:
        print("Unsupported or unable to detect system kernel. Defaulting to 30 FPS. This can be overridden in the user settings file.")
    
    framerate = int("%.0f" % framerate)
    DISPLAY["framerate"] = framerate
