""" Find.py

Module for searching items.
"""
__copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
__license__ = """ GNU General Public License

Straw 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.

Straw 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. """


import pygtk
pygtk.require('2.0')
import gtk
import gtk.glade
import gobject
import pango
import time
import Event
import utils
import FeedList
import MessageManager
from ItemList import ItemsView, ItemsPresenter
import MVP
import error

class Column:
    TITLE = 0
    FEED_TITLE = 1
    ITEM = 2
    BOLD = 3

class FindLimit(object):
    __slots__ = ('text', 'start', 'end')

    def __init__(self, text, start = None, end = None):
        self.text = text
        self.start = start
        self.end = end

    def is_subset(self, fl):
        if self.text.find(fl.text) == -1:
            return 0
        if fl.end and (self.end or self.end > fl.end):
            return 0
        if fl.start and (self.start or self.start < fl.start):
            return 0
        return 1

    def equals(self, fl):
        return (self.text == fl.text and
                self.end == fl.end and
                self.start == fl.start)

    def time_contains(self, item):
        rval = 1
        if item.pub_date: # pub_date hasn't been initialized in older versions of straw
            if self.start and self.start > item.pub_date:
                rval = 0
            if self.end and self.end < item.pub_date:
                rval = 0
        return rval

class Finder:
    def __init__(self):
        self._stack = list()
        self._feedlist = FeedList.get_instance()

    def find_matching(self, limit, items):
        if not len(items):
            for feed in self._feedlist:
                match = False
                for item in feed.items:
                    if limit.time_contains(item) and item.match(limit.text):
                        match = True
                        yield item
                if not match:
                    feed.unload_contents()
                while gtk.events_pending(): gtk.main_iteration(False)
            else:
                return
        else:
            error.log("ITEMS IS NOT NONE.!",items)
            # we expect this to be quicker than the above since the items are
            # already loaded in the memory.
            for item in items:
                if limit.time_contains(item) and item.match(limit.text):
                    yield item
        return


    def find(self, limit):
        if len(limit.text) < 1:
            del self._stack[:]
            return []
        items = []
        if len(self._stack):
            if limit.is_subset(self._stack[-1][0]):
                # the new search string is longer than the previous
                if len(self._stack[-1][1]):
                    # return existing items
                    items = self._stack[-1][1]
            else:
                # text was deleted
                length = len(self._stack)
                foundprev = 0
                while length > 0:
                    length -= 1
                    if limit.equals(self._stack[length][0]):
                        foundprev = 1
                        break
                    if limit.is_subset(self._stack[length][0]):
                        break
                self._stack = self._stack[:length+1]
                if foundprev or len(self._stack):
                    items = self._stack[-1][1]
        return (limit, items)

    def append_matches(self, limit, matches):
        lm = (limit,matches)
        self._stack.append(lm)
        return

class FindResultListView(ItemsView):
    """
    Widget: gtk.TreeView
    Model: Result Liststore
    Presenter: ItemListPresenter
    """
    def _initialize(self):
        ItemsView._initialize(self)
        self._widget.set_rules_hint(True)
        self._create_columns()
        selection =  self._widget.get_selection()
        selection.connect("changed", self._item_selection_changed)
        self._create_popup()

    def _model_set(self):
        self._widget.set_model(self._model)

    def _create_columns(self):
        renderer = gtk.CellRendererText()
        column = gtk.TreeViewColumn('Title', renderer, text=Column.TITLE,
                                    weight=Column.BOLD)
        self._widget.append_column(column)
        renderer = gtk.CellRendererText()
        column = gtk.TreeViewColumn('Feed', renderer, text=Column.FEED_TITLE,
                                    weight=Column.BOLD)
        self._widget.append_column(column)
        return

    def clear(self):
        self._widget.get_model().clear()

class FindResultPresenter(ItemsPresenter):
    def _initialize(self):
        self._connect()
        model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING,
                              gobject.TYPE_PYOBJECT, gobject.TYPE_INT)
        self.model = model
        self._finder = Finder()
        self._interrupted = False
        self._matches=list()

    def _mark_item(self, item, index=None):
        if not index:
            index = self._matches.index(item)
        self._set_column_weight(item, index)

    def find(self, limit):
        self._clear_matches()

        limit_items = self._finder.find(limit)
        if not len(limit_items):
            return self._matches
        limit, items = limit_items
        for item in self._finder.find_matching(limit, items):
            if self._interrupted:
                self._interrupted = True
                break
            treeiter = self._model.append()
            weight = pango.WEIGHT_NORMAL
            if not item.seen:
                weight = pango.WEIGHT_BOLD
            self._matches.append(item)
            self._model.set(treeiter, Column.TITLE, item.title,
                            Column.FEED_TITLE, item.feed.title,
                            Column.ITEM, item,
                            Column.BOLD, weight)

        error.log("MATCHES: %d %s %s" %(len(self._matches),limit.text,items))
        self._finder.append_matches(limit, self._matches)
        return self._matches

    def stop(self, signal):
        error.log("stopping...")
        self._interrupted = True
        return

    def clear(self):
        self._interrupted = False
        self._model.clear()
        self._clear_matches()
        return

    def _clear_matches(self):
        for feed in dict.fromkeys((item.feed for item in self._matches)).keys():
            feed.unload_contents()
        del self._matches[:]

