#!~/.pyenv/versions/3.11.6/bin/python
#
# Copyright (c) 2024 Cutieguwu | Olivia Brooks
#
# -*- coding:utf-8 -*-
# @Title: Players and Characters.
# @Author: Cutieguwu | Olivia Brooks
# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca
# @Description: AIs for players and characters.
#
# @Script: entity.py
# @Date Created: 22 Mar, 2024
# @Last Modified: 19 Jun, 2024
# @Last Modified by: Cutieguwu | Olivia Brooks
# ----------------------------------------------------------

import pygame
from pygame import mixer
from icecream import ic
from random import randint
from lib.system import DIRWORKING, FEATURES
from lib.section import Blocks


class Player():
    """
    Handles the player character and associated animations and states.
    """

    def __init__(self, game, character:str = "default_a"):
        self.GAME = game
        self.WINDOW = self.GAME.WINDOW
        self.SCALE = self.GAME.SCALE
        self.LEVEL = self.GAME.LEVEL
        
        self.images = {}

        self.x = 0
        self.y = 0
        self.layer = 1

        self.health = 100
        self.effects = []

        self.update_images(character)

        self.animationClock = 0
        self.stateDirection = "neutral"                                             # String of character state
        self.stateDirectionOld = "run_d"                                            # Last movement action by player.
        self.stateDirectionOldX = "run_l"                                           # Last x movement action for rotating to neutral from run_u
        self.stateFrame = 1                                                         # Animation frame number
        self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]   # Default inital character state.
        self.timeSinceInteraction = 0
        self.moveClock = 0

        self.update_scale()

    def adjust_center(self, x, y):
        """
        Determines if the player can be centered on the level instead of shifting position from center.
        """

        # if newPosition is greater than half the viewable area relative to dx if it were adjusted.
        # less efficient:
        # if dx were adjusted, if any wall becomes only void, adjust player position.

        dCenterX = self.SCALE.gameX / 2
        dCenterY = self.SCALE.gameY / 2

        if x <= dCenterX and self.LEVEL.dx == 0:
            # Player in left region of layout.
            # Player must move.
            pass
        elif x > (self.LEVEL.width - dCenterX) + 1:
            # Player in right region of layout.
            # Player must move.
            pass
        else:
            # Level should move.

            newDx = self.LEVEL.dx + (x - self.x)

            if newDx >= 0:
                self.LEVEL.dx = newDx
        
        if y <= dCenterY and self.LEVEL.dy == 0:
            # Player in top region of layout
            # Player must move.
            pass
        elif y > (self.LEVEL.height - dCenterY) + 1:
            # Player in bottom region of layout
            # Player must move.
            pass
        else:
            # Level should move.
            newDy = self.LEVEL.dy + (y - self.y)

            if newDy >= 0:
                self.LEVEL.dy = newDy

        self.x = x
        self.y = y

    def attack(self):
        """
        Makes the player attack.
        """

        attackPositions = []

        if self.stateDirection == "run_l":
            x = self.x - 1

            attackPositions.append((x, self.y - 1))
            attackPositions.append((x, self.y))
            attackPositions.append((x, self.y + 1))

        elif self.stateDirection == "run_r":
            x = self.x + 1

            attackPositions.append((x, self.y - 1))
            attackPositions.append((x, self.y))
            attackPositions.append((x, self.y + 1))

        elif self.stateDirection == "run_u":
            y = self.y - 1

            attackPositions.append((self.x - 1, y))
            attackPositions.append((self.x, y))
            attackPositions.append((self.x + 1, y))

        elif self.stateDirection == "run_d":
            y = self.y + 1

            attackPositions.append((self.x - 1, y))
            attackPositions.append((self.x, y))
            attackPositions.append((self.x + 1, y))

        for entity in self.GAME.entityList:
            if (entity.x, entity.y) in attackPositions:
                entity.health = entity.health - 25

    def can_interact(self, x, y):
        """
        Checks if the player can interact with the section in front of it.
        """

        try:                                                                            # Try to set the section based on coords.
            coords = (x, y)
            section = self.LEVEL.layout.LAYOUT[coords]
        except KeyError:
            for coords, block in self.LEVEL.layout.LAYOUT.items():                      # Deeper search, check if block in an area fill.
                if block.layer == self.layer:
                    try:
                        if y in range(coords[1], coords[3] + 1):
                            if x in range(coords[0], coords[2] + 1):
                                section = self.LEVEL.layout.LAYOUT[coords]              # Set the definition as the desired section.
                                break
                    except IndexError:                                                  # Block doesn't define a range.
                        pass

        try:
            if section.TEXTURE[section.state]["IS_INTERACTABLE"]:
                return coords
            else:
                return False
        except UnboundLocalError:                                                       # Could not find a block drawn at those coords.
            return False

    def can_move(self, x, y):
        """
        Checks if the player can move to a position.
        """

        try:                                                                            # Try to set the section based on coords.
            section = self.LEVEL.layout.LAYOUT[(x, y)]
        except KeyError:
            layerLast = -1

            for coords, block in self.LEVEL.layout.LAYOUT.items():                      # Deeper search, check if block in an area fill.
                if block.layer <= self.layer and block.layer >= layerLast:
                    try:
                        if y in range(coords[1], coords[3] + 1):
                            if x in range(coords[0], coords[2] + 1):
                                section = self.LEVEL.layout.LAYOUT[coords]              # Set the definition as the desired section.
                                layerLast = block.layer
                    except IndexError:                                                  # Block doesn't define a range.
                        pass

        try:
            if section.TEXTURE[section.state]["IS_COLLIDABLE"]:
                return False
            else:
                for entity in self.GAME.entityList:
                    if entity.x == x and entity.y == y:                       # If there is an entity at this location, cannot move.
                        return False
                return True
        except UnboundLocalError:                                                       # Could not find a block drawn at those coords.
            return False

    def dash(self, direction):
        """
        Dashes player in gameX and gameY.
        """

        x, y = self.x, self.y

        dashDist = 0

        canDash = False

        # If Player is not moving to same position as currently located
        while dashDist != 5:
            dashDist = dashDist + 1

            if FEATURES["matchcase"]["is_active"]:
                match direction:
                    case "run_l":
                        x = self.x - dashDist
                        self.stateDirectionOldX = direction
                    case "run_r":
                        x = self.x + dashDist
                        self.stateDirectionOldX = direction
                    case "run_u":
                        y = self.y - dashDist
                    case "run_d":
                        y = self.y + dashDist

            # Iterate until player is trying to move to a position that it can move.
            elif direction == "run_l":
                x = self.x - dashDist
                self.stateDirectionOldX = direction
            elif direction == "run_r":
                x = self.x + dashDist
                self.stateDirectionOldX = direction
            elif direction == "run_u":
                y = self.y - dashDist
            elif direction == "run_d":
                y = self.y + dashDist

            if self.can_move(x, y):
                canDash = True
                moveX, moveY = x, y
            else:                                                         # Prevent warping through blocks if dashing immediately against them.
                moveX, moveY = self.x, self.y
                canDash = False
                break

        if canDash:
            self.adjust_center(moveX, moveY)
            self.moveClock = 0

        self.update_animation(direction, is_updateMove = True)

    def draw(self, direction):
        """
        Draw the player on the screen.
        """

        self.update_move(direction)

        location = ((self.x - self.LEVEL.dx) * self.SCALE.unitBlockPx, (self.y - self.LEVEL.dy) * self.SCALE.unitBlockPx)

        self.WINDOW.window.blit(self.stateImage, location)

        for effect in self.effects:
            self.WINDOW.window.blit(effect.images[f"{self.stateDirection}_{self.stateFrame}"], location)

    def interact(self):
        """
        Interacts with the object in front of the player.
        """

        x = self.x
        y = self.y

        if self.stateDirection == "neutral":
            if self.stateDirectionOld == "run_l":
                x = self.x - 1
            elif self.stateDirectionOld == "run_r":
                x = self.x + 1
            elif self.stateDirectionOld == "run_u":
                y = self.y - 1
            elif self.stateDirectionOld == "run_d":
                y = self.y + 1
        else:
            if self.stateDirection == "run_l":
                x = self.x - 1
            elif self.stateDirection == "run_r":
                x = self.x + 1
            elif self.stateDirection == "run_u":
                y = self.y - 1
            elif self.stateDirection == "run_d":
                y = self.y + 1

        coords = self.can_interact(x, y)

        if isinstance(coords, tuple):
            section = self.LEVEL.layout.LAYOUT[coords]

            if section.layer == self.layer:
                section.on_interact()

    def update_animation(self, direction: str = None, is_updateMove = False, ovrrScaling = False):
        """
        Sets the current animation image of the player.
        """

        self.animationClock = self.animationClock + 1

        # Check time since interacted.
        if is_updateMove == True:
            self.timeSinceInteraction = 0
        elif self.stateDirection != "neutral" and is_updateMove == False:
            self.timeSinceInteraction = self.timeSinceInteraction + 1

        if ovrrScaling:                                                                                             # Replace current image with scaled.
            self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]
        elif self.animationClock >= (self.GAME.clock.get_fps() / 3) or (direction is not None and self.stateDirection != direction):
                                                                                                                    # Update animation every half a second
                                                                                                                    # Or when direction has changed.

            if self.stateFrame == 1:
                self.stateFrame = 2
            elif self.stateFrame == 2:
                self.stateFrame = 1

            # Change Direction
            if direction is not None and self.stateDirection != direction:                                          # If direction has changed, update the animation at the same time.
                self.stateDirection = direction

            # Make player idle if not moving for 3 frame updates.
            if self.timeSinceInteraction > (3 * (self.GAME.clock.get_fps() / 2)):
                if self.stateDirection == "run_u":
                    self.stateDirection = self.stateDirectionOldX
                    #ic("Returning to neutral via the last L/R direction")
                else:
                    self.stateDirection = "neutral"
                    self.timeSinceInteraction = 0
                    #ic("Returning to neutral and resetting.")

            self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]

            self.animationClock = 0

    def update_images(self, character):
        """
        Updates animation images based on the character.
        """

        self.character = character

        for key in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]:
            self.images[key] = pygame.image.load(DIRWORKING + f"/data/image/game/player/{self.character}/base/{key}.png").convert_alpha()
        
        for effect in self.effects:
            effect.update_images(self.character)

    def update_move(self, direction:str):
        """
        Moves player in gameX and gameY.
        """

        x = self.x
        y = self.y

        self.stateDirectionOld = self.stateDirection

        if direction == "run_l":
            x = self.x - 1
            self.stateDirectionOldX = direction
        elif direction == "run_r":
            x = self.x + 1
            self.stateDirectionOldX = direction
        elif direction == "run_u":
            y = self.y - 1
        elif direction == "run_d":
            y = self.y + 1

        if direction != "neutral":
            if self.can_move(x, y) and self.moveClock >= self.GAME.clock.get_fps() / 30:
                self.adjust_center(x, y)
                self.moveClock = 0
            else:
                self.moveClock = self.moveClock + 1
        
            self.update_animation(direction, is_updateMove = True)
        else:
            self.update_animation()

    def update_scale(self):
        """
        Updates the scaling of all player textures.
        """

        self.update_images(self.character)

        self.rect = (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx)

        # Scale each image
        for key, value in self.images.items():
            self.images[key] = pygame.transform.scale(value, self.rect)
        
        for effect in self.effects:
            effect.update_scale(self.character)

        self.update_animation(ovrrScaling=True)

