import math
from logging import getLogger

from simulation.level_settings import DEFAULT_LEVEL_SETTINGS
from simulation.location import Location
from simulation.game_logic import SpawnLocationFinder, ScoreLocationUpdater, MapContext, PickupUpdater, MapExpander
from simulation.cell import Cell

LOGGER = getLogger(__name__)


class WorldMap(object):
    """
    The non-player world state.
    """

    def __init__(self, grid, settings):
        """
        :param grid: All types of cells to be inserted into the map.
        :param settings: Constant values provided when generating a level/map.
        """
        self.grid = grid
        self.settings = settings
        self._spawn_location_finder = SpawnLocationFinder(self)

    @classmethod
    def _min_max_from_dimensions(cls, height, width):
        """
        The value provided by the user will be an integer both for the width and height
        components. We calculate the maximum and minimum dimensions in all directions.
        """
        max_x = int(math.floor(width / 2))
        min_x = -(width - max_x - 1)
        max_y = int(math.floor(height / 2))
        min_y = -(height - max_y - 1)
        return min_x, max_x, min_y, max_y

    @classmethod
    def generate_empty_map(cls, height, width, settings):
        new_settings = DEFAULT_LEVEL_SETTINGS.copy()
        new_settings.update(settings)

        (min_x, max_x, min_y, max_y) = WorldMap._min_max_from_dimensions(height, width)
        grid = {}
        for x in range(min_x, max_x + 1):
            for y in range(min_y, max_y + 1):
                location = Location(x, y)
                grid[location] = Cell(location)
        return cls(grid, new_settings)

    def all_cells(self):
        return self.grid.values()

    def score_cells(self):
        return (c for c in self.all_cells() if c.generates_score)

    def pickup_cells(self):
        return (c for c in self.all_cells() if c.pickup)

    def is_on_map(self, location):
        try:
            self.grid[location]
        except KeyError:
            return False
        return True

    def get_cell(self, location):
        try:
            return self.grid[location]
        except KeyError:
            # For backwards-compatibility, this throws ValueError
            raise ValueError('Location %s is not on the map' % location)

    def get_cell_by_coords(self, x, y):
        return self.get_cell(Location(x, y))

    def clear_cell_actions(self, location):
        try:
            cell = self.get_cell(location)
            cell.actions = []
        except ValueError:
            return

    def max_y(self):
        return max(self.grid.keys(), key=lambda c: c.y).y

    def min_y(self):
        return min(self.grid.keys(), key=lambda c: c.y).y

    def max_x(self):
        return max(self.grid.keys(), key=lambda c: c.x).x

    def min_x(self):
        return min(self.grid.keys(), key=lambda c: c.x).x

    @property
    def num_rows(self):
        return self.max_y() - self.min_y() + 1

    @property
    def num_cols(self):
        return self.max_x() - self.min_x() + 1

    @property
    def num_cells(self):
        return self.num_rows * self.num_cols

    def update(self, num_avatars):
        self._update_avatars()
        self._update_map(num_avatars)

    def _update_avatars(self):
        self._apply_score()
        self._apply_pickups()

    def _apply_pickups(self):
        for cell in self.pickup_cells():
            if cell.avatar is not None:
                cell.pickup.apply(cell.avatar)

    def _apply_score(self):
        for cell in self.score_cells():
            try:
                cell.avatar.score += 1
            except AttributeError:
                pass

    def _update_map(self, num_avatars):
        context = MapContext(num_avatars=num_avatars)
        MapExpander().update(self, context=context)
        ScoreLocationUpdater().update(self, context=context)
        PickupUpdater().update(self, context=context)

    def can_move_to(self, target_location):
        if not self.is_on_map(target_location):
            return False
        cell = self.get_cell(target_location)

        return (cell.habitable
                and (not cell.is_occupied or cell.avatar.is_moving)
                and len(cell.moves) <= 1)

    def attackable_avatar(self, target_location):
        """
        Return a boolean if the avatar is attackable at the given location (or will be
        after next move), else return None.
        """
        try:
            cell = self.get_cell(target_location)
        except ValueError:
            return None

        if cell.avatar:
            return cell.avatar

        if len(cell.moves) == 1:
            return cell.moves[0].avatar

        return None

    def get_no_fog_distance(self):
        return self.settings['NO_FOG_OF_WAR_DISTANCE']

    def get_partial_fog_distance(self):
        return self.settings['PARTIAL_FOG_OF_WAR_DISTANCE']

    def get_random_spawn_location(self):
        return self._spawn_location_finder.get_random_spawn_location()

    def __repr__(self):
        return repr(self.grid)

    def __iter__(self):
        return ((self.get_cell(Location(x, y))
                for y in range(self.min_y(), self.max_y() + 1))
                for x in range(self.min_x(), self.max_x() + 1))

    # Serialisation Utilities
    def get_serialised_south_west_corner(self):
        """
        Used in serialising the map size when sent to the front end. Very lightweight as
        it consists of two integers.

        :return: A dictionary with two values, x and y coordinates for the bottom left
        (south-west) corner of the map.
        """
        return {
            "x": self.min_x(),
            "y": self.min_y(),
        }

    def get_serialised_north_east_corner(self):
        """
        Used in serialising the map size when sent to the front end. Very lightweight as
        it consists of two integers.

        :return: A dictionary with two values, x and y coordinates for the top right
        (north-west) corner of the map.
        """
        return {
            "x": self.max_x(),
            "y": self.max_y(),
        }

    def serialise_score_location(self):
        """
        Used to serialise the score locations on every update.

        :return: A single list that contains all score locations. Within
        the list there are x and y coordinates.
        """

        def get_coords(cell):
            return {'location': {'x': cell.location.x, 'y': cell.location.y}}

        return [get_coords(cell) for cell in self.all_cells() if cell.generates_score]

    def serialise_obstacles(self):
        """
        Used to serialise the obstacle locations on every update.

        :return: A list that contains all the obstacle information generated by inner method.
        """

        def serialise_obstacle(cell):
            return {'location': {
                         'x': cell.location.x,
                         'y': cell.location.y,
                     },
                    'width': 1,
                    'height': 1,
                    'type': "wall",
                    'orientation': "north",
                    }

        return [serialise_obstacle(cell) for cell in self.all_cells() if not cell.habitable]


def WorldMapStaticSpawnDecorator(world_map, spawn_location):
    world_map._spawn_location_finder.get_random_spawn_location = lambda: spawn_location
    return world_map
