from __future__ import annotations

from typing import List, Tuple, Optional, Union
import numpy as np

from pyWebLayout.concrete import Page, Line, Text
from pyWebLayout.concrete.image import RenderableImage
from pyWebLayout.concrete.functional import ButtonText, FormFieldText
from pyWebLayout.concrete.table import TableRenderer, TableStyle
from pyWebLayout.abstract import Paragraph, Word
from pyWebLayout.abstract.block import Image as AbstractImage, Heading, PageBreak, Table
from pyWebLayout.abstract.functional import Button, Form, FormField
from pyWebLayout.style.concrete_style import ConcreteStyleRegistry, RenderingContext, StyleResolver
from pyWebLayout.style import Font, Alignment


def paragraph_layouter(paragraph: Paragraph,
                       page: Page,
                       start_word: int = 0,
                       pretext: Optional[Text] = None,
                       alignment_override: Optional['Alignment'] = None) -> Tuple[bool,
                                                                                  Optional[int],
                                                                                  Optional[Text]]:
    """
    Layout a paragraph of text within a given page.

    This function extracts word spacing constraints from the style system
    and uses them to create properly spaced lines of text.

    Args:
        paragraph: The paragraph to layout
        page: The page to layout the paragraph on
        start_word: Index of the first word to process (for continuation)
        pretext: Optional pretext from a previous hyphenated word
        alignment_override: Optional alignment to override the paragraph's default alignment

    Returns:
        Tuple of:
        - bool: True if paragraph was completely laid out, False if page ran out of space
        - Optional[int]: Index of first word that didn't fit (if any)
        - Optional[Text]: Remaining pretext if word was hyphenated (if any)
    """
    if not paragraph.words:
        return True, None, None

    # Validate inputs
    if start_word >= len(paragraph.words):
        return True, None, None

    # paragraph.style is already a Font object (concrete), not AbstractStyle
    # We need to get word spacing constraints from the Font's abstract style if available
    # For now, use reasonable defaults based on font size

    # Alignment for text that does not specify its own. Headings are never
    # justified - stretching a two-word title across the measure is always wrong -
    # so they fall back to flush left.
    default_alignment = getattr(page.style, 'default_alignment', None)
    if not isinstance(default_alignment, Alignment):
        default_alignment = Alignment.JUSTIFY
    if isinstance(paragraph, Heading):
        default_alignment = Alignment.LEFT

    if isinstance(paragraph.style, Font):
        # paragraph.style is already a Font (concrete style)
        font = paragraph.style
        # Use default word spacing constraints based on font size
        # Minimum spacing should be proportional to font size for better readability
        min_spacing = float(font.font_size) * 0.25  # 25% of font size
        max_spacing = float(font.font_size) * 0.5   # 50% of font size
        word_spacing_constraints = (int(min_spacing), int(max_spacing))
        text_align = default_alignment
    else:
        # paragraph.style is an AbstractStyle, resolve it
        # Ensure font_size is an int (it could be a FontSize enum)
        from pyWebLayout.style.abstract_style import FontSize
        if isinstance(paragraph.style.font_size, FontSize):
            # Use a default base font size, the resolver will handle the semantic size
            base_font_size = 16
        else:
            base_font_size = int(paragraph.style.font_size)

        rendering_context = RenderingContext(base_font_size=base_font_size)
        style_resolver = StyleResolver(rendering_context)
        style_registry = ConcreteStyleRegistry(style_resolver)
        concrete_style = style_registry.get_concrete_style(paragraph.style)
        font = concrete_style.create_font()
        word_spacing_constraints = (
            int(concrete_style.word_spacing_min),
            int(concrete_style.word_spacing_max)
        )
        # text_align is None when the source did not specify one.
        text_align = concrete_style.text_align or default_alignment

    # Apply page-level word spacing override if specified
    if hasattr(
            page.style,
            'word_spacing') and isinstance(
            page.style.word_spacing,
            int) and page.style.word_spacing > 0:
        # Add the page-level word spacing to both min and max constraints
        min_ws, max_ws = word_spacing_constraints
        word_spacing_constraints = (
            min_ws + page.style.word_spacing,
            max_ws + page.style.word_spacing
        )

    # Apply alignment override if provided
    if alignment_override is not None:
        text_align = alignment_override

    # Cap font size to page maximum if needed
    if font.font_size > page.style.max_font_size:
        # Use paragraph's font registry to create the capped font
        if hasattr(paragraph, 'get_or_create_font'):
            font = paragraph.get_or_create_font(
                font_path=font._font_path,
                font_size=page.style.max_font_size,
                colour=font.colour,
                weight=font.weight,
                style=font.style,
                decoration=font.decoration,
                background=font.background
            )
        else:
            # Fallback to direct creation (will still use global cache)
            font = Font(
                font_path=font._font_path,
                font_size=page.style.max_font_size,
                colour=font.colour,
                weight=font.weight,
                style=font.style,
                decoration=font.decoration,
                background=font.background
            )

    # Calculate baseline-to-baseline spacing: font size + additional line spacing
    # This is the vertical distance between baselines of consecutive lines
    # Formula: baseline_spacing = font_size + line_spacing (absolute pixels)
    line_spacing_value = getattr(page.style, 'line_spacing', 5)
    # Ensure line_spacing is an int (could be Mock in tests)
    if not isinstance(line_spacing_value, int):
        line_spacing_value = 5
    baseline_spacing = font.font_size + line_spacing_value

    # Get font metrics for boundary checking
    ascent, descent = font.font.getmetrics()

    def create_new_line(word: Optional[Union[Word, Text]] = None,
                        is_first_line: bool = False) -> Optional[Line]:
        """Helper function to create a new line, returns None if page is full."""
        # Check if this line's baseline and descenders would fit on the page
        if not page.can_fit_line(baseline_spacing, ascent, descent):
            return None

        # For the first line, position it so text starts at the top boundary
        # For subsequent lines, use current y_offset which tracks
        # baseline-to-baseline spacing
        if is_first_line:
            # Position line origin so that baseline (origin + ascent) is close to top
            # We want minimal space above the text, so origin should be at boundary
            y_cursor = page._current_y_offset
        else:
            y_cursor = page._current_y_offset
        x_cursor = page.content_origin[0]

        # Create a temporary Text object to calculate word width
        if word:
            temp_text = Text.from_word(word, page.draw)
            temp_text.width
        else:
            pass

        return Line(
            spacing=word_spacing_constraints,
            origin=(x_cursor, y_cursor),
            size=(page.available_width, baseline_spacing),
            draw=page.draw,
            font=font,
            halign=text_align
        )

    # Create initial line
    current_line = create_new_line()
    if not current_line:
        return False, start_word, pretext

    page.add_child(current_line)
    # Note: add_child already updates _current_y_offset based on child's origin and size
    # No need to manually increment it here

    # Track current position in paragraph
    current_pretext = pretext

    # Process words starting from start_word
    for i, word in enumerate(paragraph.words[start_word:], start=start_word):
        # Check if this is a LinkedWord and needs special handling in concrete layer
        # Note: The Line.add_word method will create Text objects internally,
        # but we may want to create LinkText for LinkedWord instances in future
        # For now, the abstract layer (LinkedWord) carries the link info,
        # and the concrete layer (LinkText) would be created during rendering

        success, overflow_text = current_line.add_word(word, current_pretext)

        if success:
            # Word fit successfully
            if overflow_text is not None:
                # If there's overflow text, we need to start a new line with it
                current_pretext = overflow_text
                current_line = create_new_line(overflow_text)
                if not current_line:
                    # If we can't create a new line, return with the current state
                    return False, i, overflow_text
                page.add_child(current_line)
                # Note: add_child already updates _current_y_offset
                # Continue to the next word
                continue
            else:
                # No overflow, clear pretext
                current_pretext = None
        else:
            # Word didn't fit, need a new line
            current_line = create_new_line(word)
            if not current_line:
                # Page is full, return current position
                return False, i, overflow_text

            # Check if the word will fit on the new line before adding it
            temp_text = Text.from_word(word, page.draw)
            if temp_text.width > current_line.size[0]:
                # Word is too wide for the line, we need to hyphenate it
                if len(word.text) >= 6:
                    # Try to hyphenate the word
                    splits = [
                        (Text(
                            pair[0],
                            word.style,
                            page.draw,
                            line=current_line,
                            source=word),
                            Text(
                            pair[1],
                            word.style,
                            page.draw,
                            line=current_line,
                            source=word)) for pair in word.possible_hyphenation()]
                    if len(splits) > 0:
                        # Use the first hyphenation point
                        first_part, second_part = splits[0]
                        current_line.add_word(word, first_part)
                        current_pretext = second_part
                        continue

            page.add_child(current_line)
            # Note: add_child already updates _current_y_offset

            # Try to add the word to the new line
            success, overflow_text = current_line.add_word(word, current_pretext)

            if not success:
                # Word still doesn't fit even on a new line
                # This might happen with very long words or narrow pages
                if overflow_text:
                    # Word was hyphenated, continue with the overflow
                    current_pretext = overflow_text
                    continue
                else:
                    # Word cannot be broken, skip it or handle as error
                    # For now, we'll return indicating we couldn't process this word
                    return False, i, None
            else:
                current_pretext = overflow_text  # May be None or hyphenated remainder

    # All words processed successfully. The line holding the final word is the
    # end of the paragraph, so it is rendered at its natural width rather than
    # justified to the full column. A paragraph continued on the next page does
    # not reach here, so its lines stay justified - which is correct.
    if current_line is not None:
        current_line.is_paragraph_end = True

    return True, None, None


