# -----------------------------------------------------------------------
# Copyright (c) 2008-2024 Jendrik Seipp
#
# RedNotebook is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# RedNotebook is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with RedNotebook; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# -----------------------------------------------------------------------

import logging

from gi.repository import Gdk, Gtk, Pango

from rednotebook.util import utils
from rednotebook.util.pango_markup import convert_from_pango, convert_to_pango


class CategoriesTreeView:
    def __init__(self, tree_view, main_window):
        self.tree_view = tree_view

        self.main_window = main_window

        # Maintain a list of all entered categories. Initialized by rn.__init__()
        self.categories = []
        self.last_category = ""

        self.statusbar = self.main_window.statusbar

        # create a TreeStore with one string column to use as the model
        self.tree_store = Gtk.TreeStore(str)

        # create the TreeView using tree_store
        self.tree_view.set_model(self.tree_store)

        # create the TreeViewColumn to display the data
        self.tvcolumn = Gtk.TreeViewColumn()
        label = Gtk.Label()
        label.set_markup("<b>" + _("Tags") + "</b>")
        label.show()
        self.tvcolumn.set_widget(label)

        # add tvcolumn to tree_view
        self.tree_view.append_column(self.tvcolumn)

        # create a CellRendererText to render the data
        self.cell = Gtk.CellRendererText()

        self.cell.set_property("editable", True)
        self.cell.connect("edited", self.edited_cb, self.tree_store)
        self.cell.connect("editing-started", self.on_editing_started)

        # add the cell to the tvcolumn and allow it to expand
        self.tvcolumn.pack_start(self.cell, True)

        # set the cell "text" attribute to column 0 - retrieve text
        # from that column in tree_store
        self.tvcolumn.add_attribute(self.cell, "markup", 0)

        # make it searchable
        self.tree_view.set_search_column(0)

        # Allow sorting on the column
        self.tvcolumn.set_sort_column_id(0)

        # Enable a context menu
        self.context_menu = self._get_context_menu()
        self.context_menu.attach_to_widget(self.tree_view, lambda x, y: None)

        self.tree_view.connect("button-press-event", self.on_button_press_event)
        self.tree_view.connect("key-press-event", self.on_key_press_event)

        # Wrap lines
        self.cell.props.wrap_mode = Pango.WrapMode.WORD
        self.cell.props.wrap_width = 200
        self.tree_view.connect_after(
            "size-allocate", self.on_size_allocate, self.tvcolumn, self.cell
        )

    def _show_error_msg(self, text):
        self.main_window.journal.show_message(text, error=True)

    def add_category(self, category):
        """Add a new category name and sort all categories."""
        if category:
            self.last_category = category
        if category is None or category in self.categories:
            return
        self.categories.append(category)
        self.categories.sort(key=utils.sort_asc)

    def node_on_top_level(self, iter):
        if isinstance(iter, Gtk.TreeIter):
            # iter is a path -> convert to iter
            iter = self.tree_store.get_iter(iter)
        assert self.tree_store.iter_is_valid(iter)
        return self.tree_store.iter_depth(iter) == 0

    def on_editing_started(self, cell, editable, path):
        # Let the renderer use text not markup temporarily
        self.tvcolumn.clear_attributes(self.cell)
        self.tvcolumn.add_attribute(self.cell, "text", 0)

        # Fetch the markup
        pango_markup = self.tree_store[path][0]

        # Tell the renderer NOT to use markup
        self.tvcolumn.clear_attributes(self.cell)
        self.tvcolumn.add_attribute(self.cell, "markup", 0)

        # We want to show txt2tags markup and not pango markup
        editable.set_text(convert_from_pango(pango_markup))

    def edited_cb(self, cell, path, new_text, liststore):
        """
        Called when text in a cell is changed

        new_text is txt2tags markup
        """
        if new_text == "text" and self.node_on_top_level(path):
            self._show_error_msg('"text" is a reserved keyword')
            return
        if len(new_text) < 1:
            self._show_error_msg(_("Empty entries are not allowed"))
            return

        liststore[path][0] = convert_to_pango(new_text)

        # Category name changed
        if self.node_on_top_level(path):
            self.add_category(new_text)

        # Update cloud
        self.main_window.cloud.update()

    def check_category(self, category):
        if category == "text":
            self._show_error_msg('"text" is a reserved keyword')
            return False
        assert category
        return True

    def add_element(self, parent, element_content):
        """
        Recursive Method for adding the content
        """
        for key, value in sorted(
            iter(element_content.items()), key=lambda key_value: key_value[0].lower()
        ):
            if key is not None:
                key_pango = convert_to_pango(key)
            new_child = self.tree_store.append(parent, [key_pango])
            if value is not None:
                self.add_element(new_child, value)

    def set_day_content(self, day):
        # We want to order the categories ascendingly
        sorted_keys = sorted(day.content.keys(), key=lambda x: x.lower())

        for key in sorted_keys:
            if key != "text":
                value = day.content[key]
                self.add_element(None, {key: value})
        self.tree_view.expand_all()

    def get_day_content(self):
        return {} if self.empty() else self._get_element_content(None)

    def _get_element_content(self, element):
        model = self.tree_store
        if self.tree_store.iter_n_children(element) == 0:
            return None
        content = {}

        for i in range(model.iter_n_children(element)):
            child = model.iter_nth_child(element, i)
            txt2tags_markup = self.get_iter_value(child)
            content[txt2tags_markup] = self._get_element_content(child)

        return content

    def empty(self, category_iter=None):
        """
        Tests whether a category has children

        If no category is given, test whether there are any categories
        """
        return self.tree_store.iter_n_children(category_iter) == 0

    def clear(self):
        self.tree_store.clear()
        assert self.empty(), self.tree_store.iter_n_children(None)

    def get_iter_value(self, iter):
        # Let the renderer use text not markup temporarily
        self.tvcolumn.clear_attributes(self.cell)
        self.tvcolumn.add_attribute(self.cell, "text", 0)

        pango_markup = self.tree_store.get_value(iter, 0)

        # Reset the renderer to use markup
        self.tvcolumn.clear_attributes(self.cell)
        self.tvcolumn.add_attribute(self.cell, "markup", 0)

        return convert_from_pango(pango_markup)

    def set_iter_value(self, iter, txt2tags_markup):
        pango_markup = convert_to_pango(txt2tags_markup)
        self.tree_store.set_value(iter, 0, pango_markup)

    def _get_category_iter(self, category_name):
        for iter_index in range(self.tree_store.iter_n_children(None)):
            current_category_iter = self.tree_store.iter_nth_child(None, iter_index)
            current_category_name = self.get_iter_value(current_category_iter)
            if str(current_category_name).lower() == str(category_name).lower():
                return current_category_iter

        # If the category was not found, return None
        logging.debug(f'Category not found: "{category_name}"')
        return None

    def add_entry(self, category, entry, undoing=False):
        self.add_category(category)

        category_iter = self._get_category_iter(category)

        entry_pango = convert_to_pango(entry)
        category_pango = convert_to_pango(category)

        # If category exists add entry to existing category, else add new category
        if category_iter is None:
            category_iter = self.tree_store.append(None, [category_pango])

        # Only add entry if there is one
        if entry_pango:
            self.tree_store.append(category_iter, [entry_pango])

        self.tree_view.expand_all()

    def get_selected_node(self):
        """
        Returns selected node or None if none is selected
        """
        tree_selection = self.tree_view.get_selection()
        model, selected_iter = tree_selection.get_selected()
        return selected_iter

    def delete_node(self, iter, undoing=False):
        if not iter:
            # The user has changed the text of the node or deleted it
            return

        # Delete ---------------------------------------------

        self.tree_store.remove(iter)

        # ----------------------------------------------------

        # Update cloud
        self.main_window.cloud.update()

    def delete_selected_node(self):
        if selected_iter := self.get_selected_node():
            self.delete_node(selected_iter)
            return

    def on_key_press_event(self, _view, event):
        keyname = Gdk.keyval_name(event.keyval)
        if keyname == "Delete":
            self._on_delete_entry_clicked(None)
        elif keyname == "F2":
            self._on_change_entry_clicked(None)

    def on_button_press_event(self, widget, event):
        # Get the path at the specific mouse position.
        path = widget.get_path_at_pos(int(event.x), int(event.y))
        if path is None:
            """If we didn't get a path then we don't want anything
            to be selected."""
            selection = widget.get_selection()
            selection.unselect_all()

        # Do not show change and delete options, if nothing is selected
        something_selected = path is not None
        uimanager = self.main_window.uimanager
        change_entry_item = uimanager.get_widget("/ContextMenu/ChangeEntry")
        change_entry_item.set_sensitive(something_selected)
        delete_entry_item = uimanager.get_widget("/ContextMenu/Delete")
        delete_entry_item.set_sensitive(something_selected)

        if event.button == 3:
            # This is a right-click.
            self.context_menu.popup(None, None, None, None, event.button, event.time)

    def _get_context_menu(self):
        context_menu_xml = """
        <ui>
        <popup action="ContextMenu">
            <menuitem action="ChangeEntry"/>
            <menuitem action="AddEntry"/>
            <menuitem action="Delete"/>
        </popup>
        </ui>"""

        uimanager = self.main_window.uimanager

        # Create an ActionGroup
        actiongroup = Gtk.ActionGroup("ContextMenuActionGroup")

        # Create actions
        actiongroup.add_actions(
            [
                (
                    "ChangeEntry",
                    None,
                    _("Change this text"),
                    None,
                    None,
                    self._on_change_entry_clicked,
                ),
                (
                    "AddEntry",
                    None,
                    _("Add a new entry"),
                    None,
                    None,
                    self._on_add_entry_clicked,
                ),
                (
                    "Delete",
                    None,
                    _("Delete this entry"),
                    None,
                    None,
                    self._on_delete_entry_clicked,
                ),
            ]
        )

        # Add the actiongroup to the uimanager
        uimanager.insert_action_group(actiongroup, 0)

        # Add a UI description
        uimanager.add_ui_from_string(context_menu_xml)

        return uimanager.get_widget("/ContextMenu")

    def _on_change_entry_clicked(self, action):
        iter = self.get_selected_node()
        self.tree_view.set_cursor(self.tree_store.get_path(iter), self.tvcolumn, True)

    def _on_add_entry_clicked(self, action):
        iter = self.get_selected_node()

        dialog = self.main_window.new_entry_dialog

        # Either nothing was selected -> show normal new_entry_dialog
        if iter is None:
            dialog.show_dialog()
        # or a category was selected
        elif self.node_on_top_level(iter):
            category = self.get_iter_value(iter)
            dialog.show_dialog(category=category)
        # or an entry was selected
        else:
            parent_iter = self.tree_store.iter_parent(iter)
            category = self.get_iter_value(parent_iter)
            dialog.show_dialog(category=category)

    def _on_delete_entry_clicked(self, action):
        self.delete_selected_node()

    def on_size_allocate(self, treeview, allocation, column, cell):
        """
        Code from pychess project
        (http://code.google.com/p/pychess/source/browse/trunk/lib/pychess/
        System/uistuff.py?r=1025#62)

        Allows dynamic line wrapping in a treeview
        """
        other_columns = (c for c in treeview.get_columns() if c != column)
        new_width = allocation.width - sum(c.get_width() for c in other_columns)
        new_width -= treeview.style_get_property("horizontal-separator") * 2

        # Customize for treeview with expanders.
        # The behaviour can only be fitted to one depth -> take the second one.
        new_width -= treeview.style_get_property("expander-size") * 3

        if cell.props.wrap_width == new_width or new_width <= 0:
            return
        cell.props.wrap_width = new_width
        store = treeview.get_model()
        iter = store.get_iter_first()
        while iter and store.iter_is_valid(iter):
            store.row_changed(store.get_path(iter), iter)
            iter = store.iter_next(iter)
        treeview.set_size_request(0, -1)

        # The heights may have changed.
        column.queue_resize()
