# -*- coding: utf-8 -*-
#
# Copyright 2008-2010 Brett Adams
# Copyright 2015 Mario Frasca <mario@anche.no>.
#
# This file is part of bauble.classic.
#
# bauble.classic 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.
#
# bauble.classic 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 bauble.classic. If not, see <http://www.gnu.org/licenses/>.
#
# Description: a collection of functions and abstract classes for creating
# editors for Bauble data
#

import datetime
import os
import sys
import weakref

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

import glib
import gtk
import gobject

from random import random
import dateutil.parser as date_parser
import lxml.etree as etree
import pango
from sqlalchemy.orm import object_mapper, object_session

from bauble.i18n import _
import bauble
import bauble.db as db
from bauble.error import check
import bauble.paths as paths
import bauble.prefs as prefs
import bauble.utils as utils

# TODO: create a generic date entry that can take a mask for the date format
# see the date entries for the accession and accession source presenters


class ValidatorError(Exception):

    def __init__(self, msg):
        self.msg = msg

    def __str__(self):
        return self.msg


class Validator(object):
    """
    The interface that other validators should implement.
    """

    def to_python(self, value):
        raise NotImplementedError


class DateValidator(Validator):
    """
    Validate that string is parseable with dateutil
    """
    def to_python(self, value):
        if not value:
            return None
        dayfirst = prefs.prefs[prefs.parse_dayfirst_pref]
        yearfirst = prefs.prefs[prefs.parse_yearfirst_pref]
        default_year = 1
        default = datetime.date(1, 1, default_year)
        try:
            date = date_parser.parse(value, dayfirst=dayfirst,
                                     yearfirst=yearfirst, default=default)
            if date.year == default_year:
                raise ValueError
        except Exception, e:
            raise ValidatorError(str(e))
        return value


# class DateTimeValidator(object):
#     pass


class StringOrNoneValidator(Validator):
    """
    If the value is an empty string then return None, else return the
    str() of the value.
    """

    def to_python(self, value):
        if value in (u'', '', None):
            return None
        return str(value)


class UnicodeOrNoneValidator(Validator):
    """
    If the value is an empty unicode string then return None, else
    return the unicode() of the value. The default encoding is
    'utf-8'.
    """
    def __init__(self, encoding='utf-8'):
        self.encoding = encoding

    def to_python(self, value):
        if value in (u'', '', None):
            return None
        return utils.to_unicode(value, self.encoding)


class UnicodeOrEmptyValidator(Validator):
    """
    If the value is an empty unicode string then return '', else
    return the unicode() of the value. The default encoding is
    'utf-8'.
    """
    def __init__(self, encoding='utf-8'):
        self.encoding = encoding

    def to_python(self, value):
        if not value.strip():
            return ''
        return utils.to_unicode(value, self.encoding)


class IntOrNoneStringValidator(Validator):
    """
    If the value is an int, long or can be cast to int then return the
    number, else return None
    """

    def to_python(self, value):
        if value is None or (isinstance(value, str) and value == ''):
            return None
        elif isinstance(value, (int, long)):
            return value
        try:
            return int(value)
        except Exception:
            raise ValidatorError('Could not convert value to int: %s (%s)'
                                 % (value, type(value)))


class FloatOrNoneStringValidator(Validator):
    """
    If the value is an int, long, float or can be cast to float then
    return the number, else return None
    """

    def to_python(self, value):
        if value is None or (isinstance(value, str) and value == ''):
            return None
        elif isinstance(value, (int, long, float)):
            return value
        try:
            return float(value)
        except Exception:
            raise ValidatorError('Could not convert value to float: %s (%s)'
                                 % (value, type(value)))


def default_completion_cell_data_func(column, renderer, model, treeiter,
                                      data=None):
    '''
    the default completion cell data function for
    GenericEditorView.attach_completions
    '''
    v = model[treeiter][0]
    renderer.set_property('markup', utils.to_unicode(v))


def default_completion_match_func(completion, key_string, treeiter):
    '''
    the default completion match function for
    GenericEditorView.attach_completions, does a case-insensitive string
    comparison of the the completions model[iter][0]
    '''
    value = completion.get_model()[treeiter][0]
    return str(value).lower().startswith(key_string.lower())


