from typing import List, Optional
from PIL import Image, ImageDraw, ImageFont
from dataclasses import dataclass
from .engine import Engine, CtxEngine
from .components import Text, Img
import os
import tempfile
import webbrowser

@dataclass
class CtxImgEngine(CtxEngine):
    """
    Context object for a single image page, storing the PIL Image and drawing context.

    Attributes:
        img (Image.Image): The PIL Image object for this page.
        draw (ImageDraw.ImageDraw): Drawing context to render components onto the image.
    """
    img: Image.Image
    draw: ImageDraw.ImageDraw


class ImgEngine(Engine):
    """
    Engine implementation to generate image pages using PIL.

    Handles creating new pages and rendering components (text and images) onto them.
    """

    def new_page(self, options):
        """
        Create a new image page based on the provided options.

        Args:
            options: PageOptions object containing width, height, background image/path or color.

        Returns:
            CtxImgEngine: A new image context with a drawing object.

        Raises:
            Exception: If width and height are not defined for a page without a background image.
        """
        if not options.background:
            if not options.width or not options.height:
                raise Exception(f"Define width and height for {options.name}")
            img = Image.new("RGBA", (options.width, options.height), options.background_color)
        else:
            img = Image.open(options.background)
            w, h = img.size
            if options.width:
                w = options.width
            if options.height:
                h = options.height
            img = img.resize((w, h))
            options.width = w
            options.height = h
        return CtxImgEngine(img=img, draw=ImageDraw.Draw(img), options=options)

    def make_component(self, page: CtxImgEngine, component):
        """
        Render a component (Text or Img) onto the given image page.

        Args:
            page (CtxImgEngine): The image context to render onto.
            component: Component instance (Text or Img).

        Raises:
            Exception: If the component image path is not defined.
        """
        if isinstance(component, Text):
            font = component.get_font()
            if font:
                font = ImageFont.truetype(font, size=component.get_size())
            else:
                font = ImageFont.load_default(size=component.get_size())

            value = component.get_value()
            x, y = component.get_position()
            
            if component.get_dimension_r():
                text_size = page.draw.textlength(value, font)
                x = (x + component.get_dimension_r() - text_size) // 2
   
            page.draw.text(
                (x, y),
                value,
                fill=component.get_color(),
                font=font
            )
            
        elif isinstance(component, Img):
            path = component.get_path()
            if not path:
                raise Exception("Image path is not defined!")
            img = Image.open(path)
            img = img.resize(component.get_dimension(), Image.LANCZOS)
            page.img.paste(img, component.get_position())


class ImageBuild:
    """
    Handles a collection of image contexts generated by the build process.

    Provides methods to convert images to PIL objects, bytes, save to disk, and preview in the browser.

    Attributes:
        _instances (List[CtxImgEngine]): List of image contexts.
        _default_suffix (str): Default file extension used when saving temporary files.
    """

    def __init__(self, instances: List[CtxImgEngine]):
        """
        Initialize ImageBuild with a list of image contexts.

        Args:
            instances (List[CtxImgEngine]): List of image page contexts.
        """
        self._instances = instances
        self._default_suffix = ".png"

    def to_images(self) -> List[Image.Image]:
        """
        Convert all image contexts to PIL Image objects.

        Returns:
            List[Image.Image]: List of images.
        """
        return [instance.img for instance in self._instances]

    def to_image(self) -> Image.Image:
        """
        Return a single image instance.

        Raises:
            ValueError: If multiple pages exist; use `to_images()` instead.

        Returns:
            Image.Image: The single image instance.
        """
        if len(self._instances) != 1:
            raise ValueError("Use to_images() when multiple pages exist")
        return self._instances[0].img

    def save(self, path: str) -> None:
        """
        Save images to disk as individual PNG files or as a PDF if requested.

        Args:
            path (str): Target file path or directory.

        Raises:
            ValueError: If there are no images to save.
        """
        if len(self._instances) == 0:
            raise ValueError("No pages to save.")
        looks_like_dir = path.endswith(os.sep) or os.path.isdir(path)
        if len(self._instances) == 1:
            ctx = self._instances[0]
            if looks_like_dir:
                os.makedirs(path, exist_ok=True)
                out_path = os.path.join(path, ctx.options.name)
            else:
                parent = os.path.dirname(path)
                if parent:
                    os.makedirs(parent, exist_ok=True)
                out_path = path
            ctx.img.save(out_path)
            return

        if path.lower().endswith(".pdf"):
            pages_rgb = [instance.img.convert("RGB") for instance in self._instances]
            parent = os.path.dirname(path)
            if parent:
                os.makedirs(parent, exist_ok=True)
            pages_rgb[0].save(path, save_all=True, append_images=pages_rgb[1:])
        else:
            os.makedirs(path, exist_ok=True)
            for i, instance in enumerate(self._instances, start=1):
                instance.img.save(os.path.join(path, f"{i}-page_{instance.options.name}.png"))

    def to_bytes(self, page: Optional[int] = None) -> bytes | List[bytes]:
        """
        Convert images to bytes.

        Args:
            page (Optional[int]): 1-based index of the page to convert. If None, all pages are returned.

        Returns:
            bytes: Single page bytes.
            List[bytes]: Multiple pages bytes.

        Raises:
            IndexError: If the requested page index is out of range.
        """
        from io import BytesIO
        if len(self._instances) == 0:
            return b""

        def img_to_bytes(img: Image.Image) -> bytes:
            buf = BytesIO()
            img.save(buf, format="PNG")
            return buf.getvalue()

        if page is not None:
            if page < 1 or page > len(self._instances):
                raise IndexError(f"Page {page} out of range (1-{len(self._instances)})")
            return img_to_bytes(self._instances[page - 1])
        else:
            return [img_to_bytes(instance.img) for instance in self._instances]

    def show(self, page: Optional[int] = None) -> None:
        """
        Display images in the default web browser.

        Args:
            page (Optional[int]): If provided, only this page is displayed; otherwise all pages.

        Notes:
            - Uses temporary files to open the images.
            - Supports multiple pages.
        """
        data = self.to_bytes(page=page)
        suffix = getattr(self, "_default_suffix", ".bin")

        if isinstance(data, list):  # multiple pages
            for idx, d in enumerate(data, start=1):
                tmp = tempfile.NamedTemporaryFile(delete=False, suffix=f"_{idx}{suffix}")
                tmp.write(d)
                tmp.flush()
                tmp.close()
                webbrowser.open(f"file://{tmp.name}")
        else:  # single page
            tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
            tmp.write(data)
            tmp.flush()
            tmp.close()
            webbrowser.open(f"file://{tmp.name}")