def pagebreak_layouter(page_break: PageBreak, page: Page) -> bool:
    """
    Handle a page break element.

    A page break signals that all subsequent content should start on a new page.
    This function always returns False to indicate that the current page is complete
    and a new page should be created for subsequent content.

    Args:
        page_break: The PageBreak block
        page: The current page (not used, but kept for consistency)

    Returns:
        bool: Always False to force creation of a new page
    """
    # Page break always forces a new page
    return False


def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] = None,
                   max_height: Optional[int] = None) -> bool:
    """
    Layout an image within a given page.

    This function places an image on the page, respecting size constraints
    and available space. Images are centered horizontally by default.

    Args:
        image: The abstract Image object to layout
        page: The page to layout the image on
        max_width: Maximum width constraint (defaults to page available width)
        max_height: Maximum height constraint (defaults to remaining page height)

    Returns:
        bool: True if image was successfully laid out, False if page ran out of space
    """
    # Use page available width if max_width not specified
    if max_width is None:
        max_width = page.available_width

    # Calculate available height on page
    available_height = page.remaining_height

    # If no space available, image doesn't fit
    if available_height <= 0:
        return False

    if max_height is None:
        max_height = available_height
    else:
        max_height = min(max_height, available_height)

    # Calculate scaled dimensions
    scaled_width, scaled_height = image.calculate_scaled_dimensions(
        max_width, max_height)

    # Check if image fits on current page
    if scaled_height is None or scaled_height > available_height:
        return False

    # Create renderable image
    x_offset = page.content_origin[0]
    y_offset = page._current_y_offset

    # Access page.draw to ensure canvas is initialized
    _ = page.draw

    renderable_image = RenderableImage(
        image=image,
        canvas=page._canvas,
        max_width=max_width,
        max_height=max_height,
        origin=(x_offset, y_offset),
        size=(scaled_width or max_width, scaled_height or max_height),
        halign=Alignment.CENTER,
        valign=Alignment.TOP
    )

    # Add to page
    page.add_child(renderable_image)

    return True