class Unnamed():
    """
    Spawns one of The Unnamed.
    """

    def __init__(self, game, x,  y, layer=1):
        self.GAME = game
        self.WINDOW = self.GAME.WINDOW
        self.SCALE = self.GAME.SCALE
        self.LEVEL = self.GAME.LEVEL
        self.PLAYER = self.GAME.PLAYER

        self.images = {}

        self.x = x
        self.y = y
        self.layer = layer

        self.health = 25

        for k in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]:
            self.images[k] = ""

        self.update_images()

        self.animationClock = 0
        self.stateDirection = "neutral"                                             # String of character state
        self.stateDirectionOld = "run_d"                                            # Last movement action by player.
        self.stateFrame = 1                                                         # Animation frame number
        self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]   # Default inital character state.
        self.timeSinceInteraction = 0
        self.actionClock = 0

        self.update_scale()

        self.GAME.entityList.append(self)

    def attack(self):
        """
        Makes the unnamed attack player.
        """

        self.PLAYER.health = self.PLAYER.health - 25
        self.actionClock = pygame.time.get_ticks()

    def calc_x(self):
        """
        Calculates an x value movement based on PLAYER.
        """

        if self.x > self.PLAYER.x:
            self.stateDirectionOld = self.stateDirection
            self.stateDirection = "run_l"
            return self.x - 1, self.y
        elif self.x < self.PLAYER.x:
            self.stateDirectionOld = self.stateDirection
            self.stateDirection = "run_r"
            return self.x + 1, self.y
        elif self.x == self.PLAYER.x and self.y == self.PLAYER.y:
            return self.x, self.y
        else:
            return self.calc_y()
    
    def calc_y(self):
        """
        Calculates a y value movement based on PLAYER.
        """

        if self.y > self.PLAYER.y:
            self.stateDirectionOld = self.stateDirection
            self.stateDirection = "run_u"
            return self.x, self.y - 1
        elif self.y < self.PLAYER.y:
            self.stateDirectionOld = self.stateDirection
            self.stateDirection = "run_d"
            return self.x, self.y + 1
        elif self.x == self.PLAYER.x and self.y == self.PLAYER.y:
            return self.x, self.y
        else:
            return self.calc_x()

    def can_move(self, x:int, y:int):
        """
        Checks if the player can move to a position.
        """

        try:                                                                            # Try to set the section based on coords.
            section = self.LEVEL.layout.LAYOUT[(x, y)]
        except KeyError:
            layerLast = -1

            for coords, block in self.LEVEL.layout.LAYOUT.items():                      # Deeper search, check if block in an area fill.
                if block.layer <= self.layer and block.layer >= layerLast:
                    try:
                        if y in range(coords[1], coords[3] + 1):
                            if x in range(coords[0], coords[2] + 1):
                                section = self.LEVEL.layout.LAYOUT[coords]              # Set the definition as the desired section.
                                layerLast = block.layer
                    except IndexError:                                                  # Block doesn't define a range.
                        pass

        try:
            if section.TEXTURE[section.state]["IS_COLLIDABLE"]:
                return False
            elif self.PLAYER.x == x and self.PLAYER.y == y:                             # If player at this location, attack.
                self.attack()
                return False
            else:
                for entity in self.GAME.entityList:
                    if entity.x == x and entity.y == y:                                 # If there is an entity at this location, cannot move.
                        return False
                return True
        except UnboundLocalError:                                                       # Could not find a block drawn at those coords.
            return False

    def draw(self):
        """
        Draw the unnamed on the screen.
        """

        self.update_move()
        self.update_animation()

        self.WINDOW.window.blit(self.stateImage, ((self.x - self.LEVEL.dx) * self.SCALE.unitBlockPx, (self.y - self.LEVEL.dy) * self.SCALE.unitBlockPx))

    def destroy(self):
        """
        Removes the unnamed from the entityList.
        """

        if self.GAME.levelOn and len(self.GAME.entityList) > 1:
            self.LEVEL.layout.LAYOUT[(self.x, self.y)] = Blocks.puddle_of_blood(self.GAME, self.layer)
            deathSound = mixer.Sound(DIRWORKING + "/data/sound/game/entity/death/jenna_cough.mp3")
            deathSound.set_volume(0.2)
            deathSound.play()

        temp = []

        for entity in self.GAME.entityList:
            if entity != self:
                temp.append(entity)
        
        self.GAME.entityList = temp

        del self

    def update_animation(self, ovrrScaling = False):
        """
        Sets the current animation image of the unnamed.
        """

        self.animationClock = self.animationClock + 1

        if ovrrScaling:                                                                 # Replace current image with scaled.
            self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]
        elif self.animationClock >= (self.GAME.clock.get_fps() / 2):   # Update animation every half a second.

            if self.stateFrame == 1:
                self.stateFrame = 2
            elif self.stateFrame == 2:
                self.stateFrame = 1

            self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"]

            self.animationClock = 0

    def update_images(self):
        """
        Updates animation images .
        """

        for key, value in self.images.items():
            self.images[key] = pygame.image.load(DIRWORKING + f"/data/image/game/entity/unnamed/{key}.png").convert_alpha()

    def update_move(self):
        """
        Updates the position of the unnamed.
        """

        if pygame.time.get_ticks() - self.actionClock >= 1000:

            if bool(randint(0, 1)):
                x, y = self.calc_x()
            else:
                x, y = self.calc_y()
            
            if self.can_move(x, y):
                self.x = x
                self.y = y
                self.actionClock = pygame.time.get_ticks()
            else:
                self.stateDirection = self.stateDirectionOld
        
        self.update_animation()

    def update_scale(self):
        """
        Updates the scaling of all unnamed textures.
        """

        self.update_images()

        self.rect = (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx)

        # Scale each image
        for key, value in self.images.items():
            self.images[key] = pygame.transform.scale(value, self.rect)

        self.update_animation(ovrrScaling=True)