"""Polygon - A closed polygon entity."""

from __future__ import annotations

import math
from collections.abc import Iterator
from typing import TYPE_CHECKING

from ..animation import typed_methods
from ..color import Color, apply_brightness
from ..core.coord import Coord, CoordLike
from ..core.relcoord import RelCoord
from ..core.entity import Entity
from ..gradient import Gradient, PaintLike
from ..renderers import SMILRenderer

if TYPE_CHECKING:
    from collections.abc import Sequence

# A vertex can be a static coordinate, an Entity (uses its position),
# or (Entity, anchor_name) to track a specific anchor.
VertexInput = CoordLike | Entity | tuple[Entity, str]


class Polygon(Entity):
    """
    A closed polygon defined by a list of vertices.

    Vertices can be static coordinates or entity references. Entity-reference
    vertices track the referenced entity's position at render time — when
    the entity moves, the polygon deforms automatically.

    Polygons can be filled, stroked, or both. Use classmethods like
    ``Polygon.star()``, ``Polygon.hexagon()`` for common shapes.

    Attributes:
        vertices: List of resolved Coord objects (computed at access time)
        fill: Fill color (or None for no fill)
        stroke: Stroke color (or None for no stroke)
        stroke_width: Stroke width

    Anchors:
        - "center": Centroid of the polygon
        - "v0", "v1", ...: Individual vertices (resolved from specs)

    Example:
        ```python
        # Triangle from static points
        tri = Polygon([(0, 0), (50, 100), (100, 0)], fill="blue")

        # Entity-reference vertices (reactive)
        a, b, c = Point(0, 0), Point(100, 0), Point(50, 80)
        tri = Polygon([a, b, c], fill="coral")
        b.move_to_surface(cell, at=(0.8, 0.3))  # triangle deforms automatically

        # Mixed static and entity-reference
        tri = Polygon([(0, 0), dot, (rect, "top_right")], fill="teal")

        # In a cell (relative coordinates 0-1)
        cell.add_polygon([(0.5, 0), (1, 1), (0, 1)], fill="red")

        # Using shape classmethods
        cell.add_polygon(Polygon.hexagon(), fill="purple")
        ```
    """

    DEFAULT_FILL = "black"
    DEFAULT_STROKE = None
    DEFAULT_STROKE_WIDTH = 1

    # -- typed animation methods (factory-generated) --
    animate_fill = typed_methods.animate_fill
    animate_stroke = typed_methods.animate_stroke
    animate_stroke_width = typed_methods.animate_stroke_width
    animate_fill_opacity = typed_methods.animate_fill_opacity
    animate_stroke_opacity = typed_methods.animate_stroke_opacity

    def __init__(
        self,
        vertices: Sequence[VertexInput],
        fill: PaintLike | None = DEFAULT_FILL,
        stroke: PaintLike | None = DEFAULT_STROKE,
        stroke_width: float = DEFAULT_STROKE_WIDTH,
        z_index: int = 0,
        opacity: float = 1.0,
        fill_opacity: float | None = None,
        stroke_opacity: float | None = None,
        fill_brightness: float | None = None,
        stroke_brightness: float | None = None,
    ) -> None:
        """
        Create a polygon from vertices.

        Args:
            vertices: List of vertex specs (at least 3). Each can be:
                - ``(x, y)`` tuple or ``Coord`` — static vertex
                - ``Entity`` — tracks the entity's position
                - ``(Entity, "anchor_name")`` — tracks a specific anchor
            fill: Fill color (None for transparent).
            stroke: Stroke color (None for no stroke).
            stroke_width: Stroke width in pixels.
            z_index: Layer ordering (higher = on top).
            opacity: Opacity for both fill and stroke (0.0-1.0).
            fill_opacity: Override opacity for fill only (None = use opacity).
            stroke_opacity: Override opacity for stroke only (None = use opacity).
            fill_brightness: Brightness multiplier for fill 0.0 (black) to 1.0 (unchanged).
            stroke_brightness: Brightness multiplier for stroke 0.0 (black) to 1.0 (unchanged).
        """
        if len(vertices) < 3:
            raise ValueError("Polygon requires at least 3 vertices")

        # Normalize vertex specs:
        #   tuple[float, float] → Coord (static)
        #   Entity → kept as-is (reactive)
        #   (Entity, str) → kept as-is (reactive + anchor)
        self._vertex_specs: list[Coord | Entity | tuple[Entity, str]] = []
        for v in vertices:
            if isinstance(v, Entity | Coord):
                self._vertex_specs.append(v)
            elif isinstance(v, tuple):
                if isinstance(v[0], Entity):
                    self._vertex_specs.append((v[0], str(v[1])))
                else:
                    self._vertex_specs.append(Coord(float(v[0]), float(v[1])))

        # Relative vertices (set by Surface.add_polygon)
        self._relative_vertices: list[RelCoord] | None = None

        # Position is centroid
        centroid = self._calculate_centroid()
        super().__init__(centroid.x, centroid.y, z_index)

        if fill_brightness is not None and fill is not None and not isinstance(fill, Gradient):
            fill = apply_brightness(fill, fill_brightness)
        if (
            stroke_brightness is not None
            and stroke is not None
            and not isinstance(stroke, Gradient)
        ):
            stroke = apply_brightness(stroke, stroke_brightness)
        self._fill = fill if isinstance(fill, Gradient) else (Color(fill) if fill else None)
        self._stroke = (
            stroke if isinstance(stroke, Gradient) else (Color(stroke) if stroke else None)
        )
        self.stroke_width = float(stroke_width)
        self.opacity = float(opacity)
        self.fill_opacity = fill_opacity
        self.stroke_opacity = stroke_opacity

    @property
    def relative_vertices(self) -> list[RelCoord] | None:
        """Relative vertex positions (fractions of reference frame), or None."""
        return self._relative_vertices

    @relative_vertices.setter
    def relative_vertices(self, value: list[RelCoord] | None) -> None:
        self._relative_vertices = value

    def _resolve_vertex(self, spec: Coord | Entity | tuple[Entity, str]) -> Coord:
        """Resolve a single vertex spec to a Coord."""
        if isinstance(spec, Coord):
            return spec
        if isinstance(spec, Entity):
            return spec.position
        # (Entity, anchor_name)
        return spec[0].anchor(spec[1])

    def _calculate_centroid(self) -> Coord:
        """Calculate the centroid (center of mass) of the polygon."""
        resolved = self.vertices
        x = sum(v.x for v in resolved) / len(resolved)
        y = sum(v.y for v in resolved) / len(resolved)
        return Coord(x, y)

    @property
    def vertices(self) -> list[Coord]:
        """The polygon vertices (resolved from specs at access time)."""
        if self._relative_vertices is not None:
            ref = self._reference or self._surface
            if ref is not None:
                ref_x, ref_y, ref_w, ref_h = ref.ref_frame()
                return [
                    Coord(ref_x + rx * ref_w, ref_y + ry * ref_h)
                    for rx, ry in self._relative_vertices
                ]
        return [self._resolve_vertex(s) for s in self._vertex_specs]

    def _has_relative_properties(self) -> bool:
        return super()._has_relative_properties() or self._relative_vertices is not None

    def _resolve_to_absolute(self) -> None:
        """Resolve relative vertices and position to absolute coordinates."""
        if self._relative_vertices is not None:
            resolved = self.vertices
            self._vertex_specs = []
            for v in resolved:
                self._vertex_specs.append(v)
            self._relative_vertices = None
            centroid = self._calculate_centroid()
            self._position = centroid
        super()._resolve_to_absolute()

    @property
    def fill(self) -> str | None:
        """The fill paint as a string (color or gradient ref), or None."""
        if self._fill is None:
            return None
        if isinstance(self._fill, Gradient):
            return self._fill.to_svg_ref()
        return self._fill.to_hex()

    @fill.setter
    def fill(self, value: str | tuple[int, int, int] | Gradient | None) -> None:
        if isinstance(value, Gradient):
            self._fill = value
        else:
            self._fill = Color(value) if value else None

    @property
    def stroke(self) -> str | None:
        """The stroke paint as a string (color or gradient ref), or None."""
        if self._stroke is None:
            return None
        if isinstance(self._stroke, Gradient):
            return self._stroke.to_svg_ref()
        return self._stroke.to_hex()

    @stroke.setter
    def stroke(self, value: str | tuple[int, int, int] | Gradient | None) -> None:
        if isinstance(value, Gradient):
            self._stroke = value
        else:
            self._stroke = Color(value) if value else None

    def _iter_paints(self) -> Iterator[Color | Gradient]:
        if self._fill is not None:
            yield self._fill
        if self._stroke is not None:
            yield self._stroke

    @property
    def rotation_center(self) -> Coord:
        """Natural pivot for rotation/scale: polygon centroid."""
        return self._calculate_centroid()

    @property
    def anchor_names(self) -> list[str]:
        """Available anchors: center and vertices."""
        return ["center"] + [f"v{i}" for i in range(len(self._vertex_specs))]

    def _named_anchor(self, name: str) -> Coord:
        """Get anchor point by name (world space)."""
        if name == "center":
            return self._to_world_space(self._calculate_centroid())
        if name.startswith("v") and name[1:].isdigit():
            idx = int(name[1:])
            if 0 <= idx < len(self._vertex_specs):
                return self._to_world_space(self._resolve_vertex(self._vertex_specs[idx]))
        raise ValueError(f"Polygon has no anchor '{name}'. Available: {self.anchor_names}")

    def _move_by(self, dx: float = 0, dy: float = 0) -> Polygon:
        """
        Move the polygon by an offset, updating vertices.

        In relative mode, converts the pixel offset to fraction adjustments
        on ``_relative_vertices``.  In pixel mode, translates static (Coord)
        vertices directly.  Entity-reference vertices follow their entity
        and are not affected.

        Args:
            dx: Horizontal offset.
            dy: Vertical offset.

        Returns:
            self, for method chaining.
        """
        if self._relative_vertices is not None:
            ref = self._reference or self._surface
            if ref is not None:
                _, _, ref_w, ref_h = ref.ref_frame()
                drx = dx / ref_w if ref_w > 0 else 0
                dry = dy / ref_h if ref_h > 0 else 0
                self._relative_vertices = [
                    RelCoord(v.rx + drx, v.ry + dry) for v in self._relative_vertices
                ]
                return self
        # Pixel mode: shift vertex specs directly
        new_specs = []
        for spec in self._vertex_specs:
            if isinstance(spec, Coord):
                new_specs.append(Coord(spec.x + dx, spec.y + dy))
            else:
                new_specs.append(spec)  # Entity refs untouched
        self._vertex_specs = new_specs
        self._position = Coord(self._position.x + dx, self._position.y + dy)
        return self

    def bounds(self, *, visual: bool = False) -> tuple[float, float, float, float]:
        """Get bounding box (world space).

        Args:
            visual: If True, expand by stroke width / 2 to reflect
                    the rendered extent.
        """
        verts = self.vertices
        if self._rotation != 0 or self._scale_factor != 1.0:
            verts = [self._to_world_space(v) for v in verts]
        min_x = min(v.x for v in verts)
        min_y = min(v.y for v in verts)
        max_x = max(v.x for v in verts)
        max_y = max(v.y for v in verts)
        if visual and self.stroke_width:
            half = self.stroke_width * self._scale_factor / 2
            min_x -= half
            min_y -= half
            max_x += half
            max_y += half
        return (min_x, min_y, max_x, max_y)

    def rotated_bounds(
        self,
        angle: float,
        *,
        visual: bool = False,
    ) -> tuple[float, float, float, float]:
        """Exact AABB of this polygon rotated by *angle* degrees around origin."""
        if angle == 0:
            return self.bounds(visual=visual)
        rad = math.radians(angle)
        cos_a, sin_a = math.cos(rad), math.sin(rad)
        verts = self.vertices
        if self._rotation != 0 or self._scale_factor != 1.0:
            verts = [self._to_world_space(v) for v in verts]
        rx = [v.x * cos_a - v.y * sin_a for v in verts]
        ry = [v.x * sin_a + v.y * cos_a for v in verts]
        min_x, max_x = min(rx), max(rx)
        min_y, max_y = min(ry), max(ry)
        if visual and self.stroke_width:
            half = self.stroke_width * self._scale_factor / 2
            min_x -= half
            min_y -= half
            max_x += half
            max_y += half
        return (min_x, min_y, max_x, max_y)

    def to_svg(self) -> str:
        """Render to SVG polygon element (delegates to renderer)."""
        return SMILRenderer().render_entity(self)

    def __repr__(self) -> str:
        return f"Polygon({len(self._vertex_specs)} vertices, fill={self.fill!r})"

    # ---- Shape classmethods ------------------------------------------------

    @classmethod
    def triangle(
        cls,
        size: float = 1.0,
        center: tuple[float, float] = (0.5, 0.5),
    ) -> list[tuple[float, float]]:
        """Generate equilateral triangle vertices (pointing up)."""
        return _triangle(size, center)

    @classmethod
    def square(
        cls,
        size: float = 0.8,
        center: tuple[float, float] = (0.5, 0.5),
    ) -> list[tuple[float, float]]:
        """Generate square vertices."""
        return _square(size, center)

    @classmethod
    def diamond(
        cls,
        size: float = 0.8,
        center: tuple[float, float] = (0.5, 0.5),
    ) -> list[tuple[float, float]]:
        """Generate diamond (rotated square) vertices."""
        return _diamond(size, center)

    @classmethod
    def hexagon(
        cls,
        size: float = 0.8,
        center: tuple[float, float] = (0.5, 0.5),
    ) -> list[tuple[float, float]]:
        """Generate regular hexagon vertices."""
        return _hexagon(size, center)

    @classmethod
    def star(
        cls,
        points: int = 5,
        size: float = 0.8,
        inner_ratio: float = 0.4,
        center: tuple[float, float] = (0.5, 0.5),
    ) -> list[tuple[float, float]]:
        """Generate star vertices."""
        return _star(points, size, inner_ratio, center)

    @classmethod
    def regular_polygon(
        cls,
        sides: int,
        size: float = 0.8,
        center: tuple[float, float] = (0.5, 0.5),
    ) -> list[tuple[float, float]]:
        """Generate regular polygon with N sides."""
        return _regular_polygon(sides, size, center)

    @classmethod
    def squircle(
        cls,
        size: float = 0.8,
        center: tuple[float, float] = (0.5, 0.5),
        n: float = 4,
        points: int = 32,
    ) -> list[tuple[float, float]]:
        """Generate squircle (superellipse) vertices."""
        return _squircle(size, center, n, points)

    @classmethod
    def rounded_rect(
        cls,
        size: float = 0.8,
        center: tuple[float, float] = (0.5, 0.5),
        corner_radius: float = 0.2,
        points_per_corner: int = 8,
    ) -> list[tuple[float, float]]:
        """Generate rectangle with rounded corners vertices."""
        return _rounded_rect(size, center, corner_radius, points_per_corner)


