"""Various utilities using the GObject framework"""

import array
import os
from typing import TYPE_CHECKING, Iterable, List, Optional, Type, TypeVar, Union, cast

import cairo
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk

from lutris import settings
from lutris.exceptions import MissingMediaError
from lutris.gui.widgets import NotificationSource
from lutris.util import datapath, magic, system
from lutris.util.log import logger

if TYPE_CHECKING:
    from lutris.gui.application import LutrisApplication
    from lutris.gui.lutriswindow import LutrisWindow

try:
    from PIL import Image
except ImportError:
    Image = None

ICON_SIZE = (32, 32)
BANNER_SIZE = (184, 69)
MEDIA_CACHE_INVALIDATED = NotificationSource()


def get_application() -> Optional["LutrisApplication"]:
    return cast("LutrisApplication", Gio.Application.get_default())


def get_required_application() -> "LutrisApplication":
    application = cast("LutrisApplication", Gio.Application.get_default())
    if not application:
        raise RuntimeError("The LutrisApplication does not exist.")
    return application


def get_main_window() -> Optional["LutrisWindow"]:
    """Return the application's main window, or None if it doesn't exist
    (though it almost alway does exist)"""
    application = get_application()
    return application.window if application else None


def get_required_main_window() -> "LutrisWindow":
    """Return the application's main window or raises an exception if
    it doesn't exist."""
    application = get_required_application()
    window = application.window
    if not window:
        raise RuntimeError("The main window does not exist.")
    return window


def get_widget_window(widget: Optional[Gtk.Widget]) -> Optional[Gtk.Window]:
    """Returns the window that contains a widget, if any. This wll return None
    for a widget that is not in a window, rather than returning the widget itself
    like get_toplevel()."""
    if widget:
        return cast(Optional[Gtk.Window], widget.get_ancestor(Gtk.Window))
    else:
        return None


TChildWidget = TypeVar("TChildWidget", bound=Gtk.Widget)


def get_widget_children(
    widget: Optional[Gtk.Widget], child_type: Optional[Type[TChildWidget]] = None
) -> List[TChildWidget]:
    """Returns the children of any widget; non-containers have no children
    and returns an empty list. This can filter out a specific type of child widget if child_type
    is not None, but otherwise it returns all children."""
    if isinstance(widget, Gtk.Container):
        if child_type:
            return [cast(TChildWidget, w) for w in widget.get_children() if isinstance(w, child_type)]
        else:
            return list(cast(Iterable[TChildWidget], widget.get_children()))
    else:
        return []


def open_uri(uri):
    """Opens a local or remote URI with the default application"""
    system.spawn(["xdg-open", uri])


def get_image_file_extension(path: str) -> Optional[str]:
    """Returns the canonical file extension for an image,
    either 'jpg' or 'png'; we deduce this from the file extension, or if that fails the
    file's 'magic' prefix bytes."""
    ext = os.path.splitext(path)[1].casefold()
    if ext in [".jpg", ".jpeg"]:
        return ".jpg"
    if ext == ".png":
        return ".png"

    try:
        file_type = magic.from_file(path).casefold()
    except OSError:
        return None  # file is missing, or can't read it

    if "jpeg image data" in file_type:
        return ".jpg"
    if "png image data" in file_type:
        return ".png"

    return None


def get_surface_size(surface):
    """Returns the size of a surface, accounting for the device scale;
    the surface's get_width() and get_height() are in physical pixels."""
    device_scale_x, device_scale_y = surface.get_device_scale()
    width = surface.get_width() / device_scale_x
    height = surface.get_height() / device_scale_y
    return width, height


