"""
Circle and Ellipse shape implementations.

Circles are defined by center point and radius.
Ellipses are defined by center point and two radii (rx, ry).
"""

from dataclasses import dataclass
from typing import TYPE_CHECKING

from .base import BoundingBox, Shape

if TYPE_CHECKING:
    from .path import Path


@dataclass
class Circle(Shape):
    """
    Circle shape.

    Attributes:
        cx: Center x coordinate.
        cy: Center y coordinate.
        r: Radius (must be positive).
        path_length: Optional SVG pathLength attribute for dash scaling.
    """

    cx: float
    cy: float
    r: float
    path_length: float | None = None

    def __post_init__(self) -> None:
        """Validate circle parameters."""
        if self.r < 0:
            raise ValueError(f"Circle radius must be non-negative, got {self.r}")

    def get_bounding_box(self) -> BoundingBox:
        """Calculate bounding box of circle."""
        return BoundingBox(
            min_x=self.cx - self.r,
            min_y=self.cy - self.r,
            max_x=self.cx + self.r,
            max_y=self.cy + self.r,
        )

    def to_path(self) -> "Path":
        """
        Convert circle to path representation using approximation.

        Uses four cubic Bezier curves to approximate a circle.

        Returns:
            Path object representing this circle.
        """

        # Use ellipse-to-path conversion with equal radii
        ellipse = Ellipse(self.cx, self.cy, self.r, self.r)
        return ellipse.to_path()

    def contains_point(self, x: float, y: float) -> bool:
        """Test if point is inside circle."""
        dx = x - self.cx
        dy = y - self.cy
        return (dx * dx + dy * dy) <= (self.r * self.r)

    def get_path_length(self) -> float:
        """
        Calculate total arc-length of circle perimeter.

        If path_length attribute is set (SVG pathLength), returns that value
        for proper dash pattern scaling. Otherwise returns computed circumference.

        Returns:
            Circumference (2πr) in pixels (or pathLength if set).
        """
        # SVG pathLength attribute overrides computed length for dash calculations
        if self.path_length is not None:
            return self.path_length

        import math

        return 2.0 * math.pi * self.r

    def get_point_at_distance(self, distance: float) -> tuple[float, float, float]:
        """
        Get point coordinates and tangent angle at specified distance along circle.

        Traverses circle clockwise starting from rightmost point (3 o'clock position).

        Args:
            distance: Distance along perimeter from starting point in pixels.

        Returns:
            Tuple of (x, y, angle) where:
            - x, y: Point coordinates
            - angle: Tangent angle in radians (perpendicular to radius)

        Notes:
            - If distance > circumference, wraps around using modulo
        """
        import math

        circumference = self.get_path_length()
        if circumference <= 0:
            return (self.cx + self.r, self.cy, math.pi / 2)

        # Wrap distance
        distance = distance % circumference

        # Convert distance to angle (radians)
        # Start at 0 radians (rightmost point), go clockwise (positive angle)
        angle_radians = distance / self.r

        # Calculate point position
        x = self.cx + self.r * math.cos(angle_radians)
        y = self.cy + self.r * math.sin(angle_radians)

        # Tangent angle is perpendicular to radius (add π/2)
        tangent_angle = angle_radians + math.pi / 2

        return (x, y, tangent_angle)


