# -*- coding: utf-8 -*-
#
# Copyright (c) 2005,2006,2007,2008,2009 Brett Adams <brett@belizebotanic.org>
# Copyright (c) 2012-2018 Mario Frasca <mario@anche.no>
#
# This file is part of ghini.desktop.
#
# ghini.desktop 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 3 of the License, or
# (at your option) any later version.
#
# ghini.desktop 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 ghini.desktop. If not, see <http://www.gnu.org/licenses/>.
#
# __init__.py -- tag plugin
#
# Description:
#
import os
import traceback

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk

import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

from sqlalchemy import (
    Column, Unicode, UnicodeText, Integer, String, ForeignKey)
from sqlalchemy.orm import relation
from sqlalchemy.orm.exc import DetachedInstanceError
from sqlalchemy import and_
from sqlalchemy.exc import DBAPIError, InvalidRequestError
from sqlalchemy.orm.session import object_session


import bauble
from bauble import ui
from bauble import db
from bauble import editor
from bauble import pluginmgr
from bauble import paths
from bauble import search
from bauble import utils
from bauble.view import InfoBox, InfoExpander, SearchView, Action

from bauble.editor import (
    GenericEditorView, GenericEditorPresenter)


class TagsMenuManager:
    def __init__(self):
        self.menu_item = None
        self.active_tag_name = None

    def reset(self, make_active_tag=None):
        """initialize or replace Tags menu in main menu
        """
        self.active_tag_name = make_active_tag and make_active_tag.tag
        tags_menu = self.build_menu()
        if self.menu_item is None:
            self.menu_item = bauble.gui.add_menu(_("Tags"), tags_menu)
        else:
            self.menu_item.set_submenu(tags_menu)
            self.menu_item.show_all()
        self.show_active_tag()

    def show_active_tag(self):
        for c in list(self.item_list.values()):
            c.set_image(None)
        widget = self.item_list.get(self.active_tag_name)
        if widget:
            image = Gtk.Image()
            image.set_from_stock(Gtk.STOCK_APPLY, Gtk.IconSize.MENU)
            widget.set_image(image)
            self.apply_active_tag_menu_item.set_sensitive(True)
            self.remove_active_tag_menu_item.set_sensitive(True)
        else:
            self.apply_active_tag_menu_item.set_sensitive(False)
            self.remove_active_tag_menu_item.set_sensitive(False)

    def item_activated(self, widget, tag_name):
        self.active_tag_name = tag_name
        self.show_active_tag()
        bauble.gui.send_command('tag="%s"' % tag_name)
        from bauble.view import SearchView
        view = bauble.gui.get_view()
        if isinstance(view, SearchView):
            view.results_view.expand_to_path(Gtk.TreePath.new_first())

    def build_menu(self):
        """build tags Gtk.Menu based on current data
        """
        self.item_list = {}
        tags_menu = Gtk.Menu()
        tag_dir = os.path.join(paths.lib_dir(), 'plugins', 'tag')
        add_tag_menu_item = ui.create_menu_item_with_image(_('Tag Selection'), 'tag.png', tag_dir)
        add_tag_menu_item.connect('activate', _on_add_tag_activated)
        self.apply_active_tag_menu_item = ui.create_menu_item_with_image(_('Apply active tag'), 'tag_apply.png', tag_dir)
        self.apply_active_tag_menu_item.connect('activate', self.on_apply_active_tag_activated)
        self.remove_active_tag_menu_item = ui.create_menu_item_with_image(_('Remove active tag'), 'tag_remove.png', tag_dir)
        self.remove_active_tag_menu_item.connect('activate', self.on_remove_active_tag_activated)
        if bauble.gui:
            accel_group = Gtk.AccelGroup()
            bauble.gui.window.add_accel_group(accel_group)
            add_tag_menu_item.add_accelerator('activate', accel_group, ord('T'),
                                              Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE)
            self.apply_active_tag_menu_item.add_accelerator('activate', accel_group, ord('Y'),
                                                            Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE)
            key, mask = Gtk.accelerator_parse('<Control><Shift>y')
            self.remove_active_tag_menu_item.add_accelerator('activate', accel_group,
                                                             key, mask, Gtk.AccelFlags.VISIBLE)
        tags_menu.append(add_tag_menu_item)

        session = db.Session()
        query = session.query(Tag).order_by(Tag.tag)
        has_tags = query.first()
        if has_tags:
            tags_menu.append(Gtk.SeparatorMenuItem())
        submenu = {'': [None, tags_menu]}  # menuitem and submenu
                                           # corresponding to full item
                                           # path; it is a list because it
                                           # needs to be mutable
        def confirm_attach_path(parts):
            full_path = ''
            parent = tags_menu
            while parts:
                name = parts.pop()
                full_path += name
                if full_path not in submenu:
                    item = Gtk.ImageMenuItem(name)
                    parent.append(item)
                    submenu[full_path] = [item, None]
                if submenu[full_path][1] is None:
                    take_this = Gtk.Menu()
                    submenu[full_path] = [item, take_this]
                    item.set_submenu(take_this)
                parent = submenu[full_path]
                full_path += '/'
        try:
            for tag in query:
                *path, tail = tag.tag.split('/')
                head = '/'.join(path)
                item = Gtk.ImageMenuItem(tail)
                submenu[tag.tag] = [item, None]
                item.set_image(None)
                item.set_always_show_image(True)
                self.item_list[tag.tag] = item
                item.connect("activate", self.item_activated, tag.tag)
                confirm_attach_path(path)
                submenu[head][1].append(item)
        except Exception:
            logger.debug(traceback.format_exc())
            msg = _('Could not create the tags menus')
            utils.message_details_dialog(msg, traceback.format_exc(),
                                         Gtk.MessageType.ERROR)
        session.close()

        if has_tags:
            tags_menu.append(Gtk.SeparatorMenuItem())
            tags_menu.append(self.apply_active_tag_menu_item)
            tags_menu.append(self.remove_active_tag_menu_item)
            self.apply_active_tag_menu_item.set_sensitive(False)
            self.remove_active_tag_menu_item.set_sensitive(False)
        return tags_menu

    def toggle_tag(self, applying):
        view = bauble.gui.get_view()
        try:
            values = view.get_selected_values()
        except AttributeError:
            msg = _('In order to tag or untag an item you must first search for '
                    'something and select one of the results.')
            bauble.gui.show_message_box(msg)
            return
        if len(values) == 0:
            msg = _('Please select something in the search results.')
            utils.message_dialog(msg)
            return
        if self.active_tag_name is None:
            msg = _('Please make sure a tag is active.')
            utils.message_dialog(msg)
            return
        applying(self.active_tag_name, values)
        view.update_bottom_notebook()
    
    def on_apply_active_tag_activated(self, *args, **kwargs):
        logger.debug("you're applying %s to the selection", self.active_tag_name)
        self.toggle_tag(applying=tag_objects)

    def on_remove_active_tag_activated(self, *args, **kwargs):
        logger.debug("you're removing %s from the selection", self.active_tag_name)
        self.toggle_tag(applying=untag_objects)


