import os
from typing import Optional
import numpy as np
from PIL import Image as PILImage, ImageDraw, ImageFont
from pyWebLayout.core.base import Renderable, Queriable
from pyWebLayout.abstract.block import Image as AbstractImage
from pyWebLayout.style import Alignment


class RenderableImage(Renderable, Queriable):
    """
    A concrete implementation for rendering Image objects.
    """

    def __init__(self, image: AbstractImage, canvas: PILImage.Image,
                 max_width: Optional[int] = None, max_height: Optional[int] = None,
                 origin=None, size=None, callback=None, sheet=None, mode=None,
                 halign=Alignment.CENTER, valign=Alignment.CENTER):
        """
        Initialize a renderable image.

        Args:
            image: The abstract Image object to render
            draw: The PIL ImageDraw object to draw on
            max_width: Maximum width constraint for the image
            max_height: Maximum height constraint for the image
            origin: Optional origin coordinates
            size: Optional size override
            callback: Optional callback function
            sheet: Optional sheet for rendering
            mode: Optional image mode
            halign: Horizontal alignment
            valign: Vertical alignment
        """
        super().__init__()
        self._abstract_image = image
        self._canvas = canvas
        self._pil_image = None
        self._error_message = None
        self._halign = halign
        self._valign = valign

        # Set origin as numpy array
        self._origin = np.array(origin) if origin is not None else np.array([0, 0])

        # Try to load the image
        self._load_image()

        # Calculate the size if not provided
        if size is None:
            size = image.calculate_scaled_dimensions(max_width, max_height)
            # Ensure we have valid dimensions, fallback to defaults if None
            if size[0] is None or size[1] is None:
                size = (100, 100)  # Default size when image dimensions are unavailable

        # Ensure dimensions are positive (can be negative if calculated from insufficient space)
        size = (max(1, size[0]), max(1, size[1]))

        # Set size as numpy array
        self._size = np.array(size)

    @property
    def origin(self) -> np.ndarray:
        """Get the origin of the image"""
        return self._origin

    @property
    def size(self) -> np.ndarray:
        """Get the size of the image"""
        return self._size

    @property
    def width(self) -> int:
        """Get the width of the image"""
        return self._size[0]

    def set_origin(self, origin: np.ndarray):
        """Set the origin of this image element"""
        self._origin = origin

    def _load_image(self):
        """Load the image from the source path"""
        try:
            # Check if the image has already been loaded into memory
            if hasattr(
                    self._abstract_image,
                    '_loaded_image') and self._abstract_image._loaded_image is not None:
                self._pil_image = self._abstract_image._loaded_image
                return

            source = self._abstract_image.source

            # Handle different types of sources
            if os.path.isfile(source):
                # Local file
                self._pil_image = PILImage.open(source)
                self._abstract_image._loaded_image = self._pil_image
            elif source.startswith(('http://', 'https://')):
                # URL - requires requests library
                try:
                    import requests
                    from io import BytesIO

                    response = requests.get(source, stream=True)
                    if response.status_code == 200:
                        self._pil_image = PILImage.open(BytesIO(response.content))
                        self._abstract_image._loaded_image = self._pil_image
                    else:
                        self._error_message = f"Failed to load image: HTTP status {response.status_code}"
                except ImportError:
                    self._error_message = "Requests library not available for URL loading"
            else:
                self._error_message = f"Unable to load image from source: {source}"

        except Exception as e:
            self._error_message = f"Error loading image: {str(e)}"
            self._abstract_image._error = self._error_message

    def render(self):
        """
        Render the image directly into the canvas using the provided draw object.
        """
        if self._pil_image:
            # Resize the image to fit the box while maintaining aspect ratio
            resized_image = self._resize_image()

            # Calculate position based on alignment
            img_width, img_height = resized_image.size
            box_width, box_height = self._size

            # Horizontal alignment
            if self._halign == Alignment.LEFT:
                x_offset = 0
            elif self._halign == Alignment.RIGHT:
                x_offset = box_width - img_width
            else:  # CENTER is default
                x_offset = (box_width - img_width) // 2

            # Vertical alignment
            if self._valign == Alignment.TOP:
                y_offset = 0
            elif self._valign == Alignment.BOTTOM:
                y_offset = box_height - img_height
            else:  # CENTER is default
                y_offset = (box_height - img_height) // 2

            # Calculate final position on canvas
            final_x = int(self._origin[0] + x_offset)
            final_y = int(self._origin[1] + y_offset)

            # Get the underlying image from the draw object to paste onto

            self._canvas.paste(
                resized_image,
                (final_x,
                 final_y,
                 final_x +
                 img_width,
                 final_y +
                 img_height))
        else:
            # Draw error placeholder
            self._draw_error_placeholder()

    def _resize_image(self) -> PILImage.Image:
        """
        Resize the image to fit within the box while maintaining aspect ratio.

        Returns:
            A resized PIL Image
        """
        if not self._pil_image:
            return PILImage.new('RGBA', tuple(self._size), (200, 200, 200, 100))

        # Get the target dimensions
        target_width, target_height = self._size

        # Ensure target dimensions are positive
        target_width = max(1, int(target_width))
        target_height = max(1, int(target_height))

        # Get the original dimensions
        orig_width, orig_height = self._pil_image.size

        # Calculate the scaling factor to maintain aspect ratio
        width_ratio = target_width / orig_width
        height_ratio = target_height / orig_height

        # Use the smaller ratio to ensure the image fits within the box
        ratio = min(width_ratio, height_ratio)

        # Calculate new dimensions
        new_width = max(1, int(orig_width * ratio))
        new_height = max(1, int(orig_height * ratio))

        # Resize the image
        if self._pil_image.mode == 'RGBA':
            resized = self._pil_image.resize((new_width, new_height), PILImage.LANCZOS)
        else:
            # Convert to RGBA if needed
            resized = self._pil_image.convert('RGBA').resize(
                (new_width, new_height), PILImage.LANCZOS)

        return resized

    def _draw_error_placeholder(self):
        """
        Draw a placeholder for when the image can't be loaded.
        """
        # Calculate the rectangle coordinates with origin offset
        x1 = int(self._origin[0])
        y1 = int(self._origin[1])
        x2 = int(self._origin[0] + self._size[0])
        y2 = int(self._origin[1] + self._size[1])

        self._draw = ImageDraw.Draw(self._canvas)
        # Draw a gray box with a border
        self._draw.rectangle([(x1, y1), (x2, y2)], fill=(
            240, 240, 240), outline=(180, 180, 180), width=2)

        # Draw an X across the box
        self._draw.line([(x1, y1), (x2, y2)], fill=(180, 180, 180), width=2)
        self._draw.line([(x1, y2), (x2, y1)], fill=(180, 180, 180), width=2)

        # Add error text if available
        if self._error_message:
            try:
                # Try to use a basic font
                font = ImageFont.load_default()

                # Draw the error message, wrapped to fit
                error_text = "Error: " + self._error_message

                # Simple text wrapping - split by words and add lines
                words = error_text.split()
                lines = []
                current_line = ""

                for word in words:
                    test_line = current_line + " " + word if current_line else word
                    text_bbox = self._draw.textbbox((0, 0), test_line, font=font)
                    text_width = text_bbox[2] - text_bbox[0]

                    if text_width <= self._size[0] - 20:  # 10px padding on each side
                        current_line = test_line
                    else:
                        lines.append(current_line)
                        current_line = word

                if current_line:
                    lines.append(current_line)

                # Draw each line
                y_pos = y1 + 10
                for line in lines:
                    text_bbox = self._draw.textbbox((0, 0), line, font=font)
                    text_width = text_bbox[2] - text_bbox[0]
                    text_height = text_bbox[3] - text_bbox[1]

                    # Center the text horizontally
                    x_pos = x1 + (self._size[0] - text_width) // 2

                    # Draw the text
                    self._draw.text((x_pos, y_pos), line, fill=(80, 80, 80), font=font)

                    # Move to the next line
                    y_pos += text_height + 2

            except Exception:
                # If text rendering fails, just draw a generic error indicator
                pass

    def in_object(self, point):
        """Check if a point is within this image"""
        point_array = np.array(point)
        relative_point = point_array - self._origin

        # Check if the point is within the image boundaries
        return (0 <= relative_point[0] < self._size[0] and
                0 <= relative_point[1] < self._size[1])