class GenericEditorView(object):
    """
    A generic class meant (not) to be subclassed, to provide the view
    for the Bauble Model-View-Presenter pattern. The idea is that you
    subclass the Presenter alone, and that the View remains as 'stupid'
    as it is conceivable.

    The presenter should interact with the view by the sole interface,
    please consider all members of the view as private, this is
    particularly true for the ones having anything to do with GTK.

    :param filename: a gtk.Builder UI definition

    :param parent: a gtk.Window or subclass to use as the parent
     window, if parent=None then bauble.gui.window is used
    """
    _tooltips = {}

    def __init__(self, filename, parent=None, root_widget_name=None):
        self.root_widget_name = root_widget_name
        builder = self.builder = utils.BuilderLoader.load(filename)
        self.filename = filename
        self.widgets = utils.BuilderWidgets(builder)
        if parent:
            self.get_window().set_transient_for(parent)
        elif bauble.gui:
            self.get_window().set_transient_for(bauble.gui.window)
        self.response = None
        self.__attached_signals = []

        # set the tooltips...use gtk.Tooltip api introducted in GTK+ 2.12
        for widget_name, markup in self._tooltips.iteritems():
            try:
                self.widgets[widget_name].set_tooltip_markup(markup)
            except Exception, e:
                values = dict(widget_name=widget_name, exception=e)
                logger.debug(_('Couldn\'t set the tooltip on widget '
                               '%(widget_name)s\n\n%(exception)s' % values))

        try:
            window = self.get_window()
        except:
            window = None
        if window is not None:
            self.connect(window, 'delete-event', self.on_window_delete)
            if isinstance(window, gtk.Dialog):
                self.connect(window, 'close', self.on_dialog_close)
                self.connect(window, 'response', self.on_dialog_response)
        self.box = set()  # the top level, meant for warnings.

    def set_label(self, widget_name, value):
        getattr(self.widgets, widget_name).set_markup(value)

    def close_boxes(self):
        while self.boxes:
            logger.debug('box is being forcibly removed')
            box = self.boxes.pop()
            self.widgets.remove_parent(box)
            box.destroy()

    def add_box(self, box):
        logger.debug('box is being added')
        self.boxes.add(box)

    def remove_box(self, box):
        logger.debug('box is being removed')
        if box in self.boxes:
            self.boxes.remove(box)
            self.widgets.remove_parent(box)
            box.destroy()
        else:
            logger.debug('box to be removed is not there')

    def add_message_box(self, message_box_type=utils.MESSAGE_BOX_INFO):
        """add a message box to the message_box_parent container

        :param type: one of MESSAGE_BOX_INFO, MESSAGE_BOX_ERROR or
          MESSAGE_BOX_YESNO
        """
        return utils.add_message_box(self.widgets.message_box_parent,
                                     message_box_type)

    def connect_signals(self, target):
        'connect all signals declared in the glade file'
        if not hasattr(self, 'signals'):
            from lxml import etree
            doc = etree.parse(self.filename)
            self.signals = doc.xpath('//signal')
        for s in self.signals:
            handler = getattr(target, s.get('handler'))
            signaller = getattr(self.widgets, s.getparent().get('id'))
            handler_id = signaller.connect(s.get('name'), handler)
            self.__attached_signals.append((signaller, handler_id))

    def set_accept_buttons_sensitive(self, sensitive):
        '''
        set the sensitivity of all the accept/ok buttons
        '''
        for wname in self.accept_buttons:
            getattr(self.widgets, wname).set_sensitive(sensitive)

    def connect(self, obj, signal, callback, *args):
        """
        Attach a signal handler for signal on obj.  For more
        information see :meth:`gobject.connect_after`

        :param obj: An instance of a subclass of gobject that will
          receive the signal

        :param signal: the name of the signal the object will receive

        :param callback: the function or method to call the object
          receives the signal

        :param args: extra args to pass the the callback
        """
        if isinstance(obj, basestring):
            obj = self.widgets[obj]
        sid = obj.connect(signal, callback, *args)
        self.__attached_signals.append((obj, sid))
        return sid

    def connect_after(self, obj, signal, callback, *args):  # data=None):
        """
        Attach a signal handler for signal on obj.  For more
        information see :meth:`gobject.connect_after`

        :param obj: An instance of a subclass of gobject that will
          receive the signal

        :param signal: the name of the signal the object will receive

        :param callback: the function or method to call the object
          receives the signal

        :param args: extra args to pass the the callback
        """
        if isinstance(obj, basestring):
            obj = self.widgets[obj]
        sid = obj.connect_after(signal, callback, *args)
        # if data:
        #     sid = obj.connect_after(signal, callback, data)
        # else:
        #     sid = obj.connect_after(signal, callback)
        self.__attached_signals.append((obj, sid))
        return sid

    def disconnect_all(self):
        """
        Disconnects all the signal handlers attached with
        :meth:`GenericEditorView.connect` or
        :meth:`GenericEditorView.connect_after`
        """
        logger.debug('GenericEditorView:disconnect_all')
        for obj, sid in self.__attached_signals:
            obj.disconnect(sid)
        del self.__attached_signals[:]

    def get_window(self):
        """
        Return the top level window for view
        """
        if self.root_widget_name is not None:
            return getattr(self.widgets, self.root_widget_name)
        else:
            raise NotImplementedError

    def get_widget_value(self, widget, index=0):
        widget = (isinstance(widget, gtk.Widget)
                  and widget
                  or self.widgets[widget])
        return utils.get_widget_value(widget, index)

    def set_widget_value(self, widget, value, markup=False, default=None,
                         index=0):
        '''
        :param widget: a widget or name of a widget in self.widgets
        :param value: the value to put in the widgets
        :param markup: whether the data in value uses pango markup
        :param default: the default value to put in the widget if value is None
        :param index: the row index to use for those widgets who use a model

        This method calls bauble.utils.set_widget_value()
        '''
        if isinstance(widget, gtk.Widget):
            utils.set_widget_value(widget, value, markup, default, index)
        else:
            utils.set_widget_value(self.widgets[widget], value, markup,
                                   default, index)

    def on_dialog_response(self, dialog, response, *args):
        '''
        Called if self.get_window() is a gtk.Dialog and it receives
        the response signal.
        '''
        logger.debug('on_dialog_response')
        dialog.hide()
        self.response = response
        return response

    def on_dialog_close(self, dialog, event=None):
        """
        Called if self.get_window() is a gtk.Dialog and it receives
        the close signal.
        """
        logger.debug('on_dialog_close')
        dialog.hide()
        return False

    def on_window_delete(self, window, event=None):
        """
        Called when the window return by get_window() receives the
        delete event.
        """
        logger.debug('on_window_delete')
        window.hide()
        return False

    def attach_completion(self, entry,
                          cell_data_func=default_completion_cell_data_func,
                          match_func=default_completion_match_func,
                          minimum_key_length=2,
                          text_column=-1):
        """
        Attach an entry completion to a gtk.Entry.  The defaults
        values for this attach_completion assumes the completion popup
        only shows text and that the text is in the first column of
        the model.

        Return the completion attached to the entry.

        NOTE: If you are selecting completions from strings in your model
        you must set the text_column parameter to the column in the
        model that holds the strings or else when you select the string
        from the completions it won't get set properly in the entry
        even though you call entry.set_text().

        :param entry: the name of the entry to attach the completion

        :param cell_data_func: the function to use to display the rows in
          the completion popup

        :param match_func: a function that returns True/False if the
          value from the model should be shown in the completions

        :param minimum_key_length: default=2

        :param text_column: the value of the text-column property on the entry,
          default is -1
        """

        # TODO: we should add a default ctrl-space to show the list of
        # completions regardless of the length of the string
        completion = gtk.EntryCompletion()
        cell = gtk.CellRendererText()  # set up the completion renderer
        completion.pack_start(cell)
        completion.set_cell_data_func(cell, cell_data_func)
        completion.set_match_func(match_func)
        completion.set_property('text-column', text_column)
        completion.set_minimum_key_length(minimum_key_length)
        completion.set_popup_completion(True)
        completion.props.popup_set_width = False
        if isinstance(entry, basestring):
            self.widgets[entry].set_completion(completion)
        else:
            entry.set_completion(completion)

        # allow later access to the match func just in case
        completion._match_func = match_func

        return completion

    # TODO: add the ability to pass a sort function
    # TODO: add a default value to set in the combo
    def init_translatable_combo(self, combo, translations, default=None,
                                cmp=None):
        """
        Initialize a gtk.ComboBox with translations values where
        model[row][0] is the value that will be stored in the database
        and model[row][1] is the value that will be visible in the
        gtk.ComboBox.

        A gtk.ComboBox initialized with this method should work with
        self.assign_simple_handler()

        :param combo:
        :param translations: a list of pairs, or a dictionary,
            of values->translation.
        """
        if isinstance(combo, basestring):
            combo = self.widgets[combo]
        combo.clear()
        # using 'object' avoids SA unicode warning
        model = gtk.ListStore(object, str)
        if isinstance(translations, dict):
            translations = sorted(translations.iteritems(), key=lambda x: x[1])
        for key, value in translations:
            model.append([key, value])
        combo.set_model(model)
        cell = gtk.CellRendererText()
        combo.pack_start(cell, True)
        combo.add_attribute(cell, 'text', 1)

    def save_state(self):
        '''
        Save the state of the view by setting a value in the preferences
        that will be called restored in restore_state
        e.g. prefs[pref_string] = pref_value
        '''
        pass

    def restore_state(self):
        '''
        Restore the state of the view, this is usually done by getting a value
        by the preferences and setting the equivalent in the interface
        '''
        pass

    def start(self):
        return self.get_window().run()

    def cleanup(self):
        """
        Should be called when after self.start() returns.

        By default all it does is call self.disconnect_all()
        """
        self.disconnect_all()

    def mark_problem(self, widget):
        pass


