#
# The Python Imaging Library.
# $Id$
#
# a Tk display interface
#
# History:
# 96-04-08 fl   Created
# 96-09-06 fl   Added getimage method
# 96-11-01 fl   Rewritten, removed image attribute and crop method
# 97-05-09 fl   Use PyImagingPaste method instead of image type
# 97-05-12 fl   Minor tweaks to match the IFUNC95 interface
# 97-05-17 fl   Support the "pilbitmap" booster patch
# 97-06-05 fl   Added file= and data= argument to image constructors
# 98-03-09 fl   Added width and height methods to Image classes
# 98-07-02 fl   Use default mode for "P" images without palette attribute
# 98-07-02 fl   Explicitly destroy Tkinter image objects
# 99-07-24 fl   Support multiple Tk interpreters (from Greg Couch)
# 99-07-26 fl   Automatically hook into Tkinter (if possible)
# 99-08-15 fl   Hook uses _imagingtk instead of _imaging
#
# Copyright (c) 1997-1999 by Secret Labs AB
# Copyright (c) 1996-1997 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations

import tkinter
from io import BytesIO
from typing import TYPE_CHECKING, Any, cast

from . import Image, ImageFile

if TYPE_CHECKING:
    from ._typing import CapsuleType

# --------------------------------------------------------------------
# Check for Tkinter interface hooks


def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
    source = None
    if "file" in kw:
        source = kw.pop("file")
    elif "data" in kw:
        source = BytesIO(kw.pop("data"))
    if not source:
        return None
    return Image.open(source)


def _pyimagingtkcall(
    command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
) -> None:
    tk = photo.tk
    try:
        tk.call(command, photo, repr(ptr))
    except tkinter.TclError:
        # activate Tkinter hook
        # may raise an error if it cannot attach to Tkinter
        from . import _imagingtk

        _imagingtk.tkinit(tk.interpaddr())
        tk.call(command, photo, repr(ptr))


# --------------------------------------------------------------------
# PhotoImage


class PhotoImage:
    """
    A Tkinter-compatible photo image.  This can be used
    everywhere Tkinter expects an image object.  If the image is an RGBA
    image, pixels having alpha 0 are treated as transparent.

    The constructor takes either a PIL image, or a mode and a size.
    Alternatively, you can use the ``file`` or ``data`` options to initialize
    the photo image object.

    :param image: Either a PIL image, or a mode string.  If a mode string is
                  used, a size must also be given.
    :param size: If the first argument is a mode string, this defines the size
                 of the image.
    :keyword file: A filename to load the image from (using
                   ``Image.open(file)``).
    :keyword data: An 8-bit string containing image data (as loaded from an
                   image file).
    """

    def __init__(
        self,
        image: Image.Image | str | None = None,
        size: tuple[int, int] | None = None,
        **kw: Any,
    ) -> None:
        # Tk compatibility: file or data
        if image is None:
            image = _get_image_from_kw(kw)

        if image is None:
            msg = "Image is required"
            raise ValueError(msg)
        elif isinstance(image, str):
            mode = image
            image = None

            if size is None:
                msg = "If first argument is mode, size is required"
                raise ValueError(msg)
        else:
            # got an image instead of a mode
            mode = image.mode
            if mode == "P":
                # palette mapped data
                image.apply_transparency()
                image.load()
                mode = image.palette.mode if image.palette else "RGB"
            size = image.size
            kw["width"], kw["height"] = size

        if mode not in ["1", "L", "RGB", "RGBA"]:
            mode = Image.getmodebase(mode)

        self.__mode = mode
        self.__size = size
        self.__photo = tkinter.PhotoImage(**kw)
        self.tk = self.__photo.tk
        if image:
            self.paste(image)

    def __del__(self) -> None:
        try:
            name = self.__photo.name
        except AttributeError:
            return
        self.__photo.name = None
        try:
            self.__photo.tk.call("image", "delete", name)
        except Exception:
            pass  # ignore internal errors

    def __str__(self) -> str:
        """
        Get the Tkinter photo image identifier.  This method is automatically
        called by Tkinter whenever a PhotoImage object is passed to a Tkinter
        method.

        :return: A Tkinter photo image identifier (a string).
        """
        return str(self.__photo)

    def width(self) -> int:
        """
        Get the width of the image.

        :return: The width, in pixels.
        """
        return self.__size[0]

    def height(self) -> int:
        """
        Get the height of the image.

        :return: The height, in pixels.
        """
        return self.__size[1]

    def paste(self, im: Image.Image) -> None:
        """
        Paste a PIL image into the photo image.  Note that this can
        be very slow if the photo image is displayed.

        :param im: A PIL image. The size must match the target region.  If the
                   mode does not match, the image is converted to the mode of
                   the bitmap image.
        """
        # convert to blittable
        ptr = im.getim()
        image = im.im
        if not image.isblock() or im.mode != self.__mode:
            block = Image.core.new_block(self.__mode, im.size)
            image.convert2(block, image)  # convert directly between buffers
            ptr = block.ptr

        _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)


# --------------------------------------------------------------------
# BitmapImage


class BitmapImage:
    """
    A Tkinter-compatible bitmap image.  This can be used everywhere Tkinter
    expects an image object.

    The given image must have mode "1".  Pixels having value 0 are treated as
    transparent.  Options, if any, are passed on to Tkinter.  The most commonly
    used option is ``foreground``, which is used to specify the color for the
    non-transparent parts.  See the Tkinter documentation for information on
    how to specify colours.

    :param image: A PIL image.
    """

    def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
        # Tk compatibility: file or data
        if image is None:
            image = _get_image_from_kw(kw)

        if image is None:
            msg = "Image is required"
            raise ValueError(msg)
        self.__mode = image.mode
        self.__size = image.size

        self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)

    def __del__(self) -> None:
        try:
            name = self.__photo.name
        except AttributeError:
            return
        self.__photo.name = None
        try:
            self.__photo.tk.call("image", "delete", name)
        except Exception:
            pass  # ignore internal errors

    def width(self) -> int:
        """
        Get the width of the image.

        :return: The width, in pixels.
        """
        return self.__size[0]

    def height(self) -> int:
        """
        Get the height of the image.

        :return: The height, in pixels.
        """
        return self.__size[1]

    def __str__(self) -> str:
        """
        Get the Tkinter bitmap image identifier.  This method is automatically
        called by Tkinter whenever a BitmapImage object is passed to a Tkinter
        method.

        :return: A Tkinter bitmap image identifier (a string).
        """
        return str(self.__photo)


def getimage(photo: PhotoImage) -> Image.Image:
    """Copies the contents of a PhotoImage to a PIL image memory."""
    im = Image.new("RGBA", (photo.width(), photo.height()))

    _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())

    return im


def _show(image: Image.Image, title: str | None) -> None:
    """Helper for the Image.show method."""

    class UI(tkinter.Label):
        def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None:
            self.image: BitmapImage | PhotoImage
            if im.mode == "1":
                self.image = BitmapImage(im, foreground="white", master=master)
            else:
                self.image = PhotoImage(im, master=master)
            if TYPE_CHECKING:
                image = cast(tkinter._Image, self.image)
            else:
                image = self.image
            super().__init__(master, image=image, bg="black", bd=0)

    if not getattr(tkinter, "_default_root"):
        msg = "tkinter not initialized"
        raise OSError(msg)
    top = tkinter.Toplevel()
    if title:
        top.title(title)
    UI(top, image).pack()