def table_layouter(
        table: Table,
        page: Page,
        style: Optional[TableStyle] = None) -> bool:
    """
    Layout a table within a given page.

    This function uses the TableRenderer to render the table at the current
    page position, advancing the page's y-offset after successful rendering.

    Args:
        table: The abstract Table object to layout
        page: The page to layout the table on
        style: Optional table styling configuration

    Returns:
        bool: True if table was successfully laid out, False if page ran out of space
    """
    # Calculate available space
    available_width = page.available_width
    x_offset = page.content_origin[0]
    y_offset = page._current_y_offset

    # Access page.draw to ensure canvas is initialized
    draw = page.draw
    canvas = page._canvas

    # Create table renderer
    origin = (x_offset, y_offset)
    renderer = TableRenderer(
        table=table,
        origin=origin,
        available_width=available_width,
        draw=draw,
        style=style,
        canvas=canvas
    )

    # Check if table fits on current page
    table_height = renderer.size[1]
    available_height = page.remaining_height

    if table_height > available_height:
        return False

    # Render the table
    renderer.render()

    # Update page y-offset
    page._current_y_offset = y_offset + table_height

    return True


def button_layouter(button: Button,
                    page: Page,
                    font: Optional[Font] = None,
                    padding: Tuple[int,
                                   int,
                                   int,
                                   int] = (4,
                                           8,
                                           4,
                                           8)) -> Tuple[bool,
                                                        str]:
    """
    Layout a button within a given page and register it for callback binding.

    This function creates a ButtonText renderable, positions it on the page,
    and registers it in the page's callback registry using the button's html_id
    (if available) or an auto-generated id.

    Args:
        button: The abstract Button object to layout
        page: The page to layout the button on
        font: Optional font for button text (defaults to page default)
        padding: Padding around button text (top, right, bottom, left)

    Returns:
        Tuple of:
        - bool: True if button was successfully laid out, False if page ran out of space
        - str: The id used to register the button in the callback registry
    """
    # Use provided font or create a default one
    if font is None:
        font = Font(font_size=14, colour=(255, 255, 255))

    # Calculate available space
    available_height = page.remaining_height

    # Create ButtonText renderable
    button_text = ButtonText(button, font, page.draw, padding=padding)

    # Check if button fits on current page
    button_height = button_text.size[1]
    if button_height > available_height:
        return False, ""

    # Position the button
    x_offset = page.content_origin[0]
    y_offset = page._current_y_offset

    button_text.set_origin(np.array([x_offset, y_offset]))

    # Register in callback registry
    html_id = button.html_id
    registered_id = page.callbacks.register(button_text, html_id=html_id)

    # Add to page
    page.add_child(button_text)

    return True, registered_id


