# import logging
from dataclasses import dataclass
from functools import cached_property
from io import StringIO
from pathlib import Path
from textwrap import indent
from typing import Union, List

from PIL import Image

from tilemapper.grid import Grid, Position
from tilemapper.tileset import TileSet, Tile


class UnsupportedTileException(Exception):
    pass


@dataclass
class BattleMap:
    """
    A 2D battle map built on a Grid of Tiles.

    Example Usage:

        >>> console = TileSetManager().load("colorized")
        >>> bmap = BattleMap(name="Example Map", source="input.txt", tileset=console)
        >>> bmap.render()

    """
    name: str = ""
    source: Union[StringIO, Path] = None
    source_data: str = ""
    tileset: TileSet = None
    width: int = 0
    height: int = 0

    def load(self) -> Grid:
        """
        Load a battle map text file and verify that the current tile set supports
        all the terrain types used in the map. Returns a Grid instance consisting
        of Tiles from the current set representing the input map.
        """
        try:
            data = self.source.read_bytes().decode()
        except AttributeError:
            data = self.source.read()
        data = data.strip("\n")
        if not self.validate_source_data(data):
            return
        self.source_data = data

        # invalidate the cache
        if hasattr(self, "grid"):
            del self.grid
        return self.grid

    def validate_source_data(self, data: str) -> bool:
        """
        Return True if every terrain type in the input data has a corresponding tile in the current tile set.
        """
        for char in set(list(data)):
            if char == "\n":
                continue
            if char not in self.tileset.character_map:
                raise UnsupportedTileException(f"The current tileset does not support the '{char}' character.")
        return True

    def render(self) -> Image:
        """
        Create a PNG image of the currenet grid.

        If a grid position does not exist (as for example if one row is shorter than anoather), the
        missing position will be rendered as empty space (using the tile set's empty space tile).

        Returns the generated Image instance.
        """
        map_image = Image.new("RGB", (self.tileset.tile_size * self.width, self.tileset.tile_size * self.height))
        empty_space = Position(y=-1, x=-1, value=self.tileset.empty_space)
        for y in range(0, self.height):
            for x in range(0, self.width):
                pos = self.grid.at(y, x)
                if not pos or pos.value == self.tileset.empty_space:
                    continue
                map_image.paste(
                    self.tileset.render_tile(pos, [a or pos for a in self.grid.adjacent(pos)]),
                    (self.tileset.tile_size * x, self.tileset.tile_size * y),
                )
        return map_image

    @cached_property
    def grid(self) -> List[List[Tile]]:
        """
        Return the current map's Grid instance.
        """
        matrix = []
        for line in self.source_data.splitlines():
            matrix.append([self.tileset.get(char) for char in line])
        self.width = max([len(line) for line in matrix])
        self.height = len(matrix)
        return Grid(data=matrix)

    @property
    def title(self) -> str:
        return f"BattleMap: {self.name} ({self.width} x {self.height}, {self.width * self.height * 5}sq.ft.)"

    @property
    def legend(self) -> str:
        output = ""
        for char in sorted(set(list(self.source_data)), key=str.lower):
            if char in self.tileset.character_map:
                output += f"{char} - {self.tileset.character_map[char]}\n"
        return output

    def __str__(self) -> str:
        output = ""
        for y in range(0, self.height):
            for x in range(0, self.width):
                try:
                    output += str(self.grid.at(y, x).value)
                except AttributeError:
                    continue
            output += "\n"
        return output.rstrip("\n")

    def __repr__(self) -> str:
        return f"\n{self.title}\n\n{indent(str(self), '    ')}\n\nLegend:\n{indent(self.legend, '  ')}"