# =============================================================================
# Shape Helper Functions (internal)
# =============================================================================


def _triangle(
    size: float = 1.0,
    center: tuple[float, float] = (0.5, 0.5),
) -> list[tuple[float, float]]:
    """
    Generate triangle vertices (equilateral, pointing up).

    Args:
        size: Scale factor (1.0 fills the cell).
        center: Center point in relative coordinates.

    Returns:
        List of (x, y) tuples for use with add_polygon().
    """
    cx, cy = center
    h = size * 0.5  # Half-height
    w = size * 0.5 * math.sqrt(3) / 2  # Half-width for equilateral

    return [
        (cx, cy - h),  # Top
        (cx + w, cy + h * 0.5),  # Bottom right
        (cx - w, cy + h * 0.5),  # Bottom left
    ]


def _square(
    size: float = 0.8,
    center: tuple[float, float] = (0.5, 0.5),
) -> list[tuple[float, float]]:
    """Generate square vertices."""
    cx, cy = center
    h = size * 0.5
    return [
        (cx - h, cy - h),  # Top-left
        (cx + h, cy - h),  # Top-right
        (cx + h, cy + h),  # Bottom-right
        (cx - h, cy + h),  # Bottom-left
    ]


def _diamond(
    size: float = 0.8,
    center: tuple[float, float] = (0.5, 0.5),
) -> list[tuple[float, float]]:
    """Generate diamond (rotated square) vertices."""
    cx, cy = center
    h = size * 0.5
    return [
        (cx, cy - h),  # Top
        (cx + h, cy),  # Right
        (cx, cy + h),  # Bottom
        (cx - h, cy),  # Left
    ]


