"""
Abstract style system for storing document styling intent.

This module defines styles in terms of semantic meaning rather than concrete
rendering parameters, allowing for flexible interpretation by different
rendering systems and user preferences.
"""

from .alignment import Alignment
from typing import Dict, Optional, Tuple, Union
from dataclasses import dataclass
from enum import Enum
from .fonts import FontWeight, FontStyle, TextDecoration


class FontFamily(Enum):
    """Semantic font family categories"""
    SERIF = "serif"
    SANS_SERIF = "sans-serif"
    MONOSPACE = "monospace"
    CURSIVE = "cursive"
    FANTASY = "fantasy"


class FontSize(Enum):
    """Semantic font sizes"""
    XX_SMALL = "xx-small"
    X_SMALL = "x-small"
    SMALL = "small"
    MEDIUM = "medium"
    LARGE = "large"
    X_LARGE = "x-large"
    XX_LARGE = "xx-large"

    # Allow numeric values as well
    @classmethod
    def from_value(cls, value: Union[str, int, float]) -> Union['FontSize', int]:
        """Convert a value to FontSize enum or return numeric value"""
        if isinstance(value, (int, float)):
            return int(value)
        if isinstance(value, str):
            try:
                return cls(value)
            except ValueError:
                # Try to parse as number
                try:
                    return int(float(value))
                except ValueError:
                    return cls.MEDIUM
        return cls.MEDIUM


# Import Alignment from the centralized location

# Use Alignment for text alignment
TextAlign = Alignment


@dataclass(frozen=True)
class AbstractStyle:
    """
    Abstract representation of text styling that captures semantic intent
    rather than concrete rendering parameters.

    This allows the same document to be rendered differently based on
    user preferences, device capabilities, or accessibility requirements.

    Being frozen=True makes this class hashable and immutable, which is
    perfect for use as dictionary keys and preventing accidental modification.
    """

    # Font properties (semantic)
    font_family: FontFamily = FontFamily.SERIF
    font_size: Union[FontSize, int] = FontSize.MEDIUM
    font_weight: FontWeight = FontWeight.NORMAL
    font_style: FontStyle = FontStyle.NORMAL
    text_decoration: TextDecoration = TextDecoration.NONE

    # Color (as semantic names or RGB)
    color: Union[str, Tuple[int, int, int]] = "black"
    background_color: Optional[Union[str, Tuple[int, int, int, int]]] = None

    # Text properties
    # None means "not specified": the page's default_alignment applies.
    text_align: Optional[TextAlign] = None
    line_height: Optional[Union[str, float]] = None  # "normal", "1.2", 1.5, etc.
    letter_spacing: Optional[Union[str, float]] = None  # "normal", "0.1em", etc.
    word_spacing: Optional[Union[str, float]] = None
    word_spacing_min: Optional[Union[str, float]] = None  # Minimum allowed word spacing
    word_spacing_max: Optional[Union[str, float]] = None  # Maximum allowed word spacing

    # Language and locale
    language: str = "en-US"

    # Hierarchy properties
    parent_style_id: Optional[str] = None

    def __post_init__(self):
        """Validate and normalize values after creation"""
        # Normalize font_size if it's a string that could be a number
        if isinstance(self.font_size, str):
            try:
                object.__setattr__(self, 'font_size', int(float(self.font_size)))
            except ValueError:
                # Keep as is if it's a semantic size name
                pass

    def __hash__(self) -> int:
        """
        Custom hash implementation to ensure consistent hashing.

        Since this is a frozen dataclass, it should be hashable by default,
        but we provide a custom implementation to ensure all fields are
        properly considered and to handle the Union types correctly.
        """
        # Convert all values to hashable forms
        hashable_values = (
            self.font_family,
            self.font_size if isinstance(self.font_size, int) else self.font_size,
            self.font_weight,
            self.font_style,
            self.text_decoration,
            self.color if isinstance(self.color, (str, tuple)) else str(self.color),
            self.background_color,
            self.text_align,
            self.line_height,
            self.letter_spacing,
            self.word_spacing,
            self.word_spacing_min,
            self.word_spacing_max,
            self.language,
            self.parent_style_id
        )

        return hash(hashable_values)

    def merge_with(self, other: 'AbstractStyle') -> 'AbstractStyle':
        """
        Create a new AbstractStyle by merging this one with another.
        The other style's properties take precedence.

        Args:
            other: AbstractStyle to merge with this one

        Returns:
            New AbstractStyle with merged values
        """
        # Get all fields from both styles
        current_dict = {
            field.name: getattr(self, field.name)
            for field in self.__dataclass_fields__.values()
        }

        other_dict = {
            field.name: getattr(other, field.name)
            for field in other.__dataclass_fields__.values()
            if getattr(other, field.name) != field.default
        }

        # Merge dictionaries (other takes precedence)
        merged_dict = current_dict.copy()
        merged_dict.update(other_dict)

        return AbstractStyle(**merged_dict)

    def with_modifications(self, **kwargs) -> 'AbstractStyle':
        """
        Create a new AbstractStyle with specified modifications.

        Args:
            **kwargs: Properties to modify

        Returns:
            New AbstractStyle with modifications applied
        """
        current_dict = {
            field.name: getattr(self, field.name)
            for field in self.__dataclass_fields__.values()
        }

        current_dict.update(kwargs)
        return AbstractStyle(**current_dict)


