#!/usr/bin/env python3
"""
Image handling for Debian Software

Dependencies:
- python3-cairosvg
- python3-pil
- python3-requests

Author: Arjen Balfoort
Date: 2026-03-29
"""

import os
import io
import hashlib
import logging
from typing import Optional
import requests
from requests.exceptions import RequestException
from PIL import Image
import cairosvg

import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf


class ImageHandler:
    """Class to handle image operations."""

    def __init__(self, logging_handler=None):
        self.logging: Optional[logging] = logging_handler
        # Get current icon theme
        display = Gdk.Display.get_default()
        self.icon_theme: Gtk.IconTheme = Gtk.IconTheme.get_for_display(display)

    def set_widget_background_color_from_image(self,
                                               widget: Gtk.Widget,
                                               image_path: str,
                                               transparency: float = 1.0) -> None:
        """
        Set the background color of a widget based on the dominant color of an image.

        Args:
            widget (Gtk.Widget): The widget to set the background color of.
            image_path (str): The path to the image.
            transparency (float): The transparency level (0.0 to 1.0).
        """
        if '/' in image_path:
            if not os.path.exists(image_path):
                self.logging.error(f"Image not found: {image_path}")
                return
        else:
            # Get icon path from name
            image_path = self.get_icon_path_from_name(image_path)
            if image_path is None:
                self.logging.error(f"Failed to get icon path for: {image_path}")
                return

        dominant_color = self.get_dominant_color(image_path)
        if len(dominant_color) != 4:
            self.logging.error(f"Failed to get dominant color from image: {image_path}")
            return
        rgba_color = self.color_to_rgba(dominant_color, alpha=1.0)  # Default to full opacity
        self.apply_background_color(widget, rgba_color, transparency)

    def theme_has_icon(self, name: str) -> bool:
        """
        Check if an icon exists in the current icon theme.

        Args:
            name (str): The name of the icon.

        Returns:
            bool: True if the icon exists, False otherwise.
        """
        return self.icon_theme.has_icon(name)

    def set_picture(self, picture: Gtk.Picture, name_or_path: str, size: int=None) -> bool:
        """
        Sets the icon for a Gtk.Picture from an icon name or path.

        Args:
            picture (Gtk.Picture): The Gtk.Picture to set the icon for.
            name_or_path (str): The name or path of the picture.
            size (int): The size of the icon (default is 64).

        Returns:
            bool: True if the icon was set successfully, False otherwise.
        """
        if name_or_path.startswith("/"):
            if os.path.exists(name_or_path):
                if size is None:
                    # Load picture from file as-is
                    texture = Gdk.Texture.new_from_filename(name_or_path)
                    picture.set_paintable(texture)
                else:
                    # Scale the picture
                    pixbuf = GdkPixbuf.Pixbuf.new_from_file(name_or_path)
                    scaled = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR)
                    texture = Gdk.Texture.new_for_pixbuf(scaled)

                    # Set the texture to the picture
                    picture.set_paintable(texture)

                return True
            self.logging.error(f"Failed to load picture: {name_or_path}")
            return False

        # Load picture from icon theme
        size = size or 64
        icon_paintable = self.icon_theme.lookup_icon(
            name_or_path, None, size, 1, Gtk.TextDirection.LTR, 0
        )
        if icon_paintable:
            picture.set_paintable(icon_paintable)
            return True
        self.logging.error(f"Failed to load picture: {name_or_path}")
        return False

    def get_icon_path_from_name(self, name: str, size: int=64) -> Optional[str]:
        """
        Get the path to an icon from its name.

        Args:
            name (str): The name of the icon.
            size (int): The size of the icon.

        Returns:
            str: The path to the icon.
        """
        icon = self.icon_theme.lookup_icon(name, None, size, 1, Gtk.TextDirection.LTR, 0)
        if icon is None:
            return None
        return icon.get_file().get_path()

    def get_dominant_color(self, image_path: str) -> Optional[tuple]:
        """
        Get the dominant color of an image, including SVG files.

        Args:
            image_path (str): The path to the image.

        Returns:
            tuple: The dominant color as an RGBA tuple.
        """
        try:
            if image_path.endswith('.svg'):
                # Convert SVG to PNG using cairosvg
                png_data = cairosvg.svg2png(url=image_path)
                img = Image.open(io.BytesIO(png_data))
            else:
                img = Image.open(image_path)

            img = img.convert("RGBA")  # Ensure the image is in RGBA mode
            img = img.resize((1, 1))   # Resize to 1x1 to get the "average" color
            dominant_color = img.getpixel((0, 0))
            return dominant_color
        except Exception as e:
            self.logging.error(f"Failed to open or process image: {e}")
            return None

    @staticmethod
    def color_to_rgba(color: tuple, alpha: float = 1.0) -> tuple:
        """
        Convert an RGB color to RGBA with a specified alpha.

        Args:
            color (tuple): The RGB color (values 0-255).
            alpha (float): The alpha value (0.0 to 1.0).

        Returns:
            tuple: The RGBA color (values normalized to 0-1).
        """
        r, g, b = color[:3]
        return (r / 255, g / 255, b / 255, alpha)

    @staticmethod
    def apply_background_color(widget: Gtk.Widget,
                               rgba_color: tuple,
                               transparency: float = 1.0) -> None:
        """
        Apply a background color to a widget.

        Args:
            widget (Gtk.Widget): The widget to apply the background color to.
            rgba_color (tuple): The RGBA color (values 0-1).
            transparency (float): The transparency multiplier (0.0 to 1.0).
        """
        transparency = max(0.0, min(1.0, transparency))
        adjusted_alpha = rgba_color[3] * transparency

        css_provider = Gtk.CssProvider()
        css = f"""
            {widget.get_css_name()} {{
                background-color: rgba({rgba_color[0]*255},
                                       {rgba_color[1]*255},
                                       {rgba_color[2]*255},
                                       {adjusted_alpha});
                border-color: rgba({rgba_color[0]*255},
                                   {rgba_color[1]*255},
                                   {rgba_color[2]*255},
                                   {rgba_color[3]});
            }}
        """
        css_provider.load_from_data(css.encode())
        style_context = widget.get_style_context()
        style_context.add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

    def load_image_from_url(self, cache_dir: str, url: str) -> Optional[Gdk.Texture]:
        """
        Load an image from a URL and return a Gdk.Texture object.
        """
        # Generate a unique filename using the URL hash
        url_hash = hashlib.md5(url.encode('utf-8')).hexdigest()
        cached_path = os.path.join(cache_dir, f"{url_hash}.png")

        texture = None
        if os.path.exists(cached_path):
            # Load the image from the cache
            texture = Gdk.Texture.new_from_filename(cached_path)
        else:
            try:
                response = requests.get(url=url, timeout=5)
                if response.status_code == 200:
                    bytes_data = GLib.Bytes.new(response.content)
                    texture = Gdk.Texture.new_from_bytes(bytes_data)
                    if texture is not None:
                        texture.save_to_png(cached_path)
                    else:
                        self.logging.error(f"Failed to create texture from image data: {url}")
                else:
                    # URL not found
                    self.logging.warning("Failed to load image from URL "
                                        f"(status {response.status_code}): {url}")
            except RequestException as e:
                # Network error (no connection, etc.)
                self.logging.warning("Network error while loading image from URL: "
                                    f"{url} ({str(e)})")

        return texture
