from typing import List, Iterator, Tuple, Dict, Optional, Any
from enum import Enum
import os
import tempfile
import urllib.request
import urllib.parse
from PIL import Image as PILImage
from .inline import Word, FormattedSpan
from ..core import Hierarchical, Styleable, FontRegistry, ContainerAware, BlockContainer


class BlockType(Enum):
    """Enumeration of different block types for classification purposes"""
    PARAGRAPH = 1
    HEADING = 2
    QUOTE = 3
    CODE_BLOCK = 4
    LIST = 5
    LIST_ITEM = 6
    TABLE = 7
    TABLE_ROW = 8
    TABLE_CELL = 9
    HORIZONTAL_RULE = 10
    LINE_BREAK = 11
    IMAGE = 12
    PAGE_BREAK = 13


class Block(Hierarchical):
    """
    Base class for all block-level elements.
    Block elements typically represent visual blocks of content that stack vertically.

    Uses Hierarchical mixin for parent-child relationship management.
    """

    def __init__(self, block_type: BlockType):
        """
        Initialize a block element.

        Args:
            block_type: The type of block this element represents
        """
        super().__init__()
        self._block_type = block_type

    @property
    def block_type(self) -> BlockType:
        """Get the type of this block element"""
        return self._block_type


class Paragraph(Styleable, FontRegistry, ContainerAware, Block):
    """
    A paragraph is a block-level element that contains a sequence of words.

    Uses Styleable mixin for style property management.
    Uses FontRegistry mixin for font caching with parent delegation.
    """

    def __init__(self, style=None):
        """
        Initialize an empty paragraph

        Args:
            style: Optional default style for words in this paragraph
        """
        super().__init__(style=style, block_type=BlockType.PARAGRAPH)
        self._words: List[Word] = []
        self._spans: List[FormattedSpan] = []

    @classmethod
    def create_and_add_to(cls, container, style=None) -> 'Paragraph':
        """
        Create a new Paragraph and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the paragraph to (must have add_block method and style property)
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created Paragraph object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container)
        style = cls._inherit_style(container, style)

        # Create the new paragraph
        paragraph = cls(style)

        # Add the paragraph to the container
        container.add_block(paragraph)

        return paragraph

    def add_word(self, word: Word):
        """
        Add a word to this paragraph.

        Args:
            word: The Word object to add
        """
        self._words.append(word)

    def create_word(self, text: str, style=None, background=None) -> Word:
        """
        Create a new word and add it to this paragraph, inheriting paragraph's style if not specified.

        This is a convenience method that uses Word.create_and_add_to() to create words
        that automatically inherit styling from this paragraph.

        Args:
            text: The text content of the word
            style: Optional Font style override. If None, attempts to inherit from paragraph
            background: Optional background color override

        Returns:
            The newly created Word object
        """
        return Word.create_and_add_to(text, self, style, background)

    def add_span(self, span: FormattedSpan):
        """
        Add a formatted span to this paragraph.

        Args:
            span: The FormattedSpan object to add
        """
        self._spans.append(span)

    def create_span(self, style=None, background=None) -> FormattedSpan:
        """
        Create a new formatted span with inherited style.

        Args:
            style: Optional Font style override. If None, inherits from paragraph
            background: Optional background color override

        Returns:
            The newly created FormattedSpan object
        """
        return FormattedSpan.create_and_add_to(self, style, background)

    @property
    def words(self) -> List[Word]:
        """Get the list of words in this paragraph"""
        return self._words

    def words_iter(self) -> Iterator[Tuple[int, Word]]:
        """
        Iterate over the words in this paragraph.

        Yields:
            Tuples of (index, word) for each word in the paragraph
        """
        for i, word in enumerate(self._words):
            yield i, word

    def spans(self) -> Iterator[FormattedSpan]:
        """
        Iterate over the formatted spans in this paragraph.

        Yields:
            Each FormattedSpan in the paragraph
        """
        for span in self._spans:
            yield span

    @property
    def word_count(self) -> int:
        """Get the number of words in this paragraph"""
        return len(self._words)

    def __len__(self):
        return self.word_count

    # get_or_create_font() is provided by FontRegistry mixin


class HeadingLevel(Enum):
    """Enumeration representing HTML heading levels (h1-h6)"""
    H1 = 1
    H2 = 2
    H3 = 3
    H4 = 4
    H5 = 5
    H6 = 6


class Heading(Paragraph):
    """
    A heading element (h1, h2, h3, etc.) that contains text with a specific heading level.
    Headings inherit from Paragraph as they contain words but have additional properties.
    """

    def __init__(self, level: HeadingLevel = HeadingLevel.H1, style=None):
        """
        Initialize a heading element.

        Args:
            level: The heading level (h1-h6)
            style: Optional default style for words in this heading
        """
        super().__init__(style)
        self._block_type = BlockType.HEADING
        self._level = level

    @classmethod
    def create_and_add_to(
            cls,
            container,
            level: HeadingLevel = HeadingLevel.H1,
            style=None) -> 'Heading':
        """
        Create a new Heading and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the heading to (must have add_block method and style property)
            level: The heading level (h1-h6)
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created Heading object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container)
        style = cls._inherit_style(container, style)

        # Create the new heading
        heading = cls(level, style)

        # Add the heading to the container
        container.add_block(heading)

        return heading

    @property
    def level(self) -> HeadingLevel:
        """Get the heading level"""
        return self._level

    @level.setter
    def level(self, level: HeadingLevel):
        """Set the heading level"""
        self._level = level


