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

from pyWebLayout.core.base import Renderable, Layoutable, Queriable
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Alignment
from .box import Box

class Page(Renderable, Queriable):
    """
    A page represents a canvas that can hold and render child renderable objects.
    It handles layout, rendering, and provides query capabilities to find which child
    contains a given point.
    """

    def __init__(self, size: Tuple[int, int], style: Optional[PageStyle] = None):
        """
        Initialize a new page.

        Args:
            size: The total size of the page (width, height) including borders
            style: The PageStyle defining borders, spacing, and appearance
        """
        self._size = size
        self._style = style if style is not None else PageStyle()
        self._children: List[Renderable] = []
        self._canvas: Optional[Image.Image] = None
        self._draw: Optional[ImageDraw.Draw] = None
        # Initialize y_offset to start of content area
        # Position the first line so its baseline is close to the top boundary
        # For subsequent lines, baseline-to-baseline spacing is used
        self._current_y_offset = self._style.border_width + self._style.padding_top
        self._is_first_line = True  # Track if we're placing the first line

    def free_space(self) -> Tuple[int, int]:
        """Get the remaining space on the page"""
        return (self._size[0], self._size[1] - self._current_y_offset)

    def can_fit_line(self, baseline_spacing: int, ascent: int = 0, descent: int = 0) -> bool:
        """
        Check if a line with the given metrics can fit on the page.
        
        Args:
            baseline_spacing: Distance from current position to next baseline
            ascent: Font ascent (height above baseline), defaults to 0 for backward compat
            descent: Font descent (height below baseline), defaults to 0 for backward compat
            
        Returns:
            True if the line fits within page boundaries
        """
        # Calculate the maximum Y position allowed (bottom boundary)
        max_y = self._size[1] - self._style.border_width - self._style.padding_bottom
        
        # If ascent/descent not provided, use simple check (backward compatibility)
        if ascent == 0 and descent == 0:
            return (self._current_y_offset + baseline_spacing) <= max_y
        
        # Calculate where the bottom of the text would be
        # Text bottom = current_y_offset + ascent + descent
        text_bottom = self._current_y_offset + ascent + descent
        
        # Check if text bottom would exceed the boundary
        return text_bottom <= max_y

    @property
    def size(self) -> Tuple[int, int]:
        """Get the total page size including borders"""
        return self._size

    @property
    def canvas_size(self) -> Tuple[int, int]:
        """Get the canvas size (page size minus borders)"""
        border_reduction = self._style.total_border_width
        return (
            self._size[0] - border_reduction,
            self._size[1] - border_reduction
        )

    @property
    def content_size(self) -> Tuple[int, int]:
        """Get the content area size (canvas minus padding)"""
        canvas_w, canvas_h = self.canvas_size
        return (
            canvas_w - self._style.total_horizontal_padding,
            canvas_h - self._style.total_vertical_padding
        )

    @property
    def border_size(self) -> int:
        """Get the border width"""
        return self._style.border_width

    @property
    def available_width(self) -> int:
        """Get the available width for content (content area width)"""
        return self.content_size[0]

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

    @property
    def draw(self) -> Optional[ImageDraw.Draw]:
        """Get the ImageDraw object for drawing on this page's canvas"""
        if self._draw is None:
            # Initialize canvas and draw context if not already done
            self._canvas = self._create_canvas()
            self._draw = ImageDraw.Draw(self._canvas)
        return self._draw

    def add_child(self, child: Renderable) -> 'Page':
        """
        Add a child renderable object to this page.

        Args:
            child: The renderable object to add

        Returns:
            Self for method chaining
        """
        self._children.append(child)
        self._current_y_offset = child.origin[1] + child.size[1]
        # Invalidate the canvas when children change
        self._canvas = None
        return self

    def remove_child(self, child: Renderable) -> bool:
        """
        Remove a child from the page.

        Args:
            child: The child to remove

        Returns:
            True if the child was found and removed, False otherwise
        """
        try:
            self._children.remove(child)
            self._canvas = None
            return True
        except ValueError:
            return False

    def clear_children(self) -> 'Page':
        """
        Remove all children from the page.

        Returns:
            Self for method chaining
        """
        self._children.clear()
        self._canvas = None
        # Reset y_offset to start of content area (after border and padding)
        self._current_y_offset = self._style.border_width + self._style.padding_top
        return self

    @property
    def children(self) -> List[Renderable]:
        """Get a copy of the children list"""
        return self._children.copy()

    def _get_child_height(self, child: Renderable) -> int:
        """
        Get the height of a child object.

        Args:
            child: The child to measure

        Returns:
            Height in pixels
        """
        if hasattr(child, '_size') and child._size is not None:
            if isinstance(child._size, (list, tuple, np.ndarray)) and len(child._size) >= 2:
                return int(child._size[1])

        if hasattr(child, 'size') and child.size is not None:
            if isinstance(child.size, (list, tuple, np.ndarray)) and len(child.size) >= 2:
                return int(child.size[1])

        if hasattr(child, 'height'):
            return int(child.height)

        # Default fallback height
        return 20

    def render_children(self):
        """
        Call render on all children in the list.
        Children draw directly onto the page's canvas via the shared ImageDraw object.
        """
        for child in self._children:
            # Synchronize draw context for Line objects before rendering
            if hasattr(child, '_draw'):
                child._draw = self._draw
            # Synchronize canvas for Image objects before rendering
            if hasattr(child, '_canvas'):
                child._canvas = self._canvas
            if hasattr(child, 'render'):
                child.render()

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

        Returns:
            PIL Image containing the rendered page
        """
        # Create the base canvas and draw object
        self._canvas = self._create_canvas()
        self._draw = ImageDraw.Draw(self._canvas)

        # Render all children - they draw directly onto the canvas
        self.render_children()

        return self._canvas

    def _create_canvas(self) -> Image.Image:
        """
        Create the base canvas with background and borders.

        Returns:
            PIL Image with background and borders applied
        """
        # Create base image
        canvas = Image.new('RGBA', self._size, (*self._style.background_color, 255))

        # Draw borders if needed
        if self._style.border_width > 0:
            draw = ImageDraw.Draw(canvas)
            border_color = (*self._style.border_color, 255)

            # Draw border rectangle inside the content area
            border_offset = self._style.border_width
            draw.rectangle([
                (border_offset, border_offset),
                (self._size[0] - border_offset - 1, self._size[1] - border_offset - 1)
            ], outline=border_color)

        return canvas

    def _get_child_position(self, child: Renderable) -> Tuple[int, int]:
        """
        Get the position where a child should be rendered.

        Args:
            child: The child object

        Returns:
            Tuple of (x, y) coordinates
        """
        if hasattr(child, '_origin') and child._origin is not None:
            if isinstance(child._origin, np.ndarray):
                return (int(child._origin[0]), int(child._origin[1]))
            elif isinstance(child._origin, (list, tuple)) and len(child._origin) >= 2:
                return (int(child._origin[0]), int(child._origin[1]))

        if hasattr(child, 'position'):
            pos = child.position
            if isinstance(pos, (list, tuple)) and len(pos) >= 2:
                return (int(pos[0]), int(pos[1]))

        # Default to origin
        return (0, 0)

    def query_point(self, point: Tuple[int, int]) -> Optional[Renderable]:
        """
        Query a point to determine which child it belongs to.

        Args:
            point: The (x, y) coordinates to query

        Returns:
            The child object that contains the point, or None if no child contains it
        """
        point_array = np.array(point)

        # Check each child (in reverse order so topmost child is found first)
        for child in reversed(self._children):
            if self._point_in_child(point_array, child):
                return child

        return None

    def _point_in_child(self, point: np.ndarray, child: Renderable) -> bool:
        """
        Check if a point is within a child's bounds.

        Args:
            point: The point to check
            child: The child to check against

        Returns:
            True if the point is within the child's bounds
        """
        # If child implements Queriable interface, use it
        if isinstance(child, Queriable) and hasattr(child, 'in_object'):
            try:
                return child.in_object(point)
            except:
                pass  # Fall back to bounds checking

        # Get child position and size for bounds checking
        child_pos = self._get_child_position(child)
        child_size = self._get_child_size(child)

        if child_size is None:
            return False

        # Check if point is within child bounds
        return (
            child_pos[0] <= point[0] < child_pos[0] + child_size[0] and
            child_pos[1] <= point[1] < child_pos[1] + child_size[1]
        )

    def _get_child_size(self, child: Renderable) -> Optional[Tuple[int, int]]:
        """
        Get the size of a child object.

        Args:
            child: The child to measure

        Returns:
            Tuple of (width, height) or None if size cannot be determined
        """
        if hasattr(child, '_size') and child._size is not None:
            if isinstance(child._size, (list, tuple, np.ndarray)) and len(child._size) >= 2:
                return (int(child._size[0]), int(child._size[1]))

        if hasattr(child, 'size') and child.size is not None:
            if isinstance(child.size, (list, tuple, np.ndarray)) and len(child.size) >= 2:
                return (int(child.size[0]), int(child.size[1]))

        if hasattr(child, 'width') and hasattr(child, 'height'):
            return (int(child.width), int(child.height))

        return None

    def in_object(self, point: Tuple[int, int]) -> bool:
        """
        Check if a point is within this page's bounds.

        Args:
            point: The (x, y) coordinates to check

        Returns:
            True if the point is within the page bounds
        """
        return (
            0 <= point[0] < self._size[0] and
            0 <= point[1] < self._size[1]
        )