def _hexagon(
    size: float = 0.8,
    center: tuple[float, float] = (0.5, 0.5),
) -> list[tuple[float, float]]:
    """Generate regular hexagon vertices."""
    cx, cy = center
    r = size * 0.5
    vertices = []
    for i in range(6):
        angle = math.pi / 6 + i * math.pi / 3  # Start from top-right
        vertices.append(
            (
                cx + r * math.cos(angle),
                cy + r * math.sin(angle),
            )
        )
    return vertices


def _star(
    points: int = 5,
    size: float = 0.8,
    inner_ratio: float = 0.4,
    center: tuple[float, float] = (0.5, 0.5),
) -> list[tuple[float, float]]:
    """
    Generate star vertices.

    Args:
        points: Number of star points.
        size: Outer radius scale.
        inner_ratio: Inner radius as fraction of outer (0-1).
        center: Center point.
    """
    cx, cy = center
    outer_r = size * 0.5
    inner_r = outer_r * inner_ratio

    vertices = []
    for i in range(points * 2):
        angle = -math.pi / 2 + i * math.pi / points  # Start from top
        r = outer_r if i % 2 == 0 else inner_r
        vertices.append(
            (
                cx + r * math.cos(angle),
                cy + r * math.sin(angle),
            )
        )
    return vertices


