#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Intro to Pygame.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Getting started with using PyGame engine.
#
# @Script: main.py
# @Date Created: 22 Mar, 2024
# @Last Modified: 19 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------

# Load lib.system to ensure everything is good to go dependency and system config wise.
from lib import system

# Knowing that all external dependencies are acessible, import the rest.
import pygame, json, threading
from pygame import freetype, mixer                                                      # Requires explicit importing.
from icecream import ic
from time import sleep
from lib import interface, level, entity, section


# Configure IceCream a bit.
# Use for release version.

def debug(msg, file="debug.txt"):
    open(system.DIRWORKING + "/logs/" + file, "w")                                      # Build a debug file if one doesn't exist.
    with open(system.DIRWORKING + "/logs/" + file, "a") as f:                           # Append each debugging report to file
        f.write(msg + "\n")   

ic.configureOutput(prefix="DEBUG | ", outputFunction=debug, includeContext=True)

#ic.configureOutput(prefix="DEBUG | ")

class Game():
    def __init__(self):

        self.initOn = True
        self.initSkip = False

        self.clock = pygame.time.Clock()

        # Set game icon for system.
        gameIcon = pygame.image.load(system.DIRWORKING + "/data/image/game/icon.png")
        pygame.display.set_icon(gameIcon)

        self.VERSION = "0.0.1-alpha"

        pygame.display.set_caption(f"Shroom of Doom: The Massacring Mushroom v{self.VERSION}")

        self.SETTINGS = {}

        self.load_settings(reset=False)

        # Initialise the window.
        self.WINDOW = self.Window(
            self,
            self.SETTINGS["window"]["size"]["width"],
            self.SETTINGS["window"]["size"]["height"]
        )

        self.bgFill = (255, 255, 255)
        self.WINDOW.window.fill(self.bgFill)

        self.SCALE = self.Scale(game=self)

        self.LEVEL = level.Level(self)

        # Set up a font
        freetype.init()
        self.REPORTFONT = freetype.Font(system.DIRWORKING + "/data/font/Domestic_Manners.ttf", self.WINDOW.height * 0.03)

        # Start audio mixer
        mixer.init()

        # Prep for BS loading screen.
        self.gameProgressBars = []

        self.bgImages = []
        self.bgImagesIndex = 0

        # Add more images to animate bg.
        for imageDir in [
            "/data/image/game/blocks/wool_colored_gray.png"
        ]:
            image = pygame.image.load(system.DIRWORKING + imageDir)

            image = pygame.transform.scale(image, (self.WINDOW.width, self.WINDOW.height))

            self.bgImages.append(image)
        
        # The following variables are created so that the progress bar is initialized to display immediately with the loading screen.
        # If not listed, can run into a condition where these are undefined as the thread hasn't reached defining them fast enough.
        self.initJobsComplete = 0
        self.initJobsTotal = 1
        self.initJobsImportIncomplete = 3                                               # Variable posessing the total number of BS Import report groups to fix scaling on progress bar if init is sped up.
        self.loadStatus = ""
        self.is_initSkipFirst = False                                                   # Variable describing if it is the first time that update_report() has seen the initSkip.

        self.initProgressBar = interface.Progress_bar(
            self,
            5,
            90,
            90,
            [
                pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/mushroom_silhouette_1.png"),
                pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/mushroom_silhouette_2.png")
            ],
            pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_left.png"),
            pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_right.png"),
            pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_clear.png"),
            pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_fill.png")
        )

        windowRefresh = threading.Thread(target=self.init_jobs)                         # Create a thread to run the mostly bs readouts.
        windowRefresh.start()                                                           # Start that thread.

        p = 0                                                                           # Number of times that pygame.K_p has been pressed.

        # Event handler and display rendering must be done on master thread due to pygame structure.
        while self.initOn:

            self.clock.tick(30)                                                         # Run at a lower framerate to reduce system resource consumption when not needed.

            self.draw_background()

            # Update objects on screen.
            pygame.display.flip()

            # Event handler
            for event in pygame.event.get():                                            # Short event handler to allow the game to be exited during startup.
                if event.type == pygame.QUIT:

                    # Cannot currently kill thread. Must let it run out, otherwise employ spaghetti code in its target function.
                    self.initOn = False
                    self.gameOn = False

                    raise SystemExit

                elif event.type == pygame.VIDEORESIZE:
                        self.WINDOW.update_window(event.w, event.h)
                    
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_p:
                        p = p + 1

                        if p == 2:
                            # Cannot currently kill thread. Must let it run out, otherwise employ spaghetti code in its target function.
                            self.initSkip = True
        
        # Clean memory
        del self.initJobsComplete, self.initJobsTotal, self.loadStatus, self.is_initSkipFirst, self.initSkip, self.initProgressBar, self.bgImagesIndex, self.bgImages, self.textRendered

    def init_jobs(self):
        """
        The actual init while the main thread handles the window rendering.
        """

        # Dicts in list to alow iteration as dicts not iterable in python 3.3.1
        self.reportsImports1 = [
            {"report": "pygame", "time": 0.25},
            {"report": "pygame.freetype", "time": 0.25},
            {"report": "icecream.ic", "time": 0.25},
            {"report": "json", "time": 0.25},
            {"report": "threading", "time": 0.5},
            {"report": "math", "time": 0.5},
            {"report": "time.sleep ;)", "time": 0.5},
            {"report": "levels", "time": 0.25},
            {"report": "entities", "time": 0.25},
            {"report": "system", "time": 0.25},
            {"report": "urmom", "time": 0.5},
            {"report": "sections.Section", "time": 0.4},
            {"report": "scenes.Scene", "time": 0.25},
            {"report": "smingusdinglebus.Dinglebob", "time": 0.5},
            {"report": "pills.BLUE - Yes, this is all a lie.", "time": 1},
            {"report": "thebritish.Food.OldeTimeFishandChips - Wait until I'm done lying.", "time": 1},
            {"report": "Cannot find randos-free-ram-bin in system repos", "time": 2},
            {"report": "Cannot find randos-free-ram-git in system repos", "time": 2}
        ]

        self.reportsImports2 = [
            {"report": "001%", "time": 2},
            {"report": "002%", "time": 1},
            {"report": "003%", "time": 2},
            {"report": "030%", "time": 0.5},
            {"report": "031%", "time": 0.5},
            {"report": "099%", "time": 4},
            {"report": "105%", "time": 2},
            {"report": "400%", "time": 3},
            {"report": "666%", "time": 3},
            {"report": "6546588795009842089420845126960248328684206489564465987951613174159489636682%", "time": 0.75}
        ]

        self.reportsImports3 = [
            {"report": "Decompressing with gzip", "time": 1},
            {"report": "Running build", "time": 1},
            {"report": "Aquiring additional permissions without permission", "time": 2},
            {"report": "Running makefile", "time": 1},
            {"report": "Build Complete.", "time": 2}
        ]

        self.initJobsTotal = len(self.reportsImports1) + len(self.reportsImports2) + len(self.reportsImports3) + 7  # 7 additional config reports.
        self.initJobsComplete = 0

        for i in range(0, 3):                                                           # Report all import lies.
            self.update_init_load("import", i)

        self.update_init_load("config", 0)

        self.update_init_load("config", 1)

        # Bad practice, but overwrite the class to load the textures once only.
        section.Textures = section.Textures(self.SCALE.unitBlockPx)

        self.update_init_load("config", 2)

        self.PLAYER = entity.Player(self)

        self.update_init_load("config", 3)

        self.entityList = []

        self.update_init_load("config", 4)

        # Temp hardcoded player control buttons.
        self.controlsPlayerMovement = {
            "run_u": [pygame.K_UP, pygame.K_w],
            "run_d": [pygame.K_DOWN, pygame.K_s],
            "run_l": [pygame.K_LEFT, pygame.K_a],
            "run_r": [pygame.K_RIGHT, pygame.K_d],
            "dash": [pygame.K_LSHIFT, pygame.K_RSHIFT]
        }

        self.controlsPlayerInteraction = {
            "trigger": [pygame.K_RETURN, pygame.K_e]
        }

        self.update_init_load("config", 5)

        self.MENU_FUNCTIONS = interface.Interface_Functions(game=self)

        self.update_init_load("config", 6)

        self.gameInteractables = []                                                           # Contains button classes that must have their conditions updated.

        self.LEVEL.load_layout(level.Scenes.Intro)

        # Temp override for user settings
        self.WINDOW.auto_configure()

        self.update_init_load("config", 7)

        self.gameOn = True
        self.initOn = False

    def update_init_load(self, type:str, item:int = 0):
        """
        Initial load reports and lies.
        """

        try:
            if not self.initSkip:
                if system.FEATURES["matchcase"]["is_active"]:
                    match type:
                        case "import":
                            match item:
                                case 0:
                                    reports = self.reportsImports1
                                    msg = "Importing Libraries: "
                                case 1:
                                    reports = self.reportsImports2
                                    msg = "Importing Libraries: ftp://www.ram.sus/downloads/nightlybuilds - Cloning... "
                                case 2:
                                    reports = self.reportsImports3
                                    msg = "Importing Libraries: randos-free-ram - "
                                case _:
                                    pass

                            self.initJobsImportIncomplete = self.initJobsImportIncomplete - 1
                        
                        case "config":
                            msg = "Configuration: "

                            match item:
                                case 0:
                                    reports = [{"report": "Not entirely a lie... if only.", "time": 2}]
                                case 1:
                                    reports = [{"report": "Section - Textures", "time": 0.25}]
                                case 2:
                                    reports = [{"report": "Entity - Player", "time": 0.25}]
                                case 3:
                                    reports = [{"report": "Entity - Other", "time": 0.25}]
                                case 4:
                                    reports = [{"report": "Game - Controls", "time": 0.25}]
                                case 5:
                                    reports = [{"report": "Interface - Control Functions", "time": 0.25}]
                                case 6:
                                    reports = [{"report": "Loading Menu Interface...", "time": 0.25}]
                                case 7:
                                    # Final report time is very quick as a manual wait is applied afterwards so that there is time for reader to register the "Complete." on display.
                                    reports = [{"report": "Complete.", "time": 2}]
                                case _:
                                    pass
                        
                        case _:
                            reports = [{"report": "Skipping BS", "time": 0.1}]
                            msg = "ha ha, you typed pp: "

                            self.update_report(reports, msg)

                else:
                    if type == "import":
                        
                        if item == 0:
                            reports = self.reportsImports1
                            msg = "Importing Libraries: "
                        elif item == 1:
                            reports = self.reportsImports2
                            msg = "Importing Libraries: ftp://www.ram.sus/downloads/nightlybuilds - Cloning... "
                        elif item == 2:
                            reports = self.reportsImports3
                            msg = "Importing Libraries: randos-free-ram - "
                        
                        self.initJobsImportIncomplete = self.initJobsImportIncomplete - 1

                    elif type == "config":
                        msg = "Configuration: "

                        if item == 0:
                            reports = [{"report": "Not entirely a lie... if only.", "time": 2}]
                        elif item == 1:
                            reports = [{"report": "Section - Textures", "time": 0.5}]
                        elif item == 2:
                            reports = [{"report": "Entity - Player", "time": 0.5}]
                        elif item == 3:
                            reports = [{"report": "Entity - Other", "time": 0.25}]
                        elif item == 4:
                            reports = [{"report": "Game - Controls", "time": 0.5}]
                        elif item == 5:
                            reports = [{"report": "Interface - Control Functions", "time": 1}]
                        elif item == 6:
                            reports = [{"report": "Loading Menu Interface...", "time": 1}]
                        elif item == 7:
                            # Final report time is very quick as a manual wait is applied afterwards so that there is time for reader to register the "Complete." on display.
                            reports = [{"report": "Complete.", "time": 1}]

            else:
                reports = [{"report": "Skipping BS", "time": 0.1}]
                msg = "ha ha, you typed pp: "

            self.update_report(reports, msg)

        except AttributeError:                                                          # If game stopped during init, avoid erroring out.
            pass

    def update_report(self, reports, msg):
        """
        Updates the displayed reports.
        """
        
        for report in reports:
            if self.is_initSkipFirst:                                                   # If skipping and done the action below, just pass. Nothing to waste time processing.
                pass

            elif self.initSkip and not self.is_initSkipFirst:                           # If supposed to skip BS, but in the middle of a series of reports, recalculate and skip. Only ic skipping report to sdtout once.
                self.initJobsTotal = self.initJobsComplete + 8 + self.initJobsImportIncomplete

                # Clean memory
                del self.initJobsImportIncomplete, self.reportsImports1, self.reportsImports2, self.reportsImports3

                report = {"report": "Skipping BS", "time": 0.1}
                msg = "ha ha, you typed pp: "

                self.loadStatus = msg + report["report"]
                ic(self.loadStatus)

                self.is_initSkipFirst = True

                break

            else:                                                                       # Otherwise ic the bs reports.

                self.loadStatus = msg + report["report"]

                ic(self.loadStatus)
                sleep(report["time"])

                self.initJobsComplete = self.initJobsComplete + 1


    def draw_background(self):
        """
        Sets and redraws the background.
        """

        if self.initOn:

            self.bgImagesIndex = self.bgImagesIndex + 1

            if self.bgImagesIndex >= len(self.bgImages):
                self.bgImagesIndex = 0

            self.WINDOW.window.blit(self.bgImages[self.bgImagesIndex], (0, 0))

            self.initProgressBar.draw(self.initJobsComplete, self.initJobsTotal)

            # Render and display the mostly bs reports.
            self.textRendered = self.REPORTFONT.render(self.loadStatus, (255, 255, 255))[0]
            
            self.WINDOW.window.blit(
                self.textRendered,
                (
                    self.initProgressBar.x + self.initProgressBar.barImages["left"].get_width(),
                    self.initProgressBar.y - self.textRendered.get_height()
                )
            )

        else:
            # Clear window
            if self.bgFill != self.LEVEL.layout.BG_FILL:                                # If the background has changed (may or may not change between levels)
                self.bgFill = self.LEVEL.layout.BG_FILL                                 # Update it locally.
                
                if isinstance(self.bgFill, pygame.surface.Surface):                     # If it's an image, scale it accordingly.

                    self.bgFill = pygame.transform.scale(self.bgFill, (self.WINDOW.width, self.WINDOW.height))

            if isinstance(self.bgFill, tuple):                                          # If background is a solid colour fill
                self.WINDOW.window.fill(self.bgFill)
            else:                                                                       # Else background is an image fill.
                self.WINDOW.window.blit(self.bgFill, (0, 0))

    def load_settings(self, reset:bool = False):
        """
        Loads user settings from file.
        """

        if reset is False:                                                              # If not told to reset settings to default, load user settings.
            with open(system.DIRWORKING + "/config/settings/user.json") as f:
                self.SETTINGS = json.load(f)
        else:
            with open(system.DIRWORKING + "/config/settings/default.json") as f:
                self.INFO["settings"] = json.load(f)

        ic(self.SETTINGS)

    def run(self):
        """
        Main run loop.
        """

        self.playerActionMove = "neutral"
        self.levelLoad = False
        self.menuOn = True

        # Required for self.update_framerate_average
        #self.framerates = []

        while self.gameOn:

            # For menus that pause the game, self.levelLoad must be made True on resume.
            if self.levelLoad:
                self.LEVEL.load_level_state()
                self.levelLoad = False

            if system.FEATURES["matchcase"]["is_active"]:
                match self.LEVEL.layout.TYPE:
                    case "level":
                        self.levelOn = True

                        while self.levelOn:

                            self.clock.tick(self.WINDOW.framerate)

                            if self.PLAYER.health <= 0:
                                # Player died, end game.
                                self.levelOn = False
                                self.LEVEL.load_layout(level.Scenes.Player_Died)
                                break

                            self.draw_background()

                            self.LEVEL.draw()

                            self.PLAYER.draw(self.playerActionMove)

                            if len(self.entityList) != 0:                               # There are still entities to kill.
                                for entity in self.entityList:
                                    if entity.health > 0:                               # Draw entities that are alive..
                                        entity.draw()
                                    else:
                                        entity.destroy()                                # Remove entities that have died.
                            else:
                                # All entities killed, end game.
                                self.LEVEL.load_layout(self.LEVEL.layout.FOLLOW_UP_LEVEL)

                            # Render Framerate
                            textRendered = self.REPORTFONT.render(str(self.clock.get_fps()), (255, 255, 255))[0]
                            self.WINDOW.window.blit(textRendered, (0, 0))

                            # Update objects on screen
                            pygame.display.flip()

                            # Event handler
                            for event in pygame.event.get():
                                match event.type:
                                    case pygame.QUIT:
                                        self.MENU_FUNCTIONS.exit_to_desktop()
                                
                                    case pygame.VIDEORESIZE:
                                        self.WINDOW.update_window(event.w, event.h)

                                    case pygame.KEYDOWN:
                                        if event.key == pygame.K_ESCAPE:                                # Pause game on ESC
                                            self.LEVEL.load_layout(level.Menus.Pause)

                                        else:
                                            for controlsDict in [
                                                self.controlsPlayerMovement,
                                                self.controlsPlayerInteraction
                                            ]:
                                                for action, keyList in controlsDict.items():            # Check movement triggers
                                                    if event.key in keyList:                            # Key pressed is associated with a movement action.
                                                        match controlsDict:
                                                            case self.controlsPlayerMovement:
                                                                if action in ["run_l", "run_r", "run_u", "run_d"]:
                                                                    self.playerActionMove = action
                                                                elif action == "dash":
                                                                    self.PLAYER.dash(self.playerActionMove)
                                                            case self.controlsPlayerInteraction:
                                                                if action == "trigger":
                                                                    self.PLAYER.interact()
                                    
                                    case pygame.KEYUP:
                                        if self.LEVEL.layout.HAS_PLAYER:                                # If layout shows a player
                                            for controlsDict in [
                                                self.controlsPlayerMovement,
                                                self.controlsPlayerInteraction
                                            ]:
                                                for action, keyList in controlsDict.items():            # Check movement triggers
                                                    if event.key in keyList:                            # Key released is associated with a movement action.
                                                        if controlsDict == self.controlsPlayerMovement:
                                                            if action == self.playerActionMove:              # Only if the most recent movement action key was released
                                                                self.playerActionMove = "neutral"            # stop the player.
                                    
                                    case pygame.MOUSEBUTTONDOWN:
                                        if 1 in pygame.mouse.get_pressed():
                                            self.PLAYER.attack()

                                    case pygame.MOUSEWHEEL:

                                        # Scale the level
                                        if self.SETTINGS["controls"]["mouse"]["is_inverted"]:
                                            scaleNew = self.SCALE.scaleGame + (event.y * 2)
                                        else:
                                            scaleNew = self.SCALE.scaleGame + (event.y * 2)
                                        
                                        if scaleNew in self.SCALE.scaleRange:           # Limit the scaling range (limit level zoom).

                                            self.SCALE.scaleGame = scaleNew

                                            ic(self.SCALE.update_units_game())

                                            ic(self.PLAYER.update_scale())

                                            for entity in self.entityList:
                                                entity.update_scale()

                                            section.Textures.update_scale(self.SCALE.unitBlockPx)

                                            self.LEVEL.update_deltas_relative_to_player()

                                            if isinstance(self.bgFill, pygame.surface.Surface): # Update the scaling of the bgFill if it's an image fill.
                                                width = self.WINDOW.width / self.GAME.gameX
                                                height = self.WINDOW.height / self.GAME.gameY

                                                pygame.transform.scale(self.bgFill, (width, height))

                                    case _:                                             # Except all other events.
                                        pass

                    case "menu":
                        self.menuOn = True
                    
                        while self.menuOn:
                            self.clock.tick(self.WINDOW.framerate)

                            self.draw_background()

                            self.LEVEL.draw()

                            if self.LEVEL.layout.HAS_PLAYER:
                                self.PLAYER.draw()

                            # Refresh interactables.
                            for interactable in self.gameInteractables:
                                interactable.draw()

                            pygame.display.flip()

                            for event in pygame.event.get():
                                match event.type:
                                    case pygame.QUIT:                                   # Exit game.
                                        self.MENU_FUNCTIONS.exit_to_desktop()
                                    
                                    case pygame.VIDEORESIZE:
                                        self.WINDOW.update_window(event.w, event.h)

                                    case pygame.KEYDOWN:
                                        if event.key == pygame.K_ESCAPE:
                                            if isinstance(self.LEVEL.layout, level.Menus.Pause):
                                                self.MENU_FUNCTIONS.return_to_game()

                                    case pygame.MOUSEBUTTONDOWN:
                                        if 1 in pygame.mouse.get_pressed():
                                            for interactable in self.gameInteractables:
                                                if interactable.is_hovered():
                                                    if interactable.functionArgs is not None:
                                                        interactable.function(interactable.functionArgs)
                                                    else:
                                                        interactable.function()

                                    case _:
                                        pass

                    case "scene":
                        self.sceneOn = True

                        escapes = 0
                        
                        while self.sceneOn:
                            self.clock.tick(self.WINDOW.framerate)

                            self.LEVEL.FRAMES.draw()

                            pygame.display.flip()

                            for event in pygame.event.get():
                                match event.type:
                                    case pygame.QUIT:                                   # Exit game.
                                        self.MENU_FUNCTIONS.exit_to_desktop()
                                    
                                    case pygame.VIDEORESIZE:
                                        self.WINDOW.update_window(event.w, event.h)

                                        self.LEVEL.FRAMES.update_scale()

                                    case pygame.KEYDOWN:
                                        if self.LEVEL.layout.IS_SKIPPABLE and event.key == pygame.K_ESCAPE:
                                            if escapes == 1:
                                                self.LEVEL.FRAMES.skip()
                                            else:
                                                escapes = escapes + 1

                                    case _:
                                        pass

            elif self.LEVEL.layout.TYPE == "level":                                     # Run the general level rederer and event handler.
                self.levelOn = True

                while self.levelOn:

                    self.clock.tick(self.WINDOW.framerate)

                    if self.PLAYER.health <= 0:
                        # Player died, end game.
                        self.LEVEL.load_layout(level.Scenes.Player_Died)

                    self.draw_background()

                    self.LEVEL.draw()

                    self.PLAYER.draw(self.playerActionMove)

                    if len(self.entityList) != 0:
                        for entity in self.entityList:
                            if entity.health > 0:
                                entity.draw()
                            else:
                                entity.destroy()
                    else:
                        # All entities killed, end game.
                        self.LEVEL.load_layout(self.LEVEL.layout.FOLLOW_UP_LEVEL)

                    # Render Framerate
                    textRendered = self.REPORTFONT.render(str(self.clock.get_fps()), (255, 255, 255))[0]
                    self.WINDOW.window.blit(textRendered, (0, 0))

                    # Update objects on screen
                    pygame.display.flip()

                    # Event handler
                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:
                            self.MENU_FUNCTIONS.exit_to_desktop()
                        
                        elif event.type == pygame.VIDEORESIZE:
                            self.WINDOW.update_window(event.w, event.h)

                        elif event.type == pygame.KEYDOWN:

                            if event.key == pygame.K_ESCAPE:                            # Pause game on ESC

                                self.LEVEL.load_layout(level.Menus.Pause)

                            else:
                                for controlsDict in [
                                    self.controlsPlayerMovement,
                                    self.controlsPlayerInteraction
                                ]:
                                    for action, keyList in controlsDict.items():        # Check movement triggers
                                        if event.key in keyList:                        # Key pressed is associated with a movement action.
                                            if controlsDict == self.controlsPlayerMovement:
                                                if action in ["run_l", "run_r", "run_u", "run_d"]:
                                                    self.playerActionMove = action      # Keep track of the current movement action.
                                                    ic(self.playerActionMove)
                                                elif action == "dash":
                                                    self.PLAYER.dash(self.playerActionMove)
                                            elif controlsDict == self.controlsPlayerInteraction:
                                                if action == "trigger":
                                                    self.PLAYER.interact()
                        
                        elif event.type == pygame.KEYUP:
                            if self.LEVEL.layout.HAS_PLAYER:                            # If layout shows a player
                                for controlsDict in [
                                    self.controlsPlayerMovement,
                                    self.controlsPlayerInteraction
                                ]:
                                    for action, keyList in controlsDict.items():        # Check movement triggers
                                        if event.key in keyList:                        # Key released is associated with a movement action.
                                            if controlsDict == self.controlsPlayerMovement:
                                                if action == self.playerActionMove:     # Only if the most recent movement action key was released
                                                    self.playerActionMove = "neutral"   # stop the player.
                        
                        elif event.type == pygame.MOUSEBUTTONDOWN:
                            if 1 in pygame.mouse.get_pressed():
                                self.PLAYER.attack()

                        elif event.type == pygame.MOUSEWHEEL:

                            # Scale the level
                            if self.SETTINGS["controls"]["mouse"]["is_inverted"]:
                                scaleNew = self.SCALE.scaleGame + (event.y * 2)
                            else:
                                scaleNew = self.SCALE.scaleGame + (event.y * 2)
                            
                            if scaleNew in self.SCALE.scaleRange:                       # Limit the scaling range (limit level zoom).

                                self.SCALE.scaleGame = scaleNew

                                ic(self.SCALE.update_units_game())

                                ic(self.PLAYER.update_scale())

                                for entity in self.entityList:
                                    entity.update_scale()

                                section.Textures.update_scale(self.SCALE.unitBlockPx)
                                self.LEVEL.FRAMES.update_scale()

                                self.LEVEL.update_deltas_relative_to_player()

                                if isinstance(self.bgFill, pygame.surface.Surface):     # Update the scaling of the bgFill if it's an image fill.
                                    width = self.WINDOW.width / self.GAME.gameX
                                    height = self.WINDOW.height / self.GAME.gameY

                                    pygame.transform.scale(self.bgFill, (width, height))

            elif self.LEVEL.layout.TYPE == "menu":                                      # Run the general menu renderer and event handler.
                self.menuOn = True
                
                while self.menuOn:
                    self.clock.tick(self.WINDOW.framerate)

                    self.draw_background()

                    self.LEVEL.draw()

                    if self.LEVEL.layout.HAS_PLAYER:
                        self.PLAYER.draw()

                    # Refresh interactables.
                    for interactable in self.gameInteractables:
                        interactable.draw()

                    pygame.display.flip()

                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:                                   # Exit game.
                            self.MENU_FUNCTIONS.exit_to_desktop()
                        
                        elif event.type == pygame.VIDEORESIZE:
                            self.WINDOW.update_window(event.w, event.h)

                        elif event.type == pygame.KEYDOWN:
                            if event.key == pygame.K_ESCAPE:
                                if isinstance(self.LEVEL.layout, level.Menus.Pause):
                                    self.MENU_FUNCTIONS.return_to_game()

                        elif event.type == pygame.MOUSEBUTTONDOWN:
                            if 1 in pygame.mouse.get_pressed():
                                for interactable in self.gameInteractables:
                                    if interactable.is_hovered():
                                        if interactable.functionArgs is not None:
                                            interactable.function(interactable.functionArgs)
                                        else:
                                            interactable.function()

            elif self.LEVEL.layout.TYPE == "scene":                                     # Run the general scene renderer and event handler.
                self.sceneOn = True

                escapes = 0
                
                while self.sceneOn:
                    self.clock.tick(self.WINDOW.framerate)

                    self.LEVEL.FRAMES.draw()

                    pygame.display.flip()

                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:                                   # Exit game.
                            self.MENU_FUNCTIONS.exit_to_desktop()
                        
                        elif event.type == pygame.VIDEORESIZE:
                            self.WINDOW.update_window(event.w, event.h)

                        elif event.type == pygame.KEYDOWN:
                            if self.LEVEL.layout.IS_SKIPPABLE and event.key == pygame.K_ESCAPE:
                                if escapes == 1:
                                    self.LEVEL.FRAMES.skip()
                                else:
                                    escapes = escapes + 1

    def update_framerate_average(self):
        framerateAverage = 0

        for i in self.framerates:
            framerateAverage += i
        
        return framerateAverage / len(self.framerates)


    class Window():
        """
        Creates a window.
        """

        def __init__(self, game, width:int = 1440, height:int = 720, framerate:int = 30):

            self.GAME = game

            self.width = width
            self.height = height
            self.framerate = framerate

            self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE)

        def auto_configure(self):
            """
            Autoconfigures display settings.
            """

            system.update_framerate()

            framerate = system.DISPLAY["framerate"]
            
            ic("Detected highest framerate for this window as", framerate)
            ic("Applying...")

            self.framerate = framerate
            self.GAME.clock.tick(self.framerate)

            ic("Done.")
        
        def toggle_fullscreen(self):
            """
            Toggles window fullscreening.
            Returns False if cannot.
            """

            # Isolate Linux platforms as this method has always worked as get_driver has supported x11 and wayland since pagame v1.
            if system.PLATFORM["kernel"] == "Linux" or system.PLATFORM["libraries"]["pygame"] >= (2, 0, 0):                                    
                pygame.display.toggle_fullscreen()
                return True
            else:
                ic("Cannot enter fullscreen. Please update pygame to v2.0.0 or newer.")
                return False
        
        def toggle_vsync(self):
            """
            Sets up vsync if possible. Falls back to pseudo vsync from system library.
            """

            if system.FEATURES["vsync"]["is_active"]:
                self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE, vsync=1)
            else:
                system.update_framerate()
        
        def update_window(self, width, height):
            """
            Updates the window.
            """

            # Window resizing handling changed in pygame v2 to be handled automatically.
            if system.FEATURES["resizeable"]["is_active"]:
                self.width = self.window.get_width()
                self.height = self.window.get_height()

            else:
                self.width = width
                self.height = height

                if system.FEATURES["vsync"]["is_active"]:
                    self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE, vsync=1)
                else:
                    self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE)

            self.GAME.SCALE.update_units_game()

    class Scale():
        """
        Handles all globally referenced scaling.
        """

        def __init__(self, game):
            self.GAME = game
            self.WINDOW = self.GAME.WINDOW

            self.scaleGlobal = 2                                                        # Adjusts other scales for HiDPI screens.
            self.scaleGame = 36 * self.scaleGlobal
            self.scaleUi = 1 * self.scaleGlobal

            self.scaleRange = range(int("%.0f" % (self.scaleGame * (1/2))), int("%.0f" % (self.scaleGame * 2)))

            self.update_units_game()

        def update_units_game(self):
            """
            Gets game units.
            """

            self.gameY = (1 / self.scaleGame) * self.WINDOW.height                      # Blocks along Y
            
            if (self.WINDOW.height / self.gameY) % 2 != 0:                              # If not filling full screen, add a block.
                self.gameY = int((self.gameY // 1) + 2)                                 # Produce a clean integer.
            else:
                self.gameY = int((self.gameY // 1) + 1)
            
            self.unitBlockPx = self.WINDOW.height / self.gameY                          # Defines the edge length in pixels of a section.

            if (self.unitBlockPx % 2) != 0:                                             # Is not an integer.
                self.unitBlockPx = (self.unitBlockPx // 1) + 1                          # Round up.

            self.gameX = self.WINDOW.width / self.unitBlockPx                           # Blocks along X

            if (self.WINDOW.width / self.gameX) % 2 != 0:                               # If not filling full screen, add a block.
                self.gameX = int((self.gameX // 1) + 2)                                 # Produce a clean integer.
            else:
                self.gameX = int((self.gameX // 1) + 1)

game = Game()
game.run()

pygame.quit()