@dataclass
class Ellipse(Shape):
    """
    Ellipse shape.

    Attributes:
        cx: Center x coordinate.
        cy: Center y coordinate.
        rx: X-axis radius (must be positive).
        ry: Y-axis radius (must be positive).
        path_length: Optional SVG pathLength attribute for dash scaling.
    """

    cx: float
    cy: float
    rx: float
    ry: float
    path_length: float | None = None

    def __post_init__(self) -> None:
        """Validate ellipse parameters."""
        if self.rx < 0:
            raise ValueError(f"Ellipse rx must be non-negative, got {self.rx}")
        if self.ry < 0:
            raise ValueError(f"Ellipse ry must be non-negative, got {self.ry}")

    def get_bounding_box(self) -> BoundingBox:
        """Calculate bounding box of ellipse."""
        return BoundingBox(
            min_x=self.cx - self.rx,
            min_y=self.cy - self.ry,
            max_x=self.cx + self.rx,
            max_y=self.cy + self.ry,
        )

    def to_path(self) -> "Path":
        """
        Convert ellipse to path representation using cubic Bezier approximation.

        Uses four cubic Bezier curves to approximate an ellipse.
        Magic number for control point offset: 4/3 * tan(π/8) ≈ 0.5522847498

        Returns:
            Path object representing this ellipse.
        """
        from .path import Path, PathCommand

        # If either radius is 0, return empty path (nothing to render)
        if self.rx == 0 or self.ry == 0:
            return Path([])

        # Bezier control point magic number for approximating circle arcs
        k = 0.5522847498

        # Control point offsets
        ox = self.rx * k
        oy = self.ry * k

        commands = [
            # Start at right-most point
            PathCommand("M", True, [self.cx + self.rx, self.cy]),
            # Top-right quadrant (clockwise)
            PathCommand(
                "C",
                True,
                [
                    self.cx + self.rx,
                    self.cy - oy,
                    self.cx + ox,
                    self.cy - self.ry,
                    self.cx,
                    self.cy - self.ry,
                ],
            ),
            # Top-left quadrant
            PathCommand(
                "C",
                True,
                [
                    self.cx - ox,
                    self.cy - self.ry,
                    self.cx - self.rx,
                    self.cy - oy,
                    self.cx - self.rx,
                    self.cy,
                ],
            ),
            # Bottom-left quadrant
            PathCommand(
                "C",
                True,
                [
                    self.cx - self.rx,
                    self.cy + oy,
                    self.cx - ox,
                    self.cy + self.ry,
                    self.cx,
                    self.cy + self.ry,
                ],
            ),
            # Bottom-right quadrant
            PathCommand(
                "C",
                True,
                [
                    self.cx + ox,
                    self.cy + self.ry,
                    self.cx + self.rx,
                    self.cy + oy,
                    self.cx + self.rx,
                    self.cy,
                ],
            ),
            PathCommand("Z", True, []),
        ]

        return Path(commands)

    def contains_point(self, x: float, y: float) -> bool:
        """Test if point is inside ellipse using ellipse equation."""
        dx = (x - self.cx) / self.rx
        dy = (y - self.cy) / self.ry
        return (dx * dx + dy * dy) <= 1.0

    def get_path_length(self) -> float:
        """
        Calculate total arc-length of ellipse perimeter.

        If path_length attribute is set (SVG pathLength), returns that value
        for proper dash pattern scaling. Otherwise uses Ramanujan's approximation.

        Returns:
            Approximate perimeter length in pixels (or pathLength if set).
        """
        # SVG pathLength attribute overrides computed length for dash calculations
        if self.path_length is not None:
            return self.path_length

        import math

        if self.rx == self.ry:
            # Circle - exact formula
            return 2.0 * math.pi * self.rx

        # Ramanujan's approximation for ellipse
        h = ((self.rx - self.ry) ** 2) / ((self.rx + self.ry) ** 2)
        circumference = math.pi * (self.rx + self.ry) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
        return circumference

    def get_point_at_distance(self, distance: float) -> tuple[float, float, float]:
        """
        Get point coordinates and tangent angle at specified distance along ellipse.

        Uses parametric ellipse equation with arc-length approximation.

        Args:
            distance: Distance along perimeter from starting point in pixels.

        Returns:
            Tuple of (x, y, angle) where:
            - x, y: Point coordinates
            - angle: Tangent angle in radians

        Notes:
            - If distance > perimeter, wraps around using modulo
            - Uses numerical approximation for ellipse arc-length
        """
        import math

        perimeter = self.get_path_length()
        if perimeter <= 0:
            return (self.cx + self.rx, self.cy, math.pi / 2)

        # Wrap distance
        distance = distance % perimeter

        # For ellipse, arc-length parameterization is complex
        # Use approximation: treat as circle and scale
        # This is not perfectly accurate but good enough for dash rendering

        # Approximate angle from distance (assuming circular arc-length)
        # Better approximation would use numerical integration
        angle_radians = 2 * math.pi * (distance / perimeter)

        # Calculate point on ellipse
        x = self.cx + self.rx * math.cos(angle_radians)
        y = self.cy + self.ry * math.sin(angle_radians)

        # Tangent angle for ellipse: derivative of parametric form
        # dx/dt = -rx*sin(t), dy/dt = ry*cos(t)
        # tangent angle = atan2(dy/dt, dx/dt)
        tangent_angle = math.atan2(
            self.ry * math.cos(angle_radians), -self.rx * math.sin(angle_radians)
        )

        return (x, y, tangent_angle)