class Quote(BlockContainer, ContainerAware, Block):
    """
    A blockquote element that can contain other block elements.
    """

    def __init__(self, style=None):
        """
        Initialize an empty blockquote

        Args:
            style: Optional default style for child blocks
        """
        super().__init__(BlockType.QUOTE)
        self._style = style

    @classmethod
    def create_and_add_to(cls, container, style=None) -> 'Quote':
        """
        Create a new Quote and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the quote to (must have add_block method and style property)
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created Quote object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container)
        style = cls._inherit_style(container, style)

        # Create the new quote
        quote = cls(style)

        # Add the quote to the container
        container.add_block(quote)

        return quote

    @property
    def style(self):
        """Get the default style for this quote"""
        return self._style

    @style.setter
    def style(self, style):
        """Set the default style for this quote"""
        self._style = style


class CodeBlock(Block):
    """
    A code block element containing pre-formatted text with syntax highlighting.
    """

    def __init__(self, language: str = ""):
        """
        Initialize a code block.

        Args:
            language: The programming language for syntax highlighting
        """
        super().__init__(BlockType.CODE_BLOCK)
        self._language = language
        self._lines: List[str] = []

    @classmethod
    def create_and_add_to(cls, container, language: str = "") -> 'CodeBlock':
        """
        Create a new CodeBlock and add it to a container.

        Args:
            container: The container to add the code block to (must have add_block method)
            language: The programming language for syntax highlighting

        Returns:
            The newly created CodeBlock object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Create the new code block
        code_block = cls(language)

        # Add the code block to the container
        if hasattr(container, 'add_block'):
            container.add_block(code_block)
        else:
            raise AttributeError(
                f"Container {type(container).__name__} must have an 'add_block' method"
            )

        return code_block

    @property
    def language(self) -> str:
        """Get the programming language"""
        return self._language

    @language.setter
    def language(self, language: str):
        """Set the programming language"""
        self._language = language

    def add_line(self, line: str):
        """
        Add a line of code to this code block.

        Args:
            line: The line of code to add
        """
        self._lines.append(line)

    def lines(self) -> Iterator[Tuple[int, str]]:
        """
        Iterate over the lines in this code block.

        Yields:
            Tuples of (line_number, line_text) for each line
        """
        for i, line in enumerate(self._lines):
            yield i, line

    @property
    def line_count(self) -> int:
        """Get the number of lines in this code block"""
        return len(self._lines)


class ListStyle(Enum):
    """Enumeration of list styles"""
    UNORDERED = 1  # <ul>
    ORDERED = 2    # <ol>
    DEFINITION = 3  # <dl>


