"""
SVG style system for visual attributes.

Styles define how shapes are rendered (fill, stroke, opacity, etc.).
Styles can be inherited from parent elements following CSS cascade rules.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from .colors import Color

if TYPE_CHECKING:
    from .dash import DashPattern


@dataclass
class Style:
    """
    Visual styling attributes for SVG elements.

    Attributes:
        fill: Fill color (None = no fill).
        fill_opacity: Fill opacity 0.0-1.0 (default 1.0).
        stroke: Stroke color (None = no stroke).
        stroke_opacity: Stroke opacity 0.0-1.0 (default 1.0).
        stroke_width: Stroke width in user units (default 1.0).
        opacity: Overall opacity 0.0-1.0 (default 1.0).
        display: Whether element is rendered (default True).
    """

    fill: Color | None = field(default_factory=lambda: Color(0, 0, 0, 255))  # Default black fill
    fill_opacity: float = 1.0
    stroke: Color | None = None
    stroke_opacity: float = 1.0
    stroke_width: float = 1.0
    opacity: float = 1.0
    display: bool = True
    stroke_dasharray: str | None = None
    stroke_dashoffset: str | None = None
    _parsed_dash_pattern: DashPattern | None = field(default=None, init=False, repr=False)

    def __post_init__(self) -> None:
        """Validate style parameters."""
        if not (0.0 <= self.fill_opacity <= 1.0):
            raise ValueError(f"fill_opacity must be in [0, 1], got {self.fill_opacity}")
        if not (0.0 <= self.stroke_opacity <= 1.0):
            raise ValueError(f"stroke_opacity must be in [0, 1], got {self.stroke_opacity}")
        if not (0.0 <= self.opacity <= 1.0):
            raise ValueError(f"opacity must be in [0, 1], got {self.opacity}")
        if self.stroke_width < 0:
            raise ValueError(f"stroke_width must be non-negative, got {self.stroke_width}")

    def get_effective_fill_opacity(self) -> float:
        """
        Calculate effective fill opacity (combines fill_opacity and overall opacity).

        Returns:
            Effective opacity value 0.0-1.0.
        """
        return self.fill_opacity * self.opacity

    def get_effective_stroke_opacity(self) -> float:
        """
        Calculate effective stroke opacity (combines stroke_opacity and overall opacity).

        Returns:
            Effective opacity value 0.0-1.0.
        """
        return self.stroke_opacity * self.opacity

    def merge_with_parent(self, parent: Style | None) -> Style:
        """
        Create a new style by inheriting from parent style.

        Properties that are explicitly set in this style override parent values.
        Properties not set inherit from parent (CSS cascade).

        Args:
            parent: Parent style to inherit from (None if no parent).

        Returns:
            New Style with inherited values.
        """
        if parent is None:
            return self

        # Create new style with inherited values
        # Note: For fill/stroke, we need to handle both:
        #   - Not specified (inherit from parent)
        #   - Explicitly set to None (don't inherit)
        # However, we can't distinguish these cases, so we use:
        # - If child has a value (including None), use child's value
        # - If child doesn't have a value, inherit from parent
        # This means "fill='none'" will properly override parent fill
        return Style(
            fill=self.fill,  # Don't override None - it's an explicit value
            fill_opacity=self.fill_opacity,
            stroke=self.stroke,  # Don't override None - it's an explicit value
            stroke_opacity=self.stroke_opacity,
            stroke_width=self.stroke_width,
            opacity=self.opacity * parent.opacity,  # Opacity multiplies
            display=self.display,
            stroke_dasharray=self.stroke_dasharray,
            stroke_dashoffset=self.stroke_dashoffset,
        )

    def get_dash_pattern(self) -> DashPattern | None:
        """
        Get parsed dash pattern, parsing and caching on first access.

        Returns:
            DashPattern instance or None for solid strokes.

        Notes:
            - Result is cached in _parsed_dash_pattern for performance
            - Returns None if stroke_dasharray is not set or is "none"
            - Malformed patterns return None (solid stroke fallback)
        """
        # Return cached value if available
        if hasattr(self, "_parsed_dash_pattern") and self._parsed_dash_pattern is not None:
            return self._parsed_dash_pattern

        # Parse on first access
        from .dash import DashPattern

        if self.stroke_dasharray:
            pattern = DashPattern.from_svg_attributes(self.stroke_dasharray, self.stroke_dashoffset)
            # Cache the result (even if None)
            object.__setattr__(self, "_parsed_dash_pattern", pattern)
            return pattern

        return None

    def has_dashed_stroke(self) -> bool:
        """
        Quick check for dash pattern presence without parsing.

        Returns:
            True if stroke-dasharray is set and not "none".
        """
        return bool(self.stroke_dasharray and self.stroke_dasharray.strip().lower() != "none")


def parse_style_attributes(attributes: dict[str, str], parent_style: Style | None = None) -> Style:
    """
    Parse SVG element attributes to create a Style object.

    Extracts style-related attributes (fill, stroke, opacity, etc.) from
    the element's attribute dictionary and creates a Style instance.

    Args:
        attributes: Dictionary of SVG element attributes.
        parent_style: Parent element's style for inheritance (None if no parent).

    Returns:
        Style object with parsed attributes.

    Example:
        >>> attrs = {'fill': '#FF0000', 'stroke': 'blue', 'stroke-width': '2'}
        >>> style = parse_style_attributes(attrs)
        >>> style.fill.r
        255
    """
    # Start with defaults or parent values
    if parent_style:
        fill = parent_style.fill
        stroke = parent_style.stroke
        stroke_width = parent_style.stroke_width
    else:
        fill = Color(0, 0, 0, 255)  # Default black fill
        stroke = None
        stroke_width = 1.0

    fill_opacity = 1.0
    stroke_opacity = 1.0
    opacity = 1.0
    display = True
    stroke_dasharray: str | None = None
    stroke_dashoffset: str | None = None

    # Parse fill
    if "fill" in attributes:
        fill_value = attributes["fill"].strip()
        if fill_value.lower() == "none":
            fill = None
        else:
            try:
                fill = Color.parse(fill_value)
            except ValueError:
                # Invalid color - use default
                pass

    # Parse fill-opacity
    if "fill-opacity" in attributes:
        try:
            fill_opacity = float(attributes["fill-opacity"])
            fill_opacity = max(0.0, min(1.0, fill_opacity))  # Clamp to [0, 1]
        except ValueError:
            pass

    # Parse stroke
    if "stroke" in attributes:
        stroke_value = attributes["stroke"].strip()
        if stroke_value.lower() == "none":
            stroke = None
        else:
            try:
                stroke = Color.parse(stroke_value)
            except ValueError:
                pass

    # Parse stroke-opacity
    if "stroke-opacity" in attributes:
        try:
            stroke_opacity = float(attributes["stroke-opacity"])
            stroke_opacity = max(0.0, min(1.0, stroke_opacity))
        except ValueError:
            pass

    # Parse stroke-width
    if "stroke-width" in attributes:
        try:
            stroke_width = float(attributes["stroke-width"])
            stroke_width = max(0.0, stroke_width)
        except ValueError:
            pass

    # Parse opacity
    if "opacity" in attributes:
        try:
            opacity = float(attributes["opacity"])
            opacity = max(0.0, min(1.0, opacity))
        except ValueError:
            pass

    # Parse display
    if "display" in attributes:
        display = attributes["display"].lower() != "none"

    # Parse stroke-dasharray
    if "stroke-dasharray" in attributes:
        stroke_dasharray = attributes["stroke-dasharray"].strip()
        if stroke_dasharray.lower() == "none":
            stroke_dasharray = None

    # Parse stroke-dashoffset
    if "stroke-dashoffset" in attributes:
        stroke_dashoffset = attributes["stroke-dashoffset"].strip()

    # Parse style attribute (CSS style string)
    if "style" in attributes:
        style_string = attributes["style"]
        style_dict = parse_css_style_string(style_string)

        # Apply style properties (override attribute values)
        if "fill" in style_dict:
            if style_dict["fill"].lower() == "none":
                fill = None
            else:
                try:
                    fill = Color.parse(style_dict["fill"])
                except ValueError:
                    pass

        if "stroke" in style_dict:
            if style_dict["stroke"].lower() == "none":
                stroke = None
            else:
                try:
                    stroke = Color.parse(style_dict["stroke"])
                except ValueError:
                    pass

        if "stroke-width" in style_dict:
            try:
                stroke_width = float(style_dict["stroke-width"])
            except ValueError:
                pass

        if "opacity" in style_dict:
            try:
                opacity = float(style_dict["opacity"])
                opacity = max(0.0, min(1.0, opacity))
            except ValueError:
                pass

        if "stroke-dasharray" in style_dict:
            val = style_dict["stroke-dasharray"].strip()
            stroke_dasharray = val if val.lower() != "none" else None

        if "stroke-dashoffset" in style_dict:
            stroke_dashoffset = style_dict["stroke-dashoffset"].strip()

    style = Style(
        fill=fill,
        fill_opacity=fill_opacity,
        stroke=stroke,
        stroke_opacity=stroke_opacity,
        stroke_width=stroke_width,
        opacity=opacity,
        display=display,
        stroke_dasharray=stroke_dasharray,
        stroke_dashoffset=stroke_dashoffset,
    )

    # Note: We don't call merge_with_parent here because we already handled
    # inheritance above by starting with parent values and overriding with
    # element attributes. Calling merge_with_parent would undo explicit
    # "none" values by re-inheriting from parent.

    return style


def parse_css_style_string(style_string: str) -> dict[str, str]:
    """
    Parse CSS-style attribute string into dictionary.

    Args:
        style_string: CSS style string (e.g., "fill: red; stroke: blue").

    Returns:
        Dictionary mapping property names to values.

    Example:
        >>> parse_css_style_string("fill: red; stroke-width: 2")
        {'fill': 'red', 'stroke-width': '2'}
    """
    result = {}

    # Split by semicolons
    declarations = style_string.split(";")

    for decl in declarations:
        decl = decl.strip()
        if not decl:
            continue

        # Split property and value by colon
        if ":" in decl:
            prop, value = decl.split(":", 1)
            prop = prop.strip().lower()
            value = value.strip()
            result[prop] = value

    return result
