"""
Concrete style system for actual rendering parameters.

This module converts abstract styles to concrete rendering parameters based on
user preferences, device capabilities, and rendering context.
"""

from typing import Dict, Optional, Tuple, Union
from dataclasses import dataclass
from .abstract_style import AbstractStyle, FontFamily, FontSize
from pyWebLayout.style.alignment import Alignment as TextAlign
from .fonts import Font, FontWeight, FontStyle, TextDecoration


@dataclass(frozen=True)
class RenderingContext:
    """
    Context information for style resolution.
    Contains user preferences and device capabilities.
    """

    # User preferences
    base_font_size: int = 16  # Base font size in points
    font_scale_factor: float = 1.0  # Global font scaling
    preferred_serif_font: Optional[str] = None
    preferred_sans_serif_font: Optional[str] = None
    preferred_monospace_font: Optional[str] = None

    # Device/environment info
    dpi: int = 96  # Dots per inch
    available_width: Optional[int] = None  # Available width in pixels
    available_height: Optional[int] = None  # Available height in pixels

    # Accessibility preferences
    high_contrast: bool = False
    large_text: bool = False
    reduce_motion: bool = False

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


@dataclass(frozen=True)
class ConcreteStyle:
    """
    Concrete representation of text styling with actual rendering parameters.

    This contains the resolved font files, pixel sizes, actual colors, etc.
    that will be used for rendering. This is also hashable for efficient caching.
    """

    # Concrete font properties
    font_path: Optional[str] = None
    font_size: int = 16  # Always in points/pixels
    color: Tuple[int, int, int] = (0, 0, 0)  # Always RGB
    background_color: Optional[Tuple[int, int, int, int]] = None  # Always RGBA or None

    # Font attributes
    weight: FontWeight = FontWeight.NORMAL
    style: FontStyle = FontStyle.NORMAL
    decoration: TextDecoration = TextDecoration.NONE

    # Layout properties
    # None means "not specified": the page's default_alignment applies.
    text_align: Optional[TextAlign] = None
    line_height: float = 1.0  # Multiplier
    letter_spacing: float = 0.0  # In pixels
    word_spacing: float = 0.0  # In pixels
    word_spacing_min: float = 0.0  # Minimum word spacing in pixels
    word_spacing_max: float = 0.0  # Maximum word spacing in pixels

    # Language and locale
    language: str = "en-US"
    min_hyphenation_width: int = 64  # In pixels

    # Reference to source abstract style
    abstract_style: Optional[AbstractStyle] = None

    def create_font(self) -> Font:
        """Create a Font object from this concrete style."""
        return Font(
            font_path=self.font_path,
            font_size=self.font_size,
            colour=self.color,
            weight=self.weight,
            style=self.style,
            decoration=self.decoration,
            background=self.background_color,
            language=self.language,
            min_hyphenation_width=self.min_hyphenation_width
        )


