import datetime
import operator
import time

from six.moves import urllib
from six import string_types
from six.moves import reduce

from stackexchange.web import WebRequestManager
from stackexchange.core import *

# Site constants
from stackexchange.sites import *

def or_none(o, k):
    try:
        return getattr(o, k)
    except:
        return None

##### Statistics    ###
class Statistics(JSONModel):
    """Stores statistics for a StackExchange site."""
    transfer = ('total_questions', 'total_unanswered', 'total_answers', 'total_comments', 'total_votes', 'total_badges', 'total_users', 'questions_per_minute', 'answers_per_minutes', 'badges_per_minute', 'display_name')

    def _extend(self, json, site):
        self.api_version = DictObject(json.api_version)

##### Content Types ###
class Answer(JSONModel):
    """Describes an answer on a StackExchange site."""

    transfer = ('is_accepted', 'locked_date', 'question_id', 'up_vote_count', 'down_vote_count', 'view_count', 'score',
        'community_owned', 'title', 'body')

    def _extend(self, json, site):
        self.id = json.answer_id
        self.accepted = self.is_accepted

        if not hasattr(json, '_params_'):
            comment = False
        else:
            comment = ('comment' in json._params_ and json._params_['comment'])

        answer_comments_url = 'answers/%d/comments' % self.id
        self.comments = site.build_from_snippet(json.comments, Comment) if comment else StackExchangeLazySequence(Comment, None, site, answer_comments_url, self._up('comments'), filter = '!-*7AsUyrEan0')

        self._question, self._owner = None, None
        if hasattr(json, 'owner'):
            self.owner_id = json.owner.get('user_id')
            self.owner_info = tuple(json.owner.values())

        self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)

        if hasattr(json, 'last_edit_date'):
            self.last_edit_date = datetime.date.fromtimestamp(json.last_edit_date)
        if hasattr(json, 'last_activity_date'):
            self.last_activity_date = datetime.date.fromtimestamp(json.last_activity_date)

        self.revisions = StackExchangeLazySequence(PostRevision, None, site, 'posts/%s/revisions' % self.id, self._up('revisions'), 'revisions')

        if hasattr(self, 'up_vote_count') and hasattr(self, 'down_vote_count'):
            self.votes = (self.up_vote_count, self.down_vote_count)

        self.url = 'http://' + self.site.root_domain + '/questions/' + str(self.question_id) + '/' + str(self.id) + '#' + str(self.id)

    def _get_user(self, id):
        if self._owner is None:
            self._owner = self.site.user(id)
        return self._owner

    def _set_user(self, ob):
        self._owner = ob

    def _get_quest(self, id):
        if self._question is None:
            self._question = self.site.question(id)
        return self._question

    def _set_quest(self, ob):
        self._question = ob

    question = property(_get_quest, _set_quest)
    owner = property(_get_user, _set_user)

    def fetch_callback(self, _, site):
        return site.answer(self.id)

    def __unicode__(self):
        return u'Answer %d' % self.id

    def __str__(self):
        return str(unicode(self))

    def __repr__(self):
        return '<Answer %d @ %x>' % (self.id, id(self))

class Question(JSONModel):
    """Describes a question on a StackExchange site."""
    transfer = ('tags', 'favorite_count', 'up_vote_count', 'down_vote_count', 'view_count', 'score', 'community_owned', 'title', 'body')

    def _extend(self, json, site):
        self.id = json.question_id

        timeline_url = 'questions/%d/timeline' % self.id
        self.timeline = StackExchangeLazySequence(TimelineEvent, None, site, timeline_url, self._up('timeline'))
        self.revisions = StackExchangeLazySequence(PostRevision, None, site, 'posts/%s/revisions' % self.id, self._up('revisions'), 'revisions')

        self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)

        comments_url = 'questions/%d/comments' % self.id
        self.comments = StackExchangeLazySequence(Comment, None, site, comments_url, self._up('comments'), filter = '!-*7AsUyrEan0')

        self.answers_url = 'questions/%d/answers' % self.id

        if hasattr(json, 'answers'):
            self.answers = [Answer(x, site) for x in json.answers]
        else:
            self.answers = []

        if hasattr(json, 'owner') and 'user_id' in json.owner:
            self.owner_id = json.owner['user_id']

            owner_dict = dict(json.owner)
            owner_dict['id'] = self.owner_id
            del owner_dict['user_id']
            owner_dict['user_type'] = UserType.from_string(owner_dict['user_type'])

            self.owner = User.partial(lambda self: self.site.user(self.id), site, owner_dict)

        self.url = 'http://' + self.site.root_domain + '/questions/' + str(self.id)

    def fetch_callback(self, _, site):
        return site.question(self.id)

    def linked(self):
        return self.site.questions(linked_to=self.id)

    def related(self):
        return self.site.questions(related_to=self.id)

    def __repr__(self):
        return "<Question '%s' @ %x>" % (self.title, id(self))