tags_menu_manager = TagsMenuManager()


def edit_callback(tags):
    tag = tags[0]
    if tag is None:
        tag = Tag()
    view = GenericEditorView(
        os.path.join(paths.lib_dir(), 'plugins', 'tag', 'tag.glade'),
        parent=None,
        root_widget_name='tag_dialog')
    for note in tag.notes:
        view.widgets.notes_list.append((note.category, "str", note.note, "gtk-apply", note.id, True))
    presenter = TagEditorPresenter(tag, view, refresh_view=True)
    error_state = presenter.start()
    if error_state:
        presenter.session.rollback()
    else:
        presenter.commit_changes()
        tags_menu_manager.reset()
    presenter.cleanup()
    return error_state


def remove_callback(tags):
    """
    :param tags: a list of :class:`Tag` objects.
    """
    tag = tags[0]
    s = '%s: %s' % (tag.__class__.__name__, utils.xml_safe(tag))
    msg = _("Are you sure you want to remove %s?") % s
    if not utils.yes_no_dialog(msg):
        return
    session = object_session(tag)
    try:
        obj = session.query(Tag).get(tag.id)
        session.delete(obj)
        session.commit()
    except Exception as e:
        msg = _('Could not delete.\n\n%s') % utils.xml_safe(e)
        utils.message_details_dialog(msg, traceback.format_exc(),
                                     type=Gtk.MessageType.ERROR)

    # reinitialize the tag menu
    tags_menu_manager.reset()
    return True