class StyleResolver:
    """
    Resolves abstract styles to concrete styles based on rendering context.

    This class handles the conversion from semantic styling intent to actual
    rendering parameters, applying user preferences and device capabilities.
    """

    def __init__(self, context: RenderingContext):
        """
        Initialize the style resolver with a rendering context.

        Args:
            context: RenderingContext with user preferences and device info
        """
        self.context = context
        self._concrete_cache: Dict[AbstractStyle, ConcreteStyle] = {}

        # Font size mapping for semantic sizes
        self._semantic_font_sizes = {
            FontSize.XX_SMALL: 0.6,
            FontSize.X_SMALL: 0.75,
            FontSize.SMALL: 0.89,
            FontSize.MEDIUM: 1.0,
            FontSize.LARGE: 1.2,
            FontSize.X_LARGE: 1.5,
            FontSize.XX_LARGE: 2.0,
        }

        # Color name mapping
        self._color_names = {
            "black": (0, 0, 0),
            "white": (255, 255, 255),
            "red": (255, 0, 0),
            "green": (0, 128, 0),
            "blue": (0, 0, 255),
            "yellow": (255, 255, 0),
            "cyan": (0, 255, 255),
            "magenta": (255, 0, 255),
            "silver": (192, 192, 192),
            "gray": (128, 128, 128),
            "maroon": (128, 0, 0),
            "olive": (128, 128, 0),
            "lime": (0, 255, 0),
            "aqua": (0, 255, 255),
            "teal": (0, 128, 128),
            "navy": (0, 0, 128),
            "fuchsia": (255, 0, 255),
            "purple": (128, 0, 128),
        }

    def resolve_style(self, abstract_style: AbstractStyle) -> ConcreteStyle:
        """
        Resolve an abstract style to a concrete style.

        Args:
            abstract_style: AbstractStyle to resolve

        Returns:
            ConcreteStyle with concrete rendering parameters
        """
        # Check cache first
        if abstract_style in self._concrete_cache:
            return self._concrete_cache[abstract_style]

        # Resolve each property
        font_path = self._resolve_font_path(abstract_style.font_family)
        font_size = self._resolve_font_size(abstract_style.font_size)
        # Ensure font_size is always an int before using in arithmetic
        font_size = int(font_size)
        color = self._resolve_color(abstract_style.color)
        background_color = self._resolve_background_color(
            abstract_style.background_color)
        line_height = self._resolve_line_height(abstract_style.line_height)
        letter_spacing = self._resolve_letter_spacing(
            abstract_style.letter_spacing, font_size)
        word_spacing = self._resolve_word_spacing(
            abstract_style.word_spacing, font_size)
        word_spacing_min = self._resolve_word_spacing(
            abstract_style.word_spacing_min, font_size)
        word_spacing_max = self._resolve_word_spacing(
            abstract_style.word_spacing_max, font_size)
        min_hyphenation_width = max(int(font_size) * 4, 32)  # At least 32 pixels

        # Apply default logic for word spacing constraints
        if word_spacing_min == 0.0 and word_spacing_max == 0.0:
            # If no constraints specified, use base word_spacing as reference
            if word_spacing > 0.0:
                word_spacing_min = word_spacing
                word_spacing_max = word_spacing * 2
            else:
                # Default constraints when no word spacing is specified
                word_spacing_min = 2.0  # Minimum 2 pixels
                word_spacing_max = font_size * 0.5  # Maximum 50% of font size
        elif word_spacing_min == 0.0:
            # Only max specified, use base word_spacing or min default
            word_spacing_min = max(word_spacing, 2.0)
        elif word_spacing_max == 0.0:
            # Only min specified, use base word_spacing or reasonable multiple
            word_spacing_max = max(word_spacing, word_spacing_min * 2)

        # Create concrete style
        concrete_style = ConcreteStyle(
            font_path=font_path,
            font_size=font_size,
            color=color,
            background_color=background_color,
            weight=abstract_style.font_weight,
            style=abstract_style.font_style,
            decoration=abstract_style.text_decoration,
            text_align=abstract_style.text_align,
            line_height=line_height,
            letter_spacing=letter_spacing,
            word_spacing=word_spacing,
            word_spacing_min=word_spacing_min,
            word_spacing_max=word_spacing_max,
            language=abstract_style.language,
            min_hyphenation_width=min_hyphenation_width,
            abstract_style=abstract_style
        )

        # Cache and return
        self._concrete_cache[abstract_style] = concrete_style
        return concrete_style

    def _resolve_font_path(self, font_family: FontFamily) -> Optional[str]:
        """Resolve font family to actual font file path."""
        if font_family == FontFamily.SERIF:
            return self.context.preferred_serif_font
        elif font_family == FontFamily.SANS_SERIF:
            return self.context.preferred_sans_serif_font
        elif font_family == FontFamily.MONOSPACE:
            return self.context.preferred_monospace_font
        else:
            # For cursive and fantasy, fall back to sans-serif
            return self.context.preferred_sans_serif_font

    def _resolve_font_size(self, font_size: Union[FontSize, int]) -> int:
        """Resolve font size to actual pixel/point size."""
        # Ensure we handle FontSize enums properly
        if isinstance(font_size, FontSize):
            # Semantic size, convert to multiplier
            multiplier = self._semantic_font_sizes.get(font_size, 1.0)
            base_size = int(self.context.base_font_size * multiplier)
        elif isinstance(font_size, int):
            # Already a concrete size, apply scaling
            base_size = font_size
        else:
            # Fallback for any other type - try to convert to int
            try:
                base_size = int(font_size)
            except (ValueError, TypeError):
                # If conversion fails, use default
                base_size = self.context.base_font_size

        # Apply global font scaling
        final_size = int(base_size * self.context.font_scale_factor)

        # Apply accessibility adjustments
        if self.context.large_text:
            final_size = int(final_size * 1.2)

        # Ensure we always return an int, minimum 8pt font
        return max(int(final_size), 8)

    def _resolve_color(
            self, color: Union[str, Tuple[int, int, int]]) -> Tuple[int, int, int]:
        """Resolve color to RGB tuple."""
        if isinstance(color, tuple):
            return color

        if isinstance(color, str):
            # Check if it's a named color
            if color.lower() in self._color_names:
                base_color = self._color_names[color.lower()]
            elif color.startswith('#'):
                # Parse hex color
                try:
                    hex_color = color[1:]
                    if len(hex_color) == 3:
                        # Short hex format #RGB -> #RRGGBB
                        hex_color = ''.join(c * 2 for c in hex_color)
                    if len(hex_color) == 6:
                        r = int(hex_color[0:2], 16)
                        g = int(hex_color[2:4], 16)
                        b = int(hex_color[4:6], 16)
                        base_color = (r, g, b)
                    else:
                        base_color = (0, 0, 0)  # Fallback to black
                except ValueError:
                    base_color = (0, 0, 0)  # Fallback to black
            else:
                base_color = (0, 0, 0)  # Fallback to black

            # Apply high contrast if needed
            if self.context.high_contrast:
                # Simple high contrast: make dark colors black, light colors white
                r, g, b = base_color
                brightness = (r + g + b) / 3
                if brightness < 128:
                    base_color = (0, 0, 0)  # Black
                else:
                    base_color = (255, 255, 255)  # White

            return base_color

        return (0, 0, 0)  # Fallback to black

    def _resolve_background_color(self,
                                  bg_color: Optional[Union[str,
                                                           Tuple[int,
                                                                 int,
                                                                 int,
                                                                 int]]]) -> Optional[Tuple[int,
                                                                                           int,
                                                                                           int,
                                                                                           int]]:
        """Resolve background color to RGBA tuple or None."""
        if bg_color is None:
            return None

        if isinstance(bg_color, tuple):
            if len(bg_color) == 3:
                # RGB -> RGBA
                return bg_color + (255,)
            return bg_color

        if isinstance(bg_color, str):
            if bg_color.lower() == "transparent":
                return None

            # Resolve as RGB then add alpha
            rgb = self._resolve_color(bg_color)
            return rgb + (255,)

        return None

    def _resolve_line_height(self, line_height: Optional[Union[str, float]]) -> float:
        """Resolve line height to multiplier."""
        if line_height is None or line_height == "normal":
            return 1.2  # Default line height

        if isinstance(line_height, (int, float)):
            return float(line_height)

        if isinstance(line_height, str):
            try:
                return float(line_height)
            except ValueError:
                return 1.2  # Fallback

        return 1.2

    def _resolve_letter_spacing(
            self, letter_spacing: Optional[Union[str, float]], font_size: int) -> float:
        """Resolve letter spacing to pixels."""
        if letter_spacing is None or letter_spacing == "normal":
            return 0.0

        if isinstance(letter_spacing, (int, float)):
            return float(letter_spacing)

        if isinstance(letter_spacing, str):
            if letter_spacing.endswith("em"):
                try:
                    em_value = float(letter_spacing[:-2])
                    return em_value * font_size
                except ValueError:
                    return 0.0
            else:
                try:
                    return float(letter_spacing)
                except ValueError:
                    return 0.0

        return 0.0

    def _resolve_word_spacing(
            self, word_spacing: Optional[Union[str, float]], font_size: int) -> float:
        """Resolve word spacing to pixels."""
        if word_spacing is None or word_spacing == "normal":
            return 0.0

        if isinstance(word_spacing, (int, float)):
            return float(word_spacing)

        if isinstance(word_spacing, str):
            if word_spacing.endswith("em"):
                try:
                    em_value = float(word_spacing[:-2])
                    return em_value * font_size
                except ValueError:
                    return 0.0
            else:
                try:
                    return float(word_spacing)
                except ValueError:
                    return 0.0

        return 0.0

    def update_context(self, **kwargs):
        """
        Update the rendering context and clear cache.

        Args:
            **kwargs: Context properties to update
        """
        # Create new context with updates
        context_dict = {
            field.name: getattr(self.context, field.name)
            for field in self.context.__dataclass_fields__.values()
        }
        context_dict.update(kwargs)

        self.context = RenderingContext(**context_dict)

        # Clear cache since context changed
        self._concrete_cache.clear()

    def clear_cache(self):
        """Clear the concrete style cache."""
        self._concrete_cache.clear()

    def get_cache_size(self) -> int:
        """Get the number of cached concrete styles."""
        return len(self._concrete_cache)


