"""  FeedCategoryList.py

Module for feed categories and category subscriptions.
"""
__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 Feed
from StringIO import StringIO
import Config
import Event
import FeedList
import OPMLImport
import error
import locale
import utils
import types

PSEUDO_ALL_KEY = 'ALL'
PSEUDO_UNCATEGORIZED_KEY = 'UNCATEGORIZED'

class FeedCategoryList(object, Event.SignalEmitter):
    def __init__(self):
        Event.SignalEmitter.__init__(self)
        self.initialize_slots(Event.FeedCategoryChangedSignal,
                              Event.FeedCategorySortedSignal,
                              Event.FeedCategoryAddedSignal,
                              Event.FeedCategoryRemovedSignal,
                              Event.FeedCategoryListLoadedSignal)
        # have to define this here so the titles can be translated
        PSEUDO_TITLES = {PSEUDO_ALL_KEY: _('All'),
                         PSEUDO_UNCATEGORIZED_KEY: _('Uncategorized')}
        self._all_category = PseudoCategory(PSEUDO_TITLES[PSEUDO_ALL_KEY],
                                            PSEUDO_ALL_KEY)
        self._un_category = PseudoCategory(
            PSEUDO_TITLES[PSEUDO_UNCATEGORIZED_KEY], PSEUDO_UNCATEGORIZED_KEY)
        self._user_categories = []
        self._pseudo_categories = (self._all_category, self._un_category)
        self._loading = False
        self._feedlist = FeedList.get_instance()
        self._feedlist.signal_connect(Event.FeedDeletedSignal,
                                self.feed_deleted)
        self._feedlist.signal_connect(Event.FeedCreatedSignal,
                                self.feed_created)
        self._feedlist.signal_connect(Event.FeedsImportedSignal,
                                      self.feeds_imported)

    def load_data(self):
        """Loads categories from config.
        Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
        {'id', feed_id1, 'from_subscription': bool} ...], ...]
        """
        cats = Config.get_instance().categories or []
        categorized = {}
        for c in cats:
            head = c[0]
            tail = c[1:]
            pseudo_key = head.get('pseudo', None)
            if pseudo_key == PSEUDO_ALL_KEY:
                fc = self.all_category
            elif pseudo_key == PSEUDO_UNCATEGORIZED_KEY:
                fc = self.un_category
            else:
                fc = FeedCategory(head['title'])
                sub = head.get('subscription', None)
                if sub is not None:
                    fc.subscription = undump_subscription(sub)
            for f in tail:
                # backwards compatibility for stuff saved with versions <= 0.23
                if type(f) is int:
                    fid = f
                    from_sub = False
                else:
                    fid = f['id']
                    from_sub = f['from_subscription']
                feed = self._feedlist.get_feed_with_id(fid)
                if feed is not None:
                    if feed in fc.feeds:
                        error.log("%s (%d) was already in %s, skipping" % (str(feed), fid, str(fc)))
                        continue

                    fc.append_feed(feed, from_sub)
                    categorized[feed] = True
            # User categories: connect pseudos later
            if not pseudo_key:
                fc.signal_connect(Event.FeedCategoryChangedSignal,
                                  self.category_changed)
                self._user_categories.append(fc)
        # just in case we've missed any feeds, go through the list
        # and add to the pseudocategories. cache the feed list of all_category
        # so we don't get a function call (and a list comprehension loop
        # inside it) on each feed. it should be ok here, there are no
        # duplicates in feedlist. right?
        pseudos_changed = False
        all_feeds = self.all_category.feeds
        for f in self._feedlist:
            if f not in all_feeds:
                self.all_category.append_feed(f, False)
                pseudos_changed = True
            uf =  categorized.get(f, None)
            if uf is None:
                self.un_category.append_feed(f, False)
                pseudos_changed = True
        if pseudos_changed:
           self.save_data()
        for cat in self.pseudo_categories:
            cat.signal_connect(
                Event.FeedCategoryChangedSignal, self.pseudo_category_changed)
        self.emit_signal(Event.FeedCategoryListLoadedSignal(self))

    def save_data(self):
        Config.get_instance().categories = [
            cat.dump() for cat in self]

    def pseudo_category_changed(self, signal):
        self.save_data()
        self.emit_signal(signal)

    def category_changed(self, signal):
        if signal.feed is not None:
            uncategorized = True
            for cat in self.user_categories:
                if signal.feed in cat.feeds:
                    uncategorized = False
                    break
            if uncategorized:
                self.un_category.append_feed(signal.feed, False)
            else:
                try:
                    self.un_category.remove_feed(signal.feed)
                except ValueError:
                    pass
        self.save_data()
        self.emit_signal(signal)

    def feed_deleted(self, signal):
        for c in self:
            try:
                c.remove_feed(signal.feed)
            except ValueError:
                pass

    def feed_created(self, signal):
        value = signal.feed
        category = signal.category
        index = signal.index
        if category and category not in self.pseudo_categories:
            if index:
                category.insert_feed(index, value, False)
            else:
                category.append_feed(value, False)
        else:
            self.un_category.append_feed(value, False)
        self.all_category.append_feed(value, False)

    def feeds_imported(self, signal):
        category = signal.category
        feeds = signal.feeds
        from_sub = signal.from_sub

        if category and category not in self.pseudo_categories:
            category.extend_feed(feeds, from_sub)
        else:
            self.un_category.extend_feed(feeds, from_sub)
        self.all_category.extend_feed(feeds, from_sub)
        return

    @property
    def user_categories(self):
        return self._user_categories

    @property
    def pseudo_categories(self):
        return self._pseudo_categories

    @property
    def all_categories(self):
        return self.pseudo_categories + tuple(self.user_categories)

    @property
    def all_category(self):
        return self._all_category

    @property
    def un_category(self):
        return self._un_category

    class CategoryIterator:
        def __init__(self, fclist):
            self._fclist = fclist
            self._index = -1

        def __iter__(self):
            return self

        def _next(self):
            self._index += 1
            i = self._index
            uclen = len(self._fclist.user_categories)
            if i < uclen:
                return self._fclist.user_categories[i]
            elif i < uclen + len(self._fclist.pseudo_categories):
                return self._fclist.pseudo_categories[i - uclen]
            else:
                raise StopIteration

        def next(self):
            v = self._next()
            return v

    def __iter__(self):
        return self.CategoryIterator(self)

    def add_category(self, category):
        category.signal_connect(Event.FeedCategoryChangedSignal,
                                self.category_changed)
        self._user_categories.append(category)
        auxlist = [(x.title.lower(),x) for x in self._user_categories]
        auxlist.sort()
        self._user_categories = [x[1] for x in auxlist]
        self.emit_signal(Event.FeedCategoryAddedSignal(self, category))
        self.save_data()

    def remove_category(self, category):
        for feed in category.feeds:
            category.remove_feed(feed)
        category.signal_disconnect(Event.FeedCategoryChangedSignal,
                                   self.category_changed)
        self._user_categories.remove(category)
        self.emit_signal(Event.FeedCategoryRemovedSignal(self, category))
        self.save_data()

