"""
Tilemap data structures for 2D grids
"""
from .geom import Point, Rect


class CellOutOfBoundsError(Exception):
    """You have tried to access a cell at a coordinates outside of the tilemap"""
    pass


class Cell:
    """
    One cell at a point in 2D space. You'll probably want to subclass this to add
    your own data.

    .. py:attribute:: point

      :py:class:`clubsandwich.geom.Point`

    .. py:attribute:: terrain

      Arbitrary value representing the type of terrain in this cell, if any

    .. py:attribute:: feature

      Arbitrary value representing the "feature" (exit, furniture...) on this
      cell, if any

    .. py:attribute:: items

      List of arbitrary values representing any items on this cell.

    .. py:attribute:: annotations

      Set of arbitrary values. Original use was to let a level generator give a
      hint to the draw function. For example if the generator places a
      rectangular room, it can mark cells as 'corner-top-left', 'wall-vert',
      etc.

    .. py:attribute:: debug_character

      Another slot for arbitrary debugging data. Intended use is to visually mark
      cells in a map.
    """

    def __init__(self, point):
        """
        :param clubsandwich.geom.Point point:
        """
        self.point = point
        self.terrain = 0
        self.feature = None
        self.items = []
        self.annotations = set()
        self.debug_character = None


class TileMap:
    """
    A collection of cells within a rectangular 2D area, with various convenience
    methods. You'll probably want to subclass this to add your own.

    .. py:attribute:: size

      :py:class:`clubsandwich.geom.Size`

    .. py:attribute:: points_of_interest

      Dict of arbitrary data. Intended use is for the level generator to pass
      data to game logic, for example to define mob positions.
    """

    def __init__(self, size, cell_class=Cell):
        """
        :param Size size:
        :param class cell_class: Class to use for cells. Instantiated with a single
                                 argument, the point: ``cell_class(Point(x, y))``
        """
        self.size = size
        self.points_of_interest = {}
        self._cells = [
            [cell_class(Point(x, y)) for y in range(size.height)] for x in range(size.width)]

    def contains_point(self, point):
        """
        :param clubsandwich.geom.Point point:

        Returns ``True`` iff *point* is within this tilemap's bounds, otherwise
        ``False``.
        """
        return point.x >= 0 and point.y >= 0 and point.x < self.size.width and point.y < self.size.height

    def cell(self, point):
        """
        :param clubsandwich.geom.Point point:

        Returns the cell at the given point, or raises
        :py:class:`CellOutOfBoundsError`.

        As a shortcut, you can use indexing syntax instead of calling this method::

          cell = tilemap[Point(x, y)]
        """
        if not self.contains_point(point):
            raise CellOutOfBoundsError(
                "Cell index out of range: {!r}".format(point))
        try:
            return self._cells[point.x][point.y]
        except IndexError:
            raise CellOutOfBoundsError(
                "Cell index out of range: {!r}".format(point))

    @property
    def cells(self):
        """Iterator of all cells in this tilemap"""
        for point in Rect(Point(0, 0), self.size).points:
            yield self.cell(point)

    def __getitem__(self, k):
        if isinstance(k, Point):
            return self.cell(k)
        return self.cells[k]