edit_action = Action('acc_edit', _('_Edit'),
                     callback=edit_callback,
                     accelerator='<ctrl>e')
remove_action = Action('tag_remove', _('_Delete'),
                       callback=remove_callback,
                       accelerator='<ctrl>Delete', multiselect=True)

tag_context_menu = [edit_action, remove_action]


class TagEditorPresenter(GenericEditorPresenter):

    widget_to_field_map = {
        'tag_name_entry': 'tag',
        'tag_desc_textbuffer': 'description'}

    view_accept_buttons = ['tag_ok_button', 'tag_cancel_button', ]

    def on_cell_edited(self, widget, path, text):
        self.view.widgets.notes_list[path][self.column] = text

    def on_focus_child(self, tree, entry):
        if entry is not None:
            self.last_entry = entry
        else:
            tv, path = tree.get_selection().get_selected()
            self.view.widgets.notes_list[path][self.column] = self.last_entry.get_text()

    def on_cell_editing_started_col0(self, *args):
        self.column = 2

    def on_cell_editing_started_col1(self, *args):
        self.column = 0

    def on_toggle_row(self, tree, path, column):
        store = self.view.widgets.notes_list
        store[path][5] = not store[path][5]
        store[path][3] = {True: 'gtk-apply', False: 'gtk-cancel'}[store[path][5]]

    def on_add_a_note_clicked(self, *args):
        #            name --> type --> content --> icon-name --> id --> keep
        #               0        1           2             3      4        5
        self.view.widgets.notes_list.append(("","str","","gtk-apply", -1, True))
    
    def on_tag_desc_textbuffer_changed(self, widget, value=None):
        return GenericEditorPresenter.on_textbuffer_changed(
            self, widget, value, attr='description')

    def commit_changes(self):
        for row in self.view.widgets.notes_list:
            category, value_type, value, icon, note_id, keep = row
            if note_id == -1:
                if keep is True:
                    # create a new note and add it to the session
                    note = TagNote(tag=self.model, category=category,
                                   note=value, type=value_type, user=db.current_user())
                    self.session.add(note)
            else:
                # retrieve and update existing note
                note = self.session.query(TagNote).filter_by(id=note_id).one()
                if keep is False:
                    self.session.delete(note)
                else:
                    note.category = category
                    note.note = value
                    note.type = value_type
        super().commit_changes()