class Comment(JSONModel):
    """Describes a comment to a question or answer on a StackExchange site."""

    transfer = ('post_id', 'score', 'edit_count', 'body')
    def _extend(self, json, site):
        self.id = json.comment_id

        self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)

        if hasattr(json, 'owner'):
            self.owner_id = json.owner['owner_id'] if 'owner_id' in json.owner else json.owner['user_id']
            self.owner = User.partial(lambda self: self.site.user(self.id), site, {
                'id': self.owner_id,
                'user_type': Enumeration.from_string(json.owner['user_type'], UserType),
                'display_name': json.owner['display_name'],
                'reputation': json.owner['reputation'],
                'profile_image': json.owner['profile_image']})
        else:
            self.owner = None

        if hasattr(json, 'reply_to'):
            self.reply_to_user_id = json.reply_to['user_id']
            self.reply_to = User.partial(lambda self: self.site.user(self.id), site, {
                'id': self.reply_to_user_id,
                'user_type': Enumeration.from_string(json.reply_to['user_type'], UserType),
                'display_name': json.reply_to['display_name'],
                'reputation': json.reply_to['reputation'],
                'profile_image': json.reply_to['profile_image']})

        if hasattr(json, 'post_type'):
            self.post_type = PostType.from_string(json.post_type)

    def _get_post(self):
        if self.post_type == PostType.Question:
            return self.site.question(self.post_id)
        elif self.post_type == PostType.Answer:
            return self.site.answer(self.post_id)
        else:
            return None

    post = property(_get_post)

    def __unicode__(self):
        return u'Comment ' + str(self.id)
    def __str__(self):
        return str(unicode(self))

#### Revisions #
class RevisionType(Enumeration):
    SingleUser = 'single_user'
    VoteBased  = 'vote_based'

class PostRevision(JSONModel):
    transfer = ('body', 'comment', 'is_question', 'is_rollback', 'last_body', 'last_title', 'revision_guid',
                'revision_number', 'title', 'set_community_wiki', 'post_id', 'last_tags', 'tags')

    def _extend(self, json, site):
        self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)
        self.revision_type = RevisionType.from_string(json.revision_type)

        part = json.user
        self.user = User.partial(lambda self: self.site.user(self.id), site, {
            'id': part['user_id'],
            'user_type': Enumeration.from_string(part['user_type'], UserType),
            'display_name': part['display_name'],
            'reputation': part['reputation'],
            'profile_image': part['profile_image']
        })

    def _get_post(self):
        if self.is_question:
            return self.site.question(self.post_id)
        else:
            return self.site.answer(self.post_id)
    post = property(_get_post)

    # The SE API seems quite inconsistent in this regard; the other methods give a post_type in their JSON
    def _get_post_type(self):
        return PostType.Question if self.is_question else PostType.Answer
    post_type = property(_get_post_type)

    def __repr__(self):
        return '<Revision %d of %s%d>' % (self.revision_number, 'Q' if self.is_question else 'A', self.post_id)

##### Tags #####
class TagSynonym(JSONModel):
    transfer = ('from_tag', 'to_tag', 'applied_count')

    def _extend(self, json, site):
        self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)
        if hasattr(json, 'last_applied_date'):
            self.last_applied_date = datetime.date.fromtimestamp(json.last_applied_date)

    def __repr__(self):
        return "<TagSynonym '%s'->'%s'>" % (self.from_tag, self.to_tag)