class ConcreteStyleRegistry:
    """
    Registry for managing concrete styles with efficient caching.

    This registry manages the mapping between abstract and concrete styles,
    and provides efficient access to Font objects for rendering.
    """

    def __init__(self, resolver: StyleResolver):
        """
        Initialize the concrete style registry.

        Args:
            resolver: StyleResolver for converting abstract to concrete styles
        """
        self.resolver = resolver
        self._font_cache: Dict[ConcreteStyle, Font] = {}

    def get_concrete_style(self, abstract_style: AbstractStyle) -> ConcreteStyle:
        """
        Get a concrete style for an abstract style.

        Args:
            abstract_style: AbstractStyle to resolve

        Returns:
            ConcreteStyle with rendering parameters
        """
        return self.resolver.resolve_style(abstract_style)

    def get_font(self, abstract_style: AbstractStyle) -> Font:
        """
        Get a Font object for an abstract style.

        Args:
            abstract_style: AbstractStyle to get font for

        Returns:
            Font object ready for rendering
        """
        concrete_style = self.get_concrete_style(abstract_style)

        # Check font cache
        if concrete_style in self._font_cache:
            return self._font_cache[concrete_style]

        # Create and cache font
        font = concrete_style.create_font()
        self._font_cache[concrete_style] = font

        return font

    def clear_caches(self):
        """Clear all caches."""
        self.resolver.clear_cache()
        self._font_cache.clear()

    def get_cache_stats(self) -> Dict[str, int]:
        """Get cache statistics."""
        return {
            "concrete_styles": self.resolver.get_cache_size(),
            "fonts": len(self._font_cache)
        }