class TagItemGUI(editor.GenericEditorView):
    '''
    Interface for tagging individual items in the results of the SearchView
    '''
    def __init__(self, values):
        filename = os.path.join(paths.lib_dir(), 'plugins', 'tag',
                                'tag.glade')
        super().__init__(filename)
        self.item_data_label = self.widgets.items_data
        self.values = values
        self.item_data_label.set_text(', '.join([str(s) for s in self.values]))
        self.connect(self.widgets.new_button,
                     'clicked', self.on_new_button_clicked)

    def get_window(self):
        return self.widgets.tag_item_dialog

    def on_new_button_clicked(self, *args):
        '''
        create a new tag
        '''
        session = db.Session()
        tag = Tag(description='')
        session.add(tag)
        error_state = edit_callback([tag])
        if not error_state:
            model = self.tag_tree.get_model()
            model.append([False, tag.tag, False])
            tags_menu_manager.reset(tag)
        session.close()

    def on_toggled(self, renderer, path, data=None):
        '''
        tag or untag the objs in self.values
        '''
        active = not renderer.get_active()
        model = self.tag_tree.get_model()
        iter = model.get_iter(path)
        model[iter][0] = active
        model[iter][2] = False
        name = model[iter][1]
        if active:
            tag_objects(name, self.values)
        else:
            untag_objects(name, self.values)

    def build_tag_tree_columns(self):
        """
        Build the tag tree columns.
        """
        renderer = Gtk.CellRendererToggle()
        self.connect(renderer, 'toggled', self.on_toggled)
        renderer.set_property('activatable', True)
        toggle_column = Gtk.TreeViewColumn(None, renderer)
        toggle_column.add_attribute(renderer, "active", 0)
        toggle_column.add_attribute(renderer, "inconsistent", 2)

        renderer = Gtk.CellRendererText()
        tag_column = Gtk.TreeViewColumn(None, renderer, text=1)

        return [toggle_column, tag_column]

    def on_key_released(self, widget, event):
        '''
        if the user hits the delete key on a selected tag in the tag editor
        then delete the tag
        '''
        keyname = Gdk.keyval_name(event.keyval)
        if keyname != "Delete":
            return
        model, row_iter = self.tag_tree.get_selection().get_selected()
        tag_name = model[row_iter][1]
        msg = _('Are you sure you want to delete the tag "%s"?') % tag_name
        if not utils.yes_no_dialog(msg):
            return
        session = db.Session()
        try:
            query = session.query(Tag)
            tag = query.filter_by(tag=str(tag_name)).one()
            session.delete(tag)
            session.commit()
            model.remove(row_iter)
            tags_menu_manager.reset()
            view = bauble.gui.get_view()
            if hasattr(view, 'update'):
                view.update()
        except Exception as e:
            utils.message_details_dialog(utils.xml_safe(str(e)),
                                         traceback.format_exc(),
                                         Gtk.MessageType.ERROR)
        finally:
            session.close()

    def start(self):
        # we keep restarting the dialog here since the gui was created with
        # glade then the 'new tag' button emits a response we want to ignore
        self.tag_tree = self.widgets.tag_tree

        # we remove the old columns and create new ones each time the
        # tag editor is started since we have to connect and
        # disconnect the toggled signal each time
        list(map(self.tag_tree.remove_column, self.tag_tree.get_columns()))
        columns = self.build_tag_tree_columns()
        for col in columns:
            self.tag_tree.append_column(col)

        # create the model
        model = Gtk.ListStore(bool, str, bool)
        tag_all, tag_some, tag_none = get_tag_ids(self.values)
        session = db.Session()  # we need close it
        tag_query = session.query(Tag)
        for tag in tag_query:
            model.append([tag.id in tag_all, tag.tag, tag.id in tag_some])
        self.tag_tree.set_model(model)

        self.tag_tree.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
        self.connect(self.tag_tree, "key-release-event", self.on_key_released)

        response = self.get_window().run()
        while response != Gtk.ResponseType.OK \
                and response != Gtk.ResponseType.DELETE_EVENT:
            response = self.get_window().run()

        self.get_window().hide()
        self.disconnect_all()
        session.close()


TagNote = db.make_note_class('Tag')