class HList(ContainerAware, Block):
    """
    An HTML list element (ul, ol, dl).
    """

    def __init__(self, style: ListStyle = ListStyle.UNORDERED, default_style=None):
        """
        Initialize a list.

        Args:
            style: The style of list (unordered, ordered, definition)
            default_style: Optional default style for child items
        """
        super().__init__(BlockType.LIST)
        self._style = style
        self._items: List[ListItem] = []
        self._default_style = default_style

    @classmethod
    def create_and_add_to(
            cls,
            container,
            style: ListStyle = ListStyle.UNORDERED,
            default_style=None) -> 'HList':
        """
        Create a new HList and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the list to (must have add_block method)
            style: The style of list (unordered, ordered, definition)
            default_style: Optional default style for child items. If None, inherits from container

        Returns:
            The newly created HList object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container)
        default_style = cls._inherit_style(container, default_style)

        # Create the new list
        hlist = cls(style, default_style)

        # Add the list to the container
        container.add_block(hlist)

        return hlist

    @property
    def style(self) -> ListStyle:
        """Get the list style"""
        return self._style

    @style.setter
    def style(self, style: ListStyle):
        """Set the list style"""
        self._style = style

    @property
    def default_style(self):
        """Get the default style for list items"""
        return self._default_style

    @default_style.setter
    def default_style(self, style):
        """Set the default style for list items"""
        self._default_style = style

    def add_item(self, item: 'ListItem'):
        """
        Add an item to this list.

        Args:
            item: The ListItem to add
        """
        self._items.append(item)
        item.parent = self

    def create_item(self, term: Optional[str] = None, style=None) -> 'ListItem':
        """
        Create a new list item and add it to this list.

        Args:
            term: Optional term for definition lists
            style: Optional style override. If None, inherits from list

        Returns:
            The newly created ListItem object
        """
        return ListItem.create_and_add_to(self, term, style)

    def items(self) -> Iterator['ListItem']:
        """
        Iterate over the items in this list.

        Yields:
            Each ListItem in the list
        """
        for item in self._items:
            yield item

    @property
    def item_count(self) -> int:
        """Get the number of items in this list"""
        return len(self._items)


class ListItem(BlockContainer, ContainerAware, Block):
    """
    A list item element that can contain other block elements.
    """

    def __init__(self, term: Optional[str] = None, style=None):
        """
        Initialize a list item.

        Args:
            term: Optional term for definition lists (dt element)
            style: Optional default style for child blocks
        """
        super().__init__(BlockType.LIST_ITEM)
        self._term = term
        self._style = style

    @classmethod
    def create_and_add_to(
            cls,
            container,
            term: Optional[str] = None,
            style=None) -> 'ListItem':
        """
        Create a new ListItem and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the list item to (must have add_item method)
            term: Optional term for definition lists (dt element)
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created ListItem object

        Raises:
            AttributeError: If the container doesn't have the required add_item method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container, required_method='add_item')
        style = cls._inherit_style(container, style)

        # Create the new list item
        item = cls(term, style)

        # Add the list item to the container
        container.add_item(item)

        return item

    @property
    def term(self) -> Optional[str]:
        """Get the definition term (for definition lists)"""
        return self._term

    @term.setter
    def term(self, term: str):
        """Set the definition term"""
        self._term = term

    @property
    def style(self):
        """Get the default style for this list item"""
        return self._style

    @style.setter
    def style(self, style):
        """Set the default style for this list item"""
        self._style = style


