"""
Base interface for all SVG shape primitives.

Shapes represent geometric primitives independent of styling and transforms.
All concrete shape classes inherit from the Shape abstract base class.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .path import Path


@dataclass
class BoundingBox:
    """Axis-aligned bounding box."""

    min_x: float
    min_y: float
    max_x: float
    max_y: float

    @property
    def width(self) -> float:
        """Get bounding box width."""
        return self.max_x - self.min_x

    @property
    def height(self) -> float:
        """Get bounding box height."""
        return self.max_y - self.min_y

    @property
    def center(self) -> tuple[float, float]:
        """Get bounding box center point."""
        return ((self.min_x + self.max_x) / 2, (self.min_y + self.max_y) / 2)


class Shape(ABC):
    """
    Abstract base class for all SVG shapes.

    Shapes represent pure geometry without styling or transforms.
    Each shape can compute its bounding box and convert to a path representation.
    """

    @abstractmethod
    def get_bounding_box(self) -> BoundingBox:
        """
        Calculate the axis-aligned bounding box of this shape.

        Returns:
            BoundingBox containing the shape's geometry.
        """
        pass

    @abstractmethod
    def to_path(self) -> "Path":
        """
        Convert this shape to a path representation.

        This allows normalizing all shapes to a common path format for rendering.

        Returns:
            Path object representing this shape's geometry.
        """
        pass

    def contains_point(self, x: float, y: float) -> bool:
        """
        Test if a point is inside this shape (for hit testing).

        Default implementation uses bounding box test.
        Subclasses can override for more accurate testing.

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

        Returns:
            True if point is inside shape, False otherwise.
        """
        bbox = self.get_bounding_box()
        return bbox.min_x <= x <= bbox.max_x and bbox.min_y <= y <= bbox.max_y

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

        This is used for dash pattern positioning along the shape's path.
        Must be implemented by subclasses that support dashed strokes.

        Returns:
            Total arc-length in user units.

        Raises:
            NotImplementedError: If shape doesn't support path measurement.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} does not implement get_path_length(). "
            "Dashed strokes will fall back to solid strokes for this shape."
        )

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

        This is used for extracting dash segments from the shape's outline.
        Must be implemented by subclasses that support dashed strokes.

        Args:
            distance: Distance along path from start (0.0 to get_path_length()).

        Returns:
            Tuple of (x, y, tangent_angle) where tangent_angle is in radians.

        Raises:
            NotImplementedError: If shape doesn't support path measurement.
            ValueError: If distance is outside [0, get_path_length()] range.
        """
        raise NotImplementedError(
            f"{self.__class__.__name__} does not implement get_point_at_distance(). "
            "Dashed strokes will fall back to solid strokes for this shape."
        )