class Tag(db.Base, db.WithNotes):
    """
    :Table name: tag
    :Columns:
      tag: :class:`sqlalchemy.types.Unicode`
        The tag name.
      description: :class:`sqlalchemy.types.Unicode`
        A description of this tag.
    """
    __tablename__ = 'tag'
    __mapper_args__ = {'order_by': 'tag'}

    # columns
    tag = Column(Unicode(64), unique=True, nullable=False)
    description = Column(UnicodeText)

    # relations
    _objects = relation('TaggedObj', cascade='all, delete-orphan',
                        backref='tag')

    __my_own_timestamp = None
    __last_objects = None

    def __str__(self):
        try:
            return str(self.tag)
        except DetachedInstanceError:
            return db.Base.__str__(self)

    def markup(self):
        return '%s Tag' % self.tag

    def tag_objects(self, objects):
        session = object_session(self)
        for obj in objects:
            cls = and_(TaggedObj.obj_class == _classname(obj),
                       TaggedObj.obj_id == obj.id,
                       TaggedObj.tag_id == self.id)
            ntagged = session.query(TaggedObj).filter(cls).count()
            if ntagged == 0:
                tagged_obj = TaggedObj(obj_class=_classname(obj), obj_id=obj.id,
                                       tag=self)
                session.add(tagged_obj)

    @property
    def objects(self):
        """return all tagged objects

        reuse last result if nothing was changed in the database since
        list was retrieved.
        """
        if self.__my_own_timestamp is not None:
            # should I update my list?
            session = object_session(self)
            last_history = session.query(db.History)\
                .order_by(db.History.timestamp.desc())\
                .limit(1).one()
            if last_history.timestamp > self.__my_own_timestamp:
                self.__last_objects = None
        if self.__last_objects is None:
            # here I update my list
            from datetime import datetime
            self.__my_own_timestamp = datetime.now()
            self.__last_objects = self.get_tagged_objects()
        # here I return my list
        return self.__last_objects

    def is_tagging(self, obj):
        """tell whether self tags obj

        """
        return obj in self.objects

    def get_tagged_objects(self):
        """
        Return all object tagged with tag.

        """
        session = object_session(self)

        r = [session.query(mapper).filter_by(id=obj_id).first()
             for mapper, obj_id in _get_tagged_object_pairs(self)]

        # if `self` was tagging objects that have been later removed from
        # the database, those reference here become `None`. we filter them
        # out, but what about we remove the reference pair?
        return [i for i in r if i is not None]

    @classmethod
    def attached_to(cls, obj):
        """return the list of tags attached to obj

        this is a class method, so more classes can invoke it.
        """
        session = object_session(obj)
        if not session:
            return []
        modname = type(obj).__module__
        clsname = type(obj).__name__
        full_cls_name = '%s.%s' % (modname, clsname)
        qto = session.query(TaggedObj).filter(
            TaggedObj.obj_class == full_cls_name,
            TaggedObj.obj_id == obj.id)
        return [i.tag for i in qto.all()]

    def search_view_markup_pair(self):
        '''provide the two lines describing object for SearchView row.
        '''
        import inspect
        logging.debug('entering search_view_markup_pair %s, %s' % (
            self, str(inspect.stack()[1])))
        objects = self.objects
        classes = set(type(o) for o in objects)
        if len(classes) == 1:
            fine_prints = _("tagging %(1)s objects of type %(2)s") % {
                '1': len(objects),
                '2': classes.pop().__name__}
        elif len(classes) == 0:
            fine_prints = _("tagging nothing")
        else:
            fine_prints = _("tagging %(1)s objects of %(2)s different types") % {
                '1': len(objects),
                '2': len(classes)}
            if len(classes) < 4:
                fine_prints += ': ' + (', '.join(
                    sorted(t.__name__ for t in classes)))
        first = '%s - <span weight="light">%s</span>' % (
            utils.xml_safe(self), fine_prints)
        second = '(%s) - <span weight="light">%s</span>' % (
            type(self).__name__,
            (self.description or '').replace('\n', ' ')[:256])
        return first, second


class TaggedObj(db.Base):
    """
    :Table name: tagged_obj
    :Columns:
      obj_id: :class:`sqlalchemy.types.Integer`
        The id of the tagged object.
      obj_class: :class:`sqlalchemy.types.Unicode`
        The class name of the tagged object.
      tag_id: :class:`sqlalchemy.types.Integer`
        A ForeignKey to :class:`Tag`.

    """
    __tablename__ = 'tagged_obj'

    # columns
    obj_id = Column(Integer, autoincrement=False)
    obj_class = Column(String(128))
    tag_id = Column(Integer, ForeignKey('tag.id'))

    def __str__(self):
        return '%s: %s' % (self.obj_class, self.obj_id)