class TagWiki(JSONModel):
    transfer = ('tag_name', 'body', 'excerpt')

    def _extend(self, json, site):
        self.body_last_edit_date = datetime.date.fromtimestamp(json.body_last_edit_date)
        self.excerpt_last_edit_date = datetime.date.fromtimestamp(json.excerpt_last_edit_date)

        if hasattr(json, 'last_body_editor'):
            body_editor = dict(json.last_body_editor)
            body_editor['id'] = body_editor['user_id']
            del body_editor['user_id']
            self.last_body_editor = User.partial(lambda s: s.site.user(self.id), site, body_editor)

        if hasattr(json, 'last_excerpt_editor'):
            excerpt_editor = dict(json.last_excerpt_editor)
            excerpt_editor['id'] = excerpt_editor['user_id']
            del excerpt_editor['user_id']
            self.last_excerpt_editor = User.partial(lambda s: s.site.user(self.id), site, excerpt_editor)

class Period(Enumeration):
    AllTime, Month = 'all-time', 'month'

class TopUser(JSONModel):
    transfer = ('score', 'post_count')

    def _extend(self, json, site):
        user_dict = dict(json.user)
        user_dict['id'] = user_dict['user_id']
        del user_dict['user_id']
        self.user = User.partial(lambda self: self.site.user(self.id), site, user_dict)

    def __repr__(self):
        return "<TopUser '%s' (score %d)>" % (self.user.display_name, self.score)

class Tag(JSONModel):
    transfer = ('name', 'count', 'fulfills_required')
    # Hack so that Site.vectorise() works correctly
    id = property(lambda self: self.name)

    def _extend(self, json, site):
        self.synonyms = StackExchangeLazySequence(TagSynonym, None, site, 'tags/%s/synonyms' % self.name, self._up('synonyms'), 'tag_synonyms')
        self.wiki = StackExchangeLazyObject(TagWiki, site, 'tags/%s/wikis' % self.name, self._up('wiki'), 'tag_wikis')

    def top_askers(self, period, **kw):
        return self.site.build('tags/%s/top-askers/%s' % (self.name, period), TopUser, 'top_users', kw)

    def top_answerers(self, period, **kw):
        return self.site.build('tags/%s/top-answerers/%s' % (self.name, period), TopUser, 'top_users', kw)

##### Users ####
class BadgeType(Enumeration):
    """Describes the rank or type of a badge: one of Bronze, Silver or Gold."""
    Bronze, Silver, Gold = range(3)

class Badge(JSONModel):
    """Describes a badge awardable on a StackExchange site."""

    transfer = ('name', 'description', 'award_count', 'tag_based')
    def _extend(self, json, site):
        self.id = json.badge_id

        badges_recipients_url = 'badges/%d/recipients' % self.id
        self.recipients = StackExchangeLazySequence(User, None, site, badges_recipients_url, self._up('recipients'))

    def __str__(self):
        return self.name
    def __repr__(self):
        return '<Badge \'%s\' @ %x>' % (self.name, id(self))

class RepChange(JSONModel):
    """Describes an event which causes a change in reputation."""

    transfer = ('user_id', 'post_id', 'post_type', 'title', 'positive_rep', 'negative_rep')
    def _extend(self, json, site):
        self.on_date = datetime.date.fromtimestamp(json.on_date)
        if hasattr(json, 'positive_rep') and hasattr(json, 'negative_rep'):
            self.score = json.positive_rep - json.negative_rep

## Timeline ##
class TimelineEventType(Enumeration):
    """Denotes the type of a timeline event."""
    _map = {'askoranswered': 'AskOrAnswered'}

    Comment = 'comment'
    AskOrAnswered = 'askoranswered'
    Badge = 'badge'
    Revision = 'revision'
    Accepted = 'accepted'

class TimelineEvent(JSONModel):
    transfer = ('user_id', 'post_id', 'comment_id', 'action', 'description', 'detail', 'comment_id')
    _post_related = (TimelineEventType.AskOrAnswered, TimelineEventType.Revision, TimelineEventType.Comment)

    def _extend(self, json, site):
        self.timeline_type = TimelineEventType.from_string(json.timeline_type)

        if self.timeline_type in self._post_related:
            self.post_type = PostType.from_string(json.post_type)
            self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)

    def _get_post(self):
        if self.timeline_type in self._post_related:
            if self.post_type == PostType.Question:
                return self.site.question(self.post_id)
            else:
                return self.site.answer(self.post_id)
        else:
            return None

    def _get_comment(self):
        if self.timeline_type == TimelineEventType.Comment:
            return self.site.comment(self.comment_id)
        else:
            return None

    def _get_badge(self):
        if self.timeline_type == TimelineEventType.Badge:
            return self.site.badge(name=self.description)
        else:
            return None

    post = property(_get_post)
    comment = property(_get_comment)
    badge = property(_get_badge)