def _regular_polygon(
    sides: int,
    size: float = 0.8,
    center: tuple[float, float] = (0.5, 0.5),
) -> list[tuple[float, float]]:
    """
    Generate regular polygon with N sides.

    Args:
        sides: Number of sides (3 = triangle, 4 = square, etc.)
        size: Scale factor.
        center: Center point.
    """
    if sides < 3:
        raise ValueError("Polygon must have at least 3 sides")

    cx, cy = center
    r = size * 0.5
    vertices = []
    for i in range(sides):
        angle = -math.pi / 2 + i * 2 * math.pi / sides  # Start from top
        vertices.append(
            (
                cx + r * math.cos(angle),
                cy + r * math.sin(angle),
            )
        )
    return vertices


def _squircle(
    size: float = 0.8,
    center: tuple[float, float] = (0.5, 0.5),
    n: float = 4,
    points: int = 32,
) -> list[tuple[float, float]]:
    """
    Generate a squircle (superellipse) - a shape between square and circle.

    Args:
        size: Scale factor (1.0 fills the cell).
        center: Center point in relative coordinates.
        n: Squareness parameter (2=circle, 4=squircle, 6+=rounded square).
        points: Number of vertices (higher = smoother).
    """
    cx, cy = center
    r = size * 0.5

    vertices = []
    for i in range(points):
        angle = 2 * math.pi * i / points

        # Superellipse formula: |x/a|^n + |y/b|^n = 1
        # Solved for x,y given angle
        cos_a = math.cos(angle)
        sin_a = math.sin(angle)

        # Sign-preserving power function
        def sgn_pow(val, exp):
            return math.copysign(abs(val) ** exp, val)

        x = sgn_pow(cos_a, 2 / n) * r
        y = sgn_pow(sin_a, 2 / n) * r

        vertices.append((cx + x, cy + y))

    return vertices


