# 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 List, Union, get_type_hints

from PIL import Image

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


class UnsupportedTileException(Exception):
    pass


class BattleMapType:
    pass


@dataclass
class BattleMap(BattleMapType):
    """
    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)
        >>> print(bmap.as_string())

    """

    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.config.legend:
                raise UnsupportedTileException(f"The current tileset does not support the '{char}' character.")
        return True

    def as_image(self) -> Image:
        """
        Create an image of the current 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 an Image instance.
        """
        if get_type_hints(self.tileset.render_grid)["return"] != Image:
            raise NotImplementedError(f"Tile set does not support image rendering.")
        return self.tileset.render_grid(grid=self.grid, width=self.width, height=self.height)

    def as_string(self) -> str:
        if get_type_hints(self.tileset.render_grid)["return"] != str:
            raise NotImplementedError(f"{self.tileset} does not support string rendering.")
        return self.tileset.render_grid(grid=self.grid, width=self.width, height=self.height)

    @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.config.legend:
                output += f"{char} - {self.tileset.config.legend[char]}\n"
        return output

    def __str__(self) -> str:
        return self.as_string()

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