#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2009  Douglas S. Blank <doug.blank@gmail.com>
# Copyright (C) 2010  Jakim Friant
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

# $Id: _gramplet.py 17354 2011-04-30 10:51:21Z nick-h $

import config
if config.get('preferences.use-bsddb3'):
    import bsddb3 as bsddb
else:
    import bsddb
import types
from gen.ggettext import gettext as _

import logging
LOG = logging.getLogger(".Gramplets")

class Gramplet(object):
    """
    Base class for non-graphical gramplet code.
    """
    def __init__(self, gui, nav_group=0):
        """
        Internal constructor for non-graphical gramplets.
        """
        self._idle_id = 0
        self.track = []
        self.active = False
        self.dirty = True
        self.has_data = True
        self._pause = False
        self._generator = None
        self._need_to_update = False
        self.option_dict = {}
        self._signal = {}
        self.option_order = []
        # links to each other:
        self.gui = gui   # plugin gramplet has link to gui
        gui.pui = self   # gui has link to plugin ui
        self.nav_group = nav_group
        self.dbstate = gui.dbstate
        self.uistate = gui.uistate
        self.init()
        self.on_load()
        self.build_options()
        self.connect(self.dbstate, "database-changed", self._db_changed)
        self.connect(self.gui.textview, "button-press-event", 
                     self.gui.on_button_press) 
        self.connect(self.gui.textview, "motion-notify-event", 
                     self.gui.on_motion)
        self.connect_signal('Person', self._active_changed)
        
        active_person = self.get_active('Person')
        if active_person: # already changed
            self._db_changed(self.dbstate.db)
            self._active_changed(active_person)
        self.post_init()

    def connect_signal(self, nav_type, method):
        """
        Connect the given method to the active-changed signal for the
        navigation type requested. 
        """
        self.uistate.register(self.dbstate, nav_type, self.nav_group)
        history = self.uistate.get_history(nav_type, self.nav_group)
        self.connect(history, "active-changed", method)

    def init(self): # once, constructor
        """
        External constructor for developers to put their initialization
        code. Designed to be overridden.
        """
        pass

    def post_init(self):
        pass

    def build_options(self):
        """
        External constructor for developers to put code for building
        options.
        """
        pass

    def main(self): # return false finishes
        """
        The main place for the gramplet's code. This is a generator.
        Generator which will be run in the background, through update().
        """
        yield False

    def on_load(self):
        """
        Gramplets should override this to take care of loading previously
        their special data.
        """
        pass

    def on_save(self):
        """
        Gramplets should override this to take care of saving their
        special data.
        """
        return

    def get_active(self, nav_type):
        """
        Return the handle of the active object for the given navigation type.
        """
        return self.uistate.get_active(nav_type, self.nav_group)

    def get_active_object(self, nav_type):
        """
        Return the object of the active handle for the given navigation type.
        Assumes nav_type is one of the codes of Db.get_by_name.
        """
        handle = self.uistate.get_active(nav_type, self.nav_group)
        if nav_type in self.dbstate.db.get_table_names() and handle:
            return self.dbstate.db.get_from_name_and_handle(nav_type, handle)
        return None

    def set_active(self, nav_type, handle):
        """
        Change the handle of the active object for the given navigation type.
        """
        self.uistate.set_active(handle, nav_type, self.nav_group)

    def active_changed(self, handle):
        """
        Developers should put their code that occurs when the active
        person is changed.
        """
        pass

    def _active_changed(self, handle):
        """
        Private code that updates the GUI when active_person is changed.
        """
        self.active_changed(handle)

    def db_changed(self):
        """
        Method executed when the database is changed. 
        """
        pass

    def link(self, text, link_type, data, size=None, tooltip=None):
        """
        Creates a clickable link in the textview area.
        """
        self.gui.link(text, link_type, data, size, tooltip)

    # Shortcuts to the gui functionality:

    def set_tooltip(self, tip):
        """
        Sets the tooltip for this gramplet.
        """
        self.gui.set_tooltip(tip)

    def get_text(self):
        """
        Returns the current text of the textview.
        """
        return self.gui.get_text()
        
    def insert_text(self, text):
        """
        Insert the given text in the textview at the cursor.
        """
        self.gui.insert_text(text)

    def render_text(self, text):
        """
        Render the given text, given that set_use_markup is on.
        """
        self.gui.render_text(text)

    def clear_text(self):
        """
        Clear all of the text from the textview.
        """
        self.gui.clear_text()
        
    def set_text(self, text, scroll_to='start'):
        """
        Clear and set the text to the given text. Additionally, move the
        cursor to the position given. Positions are: 
           'start': start of textview
           'end': end of textview
           'begin': begin of line, before setting the text.
        """
        self.gui.set_text(text, scroll_to)

    def append_text(self, text, scroll_to="end"):
        """
        Append the text to the textview. Additionally, move the
        cursor to the position given. Positions are: 
           'start': start of textview
           'end': end of textview
           'begin': begin of line, before setting the text.
        """
        self.gui.append_text(text, scroll_to)

    def set_use_markup(self, value):
        """
        Allows the use of render_text to show markup.
        """
        self.gui.set_use_markup(value)

    def set_wrap(self, value):
        """
        Set the textview to wrap or not.
        """
        textview = self.gui.textview
        import gtk
        # gtk.WRAP_NONE, gtk.WRAP_CHAR, gtk.WRAP_WORD or gtk.WRAP_WORD_CHAR.
        if value in [True, 1]:
            textview.set_wrap_mode(gtk.WRAP_WORD)
        elif value in [False, 0, None]:
            textview.set_wrap_mode(gtk.WRAP_NONE)
        elif value in ["char"]:
            textview.set_wrap_mode(gtk.WRAP_CHAR)
        elif value in ["word char"]:
            textview.set_wrap_mode(gtk.WRAP_WORD_CHAR)
        else:
            raise ValueError(
                    "Unknown wrap mode: '%s': use 0,1,'char' or 'word char')"
                        % value)

    def no_wrap(self):
        """
        The view in gramplet should not wrap. DEPRICATED: use set_wrap instead.
        """
        self.set_wrap(False)

    # Other functions of the gramplet:

    def load_data_to_text(self, pos=0):
        """
        Load information from the data portion of the saved
        Gramplet to the textview.
        """
        if len(self.gui.data) >= pos + 1:
            text = self.gui.data[pos]
            text = text.replace("\\n", chr(10))
            self.set_text(text, 'end')

    def save_text_to_data(self):
        """
        Save the textview to the data portion of a saved gramplet.
        """
        text = self.get_text()
        text = text.replace(chr(10), "\\n")
        self.gui.data.append(text)

    def update(self, *args):
        """
        The main interface for running the main method.
        """
        import gobject
        if ((not self.active or 
             self.gui.gstate in ["closed", "minimized"] or 
             not self.dbstate.open) and 
            not self.gui.force_update): 
            self.dirty = True
            if self.dbstate.open:
                #print "  %s is not active" % self.gui.gname
                self.update_has_data()
            return
        #print "     %s is UPDATING" % self.gui.gname
        self.dirty = False
        LOG.debug("gramplet updater: %s: running" % self.gui.title)
        if self._idle_id != 0:
            self.interrupt()
        self._generator = self.main()
        self._pause = False
        self._idle_id = gobject.idle_add(self._updater, 
                                         priority=gobject.PRIORITY_LOW - 10)

    def _updater(self):
        """
        Runs the generator.
        """
        LOG.debug("gramplet updater: %s" % self.gui.title)
        if not isinstance(self._generator, types.GeneratorType):
            self._idle_id = 0
            LOG.debug("gramplet updater: %s : One time, done!" % self.gui.title)
            return False
        try:
            retval = self._generator.next()
            if not retval:
                self._idle_id = 0
            if self._pause:
                LOG.debug("gramplet updater: %s: return False" % self.gui.title)
                return False
            LOG.debug("gramplet updater: %s: return %s" % 
                      (self.gui.title, retval))
            return retval
        except bsddb.db.DBCursorClosedError:
            # not sure why---caused by Data Entry Gramplet
            LOG.error("bsddb.db.DBCursorClosedError in: %s" % self.gui.title)
            return False
        except StopIteration:
            self._idle_id = 0
            self._generator.close()
            LOG.debug("gramplet updater: %s: Done!"  % self.gui.title)
            return False
        except Exception, e:
            import traceback
            LOG.error("Gramplet gave an error: %s" % self.gui.title)
            traceback.print_exc()
            print "Continuing after gramplet error..."
            self._idle_id = 0
            self.uistate.push_message(self.dbstate,
                _("Gramplet %s caused an error") % self.gui.title)
            return False

    def pause(self, *args):
        """
        Pause the main method.
        """
        self._pause = True

    def resume(self, *args):
        """
        Resume the main method that has previously paused.
        """
        import gobject
        self._pause = False
        self._idle_id = gobject.idle_add(self._updater, 
                                         priority=gobject.PRIORITY_LOW - 10)

    def update_all(self, *args):
        """
        Force the main loop to run right now (as opposed to running in background).
        """
        self._generator = self.main()
        if isinstance(self._generator, types.GeneratorType):
            for step in self._generator:
                pass

    def interrupt(self, *args):
        """
        Force the generator to stop running.
        """
        import gobject
        self._pause = True
        if self._idle_id == 0:
            gobject.source_remove(self._idle_id)
            self._idle_id = 0

    def _db_changed(self, db):
        """
        Internal method for handling items that should happen when the
        database changes. This will push a message to the GUI status bar.
        """
        self.dbstate.db = db
        self.gui.dbstate.db = db
        self.db_changed()
        self.update()

    def get_option_widget(self, label):
        """
        Retrieve an option's widget by its label text.
        """
        return self.option_dict[label][0]

    def get_option(self, label):
        """
        Retireve an option by its label text.
        """
        return self.option_dict[label][1]

    def add_option(self, option):
        """
        Add an option to the GUI gramplet.
        """
        from gui.plug import make_gui_option
        widget, label = make_gui_option(
            option, self.dbstate, self.uistate, self.track)
        self.option_dict.update({option.get_label(): [widget, option]})
        self.option_order.append(option.get_label())

    def save_update_options(self, obj):
        """
        Save a gramplet's options to file.
        """
        self.save_options()
        self.update()

    def save_options(self):
        pass

    def connect(self, signal_obj, signal, method):
        id = signal_obj.connect(signal, method)
        signal_list = self._signal.get(signal, [])
        signal_list.append((id, signal_obj))
        self._signal[signal] = signal_list

    def disconnect(self, signal):
        if signal in self._signal:
            for (id, signal_obj) in self._signal[signal]:
                signal_obj.disconnect(id)
        else:
            raise AttributeError("unknown signal: '%s'" % signal)
        
    def hidden_widgets(self):
        """
        A list of widgets to keep hidden. Needed because Gramps uses
        show_all() in some places.
        """
        return []

    def set_has_data(self, value):
        """
        Set the status as to whether this gramplet has data.
        """
        if value != self.has_data:
            self.has_data = value
            self.gui.set_has_data(value)

    def update_has_data(self):
        """
        By default, assume that the gramplet has data.
        """
        self.set_has_data(True)
