"""
Line, Polyline, and Polygon shape implementations.

These shapes are defined by sequences of points connected by straight lines.
"""

from dataclasses import dataclass
from typing import TYPE_CHECKING

from .base import BoundingBox, Shape

if TYPE_CHECKING:
    from .path import Path


@dataclass
class Line(Shape):
    """
    Line segment shape.

    Attributes:
        x1: Start point x coordinate.
        y1: Start point y coordinate.
        x2: End point x coordinate.
        y2: End point y coordinate.
        path_length: Optional SVG pathLength attribute for dash scaling.
    """

    x1: float
    y1: float
    x2: float
    y2: float
    path_length: float | None = None

    def get_bounding_box(self) -> BoundingBox:
        """Calculate bounding box of line segment."""
        return BoundingBox(
            min_x=min(self.x1, self.x2),
            min_y=min(self.y1, self.y2),
            max_x=max(self.x1, self.x2),
            max_y=max(self.y1, self.y2),
        )

    def to_path(self) -> "Path":
        """
        Convert line to path representation.

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

        commands = [
            PathCommand("M", True, [self.x1, self.y1]),
            PathCommand("L", True, [self.x2, self.y2]),
        ]

        return Path(commands)

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

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

        Returns:
            Euclidean distance between endpoints 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

        dx = self.x2 - self.x1
        dy = self.y2 - self.y1
        return math.sqrt(dx * dx + dy * dy)

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

        Args:
            distance: Distance along line from start point (x1, y1) in pixels.

        Returns:
            Tuple of (x, y, angle) where:
            - x, y: Point coordinates
            - angle: Tangent angle in radians (0 = right, π/2 = down per SVG coords)

        Notes:
            - If distance > line length, returns end point
            - If distance < 0, returns start point
        """
        import math

        length = self.get_path_length()

        # Clamp distance to [0, length]
        if distance <= 0:
            t = 0.0
        elif distance >= length:
            t = 1.0
        else:
            t = distance / length if length > 0 else 0.0

        # Linear interpolation
        x = self.x1 + t * (self.x2 - self.x1)
        y = self.y1 + t * (self.y2 - self.y1)

        # Tangent angle (direction from start to end)
        angle = math.atan2(self.y2 - self.y1, self.x2 - self.x1)

        return (x, y, angle)


@dataclass
class Polyline(Shape):
    """
    Polyline shape (connected line segments, not closed).

    Attributes:
        points: List of (x, y) coordinate tuples.
    """

    points: list[tuple[float, float]]

    def __post_init__(self) -> None:
        """Validate polyline parameters."""
        if len(self.points) < 2:
            raise ValueError(f"Polyline must have at least 2 points, got {len(self.points)}")

    def get_bounding_box(self) -> BoundingBox:
        """Calculate bounding box of polyline."""
        xs = [p[0] for p in self.points]
        ys = [p[1] for p in self.points]

        return BoundingBox(min_x=min(xs), min_y=min(ys), max_x=max(xs), max_y=max(ys))

    def to_path(self) -> "Path":
        """
        Convert polyline to path representation.

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

        if not self.points:
            return Path([])

        # Start with moveto to first point
        commands = [PathCommand("M", True, list(self.points[0]))]

        # Add lineto commands for remaining points
        for point in self.points[1:]:
            commands.append(PathCommand("L", True, list(point)))

        return Path(commands)


@dataclass
class Polygon(Shape):
    """
    Polygon shape (connected line segments, automatically closed).

    Attributes:
        points: List of (x, y) coordinate tuples.
    """

    points: list[tuple[float, float]]

    def __post_init__(self) -> None:
        """Validate polygon parameters."""
        if len(self.points) < 2:
            raise ValueError(f"Polygon must have at least 2 points, got {len(self.points)}")

    def get_bounding_box(self) -> BoundingBox:
        """Calculate bounding box of polygon."""
        xs = [p[0] for p in self.points]
        ys = [p[1] for p in self.points]

        return BoundingBox(min_x=min(xs), min_y=min(ys), max_x=max(xs), max_y=max(ys))

    def to_path(self) -> "Path":
        """
        Convert polygon to path representation.

        Returns:
            Path object representing this polygon (closed with Z command).
        """
        from .path import Path, PathCommand

        if not self.points:
            return Path([])

        # Start with moveto to first point
        commands = [PathCommand("M", True, list(self.points[0]))]

        # Add lineto commands for remaining points
        for point in self.points[1:]:
            commands.append(PathCommand("L", True, list(point)))

        # Close the path
        commands.append(PathCommand("Z", True, []))

        return Path(commands)

    def contains_point(self, x: float, y: float) -> bool:
        """
        Test if point is inside polygon using ray casting algorithm.

        Args:
            x: Point x coordinate.
            y: Point y coordinate.

        Returns:
            True if point is inside polygon, False otherwise.
        """
        n = len(self.points)
        inside = False

        p1x, p1y = self.points[0]
        for i in range(1, n + 1):
            p2x, p2y = self.points[i % n]

            if y > min(p1y, p2y):
                if y <= max(p1y, p2y):
                    if x <= max(p1x, p2x):
                        xinters = float("inf")
                        if p1y != p2y:
                            xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
                        if p1x == p2x or x <= xinters:
                            inside = not inside

            p1x, p1y = p2x, p2y

        return inside