class FindView(MVP.WidgetView):
    SECS_DAY = 86400

    def _initialize(self):
        self._widget_tree = gtk.glade.get_widget_tree(self._widget)
        self._text_entry = self._widget_tree.get_widget("find_text_entry")

    def _on_find_text_entry_insert_text(self, *args):
        gobject.timeout_add(700, self._after_change)
        return

    def _on_find_text_entry_delete_text(self, *args):
        gobject.timeout_add(700, self._after_change)
        return

    def _on_find_start_time_limit_check_toggled(self, button, *args):
        state = button.get_active()
        start_date_widget = self._widget_tree.get_widget('find_start_date_edit')
        start_date_widget.set_sensitive(state)
        stime = self._determine_time(state, start_date_widget.get_time())
        self._presenter.start_date(stime)
        return

    def _on_find_end_time_limit_check_toggled(self, button, *args):
        state = button.get_active()
        end_date_widget = self._widget_tree.get_widget('find_end_date_edit')
        end_date_widget.set_sensitive(state)
        etime = self._determine_time(state, end_date_widget.get_time() + self.SECS_DAY)
        self._presenter.end_date(etime)
        return

    def _on_find_start_date_edit_date_changed(self, widget):
        """
        Precondition: start_date_toggle is toggled (ON) or else this signal
        won't be triggered in the first place
        """
        stime = self._determine_time(True, widget.get_time())
        self._presenter.start_date(stime)
        return

    def _on_find_end_date_edit_date_changed(self, widget):
        """
        Precondition: end_date_toggle is toggled (ON) or else this signal
        won't be triggered in the first place
        """
        etime = self._determine_time(True, widget.get_time() + self.SECS_DAY)
        self._presenter.end_date(etime)
        return

    def _after_change(self):
        newtext = self._text_entry.get_text()
        self._presenter.text_changed(newtext)
        return

    def _determine_time(self, togglestate, dtime):
        xtime = None
        if togglestate:
            xtime = time.gmtime(dtime)
        return xtime

    def _get_text(self): return self._text
    text = property(_get_text)

    def get_widget(self):
        return self._widget

    def clear(self):
        self._text_entry.delete_text(0,-1)
        return

class FindPresenter(MVP.BasicPresenter):
    def _initialize(self):
        self.initialize_slots(Event.FindInterruptSignal)
        widget_tree = gtk.glade.get_widget_tree(self._view.get_widget())
        self._find_result_pres = FindResultPresenter(view =
                                                     FindResultListView(widget_tree.get_widget("find_results_treeview")))
        self.signal_connect(Event.FindInterruptSignal,
                            self._find_result_pres.stop)
        self._resultscount = widget_tree.get_widget("find_results_count_display")
        self._rendering = False
        self._text = ''
        self._start_date = None
        self._end_date = None

    def start_date(self, stime):
        self._start_date = stime
        if stime and stime is not self._start_date:
            limit = FindLimit(self._text,
                              stime,
                              self._end_date)
            self._find_items(limit)
        return

    def end_date(self, etime):
        self._end_date = etime
        if etime and etime is not self._end_date:
            limit = FindLimit(self._text,
                              self._start_date,
                              etime)
            self._find_items(limit)
        return

    def text_changed(self, newtext):
        # terminate an existing result rendering in progress
        if self._rendering:
            self.emit_signal(Event.FindInterruptSignal(self))
        changed = newtext is not self._text
        if changed:
            self._text = newtext
            self._rendering = False
        if changed and not self._rendering:
            self._rendering = True
            limit = FindLimit(self._text,
                              self._start_date,
                              self._end_date)
            self._find_items(limit)
            self._rendering = False
        return

    def _find_items(self, limit):
        matches = self._find_result_pres.find(limit)
        nmatches = len(matches)
        if nmatches <= 0: self._find_result_pres.clear()
        self._resultscount.set_text(_("%d items found") % nmatches or 0)
        return

    def clear(self):
        self._find_result_pres.clear()
        self._view.clear()

    def _find_result_presenter(self): return self._find_result_pres
    item_list = property(_find_result_presenter)


syntax highlighted by Code2HTML, v. 0.9.1