def _rounded_rect(
    size: float = 0.8,
    center: tuple[float, float] = (0.5, 0.5),
    corner_radius: float = 0.2,
    points_per_corner: int = 8,
) -> list[tuple[float, float]]:
    """
    Generate a rectangle with rounded corners.

    Args:
        size: Scale factor.
        center: Center point.
        corner_radius: Radius of corners as fraction of size (0-0.5).
        points_per_corner: Vertices per corner arc.
    """
    cx, cy = center
    half = size * 0.5
    r = min(corner_radius * size, half)  # Clamp radius

    vertices = []

    # Generate corners: top-right, bottom-right, bottom-left, top-left
    corners = [
        (cx + half - r, cy - half + r, -math.pi / 2, 0),  # Top-right
        (cx + half - r, cy + half - r, 0, math.pi / 2),  # Bottom-right
        (cx - half + r, cy + half - r, math.pi / 2, math.pi),  # Bottom-left
        (cx - half + r, cy - half + r, math.pi, 3 * math.pi / 2),  # Top-left
    ]

    for corner_x, corner_y, start_angle, end_angle in corners:
        for i in range(points_per_corner):
            t = i / points_per_corner
            angle = start_angle + t * (end_angle - start_angle)
            x = corner_x + r * math.cos(angle)
            y = corner_y + r * math.sin(angle)
            vertices.append((x, y))

    return vertices
