"""
DynamicPage implementation for pyWebLayout.

A DynamicPage is a page that dynamically sizes itself based on content and constraints.
Unlike a regular Page with fixed size, a DynamicPage measures its content first and
then layouts within the allocated space.

Use cases:
- Table cells that need to fit content
- Containers that should grow with content
- Responsive layouts that adapt to constraints
"""

from typing import Tuple, Optional, List
from dataclasses import dataclass
import numpy as np
from PIL import Image

from pyWebLayout.concrete.page import Page
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.core.base import Renderable


@dataclass
class SizeConstraints:
    """Size constraints for dynamic layout."""
    min_width: Optional[int] = None
    max_width: Optional[int] = None
    min_height: Optional[int] = None
    max_height: Optional[int] = None
    # Note: Hyphenation threshold is controlled by Font.min_hyphenation_width
    # Don't duplicate that logic here


class DynamicPage(Page):
    """
    A page that dynamically sizes itself based on content and constraints.

    The layout process has two phases:
    1. Measurement: Calculate intrinsic size needed for content
    2. Layout: Position content within allocated size

    This allows containers (like tables) to optimize space allocation before rendering.
    """

    def __init__(self,
                 constraints: Optional[SizeConstraints] = None,
                 style: Optional[PageStyle] = None):
        """
        Initialize a dynamic page.

        Args:
            constraints: Optional size constraints (min/max width/height)
            style: The PageStyle defining borders, spacing, and appearance
        """
        # Start with zero size - will be determined during measurement/layout
        super().__init__(size=(0, 0), style=style)
        self._constraints = constraints if constraints is not None else SizeConstraints()

        # Measurement state
        self._is_measured = False
        self._intrinsic_size: Optional[Tuple[int, int]] = None
        self._min_width_cache: Optional[int] = None
        self._preferred_width_cache: Optional[int] = None
        self._content_height_cache: Optional[int] = None

        # Pagination state
        self._render_offset = 0  # For partial rendering (pagination)
        self._is_laid_out = False

    @property
    def constraints(self) -> SizeConstraints:
        """Get the size constraints for this page."""
        return self._constraints

    def measure(self, available_width: Optional[int] = None) -> Tuple[int, int]:
        """
        Measure the intrinsic size needed for content.

        This walks through all children and calculates how much space they need.
        The measurement respects constraints (min/max width/height).

        Args:
            available_width: Optional width constraint for wrapping content

        Returns:
            Tuple of (width, height) needed
        """
        if self._is_measured and self._intrinsic_size is not None:
            return self._intrinsic_size

        # Apply constraints to available width
        if available_width is not None:
            if self._constraints.max_width is not None:
                available_width = min(available_width, self._constraints.max_width)
            if self._constraints.min_width is not None:
                available_width = max(available_width, self._constraints.min_width)

        # Measure content
        # For now, walk through children and sum their sizes
        total_width = 0
        total_height = 0

        for child in self._children:
            if hasattr(child, 'measure'):
                # Child is also dynamic - ask it to measure
                child_size = child.measure(available_width)
                child_width, child_height = child_size
            else:
                # Child has fixed size
                child_width = child.size[0] if hasattr(child, 'size') else 0
                child_height = child.size[1] if hasattr(child, 'size') else 0

            total_width = max(total_width, child_width)
            total_height += child_height

        # Add page padding/borders
        total_width += self._style.total_horizontal_padding + self._style.total_border_width
        total_height += self._style.total_vertical_padding + self._style.total_border_width

        # Apply constraints
        if self._constraints.min_width is not None:
            total_width = max(total_width, self._constraints.min_width)
        if self._constraints.max_width is not None:
            total_width = min(total_width, self._constraints.max_width)
        if self._constraints.min_height is not None:
            total_height = max(total_height, self._constraints.min_height)
        if self._constraints.max_height is not None:
            total_height = min(total_height, self._constraints.max_height)

        self._intrinsic_size = (total_width, total_height)
        self._is_measured = True

        return self._intrinsic_size

    def get_min_width(self) -> int:
        """
        Get minimum width needed to render content.

        This finds the widest word/element that cannot be broken,
        using Font.min_hyphenation_width for hyphenation control.

        Returns:
            Minimum width in pixels
        """
        # Check cache
        if self._min_width_cache is not None:
            return self._min_width_cache

        # Calculate minimum width based on content
        from pyWebLayout.concrete.text import Line, Text

        min_width = 0

        # Walk through children and find longest unbreakable segment
        for child in self._children:
            if isinstance(child, Line):
                # Check all words in the line
                # Font's min_hyphenation_width already controls breaking
                for text_obj in getattr(child, '_text_objects', []):
                    if isinstance(text_obj, Text) and hasattr(text_obj, '_text'):
                        word_text = text_obj._text
                        # Text stores font in _style, not _font
                        font = getattr(text_obj, '_style', None)

                        if font:
                            # Just measure the word - Font handles hyphenation rules
                            word_width = int(font.font.getlength(word_text))
                            min_width = max(min_width, word_width)
            elif hasattr(child, 'get_min_width'):
                # Child supports min width calculation
                child_min = child.get_min_width()
                min_width = max(min_width, child_min)
            elif hasattr(child, 'size'):
                # Use actual width
                min_width = max(min_width, child.size[0])

        # Add padding/borders
        min_width += self._style.total_horizontal_padding + self._style.total_border_width

        # Apply minimum constraint
        if self._constraints.min_width is not None:
            min_width = max(min_width, self._constraints.min_width)

        self._min_width_cache = min_width
        return min_width

    def get_preferred_width(self) -> int:
        """
        Get preferred width (no wrapping).

        This returns the width needed to render all content without any
        line wrapping.

        Returns:
            Preferred width in pixels
        """
        # Check cache
        if self._preferred_width_cache is not None:
            return self._preferred_width_cache

        # Calculate preferred width (no wrapping)
        from pyWebLayout.concrete.text import Line

        pref_width = 0

        for child in self._children:
            if isinstance(child, Line):
                # Get line width without wrapping (including spacing between words)
                text_objects = getattr(child, '_text_objects', [])
                if text_objects:
                    line_width = 0
                    for i, text_obj in enumerate(text_objects):
                        if hasattr(text_obj, '_text') and hasattr(text_obj, '_style'):
                            # Text stores font in _style, not _font
                            word_width = text_obj._style.font.getlength(text_obj._text)
                            line_width += word_width

                            # Add spacing after word (except last word)
                            if i < len(text_objects) - 1:
                                # Get spacing from Line if available, otherwise use default
                                spacing = getattr(child, '_spacing', (3, 6))
                                # Use minimum spacing for preferred width calculation
                                line_width += spacing[0] if isinstance(spacing, tuple) else 3

                    pref_width = max(pref_width, line_width)
            elif hasattr(child, 'get_preferred_width'):
                child_pref = child.get_preferred_width()
                pref_width = max(pref_width, child_pref)
            elif hasattr(child, 'size'):
                # Use actual size
                pref_width = max(pref_width, child.size[0])

        # Add padding/borders
        pref_width += self._style.total_horizontal_padding + self._style.total_border_width

        # Apply constraints
        if self._constraints.max_width is not None:
            pref_width = min(pref_width, self._constraints.max_width)
        if self._constraints.min_width is not None:
            pref_width = max(pref_width, self._constraints.min_width)

        self._preferred_width_cache = pref_width
        return pref_width

    def measure_content_height(self) -> int:
        """
        Measure total height needed to render all content.

        This is used for pagination to know how much content remains.

        Returns:
            Total height in pixels
        """
        # Check cache
        if self._content_height_cache is not None:
            return self._content_height_cache

        total_height = 0

        for child in self._children:
            if hasattr(child, 'measure_content_height'):
                child_height = child.measure_content_height()
            elif hasattr(child, 'size'):
                child_height = child.size[1]
            else:
                child_height = 0

            total_height += child_height

        # Add padding/borders
        total_height += self._style.total_vertical_padding + self._style.total_border_width

        self._content_height_cache = total_height
        return total_height

    def layout(self, size: Tuple[int, int]):
        """
        Layout content within the given size.

        This is called after measurement to position children within
        the allocated space.

        Args:
            size: The final size allocated to this page (width, height)
        """
        # Set the page size
        self._size = size

        # Position children sequentially
        # Use the same logic as Page but now we know our final size
        content_x = self._style.border_width + self._style.padding_left
        content_y = self._style.border_width + self._style.padding_top

        self._current_y_offset = content_y
        self._is_first_line = True

        # Children position themselves, we just track y_offset
        # The actual positioning happens when children render

        self._is_laid_out = True
        self._dirty = True  # Mark for re-render

    def render(self) -> Image.Image:
        """
        Render the page with all its children.

        If not yet measured/laid out, use intrinsic sizing.

        Returns:
            PIL Image containing the rendered page
        """
        # Ensure we have a valid size
        if self._size[0] == 0 or self._size[1] == 0:
            if not self._is_measured:
                # Auto-measure with no constraints
                self.measure()

            if self._intrinsic_size:
                self._size = self._intrinsic_size
            else:
                # Fallback to minimum size
                self._size = (100, 100)

        # Use parent's render implementation
        return super().render()

    # Pagination Support
    # ------------------

    def render_partial(self, available_height: int) -> int:
        """
        Render as much content as fits in available_height.

        This is used for pagination when a page needs to be split across
        multiple output pages.

        Args:
            available_height: Height available on current page

        Returns:
            Amount of content rendered (in pixels)
        """
        # Calculate how many children fit in available height
        rendered_height = 0
        content_start_y = self._style.border_width + self._style.padding_top

        for i, child in enumerate(self._children):
            # Skip already rendered children
            if rendered_height < self._render_offset:
                if hasattr(child, 'size'):
                    rendered_height += child.size[1]
                continue

            # Check if this child fits
            child_height = child.size[1] if hasattr(child, 'size') else 0

            if rendered_height + child_height <= available_height:
                # Child fits - render it
                if hasattr(child, 'render'):
                    child.render()
                rendered_height += child_height
            else:
                # No more space
                break

        # Update render offset for next call
        self._render_offset = rendered_height

        return rendered_height

    def has_more_content(self) -> bool:
        """
        Check if there's unrendered content remaining.

        Returns:
            True if more content needs to be rendered
        """
        total_height = self.measure_content_height()
        return self._render_offset < total_height

    def reset_pagination(self):
        """Reset pagination to render from beginning."""
        self._render_offset = 0

    def invalidate_caches(self):
        """Invalidate all measurement caches (call when children change)."""
        self._is_measured = False
        self._intrinsic_size = None
        self._min_width_cache = None
        self._preferred_width_cache = None
        self._content_height_cache = None
        self._is_laid_out = False

    def add_child(self, child: Renderable) -> 'DynamicPage':
        """
        Add a child and invalidate caches.

        Args:
            child: The renderable object to add

        Returns:
            Self for method chaining
        """
        super().add_child(child)
        self.invalidate_caches()
        return self

    def clear_children(self) -> 'DynamicPage':
        """
        Remove all children and invalidate caches.

        Returns:
            Self for method chaining
        """
        super().clear_children()
        self.invalidate_caches()
        return self
