"""
Enhanced ereader layout system with position tracking, font scaling, and multi-page support.

This module provides the core infrastructure for building high-performance ereader applications
with features like:
- Precise position tracking tied to abstract document structure
- Font scaling support
- Bidirectional page rendering (forward/backward)
- Chapter navigation based on HTML headings
- Multi-process page buffering
- Sub-second page rendering performance
"""

from __future__ import annotations
from dataclasses import dataclass, asdict
from typing import List, Dict, Tuple, Optional, Any

from pyWebLayout.abstract.block import Block, Paragraph, Heading, HeadingLevel, Table, HList, Image
from pyWebLayout.abstract.inline import Word
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.text import Text
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.style.fonts import BundledFont, get_bundled_font_path, FontWeight, FontStyle
from pyWebLayout.layout.document_layouter import paragraph_layouter, image_layouter


@dataclass
class RenderingPosition:
    """
    Complete state for resuming rendering at any point in a document.
    Position is tied to abstract document structure for stability across font changes.
    """
    chapter_index: int = 0                      # Which chapter (based on headings)
    block_index: int = 0                       # Which block within chapter
    # Which word within block (for paragraphs)
    word_index: int = 0
    table_row: int = 0                         # Which row for tables
    table_col: int = 0                         # Which column for tables
    list_item_index: int = 0                   # Which item for lists
    remaining_pretext: Optional[str] = None     # Hyphenated word continuation
    page_y_offset: int = 0                     # Vertical position on page

    def to_dict(self) -> Dict[str, Any]:
        """Serialize position for saving to file/database"""
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'RenderingPosition':
        """Deserialize position from saved state"""
        return cls(**data)

    def copy(self) -> 'RenderingPosition':
        """Create a copy of this position"""
        return RenderingPosition(**asdict(self))

    def __eq__(self, other) -> bool:
        """Check if two positions are equal"""
        if not isinstance(other, RenderingPosition):
            return False
        return asdict(self) == asdict(other)

    def __hash__(self) -> int:
        """Make position hashable for use as dict key"""
        return hash(tuple(asdict(self).values()))


class ChapterInfo:
    """Information about a chapter/section in the document"""

    def __init__(
            self,
            title: str,
            level: HeadingLevel,
            position: RenderingPosition,
            block_index: int):
        self.title = title
        self.level = level
        self.position = position
        self.block_index = block_index


class ChapterNavigator:
    """
    Handles chapter/section navigation based on HTML heading structure (H1-H6).
    Builds a table of contents and provides navigation capabilities.
    """

    def __init__(self, blocks: List[Block]):
        self.blocks = blocks
        self.chapters: List[ChapterInfo] = []
        self._build_chapter_map()

    def _build_chapter_map(self):
        """Scan blocks for headings and build chapter navigation map"""
        current_chapter_index = 0

        # Check if first block is a cover image and add it to TOC
        if self.blocks and isinstance(self.blocks[0], Image):
            cover_position = RenderingPosition(
                chapter_index=0,
                block_index=0,
                word_index=0,
                table_row=0,
                table_col=0,
                list_item_index=0
            )

            cover_info = ChapterInfo(
                title="Cover",
                level=HeadingLevel.H1,  # Treat as top-level entry
                position=cover_position,
                block_index=0
            )

            self.chapters.append(cover_info)

        for block_index, block in enumerate(self.blocks):
            if isinstance(block, Heading):
                # Create position for this heading
                position = RenderingPosition(
                    chapter_index=current_chapter_index,
                    block_index=block_index,  # Use actual block index
                    word_index=0,
                    table_row=0,
                    table_col=0,
                    list_item_index=0
                )

                # Extract heading text
                heading_text = self._extract_heading_text(block)

                chapter_info = ChapterInfo(
                    title=heading_text,
                    level=block.level,
                    position=position,
                    block_index=block_index
                )

                self.chapters.append(chapter_info)

                # Only increment chapter index for top-level headings (H1)
                if block.level == HeadingLevel.H1:
                    current_chapter_index += 1

    def _extract_heading_text(self, heading: Heading) -> str:
        """Extract text content from a heading block"""
        words = []
        for position, word in heading.words_iter():
            if isinstance(word, Word):
                words.append(word.text)
        return " ".join(words)

    def get_table_of_contents(
            self) -> List[Tuple[str, HeadingLevel, RenderingPosition]]:
        """Generate table of contents from heading structure"""
        return [(chapter.title, chapter.level, chapter.position)
                for chapter in self.chapters]

    def get_chapter_position(self, chapter_title: str) -> Optional[RenderingPosition]:
        """Get rendering position for a chapter by title"""
        for chapter in self.chapters:
            if chapter.title.lower() == chapter_title.lower():
                return chapter.position
        return None

    def get_current_chapter(self, position: RenderingPosition) -> Optional[ChapterInfo]:
        """Determine which chapter contains the current position"""
        if not self.chapters:
            return None

        # Find the chapter that contains this position
        for i, chapter in enumerate(self.chapters):
            # Check if this is the last chapter or if position is before next chapter
            if i == len(self.chapters) - 1:
                return chapter

            next_chapter = self.chapters[i + 1]
            if position.chapter_index < next_chapter.position.chapter_index:
                return chapter

        return self.chapters[0] if self.chapters else None


