"""
Dash pattern parsing and segment generation for SVG stroke-dasharray.

This module provides the DashPattern class for parsing SVG stroke-dasharray
and stroke-dashoffset attributes, and generating dash segment positions along paths.
"""

from __future__ import annotations

import logging
from collections.abc import Iterator
from dataclasses import dataclass

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class DashPattern:
    """
    Represents a normalized SVG stroke dash pattern.

    Attributes:
        dash_lengths: Even-length tuple of dash and gap lengths in pixels.
                     Odd-length patterns are automatically doubled per SVG spec.
        offset: Pattern offset in pixels. Wraps using modulo.
        total_length: Pre-computed pattern cycle length (sum of dash_lengths).
    """

    dash_lengths: tuple[float, ...]
    offset: float
    total_length: float

    @classmethod
    def from_svg_attributes(
        cls, dasharray: str | None, dashoffset: str | None = None
    ) -> DashPattern | None:
        """
        Parse SVG stroke-dasharray and stroke-dashoffset attributes.

        Args:
            dasharray: SVG stroke-dasharray value (e.g., "5,3", "10", "none").
            dashoffset: SVG stroke-dashoffset value (e.g., "5", "-10").

        Returns:
            DashPattern instance or None for solid strokes.

        Raises:
            ValueError: If dasharray contains invalid numeric values (NaN, Infinity).

        Examples:
            >>> DashPattern.from_svg_attributes("5,3")
            DashPattern(dash_lengths=(5.0, 3.0), offset=0.0, total_length=8.0)

            >>> DashPattern.from_svg_attributes("10")  # Single value
            DashPattern(dash_lengths=(10.0, 10.0), offset=0.0, total_length=20.0)

            >>> DashPattern.from_svg_attributes("5,3,2")  # Odd-length
            DashPattern(dash_lengths=(5.0, 3.0, 2.0, 5.0, 3.0, 2.0), offset=0.0, total_length=20.0)

            >>> DashPattern.from_svg_attributes("none")
            None  # Solid stroke

        Notes:
            - Negative or zero dash/gap values are clamped to zero
            - Whitespace and comma delimiters are both accepted
            - Malformed patterns log warning and return None (solid stroke fallback)
        """
        import math
        import re

        # Handle None or empty string
        if not dasharray or not dasharray.strip():
            return None

        dasharray = dasharray.strip().lower()

        # Handle "none" keyword
        if dasharray == "none":
            return None

        # Parse dasharray values (handle both comma and whitespace delimiters)
        # Split by comma or whitespace
        parts = re.split(r"[,\s]+", dasharray)

        try:
            dash_values = []
            for part in parts:
                if part:  # Skip empty strings
                    value = float(part)
                    # Check for NaN and Infinity
                    if math.isnan(value) or math.isinf(value):
                        logger.warning(
                            f"Invalid dasharray value: {part} (NaN or Infinity). Falling back to solid stroke."
                        )
                        return None
                    # Clamp negative values to zero
                    if value < 0:
                        logger.warning(f"Negative dasharray value: {value}. Clamping to 0.")
                        value = 0.0
                    dash_values.append(value)
        except (ValueError, TypeError) as e:
            logger.warning(
                f"Malformed dasharray pattern: {dasharray}. Error: {e}. Falling back to solid stroke."
            )
            return None

        # Handle empty result
        if not dash_values:
            return None

        # Remove zero-length values (they don't contribute to pattern)
        dash_values = [v for v in dash_values if v > 0]
        if not dash_values:
            logger.warning(
                "All dasharray values are zero or negative. Falling back to solid stroke."
            )
            return None

        # Handle single value - repeat it (per SVG spec: "10" → "10,10")
        if len(dash_values) == 1:
            dash_values = [dash_values[0], dash_values[0]]

        # Handle odd-length patterns - double the sequence (per SVG spec)
        if len(dash_values) % 2 == 1:
            dash_values = dash_values + dash_values

        # Calculate total pattern length
        total_length = sum(dash_values)

        # Parse offset
        parsed_offset = 0.0
        if dashoffset:
            try:
                parsed_offset = float(dashoffset.strip())
                if math.isnan(parsed_offset) or math.isinf(parsed_offset):
                    logger.warning(f"Invalid dashoffset value: {dashoffset}. Using 0.")
                    parsed_offset = 0.0
            except (ValueError, TypeError):
                logger.warning(f"Malformed dashoffset: {dashoffset}. Using 0.")
                parsed_offset = 0.0

        # Normalize offset to [0, total_length) using modulo
        if total_length > 0:
            parsed_offset = parsed_offset % total_length

        return cls(dash_lengths=tuple(dash_values), offset=parsed_offset, total_length=total_length)

    def generate_segments(self, path_length: float) -> Iterator[tuple[float, float]]:
        """
        Generate visible dash segment positions along a path.

        Args:
            path_length: Total arc-length of the path in pixels.

        Yields:
            (start_distance, end_distance) tuples for each visible dash segment.
            Segments are clamped to [0, path_length].

        Examples:
            >>> pattern = DashPattern.from_svg_attributes("10,5")
            >>> list(pattern.generate_segments(30))
            [(0.0, 10.0), (15.0, 25.0)]  # Two dashes

            >>> pattern = DashPattern.from_svg_attributes("10,5", "5")  # With offset
            >>> list(pattern.generate_segments(30))
            [(0.0, 5.0), (10.0, 20.0), (25.0, 30.0)]  # Offset shifts pattern

        Notes:
            - Uses generator pattern for memory efficiency (O(1) memory)
            - Gaps are not yielded (only visible dash segments)
            - Handles offset wrapping automatically
        """
        if path_length <= 0 or self.total_length <= 0:
            return

        # Start position accounting for offset
        current_distance = -self.offset
        pattern_index = 0

        # Iterate through pattern until we exceed path_length
        while current_distance < path_length:
            # Current segment length
            segment_length = self.dash_lengths[pattern_index]

            # Calculate segment end
            segment_end = current_distance + segment_length

            # Check if this is a dash (even indices) or gap (odd indices)
            is_dash = pattern_index % 2 == 0

            if is_dash:
                # Clamp to valid range [0, path_length]
                start = max(0.0, current_distance)
                end = min(path_length, segment_end)

                # Only yield if there's a visible segment
                if start < end:
                    yield (start, end)

            # Move to next segment
            current_distance = segment_end
            pattern_index = (pattern_index + 1) % len(self.dash_lengths)