def form_field_layouter(field: FormField, page: Page, font: Optional[Font] = None,
                        field_height: int = 24) -> Tuple[bool, str]:
    """
    Layout a form field within a given page and register it for callback binding.

    This function creates a FormFieldText renderable, positions it on the page,
    and registers it in the page's callback registry.

    Args:
        field: The abstract FormField object to layout
        page: The page to layout the field on
        font: Optional font for field label (defaults to page default)
        field_height: Height of the input field area

    Returns:
        Tuple of:
        - bool: True if field was successfully laid out, False if page ran out of space
        - str: The id used to register the field in the callback registry
    """
    # Use provided font or create a default one
    if font is None:
        font = Font(font_size=12, colour=(0, 0, 0))

    # Calculate available space
    available_height = page.remaining_height

    # Create FormFieldText renderable
    field_text = FormFieldText(field, font, page.draw, field_height=field_height)

    # Check if field fits on current page
    total_field_height = field_text.size[1]
    if total_field_height > available_height:
        return False, ""

    # Position the field
    x_offset = page.content_origin[0]
    y_offset = page._current_y_offset

    field_text.set_origin(np.array([x_offset, y_offset]))

    # Register in callback registry (use field name as html_id fallback)
    html_id = getattr(field, '_html_id', None) or field.name
    registered_id = page.callbacks.register(field_text, html_id=html_id)

    # Add to page
    page.add_child(field_text)

    return True, registered_id


def form_layouter(form: Form, page: Page, font: Optional[Font] = None,
                  field_spacing: int = 10) -> Tuple[bool, List[str]]:
    """
    Layout a complete form with all its fields within a given page.

    This function creates FormFieldText renderables for all fields in the form,
    positions them vertically, and registers both the form and its fields in
    the page's callback registry.

    Args:
        form: The abstract Form object to layout
        page: The page to layout the form on
        font: Optional font for field labels (defaults to page default)
        field_spacing: Vertical spacing between fields in pixels

    Returns:
        Tuple of:
        - bool: True if form was successfully laid out, False if page ran out of space
        - List[str]: List of registered ids for all fields (empty if layout failed)
    """
    # Use provided font or create a default one
    if font is None:
        font = Font(font_size=12, colour=(0, 0, 0))

    # Track registered field ids
    field_ids = []

    # Layout each field in the form
    for field_name, field in form._fields.items():
        # Add spacing before each field (except the first)
        if field_ids:
            page._current_y_offset += field_spacing

        # Layout the field
        success, field_id = form_field_layouter(field, page, font)

        if not success:
            # Couldn't fit this field, return failure
            return False, []

        field_ids.append(field_id)

    # Register the form itself (optional, for form submission)
    # Note: The form doesn't have a visual representation, but we can track it
    # for submission callbacks
    # form_id = page.callbacks.register(form, html_id=form.html_id)

    return True, field_ids


