"""
Path shape implementation.

Paths are the most general SVG shape type, supporting all SVG path commands.
All other shapes can be converted to paths.
"""

from dataclasses import dataclass

from .base import BoundingBox, Shape


@dataclass
class PathCommand:
    """
    Represents a single path command (M, L, C, Q, A, Z, etc.).

    Attributes:
        command_type: Command letter (M, L, C, Q, A, Z, H, V, S, T).
        absolute: True for uppercase (absolute coords), False for lowercase (relative).
        points: List of coordinate values (varies by command type).
    """

    command_type: str
    absolute: bool
    points: list[float]

    def __post_init__(self) -> None:
        """Validate command parameters."""
        valid_commands = ["M", "L", "H", "V", "C", "S", "Q", "T", "A", "Z"]
        if self.command_type.upper() not in valid_commands:
            raise ValueError(f"Invalid path command: {self.command_type}")


@dataclass
class Path(Shape):
    """
    Path shape - most general SVG shape type.

    A path is a sequence of drawing commands that can represent any shape.

    Attributes:
        commands: List of PathCommand objects defining the path.
        path_length: Optional SVG pathLength attribute for dash scaling.
    """

    commands: list[PathCommand]
    path_length: float | None = None

    def __post_init__(self) -> None:
        """Validate path."""
        if self.commands and self.commands[0].command_type.upper() != "M":
            raise ValueError("Path must start with MoveTo (M) command")

    def get_bounding_box(self) -> BoundingBox:
        """
        Calculate bounding box of path.

        This is an approximation that uses command endpoints.
        Accurate bounding box calculation would need to evaluate Bezier curves.

        Returns:
            BoundingBox containing the path's geometry.
        """
        if not self.commands:
            return BoundingBox(0, 0, 0, 0)

        # Extract all coordinate points from commands
        all_x = []
        all_y = []
        current_x, current_y = 0.0, 0.0

        for cmd in self.commands:
            if cmd.command_type.upper() == "Z":
                # Close path - no new coordinates
                continue

            # Parse points from command
            points = cmd.points

            if cmd.command_type.upper() == "H":
                # Horizontal line - only x coordinate
                for i in range(0, len(points)):
                    x = points[i] if cmd.absolute else current_x + points[i]
                    all_x.append(x)
                    all_y.append(current_y)
                    current_x = x
            elif cmd.command_type.upper() == "V":
                # Vertical line - only y coordinate
                for i in range(0, len(points)):
                    y = points[i] if cmd.absolute else current_y + points[i]
                    all_x.append(current_x)
                    all_y.append(y)
                    current_y = y
            else:
                # Commands with x,y pairs
                for i in range(0, len(points), 2):
                    if i + 1 < len(points):
                        x = points[i] if cmd.absolute else current_x + points[i]
                        y = points[i + 1] if cmd.absolute else current_y + points[i + 1]
                        all_x.append(x)
                        all_y.append(y)

                        # Update current position to last point
                        if cmd.command_type.upper() in ["M", "L", "T"]:
                            current_x, current_y = x, y
                        elif cmd.command_type.upper() in ["C", "S"]:
                            # Cubic bezier - last two coords are end point
                            if i == len(points) - 2:
                                current_x, current_y = x, y
                        elif cmd.command_type.upper() == "Q":
                            # Quadratic bezier - last two coords are end point
                            if i == len(points) - 2:
                                current_x, current_y = x, y
                        elif cmd.command_type.upper() == "A":
                            # Arc - last two coords are end point
                            if i == len(points) - 2:
                                current_x, current_y = x, y

        if not all_x:
            return BoundingBox(0, 0, 0, 0)

        return BoundingBox(min_x=min(all_x), min_y=min(all_y), max_x=max(all_x), max_y=max(all_y))

    def to_path(self) -> "Path":
        """
        Return self (path is already in path format).

        Returns:
            This path object.
        """
        return self

    def flatten(self, tolerance: float = 0.5) -> list[tuple[float, float]]:
        """
        Convert path to a sequence of line segments (flatten curves).

        This converts all curve commands (C, Q, A) to sequences of line segments.

        Args:
            tolerance: Maximum distance between curve and approximation.

        Returns:
            List of (x, y) points representing flattened path.
        """
        points = []
        current_x, current_y = 0.0, 0.0
        start_x, start_y = 0.0, 0.0
        last_control_x, last_control_y = 0.0, 0.0  # Track last control point for S command
        last_cmd_type = ""

        for cmd in self.commands:
            cmd_type = cmd.command_type.upper()

            if cmd_type == "M":
                # MoveTo
                x = cmd.points[0] if cmd.absolute else current_x + cmd.points[0]
                y = cmd.points[1] if cmd.absolute else current_y + cmd.points[1]
                current_x, current_y = x, y
                start_x, start_y = x, y
                points.append((x, y))

            elif cmd_type == "L":
                # LineTo
                x = cmd.points[0] if cmd.absolute else current_x + cmd.points[0]
                y = cmd.points[1] if cmd.absolute else current_y + cmd.points[1]
                current_x, current_y = x, y
                points.append((x, y))

            elif cmd_type == "H":
                # Horizontal line
                x = cmd.points[0] if cmd.absolute else current_x + cmd.points[0]
                current_x = x
                points.append((x, current_y))

            elif cmd_type == "V":
                # Vertical line
                y = cmd.points[0] if cmd.absolute else current_y + cmd.points[0]
                current_y = y
                points.append((current_x, y))

            elif cmd_type == "C":
                # Cubic Bezier - flatten using subdivision
                if len(cmd.points) >= 6:
                    x1, y1 = cmd.points[0], cmd.points[1]
                    x2, y2 = cmd.points[2], cmd.points[3]
                    x, y = cmd.points[4], cmd.points[5]

                    if not cmd.absolute:
                        x1 += current_x
                        y1 += current_y
                        x2 += current_x
                        y2 += current_y
                        x += current_x
                        y += current_y

                    # Simple flattening - subdivide into line segments
                    # (Full implementation would use adaptive subdivision)
                    steps = max(10, int(tolerance * 20))
                    for i in range(1, steps + 1):
                        t = i / steps
                        # Cubic Bezier formula
                        px = (
                            (1 - t) ** 3 * current_x
                            + 3 * (1 - t) ** 2 * t * x1
                            + 3 * (1 - t) * t**2 * x2
                            + t**3 * x
                        )
                        py = (
                            (1 - t) ** 3 * current_y
                            + 3 * (1 - t) ** 2 * t * y1
                            + 3 * (1 - t) * t**2 * y2
                            + t**3 * y
                        )
                        points.append((px, py))

                    last_control_x, last_control_y = x2, y2
                    current_x, current_y = x, y
                    last_cmd_type = "C"

            elif cmd_type == "Q":
                # Quadratic Bezier - flatten using subdivision
                if len(cmd.points) >= 4:
                    x1, y1 = cmd.points[0], cmd.points[1]
                    x, y = cmd.points[2], cmd.points[3]

                    if not cmd.absolute:
                        x1 += current_x
                        y1 += current_y
                        x += current_x
                        y += current_y

                    # Simple flattening
                    steps = max(10, int(tolerance * 20))
                    for i in range(1, steps + 1):
                        t = i / steps
                        # Quadratic Bezier formula
                        px = (1 - t) ** 2 * current_x + 2 * (1 - t) * t * x1 + t**2 * x
                        py = (1 - t) ** 2 * current_y + 2 * (1 - t) * t * y1 + t**2 * y
                        points.append((px, py))

                    current_x, current_y = x, y

            elif cmd_type == "S":
                # Smooth Cubic Bezier
                # Reflects the previous control point or uses current point
                if len(cmd.points) >= 4:
                    # Reflect previous control point if last command was C or S
                    if last_cmd_type in ("C", "S"):
                        x1 = 2 * current_x - last_control_x
                        y1 = 2 * current_y - last_control_y
                    else:
                        x1, y1 = current_x, current_y

                    x2, y2 = cmd.points[0], cmd.points[1]
                    x, y = cmd.points[2], cmd.points[3]

                    if not cmd.absolute:
                        x2 += current_x
                        y2 += current_y
                        x += current_x
                        y += current_y

                    # Flatten as cubic Bezier
                    steps = max(10, int(tolerance * 20))
                    for i in range(1, steps + 1):
                        t = i / steps
                        px = (
                            (1 - t) ** 3 * current_x
                            + 3 * (1 - t) ** 2 * t * x1
                            + 3 * (1 - t) * t**2 * x2
                            + t**3 * x
                        )
                        py = (
                            (1 - t) ** 3 * current_y
                            + 3 * (1 - t) ** 2 * t * y1
                            + 3 * (1 - t) * t**2 * y2
                            + t**3 * y
                        )
                        points.append((px, py))

                    last_control_x, last_control_y = x2, y2
                    current_x, current_y = x, y
                    last_cmd_type = "S"

            elif cmd_type == "Z":
                # Close path
                if (current_x, current_y) != (start_x, start_y):
                    points.append((start_x, start_y))
                current_x, current_y = start_x, start_y

            elif cmd_type == "A":
                # Arc - approximate with line segments using proper arc calculation
                if len(cmd.points) >= 7:
                    rx, ry = cmd.points[0], cmd.points[1]
                    x_axis_rotation = cmd.points[2]
                    large_arc_flag = cmd.points[3]
                    sweep_flag = cmd.points[4]
                    x = cmd.points[5] if cmd.absolute else current_x + cmd.points[5]
                    y = cmd.points[6] if cmd.absolute else current_y + cmd.points[6]

                    # Handle degenerate cases
                    if rx == 0 or ry == 0 or (current_x == x and current_y == y):
                        # Degenerate to line
                        points.append((x, y))
                    else:
                        import math

                        # Convert to radians
                        phi = math.radians(x_axis_rotation)
                        cos_phi = math.cos(phi)
                        sin_phi = math.sin(phi)

                        # Compute center point (simplified - assumes no rotation for now)
                        dx = (current_x - x) / 2
                        dy = (current_y - y) / 2

                        # Transform to unit circle
                        x1 = cos_phi * dx + sin_phi * dy
                        y1 = -sin_phi * dx + cos_phi * dy

                        # Correct radii if needed
                        lambda_sq = (x1 / rx) ** 2 + (y1 / ry) ** 2
                        if lambda_sq > 1:
                            rx *= math.sqrt(lambda_sq)
                            ry *= math.sqrt(lambda_sq)

                        # Calculate center
                        sq = max(0, (rx * ry) ** 2 - (rx * y1) ** 2 - (ry * x1) ** 2)
                        sq = math.sqrt(sq / ((rx * y1) ** 2 + (ry * x1) ** 2))
                        if large_arc_flag == sweep_flag:
                            sq = -sq

                        cx1 = sq * rx * y1 / ry
                        cy1 = -sq * ry * x1 / rx

                        # Transform back
                        cx = cos_phi * cx1 - sin_phi * cy1 + (current_x + x) / 2
                        cy = sin_phi * cx1 + cos_phi * cy1 + (current_y + y) / 2

                        # Calculate angles
                        ux = (x1 - cx1) / rx
                        uy = (y1 - cy1) / ry
                        vx = (-x1 - cx1) / rx
                        vy = (-y1 - cy1) / ry

                        n = math.sqrt(ux * ux + uy * uy)
                        p = ux
                        theta1 = math.acos(p / n) if n != 0 else 0
                        if uy < 0:
                            theta1 = -theta1

                        n = math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
                        p = ux * vx + uy * vy
                        dtheta = math.acos(p / n) if n != 0 else 0
                        if ux * vy - uy * vx < 0:
                            dtheta = -dtheta

                        if sweep_flag and dtheta < 0:
                            dtheta += 2 * math.pi
                        elif not sweep_flag and dtheta > 0:
                            dtheta -= 2 * math.pi

                        # Generate points along arc
                        steps = max(20, int(abs(dtheta) * 10))
                        for i in range(1, steps + 1):
                            t = i / steps
                            angle = theta1 + t * dtheta

                            # Point on ellipse
                            px = rx * math.cos(angle)
                            py = ry * math.sin(angle)

                            # Rotate and translate
                            px_rot = cos_phi * px - sin_phi * py + cx
                            py_rot = sin_phi * px + cos_phi * py + cy

                            points.append((px_rot, py_rot))

                    current_x, current_y = x, y
                    last_cmd_type = "A"

        return points

    def get_path_length(self) -> float:
        """
        Calculate total path length by summing segment lengths.

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

        For straight lines, uses Euclidean distance.
        For curves, flattens them and sums the flattened segment lengths.

        Returns:
            Total path length in user units (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

        # Flatten path to get all points
        flattened_points = self.flatten(tolerance=0.5)

        if len(flattened_points) < 2:
            return 0.0

        # Sum Euclidean distances between consecutive points
        total_length = 0.0
        for i in range(1, len(flattened_points)):
            x1, y1 = flattened_points[i - 1]
            x2, y2 = flattened_points[i]
            segment_length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
            total_length += segment_length

        return total_length

    def get_point_at_distance(self, distance: float) -> tuple[float, float, float]:
        """
        Get point on path at specified distance from start.

        Traverses the path segments until the target distance is reached.
        Wraps around if distance exceeds path length.

        Args:
            distance: Distance from path start (wraps if > path_length).

        Returns:
            Tuple of (x, y, angle_degrees) where angle is tangent direction.
        """
        import math

        # Flatten path to get all points
        flattened_points = self.flatten(tolerance=0.5)

        if len(flattened_points) < 2:
            return (0.0, 0.0, 0.0)

        # Calculate total path length and handle wrapping
        path_length = self.get_path_length()
        if path_length == 0:
            return (flattened_points[0][0], flattened_points[0][1], 0.0)

        # Wrap distance within path length
        wrapped_distance = distance % path_length

        # Walk through segments to find target distance
        accumulated_distance = 0.0
        for i in range(1, len(flattened_points)):
            x1, y1 = flattened_points[i - 1]
            x2, y2 = flattened_points[i]

            segment_length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

            if accumulated_distance + segment_length >= wrapped_distance:
                # Target point is on this segment
                t = (
                    (wrapped_distance - accumulated_distance) / segment_length
                    if segment_length > 0
                    else 0
                )

                # Interpolate position
                x = x1 + t * (x2 - x1)
                y = y1 + t * (y2 - y1)

                # Calculate tangent angle
                angle_rad = math.atan2(y2 - y1, x2 - x1)
                angle_deg = math.degrees(angle_rad)

                return (x, y, angle_deg)

            accumulated_distance += segment_length

        # Should not reach here, but fallback to end point
        x_end, y_end = flattened_points[-1]
        x_prev, y_prev = (
            flattened_points[-2] if len(flattened_points) >= 2 else flattened_points[-1]
        )

        angle_rad = math.atan2(y_end - y_prev, x_end - x_prev)
        angle_deg = math.degrees(angle_rad)

        return (x_end, y_end, angle_deg)
