##
# 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)
