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

from typing import Callable, Optional

from iotas.category import Category, CategorySpecialPurpose
from iotas.category_manager import CategoryManager
from iotas.sidebar_row import SidebarRow
from iotas.sync_manager import SyncManager
from iotas.theme_selector import ThemeSelector


@Gtk.Template(resource_path="/org/gnome/gitlab/cheywood/Iotas/ui/sidebar.ui")
class Sidebar(Adw.Bin):
    __gtype_name__ = "Sidebar"

    _listview = Gtk.Template.Child()
    _close_button = Gtk.Template.Child()
    _menu_button = Gtk.Template.Child()

    def __init__(self):
        super().__init__()
        self.__init_listview()

    def setup(
        self,
        sync_manager: SyncManager,
        category_manager: CategoryManager,
        category_activated_func: Callable[[Category], None],
    ) -> None:
        """Perform initial setup.

        :param SyncManager sync_manager: Remote sync manager
        :param CategoryManager: Category manager
        :param Callable[[Category], None] category_activated_func: Function to call upon category
            activation.
        """
        self.__sync_manager = sync_manager
        self.__category_manager = category_manager
        self.__category_activated_func = category_activated_func
        self.__filter_model.set_model(self.__category_manager.list_model)
        self.__category_manager.connect("initial-load-complete", self.__populate)
        self._menu_button.get_popover().add_child(ThemeSelector(), "theme")

    def select_and_fetch_category(
        self,
        category_name: Optional[str],
        purpose: Optional[CategorySpecialPurpose] = None,
    ) -> Optional[Category]:
        """Select and fetch the specified category.

        :param Optional[str]: Name of category
        :param Optional[CategorySpecialPurpose]: Alternatively, a category special purpose
        :return: The category, if found
        :rtype: Optional[Category]
        """
        category = None
        if purpose is not None:
            (category, index) = self.__category_manager.get_special_purpose_category(purpose)
        else:
            (category, index) = self.__category_manager.get_normal_category(category_name)
        self.__selection_model.set_selected(index)
        return category

    def take_focus(self) -> None:
        """Grab keyboard focus."""
        self._listview.grab_focus()
        # Ensure one of the rows has focus, but don't force it to the top row
        if self._listview.has_focus():
            for row in self._listview:
                row.grab_focus()
                break

    def has_focus_within(self) -> bool:
        """Fetch whether the sidebar or any of its rows have focus.

        :return: Whethe focused
        :rtype: bool
        """
        # There's considerably more complexity here than expected due to Gtk.Widget.is_ancestor
        # not behaving as expected.
        if self._listview.has_focus():
            return True
        else:
            for row in self._listview:
                sidebar_row = row.get_first_child()
                if sidebar_row.has_focus():
                    return True
        return False

    def show_buttons(self, close_visible: bool, menu_visible: bool) -> None:
        """Set visibility on buttons.

        :param bool close_visible: Close button visibility
        :param bool menu_visible: Menu button visibility
        """
        self._close_button.set_visible(close_visible)
        self._menu_button.set_visible(menu_visible)

    def __init_listview(self) -> None:
        self.__factory = Gtk.SignalListItemFactory()
        self.__factory.connect("setup", self.__setup_listitem)
        self.__factory.connect("bind", self.__bind_listitem)
        self.__factory.connect("unbind", self.__unbind_listitem)
        self._listview.set_factory(self.__factory)

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

        def filter_root_categories(category: Category) -> bool:
            return "/" not in category.name

        self.__custom_filter.set_filter_func(filter_root_categories)

    def __populate(self, _obj: GObject.Object) -> None:
        self.__tree_model = Gtk.TreeListModel.new(
            self.__filter_model, False, False, self.__create_subcategory_models
        )
        self.__selection_model = Gtk.SingleSelection.new(self.__tree_model)
        self._listview.set_model(self.__selection_model)

        self._listview.connect("activate", self.__on_listview_row_activated)

        controller = Gtk.EventControllerKey()
        controller.connect("key-pressed", self.__on_key_pressed)
        self._listview.add_controller(controller)

    def __create_subcategory_models(self, category: Category) -> Optional[Gio.ListModel]:
        if not category.is_sub_category:

            def filter_toplevel_parent(category: Category, parent_name: str) -> bool:
                return parent_name != category.name and category.name.startswith(parent_name + "/")

            filter = Gtk.CustomFilter.new(filter_toplevel_parent, category.name)
            model = Gtk.FilterListModel.new(self.__category_manager.list_model)
            model.set_filter(filter)
            if model.get_n_items() > 0:
                model.connect("notify::n-items", self.__subcategory_store_items_changed, category)
                return model
        return None

    def __setup_listitem(self, _factory: Gtk.SignalListItemFactory, listitem: Gtk.ListItem) -> None:
        row = SidebarRow(self.__category_activated_func, self.__category_manager)
        listitem.set_child(row)

    def __bind_listitem(self, _factory: Gtk.SignalListItemFactory, listitem: Gtk.ListItem) -> None:
        expander = listitem.get_child()
        tree_list_row = listitem.get_item()
        expander.set_list_row(tree_list_row)
        category = tree_list_row.get_item()
        expander.bind(category)

    def __unbind_listitem(
        self, _factory: Gtk.SignalListItemFactory, listitem: Gtk.ListItem
    ) -> None:
        row = listitem.get_child()
        row.unbind()

    def __subcategory_store_items_changed(
        self, model: Gtk.FilterListModel, _n_items: int, category: Category
    ) -> None:
        if model.get_n_items() == 0:
            self.__category_manager.recreate_category_for_expandable_change(category)

    def __expand_selected_row(self, expanded: bool) -> None:
        selected = self.__selection_model.get_selected()
        selected_row = self.__selection_model.get_item(selected)
        if selected_row.is_expandable() and selected_row.get_expanded() is not expanded:
            selected_row.set_expanded(expanded)

    def __on_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ):
        if keyval in (Gdk.KEY_Right, Gdk.KEY_KP_Right):
            self.__expand_selected_row(True)
            return Gdk.EVENT_STOP
        elif keyval in (Gdk.KEY_Left, Gdk.KEY_KP_Left):
            self.__expand_selected_row(False)
            return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

    def __on_listview_row_activated(self, listview: Gtk.ListView, position: int) -> None:
        model = listview.get_model()
        model.select_item(position, True)
        row = model.get_item(position)
        category = row.get_item()
        self.__category_activated_func(category)