##############

class PostType(Enumeration):
    """Denotes the type of a post: a question or an answer."""
    Question, Answer = 'question', 'answer'

class UserType(Enumeration):
    """Denotes the status of a user on a site: whether it is Anonymous, Unregistered, Registered or a Moderator."""
    Anonymous = 'anonymous'
    Registered = 'registered'
    Unregistered = 'unregistered'
    Moderator = 'moderator'

class FormattedReputation(int):
    def format(rep):
        """Formats the reputation score like it is formatted on the sites. Heavily based on CMS' JavaScript implementation at
        http://stackapps.com/questions/1012/how-to-format-reputation-numbers-similar-to-stack-exchange-sites/1019#1019"""
        str_rep = str(rep)

        if rep < 1000:
            return str_rep
        elif rep < 10000:
            return '%s,%s' % (str_rep[0], str_rep[1:])
        elif rep % 1000 == 0:
            return '%dk' % (rep / 1000.0)
        else:
            return '%.1fk' % (rep / 1000.0)

class TopTag(JSONModel):
    transfer = ('tag_name', 'question_score', 'question_count', 'answer_score', 'answer_count')

    def __repr__(self):
        return "<TopTag '%s' Q:%d A:%d>" % (self.tag_name, self.question_score, self.answer_score)

class User(JSONModel):
    """Describes a user on a StackExchange site."""

    transfer = ('display_name', 'profile_image', 'age', 'website_url', 'location', 'about_me',
        'view_count', 'up_vote_count', 'down_vote_count', 'account_id', 'profile_image')
    def _extend(self, json, site):
        self.id = json.user_id
        self.creation_date = datetime.datetime.fromtimestamp(json.creation_date)
        self.last_access_date = datetime.date.fromtimestamp(json.last_access_date)
        self.reputation = FormattedReputation(json.reputation)

        # for compatibility reasons; this field name changed in v2.x
        self.association_id = json.account_id

        user_questions_url = 'users/%d/questions' % self.id
        question_count = or_none(json, 'question_count')
        self.questions = StackExchangeLazySequence(Question, question_count, site, user_questions_url, self._up('questions'))

        user_favorites_url = 'users/%d/favorites' % self.id
        self.favorites = StackExchangeLazySequence(Question, None, site, user_favorites_url, self._up('favorites'), 'questions')

        self.no_answers_questions = StackExchangeLazySequence(Question, None, site, 'users/%d/questions/no-answers' % self.id, self._up('no_answers_questions'), 'questions')
        self.unanswered_questions = StackExchangeLazySequence(Question, None, site, 'users/%d/questions/unanswered' % self.id, self._up('unanswered_questions'), 'questions')
        self.unaccepted_questions = StackExchangeLazySequence(Question, None, site, 'users/%d/questions/unaccepted' % self.id, self._up('unaccepted_questions'), 'questions')

        user_answers_url = 'users/%d/answers' % self.id
        answer_count = or_none(json, 'answer_count')
        self.answers = StackExchangeLazySequence(Answer, answer_count, site, user_answers_url, self._up('answers'))

        # Grr, American spellings. Using them for consistency with official API.
        user_tags_url = 'users/%d/tags' % self.id
        self.tags = StackExchangeLazySequence(Tag, None, site, user_tags_url, self._up('tags'))

        user_badges_url = 'users/%d/badges' % self.id
        self.badges = StackExchangeLazySequence(Badge, None, site, user_badges_url, self._up('badges'))

        user_timeline_url = 'users/%d/timeline' % self.id
        self.timeline = StackExchangeLazySequence(TimelineEvent, None, site, user_timeline_url, self._up('timeline'), 'user_timelines')

        user_reputation_url = 'users/%d/reputation' % self.id
        self.reputation_detail = StackExchangeLazySequence(RepChange, None, site, user_reputation_url, self._up('reputation_detail'))

        user_mentioned_url = 'users/%d/mentioned' % self.id
        self.mentioned = StackExchangeLazySequence(Comment, None, site, user_mentioned_url, self._up('mentioned'), 'comments')

        user_comments_url = 'users/%d/comments' % self.id
        self.comments = StackExchangeLazySequence(Comment, None, site, user_comments_url, self._up('comments'))

        self.top_answer_tags = StackExchangeLazySequence(TopTag, None, site, 'users/%d/top-answer-tags' % self.id, self._up('top_answer_tags'), 'top_tags')
        self.top_question_tags = StackExchangeLazySequence(TopTag, None, site, 'users/%d/top-question-tags' % self.id, self._up('top_question_tags'), 'top_tags')

        if hasattr(self, 'up_vote_count') and hasattr(self, 'down_vote_count'):
            self.vote_counts = (self.up_vote_count, self.down_vote_count)

        self.type = Enumeration.from_string(json.user_type, UserType) if hasattr(json, 'user_type') else None


        gold = json.badge_counts['gold'] if 'gold' in json.badge_counts else 0
        silver = json.badge_counts['silver'] if 'silver' in json.badge_counts else 0
        bronze = json.badge_counts['bronze'] if 'bronze' in json.badge_counts else 0
        self.badge_counts_t = (gold, silver, bronze)
        self.badge_counts = {
            BadgeType.Gold:   gold,
            BadgeType.Silver: silver,
            BadgeType.Bronze: bronze
        }
        self.gold_badges, self.silver_badges, self.bronze_badges = self.badge_counts_t
        self.badge_total = reduce(operator.add, self.badge_counts_t)
        self.is_moderator = self.type == UserType.Moderator

        self.url = 'http://' + self.site.root_domain + '/users/' + str(self.id)

    def has_privilege(self, privilege):
        return self.reputation >= privilege.reputation

    def _get_real_tag(self, tag):
        return tag.name if isinstance(tag, Tag) else tag

    def top_answers_in_tag(self, tag, **kw):
        return self.site.build('users/%d/tags/%s/top-answers' % (self.id, self._get_real_tag(tag)), Answer, 'answers', kw)

    def top_questions_in_tag(self, tag, **kw):
        return self.site.build('users/%d/tags/%s/top-questions' % (self.id, self._get_real_tag(tag)), Question, 'questions', kw)

    def comments_to(self, user, **kw):
        uid = user.id if isinstance(user, User) else user
        return self.site.build('users/%d/comments/%d' % (self.id, uid), Comment, 'comments' ,kw)

    def __unicode__(self):
        return 'User %d [%s]' % (self.id, self.display_name)
    def __str__(self):
        return str(unicode(self))
    def __repr__(self):
        return "<User '%s' (%d) @ %x>" % (self.display_name, self.id, id(self))