# It might be good to have a superclass FeedCategorySubscription or something
# so we could support different formats. However, I don't know of any other
# relevant format used for this purpose, so that can be done later if needed.
# Of course, they could also just implement the same interface.
class OPMLCategorySubscription(object, Event.SignalEmitter):
    REFRESH_DEFAULT = -1

    def __init__(self, location=None):
        Event.SignalEmitter.__init__(self)
        self.initialize_slots(Event.FeedCategoryChangedSignal,
                              Event.SubscriptionContentsUpdatedSignal)
        self._location = location
        self._username = None
        self._password = None
        self._previous_etag = None
        self._contents = None
        self._frequency = OPMLCategorySubscription.REFRESH_DEFAULT
        self._last_poll = 0
        self._error = None

    @apply
    def location():
        doc = ""
        def fget(self):
            return self._location
        def fset(self, location):
            self._location = location
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def username():
        doc = ""
        def fget(self):
            return self._username
        def fset(self, username):
            self._username = username
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def password():
        doc = ""
        def fget(self):
            return self._password
        def fset(self):
            self._password = password
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def previous_etag():
        doc = ""
        def fget(self):
            return self._previous_etag
        def fset(self, etag):
            self._previous_etag = etag
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def frequency():
        doc = ""
        def fget(self):
            return self._frequency
        def fset(self, freq):
            self._frequency = freq
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def last_poll():
        doc = ""
        def fget(self):
            return self._last_poll
        def fset(self, last_poll):
            self._last_poll = last_poll
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def error():
        doc = ""
        def fget(self):
            return self._error
        def fset(self, error):
            self._error = error
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    def parse(self, data):
        datastream = StringIO(data)
        entries = OPMLImport.read(datastream)
        contents = [(e.url, e.text) for e in entries]
        updated = contents == self._contents
        self._contents = contents
        if updated:
            self.emit_signal(Event.SubscriptionContentsUpdatedSignal(self))
        return

    @property
    def contents(self):
        return self._contents

    @classmethod
    def undump(klass, dictionary):
        sub = klass()
        sub.location = dictionary.get('location')
        sub.username = dictionary.get('username')
        sub.password = dictionary.get('password')
        sub.frequency = dictionary.get(
            'frequency', OPMLCategorySubscription.REFRESH_DEFAULT)
        sub.last_poll = dictionary.get('last_poll', 0)
        sub.error = dictionary.get('error')
        return sub

    def dump(self):
        return {'type': 'opml',
                'location': self.location,
                'username': self.username,
                'password': self.password,
                'frequency': self.frequency,
                'last_poll': self.last_poll,
                'error': self.error}

