#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Levels in a game.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: Classes descibing different levels in a pygame game.
#
# @Script: level.py
# @Date Created: 16 Apr, 2024
# @Last Modified: 19 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------

import pygame
from pygame import mixer
from pygame.image import load
from icecream import ic
from lib.system import DIRWORKING, FEATURES
from lib.section import Section, Blocks
from lib.interface import Button, Checkbox
from lib.entity import *                                                                # Bad practise and loads player class, but loads all entities for easy reference.


class Level():
    """
    Creates and manages the level scape.
    """

    def __init__(self, game):
        self.GAME = game
        self.WINDOW = self.GAME.WINDOW
        self.SCALE = self.GAME.SCALE
        self.SECTION = Section(self.GAME)
        self.FRAMES = Frames(self.GAME)

        self.layout = Menus.Blank()

        self.dx = 0
        self.dy = 0

        self.musicStartTime = 0

        if self.layout.TYPE == "level":
            self.get_level_size()

    def draw(self):
        """
        Draws the level on the screen.
        """

        if self.layout.TYPE == "level":
            self.SECTION.draw(self.layout.LAYOUT)
        elif self.layout.TYPE == "scene":
            self.FRAMES.draw()

        try:
            if pygame.time.get_ticks() - self.musicStartTime >= self.layout.MUSIC.get_length() * 1000:
                ic(self.layout.MUSIC.play())
                self.musicStartTime = pygame.time.get_ticks()
        except AttributeError:
            pass

    def load_layout(self, layout:object, startPositionKey="default"):
        """
        Load a new layout configuration.
        """

        self.GAME.playerActionMove = "neutral"

        try:
            ic(self.layout.MUSIC.stop())
        except AttributeError:
            pass

        if self.layout.TYPE == "menu":
            for interactable in self.layout.INTERACTABLES:
                interactable.remove()

        layout = layout(game=self.GAME)

        if isinstance(layout, Menus.Pause):  # Save the level state before loading something that isn't the next level.
            self.save_level_state()

        if self.layout.TYPE == "level" and layout.TYPE != "level":
            self.GAME.levelOn = False
        elif self.layout.TYPE == "menu" and layout.TYPE != "menu":
            self.GAME.menuOn = False
        elif self.layout.TYPE == "scene" and layout.TYPE != "scene":
            self.GAME.sceneOn = False

        if self.layout.TYPE == "level":
            for entity in self.GAME.entityList:
                entity.destroy()

        self.layout = layout
        ic(self.layout.__class__)

        if self.layout.TYPE == "level":
            self.update_level_size()

            self.GAME.PLAYER.health = 100
            self.GAME.PLAYER.effects = []
            self.GAME.PLAYER.x = self.layout.STARTPOSITIONS[startPositionKey][0]
            self.GAME.PLAYER.y = self.layout.STARTPOSITIONS[startPositionKey][1]

            self.update_deltas_relative_to_player()

        elif self.layout.TYPE == "scene":
            self.FRAMES.load_images()

        # The following shouldn't need to exist, however pygame does not like using a low musicStartTime to trigger the first audio playback.
        try:
            self.layout.MUSIC.play()
            self.musicStartTime = pygame.time.get_ticks()
        except AttributeError:
            pass

    def load_level_state(self):
        """
        Loads the last state of a level.
        """

        self.layout = self.layoutOld
        self.GAME.entityList = self.entityOld
        self.GAME.levelLoad = True

    def save_level_state(self):
        """
        Saves the last state of a level.
        """

        self.layoutOld = self.layout
        self.entityOld = self.GAME.entityList

    def update_deltas_relative_to_player(self):
        """
        Called with a scale update.
        Recalculates self.dx and self.dy so that player is centered properly.
        """

        borderWidthX = (self.SCALE.gameX // 2) + 1
        borderWidthY = (self.SCALE.gameY // 2) + 1

        if self.GAME.PLAYER.x < borderWidthX:
            # Player is in left border
            self.dx = 0

        elif self.GAME.PLAYER.x > self.width - borderWidthX:
            # Player is in right border
            self.dx = self.width - self.SCALE.gameX

        else:
            # Player is within x boundaries
            self.dx = self.GAME.PLAYER.x - borderWidthX


        if self.GAME.PLAYER.y < borderWidthY:
            # Player is in the top border
            self.dy = 0

        elif self.GAME.PLAYER.y > self.height - borderWidthY:
            # Player is in bottom border.
            self.dy = self.height - self.SCALE.gameY

        else:
            # Player is within y boundaries.
            self.dy = self.GAME.PLAYER.y - borderWidthY
        
        self.dx = int(self.dx)
        self.dy = int(self.dy)

    def update_level_size(self):
        """
        Updates the size of the active layout
        """
        width = 0
        height = 0

        for coords, block in self.layout.LAYOUT.items():
            try:
                if coords[2] > width:
                    width = coords[2]
                
                if coords[3] > height:
                    height = coords[3]

            except IndexError:
                if coords[0] > width:
                    width = coords[0]
                
                if coords[1] > height:
                    height = coords[1]
        
        self.width = width
        self.height = height

"""
NAME        :str
HAS_PLAYER  :bool
TYPE        :str = "scene", "menu", "level"

scene           # Does not scale with scaleGame or scaleUi
                # Each obj in LAYOUT is a different frame, not block sections.
                # Does not have a player, so HAS_PLAYER is not checked, and can be removed from definition.

menu            # Does not scale with scaleGame
                # Has a custom event handler used while menu is active. Handler defined in run().

LAYOUT          :dict = {(x, y, rangeX, rangeY): obj}
BGFILL          :tuple = (r, g, b)
                :pygame.surface.Surface
FOLLOW_UP_LEVEL :level.Levels, level.Menus, level.Scenes
"""

class Scenes():
    """
    Class for organising scenes.
    """

    """
    class Example():
        def __init__(self):
            self.IS_SKIPPABLE = True
            self.LAYOUT = [
                            {
                            "frame":        Textures.EXAMPLE_0,                                     # Frame image
                            "frameTime":    5                                                       # Time in seconds that this frame is displayed.
                            },
                            {
                            "frame":        Textures.EXAMPLE_1,
                            "frameTime":    2
                            }
                        ]
            
    """

    class Intro():
        def __init__(self, **kwargs):

            self.TYPE = "scene"
            self.LAYOUT = [
                DIRWORKING + "/data/image/game/blocks/wool_colored_blue.png",
                DIRWORKING + "/data/image/game/blocks/wool_colored_cyan.png",
                DIRWORKING + "/data/image/game/blocks/wool_colored_light_blue.png"
            ]
            self.FOLLOW_UP_LEVEL = Menus.Main
            self.IS_SKIPPABLE = True
            self.TIME_PER_FRAME = 1
            self.BG_FILL = (0, 0, 0)
    
    class Commencing_Slaughter():
        def __init__(self, **kwargs):

            self.TYPE = "scene"
            self.LAYOUT = [
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_4.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png",
                DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png"
            ]
            self.FOLLOW_UP_LEVEL = Levels.Example
            self.IS_SKIPPABLE = False
            self.TIME_PER_FRAME = 0.25

            self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/scenes/commencing_slaughter.mp3")

    class Player_Died():
        def __init__(self, **kwargs):

            self.TYPE = "scene"
            self.LAYOUT = [
                DIRWORKING + "/data/image/game/blocks/wool_colored_red.png"
            ]
            self.FOLLOW_UP_LEVEL = Menus.Player_Died
            self.IS_SKIPPABLE = False

            self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav")

            self.TIME_PER_FRAME = self.MUSIC.get_length()

    class Player_Won():
        def __init__(self, **kwargs):

            self.TYPE = "scene"
            self.LAYOUT = [
                DIRWORKING + "/data/image/game/blocks/wool_colored_lime.png"
            ]
            self.FOLLOW_UP_LEVEL = Menus.Player_Won
            self.IS_SKIPPABLE = True

            self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav")

            self.TIME_PER_FRAME = self.MUSIC.get_length()

class Menus():
    """
    Class for organising menus.
    """

    class Blank():
        def __init__(self, **kwargs):
            self.TYPE = "menu"
            self.HAS_PLAYER = False
            self.INTERACTABLES = []
            self.BG_FILL = (0, 0, 0)

    class Main():
        def __init__(self, game):

            self.TYPE = "menu"
            self.HAS_PLAYER = False
            self.INTERACTABLES = [
                Button(
                    game,
                    "start",
                    100,
                    100,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.LEVEL.load_layout,
                    args=Scenes.Commencing_Slaughter
                ),
                Button(
                    game,
                    "settings",
                    100,
                    200,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.LEVEL.load_layout,
                    #args=Menus.Settings
                    args=Menus.Main
                ),
                Button(
                    game,
                    "exit_to_desktop",
                    100,
                    300,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.MENU_FUNCTIONS.exit_to_desktop
                )
            ]

            self.BG_FILL = pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert()
            self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/ui/menu_main/Backwards-Souls-SoundBible.com-87826574.wav")

    class Settings():
        def __init__(self, game):

            self.TYPE = "menu"
            self.HAS_PLAYER = False
            self.INTERACTABLES = [
                Checkbox(
                    game,
                    "fullscreen",
                    100,
                    100,
                    pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_hover.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_active.png").convert_alpha(),
                    lambda: FEATURES["fullscreen"]["is_active"],                        # Needs to be re-evaluated. Thus lambda function.
                    game.MENU_FUNCTIONS.enter_fullscreen
                )
            ]

            self.BG_FILL = pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert()

    class Pause():
        def __init__(self, game):

            self.TYPE = "menu"
            self.HAS_PLAYER = False
            self.INTERACTABLES = [
                Button(
                    game,
                    "return_to_game",
                    100,
                    100,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.MENU_FUNCTIONS.return_to_game
                ),
                Button(
                    game,
                    "settings",
                    100,
                    200,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.MENU_FUNCTIONS.return_to_game
                    #game.LEVEL.load_layout,
                    #args=Menus.Settings
                ),
                Button(
                    game,
                    "exit_to_menu",
                    100,
                    300,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.LEVEL.load_layout,
                    args=Menus.Main
                )
            ]

            # Save a snapshot of the window to overlay the pause menu on.
            pygame.image.save(game.WINDOW.window, DIRWORKING + "/temp/pausescreen.png")
            self.BG_FILL = pygame.image.load(DIRWORKING + "/temp/pausescreen.png").convert()

    class Player_Died():
        def __init__(self, game):
            self.TYPE = "menu"
            self.HAS_PLAYER = False
            self.INTERACTABLES = [
                Button(
                    game,
                    "exit_to_menu",
                    100,
                    300,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.LEVEL.load_layout,
                    args=Menus.Main
                )
            ]
            self.BG_FILL = load(DIRWORKING + "/data/image/game/blocks/wool_colored_red.png")

    class Player_Won():
        def __init__(self, game):
            self.TYPE = "menu"
            self.HAS_PLAYER = False
            self.INTERACTABLES = [
                Button(
                    game,
                    "exit_to_menu",
                    100,
                    300,
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(),
                    pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(),
                    game.LEVEL.load_layout,
                    args=Menus.Main
                )
            ]
            self.BG_FILL = load(DIRWORKING + "/data/image/game/blocks/wool_colored_lime.png")

class Levels():
    """
    Class for organising levels.
    """

    class Example():
        def __init__(self, game, **kwargs):

            self.TYPE = "level"
            self.HAS_PLAYER = True
            self.LAYOUT = {
                (0, 0, 96, 64): Blocks.block_blue(),
                (2, 1): Blocks.block_rainbow(layer=1),
                (14, 5): Blocks.block_rainbow(layer=1),
                (3, 3, 5, 5): Blocks.puddle_of_souls(game, layer=1),
                (7, 7): Blocks.puddle_of_souls(game, layer=1),
                (4, 9): Blocks.puddle_of_souls(game, layer=1)
            }
            self.ENTITIES = [
                Unnamed(game, 16, 16),
                Unnamed(game,  16, 17),
                Unnamed(game,  17, 16),
                Unnamed(game,  17, 17),
                Unnamed(game,  15, 23),
                Unnamed(game,  41, 52),
                Unnamed(game,  5, 17)
            ]
            self.STARTPOSITIONS = {"default": (0, 0)}
            self.BG_FILL = (0, 0, 0)
            self.FOLLOW_UP_LEVEL = Scenes.Player_Won

class Frames():
    """
    Class for methods pertaining to scenes.
    """

    def __init__(self, game):

        self.GAME = game
        self.WINDOW = self.GAME.WINDOW
        self.clear()

    def clear(self):
        """
        Clear the frames from memory.
        """

        self.images = [
            pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert()
        ]
        self.imageNumber = -1
        self.frames = 0                                                           # Frames rendered by system

    def draw(self):
        """
        Draws the scene.
        """

        if self.frames / self.GAME.clock.get_fps() > self.GAME.LEVEL.layout.TIME_PER_FRAME:    # Use number of frames displaying an image / framerate to get time in sec.
            self.imageNumber = self.imageNumber + 1                                     # Increment the image index
            self.frames = 0

            if len(self.images) == self.imageNumber:
                self.GAME.LEVEL.load_layout(self.GAME.LEVEL.layout.FOLLOW_UP_LEVEL)
                self.clear()
            else:
                self.WINDOW.window.blit(self.images[self.imageNumber], (0, 0))
        elif self.imageNumber == -1:
            self.imageNumber = 0
            self.WINDOW.window.blit(self.images[self.imageNumber], (0, 0))

        else:
            self.frames = self.frames + 1

    def load_images(self):
        """
        Load frames for a scene.
        """

        self.images = []
        
        for path in self.GAME.LEVEL.layout.LAYOUT:
            self.images.append(load(path).convert_alpha())
        
        self.scale_images()
    
    def scale_images(self):
        """
        Scales all images currently loaded.
        """

        for image in self.images:
            self.images[self.images.index(image)] = pygame.transform.scale(image, (self.WINDOW.width, self.WINDOW.height))

    def skip(self):
        """
        Skips the level.
        """

        self.clear()
        self.GAME.LEVEL.load_layout(self.GAME.LEVEL.layout.FOLLOW_UP_LEVEL)

    def update_scale(self):
        """
        Updates the scaling of the active frames.
        """

        if self.WINDOW.width > self.images[0].get_width() or self.WINDOW.height > self.images[0].get_height():
            self.load_images()
        else:
            self.scale_images()