"""
This module contains the class that implements functionality for grabbing
RDF/RSS/Atom feeds.

It is based on the I{Universal Feed Parser} python module available at
U{http://feedparser.org/}.
"""

import feedparser
from copy import deepcopy
import urllib2

from zope.component import getUtility

from interfaces import INyFeedHarvester
from naaya.core.utils import unescape_html_entities


class NyFeed:
    """
    Class that implements functionality for grabbing RDF/RSS/Atom feeds.
    """

    def __init__(self):
        """
        Initialize variables:

        B{__feed_gone} - flag that signals that the feed URL no longer exists

        B{__feed_feed} - contains all the feed parsed data

        B{__feed_status} - HTTP status code returned when the feed was grabbed

        B{__feed_version} - feed version

        B{__feed_bozo_exception} - stores the caught exception if any

        B{__feed_encoding} - feed character encoding

        B{__feed_etag} - HTTP ETag header value

        B{__feed_modified} - HTTP last-modified date of the feed

        B{__feed_entries} - feed items
        """
        self.__feed_gone = 0
        self.__feed_feed = None
        self.__feed_status = None
        self.__feed_version = None
        self.__feed_bozo_exception = None
        self.__feed_encoding = None
        self.__feed_etag = None
        self.__feed_modified = None
        self.__feed_entries = []
        self.__feed_lang = None

    def __set_feed(self, gone=0, feed=None, status=None, version=None,
                   bozo_exception=None, encoding=None, etag=None,
                   modified=None, entries=None):
        """
        Set feed data after the feed was parsed.
        """

        self.__feed_gone = gone
        if feed is not None:
            self.__feed_feed = deepcopy(feed)
        else:
            self.__feed_feed = None
        self.__feed_status = status
        self.__feed_version = version
        self.__feed_bozo_exception = bozo_exception
        self.__feed_encoding = encoding
        self.__feed_etag = etag
        self.__feed_modified = modified
        if entries is not None:
            for entry in entries:
                if isinstance(entry, dict) and 'title' in entry:
                    entry['title'] = unescape_html_entities(entry['title'])
            self.__feed_entries = deepcopy(entries)
        else:
            self.__feed_entries = []
        self._p_changed = 1

    # api
    def get_feed_feed(self):
        """ Getter for __feed_feed """

        return self.__feed_feed

    def get_feed_gone(self):
        """ Getter for I{__feed_gone}. """

        return self.__feed_gone

    def get_feed_status(self):
        """ Getter for I{__feed_status}. """

        return self.__feed_status

    def get_feed_version(self):
        """ Getter for I{__feed_version}. """

        return self.__feed_version

    def get_feed_bozo_exception(self):
        """ Getter for I{__feed_bozo_exception}. """

        return self.__feed_bozo_exception

    def get_feed_encoding(self):
        """ Getter for I{__feed_encoding}. """

        return self.__feed_encoding

    def get_feed_etag(self):
        """ Getter for I{__feed_etag}. """

        return self.__feed_etag

    def get_feed_modified(self):
        """ Getter for I{__feed_modified}. """

        return self.__feed_modified

    def get_feed_title(self):
        """ Getter for feed title. """

        try:
            return self.__feed_feed.title
        except:
            return ''

    def get_feed_lang(self):
        """ Getter for feed language. """

        try:
            return self.__feed_feed.language.lower()
        except AttributeError:
            return None

    def get_feed_url(self):
        """
        Returns the value of the feed url (this can be stored in whatever
        object variable).

        B{Abstract method; this should be implemented by the class who extends
        I{NyFeed}.}
        """

        raise NotImplementedError('get_feed_url')

    def set_new_feed_url(self):
        """
        After receiving a 301 status code, the feed url must be changed.

        B{Abstract method; this should be implemented by the class who extends
        I{NyFeed}.}
        """

        raise NotImplementedError('set_new_feed_url')

    def get_feed_items(self):
        """ Getter for I{__feed_entries}. """

        return self.__feed_entries

    def count_feed_items(self):
        """ Returns the number of items. """

        return len(self.__feed_entries)

    def get_feed_item_title(self, item):
        """
        Returns the title of an item.
        @param item: feed item
        """

        return item['title']

    def get_feed_item_link(self, item):
        """
        Returns the URL of an item.
        @param item: feed item
        """

        return item['link']

    def get_feed_item_keys(self, item):
        """
        Returns all item keys without I{title} and I{link}.
        @param item: feed item
        """

        l_keys = item.keys()
        if 'title' in l_keys:
            l_keys.remove('title')
        if 'link' in l_keys:
            l_keys.remove('link')
        return l_keys

    def get_feed_item_value(self, item, key):
        """
        Returns an item's key value.
        @param item: feed item
        @param key: key name
        """

        value = item.get(key, None)
        if isinstance(value, unicode):
            value = value
        return value

    def harvest_feed(self, http_proxy=None, harvester_name=None):
        """
        Handles the feed grabbing and parsing.
        """

        if harvester_name is not None:
            # Don't use the default parser; we are configured to use a custom
            # one. Let's see if we can grab it.
            try:
                harvester = getUtility(INyFeedHarvester, context=self,
                                       name=harvester_name)
            except:
                self.log_current_error()
                return

            # OK, custom parser found, run it.
            result = harvester.harvest_feed(self)
            self.__set_feed(**result)
            return

        # Default parser
        if http_proxy:
            proxy = urllib2.ProxyHandler({"http": http_proxy})
            p = feedparser.parse(self.get_feed_url(), etag=self.__feed_etag,
                                 modified=self.__feed_modified,
                                 handlers=[proxy])
        else:
            p = feedparser.parse(self.get_feed_url(), etag=self.__feed_etag,
                                 modified=self.__feed_modified)

        if p.get('bozo', 0) == 1:
            # some error occurred
            if 'status' in p:
                if p.status == 304:
                    # the feed was not modified; do nothing
                    self.__feed_status = p.status
                    self._p_changed = 1
                elif p.status == 301:
                    # the feed was permanently moved to a new location
                    # update the new location and also store feed stuff
                    self.set_new_feed_url(p.url)
                    if getattr(p, 'etag', '') != '':
                        etag = p.etag
                    else:
                        etag = None
                    if 'modified' in p:
                        modified = p.modified
                    else:
                        modified = None
                    self.__set_feed(feed=p.feed, status=p.status,
                                    version=p.version, encoding=p.encoding,
                                    etag=etag, modified=modified,
                                    entries=p.entries)
                elif p.status == 200:
                    # the feed was harvested but with a warning
                    # store feed stuff
                    if getattr(p, 'etag', '') != '':
                        etag = p.etag
                    else:
                        etag = None
                    if 'modified' in p:
                        modified = p.modified
                    else:
                        modified = None
                    self.__set_feed(feed=p.feed, status=p.status,
                                    version=p.version, encoding=p.encoding,
                                    etag=etag, modified=modified,
                                    entries=p.entries)
                else:
                    # don't know how to handle this; set it as error
                    error = str(p.bozo_exception)
                    self.__set_feed(bozo_exception=error)
            else:
                error = str(p.bozo_exception)
                self.__set_feed(bozo_exception=error)
        else:
            # no error; check for status
            if 'status' in p:
                if p.status == 301:
                    # the feed was permanently moved to a new location
                    # update the new location and also store feed stuff
                    self.set_new_feed_url(p.url)
                    if getattr(p, 'etag', '') != '':
                        etag = p.etag
                    else:
                        etag = None
                    if 'modified' in p:
                        modified = p.modified
                    else:
                        modified = None
                    self.__set_feed(feed=p.feed, status=p.status,
                                    version=p.version, encoding=p.encoding,
                                    etag=etag, modified=modified,
                                    entries=p.entries)
                elif p.status == 302:
                    # the feed was temporarily moved to a new location
                    # store feed stuff
                    if getattr(p, 'etag', '') != '':
                        etag = p.etag
                    else:
                        etag = None
                    if 'modified' in p:
                        modified = p.modified
                    else:
                        modified = None
                    self.__set_feed(feed=p.feed, status=p.status,
                                    version=p.version, encoding=p.encoding,
                                    etag=etag, modified=modified,
                                    entries=p.entries)
                elif p.status == 304:
                    # the feed was not modified; do nothing
                    self.__feed_status = p.status
                    self._p_changed = 1
                elif p.status == 200:
                    # everything is OK
                    # store feed stuff
                    if getattr(p, 'etag', '') != '':
                        etag = p.etag
                    else:
                        etag = None
                    if 'modified' in p:
                        modified = p.modified
                    else:
                        modified = None
                    self.__set_feed(feed=p.feed, status=p.status,
                                    version=p.version, encoding=p.encoding,
                                    etag=etag, modified=modified,
                                    entries=p.entries)
                elif p.status == 410:
                    # the feed is gone; DO NOT HARVEST ANYMORE!!!
                    self.set_new_feed_url('')
                    error = 'The feed is gone. Do not harvest it anymore!'
                    self.__set_feed(gone=1, bozo_exception=error)
                else:
                    # don't know how to handle this; set it as error
                    error = "Don't know how to handle this status %s" % \
                            p.status
                    self.__set_feed(bozo_exception=error, status=p.status)
            else:
                # don't know how to handle this; set it as error
                error = 'Don\'t know how to handle this, missing status'
                self.__set_feed(bozo_exception=error)
        p = None