class AbstractStyleRegistry:
    """
    Registry for managing abstract document styles.

    This registry stores the semantic styling intent and provides
    deduplication and inheritance capabilities using hashable AbstractStyle objects.
    """

    def __init__(self):
        """Initialize an empty abstract style registry."""
        self._styles: Dict[str, AbstractStyle] = {}
        # Reverse mapping using hashable styles
        self._style_to_id: Dict[AbstractStyle, str] = {}
        self._next_id = 1

        # Create and register the default style
        self._default_style = self._create_default_style()

    def _create_default_style(self) -> AbstractStyle:
        """Create the default document style."""
        default_style = AbstractStyle()
        style_id = "default"
        self._styles[style_id] = default_style
        self._style_to_id[default_style] = style_id
        return default_style

    @property
    def default_style(self) -> AbstractStyle:
        """Get the default style for the document."""
        return self._default_style

    def _generate_style_id(self) -> str:
        """Generate a unique style ID."""
        style_id = f"abstract_style_{self._next_id}"
        self._next_id += 1
        return style_id

    def get_style_id(self, style: AbstractStyle) -> Optional[str]:
        """
        Get the ID for a given style if it exists in the registry.

        Args:
            style: AbstractStyle to find

        Returns:
            Style ID if found, None otherwise
        """
        return self._style_to_id.get(style)

    def register_style(
            self,
            style: AbstractStyle,
            style_id: Optional[str] = None) -> str:
        """
        Register a style in the registry.

        Args:
            style: AbstractStyle to register
            style_id: Optional style ID. If None, one will be generated

        Returns:
            The style ID
        """
        # Check if style already exists
        existing_id = self.get_style_id(style)
        if existing_id is not None:
            return existing_id

        if style_id is None:
            style_id = self._generate_style_id()

        self._styles[style_id] = style
        self._style_to_id[style] = style_id
        return style_id

    def get_or_create_style(self,
                            style: Optional[AbstractStyle] = None,
                            parent_id: Optional[str] = None,
                            **kwargs) -> Tuple[str, AbstractStyle]:
        """
        Get an existing style or create a new one.

        Args:
            style: AbstractStyle object. If None, created from kwargs
            parent_id: Optional parent style ID
            **kwargs: Individual style properties (used if style is None)

        Returns:
            Tuple of (style_id, AbstractStyle)
        """
        # Create style object if not provided
        if style is None:
            # Filter out None values from kwargs
            filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None}
            if parent_id:
                filtered_kwargs['parent_style_id'] = parent_id
            style = AbstractStyle(**filtered_kwargs)

        # Check if we already have this style (using hashable property)
        existing_id = self.get_style_id(style)
        if existing_id is not None:
            return existing_id, style

        # Create new style
        style_id = self.register_style(style)
        return style_id, style

    def get_style_by_id(self, style_id: str) -> Optional[AbstractStyle]:
        """Get a style by its ID."""
        return self._styles.get(style_id)

    def create_derived_style(self, base_style_id: str, **
                             modifications) -> Tuple[str, AbstractStyle]:
        """
        Create a new style derived from a base style.

        Args:
            base_style_id: ID of the base style
            **modifications: Properties to modify

        Returns:
            Tuple of (new_style_id, new_AbstractStyle)
        """
        base_style = self.get_style_by_id(base_style_id)
        if base_style is None:
            raise ValueError(f"Base style '{base_style_id}' not found")

        # Create derived style
        derived_style = base_style.with_modifications(**modifications)
        return self.get_or_create_style(derived_style)

    def resolve_effective_style(self, style_id: str) -> AbstractStyle:
        """
        Resolve the effective style including inheritance.

        Args:
            style_id: Style ID to resolve

        Returns:
            Effective AbstractStyle with inheritance applied
        """
        style = self.get_style_by_id(style_id)
        if style is None:
            return self._default_style

        if style.parent_style_id is None:
            return style

        # Recursively resolve parent styles
        parent_style = self.resolve_effective_style(style.parent_style_id)
        return parent_style.merge_with(style)

    def get_all_styles(self) -> Dict[str, AbstractStyle]:
        """Get all registered styles."""
        return self._styles.copy()

    def get_style_count(self) -> int:
        """Get the number of registered styles."""
        return len(self._styles)