def _get_tagged_object_pairs(tag):
    """
    :param tag: a Tag instance
    """

    kids = []
    for obj in tag._objects:
        try:
            # __import__ "from_list" parameters has to be a list of strings
            module_name, part, cls_name = str(obj.obj_class).rpartition('.')
            module = __import__(module_name, globals(), locals(),
                                module_name.split('.')[1:])
            cls = getattr(module, cls_name)
            kids.append((cls, obj.obj_id))
        except KeyError as e:
            logger.warning('KeyError -- tag.get_tagged_objects(%s): %s'
                           % (tag, e))
            continue
        except DBAPIError as e:
            logger.warning('DBAPIError -- tag.get_tagged_objects(%s): %s'
                           % (tag, e))
            continue
        except AttributeError as e:
            logger.warning('AttributeError -- tag.get_tagged_objects(%s): %s'
                           % (tag, e))
            logger.warning('Could not get the object for %s.%s(%s)'
                           % (module_name, cls_name, obj.obj_id))
            continue

    return kids


def create_named_empty_tag(name):
    """make sure the named tag exists
    """
    session = db.Session()
    try:
        tag = session.query(Tag).filter_by(tag=name).one()
    except InvalidRequestError as e:
        logger.debug("%s - %s" % (type(e), e))
        tag = Tag(tag=name)
        session.add(tag)
        session.commit()
    session.close()
    return


def untag_objects(name, objs):
    """
    Remove the tag name from objs.

    :param name: The name of the tag
    :type name: str
    :param objs: The list of objects to untag.
    :type objs: list
    """
    name = utils.utf8(name)
    if not objs:
        create_named_empty_tag(name)
        return
    session = object_session(objs[0])
    try:
        tag = session.query(Tag).filter_by(tag=name).one()
    except Exception as e:
        logger.info("Can't remove non existing tag from non-empty list of objects"
                    "%s - %s" % (type(e), e))
        return
    # same = lambda item, y: item.obj_class == _classname(y) and item.obj_id == y.id
    objs = set((_classname(y), y.id) for y in objs)
    for item in tag._objects:
        if (item.obj_class, item.obj_id) not in objs:
            continue
        o = session.query(TaggedObj).filter_by(id=item.id).one()
        session.delete(o)
    session.commit()


# create the classname stored in the tagged_obj table
_classname = lambda x: '%s.%s' % (type(x).__module__, type(x).__name__)


def tag_objects(name, objects):
    """create or retrieve a tag, use it to tag list of objects

    :param name: The tag name, if it's a str object then it will be
      converted to unicode() using the default encoding. If a tag with
      this name doesn't exist it will be created
    :type name: str
    :param obj: A list of mapped objects to tag.
    :type obj: list
    """
    name = utils.utf8(name)
    if not objects:
        create_named_empty_tag(name)
        return
    session = object_session(objects[0])
    try:
        tag = session.query(Tag).filter_by(tag=name).one()
    except InvalidRequestError as e:
        logger.debug("%s - %s" % (type(e), e))
        tag = Tag(tag=name)
        session.add(tag)
    tag.tag_objects(objects)
    session.commit()


def get_tag_ids(objs):
    """Return a 3-tuple describing which tags apply to objs.

    the result tuple is composed of lists.  First list contains the id of
    the tags that apply to all objs.  Second list contains the id of the
    tags that apply to one or more objs, but not all.  Third list contains
    the id of the tags that do not apply to any objs.

    :param objs: a list or tuple of objects

    """
    session = object_session(objs[0])
    tag_id_query = session.query(Tag.id).join('_objects')
    starting_now = True
    s_all = set()
    s_some = set()
    s_none = set(i[0] for i in tag_id_query)  # per default none apply
    for obj in objs:
        clause = and_(TaggedObj.obj_class == _classname(obj),
                      TaggedObj.obj_id == obj.id)
        applied_tag_ids = [r[0] for r in tag_id_query.filter(clause)]
        if starting_now:
            s_all = set(applied_tag_ids)
            starting_now = False
        else:
            s_all.intersection_update(applied_tag_ids)
        s_some.update(applied_tag_ids)
        s_none.difference_update(applied_tag_ids)

    s_some.difference_update(s_all)
    return (s_all, s_some, s_none)


