import random
import time
import colors
from compositecore import Leaf, CompositeMessage
from console import console
import direction
import geometry
import icon
import libtcodpy as libtcod
import turn


class DungeonMask(Leaf):
    """
    Holds the visibility mask and solidity mask of the entity
    """

    def __init__(self):
        super(DungeonMask, self).__init__()
        self._dungeon_map = None
        self.dungeon_map_needs_total_update = True
        self.last_sight_radius = -1
        self.component_type = "dungeon_mask"
        self._dirty_point_list = []

    @property
    def dungeon_map(self):
        if self._dungeon_map is None:
            self.init_dungeon_map_if_has_dungeon_level()
        return self._dungeon_map

    @dungeon_map.setter
    def dungeon_map(self, value):
        self._dungeon_map = value

    def __getstate__(self):
        state = dict(self.__dict__)
        del state["_dungeon_map"]
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self.dungeon_map_needs_total_update = True
        self.dungeon_map = None

    def init_dungeon_map_if_has_dungeon_level(self):
        """
        Initiates the dungeon map of a dungeon_level, if available.
        """
        if self.has_sibling("dungeon_level") and not self.parent.dungeon_level.value is None:
            self._init_dungeon_map()
            self.dungeon_map_needs_total_update = True

    def _init_dungeon_map(self):
        """
        Initiates the dungeon map of a dungeon_level.
        """
        self.dungeon_map = libtcod.map_new(self.parent.dungeon_level.value.width,
                                           self.parent.dungeon_level.value.height)

    def signal_dirty_point(self, point):
        self._dirty_point_list.append(point)

    def can_see_point(self, point):
        """
        Checks if a particular point is visible to this entity.

        Args:
            point (int, int): The point to check.
        """
        x, y = point
        return libtcod.map_is_in_fov(self.dungeon_map, x, y)

    def on_tick(self, _):
        sight_radius = self.parent.sight_radius.value
        if not self.last_sight_radius == sight_radius:
            self.update_fov()

    def update_fov(self):
        """
        Calculates the Field of Vision from the dungeon_map.
        """
        x, y = self.parent.position.value
        sight_radius = self.parent.sight_radius.value
        libtcod.map_compute_fov(self.dungeon_map, x, y,
                                sight_radius, True)
        self.last_sight_radius = sight_radius

    def print_walkable_map(self):
        """
        Prints a map of where this entity is allowed to walk.
        """
        for y in range(libtcod.map_get_height(self.dungeon_map)):
            line = ""
            for x in range(libtcod.map_get_width(self.dungeon_map)):
                if libtcod.map_is_walkable(self.dungeon_map, x, y):
                    line += " "
                else:
                    line += "#"
            print(line)

    def print_is_transparent_map(self):
        """
        Prints a map of what this entity can see through.
        """
        for y in range(libtcod.map_get_height(self.dungeon_map)):
            line = ""
            for x in range(libtcod.map_get_width(self.dungeon_map)):
                if libtcod.map_is_transparent(self.dungeon_map, x, y):
                    line += " "
                else:
                    line += "#"
            print(line)

    def print_visible_map(self):
        """
        Prints a map of what this entity sees right now.
        """
        for y in range(libtcod.map_get_height(self.dungeon_map)):
            line = ""
            for x in range(libtcod.map_get_width(self.dungeon_map)):
                if libtcod.map_is_in_fov(self.dungeon_map, x, y):
                    line += " "
                else:
                    line += "#"
            print(line)

    def before_tick(self, time):
        self.update_dungeon_map_if_its_old()

    def update_dungeon_map_if_its_old(self):
        """
        Updates the dungeon map it is older than the latest change.
        """
        if self.dungeon_map_needs_total_update:
            self.update_dungeon_map()
            self.update_fov()
        elif len(self._dirty_point_list) > 0:
            for point in self._dirty_point_list:
                x, y = point
                self.update_dungeon_map_point(x, y)
            self._dirty_point_list = []
            self.update_fov()

    def update_dungeon_map_point(self, x, y):
        dungeon_level = self.parent.dungeon_level.value
        terrain = dungeon_level.get_tile_or_unknown((x, y)).get_terrain()
        dungeon_feature = dungeon_level.get_tile_or_unknown((x, y)).get_dungeon_feature()
        is_opaque = terrain.has("is_opaque") or (dungeon_feature and dungeon_feature.has("is_opaque"))
        libtcod.map_set_properties(self.dungeon_map, x, y, 0 if is_opaque else 1,
                                   self.parent.mover.can_pass_terrain(terrain))

    def update_dungeon_map(self):
        """
        Updates the dungeon map.
        """
        dungeon_level = self.parent.dungeon_level.value
        for y in range(dungeon_level.height):
            for x in range(dungeon_level.width):
                self.update_dungeon_map_point(x, y)
        self.dungeon_map_needs_total_update = False
        self._dirty_point_list = []

    def send_message(self, message):
        """
        Handles received messages.
        """
        if message == CompositeMessage.DUNGEON_LEVEL_CHANGED:
            self.init_dungeon_map_if_has_dungeon_level()
            self.update_dungeon_map()
            self.update_fov()
        if message == CompositeMessage.POSITION_CHANGED:
            self.update_fov()


