from gettext import gettext as _
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk

from functools import wraps
from threading import Event
from typing import Any, Callable, Optional


class ComboRowHelper(GObject.Object):

    __gsignals__ = {
        "changed": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
    }

    def __init__(
        self,
        combo: Adw.ComboRow,
        options: dict[str, str],
        selected: str,
    ) -> None:
        super().__init__()
        self.__option_keys = list(options.keys())

        model = Gtk.StringList()
        for v in options.values():
            model.append(v)

        combo.set_model(model)
        combo.set_selected(self.__option_keys.index(selected))
        combo.connect("notify::selected", self.__on_selection_change)

    def __on_selection_change(self, obj: Adw.ComboRow, param: GObject.ParamSpec) -> None:
        index = obj.get_property(param.name)
        if index >= len(self.__option_keys):
            return
        selected_key = self.__option_keys[index]
        self.emit("changed", selected_key)


def add_mouse_button_accel(
    widget: Gtk.Widget,
    function: Callable[[], None],
    propagation: Gtk.PropagationPhase = Gtk.PropagationPhase.BUBBLE,
) -> None:
    """Add a mouse button gesture.

    :param Gtk.Widget widget: Widget to add on
    :param Callable[[], None] function: Callback function
    :param Gtk.PropagationPhase propagation: Propagation phase
    """
    gesture = Gtk.GestureClick.new()
    gesture.set_button(0)
    gesture.set_propagation_phase(propagation)
    gesture.connect("pressed", function)
    widget.add_controller(gesture)
    # Keep the gesture in scope
    widget._gesture_click = gesture


def idle_add_wait(function, *args, **kwargs) -> Any:
    """Execute function in the GLib main loop and wait.

    :param Callbable function: The function to call
    :param args: Any positional arguments
    :param kwargs: Any key word arguments
    :return: The result the function call
    :rtype: None or the result
    """
    function_completed = Event()
    results = []

    @wraps(function)
    def wrapper():
        results.append(function(*args, **kwargs))
        function_completed.set()
        return False

    GLib.idle_add(wrapper)
    function_completed.wait()
    return results.pop()


def show_error_dialog(parent: Gtk.Widget, message: str) -> None:
    """Show a modal error dialog.

    :param Gtk.Widget parent: Parent widget
    :param str message: Message text
    """
    # Translators: Title
    dialog = Adw.AlertDialog.new(_("Error"), message)
    # Translators: Button
    dialog.add_response("close", _("Close"))
    dialog.set_close_response("close")
    dialog.present(parent)


def check_for_search_starting(
    controller: Gtk.EventControllerKey, keyval: int, state: Gdk.ModifierType
) -> bool:
    """Check if starting search via type to search.

    :param Gtk.EventControllerKey controller: The key controller
    :param int keyval: The key value
    :param Gdk.ModifierType state: Any modifier state
    :return: Whether search is starting
    :rtype: bool
    """
    if keyval in (
        Gdk.KEY_space,
        Gdk.KEY_Return,
        Gdk.KEY_KP_Enter,
        Gdk.KEY_Tab,
        Gdk.KEY_KP_Tab,
        Gdk.KEY_Left,
        Gdk.KEY_KP_Left,
        Gdk.KEY_Right,
        Gdk.KEY_KP_Right,
        Gdk.KEY_Home,
        Gdk.KEY_KP_Home,
        Gdk.KEY_End,
        Gdk.KEY_KP_End,
        Gdk.KEY_Page_Up,
        Gdk.KEY_KP_Page_Up,
        Gdk.KEY_Page_Down,
        Gdk.KEY_KP_Page_Down,
        Gdk.KEY_Control_L,
        Gdk.KEY_Control_R,
        Gdk.KEY_Alt_L,
        Gdk.KEY_Alt_R,
        Gdk.KEY_Meta_L,
        Gdk.KEY_Meta_R,
        Gdk.KEY_Shift_L,
        Gdk.KEY_Shift_R,
        Gdk.KEY_BackSpace,
        Gdk.KEY_Delete,
        Gdk.KEY_KP_Delete,
        Gdk.KEY_Escape,
    ):
        return False
    elif state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.ALT_MASK):
        return False
    else:
        return True


# A concession for mobile device Gdk.Device entries seeming to report having cursors (at time of
# writing in 2024). Currently being used to determine if the device likely isn't being driven by a
# cursor and so as a result can't rely on hover interaction. It's obviously a loose match though,
# somebody could be using a tiny window on desktop, landscape orientation on mobile or we're in the
# future and 360 width is no longer relevant. Hence "likely".
def is_likely_mobile_device() -> Optional[bool]:
    """Check if device is likely a mobile.

    Loose check, see comment.

    :returns: Whether likely a phone or None during initialisation
    :rtype: bool, optional
    """
    app = Gio.Application.get_default()
    if not app:
        return None
    window = app.get_active_window()
    return window.get_width() <= 360


def have_window_with_width() -> bool:
    """Check if the window has been initialised and has a width.

    :returns: Whether window has a width.
    :rtype: bool
    """
    app = Gio.Application.get_default()
    if not app:
        return False
    window = app.get_active_window()
    if not window:
        return False
    return window.get_width() > 0


def check_for_open_first_result_shortcut(keyval: int, state: Gdk.ModifierType) -> bool:
    """Check keyboard shortcut to activate first result has been pressed.

    :param int keyval: The key value
    :param Gdk.ModifierType state: Any modifier state
    :return: Whether pressed
    :rtype: bool
    """
    return keyval == Gdk.KEY_Return and state & Gdk.ModifierType.ALT_MASK