class DontCommitException(Exception):
    """
    This is used for GenericModelViewPresenterEditor.commit_changes() to
    signal that for some reason the editor doesn't want to commit the current
    values and would like to redisplay
    """
    pass


class GenericEditorPresenter(object):
    """
    The presenter of the Model View Presenter Pattern

    :param model: an object instance mapped to an SQLAlchemy table
    :param view: should be an instance of GenericEditorView

    The presenter should usually be initialized in the following order:
    1. initialize the widgets
    2. refresh the view, put values from the model into the widgets
    3. connect the signal handlers
    """
    problem_color = gtk.gdk.color_parse('#FFDCDF')
    widget_to_field_map = {}
    view_accept_buttons = []

    PROBLEM_DUPLICATE = random()

    def __init__(self, model, view, refresh_view=False):
        self.model = model
        self.view = view
        self.problems = set()
        self._dirty = False
        self.session = object_session(model)
        #logger.debug("session, model, view = %s, %s, %s"
        #             % (self.session, model, view))
        if view:
            view.accept_buttons = self.view_accept_buttons
            if model and refresh_view:
                self.refresh_view()
            view.connect_signals(self)

    def refresh_view(self):
        for widget, attr in self.widget_to_field_map.items():
            value = getattr(self.model, attr) or ''
            self.view.set_widget_value(widget, value)

    def commit_changes(self):
        '''
        Commit the changes to self.session()
        '''
        objs = list(self.session)
        try:
            self.session.flush()
        except Exception, e:
            logger.warning(e)
            self.session.rollback()
            self.session.add_all(objs)
            raise
        return True

    def __set_model_attr(self, attr, value):
        if getattr(self.model, attr) != value:
            setattr(self.model, attr, value)
            self._dirty = True
            self.view._dirty = True
            self.view.set_accept_buttons_sensitive(not self.has_problems())

    def __get_widget_attr(self, widget):
        from types import StringTypes
        widgetname = (isinstance(widget, StringTypes)
                      and widget
                      or gtk.Buildable.get_name(widget))
        return self.widget_to_field_map.get(widgetname)

    def on_textbuffer_changed(self, widget, value=None, attr=None):
        "handle 'changed' signal on textbuffer widgets."

        if attr is None:
            attr = self.__get_widget_attr(widget)
        if attr is None:
            return
        if value is None:
            value = widget.props.text
            value = value and utils.utf8(value) or None
        logger.debug("on_text_entry_changed(%s, %s) - %s → %s"
                     % (widget, attr, getattr(self.model, attr), value))
        self.__set_model_attr(attr, value)

    def on_text_entry_changed(self, widget, value=None):
        "handle 'changed' signal on generic text entry widgets."

        attr = self.__get_widget_attr(widget)
        if attr is None:
            return
        if value is None:
            value = widget.props.text
            value = value and utils.utf8(value) or None
        logger.debug("on_text_entry_changed(%s, %s) - %s → %s"
                     % (widget, attr, getattr(self.model, attr), value))
        self.__set_model_attr(attr, value)

    def on_unique_text_entry_changed(self, widget, value=None):
        "handle 'changed' signal on text entry widgets with an uniqueness "
        "constraint."

        attr = self.__get_widget_attr(widget)
        if attr is None:
            return
        if value is None:
            value = widget.props.text
            value = value and utils.utf8(value) or None
        if getattr(self.model, attr) == value:
            return
        logger.debug("on_unique_text_entry_changed(%s, %s) - %s → %s"
                     % (widget, attr, getattr(self.model, attr), value))
        ## check uniqueness
        klass = self.model.__class__
        k_attr = getattr(klass, attr)
        q = self.session.query(klass)
        q = q.filter(k_attr == value)
        omonym = q.first()
        if omonym is not None and omonym is not self.model:
            self.add_problem(self.PROBLEM_DUPLICATE, widget)
        ## ok
        self.__set_model_attr(attr, value)

    def on_datetime_entry_changed(self, widget, value=None):
        "handle 'changed' signal on datetime entry widgets."

        attr = self.__get_widget_attr(widget)
        logger.debug("on_datetime_entry_changed(%s, %s)" % (widget, attr))
        if value is None:
            value = widget.props.text
            value = value and utils.utf8(value) or None
        self.__set_model_attr(attr, value)

    def on_check_toggled(self, widget, value=None):
        "handle toggled signal on check buttons"
        attr = self.__get_widget_attr(widget)
        if value is None:
            value = widget.get_active()
            widget.set_inconsistent(False)
        self.__set_model_attr(attr, value)

    def on_relation_entry_changed(self, widget, value=None):
        attr = self.__get_widget_attr(widget)
        logger.debug('calling unimplemented on_relation_entry_changed(%s, %s)'
                     % (widget, attr))

    def dirty(self):
        logger.info('calling deprecated "dirty". use "is_dirty".')
        return self.is_dirty()

    # whether the presenter should be commited or not
    def is_dirty(self):
        """is the presenter dirty?

        the presenter is dirty depending on whether it has changed anything
        that needs to be committed.  This doesn't necessarily imply that the
        session is not dirty nor is it required to change back to True if
        the changes are committed.
        """
        return self._dirty

    def has_problems(self, widget=None):
        """
        Return True/False depending on if widget has any problems
        attached to it. if no widget is specified, result is True if
        there is any problem at all.
        """
        if widget is None:
            return self.problems and True or False
        for p, w in self.problems:
            if widget == w:
                return True
        return False

    def clear_problems(self):
        """
        Clear all the problems from all widgets associated with the presenter
        """
        tmp = self.problems.copy()
        map(lambda p: self.remove_problem(p[0], p[1]), tmp)
        self.problems.clear()

    def remove_problem(self, problem_id, problem_widgets=None):
        """
        Remove problem_id from self.problems and reset the background
        color of the widget(s) in problem_widgets.  If problem_id is
        None and problem_widgets is None then method won't do anything.

        :param problem_id: the problem to remove, if None then remove
         any problem from the problem_widget(s)

        :param problem_widgets: a gtk.Widget instance to remove the problem
         from, if None then remove all occurrences of problem_id regardless
         of the widget
        """
        logger.debug('remove_problem(%s, %s, %s)' %
                     (self, problem_id, problem_widgets))
        if problem_id is None and problem_widgets is None:
            logger.warning('invoke remove_problem with None, None')
            # if no problem id and not problem widgets then don't do anything
            return

        tmp = self.problems.copy()
        for p, w in tmp:
            if (w == problem_widgets and p == problem_id) or \
                    (problem_widgets is None and p == problem_id) or \
                    (w == problem_widgets and problem_id is None):
                if w:
                    w.modify_bg(gtk.STATE_NORMAL, None)
                    w.modify_base(gtk.STATE_NORMAL, None)
                    w.queue_draw()
                self.problems.remove((p, w))

    def add_problem(self, problem_id, problem_widgets=None):
        """
        Add problem_id to self.problems and change the background of widget(s)
        in problem_widgets.

        :param problem_id: A unique id for the problem.

        :param problem_widgets: either a widget or list of widgets
          whose background color should change to indicate a problem
          (default=None)
        """
        ## map case list of widget to list of cases single widget.
        if isinstance(problem_widgets, (tuple, list)):
            map(lambda w: self.add_problem(problem_id, w), problem_widgets)

        ## here single widget.
        widget = problem_widgets
        self.problems.add((problem_id, widget))
        if not isinstance(widget, gtk.Widget):
            try:
                widget = getattr(self.view.widgets, widget)
            except:
                logger.info("can't get widget %s" % widget)
        from types import StringTypes
        if isinstance(widget, StringTypes):
            self.view.mark_problem(widget)
        elif widget is not None:
            widget.modify_bg(gtk.STATE_NORMAL, self.problem_color)
            widget.modify_base(gtk.STATE_NORMAL, self.problem_color)
            widget.queue_draw()

    def init_enum_combo(self, widget_name, field):
        """
        Initialize a gtk.ComboBox widget with name widget_name from
        enum values in self.model.field

        :param widget_name:

        :param field:
        """
        combo = self.view.widgets[widget_name]
        mapper = object_mapper(self.model)
        values = sorted(mapper.c[field].type.values)
        # WARNING: this is really dangerous since it might mean that a
        # value is stored in the column that is not in the Enum
        #
        #if None in values:
        #    values.remove(None)
        #    values.insert(0, '')
        utils.setup_text_combobox(combo, values)

    def set_model_attr(self, attr, value, validator=None):
        """
        It is best to use this method to set values on the model
        rather than setting them directly.  Derived classes can
        override this method to take action when the model changes.

        :param attr: the attribute on self.model to set
        :param value: the value the attribute will be set to
        :param validator: validates the value before setting it
        """
        logger.debug('editor.set_model_attr(%s, %s)' % (attr, value))
        if validator:
            try:
                value = validator.to_python(value)
                self.remove_problem('BAD_VALUE_%s' % attr)
            except ValidatorError, e:
                logger.debug("GenericEditorPresenter.set_model_attr %s" % e)
                self.add_problem('BAD_VALUE_%s' % attr)
            else:
                setattr(self.model, attr, value)
        else:
            setattr(self.model, attr, value)

    def assign_simple_handler(self, widget_name, model_attr, validator=None):
        '''
        Assign handlers to widgets to change fields in the model.

        :param widget_name:

        :param model_attr:

        :param validator:

        Note: Where widget is a gtk.ComboBox or gtk.ComboBoxEntry then
        the value is assumed to be stored in model[row][0]
        '''
        widget = self.view.widgets[widget_name]
        check(widget is not None, _('no widget with name %s') % widget_name)

        class ProblemValidator(Validator):

            def __init__(self, presenter, wrapped):
                self.presenter = presenter
                self.wrapped = wrapped

            def to_python(self, value):
                try:
                    value = self.wrapped.to_python(value)
                    self.presenter.remove_problem('BAD_VALUE_%s'
                                                  % model_attr, widget)
                except Exception, e:
                    logger.debug("GenericEditorPresenter.ProblemValidator"
                                 ".to_python %s" % e)
                    self.presenter.add_problem('BAD_VALUE_%s'
                                               % model_attr, widget)
                    raise
                return value

        if validator:
            validator = ProblemValidator(self, validator)

        if isinstance(widget, gtk.Entry):
            def on_changed(entry):
                self.set_model_attr(model_attr, entry.props.text, validator)
            self.view.connect(widget, 'changed', on_changed)
        elif isinstance(widget, gtk.TextView):
            def on_changed(textbuff):
                self.set_model_attr(model_attr, textbuff.props.text, validator)
            buff = widget.get_buffer()
            self.view.connect(buff, 'changed', on_changed)
        elif isinstance(widget, gtk.ComboBox):
            # this also handles gtk.ComboBoxEntry since it extends
            # gtk.ComboBox
            def combo_changed(combo, data=None):
                if not combo.get_active_iter():
                    # get here if there is no model on the ComboBoxEntry
                    return
                model = combo.get_model()
                value = model[combo.get_active_iter()][0]
                if model is None or combo.get_active_iter() is None:
                    return
                value = combo.get_model()[combo.get_active_iter()][0]
                if isinstance(widget, gtk.ComboBoxEntry):
                    widget.child.set_text(utils.utf8(value))
                self.set_model_attr(model_attr, value, validator)

            def entry_changed(entry, data=None):
                self.set_model_attr(model_attr, entry.props.text, validator)

            self.view.connect(widget, 'changed', combo_changed)
            if isinstance(widget, gtk.ComboBoxEntry):
                self.view.connect(widget.child, 'changed', entry_changed)
        elif isinstance(widget, (gtk.ToggleButton, gtk.CheckButton,
                                 gtk.RadioButton)):
            def toggled(button, data=None):
                active = button.get_active()
