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, Tuple


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

    def __init__(
        self,
        combo: Adw.ComboRow,
        options: Tuple[Tuple[str, str]],
        selected_value: str,
    ):
        super().__init__()
        self.__combo = combo
        self.__factory = Gtk.SignalListItemFactory()
        self.__factory.connect("setup", self.__on_setup_listitem)
        self.__factory.connect("bind", self.__on_bind_listitem)
        combo.set_factory(self.__factory)

        self.__store = Gio.ListStore(item_type=self.ItemWrapper)
        i = 0
        selected_index = 0
        for option in options:
            if option[1] == selected_value:
                selected_index = i
            i += 1
            self.__store.append(self.ItemWrapper(option[0], option[1]))
        combo.set_model(self.__store)

        combo.set_selected(selected_index)
        combo.connect("notify::selected-item", self.__on_selected)

    class ItemWrapper(GObject.Object):
        def __init__(self, name: str, value: str):
            super().__init__()
            self.name = name
            self.value = value

    def __on_selected(self, combo: Adw.ComboRow, selected_item: GObject.ParamSpec) -> None:
        value = self.__combo.get_selected_item().value
        self.emit("changed", value)

    def __on_setup_listitem(self, factory: Gtk.ListItemFactory, list_item: Gtk.ListItem) -> None:
        label = Gtk.Label()
        list_item.set_child(label)
        list_item.row_w = label

    def __on_bind_listitem(self, factory: Gtk.ListItemFactory, list_item: Gtk.ListItem) -> None:
        label = list_item.get_child()
        label.set_text(list_item.get_item().name)


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 add_mouse_right_click_accel(
    widget: Gtk.Widget,
    function: Callable[[], None],
    propagation: Gtk.PropagationPhase = Gtk.PropagationPhase.BUBBLE,
) -> None:
    """Add a mouse right click gesture.

    :param Gtk.Widget widget: Widget to add on
    :param Callable[[], None] function: Callback function
    :param Gtk.PropagationPhase propagation: Propagation phase
    """

    def callback(gesture: Gtk.GestureClick, _n_press: int, _x: float, _y: float) -> None:
        if gesture.get_current_button() == 3:  # Right click
            function()

    add_mouse_button_accel(widget, callback, propagation)


def add_touch_longpress_accel(
    widget: Gtk.Widget,
    function: Callable[[], None],
    propagation: Gtk.PropagationPhase = Gtk.PropagationPhase.BUBBLE,
) -> None:
    """Add a long press touch gesture.

    :param Gtk.Widget widget: Widget to add on
    :param Callable[[], None] function: Callback function
    :param Gtk.PropagationPhase propagation: Propagation phase
    """
    gesture = Gtk.GestureLongPress.new()
    gesture.set_propagation_phase(propagation)
    gesture.set_touch_only(True)

    def longpress(_gesture: Gtk.GestureLongPress, _x: float, _y: float) -> None:
        function()

    gesture.connect("pressed", longpress)

    widget.add_controller(gesture)
    # Keep the gesture in scope
    widget._gesture_longpress = 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.Window, message: str) -> None:
    """Show a modal error dialog.

    :param Gtk.Window parent: Parent window
    :param str message: Message text
    """
    # Translators: Title
    dialog = Adw.MessageDialog.new(parent, _("Error"), message)
    # Translators: Button
    dialog.add_response("close", _("Close"))
    dialog.set_close_response("close")
    dialog.show()


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
