from abc import ABC
from typing import Optional, Tuple, TYPE_CHECKING, Any, Dict
import numpy as np


if TYPE_CHECKING:
    from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration


class Renderable(ABC):
    """
    Abstract base class for any object that can be rendered to an image.
    All renderable objects must implement the render method.
    """

    def render(self):
        """
        Render the object to an image.

        Returns:
            PIL.Image: The rendered image
        """

    @property
    def origin(self):
        return self._origin


class Interactable(ABC):
    """
    Abstract base class for any object that can be interacted with.
    Interactable objects must have a callback that is executed when interacted with.
    """

    def __init__(self, callback=None):
        """
        Initialize an interactable object.

        Args:
            callback: The function to call when this object is interacted with
        """
        self._callback = callback

    def interact(self, point: np.generic):
        """
        Handle interaction at the given point.

        Args:
            point: The coordinates of the interaction

        Returns:
            The result of calling the callback function with the point
        """
        if self._callback is None:
            return None
        return self._callback(point)


class Layoutable(ABC):
    """
    Abstract base class for any object that can be laid out.
    Layoutable objects must implement the layout method which arranges their contents.
    """

    def layout(self):
        """
        Layout the object's contents.
        This method should be called before rendering to properly arrange the object's contents.
        """


class Queriable(ABC):

    def in_object(self, point: np.generic):
        """
        check if a point is in the object
        """
        point_array = np.array(point)
        relative_point = point_array - self._origin
        return np.all((0 <= relative_point) & (relative_point < self.size))


# ==============================================================================
# Mixins - Reusable components for common patterns
# ==============================================================================


class Hierarchical:
    """
    Mixin providing parent-child relationship management.

    Classes using this mixin can track their parent in a document hierarchy.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._parent: Optional[Any] = None

    @property
    def parent(self) -> Optional[Any]:
        """Get the parent object containing this object, if any"""
        return self._parent

    @parent.setter
    def parent(self, parent: Any):
        """Set the parent object"""
        self._parent = parent


class Geometric:
    """
    Mixin providing origin and size properties for positioned elements.

    Provides standard geometric properties for elements that have a position
    and size in 2D space. Uses numpy arrays for efficient calculations.
    """

    def __init__(self, *args, origin=None, size=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._origin = np.array(origin) if origin is not None else np.array([0, 0])
        self._size = np.array(size) if size is not None else np.array([0, 0])

    @property
    def origin(self) -> np.ndarray:
        """Get the origin (top-left corner) of the element"""
        return self._origin

    @origin.setter
    def origin(self, origin: np.ndarray):
        """Set the origin of the element"""
        self._origin = np.array(origin)

    @property
    def size(self) -> np.ndarray:
        """Get the size (width, height) of the element"""
        return self._size

    @size.setter
    def size(self, size: np.ndarray):
        """Set the size of the element"""
        self._size = np.array(size)

    def set_origin(self, origin: np.ndarray):
        """Set the origin of this element (alternative setter method)"""
        self._origin = np.array(origin)


class Styleable:
    """
    Mixin providing style property management.

    Classes using this mixin can have a style property that can be
    inherited from parents or set explicitly.
    """

    def __init__(self, *args, style=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._style = style

    @property
    def style(self) -> Optional[Any]:
        """Get the style for this element"""
        return self._style

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


class FontRegistry:
    """
    Mixin providing font caching and creation with parent delegation.

    This mixin allows classes to maintain a local font registry and create/reuse
    Font objects efficiently. It supports parent delegation, where font requests
    can cascade up to a parent container if one exists.

    Classes using this mixin should also use Hierarchical to support parent delegation.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._fonts: Dict[str, 'Font'] = {}

    def get_or_create_font(self,
                           font_path: Optional[str] = None,
                           font_size: int = 16,
                           colour: Tuple[int, int, int] = (0, 0, 0),
                           weight: 'FontWeight' = None,
                           style: 'FontStyle' = None,
                           decoration: 'TextDecoration' = None,
                           background: Optional[Tuple[int, int, int, int]] = None,
                           language: str = "en_EN",
                           min_hyphenation_width: Optional[int] = None) -> 'Font':
        """
        Get or create a font with the specified properties.

        This method will first check if a parent object has a get_or_create_font
        method and delegate to it. Otherwise, it will manage fonts locally.

        Args:
            font_path: Path to the font file (.ttf, .otf). If None, uses default font.
            font_size: Size of the font in points.
            colour: RGB color tuple for the text.
            weight: Font weight (normal or bold).
            style: Font style (normal or italic).
            decoration: Text decoration (none, underline, or strikethrough).
            background: RGBA background color for the text. If None, transparent background.
            language: Language code for hyphenation and text processing.
            min_hyphenation_width: Minimum width in pixels required for hyphenation.

        Returns:
            Font object (either existing or newly created)
        """
        # Import here to avoid circular imports
        from pyWebLayout.style import Font, FontWeight, FontStyle, TextDecoration

        # Set defaults for enum types
        if weight is None:
            weight = FontWeight.NORMAL
        if style is None:
            style = FontStyle.NORMAL
        if decoration is None:
            decoration = TextDecoration.NONE

        # If we have a parent with font management, delegate to parent
        if hasattr(
                self,
                '_parent') and self._parent and hasattr(
                self._parent,
                'get_or_create_font'):
            return self._parent.get_or_create_font(
                font_path=font_path,
                font_size=font_size,
                colour=colour,
                weight=weight,
                style=style,
                decoration=decoration,
                background=background,
                language=language,
                min_hyphenation_width=min_hyphenation_width
            )

        # Otherwise manage our own fonts
        # Create a unique key for this font configuration
        bg_tuple = background if background else (255, 255, 255, 0)
        min_hyph_width = min_hyphenation_width if min_hyphenation_width is not None else font_size * 4

        font_key = (
            font_path,
            font_size,
            colour,
            weight.value if hasattr(weight, 'value') else weight,
            style.value if hasattr(style, 'value') else style,
            decoration.value if hasattr(decoration, 'value') else decoration,
            bg_tuple,
            language,
            min_hyph_width
        )

        # Convert tuple to string for dictionary key
        key_str = str(font_key)

        # Check if we already have this font
        if key_str in self._fonts:
            return self._fonts[key_str]

        # Create new font and store it
        new_font = Font(
            font_path=font_path,
            font_size=font_size,
            colour=colour,
            weight=weight,
            style=style,
            decoration=decoration,
            background=background,
            language=language,
            min_hyphenation_width=min_hyphenation_width
        )

        self._fonts[key_str] = new_font
        return new_font