#                debug('toggled %s: %s' % (widget_name, active))
                button.set_inconsistent(False)
                self.set_model_attr(model_attr, active, validator)
            self.view.connect(widget, 'toggled', toggled)
        else:
            raise ValueError('assign_simple_handler() -- '
                             'widget type not supported: %s' % type(widget))

    def assign_completions_handler(self, widget, get_completions,
                                   on_select=lambda v: v):
        """
        Dynamically handle completions on a gtk.Entry.

        :param widget: a gtk.Entry instance or widget name

        :param get_completions: the method to call when a list of
          completions is requested, returns a list of completions

        :param on_select: callback for when a value is selected from
          the list of completions
        """
        if not isinstance(widget, gtk.Entry):
            widget = self.view.widgets[widget]
        PROBLEM = hash(gtk.Buildable.get_name(widget))

        def add_completions(text):
            if get_completions is None:
                # get_completions is None usually means that the
                # completions model already has a static list of
                # completions
                return
            # get the completions using [0:key_length] as the start of
            # the string

            def idle_callback(values):
                completion = widget.get_completion()
                utils.clear_model(completion)
                completion_model = gtk.ListStore(object)
                for v in values:
                    completion_model.append([v])
                completion.set_model(completion_model)

            key_length = widget.get_completion().props.minimum_key_length
            values = get_completions(text[:key_length])
            gobject.idle_add(idle_callback, values)

        def on_changed(entry, *args):
            text = entry.get_text()
            comp = entry.get_completion()
            comp_model = comp.get_model()
            found = []
            if comp_model:
                # search the tree model to see if the text in the
                # entry matches one of the completions, if so then
                # emit the match-selected signal, this allows us to
                # type a match in the entry without having to select
                # it from the popup
                def _cmp(row, data):
                    return utils.utf8(row[0]) == text
                found = utils.search_tree_model(comp_model, text, _cmp)
                if len(found) == 1:
                    v = comp.get_model()[found[0]][0]
                    # only auto select if the full string has been entered
                    if text.lower() == utils.utf8(v).lower():
                        comp.emit('match-selected', comp.get_model(), found[0])
                    else:
                        found = None

            if text != '' and not found and PROBLEM not in self.problems:
                self.add_problem(PROBLEM, widget)
                on_select(None)

            key_length = widget.get_completion().props.minimum_key_length
            if (not comp_model and len(text) > key_length) or \
                    len(text) == key_length:
                add_completions(text)

            # if entry is empty select nothing and remove all problem
            if text == '':
                on_select(None)
                self.remove_problem(PROBLEM, widget)

            return True

        def on_match_select(completion, compl_model, treeiter):
            value = compl_model[treeiter][0]
            # temporarily block the changed ID so that this function
            # doesn't get called twice
            widget.handler_block(_changed_sid)
            widget.props.text = utils.utf8(value)
            widget.handler_unblock(_changed_sid)
            self.remove_problem(PROBLEM, widget)
            on_select(value)
            return True  # return True or on_changed() will be called with ''

        completion = widget.get_completion()
        check(completion is not None, 'the gtk.Entry %s doesn\'t have a '
              'completion attached to it' % widget.get_name())

        _changed_sid = self.view.connect(widget, 'changed', on_changed)
        self.view.connect(completion, 'match-selected', on_match_select)

    def start(self):
        """
        run the dialog associated to the view
        """
        return self.view.get_window().run()

    def cleanup(self):
        """
        Revert any changes the presenter might have done to the
        widgets so that next time the same widgets are open everything
        will be normal.

        By default it only calls self.view.cleanup()
        """
        self.clear_problems()
        if isinstance(self.view, GenericEditorView):
            self.view.cleanup()