def get_scaled_surface_by_path(path, size, device_scale, preserve_aspect_ratio=True):
    """Returns a Cairo surface containing the image at the path given. It has the size indicated.

    You specify the device_scale, and the bitmap is generated at an enlarged size accordingly,
    but with the device scale of the surface also set; in this way a high-DPI image can be
    rendered conveniently.

    If you pass True for preserve_aspect_ratio, the aspect ratio of the image is preserved,
    but will be no larger than the size (times the device_scale).

    If there's no file at the path, or it is empty, this function returns None.
    """
    pixbuf = get_pixbuf_by_path(path)
    if not pixbuf:
        return None

    pixbuf_width = pixbuf.get_width()
    pixbuf_height = pixbuf.get_height()

    scale_x = (size[0] / pixbuf_width) * device_scale
    scale_y = (size[1] / pixbuf_height) * device_scale

    if preserve_aspect_ratio:
        scale_x = min(scale_x, scale_y)
        scale_y = scale_x

    pixel_width = round(pixbuf_width * scale_x)
    pixel_height = round(pixbuf_height * scale_y)

    surface = cairo.ImageSurface(cairo.Format.ARGB32, pixel_width, pixel_height)  # pylint:disable=no-member
    cr = cairo.Context(surface)  # pylint:disable=no-member
    cr.scale(scale_x, scale_y)
    Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
    cr.get_source().set_extend(cairo.Extend.PAD)  # pylint: disable=no-member
    cr.paint()
    surface.set_device_scale(device_scale, device_scale)
    return surface


def get_default_icon_path(size):
    """Returns the path to the default icon for the size given; it's
    a Lutris icon for a square size, and a gradient for other sizes."""
    if not size or size[0] == size[1]:
        filename = "media/default_icon.png"
    else:
        filename = "media/default_banner.png"
    return os.path.join(datapath.get(), filename)


def get_pixbuf_by_path(path, size=None, preserve_aspect_ratio=True):
    """Reads an image file and returns the pixbuf. If you provide a size, this scales
    the file to fit that size, preserving the aspect ratio if preserve_aspect_ratio is
    True. If the file is missing or empty, or if 'path' is None or empty,
    this returns None. Still raises GLib.GError for corrupt files."""
    if not system.path_exists(path, exclude_empty=True):
        return None

    if size:
        # new_from_file_at_size scales but preserves aspect ratio
        width, height = size
        if preserve_aspect_ratio:
            return GdkPixbuf.Pixbuf.new_from_file_at_size(path, width, height)

        return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, preserve_aspect_ratio=False)

    return GdkPixbuf.Pixbuf.new_from_file(path)


def get_required_pixbuf_by_path(path, size=None, preserve_aspect_ratio=True):
    """Reads an image file and returns the pixbuf. If you provide a size, this scales
    the file to fit that size, preserving the aspect ratio if preserve_aspect_ratio is
    True. If the file is missing or unreadable, or if 'path' is None or empty, this raises
    MissingMediaError."""
    try:
        pixbuf = get_pixbuf_by_path(path, size, preserve_aspect_ratio)
        if not pixbuf:
            raise MissingMediaError(filename=path)
        return pixbuf
    except GLib.GError as ex:
        logger.exception("Unable to load icon from image %s", path)
        raise MissingMediaError(message=str(ex), filename=path) from ex


def has_stock_icon(name):
    """This tests if a GTK stock icon is known; if not we can try a fallback."""
    if not name:
        return False

    theme = Gtk.IconTheme.get_default()
    return theme.has_icon(name)


def pick_stock_icon(names: Union[str, Iterable[str]], fallback_name: str = "package-x-generic-symbolic") -> str:
    """Used to select a stock icon that actually exists in the icon set; it tries all the names given,
    and returns the first where has_stock_icon is true. If none pass the test, returns fallback_name instead."""
    if isinstance(names, str):
        names = [names]

    for name in names:
        if has_stock_icon(name):
            return name

    return fallback_name