class Path(Leaf):
    """
    Composites holding this has a path that it may step through.
    """

    def __init__(self):
        super(Path, self).__init__()
        self._path = None
        self.position_list = []
        self.component_type = "path"

    def __getstate__(self):
        state = dict(self.__dict__)
        del state["_path"]
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self._path = None

    @property
    def path(self):
        if self._path is None:
            self.init_path()
        return self._path

    def init_path(self):
        """
        Initiates the path using the dungeon map, from the DungeonMask module.
        """
        dungeon_map = self.parent.dungeon_mask.dungeon_map
        self._path = libtcod.path_new_using_map(dungeon_map, 1.0)

    def has_path(self):
        """
        Returns True if the entity has a path to walk.
        """
        if len(self.position_list) > 0:
            return True
        return False

    def try_step_path(self):
        """
        Tries to step the entity along the path, relies on the mover module.
        """
        if not self.has_path():
            return 0
        next_point = self.position_list.pop()
        if not geometry.chess_distance(next_point, self.parent.position.value) == 1:
            self.set_line_path(next_point)
            next_point = self.position_list.pop()
        energy_spent = self.parent.stepper.try_move_or_bump(next_point)
        if energy_spent <= 0:
            energy_spent = self.try_step_left_or_right(next_point)
            if energy_spent <= 0:
                self.clear()
        return energy_spent

    def try_step_left_or_right(self, next_point):
        step_direction = geometry.sub_2d(next_point, self.parent.position.value)
        if step_direction not in direction.DIRECTIONS:
            return False
        alternate_directions = [direction.turn_slight_left(step_direction),
                                direction.turn_slight_right(step_direction)]
        random.shuffle(alternate_directions)
        for d in alternate_directions:
            new_position = geometry.add_2d(d, self.parent.position.value)
            terrain = self.parent.dungeon_level.value.get_tile_or_unknown(new_position).get_terrain()
            if self.parent.mover.can_pass_terrain(terrain):
                energy_spent = self.parent.stepper.try_move_or_bump(new_position)
                if energy_spent > 0:
                    return energy_spent
        return 0

    def compute_path(self, destination):
        sx, sy = self.parent.position.value
        dx, dy = destination
        libtcod.path_compute(self.path, sx, sy, dx, dy)
        self.clear()
        x, y = libtcod.path_walk(self.path, True)
        while not x is None:
            self.position_list.insert(0, (x, y))
            x, y = libtcod.path_walk(self.path, True)

    def set_line_path(self, destination):
        sx, sy = self.parent.position.value
        dx, dy = destination
        libtcod.line_init(sx, sy, dx, dy)
        self.clear()
        x, y = libtcod.line_step()
        while not x is None:
            self.position_list.insert(0, (x, y))
            x, y = libtcod.line_step()

    def clear(self):
        self.position_list = []

    def draw(self, camera):
        points = list(self.position_list)
        points.reverse()
        point = None
        for point in points:
            if not self.parent.mover.can_pass_terrain(
                    self.parent.dungeon_level.value.get_tile_or_unknown(point).get_terrain()):
                break
            console.set_symbol(camera.dungeon_to_screen_position(point), icon.FOOT_STEPS)
            console.set_color_fg(camera.dungeon_to_screen_position(point), colors.GRAY)
        if point:
            console.set_symbol(camera.dungeon_to_screen_position(point), "X")
            console.set_color_fg(camera.dungeon_to_screen_position(point), colors.LIGHT_ORANGE)

    def send_message(self, message):
        if message == CompositeMessage.DUNGEON_LEVEL_CHANGED:
            self.init_path()