"""Vector shapes and path operations for compositing."""

import logging
from typing import TYPE_CHECKING, Generator

import numpy as np
from PIL import Image

from psd_tools.composite._compat import require_aggdraw

if TYPE_CHECKING:
    from psd_tools.api.layers import Layer

logger = logging.getLogger(__name__)


@require_aggdraw
def draw_vector_mask(layer: "Layer") -> np.ndarray:
    """
    Draw a vector mask.

    Requires aggdraw for bezier curve rasterization.
    """
    return _draw_path(layer, brush={"color": 255})


@require_aggdraw
def draw_stroke(layer: "Layer") -> np.ndarray:
    """
    Draw a stroke.

    Requires aggdraw for bezier curve rasterization.
    """
    if layer.stroke is None:
        raise ValueError("Layer stroke is required to draw a stroke.")
    desc = layer.stroke._data
    # _CAP = {
    #     'strokeStyleButtCap': 0,
    #     'strokeStyleSquareCap': 1,
    #     'strokeStyleRoundCap': 2,
    # }
    # _JOIN = {
    #     'strokeStyleMiterJoin': 0,
    #     'strokeStyleRoundJoin': 2,
    #     'strokeStyleBevelJoin': 3,
    # }
    width = float(desc.get("strokeStyleLineWidth", 1.0))
    # linejoin = desc.get('strokeStyleLineJoinType', None)
    # linejoin = linejoin.enum if linejoin else 'strokeStyleMiterJoin'
    # linecap = desc.get('strokeStyleLineCapType', None)
    # linecap = linecap.enum if linecap else 'strokeStyleButtCap'
    # miterlimit = desc.get('strokeStyleMiterLimit', 100.0) / 100.
    # aggdraw >= 1.3.12 will support additional params.
    return _draw_path(
        layer,
        pen={
            "color": 255,
            "width": width,
            # 'linejoin': _JOIN.get(linejoin, 0),
            # 'linecap': _CAP.get(linecap, 0),
            # 'miterlimit': miterlimit,
        },
    )


def _draw_path(
    layer: "Layer",
    brush: dict[str, int | float] | None = None,
    pen: dict[str, int | float] | None = None,
) -> np.ndarray:
    if layer.vector_mask is None:
        raise ValueError("Layer does not have a vector mask.")
    height, width = layer._psd.height, layer._psd.width
    color = 0
    if layer.vector_mask.initial_fill_rule and len(layer.vector_mask.paths) == 0:
        color = 1
    mask = np.full((height, width, 1), color, dtype=np.float32)

    # Group merged path components.
    paths: list[list] = []
    for subpath in layer.vector_mask.paths:
        if subpath.operation == -1:
            paths[-1].append(subpath)
        else:
            paths.append([subpath])

    # Apply shape operation.
    first = True
    for subpath_list in paths:
        plane = _draw_subpath(subpath_list, width, height, brush, pen)
        assert mask.shape == (height, width, 1)
        assert plane.shape == mask.shape

        op = subpath_list[0].operation
        if op == 0:  # Exclude = Union - Intersect.
            mask = mask + plane - 2 * mask * plane
        elif op == 1:  # Union (Combine).
            mask = mask + plane - mask * plane
        elif op == 2:  # Subtract.
            if first and brush:
                mask = 1 - mask
            mask = np.maximum(0, mask - plane)
        elif op == 3:  # Intersect.
            if first and brush:
                mask = 1 - mask
            mask = mask * plane
        first = False

    return np.minimum(1, np.maximum(0, mask))


def _draw_subpath(
    subpath_list: list,
    width: int,
    height: int,
    brush: dict[str, int | float] | None,
    pen: dict[str, int | float] | None,
) -> np.ndarray:
    """
    Rasterize Bezier curves using aggdraw.

    TODO: Replace aggdraw implementation with skimage.draw.

    Note: Callers must be decorated with @needs_aggdraw before calling.
    """
    import aggdraw  # type: ignore[import-not-found]  # noqa: PLC0415

    mask = Image.new("L", (width, height), 0)
    draw = aggdraw.Draw(mask)
    pen = aggdraw.Pen(**pen) if pen else None
    brush = aggdraw.Brush(**brush) if brush else None
    for subpath in subpath_list:
        if len(subpath) <= 1:
            logger.warning("not enough knots: %d" % len(subpath))
            continue
        path = " ".join(map(str, _generate_symbol(subpath, width, height)))
        symbol = aggdraw.Symbol(path)
        draw.symbol((0, 0), symbol, pen, brush)
    draw.flush()
    del draw
    return np.expand_dims(np.array(mask).astype(np.float32) / 255.0, 2)


def _generate_symbol(
    path,
    width: int,
    height: int,
    command: str = "C",
) -> Generator[str | float, None, None]:
    """Sequence generator for SVG path."""
    if len(path) == 0:
        return

    # Initial point.
    yield "M"
    yield path[0].anchor[1] * width
    yield path[0].anchor[0] * height
    yield command

    # Closed path or open path
    points = (
        zip(path, path[1:] + path[0:1]) if path.is_closed() else zip(path, path[1:])
    )

    # Rest of the points.
    for p1, p2 in points:
        yield p1.leaving[1] * width
        yield p1.leaving[0] * height
        yield p2.preceding[1] * width
        yield p2.preceding[0] * height
        yield p2.anchor[1] * width
        yield p2.anchor[0] * height

    if path.is_closed():
        yield "Z"