class DocumentLayouter:
    """
    Document layouter that orchestrates layout of various abstract elements.

    Delegates to specialized layouters for different content types:
    - paragraph_layouter for text paragraphs
    - image_layouter for images
    - table_layouter for tables

    This class acts as a coordinator, managing the overall document flow
    and page context while delegating specific layout tasks to specialized
    layouter functions.
    """

    def __init__(self, page: Page):
        """
        Initialize the document layouter with a page.

        Args:
            page: The page to layout content on
        """
        self.page = page
        # Create a style resolver if page doesn't have one
        if hasattr(page, 'style_resolver'):
            style_resolver = page.style_resolver
        else:
            # Create a default rendering context and style resolver
            from pyWebLayout.style.concrete_style import RenderingContext
            context = RenderingContext()
            style_resolver = StyleResolver(context)
        self.style_registry = ConcreteStyleRegistry(style_resolver)

    def layout_paragraph(self,
                         paragraph: Paragraph,
                         start_word: int = 0,
                         pretext: Optional[Text] = None) -> Tuple[bool,
                                                                  Optional[int],
                                                                  Optional[Text]]:
        """
        Layout a paragraph using the paragraph_layouter.

        Args:
            paragraph: The paragraph to layout
            start_word: Index of the first word to process (for continuation)
            pretext: Optional pretext from a previous hyphenated word

        Returns:
            Tuple of (success, failed_word_index, remaining_pretext)
        """
        return paragraph_layouter(paragraph, self.page, start_word, pretext)

    def layout_image(self, image: AbstractImage, max_width: Optional[int] = None,
                     max_height: Optional[int] = None) -> bool:
        """
        Layout an image using the image_layouter.

        Args:
            image: The abstract Image object to layout
            max_width: Maximum width constraint (defaults to page available width)
            max_height: Maximum height constraint (defaults to remaining page height)

        Returns:
            bool: True if image was successfully laid out, False if page ran out of space
        """
        return image_layouter(image, self.page, max_width, max_height)

    def layout_table(self, table: Table, style: Optional[TableStyle] = None) -> bool:
        """
        Layout a table using the table_layouter.

        Args:
            table: The abstract Table object to layout
            style: Optional table styling configuration

        Returns:
            bool: True if table was successfully laid out, False if page ran out of space
        """
        return table_layouter(table, self.page, style)

    def layout_button(self,
                      button: Button,
                      font: Optional[Font] = None,
                      padding: Tuple[int,
                                     int,
                                     int,
                                     int] = (4,
                                             8,
                                             4,
                                             8)) -> Tuple[bool,
                                                          str]:
        """
        Layout a button using the button_layouter.

        Args:
            button: The abstract Button object to layout
            font: Optional font for button text
            padding: Padding around button text

        Returns:
            Tuple of (success, registered_id)
        """
        return button_layouter(button, self.page, font, padding)

    def layout_form(self, form: Form, font: Optional[Font] = None,
                    field_spacing: int = 10) -> Tuple[bool, List[str]]:
        """
        Layout a form using the form_layouter.

        Args:
            form: The abstract Form object to layout
            font: Optional font for field labels
            field_spacing: Vertical spacing between fields

        Returns:
            Tuple of (success, list_of_field_ids)
        """
        return form_layouter(form, self.page, font, field_spacing)

    def layout_document(
            self, elements: List[Union[Paragraph, AbstractImage, Table, Button, Form]]) -> bool:
        """
        Layout a list of abstract elements (paragraphs, images, tables, buttons, and forms).

        This method delegates to specialized layouters based on element type:
        - Paragraphs are handled by layout_paragraph
        - Images are handled by layout_image
        - Tables are handled by layout_table
        - Buttons are handled by layout_button
        - Forms are handled by layout_form

        Args:
            elements: List of abstract elements to layout

        Returns:
            True if all elements were successfully laid out, False otherwise
        """
        for element in elements:
            if isinstance(element, Paragraph):
                success, _, _ = self.layout_paragraph(element)
                if not success:
                    return False
            elif isinstance(element, AbstractImage):
                success = self.layout_image(element)
                if not success:
                    return False
            elif isinstance(element, Table):
                success = self.layout_table(element)
                if not success:
                    return False
            elif isinstance(element, Button):
                success, _ = self.layout_button(element)
                if not success:
                    return False
            elif isinstance(element, Form):
                success, _ = self.layout_form(element)
                if not success:
                    return False
            # Future: elif isinstance(element, CodeBlock): use code_layouter
        return True
