# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import GLib, GObject

from collections import OrderedDict
from threading import Thread
from typing import Optional

from iotas.category_storage import CategoryStorage
from iotas.category import Category, CategorySpecialPurpose
from iotas.category_list_model import CategoryListModel
from iotas.category_treeview_list_store import CategoryTreeViewListStore


class CategoryManager(GObject.Object):
    __gtype_name__ = "CategoryManager"
    __gsignals__ = {
        "initial-load-complete": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    def __init__(self, db: CategoryStorage) -> None:
        super().__init__()
        self.__db = db
        self.__list_model = CategoryListModel()
        self.__tree_store = CategoryTreeViewListStore()
        self.__all_category: Optional[Category] = None
        self.__uncategorised_category: Optional[Category] = None
        self.__categories: OrderedDict[str, Category] = OrderedDict()
        self.__have_sub_category = False

    def populate(self) -> None:
        """Populate categories from database."""

        def chain_population_to_stores() -> None:
            self.__list_model.populate(self.__get_with_special())
            # Lean on the sorting in the list store to populate the tree store with its sorted
            # list including parent categories
            categories = []
            for i in range(2, self.__list_model.get_n_items()):
                categories.append(self.__list_model.get_item(i))
            self.__tree_store.populate(categories)
            self.emit("initial-load-complete")

        def thread_do() -> None:
            categories = self.__db.get_all_categories()
            for category in categories:
                if category.special_purpose == CategorySpecialPurpose.ALL:
                    self.__all_category = category
                elif category.special_purpose == CategorySpecialPurpose.UNCATEGORISED:
                    self.__uncategorised_category = category
                else:
                    self.__categories[category.name] = category
                    if category.is_sub_category:
                        self.__ensure_parents(category, False)
                        self.have_sub_category = True

            GLib.idle_add(chain_population_to_stores)

        thread = Thread(target=thread_do)
        thread.daemon = True
        thread.start()

    def note_added(self, new_category: str) -> None:
        """Make updates for a created note.

        :param Optional[str] new_category: The category a note has been added to
        """
        self.note_category_changed(None, new_category)

    def note_category_changed(
        self, old_category: Optional[str], new_category: Optional[str]
    ) -> None:
        """Make updates for a note changing category.

        :param Optional[str] old_category: The category a note has been removed from
        :param Optional[str] new_category: The category a note has been added to
        """
        if old_category is not None:
            self.__decrement_category(old_category, new_category is None)

        if new_category is not None:
            if new_category != "":
                category_updated = self.__increment_category(new_category)
                if not category_updated:
                    self.__add_new_category(new_category)
            else:
                # Moving into uncategorised
                self.__increment_uncategorised()
            if old_category is None:
                # Handle new note creation
                self.__increment_all()

        if old_category is not None:
            self.__handle_possible_emptied_category(old_category)

    def note_deleted(self, old_category: str) -> None:
        """Make updates for a deleted note.

        :param Optional[str] old_category: The category a note has been removed from
        """
        self.note_category_changed(old_category, None)

    # TODO with Gtk v4.10+ replace this with a binding from the model count to TreeExpander's
    # hide-expander
    def recreate_category_for_expandable_change(self, category: Category) -> None:
        """Recreate the provided category.

        Triggers an update in the model when a category changes in and out of leaf state.

        :param Category category: The category to recreate
        """
        parent = category.get_parent()
        if parent is not None:
            parent.remove_child(category)
        new_category = category.duplicate()

        del self.__categories[category.name]
        self.__list_model.remove(category)
        self.__tree_store.delete(category)

        self.__categories[category.name] = new_category
        self.__list_model.add(new_category)
        self.__tree_store.add(new_category)
        if parent is not None:
            parent.add_child(new_category)

    def get_normal_category(self, name: str) -> tuple[Category | None, int]:
        return self.__list_model.get_non_special(name)

    def get_special_purpose_category(self, purpose: CategorySpecialPurpose) -> tuple[Category, int]:
        return self.__list_model.get_special(purpose)

    @GObject.Property(type=bool, default=False)
    def have_sub_category(self) -> bool:
        return self.__have_sub_category

    @have_sub_category.setter
    def set_have_sub_category(self, value: bool) -> None:
        self.__have_sub_category = value

    @GObject.Property(type=CategoryListModel, default=False)
    def list_model(self) -> CategoryListModel:
        return self.__list_model

    @GObject.Property(type=CategoryTreeViewListStore, default=False)
    def tree_store(self) -> CategoryTreeViewListStore:
        return self.__tree_store

    @GObject.Property(type=Category, default=None)
    def all_category(self) -> Optional[Category]:
        return self.__all_category

    def __get_with_special(self) -> list[Category]:
        assert self.__uncategorised_category
        assert self.__all_category
        out = list(self.__categories.values())
        out.insert(0, self.__uncategorised_category)
        out.insert(0, self.__all_category)
        return out

    def __add_new_category(self, category_name: str) -> None:
        category = Category(category_name, 1)
        self.__categories[category_name] = category
        parent = None
        if category.is_sub_category:
            self.__ensure_parents(category, True)
            parent = category.get_parent()
            self.have_sub_category = True
        self.__list_model.add(category)
        self.__tree_store.add(category)
        # If parent has just changed to expandable, recreate it
        if parent is not None and parent.number_of_direct_children == 1:
            self.recreate_category_for_expandable_change(parent)

    def __increment_category(self, category_name: str) -> bool:
        category_updated = False
        if category_name in self.__categories:
            self.__categories[category_name].increase_note_count()
            category_updated = True
        return category_updated

    def __decrement_category(self, old_category_name: str, note_removed: bool) -> None:
        assert self.__uncategorised_category
        assert self.__all_category
        if old_category_name == "":
            # Moving out of uncategorised
            self.__uncategorised_category.decrease_note_count()
        else:
            if old_category_name in self.__categories:
                self.__categories[old_category_name].decrease_note_count()

        # Handle deletion
        if note_removed:
            self.__all_category.decrease_note_count()

    def __handle_possible_emptied_category(self, old_category: str) -> None:
        if old_category not in self.__categories:
            return

        category = self.__categories[old_category]
        if category.note_count_with_children == 0:
            delete_stack = [category]
            parent = category.get_parent()
            while parent is not None and parent.note_count_with_children == 0:
                delete_stack.append(parent)
                parent = parent.get_parent()
            delete_stack.reverse()

            delete_parent = delete_stack[0].get_parent()
            if delete_parent is not None:
                # If we reach a node to be removed that has a parent with other notes
                # (on itself or siblings) remove this node from its parent
                delete_parent.remove_child(delete_stack[0])

            # Iterate deleting
            for parent in delete_stack:
                del self.__categories[parent.name]
                self.__list_model.remove(parent)
                self.__tree_store.delete(parent)

        self.__refresh_have_sub_category()

    def __increment_uncategorised(self) -> None:
        assert self.__uncategorised_category
        self.__uncategorised_category.increase_note_count()

    def __increment_all(self) -> None:
        assert self.__all_category
        self.__all_category.increase_note_count()

    def __ensure_parents(self, category: Category, populate_models: bool) -> None:
        parents = category.name.split("/")[:-1]
        full_path_parents = []
        parts = []
        for parent_name in parents:
            parts.append(parent_name)
            full_path_parents.append("/".join(parts))

        level_child = category
        for parent_name in reversed(full_path_parents):
            if level_child.get_parent() is None:
                # Check for existing
                if parent_name in self.__categories:
                    parent = self.__categories[parent_name]
                else:
                    # Create if one doesn't exist
                    parent = Category(parent_name, 0)
                    self.__categories[parent_name] = parent
                    if populate_models:
                        self.__list_model.add(parent)
                        self.__tree_store.add(parent)
                level_child.set_parent(parent)

                # Add child
                parent.add_child(level_child)

                # Update child note count
                parent.increase_child_note_count(level_child.note_count_with_children)

            # Prepare as child for next level
            level_child = parent

    def __refresh_have_sub_category(self):
        have = False
        for category in self.__categories.values():
            if category.is_sub_category:
                have = True
                break
        if self.have_sub_category != have:
            self.have_sub_category = have