class Privilege(JSONModel):
    transfer = ('short_description', 'description', 'reputation')


class QuestionsQuery(object):
    def __init__(self, site):
        self.site = site

    def __call__(self, ids=None, user_id=None, **kw):
        self.site.check_filter(kw)

        # Compatibility hack, as user_id= was in versions below v1.1
        if ids is None and user_id is not None:
            return self.by_user(user_id, **kw)
        elif ids is None and user_id is None:
            return self.site.build('questions', Question, 'questions', kw)
        else:
            return self.site._get(Question, ids, 'questions', kw)

    def linked_to(self, qn, **kw):
        self.site.check_filter(kw)
        url = 'questions/%s/linked' % self.site.vectorise(qn, Question)
        return self.site.build(url, Question, 'questions', kw)

    def related_to(self, qn, **kw):
        self.site.check_filter(kw)
        url = 'questions/%s/related' % self.site.vectorise(qn, Question)
        return self.site.build(url, Question, 'questions', kw)

    def by_user(self, usr, **kw):
        self.site.check_filter(kw)
        kw['user_id'] = usr
        return self.site._user_prop('questions', Question, 'questions', kw)

    def unanswered(self, by=None, **kw):
        self.site.check_filter(kw)

        if by is None:
            return self.site.build('questions/unanswered', Question, 'questions', kw)
        else:
            kw['user_id'] = by
            return self.site._user_prop('questions/unanswered', Question, 'questions', kw)

    def no_answers(self, by=None, **kw):
        self.site.check_filter(kw)

        if by is None:
            return self.site.build('questions/no-answers', Question, 'questions', kw)
        else:
            kw['user_id'] = by
            return self.site._user_prop('questions/no-answers', Question, 'questions', kw)

    def unaccepted(self, by, **kw):
        self.site.check_filter(kw)
        kw['user_id'] = by
        return self.site._user_prop('questions/unaccepted', Questions, 'questions', kw)

    def favorited_by(self, by, **kw):
        self.site.check_filter(kw)
        kw['user_id'] = by
        return self.site._user_prop('favorites', Question, 'questions', kw)