class TableCell(BlockContainer, ContainerAware, Block):
    """
    A table cell element that can contain other block elements.
    """

    def __init__(
            self,
            is_header: bool = False,
            colspan: int = 1,
            rowspan: int = 1,
            style=None):
        """
        Initialize a table cell.

        Args:
            is_header: Whether this cell is a header cell (th) or data cell (td)
            colspan: Number of columns this cell spans
            rowspan: Number of rows this cell spans
            style: Optional default style for child blocks
        """
        super().__init__(BlockType.TABLE_CELL)
        self._is_header = is_header
        self._colspan = colspan
        self._rowspan = rowspan
        self._style = style

    @classmethod
    def create_and_add_to(cls, container, is_header: bool = False, colspan: int = 1,
                          rowspan: int = 1, style=None) -> 'TableCell':
        """
        Create a new TableCell and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the cell to (must have add_cell method)
            is_header: Whether this cell is a header cell (th) or data cell (td)
            colspan: Number of columns this cell spans
            rowspan: Number of rows this cell spans
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created TableCell object

        Raises:
            AttributeError: If the container doesn't have the required add_cell method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container, required_method='add_cell')
        style = cls._inherit_style(container, style)

        # Create the new table cell
        cell = cls(is_header, colspan, rowspan, style)

        # Add the cell to the container
        container.add_cell(cell)

        return cell

    @property
    def is_header(self) -> bool:
        """Check if this is a header cell"""
        return self._is_header

    @is_header.setter
    def is_header(self, is_header: bool):
        """Set whether this is a header cell"""
        self._is_header = is_header

    @property
    def colspan(self) -> int:
        """Get the column span"""
        return self._colspan

    @colspan.setter
    def colspan(self, colspan: int):
        """Set the column span"""
        self._colspan = max(1, colspan)  # Ensure minimum of 1

    @property
    def rowspan(self) -> int:
        """Get the row span"""
        return self._rowspan

    @rowspan.setter
    def rowspan(self, rowspan: int):
        """Set the row span"""
        self._rowspan = max(1, rowspan)  # Ensure minimum of 1

    @property
    def style(self):
        """Get the default style for this table cell"""
        return self._style

    @style.setter
    def style(self, style):
        """Set the default style for this table cell"""
        self._style = style


class TableRow(ContainerAware, Block):
    """
    A table row element containing table cells.
    """

    def __init__(self, style=None):
        """
        Initialize an empty table row

        Args:
            style: Optional default style for child cells
        """
        super().__init__(BlockType.TABLE_ROW)
        self._cells: List[TableCell] = []
        self._style = style

    @classmethod
    def create_and_add_to(
            cls,
            container,
            section: str = "body",
            style=None) -> 'TableRow':
        """
        Create a new TableRow and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the row to (must have add_row method)
            section: The section to add the row to ("header", "body", or "footer")
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created TableRow object

        Raises:
            AttributeError: If the container doesn't have the required add_row method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container, required_method='add_row')
        style = cls._inherit_style(container, style)

        # Create the new table row
        row = cls(style)

        # Add the row to the container
        container.add_row(row, section)

        return row

    @property
    def style(self):
        """Get the default style for this table row"""
        return self._style

    @style.setter
    def style(self, style):
        """Set the default style for this table row"""
        self._style = style

    def add_cell(self, cell: TableCell):
        """
        Add a cell to this row.

        Args:
            cell: The TableCell to add
        """
        self._cells.append(cell)
        cell.parent = self

    def create_cell(
            self,
            is_header: bool = False,
            colspan: int = 1,
            rowspan: int = 1,
            style=None) -> TableCell:
        """
        Create a new table cell and add it to this row.

        Args:
            is_header: Whether this cell is a header cell
            colspan: Number of columns this cell spans
            rowspan: Number of rows this cell spans
            style: Optional style override. If None, inherits from row

        Returns:
            The newly created TableCell object
        """
        return TableCell.create_and_add_to(self, is_header, colspan, rowspan, style)

    def cells(self) -> Iterator[TableCell]:
        """
        Iterate over the cells in this row.

        Yields:
            Each TableCell in the row
        """
        for cell in self._cells:
            yield cell

    @property
    def cell_count(self) -> int:
        """Get the number of cells in this row"""
        return len(self._cells)