class ChildPresenter(GenericEditorPresenter):
    """
    This Presenter acts as a proxy to another presenter that shares
    the same view. This avoids circular references by not having a
    presenter within a presenter that both hold references to the
    view.

    This Presenter keeps a weakref to the parent presenter and
    provides a pass through to the parent presenter for calling
    methods that reference the view.
    """

    def __init__(self, model, view):
        super(ChildPresenter, self).__init__(model, view)
        #self._view_ref = weakref.ref(view)

    def _get_view(self):
        return self._view_ref()

    def _set_view(self, view):
        if isinstance(view, GenericEditorView):
            self._view_ref = weakref.ref(view)
        else:
            raise ValueError('view must be an instance of GenericEditorView')

    view = property(_get_view, _set_view)


class GenericModelViewPresenterEditor(object):
    '''
    GenericModelViewPresenterEditor assume that model is an instance
    of object mapped to a SQLAlchemy table

    The editor creates its own session and merges the model into
    it.  If the model is already in another session that original
    session will not be effected.

    When creating a subclass of this editor then you should explicitly
    close the session when you are finished with it.

    :param model: an instance of an object mapped to a SQLAlchemy
      Table, the model will be copied and merged into self.session so
      that the original model will not be changed

    :param parent: the parent windows for the view or None
    '''
    ok_responses = ()

    def __init__(self, model, parent=None):
        self.session = db.Session()
        self.model = self.session.merge(model)

    def attach_response(self, dialog, response, keyname, mask):
        '''
        Attach a response to dialog when keyname and mask are pressed
        '''
        def callback(widget, event, key, mask):