class FontFamilyOverride:
    """
    Manages font family preferences for ereader rendering.
    Allows dynamic font family switching without modifying source blocks.
    """

    def __init__(self, preferred_family: Optional[BundledFont] = None):
        """
        Initialize font family override.

        Args:
            preferred_family: Preferred bundled font family (None = use original fonts)
        """
        self.preferred_family = preferred_family

    def override_font(self, font: Font) -> Font:
        """
        Create a new font with the preferred family while preserving other attributes.

        Args:
            font: Original font object

        Returns:
            Font with overridden family, or original if no override is set
        """
        if self.preferred_family is None:
            return font

        # Get the appropriate font path for the preferred family
        # preserving the original font's weight and style
        new_font_path = get_bundled_font_path(
            family=self.preferred_family,
            weight=font.weight,
            style=font.style
        )

        # If we couldn't find a matching font, fall back to original
        if new_font_path is None:
            return font

        # Create a new font with the overridden path
        return Font(
            font_path=new_font_path,
            font_size=font.font_size,
            colour=font.colour,
            weight=font.weight,
            style=font.style,
            decoration=font.decoration,
            background=font.background,
            language=font.language,
            min_hyphenation_width=font.min_hyphenation_width
        )


class FontScaler:
    """
    Handles font scaling operations for ereader font size adjustments.
    Applies scaling at layout/render time while preserving original font objects.
    """

    @staticmethod
    def scale_font(font: Font, scale_factor: float, family_override: Optional[FontFamilyOverride] = None) -> Font:
        """
        Create a scaled version of a font for layout calculations.

        Args:
            font: Original font object
            scale_factor: Scaling factor (1.0 = no change, 2.0 = double size, etc.)
            family_override: Optional font family override

        Returns:
            New Font object with scaled size and optional family override
        """
        # Apply family override first if specified
        working_font = font
        if family_override is not None:
            working_font = family_override.override_font(font)

        # Then apply scaling
        if scale_factor == 1.0:
            return working_font

        scaled_size = max(1, int(working_font.font_size * scale_factor))

        return Font(
            font_path=working_font._font_path,
            font_size=scaled_size,
            colour=working_font.colour,
            weight=working_font.weight,
            style=working_font.style,
            decoration=working_font.decoration,
            background=working_font.background,
            language=working_font.language,
            min_hyphenation_width=working_font.min_hyphenation_width
        )

    @staticmethod
    def scale_word_spacing(spacing: Tuple[int, int],
                           scale_factor: float) -> Tuple[int, int]:
        """Scale word spacing constraints proportionally"""
        if scale_factor == 1.0:
            return spacing

        min_spacing, max_spacing = spacing
        return (
            max(1, int(min_spacing * scale_factor)),
            max(2, int(max_spacing * scale_factor))
        )


