# 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
        if hasattr(self, "title"):
            del self.title
        if hasattr(self, "legend"):
            del self.legend
        if hasattr(self, "coordinates"):
            del self.coordinates
        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("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)

    @cached_property
    def title(self) -> str:
        return f"BattleMap: {self.name} ({self.width} x {self.height})"

    @cached_property
    def legend(self) -> str:
        output = ""
        locations = False
        for char in sorted(set(list(self.source_data)), key=str.lower):
            if char in self.tileset.config.legend:
                if char in "0123456789":
                    locations = True
                    continue
                output += f"{char} - {self.tileset.config.legend[char]}\n"
        if locations:
            output += f"0-9 - locations"
            justified = ""
            for line in output.splitlines():
                (key, sep, desc) = line.partition(" - ")
                justified += f"{key.rjust(3, ' ')}{sep}{desc}\n"
            output = "".join(justified)
        return output

    @cached_property
    def top_coordinates(self) -> str:
        row = next(row for row in self.grid.data if len(row) == self.width)
        max_len = max([len(cell.x_coordinate) for cell in row])
        lines = list(("" for i in range(max_len)))
        for pos in row:
            for i in range(len(pos.x_coordinate)):
                lines[max_len - i - 1] += pos.x_coordinate[i]
        return "\n".join((line.rjust(len(row), " ") for line in lines))

    @cached_property
    def left_coordinates(self) -> List[str]:
        return [row[0].y_coordinate if row else "" for row in self.grid.data]

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

    def __repr__(self) -> str:
        lines = ""
        left_coords = self.left_coordinates
        i = 0
        for line in str(self).splitlines():
            lines += f"{left_coords[i].rjust(2, ' ')} │ {line}".ljust(self.width, " ") + " │\n"
            i = i + 1
        top_break = "┌" + ("─" * (self.width + 2)) + "┐"
        bot_break = "└" + ("─" * (self.width + 2)) + "┘"
        return "\n".join(
            [
                "",
                self.title,
                "",
                indent(self.top_coordinates, " " * 5),
                indent(top_break, " " * 3),
                lines.rstrip(),
                indent(bot_break, " " * 3),
                "",
                "Legend:",
                indent(self.legend, "  "),
            ]
        )
