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

from typing import Any

from iotas.outline_generator import OutlineHeading
from iotas.ui_utils import check_for_search_starting, check_for_open_first_result_shortcut


class _HeadingRow(Adw.ActionRow):
    """Row for outline dialog headings.

    :param str label: Text
    :param int line: Markdown line number
    """

    def __init__(self, label: str, line: int) -> None:
        super().__init__()
        self.set_title(label)
        self.line = line
        self.set_activatable(True)


class _SizeReportingListbox(Gtk.ListBox):

    __gsignals__ = {
        "new-height": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
    }

    def __init__(self) -> None:
        super().__init__()
        self.set_selection_mode(Gtk.SelectionMode.NONE)
        self.set_activate_on_single_click(True)
        self.add_css_class("boxed-list")

    def do_size_allocate(self, width: int, height: int, baseline: int) -> None:
        Gtk.ListBox.do_size_allocate(self, width, height, baseline)
        self.emit("new-height", height)


@Gtk.Template(resource_path="/org/gnome/World/Iotas/ui/outline_dialog.ui")
class OutlineDialog(Adw.Dialog):
    """Note outline dialog."""

    __gtype_name__ = "OutlineDialog"

    __gsignals__ = {
        "jump-to": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
    }

    _header_stack = Gtk.Template.Child()
    _main_headerbar = Gtk.Template.Child()
    _filter_headerbar = Gtk.Template.Child()
    _stack = Gtk.Template.Child()
    _spinner = Gtk.Template.Child()
    _list = Gtk.Template.Child()
    _no_headings = Gtk.Template.Child()
    _filter_entry = Gtk.Template.Child()
    _zero_filtered = Gtk.Template.Child()

    def __init__(self) -> None:
        super().__init__()

        self.__store = Gio.ListStore(item_type=OutlineHeading)
        self.__filter_term = ""

        self.__filter_model = Gtk.FilterListModel()
        self.__filter = Gtk.CustomFilter()
        self.__filter_model.set_filter(self.__filter)

        self.__filter.set_filter_func(self.__filter_heading)
        self.__filter_model.set_model(self.__store)

        self.__listbox = _SizeReportingListbox()
        self.__listbox.connect(
            "new-height", lambda _o, height: self.__update_for_list_height(height)
        )
        self.__listbox.connect("row-activated", lambda _o, row: self.__activate_row(row))
        self._list.set_child(self.__listbox)

        controller = Gtk.EventControllerKey()
        controller.connect("key-pressed", self.__on_listbox_key_pressed)
        self.__listbox.add_controller(controller)

        controller = Gtk.EventControllerKey()
        controller.connect("key-pressed", self.__on_entry_key_pressed)
        self._filter_entry.add_controller(controller)

        self.__listbox.bind_model(self.__filter_model, self.__create_row, None)

    def populate(self, headings: list[OutlineHeading], rtl: bool) -> None:
        """Populate headings.

        :param list[OutlineHeading] headings: Headings to populate
        :param bool rtl: Whether direction is right-to-left
        """
        self._spinner.set_visible(False)

        if not headings:
            self._stack.set_visible_child(self._no_headings)
            return

        self.__rtl = rtl

        for heading in headings:
            self.__store.append(heading)

        self._main_headerbar.add_css_class("list-matched")
        self._stack.set_visible_child(self._list)
        self.__listbox.grab_focus()

    @Gtk.Template.Callback()
    def _on_close_clicked(self, _button: Gtk.Button) -> None:
        self.close()

    @Gtk.Template.Callback()
    def _on_filter_changed(self, entry: Gtk.Editable) -> None:
        if not list(self.__store):
            return
        self.__filter_term = entry.get_text()
        self.__filter.set_filter_func(self.__filter_heading)
        if self.__filter_term != "" and not list(self.__listbox):
            self._stack.set_visible_child(self._zero_filtered)
        elif self._stack.get_visible_child() != self._list:
            self._stack.set_visible_child(self._list)

    @Gtk.Template.Callback()
    def _on_stop_filter(self, entry: Gtk.Editable) -> None:
        entry.set_text("")
        self.__filter_term = ""
        self.__filter.set_filter_func(self.__filter_heading)
        self.__listbox.grab_focus()
        self._header_stack.set_visible_child(self._main_headerbar)

    def __on_listbox_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if self.__check_if_filter_starting(controller, keyval, state, clear=False):
            self.__show_filter()
            return Gdk.EVENT_STOP

        if not self.__listbox.is_focus():
            return Gdk.EVENT_PROPAGATE

        if keyval in (Gdk.KEY_Down, Gdk.KEY_KP_Down):
            row = self.__listbox.get_row_at_index(0)
            row.grab_focus()
            return Gdk.EVENT_STOP
        elif keyval in (Gdk.KEY_Up, Gdk.KEY_KP_Up):
            count = len(list(self.__listbox))
            row = self.__listbox.get_row_at_index(count - 1)
            row.grab_focus()
            return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

    def __on_entry_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if keyval in (Gdk.KEY_Down, Gdk.KEY_KP_Down):
            row = self.__listbox.get_row_at_index(0)
            row.grab_focus()
            return Gdk.EVENT_STOP
        elif check_for_open_first_result_shortcut(keyval, state):
            if list(self.__listbox):
                row = self.__listbox.get_row_at_index(0)
                self.__activate_row(row)

        return Gdk.EVENT_PROPAGATE

    def __update_for_list_height(self, height: int) -> None:
        window_height = self.get_root().get_height()
        threshold = window_height * 0.9
        if height > threshold:
            height = threshold
        GLib.idle_add(self._stack.set_property, "height-request", height)

    def __show_filter(self) -> None:
        self._header_stack.set_visible_child(self._filter_headerbar)
        self._filter_entry.grab_focus()

    def __check_if_filter_starting(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        state: Gdk.ModifierType,
        clear: bool = True,
    ) -> bool:
        if not check_for_search_starting(controller, keyval, state):
            return False

        if clear:
            self._filter_entry.set_text("")
        if controller.forward(self._filter_entry.get_delegate()):
            return True

        return False

    def __create_row(self, heading: OutlineHeading, _user_data: Any) -> Gtk.Widget:
        row = _HeadingRow(heading.text, heading.line)
        if heading.indent_level:
            row.add_prefix
            padding = Gtk.Label()
            padding.set_text("     " * heading.indent_level)
            if self.__rtl:
                row.add_suffix(padding)
            else:
                row.add_prefix(padding)
        return row

    def __filter_heading(self, heading: OutlineHeading) -> bool:
        return self.__filter_term.lower() in heading.text.lower()

    def __activate_row(self, row: _HeadingRow) -> None:
        self.emit("jump-to", row.line)
        self.close()
