# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later
from gi.repository import Adw, Gdk, GLib, GObject, Gtk

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
from iotas.ui_utils import check_for_open_first_result_shortcut


@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.Entry = Gtk.Template.Child()
    _completion: Gtk.EntryCompletion = Gtk.Template.Child()
    _apply_button: Gtk.Button = Gtk.Template.Child()
    _clear_button: Gtk.Button = Gtk.Template.Child()

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

        self.__model: CategoryTreeViewListStore
        self.__notes: list[Note] = []
        self.__changeset: list[tuple[Note, str]] = []

        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()

    def select_all_text(self) -> None:
        """Select all entry text"""
        self._entry.select_region(0, -1)

    @GObject.Property(type=bool, default=False)
    def entry_focused(self) -> bool:
        """Get whether the entry has focus."""
        entry_has_focus = self._entry.has_focus()
        # The Gtk.Text appears to get the focus, so check for that
        focused = self.get_root().get_focus()
        entry_child_has_focus = focused is not None and focused.is_ancestor(self._entry)
        return entry_has_focus or entry_child_has_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")

    @Gtk.Template.Callback()
    def _on_entry_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if check_for_open_first_result_shortcut(keyval, state):
            self.__open_first_filter_match()

        return Gdk.EVENT_PROPAGATE

    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 self.__match_term_to_category(compare_category, value)

    def __match_term_to_category(self, category: Category, term: str) -> bool:
        return term.lower() in category.name.lower()

    def __open_first_filter_match(self) -> None:
        term = self._entry.get_text()
        for item in self.__model:
            category = self.__model.get_value(item.iter, 1)
            if self.__match_term_to_category(category, term):
                self._entry.set_text(category.name)
                self.__apply_change()
                break
