from __future__ import annotations

# Std lib
from typing import Unpack, TypedDict, Literal

# 3rd party lib
from matplotlib.font_manager import FontProperties
from matplotlib.textpath import TextPath
from shapely import Polygon, MultiPolygon, unary_union
from shapely.affinity import translate

# Local lib
from .shape import Shape


class TextKwargs(TypedDict, total=False):
    x: float
    y: float
    font_height: float
    font: Literal[
        # Windows Fonts
        "Arial", "Arial Black", "Calibri", "Cambria", "Candara", "Comic Sans MS",
        "Consolas", "Courier New", "Georgia", "Impact", "Lucida Console",
        "Lucida Sans Unicode", "Microsoft Sans Serif", "Segoe UI", "Tahoma",
        "Times New Roman", "Trebuchet MS", "Verdana",

        # macOS Fonts
        "American Typewriter", "Arial", "Avenir", "Big Caslon", "Brush Script",
        "Chalkboard", "Courier", "Geneva", "Gill Sans", "Helvetica",
        "Helvetica Neue", "Hoefler Text", "Menlo", "Monaco", "Optima",
        "Palatino", "Snell Roundhand", "Times", "Zapfino",

        # Linux Fonts (Commonly Available)
        "DejaVu Sans", "DejaVu Serif", "Liberation Sans", "Liberation Serif",
        "Liberation Mono", "Noto Sans", "Noto Serif", "Noto Mono", "Ubuntu",
        "Ubuntu Mono"
    ]
    boldness: int
    millable: bool
    millable_override: bool


class Text(Shape):

    # region Class Body

    # Class variables
    _default_geometry = Shape._default_geometry.copy()
    _default_geometry.update({"font_height": 1000, "font": "arial", "boldness": 500})

    # Instance variables
    _font_height: float
    _font: str
    _boldness: int
    _text: str

    __slots__ = ["_font_height", "_text", "_font", "_boldness"]

    # endregion Class Body

    # region Dev Methods

    def __init__(self, text: str, **kwargs: Unpack[TextKwargs]) -> None:
        super().__init__(**kwargs)
        self._text = text

    def _get_polygon(self) -> MultiPolygon:
        """
        Converts text to a Shapely MultiPolygon while preserving holes,
        with support for bold fonts.
        """

        # Set up font properties (change family and style as needed)
        try:
            font_prop = FontProperties(family=self._font, weight=self._boldness)  # Example: Arial Bold
        except ValueError as e:
            if f"weight={self._boldness} is invalid" in str(e):
                raise ValueError(f"The boldness of a font can only be between 0 and 1000, got {self._boldness}.")
            else:
                raise e

        # Create the text path
        abspos = self._get_absolute_position()
        path = TextPath((float(abspos[0]), float(abspos[1])), self._text, size=self._font_height * (1 / 0.725),
                        prop=font_prop)

        # Process the polygons (same as before)
        contours = [Polygon(vertices) for vertices in path.to_polygons() if len(vertices) >= 3]

        if not contours:
            return MultiPolygon()

        contours.sort(key=lambda p: p.area, reverse=True)
        polygons = []
        used_as_hole = set()

        for i, outer in enumerate(contours):
            if i in used_as_hole:
                continue
            holes = []
            for j, hole in enumerate(contours):
                if j != i and j not in used_as_hole and outer.contains(hole):
                    holes.append(hole.exterior)
                    used_as_hole.add(j)
            polygons.append(Polygon(outer.exterior, holes))

        final_geometry = MultiPolygon(polygons)
        minx, miny, maxx, maxy = final_geometry.bounds
        centered_geometry = translate(final_geometry, xoff=-(maxx - minx) / 2, yoff=-(maxy - miny) / 2)

        return centered_geometry

    # endregion Dev Methods

    # region User Methods

    def copy(self, **kwargs) -> Text:
        return super().copy(**kwargs)

    # endregion User Methods

    # region Properties

    @property
    def x_span(self) -> float:
        return self.x_max - self.x_min

    @property
    def y_span(self) -> float:
        return self.y_max - self.y_min

    @property
    def text(self) -> str:
        return self._text

    @text.setter
    def text(self, text: str):
        self._text = str(text)

    @property
    def font_size(self) -> int:
        return self._font_height

    @font_size.setter
    def font_size(self, value: int):
        self._font_height = int(value)

    # endregion Properties