class Site(object):
    """Stores information and provides methods to access data on a StackExchange site. This class is the 'root' of the API - all data is accessed
through here."""

    def __init__(self, domain, app_key=None, cache=1800, impose_throttling=False):
        self.domain = domain
        self.app_key = app_key
        self.api_version = '2.2'

        self.impose_throttling = impose_throttling
        self.throttle_stop = True
        self.cache_options = {'cache': False} if cache == 0 else {'cache': True, 'cache_age': cache}

        self.include_body = False
        self.include_comments = False

        # In API v2.x, we generally don't get given api. at the start of these things, nor are they
        # strictly domains in many cases. We continue to accept api.* names for compatibility.
        domain_components = self.domain.split('.')
        if domain_components[0] == 'api':
            self.root_domain = '.'.join(domain_components[1:])
        else:
            self.root_domain = domain

    URL_Roots = {
        User: 'users/%s',
        Badge: 'badges/%s',
        Answer: 'answers/%s',
        Comment: 'comments/%s',
        Question: 'questions/%s',
    }

    def check_filter(self, kw):
        if 'answers' not in kw:
            kw['answers'] = 'true'
        if self.include_body:
            kw['body'] = 'true'
        if self.include_comments:
            kw['comments'] = 'true'

        # for API v2.x, the comments, body and answers parameters no longer
        # exist; instead, we have to use filters. for now, take the easy way
        # out and just rewrite them in terms of the new filters.
        if 'filter' not in kw:
            filter_name = '_'

            if kw.get('body'):
                filter_name += 'b'
                del kw['body']
            if kw.get('comments'):
                filter_name += 'c'
                del kw['comments']
            if kw.get('answers'):
                filter_name += 'a'
                del kw['answers']

            if filter_name == '_ca':
                # every other compatibility filter name works in the above
                # order except this one...
                kw['filter'] = '_ac'
            elif filter_name != '_':
                kw['filter'] = filter_name

    def _kw_to_str(self, ob):
        try:
            if isinstance(ob, datetime.datetime):
                return str(time.mktime(ob.timetuple()))
            elif isinstance(ob, string_types):
                return ob
            else:
                i = iter(ob)
                return ';'.join(i)
        except TypeError:
            return str(ob).lower()

    def _request(self, to, params):
        url = 'http://api.stackexchange.com/' + self.api_version + '/' + to
        params['site'] = params.get('site', self.root_domain)

        new_params = {}
        for k, v in params.items():
            if v is None:
                pass
            elif k in ('fromdate', 'todate'):
                # bit of a HACKish workaround for a reported issue; force to an integer
                new_params[k] = str(int(v))
            else:
                new_params[k] = self._kw_to_str(v)

        if self.app_key != None:
            new_params['key'] = self.app_key

        request_properties = dict([(x, getattr(self, x)) for x in ('impose_throttling', 'throttle_stop')])
        request_properties.update(self.cache_options)
        request_mgr = WebRequestManager(**request_properties)

        json, info = request_mgr.json_request(url, new_params)

        if 'quota_remaining' in json and 'quota_max' in json:
            self.rate_limit = (json['quota_remaining'], json['quota_max'])
            self.requests_used = self.rate_limit[1] - self.rate_limit[0]
            self.requests_left = self.rate_limit[0]

        return json

    def _user_prop(self, qs, typ, coll, kw, prop='user_id'):
        if prop not in kw:
            raise LookupError('No user ID provided.')
        else:
            tid = self.vectorise(kw[prop], User)
            del kw[prop]

            return self.build('users/%s/%s' % (tid, qs), typ, coll, kw)

    def be_inclusive(self):
        """Include the body and comments of a post, where appropriate, by default."""
        self.include_body, self.include_comments = True, True

    def build(self, url, typ, collection, kw={}):
        """Builds a StackExchangeResultset object from the given URL and type."""
        if 'body' not in kw:
            kw['body'] = str(self.include_body).lower()
        if 'comments' not in kw:
            kw['comments'] = str(self.include_comments).lower()

        json = self._request(url, kw)
        return JSONMangler.json_to_resultset(self, json, typ, collection, (self, url, typ, collection, kw))

    def build_from_snippet(self, json, typ):
        return StackExchangeResultSet([typ(x, self) for x in json])

    def vectorise(self, lst, or_of_type=None):
        # Ensure we're always dealing with an iterable
        allowed_types = or_of_type
        if allowed_types is not None and not hasattr(allowed_types, '__iter__'):
            allowed_types = (allowed_types, )

        if isinstance(lst, string_types) or type(lst).__name__ == 'bytes':
            return lst
        elif hasattr(lst, '__iter__'):
            return ';'.join([self.vectorise(x, or_of_type) for x in lst])
        elif allowed_types is not None and any([isinstance(lst, typ) for typ in allowed_types]) and hasattr(lst, 'id'):
            return str(lst.id)
        else:
            return str(lst).lower()

    def _get(self, typ, ids, coll, kw):
        root = self.URL_Roots[typ] % self.vectorise(ids)
        return self.build(root, typ, coll, kw)


    def user(self, nid, **kw):
        """Retrieves an object representing the user with the ID `nid`."""
        u, = self.users((nid,), **kw)
        return u

    def users(self, ids=[], **kw):
        """Retrieves a list of the users with the IDs specified in the `ids' parameter."""
        return self._get(User, ids, 'users', kw)
    
    def users_by_name(self, name, **kw):
        kw['filter'] = name
        return self.users(**kw)

    def moderators(self, **kw):
        """Retrieves a list of the moderators on the site."""
        return self.build('users/moderators', User, 'users', kw)

    def answer(self, nid, **kw):
        """Retrieves an object describing the answer with the ID `nid`."""
        a, = self.answers((nid,), **kw)
        return a

    def answers(self, ids=None, **kw):
        """Retrieves a set of the answers with the IDs specified in the 'ids' parameter, or by the
        user_id specified."""
        self.check_filter(kw)
        if ids is None and 'user_id' in kw:
            return self._user_prop('answers', Answer, 'answers', kw)
        elif ids is None:
            return self.build('answers', Answer, 'answers', kw)
        else:
            return self._get(Answer, ids, 'answers', kw)

    def comment(self, nid, **kw):
        """Retrieves an object representing a comment with the ID `nid`."""
        c, = self.comments((nid,), **kw)
        return c

    def comments(self, ids=None, posts=None, **kw):
        """Returns all the comments on the site."""
        if ids is None:
            if posts is None:
                return self.build('comments', Comment, 'comments', kw)
            else:
                url = 'posts/%s/comments' % self.vectorise(posts, (Question, Answer))
                return self.build(url, Comment, 'comments', kw)
        else:
            return self.build('comments/%s' % self.vectorise(ids), Comment, 'comments', kw)

    def question(self, nid, **kw):
        """Retrieves an object representing a question with the ID `nid`. Note that an answer ID can not be specified -
unlike on the actual site, you will receive an error rather than a redirect to the actual question."""
        q, = self.questions((nid,), **kw)
        return q

    questions = property(lambda s: QuestionsQuery(s))

    def recent_questions(self, **kw):
        """Returns the set of the most recent questions on the site, by last activity."""
        if 'answers' not in kw:
            kw['answers'] = 'true'
        return self.build('questions', Question, 'questions', kw)

    def users_with_badge(self, bid, **kw):
        """Returns the set of all the users who have been awarded the badge with the ID 'bid'."""
        return self.build('badges/' + str(bid), User, 'users', kw)

    def all_badges(self, **kw):
        """Returns the set of all the badges which can be awarded on the site, excluding those which are awarded for specific tags."""
        return self.build('badges', Badge, 'badges', kw)

    def badges(self, ids=None, **kw):
        """Returns the users with the badges with IDs."""
        if ids == None:
            return self._user_prop('badges', Badge, 'users', kw)
        else:
            return self._get(Badge, ids, 'users', kw)

    def badge(self, nid = None, name=None, **kw):
        """Returns an object representing the badge with the ID 'nid', or with the name passed in as name=."""
        if nid is not None and name is None:
            b, = self.build('badges/%d' % nid, Badge, 'badges', kw)
            return b
        elif nid is None and name is not None:
            # We seem to need to get all badges and find it by name. Sigh.
            kw['inname'] = name
            all_badges = self.build('badges', Badge, 'badges', kw)
            for badge in all_badges:
                if badge.name == name:
                    return badge
            return None
        else:
            raise KeyError('Supply exactly one of the following: a badge ID, a badge name')

    def privileges(self, **kw):
        """Returns all the privileges a user can have on the site."""
        return self.build('privileges', Privilege, 'privileges', kw)

    def all_nontag_badges(self, **kw):
        """Returns the set of all badges which are not tag-based."""
        return self.build('badges/name', Badge, 'badges', kw)

    def all_tag_badges(self, **kw):
        """Returns the set of all the tag-based badges: those which are awarded for performance on a specific tag."""
        return self.build('badges/tags', Badge, 'badges', kw)

    def all_tags(self, **kw):
        '''Returns the set of all tags on the site.'''
        return self.build('tags', Tag, 'tags', kw)

    def stats(self, **kw):
        '''Returns statistical information on the site, such as number of questions.'''
        return self.build('stats', Statistics, 'statistics', kw)[0]

    def revision(self, post, guid, **kw):
        real_id = post.id if isinstance(post, Question) or isinstance(post, Answer) else post
        return self.build('revisions/%d/%s' % (real_id, guid), PostRevision, 'revisions', kw)[0]

    def revisions(self, post, **kw):
        return self.build('revisions/' + self.vectorise(post, (Question, Answer)), PostRevision, 'revisions', kw)

    def search(self, **kw):
        return self.build('search', Question, 'questions', kw)

    def search_advanced(self, **kw):
        kw['body'] = None
        kw['comments'] = None
        return self.build('search/advanced', Question, 'questions', kw)

    def similar(self, title, tagged=None, nottagged=None, **kw):
        if 'answers' not in kw:
            kw['answers'] = True
        if tagged is not None:
            kw['tagged'] = self.vectorise(tagged, Tag)
        if nottagged is not None:
            kw['nottagged'] = self.vectorise(nottagged, Tag)

        kw['title'] = title
        return self.build('similar', Question, 'questions', kw)

    def tags(self, **kw):
        return self.build('tags', Tag, 'tags', kw)

    def tag(self, tag, **kw):
        return self.build('tags/%s/info' % tag, Tag, 'tags', kw)[0]

    def tag_synonyms(self, **kw):
        return self.build('tags/synonyms', TagSynonym, 'tag_synonyms', kw)

    def error(self, id, **kw):
        # for some reason, the SE API couldn't possible just ignore site=
        kw['site'] = None
        return self._request('errors/%d' % id, kw)

    def __add__(self, other):
        if isinstance(other, Site):
            return CompositeSite(self, other)
        else:
            raise NotImplemented

class CompositeSite(object):
    def __init__(self, s1, s2):
        self.site_one = s1
        self.site_two = s2

    def __getattr__(self, a):
        if hasattr(self.site_one, a) and hasattr(self.site_two, a) and callable(getattr(self.site_one, a)):
            def handle(*ps, **kws):
                res1 = getattr(self.site_one, a)(*ps, **kws)
                res2 = getattr(self.site_two, a)(*ps, **kws)

                if hasattr(res1, '__iter__') and hasattr(res2, '__iter__'):
                    return res1 + res2
                else:
                    return (res1, res2)

            return handle
        else:
            raise AttributeError(a)

    def __sub__(self, other):
        if other is self.site_one:
            return self.site_two
        elif other is self.site_two:
            return self.site_one
        else:
            raise NotImplemented