def _on_add_tag_activated(*args, **kwargs):
    # get the selection from the search view
    view = bauble.gui.get_view()
    try:
        values = view.get_selected_values()
    except AttributeError:
        msg = _('In order to tag an item you must first search for '
                'something and select one of the results.')
        bauble.gui.show_message_box(msg)
        return
    if len(values) == 0:
        msg = _('Nothing selected')
        utils.message_dialog(msg)
        return
    tagitem = TagItemGUI(values)
    tagitem.start()
    view.update_bottom_notebook()


class GeneralTagExpander(InfoExpander):
    """
    generic information about an accession like
    number of clones, provenance type, wild provenance type, speciess
    """

    def __init__(self, widgets):
        '''
        '''
        super().__init__(_("General"), widgets)
        general_box = self.widgets.general_box
        self.widgets.general_window.remove(general_box)
        self.vbox.pack_start(general_box, True, True, 0)
        self.table_cells = []

    def update(self, row):
        on_label_clicked = lambda l, e, x: bauble.gui.send_command(x)
        self.current_obj = row
        self.widget_set_value('ib_name_label', row.tag)
        self.widget_set_value('ib_description_label', row.description)
        objects = row.objects
        classes = set(type(o) for o in objects)
        row_no = 1
        table = self.widgets.tag_ib_general_table
        for w in self.table_cells:
            table.remove(w)
        self.table_cells = []
        for c in classes:
            obj_ids = [str(o.id) for o in objects if isinstance(o, c)]
            lab = Gtk.Label()
            lab.set_alignment(0, .5)
            lab.set_text(c.__name__)
            table.attach(lab, 0, 1, row_no, row_no + 1)

            eb = Gtk.EventBox()
            leb = Gtk.Label()
            leb.set_alignment(0, .5)
            eb.add(leb)
            table.attach(eb, 1, 2, row_no, row_no + 1)
            leb.set_text(" %s " % len(obj_ids))
            utils.make_label_clickable(
                leb, on_label_clicked,
                '%s where id in %s' % (c.__name__.lower(), ', '.join(obj_ids)))

            self.table_cells.append(lab)
            self.table_cells.append(eb)

            row_no += 1
        table.show_all()


class TagInfoBox(InfoBox):
    """
    - general info
    - source
    """
    def __init__(self):
        super().__init__()
        filename = os.path.join(paths.lib_dir(), "plugins", "tag",
                                "tag.glade")
        self.widgets = utils.BuilderWidgets(filename)
        self.general = GeneralTagExpander(self.widgets)
        self.add_expander(self.general)

    def update(self, row):
        self.general.update(row)

class TagPlugin(pluginmgr.Plugin):
    provides = {'Tag': Tag}

    @classmethod
    def init(cls):
        pluginmgr.provided.update(cls.provides)
        from bauble.view import SearchView
        from functools import partial
        mapper_search = search.get_strategy('MapperSearch')
        mapper_search.add_meta(('tag', 'tags'), Tag, ['tag'])
        SearchView.row_meta[Tag].set(
            children=partial(db.natsort, 'objects'),
            infobox=TagInfoBox,
            context_menu=tag_context_menu)
        SearchView.bottom_info[Tag] = {
            'page_widget': 'taginfo_scrolledwindow',
            'fields_used': ['tag', 'description'],
            'glade_name': os.path.join(paths.lib_dir(),
                                       'plugins/tag/tag.glade'),
            'name': _('Tags'),
            }
        if bauble.gui is not None:
            tags_menu_manager.reset()
        else:
            pass


plugin = TagPlugin