class Table(ContainerAware, Block):
    """
    A table element containing rows and cells.
    """

    def __init__(self, caption: Optional[str] = None, style=None):
        """
        Initialize a table.

        Args:
            caption: Optional caption for the table
            style: Optional default style for child rows
        """
        super().__init__(BlockType.TABLE)
        self._caption = caption
        self._rows: List[TableRow] = []
        self._header_rows: List[TableRow] = []
        self._footer_rows: List[TableRow] = []
        self._style = style

    @classmethod
    def create_and_add_to(
            cls,
            container,
            caption: Optional[str] = None,
            style=None) -> 'Table':
        """
        Create a new Table and add it to a container, inheriting style from
        the container if not explicitly provided.

        Args:
            container: The container to add the table to (must have add_block method)
            caption: Optional caption for the table
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created Table object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Validate container and inherit style using ContainerAware utilities
        cls._validate_container(container)
        style = cls._inherit_style(container, style)

        # Create the new table
        table = cls(caption, style)

        # Add the table to the container
        container.add_block(table)

        return table

    @property
    def caption(self) -> Optional[str]:
        """Get the table caption"""
        return self._caption

    @caption.setter
    def caption(self, caption: Optional[str]):
        """Set the table caption"""
        self._caption = caption

    @property
    def style(self):
        """Get the default style for this table"""
        return self._style

    @style.setter
    def style(self, style):
        """Set the default style for this table"""
        self._style = style

    def add_row(self, row: TableRow, section: str = "body"):
        """
        Add a row to this table.

        Args:
            row: The TableRow to add
            section: The section to add the row to ("header", "body", or "footer")
        """
        row.parent = self

        if section.lower() == "header":
            self._header_rows.append(row)
        elif section.lower() == "footer":
            self._footer_rows.append(row)
        else:  # Default to body
            self._rows.append(row)

    def create_row(self, section: str = "body", style=None) -> TableRow:
        """
        Create a new table row and add it to this table.

        Args:
            section: The section to add the row to ("header", "body", or "footer")
            style: Optional style override. If None, inherits from table

        Returns:
            The newly created TableRow object
        """
        return TableRow.create_and_add_to(self, section, style)

    def header_rows(self) -> Iterator[TableRow]:
        """
        Iterate over the header rows in this table.

        Yields:
            Each TableRow in the header section
        """
        for row in self._header_rows:
            yield row

    def body_rows(self) -> Iterator[TableRow]:
        """
        Iterate over the body rows in this table.

        Yields:
            Each TableRow in the body section
        """
        for row in self._rows:
            yield row

    def footer_rows(self) -> Iterator[TableRow]:
        """
        Iterate over the footer rows in this table.

        Yields:
            Each TableRow in the footer section
        """
        for row in self._footer_rows:
            yield row

    def all_rows(self) -> Iterator[Tuple[str, TableRow]]:
        """
        Iterate over all rows in this table with their section labels.

        Yields:
            Tuples of (section, row) for each row in the table
        """
        for row in self._header_rows:
            yield ("header", row)
        for row in self._rows:
            yield ("body", row)
        for row in self._footer_rows:
            yield ("footer", row)

    @property
    def row_count(self) -> Dict[str, int]:
        """Get the row counts by section"""
        return {
            "header": len(self._header_rows),
            "body": len(self._rows),
            "footer": len(self._footer_rows),
            "total": len(self._header_rows) + len(self._rows) + len(self._footer_rows)
        }


class Image(Block):
    """
    An image element with source, dimensions, and alternative text.
    """

    def __init__(
            self,
            source: str = "",
            alt_text: str = "",
            width: Optional[int] = None,
            height: Optional[int] = None):
        """
        Initialize an image element.

        Args:
            source: The image source URL or path
            alt_text: Alternative text for accessibility
            width: Optional image width in pixels
            height: Optional image height in pixels
        """
        super().__init__(BlockType.IMAGE)
        self._source = source
        self._alt_text = alt_text
        self._width = width
        self._height = height

    @classmethod
    def create_and_add_to(
            cls,
            container,
            source: str = "",
            alt_text: str = "",
            width: Optional[int] = None,
            height: Optional[int] = None) -> 'Image':
        """
        Create a new Image and add it to a container.

        Args:
            container: The container to add the image to (must have add_block method)
            source: The image source URL or path
            alt_text: Alternative text for accessibility
            width: Optional image width in pixels
            height: Optional image height in pixels

        Returns:
            The newly created Image object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Create the new image
        image = cls(source, alt_text, width, height)

        # Add the image to the container
        if hasattr(container, 'add_block'):
            container.add_block(image)
        else:
            raise AttributeError(
                f"Container {type(container).__name__} must have an 'add_block' method"
            )

        return image

    @property
    def source(self) -> str:
        """Get the image source"""
        return self._source

    @source.setter
    def source(self, source: str):
        """Set the image source"""
        self._source = source

    @property
    def alt_text(self) -> str:
        """Get the alternative text"""
        return self._alt_text

    @alt_text.setter
    def alt_text(self, alt_text: str):
        """Set the alternative text"""
        self._alt_text = alt_text

    @property
    def width(self) -> Optional[int]:
        """Get the image width"""
        return self._width

    @width.setter
    def width(self, width: Optional[int]):
        """Set the image width"""
        self._width = width

    @property
    def height(self) -> Optional[int]:
        """Get the image height"""
        return self._height

    @height.setter
    def height(self, height: Optional[int]):
        """Set the image height"""
        self._height = height

    def get_dimensions(self) -> Tuple[Optional[int], Optional[int]]:
        """
        Get the image dimensions as a tuple.

        Returns:
            Tuple of (width, height)
        """
        return (self._width, self._height)

    def get_aspect_ratio(self) -> Optional[float]:
        """
        Calculate the aspect ratio of the image.

        Returns:
            The aspect ratio (width/height) or None if either dimension is missing
        """
        if self._width is not None and self._height is not None and self._height > 0:
            return self._width / self._height
        return None

    def calculate_scaled_dimensions(self,
                                    max_width: Optional[int] = None,
                                    max_height: Optional[int] = None) -> Tuple[Optional[int],
                                                                               Optional[int]]:
        """
        Calculate scaled dimensions that fit within the given constraints.

        Args:
            max_width: Maximum allowed width
            max_height: Maximum allowed height

        Returns:
            Tuple of (scaled_width, scaled_height)
        """
        if self._width is None or self._height is None:
            return (self._width, self._height)

        width, height = self._width, self._height

        # Scale down if needed
        if max_width is not None and width > max_width:
            height = int(height * max_width / width)
            width = max_width

        if max_height is not None and height > max_height:
            width = int(width * max_height / height)
            height = max_height

        return (width, height)

    def _is_url(self, source: str) -> bool:
        """
        Check if the source is a URL.

        Args:
            source: The source string to check

        Returns:
            True if the source appears to be a URL, False otherwise
        """
        parsed = urllib.parse.urlparse(source)
        return bool(parsed.scheme and parsed.netloc)

    def _download_to_temp(self, url: str) -> str:
        """
        Download an image from a URL to a temporary file.

        Args:
            url: The URL to download from

        Returns:
            Path to the temporary file

        Raises:
            urllib.error.URLError: If the download fails
        """
        # Create a temporary file
        temp_fd, temp_path = tempfile.mkstemp(suffix='.tmp')

        try:
            # Download the image
            with urllib.request.urlopen(url) as response:
                # Write the response data to the temporary file
                with os.fdopen(temp_fd, 'wb') as temp_file:
                    temp_file.write(response.read())

            return temp_path
        except BaseException:
            # Clean up the temporary file if download fails
            try:
                os.close(temp_fd)
            except BaseException:
                pass
            try:
                os.unlink(temp_path)
            except BaseException:
                pass
            raise

    def load_image_data(self,
                        auto_update_dimensions: bool = True) -> Tuple[Optional[str],
                                                                      Optional[PILImage.Image]]:
        """
        Load image data using PIL, handling both local files and URLs.

        Args:
            auto_update_dimensions: If True, automatically update width and height from the loaded image

        Returns:
            Tuple of (file_path, PIL_Image_object). For URLs, file_path is the temporary file path.
            Returns (None, None) if loading fails.
        """
        if not self._source:
            return None, None

        file_path = None
        temp_file = None

        try:
            if self._is_url(self._source):
                # Download to temporary file
                temp_file = self._download_to_temp(self._source)
                file_path = temp_file
            else:
                # Use local file path
                file_path = self._source

            # Open with PIL
            with PILImage.open(file_path) as img:
                # Load the image data
                img.load()

                # Update dimensions if requested
                if auto_update_dimensions:
                    self._width, self._height = img.size

                # Return a copy to avoid issues with the context manager
                return file_path, img.copy()

        except Exception:
            # Clean up temporary file on error
            if temp_file and os.path.exists(temp_file):
                try:
                    os.unlink(temp_file)
                except BaseException:
                    pass
            return None, None

    def get_image_info(self) -> Dict[str, Any]:
        """
        Get detailed information about the image using PIL.

        Returns:
            Dictionary containing image information including format, mode, size, etc.
            Returns empty dict if image cannot be loaded.
        """
        file_path, img = self.load_image_data(auto_update_dimensions=False)

        if img is None:
            return {}

        # Try to determine format from the image, file extension, or source
        img_format = img.format
        if img_format is None:
            # Try to determine format from file extension
            format_map = {
                '.jpg': 'JPEG',
                '.jpeg': 'JPEG',
                '.png': 'PNG',
                '.gif': 'GIF',
                '.bmp': 'BMP',
                '.tiff': 'TIFF',
                '.tif': 'TIFF'
            }

            # First try the actual file path if available
            if file_path:
                ext = os.path.splitext(file_path)[1].lower()
                img_format = format_map.get(ext)

            # If still no format and we have a URL source, try the original URL
            if img_format is None and self._is_url(self._source):
                ext = os.path.splitext(
                    urllib.parse.urlparse(
                        self._source).path)[1].lower()
                img_format = format_map.get(ext)

        info = {
            'format': img_format,
            'mode': img.mode,
            'size': img.size,
            'width': img.width,
            'height': img.height,
        }

        # Add additional info if available
        if hasattr(img, 'info'):
            info['info'] = img.info

        # Clean up temporary file if it was created
        if file_path and self._is_url(self._source):
            try:
                os.unlink(file_path)
            except BaseException:
                pass

        return info


