# Source code for pypump.models.feed

    ##
    # Copyright (C) 2013 Jessica T. (Tsyesika) <xray7224@googlemail.com>
    #
    # 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 3 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, see <http://www.gnu.org/licenses/>.
    ##

    import logging

    import six

    from pypump.exceptions import PyPumpException
    from pypump.models import PumpObject, Mapper

    _log = logging.getLogger(__name__)

    class ItemList(object):
        """ This object is returned when iterating over a :class:`Feed <pypump.models.feed.Feed>`.

        :param feed: Feed object: Feed object to return items from
        :param offset: int or PumpObject: beginning of slice
        :param stop: int or PumpObject: end of slice
        :param limit: int or None: Number of objects to return
        :param since: PumpObject: Return objects newer than this
        :param before: PumpObject: Return objects older than this
        :param cached: bool: Return objects from feed._items instead of API
        :raises PyPumpException: if offset is given as well as before/since
        :raises PyPumpException: if both before since are given
        """

        _done = False

        def __init__(self, feed, offset=None, stop=None, limit=None, since=None, before=None, cached=False):
            self.cache = []
            self.feed = feed
            self.url = self.feed.url
            self.itemcount = 0
            self._offset = offset

            # set limit based on offset and stop
            if isinstance(stop, int):
                if isinstance(offset, int):
                    self._limit = stop - offset
                else:
                    self._limit = stop
            else:
                self._limit = limit

            # set since to stop if stop is a PumpObject
            if self.get_obj_id(stop):
                self._since = self.get_obj_id(stop)
            else:
                self._since = self.get_obj_id(since)

            # set before to offset if offset is a PumpObject
            if self.get_obj_id(offset):
                self._before = self.get_obj_id(offset)
                self._offset = None
            else:
                self._before = self.get_obj_id(before)

            self._cached = cached

            if self._offset and (self._since or self._before):
                raise PyPumpException("can not have both offset and since/before parameters")
            elif self._since and self._before:
                raise PyPumpException("can not have both since and before parameters")

        def get_obj_id(self, item):
            """ Get the id of a PumpObject.

            :param item: id string or PumpObject
            """
            if item is not None:
                if isinstance(item, six.string_types):
                    return item
                elif hasattr(item, 'id'):
                    return item.id

        def get_page(self, url):
            """ Get a page of items from API """
            if url:
                data = self.feed._request(url, offset=self._offset, since=self._since, before=self._before)

                # set values to False to avoid using them for next request
                self._before = False if self._before is not None else None
                self._since = False if self._since is not None else None
                if getattr(self.feed, 'issue65', False):
                    self._offset = False
                if self._since is not None:
                    # we want oldest items first when using 'since'
                    return reversed(data['items'])
                else:
                    return data['items']
            else:
                return []

        def get_cached(self):
            """ Get items from feed cache while trying to emulate
            how API handles offset/since/before parameters
            """
            def id_in_list(list, id):
                if id:
                    if [i for i in list if i.id == id]:
                        return True
                    else:
                        raise PyPumpException("id %r not in feed." % self._since)

            tmp = []
            if self._before is not None:
                # return list based on before param
                if not id_in_list(self.feed._items, self._before):
                    return tmp
                if isinstance(self._before, six.string_types):
                    found = False
                    for i in self.feed._items:
                        if not found:
                            if i.id == self._before:
                                found = True
                            continue
                        else:
                            tmp.append(i)
                    self._before = False
                return tmp

            if self._since is not None:
                # return list based on since param
                if not id_in_list(self.feed._items, self._since):
                    return tmp
                if isinstance(self._since, six.string_types):
                    found = False
                    for i in self.feed._items:
                        if i.id == self._since:
                            found = True
                            break
                        else:
                            tmp.append(i)
                    self._since = False
                return reversed(tmp)

            if not hasattr(self, 'usedcache'):
                self.usedcache = True  # invalidate cache

                if isinstance(self._offset, int):
                    # return list based on offset
                    return self.feed._items[self._offset:]

                return self.feed._items
            else:
                return tmp

        @property
        def done(self):
            """ Check if we should stop returning objects """
            if self._done:
                return self._done

            if self._limit is None:
                self._done = False
            elif self.itemcount >= self._limit:
                self._done = True

            return self._done

        def _build_cache(self):
            """ Build a list of objects from feed's cached items or API page"""
            self.cache = []
            if self.done:
                return

            for i in (self.get_cached() if self._cached else self.get_page(self.url)):
                if not self._cached:
                    # some objects don't have objectType set (inbox activities)
                    if not i.get("objectType"):
                        i["objectType"] = self.feed.object_types[0]
                    obj = Mapper(pypump=self.feed._pump).get_object(i)

                else:
                    obj = i
                self.cache.append(obj)

            # ran out of items
            if len(self.cache) <= 0:
                self._done = True

            # check what to do next time
            if getattr(self.feed, 'issue65', False):
                # work around API bug for favorites feed, see https://github.com/xray7224/PyPump/issues/65
                if self._offset is None:
                    self._offset = 0
                self._offset += 20
            elif self._since is not None:
                if self.feed.links.get('prev'):
                    self.url = self.feed.links['prev']
                    del self.feed.links['prev']  # avoid using it again
            else:
                if self.feed.links.get('next'):
                    self.url = self.feed.links['next']
                    del self.feed.links['next']  # avoid using it again
                else:
                    self.url = None

        def __getitem__(self, key):
            """
            This method has the same limitations as the method on :class:`Feed <pypump.models.feed.Feed>`

            Additionally raises `PyPumpException` if an offset is specified as well as before/since.
            """
            if isinstance(key, slice):
                return self._getslice(key)

            if type(key) is not int:
                raise TypeError('index must be integer')

            total = self._limit or self.feed.total_items
            if key > total or key < -total:
                raise IndexError("ItemList index out of range")

            if self._since or self._before:
                # we can't combine since/before and offset, so grab all results up to the one we want
                if key < 0:
                    key = key + total
                items = ItemList(self.feed, before=self._before, since=self._since, limit=key + 1, cached=self.feed.is_cached)
                items = list(items)

                # last item fetched will be the one for us
                return items[-1]
            else:
                if self._offset:
                    # shift key by current offset
                    if key >= 0:
                        key = self._offset + key
                    else:
                        key = key + total + self._offset
                elif key < 0:
                    key = key + total

                item = ItemList(self.feed, limit=1, offset=key, stop=key + 1, cached=self.feed.is_cached)
                try:
                    return item.next()
                except StopIteration:
                    raise IndexError("ItemList index out of range")

        def _getslice(self, s):
            if not isinstance(s.start, (type(None), int)) or not isinstance(s.stop, (type(None), int)):
                raise TypeError('slice indices must be integers or None')

            if self._before is not None or self._since is not None:
                if s.start is not None:
                    raise PyPumpException("can not have both offset and since/before parameters")
                elif s.stop is not None and s.stop < 0:
                    raise PyPumpException("can not count backwards with since/before parameters")
                return ItemList(self.feed, before=self._before, since=self._since, limit=s.stop, cached=self.feed.is_cached)

            offset = self._offset or 0
            stop = s.stop

            if stop is None:
                stop = len(self)
            elif isinstance(stop, int) and stop < 0:
                stop = len(self) + stop
            stop =  stop + offset

            if s.start is not None:
                if s.start > 0:
                    offset = offset + s.start
                elif s.start < 0:
                    offset = len(self) + offset + s.start

            return ItemList(self.feed, offset=offset, stop=stop, cached=self.feed.is_cached)

        def __len__(self):
            return len([item for item in self])

        def __next__(self):
            """ Return next object or raise StopIteration """
            if len(self.cache) <= 0:
                self._build_cache()

            if self.done:
                raise StopIteration
            else:
                obj = self.cache.pop(0)

            self.itemcount += 1
            return obj

        def next(self):
            return self.__next__()

        def clone(self):
            return ItemList(self.feed,
                            limit=self._limit,
                            offset=self._offset,
                            before=self._before,
                            since=self._since,
                            cached=self.feed.is_cached
                            )

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

    class Feed(PumpObject):
        """ This object represents a basic pump.io **feed**, which is used for
        navigating a list of objects (inbox, followers, shares, likes and so on).
        """
        _ignore_attr = []
        _mapping = {
            "id": "url",
            "object_types": "objectTypes",
            "_items": "items",
            "total_items": "totalItems",
        }

        def __init__(self, url=None, *args, **kwargs):
            super(Feed, self).__init__(*args, **kwargs)
            self.url = url or None

        @property
        def is_cached(self):
            return self._items is not None and self.total_items is not None and len(self._items) >= self.total_items

        def items(self, offset=None, limit=20, since=None, before=None, *args, **kwargs):
            """ Get a feed's items.

            :param offset: Amount of items to skip before returning data
            :param since:  Return items added after this id (ordered old -> new)
            :param before: Return items added before this id (ordered new -> old)
            :param limit: Amount of items to return
            """
            return ItemList(self, offset=offset, limit=limit, since=since, before=before, cached=self.is_cached)

        def _request(self, url, offset=None, since=None, before=None):
            params = dict()
            for i in ["offset", "since", "before"]:
                if eval(i):
                    params[i] = eval(i)
            _log.debug("Feed._request: url: %s, params: %s", url, params)
            data = self._pump.request(url, params=params)
            self.unserialize(data)
            return data

        def unserialize(self, data):
            Mapper(pypump=self._pump).parse_map(self, data=data)
            self._add_links(data)
            self.url = data.get('pump_io', {}).get('proxyURL') or self.url
            return self

        def _subfeed(self, feedname):
            """ Used for Inbox/Outbox major/minor/direct subfeeds """
            url = self.url
            if not url.endswith("/"):
                url += "/"
            return url + feedname

        def __getitem__(self, key):
            """
            ``key`` should either be an integer or a ``slice`` object. If a ``slice``
            object is passed in with a step parameter, the stepping will be silently
            ignored. For example::

                >>> inbox = pump.me.inbox[slice(0, 10, 2)]
                >>> print(len(inbox))
                10  # step been ignored
            """
            if isinstance(key, slice):
                stop = key.stop
                if stop is None:
                    stop = len(self)
                elif isinstance(stop, int) and stop < 0:
                    stop = len(self) + stop

                return ItemList(self, offset=key.start, stop=stop, cached=self.is_cached)

            if type(key) is not int:
                raise TypeError('index must be integer')

            item = ItemList(self, limit=1, offset=key, stop=key + 1, cached=self.is_cached)
            return item[key]

        def __len__(self):
            if self.total_items is None:
                # a hacky way to populate the cache
                list(ItemList(self, limit=1, cached=False))
            return self.total_items

        def __iter__(self):
            return self.items(limit=None)

        def __repr__(self):
            return '<Feed: {url}>'.format(url=self.url)

        def __unicode__(self):
            return u'{name}'.format(name=self.display_name or '')

    class Followers(Feed):
        """ Person's followers """

    class Following(Feed):
        """ People followed by Person """

    class Favorites(Feed):
        """ Person's favorites """
        # API bug, can only get 20 items, see https://github.com/xray7224/PyPump/issues/65
        # mark feed so we can enable bug work around in ItemList._build_cache()
        issue65 = True

    class Inbox(Feed):
        """ This object represents a pump.io **inbox feed**,
        it contains all activities posted to the owner of the inbox.

        Example:
            >>> for activity in pump.me.inbox.items(limit=3):
            ...     print(activity)
            Alice posted a note
            Bob posted a comment in reply to a note
            Alice liked a comment
        """

        _direct = None
        _minor = None
        _major = None

        def __init__(self, *args, **kwargs):
            super(Inbox, self).__init__(*args, **kwargs)

        @property
        def direct(self):
            """ Direct inbox feed,
            contains activities addressed directly to the owner of the inbox.
            """
            url = self._subfeed("direct")
            if "direct" in self.url or "major" in self.url or "minor" in self.url:
                return self
            self._direct = self._direct or self.__class__(url, pypump=self._pump)
            return self._direct

        @property
        def major(self):
            """ Major inbox feed, contains major activities such as notes and images. """
            url = self._subfeed("major")
            if "major" in self.url or "minor" in self.url:
                return self
            self._major = self._major or self.__class__(url, pypump=self._pump)
            return self._major

        @property
        def minor(self):
            """ Minor inbox feed, contains minor activities such as likes, shares and follows. """
            url = self._subfeed("minor")
            if "minor" in self.url or "major" in self.url:
                return self
            self._minor = self._minor or self.__class__(url, pypump=self._pump)
            return self._minor

    class Outbox(Feed):
        """ This object represents a pump.io **outbox feed**,
        it contains all activities posted by the owner of the outbox.

        Example:
            >>> for activity in pump.me.outbox.items(limit=3):
            ...     print(activity)
            Bob posted a note
            Bob liked an image
            Bob followed Alice
        """

        _major = None
        _minor = None

        def __init__(self, *args, **kwargs):
            super(Outbox, self).__init__(*args, **kwargs)

        @property
        def major(self):
            """ Major outbox feed, contains major activities such as notes and images. """
            url = self._subfeed("major")
            if "major" in self.url or "minor" in self.url:
                return self
            self._major = self._major or self.__class__(url, pypump=self._pump)
            return self._major

        @property
        def minor(self):
            """ Minor outbox feed, contains minor activities such as likes, shares and follows. """
            url = self._subfeed("minor")
            if "major" in self.url or "minor" in self.url:
                return self
            self._minor = self._minor or self.__class__(url, pypump=self._pump)
            return self._minor

    class Lists(Feed):
        """ This object represents a pump.io **lists feed**,
        it contains the :class:`collections <pypump.models.collection.Collection>` (or lists) created by the owner.

        Example:
            >>> for i in pump.me.lists.items():
            ...     print(i)
            Coworkers
            Acquaintances
            Family
            Friends
        """

        # API bug, offset and count doesnt work right,
        # see https://github.com/e14n/pump.io/issues/794
        # TODO can not see lists for persons on remote server (need more auth than 2-leg)
        _membertype = "person"

        @property
        def membertype(self):
            return self._membertype

        def create(self, display_name, content=None):
            """ Create a new user list :class:`collection <pypump.models.collection.Collection>`.

            :param display_name: List title.
            :param content: (optional) List description.

            Example:
                >>> pump.me.lists.create(display_name='Friends', content='List of friends')
                >>> myfriends = pump.me.lists['Friends']
                >>> print(myfriends)
                Friends
            """

            activity = {
                "verb": "create",
                "object": {
                    "objectType": "collection",
                    "objectTypes": [self.membertype],
                    "displayName": display_name,
                    "content": content
                }
            }
            if self._post_activity(activity, unserialize=False):
                return self[display_name]

        def __getitem__(self, key):
            if isinstance(key, six.string_types):
                lists = list(self)
                for i in lists:
                    if i.display_name == key:
                        return i
            else:
                return super(Lists, self).__getitem__(key)