class MetadataContainer:
    """
    Mixin providing metadata dictionary management.

    Allows classes to store and retrieve arbitrary metadata as key-value pairs.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._metadata: Dict[Any, Any] = {}

    def set_metadata(self, key: Any, value: Any):
        """
        Set a metadata value.

        Args:
            key: The metadata key
            value: The metadata value
        """
        self._metadata[key] = value

    def get_metadata(self, key: Any) -> Optional[Any]:
        """
        Get a metadata value.

        Args:
            key: The metadata key

        Returns:
            The metadata value, or None if not set
        """
        return self._metadata.get(key)


class BlockContainer:
    """
    Mixin providing block management methods.

    Provides standard methods for managing block-level children including
    adding blocks and creating common block types.

    Classes using this mixin should also use Styleable to support style inheritance.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._blocks = []

    def blocks(self):
        """
        Get an iterator over the blocks in this container.

        Can be used as blocks() for iteration or accessing the _blocks list directly.

        Returns:
            Iterator over blocks
        """
        return iter(self._blocks)

    def add_block(self, block):
        """
        Add a block to this container.

        Args:
            block: The block to add
        """
        self._blocks.append(block)
        if hasattr(block, 'parent'):
            block.parent = self

    def create_paragraph(self, style=None):
        """
        Create a new paragraph and add it to this container.

        Args:
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created Paragraph object
        """
        from pyWebLayout.abstract.block import Paragraph

        if style is None and hasattr(self, '_style'):
            style = self._style

        paragraph = Paragraph(style)
        self.add_block(paragraph)
        return paragraph

    def create_heading(self, level=None, style=None):
        """
        Create a new heading and add it to this container.

        Args:
            level: The heading level (h1-h6)
            style: Optional style override. If None, inherits from container

        Returns:
            The newly created Heading object
        """
        from pyWebLayout.abstract.block import Heading, HeadingLevel

        if level is None:
            level = HeadingLevel.H1

        if style is None and hasattr(self, '_style'):
            style = self._style

        heading = Heading(level, style)
        self.add_block(heading)
        return heading


class ContainerAware:
    """
    Mixin providing support for the create_and_add_to factory pattern.

    This is a base that can be extended to provide the create_and_add_to
    class method pattern used throughout the abstract module.

    Note: This is a framework for future refactoring. Currently, each class
    has its own create_and_add_to implementation due to varying constructor
    signatures. This mixin provides a foundation for standardizing that pattern.
    """

    @classmethod
    def _validate_container(cls, container, required_method='add_block'):
        """
        Validate that a container has the required method.

        Args:
            container: The container to validate
            required_method: The method name to check for

        Raises:
            AttributeError: If the container doesn't have the required method
        """
        if not hasattr(container, required_method):
            raise AttributeError(
                f"Container {type(container).__name__} must have a '{required_method}' method"
            )

    @classmethod
    def _inherit_style(cls, container, style=None):
        """
        Inherit style from container if not explicitly provided.

        Args:
            container: The container to inherit from
            style: Optional explicit style

        Returns:
            The style to use (explicit or inherited)
        """
        if style is not None:
            return style

        if hasattr(container, 'style'):
            return container.style
        elif hasattr(container, 'default_style'):
            return container.default_style

        return None