class LinkedImage(Image):
    """
    An Image that is also a Link - clickable images that navigate or trigger callbacks.
    """

    def __init__(self, source: str, alt_text: str, location: str,
                 width: Optional[int] = None, height: Optional[int] = None,
                 link_type=None,
                 callback: Optional[Any] = None,
                 params: Optional[Dict[str, Any]] = None,
                 title: Optional[str] = None):
        """
        Initialize a linked image.

        Args:
            source: The image source URL or path
            alt_text: Alternative text for accessibility
            location: The link target (URL, bookmark, etc.)
            width: Optional image width in pixels
            height: Optional image height in pixels
            link_type: Type of link (INTERNAL, EXTERNAL, etc.)
            callback: Optional callback for link activation
            params: Parameters for the link
            title: Tooltip/title for the link
        """
        # Initialize Image
        super().__init__(source, alt_text, width, height)

        # Store link properties
        # Import here to avoid circular imports at module level
        from pyWebLayout.abstract.functional import LinkType
        self._location = location
        self._link_type = link_type or LinkType.EXTERNAL
        self._callback = callback
        self._params = params or {}
        self._link_title = title

    @property
    def location(self) -> str:
        """Get the link target location"""
        return self._location

    @property
    def link_type(self):
        """Get the type of link"""
        return self._link_type

    @property
    def link_callback(self) -> Optional[Any]:
        """Get the link callback"""
        return self._callback

    @property
    def params(self) -> Dict[str, Any]:
        """Get the link parameters"""
        return self._params

    @property
    def link_title(self) -> Optional[str]:
        """Get the link title/tooltip"""
        return self._link_title

    def execute_link(self, context: Optional[Dict[str, Any]] = None) -> Any:
        """
        Execute the link action.

        Args:
            context: Optional context dict (e.g., {'alt_text': image.alt_text})

        Returns:
            The result of the link execution
        """
        from pyWebLayout.abstract.functional import LinkType

        # Add image info to context
        full_context = {
            **self._params,
            'alt_text': self._alt_text,
            'source': self._source}
        if context:
            full_context.update(context)

        if self._link_type in (LinkType.API, LinkType.FUNCTION) and self._callback:
            return self._callback(self._location, **full_context)
        else:
            # For INTERNAL and EXTERNAL links, return the location
            return self._location