def undump_subscription(dictionary):
    try:
        if dictionary.get('type') == 'opml':
            return OPMLCategorySubscription.undump(dictionary)
    except Exception, e:
        error.log("exception while undumping subscription: " + str(e))
        raise

class CategoryMember(object):
    def __init__(self, feed=None, from_sub=False):
        self._feed = feed
        self._from_subscription = from_sub

    @apply
    def feed():
        doc = ""
        def fget(self):
            return self._feed
        def fset(self, feed):
            self._feed = feed
        return property(**locals())

    @apply
    def from_subscription():
        doc = ""
        def fget(self):
            return self._from_subscription
        def fset(self, p):
            self._from_subscription = p
        return property(**locals())

class FeedCategory(list, Event.SignalEmitter):
    def __init__(self, title=""):
        Event.SignalEmitter.__init__(self)
        self.initialize_slots(Event.FeedCategoryChangedSignal,
                              Event.FeedCategorySortedSignal)
        self._title = title
        self._subscription = None

    @apply
    def title():
        doc = ""
        def fget(self):
            return self._title
        def fset(self, title):
            self._title = title
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    @apply
    def subscription():
        doc = ""
        def fget(self):
            return self._subscription
        def fset(self, sub):
            if self._subscription:
                self._subscription.signal_disconnect(
                    Event.FeedCategoryChangedSignal, self._subscription_changed)
                self._subscription.signal_disconnect(
                    Event.SubscriptionContentsUpdatedSignal,
                    self._subscription_contents_updated)
            if sub:
                sub.signal_connect(Event.FeedCategoryChangedSignal,
                                   self._subscription_changed)
                sub.signal_connect(Event.SubscriptionContentsUpdatedSignal,
                                   self._subscription_contents_updated)
            self._subscription = sub
            self.emit_signal(Event.FeedCategoryChangedSignal(self))
        return property(**locals())

    def read_contents_from_subscription(self):
        if self.subscription is None:
            return
        subfeeds = self.subscription.contents
        sfdict = dict(subfeeds)
        feedlist = FeedList.get_instance()
        current = dict([(feed.location, feed) for feed in self.feeds])
        allfeeds = dict([(feed.location, feed) for feed in feedlist])
        common, toadd, toremove = utils.listdiff(sfdict.keys(), current.keys())
        existing, nonexisting, ignore = utils.listdiff(
            toadd, allfeeds.keys())

        newfeeds = [Feed.Feed.create_new_feed(sfdict[f], f) for f in nonexisting]
        feedlist.extend(self, newfeeds, from_sub=True) # will call extend_feed
        self.extend_feed([allfeeds[f] for f in existing], True)

        for f in toremove:
            index = self.index_feed(allfeeds[f])
            member = self[index]
            if member.from_subscription:
                self.remove_feed(allfeeds[f])
        return

    def _subscription_changed(self, signal):
        self.emit_signal(Event.FeedCategoryChangedSignal(self))

    def _subscription_contents_updated(self, signal):
        self.read_contents_from_subscription()

    def __str__(self):
        return "FeedCategory %s" % self.title

    def __hash__(self):
        return hash(id(self))

    def append(self, value):
        error.log("warning, probably should be using append_feed?")
        list.append(self, value)
        self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value.feed))

    def append_feed(self, value, from_sub):
        list.append(self, CategoryMember(value, from_sub))
        self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value))

    def extend_feed(self, values, from_sub):
        list.extend(self, [CategoryMember(v, from_sub) for v in values])
        self.emit_signal(Event.FeedCategoryChangedSignal(self))

    def insert(self, index, value):
        error.log("warning, probably should be using insert_feed?")
        list.insert(self, index, value)
        self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value.feed))

    def insert_feed(self, index, value, from_sub):
        list.insert(self, index, CategoryMember(value, from_sub))
        self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value))

    def remove(self, value):
        list.remove(self, value)
        self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value.feed))

    def remove_feed(self, value):
        for index, member in enumerate(self):
            if member.feed is value:
                del self[index]
                break
        else:
            raise ValueError(value)
        self.emit_signal(Event.FeedCategoryChangedSignal(self, feed=value))

    def reverse(self):
        list.reverse(self)
        self.emit_signal(Event.FeedCategorySortedSignal(self,
                                                        reverse=True))

    def index_feed(self, value):
        for index, f in enumerate(self):
            if self[index].feed is value:
                return index
        raise ValueError(value)

    def _sort_dsu(self, seq):
        aux_list = [(x.feed.title.lower(), x) for x in seq]
        aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
        return [x[1] for x in aux_list]

    def sort(self, indices=None):
        if not indices or len(indices) == 1:
            self[:] = self._sort_dsu(self)
        else:
            items = self._sort_dsu(indices)
            for i,x in enumerate(items):
                list.__setitem__(self, indices[i], items[i])
        self.emit_signal(Event.FeedCategorySortedSignal(self))

    def move_feed(self, source, target):
        if target > source:
            target -= 1
        if target == source:
            return
        t = self[source]
        del self[source]
        list.insert(self, target, t)
        self.emit_signal(Event.FeedCategoryChangedSignal(self))

    def dump(self):
        head = {'title': self.title}
        if self.subscription is not None:
            head['subscription'] = self.subscription.dump()
        return [head] + [
            {'id': f.feed.id, 'from_subscription': f.from_subscription}
            for f in self]

    @property
    def feeds(self):
        return [f.feed for f in self]

    def __eq__(self, ob):
        if isinstance(ob, types.NoneType):
            return 0
        elif isinstance(ob, FeedCategory):
            return self.title == ob.title and list.__eq__(self, ob)
        else:
            raise NotImplementedError

    def __contains__(self, item):
        error.log("warning, should probably be querying the feeds property instead?")
        return list.__contains__(self, item)

class PseudoCategory(FeedCategory):
    def __init__(self, title="", key=None):
        if key not in (PSEUDO_ALL_KEY, PSEUDO_UNCATEGORIZED_KEY):
            raise ValueError, "Invalid key"
        FeedCategory.__init__(self, title)
        self._pseudo_key = key

    def __str__(self):
        return "PseudoCategory %s" % self.title

    def dump(self):
        return [{'pseudo': self._pseudo_key, 'title': ''}] + [
            {'id': f.feed.id, 'from_subscription': False} for f in self]

    def append_feed(self, feed, from_sub):
        assert not from_sub
        FeedCategory.append_feed(self, feed, False)

    def insert_feed(self, index, feed, from_sub):
        assert not from_sub
        FeedCategory.insert_feed(self, index, feed, False)

fclist = None

def get_instance():
    global fclist
    if fclist is None:
        fclist = FeedCategoryList()
    return fclist
