import colorsys
import logging
from dataclasses import dataclass
from typing import Self, cast

import numpy as np
from PIL import Image as PILImage
from PIL import ImageDraw
from PIL.Image import Image, Resampling
from PIL.ImageFont import FreeTypeFont

from .config import Config

type _RGB = tuple[int, int, int]
type _XY = tuple[float, float]
type _Box = tuple[int, int, int, int]

_logger = logging.getLogger(__name__)


@dataclass(slots=True, frozen=True)
class AdventImage:
    img: Image

    @classmethod
    async def from_img(cls, img: Image, cfg: Config) -> Self:
        """
        Einen quadratischen Ausschnitt aus der Mitte des Bilds nehmen
        """

        # Farbmodell festlegen
        img = img.convert(mode="RGB")

        # Größen bestimmen
        width, height = img.size
        square = min(width, height)

        # zuschneiden
        img = img.crop(
            box=(
                int((width - square) / 2),
                int((height - square) / 2),
                int((width + square) / 2),
                int((height + square) / 2),
            )
        )

        # skalieren
        return cls(
            img.resize(
                size=(cfg.image.size, cfg.image.size),
                resample=Resampling.LANCZOS,
            )
        )

    async def get_text_box(
        self,
        xy: _XY,
        text: str,
        font: FreeTypeFont,
        anchor: str | None = "mm",
        **text_kwargs,
    ) -> _Box | None:
        """
        Koordinaten (links, oben, rechts, unten) des betroffenen
        Rechtecks bestimmen, wenn das Bild mit einem Text
        versehen wird
        """

        # Neues 1-Bit Bild, gleiche Größe
        mask = PILImage.new(mode="1", size=self.img.size)

        # Text auf Maske auftragen
        ImageDraw.Draw(mask).text(
            xy=xy,
            text=text,
            font=font,
            anchor=anchor,
            fill=1,
            **text_kwargs,
        )

        # betroffenen Pixelbereich bestimmen
        return mask.getbbox()

    async def get_average_color(
        self,
        box: _Box,
    ) -> _RGB:
        """
        Durchschnittsfarbe eines rechteckigen Ausschnitts in
        einem Bild berechnen
        """

        pixel_data = np.asarray(self.img.crop(box))
        mean_color: np.ndarray = np.mean(pixel_data, axis=(0, 1))

        return cast(_RGB, mean_color.astype(int))

    async def hide_text(
        self,
        xy: _XY,
        text: str,
        font: FreeTypeFont,
        anchor: str | None = "mm",
        **text_kwargs,
    ) -> None:
        """
        Text `text` in Bild an Position `xy` verstecken.
        Weitere Parameter wie bei `ImageDraw.text()`.
        """

        # betroffenen Bildbereich bestimmen
        text_box = await self.get_text_box(
            xy=xy, text=text, font=font, anchor=anchor, **text_kwargs
        )

        if text_box is None:
            _logger.warning("Konnte Bildbereich nicht finden!")
            return

        # Durchschnittsfarbe bestimmen
        text_color = await self.get_average_color(
            box=text_box,
        )

        # etwas heller/dunkler machen
        tc_h, tc_s, tc_v = colorsys.rgb_to_hsv(*text_color)
        tc_v = int((tc_v - 127) * 0.97) + 127

        if tc_v < 127:
            tc_v += 3

        else:
            tc_v -= 3

        text_color: tuple[int | float, int | float, int | float] = colorsys.hsv_to_rgb(
            tc_h, tc_s, tc_v
        )
        text_color = cast(_RGB, tuple(int(val) for val in text_color))

        # Buchstaben verstecken
        ImageDraw.Draw(self.img).text(
            xy=xy,
            text=text,
            font=font,
            fill=text_color,
            anchor=anchor,
            **text_kwargs,
        )