#            debug(gtk.gdk.keyval_name(event.keyval))
            if event.keyval == gtk.gdk.keyval_from_name(key) \
                    and (event.state & mask):
                widget.response(response)
        dialog.add_events(gtk.gdk.KEY_PRESS_MASK)
        dialog.connect("key-press-event", callback, keyname, mask)

    def commit_changes(self):
        '''
        Commit the changes to self.session()
        '''
        objs = list(self.session)
        try:
            self.session.flush()
        except Exception, e:
            logger.warning(e)
            self.session.rollback()
            self.session.add_all(objs)
            raise
        return True

    def __del__(self):
        if hasattr(self, 'session'):
            # in case one of the check()'s fail in __init__
            self.session.commit()
            self.session.close()


class NoteBox(gtk.HBox):
    glade_ui = 'notes.glade'

    def set_content(self, text):
        buff = gtk.TextBuffer()
        self.widgets.note_textview.set_buffer(buff)
        utils.set_widget_value(self.widgets.note_textview,
                               text or '')
        buff.connect('changed', self.on_note_buffer_changed)

    def __init__(self, presenter, model=None):
        super(NoteBox, self).__init__()

        # open the glade file and extract the markup that the
        # expander will use
        filename = os.path.join(paths.lib_dir(), self.glade_ui)
        xml = etree.parse(filename)
        el = xml.find("//object[@id='notes_box']")
        builder = gtk.Builder()
        s = '<interface>%s</interface>' % etree.tostring(el)
        if sys.platform == 'win32':
            # NOTE: PyGTK for Win32 is broken so we have to include
            # this little hack
            #
            # TODO: is this only a specific set of version of
            # PyGTK/GTK...it was only tested with PyGTK 2.12
            builder.add_from_string(s, -1)
        else:
            builder.add_from_string(s)
        self.widgets = utils.BuilderWidgets(builder)

        notes_box = self.widgets.notes_box
        self.widgets.remove_parent(notes_box)
        self.pack_start(notes_box, expand=True, fill=True)

        self.session = object_session(presenter.model)
        self.presenter = presenter
        if model:
            self.model = model
        else:
            self.model = presenter.note_cls()

        self.widgets.notes_expander.props.use_markup = True
        self.widgets.notes_expander.props.label = ''
        self.widgets.notes_expander.props.label_widget.\
            ellipsize = pango.ELLIPSIZE_END

        # set the model values on the widgets
        mapper = object_mapper(self.model)
        values = utils.get_distinct_values(mapper.c['category'],
                                           self.session)
        utils.setup_text_combobox(self.widgets.category_comboentry, values)
        utils.set_widget_value(self.widgets.category_comboentry,
                               self.model.category or '')
        utils.setup_date_button(None, self.widgets.date_entry,
                                self.widgets.date_button)
        date_str = utils.today_str()
        if self.model.date:
            format = prefs.prefs[prefs.date_format_pref]
            date_str = self.model.date.strftime(format)
        utils.set_widget_value(self.widgets.date_entry, date_str)
        utils.set_widget_value(self.widgets.user_entry,
                               self.model.user or '')
        self.set_content(self.model.note)

        # connect the signal handlers
        self.widgets.date_entry.connect(
            'changed', self.on_date_entry_changed)
        self.widgets.user_entry.connect(
            'changed', self.on_user_entry_changed)
        # connect category comboentry widget and child entry
        self.widgets.category_comboentry.connect(
            'changed', self.on_category_combo_changed)
        self.widgets.category_comboentry.child.connect(
            'changed', self.on_category_entry_changed)
        self.widgets.notes_remove_button.connect(
            'clicked', self.on_notes_remove_button)

        self.update_label()
        self.show_all()

    def set_expanded(self, expand):
        self.widgets.notes_expander.props.expanded = expand

    def on_notes_remove_button(self, button, *args):
        """
        """
        if self.model in self.presenter.notes:
            self.presenter.notes.remove(self.model)
        self.widgets.remove_parent(self.widgets.notes_box)
        self.presenter._dirty = True
        self.presenter.parent_ref().refresh_sensitivity()

    def on_date_entry_changed(self, entry, *args):
        PROBLEM = 'BAD_DATE'
        text = entry.props.text
        try:
            text = DateValidator().to_python(text)
        except Exception, e:
            logger.debug(e)
            self.presenter.add_problem(PROBLEM, entry)
        else:
            self.presenter.remove_problem(PROBLEM, entry)
            self.set_model_attr('date', text)

    def on_user_entry_changed(self, entry, *args):
        value = utils.utf8(entry.props.text)
        if not value:  # if value == ''
            value = None
        self.set_model_attr('user', value)

    def on_category_combo_changed(self, combo, *args):
        """
        Sets the text on the entry.  The model value is set in the
        entry "changed" handler.
        """
        text = ''
        treeiter = combo.get_active_iter()
        if treeiter:
            text = utils.utf8(combo.get_model()[treeiter][0])
        else:
            return
        self.widgets.category_comboentry.child.props.text = \
            utils.utf8(text)

    def on_category_entry_changed(self, entry, *args):
        """
        """
        value = utils.utf8(entry.props.text)
        if not value:  # if value == ''
            value = None
        self.set_model_attr('category', value)

    def on_note_buffer_changed(self, buff, *args):
        value = utils.utf8(buff.props.text)
        if not value:  # if value == ''
            value = None
        self.set_model_attr('note', value)

    def update_label(self):
        label = []
        date_str = None
        if self.model.date and isinstance(self.model.date, datetime.date):
            format = prefs.prefs[prefs.date_format_pref]
            date_str = utils.xml_safe(
                self.model.date.strftime(format))
        elif self.model.date:
            date_str = utils.xml_safe(self.model.date)
        else:
            date_str = self.widgets.date_entry.props.text

        if self.model.user and date_str:  # and self.model.date:
            label.append(_('%(user)s on %(date)s') %
                         dict(user=utils.xml_safe(self.model.user),
                              date=date_str))
        elif date_str:
            label.append('%s' % date_str)
        elif self.model.user:
            label.append('%s' % utils.xml_safe(self.model.user))

        if self.model.category:
            label.append('(%s)' % utils.xml_safe(self.model.category))

        if self.model.note:
            note_str = ' : %s' % utils.xml_safe(self.model.note).\
                replace('\n', '  ')
            max_length = 25
            # label.props.ellipsize doesn't work properly on a
            # label in an expander we just do it ourselves here
            if len(self.model.note) > max_length:
                label.append('%s ...' % note_str[0:max_length-1])
            else:
                label.append(note_str)

        self.widgets.notes_expander.set_label(' '.join(label))

    def set_model_attr(self, attr, value):
        setattr(self.model, attr, value)
        self.presenter._dirty = True
        if attr != 'date' and not self.model.date:
            # this is a little voodoo to set the date on the model
            # since when we create a new note box we add today's
            # date to the entry but we don't set the model so the
            # presenter doesn't appear dirty...we have to use a
            # tmp variable since the changed signal won't fire if
            # the new value is the same as the old
            entry = self.widgets.date_entry
            tmp = entry.props.text
            entry.props.text = ''
            entry.props.text = tmp
            # if the note is new and isn't yet associated with an
            # accession then set the accession when we start
            # changing values, this way we can setup a dummy
            # verification in the interface
            self.presenter.notes.append(self.model)

        self.update_label()

        self.presenter.parent_ref().refresh_sensitivity()

    @classmethod
    def is_valid_note(cls, note):
        return True


