# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# Copyright (c) 2009  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

import gtk
import pango

from rednotebook.util import markup
from rednotebook import undo


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

        self.main_window = main_window
        self.undo_redo_manager = main_window.undo_redo_manager

        # Maintain a list of all entered categories. Initialized by rn.__init__()
        self.categories = None

        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>' + _('Categories') + '</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, 'text', 0)
        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.WRAP_WORD
        self.cell.props.wrap_width = 200
        self.tree_view.connect_after("size-allocate", self.on_size_allocate, self.tvcolumn, self.cell)

    def node_on_top_level(self, iter):
        if not type(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]

        # Reset the renderer 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(markup.convert_from_pango(pango_markup))

    def edited_cb(self, cell, path, new_text, user_data):
        '''
        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.statusbar.show_text('"text" is a reserved keyword', error=True)
            return
        if len(new_text) < 1:
            self.statusbar.show_text(_('Empty entries are not allowed'), error=True)
            return

        liststore = user_data
        pango_markup = markup.convert_to_pango(new_text)
        liststore[path][0] = pango_markup

        # Category name changed
        if self.node_on_top_level(path):
            if new_text not in self.categories:
                self.categories.insert(0, new_text)

        # Tag name changed
        else:
            iter = self.tree_store.get_iter(path)
            iter_parent = self.tree_store.iter_parent(iter)
            tags_iter = self._get_category_iter('Tags')

            tags_node_is_parent = self.get_iter_value(iter_parent).capitalize() == 'Tags'
            if tags_node_is_parent and self.node_on_top_level(iter_parent):
                self.main_window.journal.save_old_day()

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

    def check_category(self, category):
        if category == 'text':
            self.statusbar.show_text('"text" is a reserved keyword', error=True)
            return False
        if len(category) < 1:
            self.statusbar.show_text(_('Empty category names are not allowed'), error=True)
            return False

        return True

    def check_entry(self, text):
        if len(text) < 1:
            self.statusbar.show_text(_('Empty entries are not allowed'), error=True)
            return False

        return True

    def add_element(self, parent, element_content):
        '''
        Recursive Method for adding the content
        '''
        # We want to order the entries ascendingly
        ascending = lambda (key, value): key.lower()

        for key, value in sorted(element_content.iteritems(), key=ascending):
            if key is not None:
                key_pango = markup.convert_to_pango(key)
            new_child = self.tree_store.append(parent, [key_pango])
            if not value == None:
                self.add_element(new_child, value)

    def set_day_content(self, day):
        # We want to order the categories ascendingly, having Tags first
        ascending = lambda x: '000' if x.lower() == 'tags' else x.lower()
        sorted_keys = sorted(day.content.keys(), key=ascending)

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

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

        content = self._get_element_content(None)

        return content

    def _get_element_content(self, element):
        model = self.tree_store
        if self.tree_store.iter_n_children(element) == 0:
            return None
        else:
            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).decode('utf-8')

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

        # We want to have txt2tags markup and not pango markup
        text = markup.convert_from_pango(pango_markup)
        return text

    def set_iter_value(self, iter, txt2tags_markup):
        '''
        text is txt2tags markup
        '''
        pango_markup = markup.convert_to_pango(txt2tags_markup)
        self.tree_store.set_value(iter, 0, pango_markup)

    def find_iter(self, category, entry):
        logging.debug('Looking for iter: "%s", "%s"' % (category, entry))
        category_iter = self._get_category_iter(category)

        if not category_iter:
            # If the category was not found, return None
            return None

        for iter_index in range(self.tree_store.iter_n_children(category_iter)):
            current_entry_iter = self.tree_store.iter_nth_child(category_iter, iter_index)
            current_entry = self.get_iter_value(current_entry_iter)
            if str(current_entry) == str(entry):
                return current_entry_iter

        # If the entry was not found, return None
        logging.debug('Iter not found: "%s", "%s"' % (category, entry))
        return None

    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('Category not found: "%s"' % category_name)
        return None

    def add_entry(self, category, entry, undoing=False):
        if category not in self.categories and category is not None:
            self.categories.insert(0, category)

        category_iter = self._get_category_iter(category)

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

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

        if not undoing:
            undo_func = lambda: self.delete_node(self.find_iter(category, entry), undoing=True)
            redo_func = lambda: self.add_entry(category, entry, undoing=True)
            action = undo.Action(undo_func, redo_func, 'categories_tree_view')
            self.undo_redo_manager.add_action(action)

        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

        # Save for undoing ------------------------------------

        # An entry is deleted
        # We want to delete empty categories too
        if not self.node_on_top_level(iter):
            deleting_entry = True
            category_iter = self.tree_store.iter_parent(iter)
            category = self.get_iter_value(category_iter)
            entries = [self.get_iter_value(iter)]

        # A category is deleted
        else:
            deleting_entry = False
            category_iter = iter
            category = self.get_iter_value(category_iter)
            entries = self._get_element_content(category_iter).keys()

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

        self.tree_store.remove(iter)

        # Delete empty category
        if deleting_entry and self.empty(category_iter):
            self.tree_store.remove(category_iter)

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

        if not undoing:

            def undo_func():
                for entry in entries:
                    self.add_entry(category, entry, undoing=True)

            def redo_func():
                for entry in entries:
                    delete_iter = self.find_iter(category, entry)
                    self.delete_node(delete_iter, undoing=True)

            action = undo.Action(undo_func, redo_func, 'categories_tree_view')
            self.undo_redo_manager.add_action(action)

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

    def delete_selected_node(self):
        '''
        This method used to show a warning dialog. This has become obsolete
        with the addition of undo functionality for the categories
        '''
        selected_iter = self.get_selected_node()
        if selected_iter:
            self.delete_node(selected_iter)
            return

    def on_key_press_event(self, widget, event):
        """
        @param widget - gtk.TreeView - The Tree View
        @param event - gtk.gdk.event - Event information

        Delete an annotation node when user hits "Delete"
        """
        keyname = gtk.gdk.keyval_name(event.keyval)
        logging.info('Pressed key: %s' % keyname)

        if keyname == 'Delete':
            self._on_delete_entry_clicked(None)
        elif keyname == 'Menu':
            # Does not work
            logging.info('Context Menu does not work')
            self.context_menu.popup(None, None, None, 0, event.time)

    def on_button_press_event(self, widget, event):
        """
        @param widget - gtk.TreeView - The Tree View
        @param event - gtk.gdk.event - Event information
        """
        #Get the path at the specific mouse position
        path = widget.get_path_at_pos(int(event.x), int(event.y))
        if (path == 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, 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')

        new_entry_dialog = self.main_window.new_entry_dialog

        # Create actions
        actiongroup.add_actions([
            ('ChangeEntry', gtk.STOCK_EDIT,
                _('Change this text'),
                None, None, self._on_change_entry_clicked
            ),
            ('AddEntry', gtk.STOCK_NEW,
                _('Add a new entry'),
                None, None, self._on_add_entry_clicked
            ),
            ('Delete', gtk.STOCK_DELETE,
                _('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)

        # Create a Menu
        menu = uimanager.get_widget('/ContextMenu')
        return menu

    def _on_change_entry_clicked(self, action):
        iter = self.get_selected_node()
        self.tree_view.set_cursor(self.tree_store.get_path(iter),
                                focus_column=self.tvcolumn, start_editing=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()