def get_runtime_icon_path(icon_name):
    """Finds the icon file for an icon whose name is given; this searches the icons
    in Lutris's runtime directory. The name is normalized by removing spaces
    and lower-casing it, and both .png and .svg files with the name can be found.

    Arguments:
    icon_name -- The name of the icon to retrieve

    Returns:
        The path to the icon, or None if it wasn't found.
    """
    filename = icon_name.lower().replace(" ", "")
    # We prefer bitmaps over SVG, because we've got some SVG icons with the
    # wrong size (oops) and this avoids them.
    search_directories = [
        "icons/hicolor/64x64/apps",
        "icons/hicolor/24x24/apps",
        "icons",
        "icons/hicolor/scalable/apps",
        "icons/hicolor/symbolic/apps",
    ]
    extensions = [".png", ".svg"]
    for search_dir in search_directories:
        for ext in extensions:
            icon_path = os.path.join(settings.RUNTIME_DIR, search_dir, filename + ext)
            if os.path.exists(icon_path):
                return icon_path
    return None


def convert_to_background(background_path, target_size=(320, 1080)):
    """Converts an image to a pane background"""
    coverart = Image.open(background_path)
    coverart = coverart.convert("RGBA")

    target_width, target_height = target_size
    image_height = int(target_height * 0.80)  # 80% of the mask is visible
    orig_width, orig_height = coverart.size

    # Resize and crop coverart
    width = int(orig_width * (image_height / orig_height))
    offset = int((width - target_width) / 2)
    coverart = coverart.resize((width, image_height), resample=Image.Resampling.BICUBIC)
    coverart = coverart.crop((offset, 0, target_width + offset, image_height))

    # Resize canvas of coverart by putting transparent pixels on the bottom
    coverart_bg = Image.new("RGBA", (target_width, target_height), (0, 0, 0, 0))
    coverart_bg.paste(coverart, (0, 0, target_width, image_height))

    # Apply a tint to the base image
    # tint = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 255))
    # coverart = Image.blend(coverart, tint, 0.6)

    # Paste coverart on transparent image while applying a gradient mask
    background = Image.new("RGBA", (target_width, target_height), (0, 0, 0, 0))
    mask = Image.open(os.path.join(datapath.get(), "media/mask.png"))
    background.paste(coverart_bg, mask=mask)

    return background


def thumbnail_image(base_image, target_size):
    base_width, base_height = base_image.size
    base_ratio = base_width / base_height
    target_width, target_height = target_size
    target_ratio = target_width / target_height

    # Resize and crop coverart
    if base_ratio >= target_ratio:
        width = int(base_width * (target_height / base_height))
        height = target_height
    else:
        width = target_width
        height = int(base_height * (target_width / base_width))
    x_offset = int((width - target_width) / 2)
    y_offset = int((height - target_height) / 2)
    base_image = base_image.resize((width, height), resample=Image.Resampling.BICUBIC)
    base_image = base_image.crop((x_offset, y_offset, width - x_offset, height - y_offset))
    return base_image


def paste_overlay(base_image, overlay_image, position=0.7):
    base_width, base_height = base_image.size
    overlay_width, overlay_height = overlay_image.size
    offset_x = int((base_width - overlay_width) / 2)
    offset_y = int((base_height - overlay_height) / 2)
    base_image.paste(
        overlay_image, (offset_x, offset_y, overlay_width + offset_x, overlay_height + offset_y), mask=overlay_image
    )
    return base_image


def image2pixbuf(image):
    """Converts a PIL Image to a GDK Pixbuf"""
    image_array = array.array("B", image.tobytes())
    width, height = image.size
    return GdkPixbuf.Pixbuf.new_from_data(image_array, GdkPixbuf.Colorspace.RGB, True, 8, width, height, width * 4)


def load_icon_theme():
    """Add the lutris icon folder to the default theme"""
    icon_theme = Gtk.IconTheme.get_default()
    local_theme_path = os.path.join(settings.RUNTIME_DIR, "icons")
    if local_theme_path not in icon_theme.get_search_path():
        icon_theme.prepend_search_path(local_theme_path)