class PictureBox(NoteBox):
    glade_ui = 'pictures.glade'
    last_folder = '.'

    def __init__(self, presenter, model=None):
        super(PictureBox, self).__init__(presenter, model)
        utils.set_widget_value(self.widgets.category_comboentry,
                               u'<picture>')
        self.presenter._dirty = False

        self.widgets.picture_button.connect(
            "clicked", self.on_activate_browse_button)

    def set_content(self, basename):
        for w in list(self.widgets.picture_button.children()):
            w.destroy()
        if basename is not None:
            im = gtk.Image()
            try:
                thumbname = os.path.join(
                    prefs.prefs[prefs.picture_root_pref], 'thumbs', basename)
                filename = os.path.join(
                    prefs.prefs[prefs.picture_root_pref], basename)
                if os.path.isfile(thumbname):
                    pixbuf = gtk.gdk.pixbuf_new_from_file(thumbname)
                else:
                    fullbuf = gtk.gdk.pixbuf_new_from_file(filename)
                    scale_x = fullbuf.get_width() / 400.0
                    scale_y = fullbuf.get_height() / 400.0
                    scale = max(scale_x, scale_y, 1)
                    x = int(fullbuf.get_width() / scale)
                    y = int(fullbuf.get_height() / scale)
                    pixbuf = fullbuf.scale_simple(
                        x, y, gtk.gdk.INTERP_BILINEAR)
                im.set_from_pixbuf(pixbuf)
            except glib.GError, e:
                logger.debug("picture %s caused glib.GError %s" %
                             (basename, e))
                label = _('picture file %s not found.') % basename
                im = gtk.Label()
                im.set_text(label)
            except Exception, e:
                logger.warning(e)
                im = gtk.Label()
                im.set_text(e)
        else:
            # make button hold some text
            im = gtk.Label()
            im.set_text(_('Choose a file...'))
        im.show()
        self.widgets.picture_button.add(im)
        self.widgets.picture_button.show()

    def on_activate_browse_button(self, widget, data=None):
        fileChooserDialog = gtk.FileChooserDialog(
            _("Choose a file..."), None,
            buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
                     gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
        try:
            logger.debug('about to set current folder - %s' % self.last_folder)
            fileChooserDialog.set_current_folder(self.last_folder)
            fileChooserDialog.run()
            filename = fileChooserDialog.get_filename()
            if filename:
                import shutil
                ## copy file to picture_root_dir (if not yet there).
                if not filename.startswith(
                        prefs.prefs[prefs.picture_root_pref]):
                    shutil.copy(
                        filename, prefs.prefs[prefs.picture_root_pref])
                ## make thumbnail in same directory
                from PIL import Image
                im = Image.open(filename)
                im.thumbnail((400, 400))
                PictureBox.last_folder, basename = os.path.split(filename)
                logger.debug('new current folder is: %s' % self.last_folder)
                full_dest_path = os.path.join(
                    prefs.prefs[prefs.picture_root_pref], 'thumbs', basename)
                logger.debug('copying %s to %s' % (filename, full_dest_path))
                im.save(full_dest_path)
                ## get dirname and basename from selected file, memorize
                ## dirname
                ## make sure the category is <picture>
                self.set_model_attr('category', u'<picture>')
                ## store basename in note field and fire callbacks.
                self.set_model_attr('note', basename)
                self.set_content(basename)
        except Exception, e:
            logger.warning("unhandled exception in editor.py: %s" % e)
        fileChooserDialog.destroy()

    def on_category_entry_changed(self, entry, *args):
        pass

    @classmethod
    def is_valid_note(cls, note):
        return note.category == u'<picture>'


# TODO: create a separate class for browsing notes in a treeview
# structure

# TODO: add an "editable" property to the NotesPresenter and if it is
# True then show the add/remove buttons

class NotesPresenter(GenericEditorPresenter):
    """
    The NotesPresenter provides a generic presenter for editor notes
    on an item in the database.  This presenter requires that the
    notes property provide a specific interface.

    :param presenter: the parent presenter of this presenter
    :param notes_property: the string name of the notes property of
      the presenter.model
    :param parent_container: the gtk.Container to add the notes editor box to
    """

    ContentBox = NoteBox

    def __init__(self, presenter, notes_property, parent_container):
        super(NotesPresenter, self).__init__(presenter.model, None)

        # The glade file named in ContentBox is structured with two top
        # GtkWindow next to each other. Here, by not doing any lookup, we
        # get the first one, from which we extract the 'notes_editor_box'
        # child. This is expected to contain a 'notes_expander_box' vertical
        # box, which will host all expanders.  In the content box we
        # extract, from the same file, the widget named 'notes_box'.
        filename = os.path.join(paths.lib_dir(), self.ContentBox.glade_ui)
        xml = etree.parse(filename)
        builder = gtk.Builder()
        import sys
        if sys.platform == 'win32':
            # NOTE: PyGTK for Win32 is broken so we have to include
            # this little hack
            #
            # TODO: is this only a specific set of version of
            # PyGTK/GTK...it was only tested with PyGTK 2.12
            builder.add_from_string(etree.tostring(xml), -1)
        else:
            builder.add_from_string(etree.tostring(xml))
        self.widgets = utils.BuilderWidgets(builder)

        self.parent_ref = weakref.ref(presenter)
        self.note_cls = object_mapper(presenter.model).\
            get_property(notes_property).mapper.class_
        self.notes = getattr(presenter.model, notes_property)
        self.parent_container = parent_container
        editor_box = self.widgets.notes_editor_box  # gtk.VBox()
        self.widgets.remove_parent(editor_box)
        parent_container.add(editor_box)

        # the `expander`s are added to self.box
        self.box = self.widgets.notes_expander_box

        valid_notes_count = 0
        for note in self.notes:
            if self.ContentBox.is_valid_note(note):
                box = self.add_note(note)
                box.set_expanded(False)
                valid_notes_count += 1

        if valid_notes_count < 1:
            self.add_note()

        logger.debug('notes: %s' % self.notes)
        logger.debug('children: %s' % self.box.get_children())
        self.box.get_children()[0].set_expanded(True)  # expand first one

        self.widgets.notes_add_button.connect(
            'clicked', self.on_add_button_clicked)
        self.box.show_all()

    def on_add_button_clicked(self, *args):
        box = self.add_note()
        box.set_expanded(True)

    def add_note(self, note=None):
        """
        Add a new note to the model.
        """
        expander = self.ContentBox(self, note)
        self.box.pack_start(expander, expand=False, fill=False)  # padding=10
        self.box.reorder_child(expander, 0)
        expander.show_all()
        return expander


class PicturesPresenter(NotesPresenter):
    """pictures are associated to notes of category <picture>.

    you add a picture and you see a picture but the database will just hold
    the name of the corresponding file.

    as for other presenters, you can expand/collapse each inserted
    picture, you add or remove pictures, you see them on screen.

    this class works just the same as the NotesPresenter, with the
    note_textview replaced by a Button containing an Image.
    """

    ContentBox = PictureBox

    def __init__(self, presenter, notes_property, parent_container):
        super(PicturesPresenter, self).__init__(
            presenter, notes_property, parent_container)

        self.box.get_children()[0].set_expanded(False)  # expand none
