# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later

from functools import reduce
from queue import PriorityQueue
from random import randint
from typing import Dict, Tuple

from ..interfaces import FightingEntity, InventoryHolder


class Player(InventoryHolder, FightingEntity):
    """
    The class of the player
    """
    current_xp: int = 0
    max_xp: int = 10
    paths: Dict[Tuple[int, int], Tuple[int, int]]

    def __init__(self, name: str = "player", maxhealth: int = 20,
                 strength: int = 5, intelligence: int = 1, charisma: int = 1,
                 dexterity: int = 1, constitution: int = 1, level: int = 1,
                 current_xp: int = 0, max_xp: int = 10, inventory: list = None,
                 hazel: int = 42, *args, **kwargs) \
            -> None:
        super().__init__(name=name, maxhealth=maxhealth, strength=strength,
                         intelligence=intelligence, charisma=charisma,
                         dexterity=dexterity, constitution=constitution,
                         level=level, *args, **kwargs)
        self.current_xp = current_xp
        self.max_xp = max_xp
        self.inventory = self.translate_inventory(inventory or [])
        self.paths = dict()
        self.hazel = hazel

    def move(self, y: int, x: int) -> None:
        """
        Moves the view of the map (the point on which the camera is centered)
        according to the moves of the player.
        """
        super().move(y, x)
        self.map.currenty = y
        self.map.currentx = x
        self.recalculate_paths()

    def level_up(self) -> None:
        """
        Add levels to the player as much as it is possible.
        """
        while self.current_xp > self.max_xp:
            self.level += 1
            self.current_xp -= self.max_xp
            self.max_xp = self.level * 10
            self.health = self.maxhealth
            self.strength = self.strength + 1
            # TODO Remove it, that's only fun
            self.map.spawn_random_entities(randint(3 * self.level,
                                                   10 * self.level))

    def add_xp(self, xp: int) -> None:
        """
        Add some experience to the player.
        If the required amount is reached, level up.
        """
        self.current_xp += xp
        self.level_up()

    # noinspection PyTypeChecker,PyUnresolvedReferences
    def check_move(self, y: int, x: int, move_if_possible: bool = False) \
            -> bool:
        """
        If the player tries to move but a fighting entity is there,
        the player fights this entity.
        If the entity dies, the player is rewarded with some XP
        """
        # Don't move if we are dead
        if self.dead:
            return False
        for entity in self.map.entities:
            if entity.y == y and entity.x == x:
                if entity.is_fighting_entity():
                    self.map.logs.add_message(self.hit(entity))
                    if entity.dead:
                        self.add_xp(randint(3, 7))
                    return True
                elif entity.is_item():
                    entity.hold(self)
        return super().check_move(y, x, move_if_possible)

    def recalculate_paths(self, max_distance: int = 8) -> None:
        """
        Use Dijkstra algorithm to calculate best paths for monsters to go to
        the player. Actually, the paths are computed for each tile adjacent to
        the player then for each step the monsters use the best path avaliable.
        """
        distances = []
        predecessors = []
        # four Dijkstras, one for each adjacent tile
        for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
            queue = PriorityQueue()
            new_y, new_x = self.y + dir_y, self.x + dir_x
            if not 0 <= new_y < self.map.height or \
                    not 0 <= new_x < self.map.width or \
                    not self.map.tiles[new_y][new_x].can_walk():
                continue
            queue.put(((1, 0), (new_y, new_x)))
            visited = [(self.y, self.x)]
            distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
            predecessors.append({(new_y, new_x): (self.y, self.x)})
            while not queue.empty():
                dist, (y, x) = queue.get()
                if dist[0] >= max_distance or (y, x) in visited:
                    continue
                visited.append((y, x))
                for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                    new_y, new_x = y + diff_y, x + diff_x
                    if not 0 <= new_y < self.map.height or \
                            not 0 <= new_x < self.map.width or \
                            not self.map.tiles[new_y][new_x].can_walk():
                        continue
                    new_distance = (dist[0] + 1,
                                    dist[1] + (not self.map.is_free(y, x)))
                    if not (new_y, new_x) in distances[-1] or \
                            distances[-1][(new_y, new_x)] > new_distance:
                        predecessors[-1][(new_y, new_x)] = (y, x)
                        distances[-1][(new_y, new_x)] = new_distance
                        queue.put((new_distance, (new_y, new_x)))
        # For each tile that is reached by at least one Dijkstra, sort the
        # different paths by distance to the player. For the technical bits :
        # The reduce function is a fold starting on the first element of the
        # iterable, and we associate the points to their distance, sort
        # along the distance, then only keep the points.
        self.paths = {}
        for y, x in reduce(set.union,
                           [set(p.keys()) for p in predecessors], set()):
            self.paths[(y, x)] = [p for d, p in sorted(
                [(distances[i][(y, x)], predecessors[i][(y, x)])
                 for i in range(len(distances)) if (y, x) in predecessors[i]])]

    def save_state(self) -> dict:
        """
        Saves the state of the entity into a dictionary
        """
        d = super().save_state()
        d["current_xp"] = self.current_xp
        d["max_xp"] = self.max_xp
        return d