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

from typing import Tuple

from iotas.category import Category
from iotas.category_treeview_list_store import CategoryTreeViewListStore
from iotas.note import Note
from iotas.text_utils import sanitise_path


@Gtk.Template(resource_path="/org/gnome/World/Iotas/ui/category_header_bar.ui")
class CategoryHeaderBar(Adw.Bin):
    __gtype_name__ = "CategoryHeaderBar"

    __gsignals__ = {
        "abort": (GObject.SignalFlags.RUN_FIRST, None, ()),
        "categories-changed": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    _entry = Gtk.Template.Child()
    _completion = Gtk.Template.Child()
    _apply_button = Gtk.Template.Child()
    _clear_button = Gtk.Template.Child()

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

        self.__model = None
        self.__notes = []
        self.__changeset = []

        completion_cell = Gtk.CellRendererText.new()
        completion_cell.set_property("xpad", 6)
        completion_cell.set_property("ypad", 6)
        self._completion.pack_start(completion_cell, False)
        self._completion.add_attribute(completion_cell, "text", 0)
        self._completion.connect("match-selected", self.__on_category_selected)
        self._entry.connect("icon-release", lambda _e, _p: self.__on_icon_released())
        self.__category_buffer = self._entry.get_buffer()
        self.__category_buffer.connect(
            "notify::text", lambda _o, _v: self.__update_clear_field_icon_visibility()
        )

    def setup(self, model: CategoryTreeViewListStore) -> None:
        """Perform initial setup.

        :param CategoryTreeViewListStore model: Categories model
        """
        self.__model = model
        self._completion.set_model(self.__model)
        self._completion.set_match_func(self.__completion_match_check)

    def replace_notes(self, notes: list[Note]) -> None:
        """Replace the notes on which the header is working.

        For selection changes in the index once the header is already visible.

        :param list[Note] notes: Notes to work on
        """
        self.__notes = notes.copy()
        self.__refresh_actions()

    def activate(self, notes: list[Note]) -> None:
        """Activate header bar.

        :param list[Note] notes: Notes to work on
        """
        self.__notes = notes.copy()
        if self.__model.iter_n_children(None) > 0:
            self.__show_popup()

        # If editing multiple notes only populate the entry if they're all the same
        uniform_category = notes[0].category
        if len(notes) > 1:
            for note in notes[1:]:
                if note.category != uniform_category:
                    uniform_category = ""
                    break
        self._entry.set_text(Category.generate_display_name(uniform_category))

        self._entry.grab_focus()
        self._entry.select_region(0, -1)
        self.__refresh_actions()

    def clear_and_apply(self) -> None:
        """Clear category and apply."""
        self._entry.set_text("")
        self.__apply_change()

    def take_changeset(self) -> list[Tuple[Note, str]]:
        """Fetch changeset and clear.

        :return: List of modified notes with their previous categories
        :rtype: list[Tuple[Note, str]]
        """
        changeset = self.__changeset
        self.__changeset = []
        return changeset

    def focus(self) -> None:
        """Grab focus to entry."""
        self._entry.grab_focus()

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

    @Gtk.Template.Callback()
    def _on_entry_activated(self, _entry: Gtk.Entry) -> None:
        self.__apply_change()

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

    @Gtk.Template.Callback()
    def _on_abort(self, _button: Gtk.Button) -> None:
        self.emit("abort")

    def __on_icon_released(self) -> None:
        # Intentionally allowed for either icon
        if self._entry.get_text() == "":
            self.__show_popup()
        else:
            self._entry.get_buffer().delete_text(0, -1)
            self._entry.set_property("secondary-icon-name", "")

    def __on_category_selected(
        self, _completion: Gtk.EntryCompletion, store: Gtk.TreeModel, iter: Gtk.TreeIter
    ) -> bool:
        new_category = store.get_value(iter, 0)
        self.__update_category(new_category)
        return True

    def __apply_change(self) -> None:
        if not self.__notes:
            return
        new_category = self._entry.get_text().strip()
        self.__update_category(new_category)

    def __show_popup(self) -> None:
        child = self._entry.get_first_child()
        while not isinstance(child, Gtk.Popover):
            child = child.get_next_sibling()
        child.set_visible(True)
        self._completion.complete()

    def __refresh_actions(self) -> None:
        self._apply_button.set_sensitive(self.__notes)
        self._clear_button.set_sensitive(self.__notes)

    def __update_category(self, new_category: str) -> None:
        if "/" in new_category:
            parts = new_category.split("/")
            parts = [sanitise_path(x).strip() for x in parts]
            parts = [x for x in parts if x != ""]
            new_category = "/".join(parts)

        self.__changeset = []
        for note in self.__notes:
            if new_category != note.category:
                old_category = note.category
                note.category = new_category
                # Choosing to not bump last_modified for category updates
                note.flag_changed(update_last_modified=False)
                self.__changeset.append((note, old_category))
        GLib.idle_add(self.emit, "categories-changed")

    def __update_clear_field_icon_visibility(self) -> None:
        icon_showing = self._entry.get_property("secondary-icon_name")
        if self.__category_buffer.get_text() != "":
            if not icon_showing:
                self._entry.set_property("secondary-icon-name", "edit-clear-symbolic")
        else:
            if icon_showing:
                self._entry.set_property("secondary-icon-name", "")

    def __completion_match_check(
        self, _completion: Gtk.EntryCompletion, value: str, compare_iter: Gtk.TreeIter
    ) -> bool:
        """GtkEntryCompletion matching function.

        :param GtkEntryCompletion _completion: The EntryCompletion
        :param str value: The string to match
        :param GtkTreeIter compare_iter: The GtkTreeIter pointing at the value to compare
        :returns: Whether they match
        :rtype: bool
        """
        compare_category = self.__model.get_value(compare_iter, 1)
        if compare_category is None:
            # Seems to occur applying a dropdown value
            return False
        else:
            return value.lower() in compare_category.name.lower()