class BidirectionalLayouter:
    """
    Core layout engine supporting both forward and backward page rendering.
    Handles font scaling and maintains position state.
    """

    def __init__(self,
                 blocks: List[Block],
                 page_style: PageStyle,
                 page_size: Tuple[int,
                                  int] = (800,
                                          600),
                 alignment_override=None,
                 font_family_override: Optional[FontFamilyOverride] = None):
        self.blocks = blocks
        self.page_style = page_style
        self.page_size = page_size
        self.chapter_navigator = ChapterNavigator(blocks)
        self.alignment_override = alignment_override
        self.font_family_override = font_family_override

    def render_page_forward(self, position: RenderingPosition,
                            font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
        """
        Render a page starting from the given position, moving forward through the document.

        Args:
            position: Starting position in document
            font_scale: Font scaling factor

        Returns:
            Tuple of (rendered_page, next_position)
        """
        page = Page(size=self.page_size, style=self.page_style)
        current_pos = position.copy()

        # Start laying out blocks from the current position
        while current_pos.block_index < len(self.blocks) and page.free_space()[1] > 0:
            # Additional bounds check to prevent IndexError
            if current_pos.block_index >= len(self.blocks):
                break

            block = self.blocks[current_pos.block_index]

            # Apply font scaling to the block
            scaled_block = self._scale_block_fonts(block, font_scale)

            # Try to fit the block on the current page
            success, new_pos = self._layout_block_on_page(
                scaled_block, page, current_pos, font_scale)

            if not success:
                # The block did not fit in its entirety. It may still have been
                # laid out partially - a paragraph larger than one page places as
                # many lines as fit and reports the word it stopped at. Keeping
                # that resume point is what allows the next page to continue;
                # discarding it tells the caller no progress was made, which
                # dead-ends navigation on the block forever.
                if self._position_compare(new_pos, current_pos) > 0:
                    current_pos = new_pos
                break

            # Add inter-block spacing after successfully laying out a block
            # Only add if we're not at the end of the document and there's space
            if new_pos.block_index < len(self.blocks):
                page._current_y_offset += self.page_style.inter_block_spacing

            # Ensure new position doesn't go beyond bounds
            if new_pos.block_index >= len(self.blocks):
                # We've reached the end of the document
                current_pos = new_pos
                break

            current_pos = new_pos

        return page, current_pos

    def render_page_backward(self,
                             end_position: RenderingPosition,
                             font_scale: float = 1.0) -> Tuple[Page,
                                                               RenderingPosition]:
        """
        Render a page that ends at the given position, filling backward.
        Critical for "previous page" navigation.

        Uses iterative refinement to find the correct start position that
        results in a page ending at (or very close to) the target position.

        Args:
            end_position: Position where page should end
            font_scale: Font scaling factor

        Returns:
            Tuple of (rendered_page, start_position)
        """
        # Handle edge case: already at beginning
        if end_position.block_index == 0 and end_position.word_index == 0:
            return self.render_page_forward(end_position, font_scale)

        # Start with initial estimate
        estimated_start = self._estimate_page_start(end_position, font_scale)

        # Iterative refinement: keep adjusting until we converge or hit max iterations
        max_iterations = 10
        best_page = None
        best_start = estimated_start
        best_distance = float('inf')

        for iteration in range(max_iterations):
            # Render forward from current estimate
            page, actual_end = self.render_page_forward(estimated_start, font_scale)

            # Calculate how far we are from target
            comparison = self._position_compare(actual_end, end_position)

            # Perfect match or close enough (within same block)
            # BUT: ensure we actually moved backward (estimated_start < end_position)
            if comparison == 0:
                # Check if we actually found a valid previous page
                if self._position_compare(estimated_start, end_position) < 0:
                    return page, estimated_start
                # If estimated_start >= end_position, we haven't moved backward
                # Continue iterating to find a better position
                elif iteration == 0:
                    # On first iteration, if we can't find a previous position,
                    # we're likely at or near the beginning
                    break

            # Track best result so far
            distance = abs(actual_end.block_index - end_position.block_index)
            if distance < best_distance:
                best_distance = distance
                best_page = page
                best_start = estimated_start.copy()

            # Adjust estimate for next iteration
            estimated_start = self._adjust_start_estimate(
                estimated_start, end_position, actual_end)

            # Safety: don't go before document start
            if estimated_start.block_index < 0:
                estimated_start.block_index = 0
                estimated_start.word_index = 0

        # If we exhausted iterations, return best result found
        # BUT: ensure we didn't return the same position (no backward progress)
        final_page = best_page if best_page else page
        final_start = best_start

        # Safety check: if final_start >= end_position, we failed to move backward
        # This can happen at the beginning of the document or when estimation failed
        if self._position_compare(final_start, end_position) >= 0:
            # Can't go further back - check if we're at the absolute beginning
            if end_position.block_index == 0 and end_position.word_index == 0:
                # Already at beginning, return as-is
                return final_page, final_start

            # Fallback strategy: try a more aggressive backward jump
            # Start from several blocks before the current position
            blocks_to_jump = max(1, min(5, end_position.block_index))
            fallback_pos = RenderingPosition(
                chapter_index=end_position.chapter_index,
                block_index=max(0, end_position.block_index - blocks_to_jump),
                word_index=0
            )

            # Render forward from the fallback position
            fallback_page, fallback_end = self.render_page_forward(fallback_pos, font_scale)

            # Verify the fallback actually moved us backward
            if self._position_compare(fallback_pos, end_position) < 0:
                return fallback_page, fallback_pos

            # If even the fallback didn't work, we're likely at the beginning
            # Return a page starting from the beginning
            return self.render_page_forward(RenderingPosition(), font_scale)

        return final_page, final_start

    def _scale_block_fonts(self, block: Block, font_scale: float) -> Block:
        """Apply font scaling and font family override to all fonts in a block"""
        # Check if we need to do any transformation
        if font_scale == 1.0 and self.font_family_override is None:
            return block

        # This is a simplified implementation
        # In practice, we'd need to handle each block type appropriately
        if isinstance(block, (Paragraph, Heading)):
            scaled_block_style = FontScaler.scale_font(block.style, font_scale, self.font_family_override)
            if isinstance(block, Heading):
                scaled_block = Heading(block.level, scaled_block_style)
            else:
                scaled_block = Paragraph(scaled_block_style)

            # words_iter() returns tuples of (position, word)
            for position, word in block.words_iter():
                if isinstance(word, Word):
                    scaled_word = Word(
                        word.text, FontScaler.scale_font(
                            word.style, font_scale, self.font_family_override))
                    scaled_block.add_word(scaled_word)
            return scaled_block

        return block

    def _layout_block_on_page(self,
                              block: Block,
                              page: Page,
                              position: RenderingPosition,
                              font_scale: float) -> Tuple[bool,
                                                          RenderingPosition]:
        """
        Try to layout a block on the page starting from the given position.

        Returns:
            Tuple of (success, new_position)
        """
        if isinstance(block, Paragraph):
            return self._layout_paragraph_on_page(block, page, position, font_scale)
        elif isinstance(block, Heading):
            return self._layout_heading_on_page(block, page, position, font_scale)
        elif isinstance(block, Table):
            return self._layout_table_on_page(block, page, position, font_scale)
        elif isinstance(block, HList):
            return self._layout_list_on_page(block, page, position, font_scale)
        elif isinstance(block, Image):
            return self._layout_image_on_page(block, page, position, font_scale)
        else:
            # Skip unknown block types
            new_pos = position.copy()
            new_pos.block_index += 1
            return True, new_pos

    def _layout_paragraph_on_page(self,
                                  paragraph: Paragraph,
                                  page: Page,
                                  position: RenderingPosition,
                                  font_scale: float) -> Tuple[bool,
                                                              RenderingPosition]:
        """
        Layout a paragraph on the page using the core paragraph_layouter.
        Integrates font scaling and position tracking with the proven layout logic.

        Args:
            paragraph: The paragraph to layout (already scaled if font_scale != 1.0)
            page: The page to layout on
            position: Current rendering position
            font_scale: Font scaling factor (used for context, paragraph should already be scaled)

        Returns:
            Tuple of (success, new_position)
        """
        # Convert remaining_pretext from string to Text object if needed
        pretext_obj = None
        if position.remaining_pretext:
            # Create a Text object from the pretext string
            pretext_obj = Text(
                position.remaining_pretext,
                paragraph.style,
                page.draw,
                line=None,
                source=None
            )

        # Call the core paragraph layouter with alignment override if set
        success, failed_word_index, remaining_pretext = paragraph_layouter(
            paragraph,
            page,
            start_word=position.word_index,
            pretext=pretext_obj,
            alignment_override=self.alignment_override
        )

        # Create new position based on the result
        new_pos = position.copy()

        if success:
            # Paragraph was fully laid out, move to next block
            new_pos.block_index += 1
            new_pos.word_index = 0
            new_pos.remaining_pretext = None
            return True, new_pos
        else:
            # Paragraph was not fully laid out
            if failed_word_index is not None:
                # Update position to the word that didn't fit
                new_pos.word_index = failed_word_index

                # Convert Text object back to string if there's remaining pretext
                if remaining_pretext is not None and hasattr(remaining_pretext, 'text'):
                    new_pos.remaining_pretext = remaining_pretext.text
                else:
                    new_pos.remaining_pretext = None

                return False, new_pos
            else:
                # No specific word failed, but layout wasn't successful
                # This shouldn't normally happen, but handle it gracefully
                return False, position

    def _layout_heading_on_page(self,
                                heading: Heading,
                                page: Page,
                                position: RenderingPosition,
                                font_scale: float) -> Tuple[bool,
                                                            RenderingPosition]:
        """Layout a heading on the page"""
        # Similar to paragraph but with heading-specific styling
        return self._layout_paragraph_on_page(heading, page, position, font_scale)

    def _layout_table_on_page(self,
                              table: Table,
                              page: Page,
                              position: RenderingPosition,
                              font_scale: float) -> Tuple[bool,
                                                          RenderingPosition]:
        """Layout a table on the page with column fitting and row continuation"""
        # This is a complex operation that would need full table layout logic
        # For now, skip tables
        new_pos = position.copy()
        new_pos.block_index += 1
        new_pos.table_row = 0
        new_pos.table_col = 0
        return True, new_pos

    def _layout_list_on_page(self,
                             hlist: HList,
                             page: Page,
                             position: RenderingPosition,
                             font_scale: float) -> Tuple[bool,
                                                         RenderingPosition]:
        """Layout a list on the page"""
        # This would need list-specific layout logic
        # For now, skip lists
        new_pos = position.copy()
        new_pos.block_index += 1
        new_pos.list_item_index = 0
        return True, new_pos

    def _layout_image_on_page(self,
                              image: Image,
                              page: Page,
                              position: RenderingPosition,
                              font_scale: float) -> Tuple[bool,
                                                          RenderingPosition]:
        """
        Layout an image on the page using the image_layouter.

        Args:
            image: The Image block to layout
            page: The page to layout on
            position: Current rendering position (should be at the start of this image block)
            font_scale: Font scaling factor (not used for images, but kept for consistency)

        Returns:
            Tuple of (success, new_position)
            - success: True if image was laid out, False if page ran out of space
            - new_position: Updated position (next block if success, same block if failed)
        """
        # Try to layout the image on the current page
        success = image_layouter(
            image=image,
            page=page,
            max_width=None,  # Use page available width
            max_height=None   # Use page available height
        )

        new_pos = position.copy()

        if success:
            # Image was successfully laid out, move to next block
            new_pos.block_index += 1
            new_pos.word_index = 0
            return True, new_pos
        else:
            # Image didn't fit on current page, signal to continue on next page
            # Keep same position so it will be attempted on the next page
            return False, position

    def _estimate_page_start(
            self,
            end_position: RenderingPosition,
            font_scale: float) -> RenderingPosition:
        """Estimate where a page should start to end at the given position"""
        # This is a simplified heuristic - a full implementation would be more
        # sophisticated
        estimated_start = end_position.copy()

        # Move back by an estimated number of blocks that would fit on a page
        estimated_blocks_per_page = max(1, int(10 / font_scale))  # Rough estimate
        estimated_start.block_index = max(
            0, end_position.block_index - estimated_blocks_per_page)
        estimated_start.word_index = 0

        return estimated_start

    def _adjust_start_estimate(
            self,
            current_start: RenderingPosition,
            target_end: RenderingPosition,
            actual_end: RenderingPosition) -> RenderingPosition:
        """
        Adjust start position estimate based on overshoot/undershoot.
        Uses proportional adjustment to converge faster.
        """
        adjusted = current_start.copy()

        # Calculate the difference between actual and target end positions
        block_diff = actual_end.block_index - target_end.block_index

        comparison = self._position_compare(actual_end, target_end)

        if comparison < 0:  # Undershot - rendered to block X but need to reach block Y where X < Y
            # We didn't render far enough forward
            # Need to start at a LATER block (higher index) so the page includes more content
            adjustment = max(1, abs(block_diff) // 2)
            new_index = adjusted.block_index + adjustment
            # Clamp to valid range
            if len(self.blocks) > 0:
                adjusted.block_index = min(len(self.blocks) - 1, max(0, new_index))
            else:
                adjusted.block_index = max(0, new_index)
        elif comparison > 0:  # Overshot - rendered past the target
            # We rendered too far forward
            # Need to start at an EARLIER block (lower index) so the page doesn't go as far
            adjustment = max(1, abs(block_diff) // 2)
            adjusted.block_index = max(0, adjusted.block_index - adjustment)

        # Reset word index when adjusting blocks
        adjusted.word_index = 0

        return adjusted

    def _position_compare(self, pos1: RenderingPosition,
                          pos2: RenderingPosition) -> int:
        """Compare two positions (-1: pos1 < pos2, 0: equal, 1: pos1 > pos2)"""
        if pos1.chapter_index != pos2.chapter_index:
            return 1 if pos1.chapter_index > pos2.chapter_index else -1
        if pos1.block_index != pos2.block_index:
            return 1 if pos1.block_index > pos2.block_index else -1
        if pos1.word_index != pos2.word_index:
            return 1 if pos1.word_index > pos2.word_index else -1
        return 0


# Add can_fit_line method to Page class if it doesn't exist
def _add_page_methods():
    """Add missing methods to Page class"""
    if not hasattr(Page, 'can_fit_line'):
        def can_fit_line(self, line_height: int) -> bool:
            """Check if a line of given height can fit on the page"""
            available_height = self.content_size[1] - self._current_y_offset
            return available_height >= line_height

        Page.can_fit_line = can_fit_line

    if not hasattr(Page, 'available_width'):
        @property
        def available_width(self) -> int:
            """Get available width for content"""
            return self.content_size[0]

        Page.available_width = available_width


# Apply the page methods
_add_page_methods()