class HorizontalRule(Block):
    """
    A horizontal rule element (hr tag).
    """

    def __init__(self):
        """Initialize a horizontal rule element."""
        super().__init__(BlockType.HORIZONTAL_RULE)

    @classmethod
    def create_and_add_to(cls, container) -> 'HorizontalRule':
        """
        Create a new HorizontalRule and add it to a container.

        Args:
            container: The container to add the horizontal rule to (must have add_block method)

        Returns:
            The newly created HorizontalRule object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Create the new horizontal rule
        hr = cls()

        # Add the horizontal rule to the container
        if hasattr(container, 'add_block'):
            container.add_block(hr)
        else:
            raise AttributeError(
                f"Container {type(container).__name__} must have an 'add_block' method"
            )

        return hr


class PageBreak(Block):
    """
    A page break element that forces content to start on a new page.

    When encountered during layout, this block signals that all subsequent
    content should be placed on a new page, even if the current page has
    available space.
    """

    def __init__(self):
        """Initialize a page break element."""
        super().__init__(BlockType.PAGE_BREAK)

    @classmethod
    def create_and_add_to(cls, container) -> 'PageBreak':
        """
        Create a new PageBreak and add it to a container.

        Args:
            container: The container to add the page break to (must have add_block method)

        Returns:
            The newly created PageBreak object

        Raises:
            AttributeError: If the container doesn't have the required add_block method
        """
        # Create the new page break
        page_break = cls()

        # Add the page break to the container
        if hasattr(container, 'add_block'):
            container.add_block(page_break)
        else:
            raise AttributeError(
                f"Container {type(container).__name__} must have an 'add_block' method"
            )

        return page_break
