from .helpers import validate_params, validate_blogname
from .request import TumblrRequest


class TumblrRestClient:
    """
    A Python Client for the Tumblr API
    """

    def __init__(self, consumer_key, consumer_secret="", oauth_token="", oauth_secret="", host="https://api.tumblr.com"):
        """
        Initializes the TumblrRestClient object, creating the TumblrRequest
        object which deals with all request formatting.

        :param consumer_key: a string, the consumer key of your
                             Tumblr Application
        :param consumer_secret: a string, the consumer secret of
                                your Tumblr Application
        :param oauth_token: a string, the user specific token, received
                            from the /access_token endpoint
        :param oauth_secret: a string, the user specific secret, received
                             from the /access_token endpoint
        :param host: the host that are you trying to send information to,
                     defaults to https://api.tumblr.com

        :returns: None
        """
        self.request = TumblrRequest(consumer_key, consumer_secret, oauth_token, oauth_secret, host)

    async def info(self):
        """
        Gets the information about the current given user

        :returns: A dict created from the JSON response
        """
        return await self.send_api_request("get", "/v2/user/info")

    @validate_blogname
    async def avatar(self, blogname, size=64):
        """
        Retrieves the url of the blog's avatar

        :param blogname: a string, the blog you want the avatar for

        :returns: A dict created from the JSON response
        """
        url = "/v2/blog/{}/avatar/{}".format(blogname, size)
        return await self.send_api_request("get", url)

    async def likes(self, **kwargs):
        """
        Gets the current given user's likes
        :param limit: an int, the number of likes you want returned
        (DEPRECATED) :param offset: an int, the like you want to start at, for pagination.
        :param before: an int, the timestamp for likes you want before.
        :param after: an int, the timestamp for likes you want after.

            # Start at the 20th like and get 20 more likes.
            client.likes({'offset': 20, 'limit': 20})

        :returns: A dict created from the JSON response
        """
        return await self.send_api_request("get", "/v2/user/likes", kwargs, ["limit", "offset", "before", "after"])

    async def following(self, **kwargs):
        """
        Gets the blogs that the current user is following.
        :param limit: an int, the number of likes you want returned
        :param offset: an int, the blog you want to start at, for pagination.

            # Start at the 20th blog and get 20 more blogs.
            client.following({'offset': 20, 'limit': 20})

        :returns: A dict created from the JSON response
        """
        return await self.send_api_request("get", "/v2/user/following", kwargs, ["limit", "offset"])

    async def dashboard(self, **kwargs):
        """
        Gets the dashboard of the current user

        :param limit: an int, the number of posts you want returned
        :param offset: an int, the posts you want to start at, for pagination.
        :param type:   the type of post you want to return
        :param since_id:  return only posts that have appeared after this ID
        :param reblog_info: return reblog information about posts
        :param notes_info:  return notes information about the posts

        :returns: A dict created from the JSON response
        """
        return await self.send_api_request("get", "/v2/user/dashboard", kwargs, ["limit", "offset", "type", "since_id", "reblog_info", "notes_info"])

    async def tagged(self, tag, **kwargs):
        """
        Gets a list of posts tagged with the given tag

        :param tag: a string, the tag you want to look for
        :param before: a unix timestamp, the timestamp you want to start at
                       to look at posts.
        :param limit: the number of results you want
        :param filter: the post format that you want returned: html, text, raw

            client.tagged("gif", limit=10)

        :returns: a dict created from the JSON response
        """
        kwargs.update({'tag': tag})
        return await self.send_api_request("get", '/v2/tagged', kwargs, ['before', 'limit', 'filter', 'tag', 'api_key'], True)

    @validate_blogname
    async def posts(self, blogname, type=None, **kwargs):
        """
        Gets a list of posts from a particular blog

        :param blogname: a string, the blogname you want to look up posts
                         for. eg: codingjester.tumblr.com
        :param id: an int, the id of the post you are looking for on the blog
        :param tag: a string, the tag you are looking for on posts
        :param limit: an int, the number of results you want
        :param offset: an int, the offset of the posts you want to start at.
        :param before: an int, the timestamp for posts you want before.
        :param filter: the post format you want returned: HTML, text or raw.
        :param type: the type of posts you want returned, e.g. video. If omitted returns all post types.

        :returns: a dict created from the JSON response
        """
        if type is None:
            url = '/v2/blog/{}/posts'.format(blogname)
        else:
            url = '/v2/blog/{}/posts/{}'.format(blogname, type)
        return await self.send_api_request("get", url, kwargs, ['id', 'tag', 'limit', 'offset', 'before', 'reblog_info', 'notes_info', 'filter', 'api_key', 'npf'], True)

    @validate_blogname
    async def blog_info(self, blogname):
        """
        Gets the information of the given blog

        :param blogname: the name of the blog you want to information
                         on. eg: codingjester.tumblr.com

        :returns: a dict created from the JSON response of information
        """
        url = "/v2/blog/{}/info".format(blogname)
        return await self.send_api_request("get", url, {}, ['api_key'], True)

    @validate_blogname
    async def blog_following(self, blogname, **kwargs):
        """
        Gets the publicly exposed list of blogs that a blog follows

        :param blogname: the name of the blog you want to get information on.
                         eg: codingjester.tumblr.com

        :param limit: an int, the number of blogs you want returned
        :param offset: an int, the blog to start at, for pagination.

            # Start at the 20th blog and get 20 more blogs.
            client.blog_following('pytblr', offset=20, limit=20})

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/following".format(blogname)
        return await self.send_api_request("get", url, kwargs, ['limit', 'offset'])

    @validate_blogname
    async def followers(self, blogname, **kwargs):
        """
        Gets the followers of the given blog
        :param limit: an int, the number of followers you want returned
        :param offset: an int, the follower to start at, for pagination.

            # Start at the 20th blog and get 20 more blogs.
            client.followers({'offset': 20, 'limit': 20})

        :returns: A dict created from the JSON response
        """
        url = "/v2/blog/{}/followers".format(blogname)
        return await self.send_api_request("get", url, kwargs, ['limit', 'offset'])

    @validate_blogname
    async def blog_likes(self, blogname, **kwargs):
        """
        Gets the current given user's likes
        :param limit: an int, the number of likes you want returned
        (DEPRECATED) :param offset: an int, the like you want to start at, for pagination.
        :param before: an int, the timestamp for likes you want before.
        :param after: an int, the timestamp for likes you want after.

            # Start at the 20th like and get 20 more likes.
            client.blog_likes({'offset': 20, 'limit': 20})

        :returns: A dict created from the JSON response
        """
        url = "/v2/blog/{}/likes".format(blogname)
        return await self.send_api_request("get", url, kwargs, ['limit', 'offset', 'before', 'after'], True)

    @validate_blogname
    async def queue(self, blogname, **kwargs):
        """
        Gets posts that are currently in the blog's queue

        :param limit: an int, the number of posts you want returned
        :param offset: an int, the post you want to start at, for pagination.
        :param filter: the post format that you want returned: HTML, text, raw.

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/posts/queue".format(blogname)
        return await self.send_api_request("get", url, kwargs, ['limit', 'offset', 'filter', 'npf'])

    @validate_blogname
    async def drafts(self, blogname, **kwargs):
        """
        Gets posts that are currently in the blog's drafts
        :param filter: the post format that you want returned: HTML, text, raw.

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/posts/draft".format(blogname)
        return await self.send_api_request("get", url, kwargs, ['filter', 'npf'])

    @validate_blogname
    async def submission(self, blogname, **kwargs):
        """
        Gets posts that are currently in the blog's submission list

        :param offset: an int, the post you want to start at, for pagination.
        :param filter: the post format that you want returned: HTML, text, raw.

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/posts/submission".format(blogname)
        return await self.send_api_request("get", url, kwargs, ['offset', 'filter', 'npf'])

    @validate_blogname
    async def follow(self, blogname):
        """
        Follow the url of the given blog

        :param blogname: a string, the blog url you want to follow

        :returns: a dict created from the JSON response
        """
        url = "/v2/user/follow"
        return await self.send_api_request("post", url, {'url': blogname}, ['url'])

    @validate_blogname
    async def unfollow(self, blogname):
        """
        Unfollow the url of the given blog

        :param blogname: a string, the blog url you want to follow

        :returns: a dict created from the JSON response
        """
        url = "/v2/user/unfollow"
        return await self.send_api_request("post", url, {'url': blogname}, ['url'])

    async def like(self, id, reblog_key):
        """
        Like the post of the given blog

        :param id: an int, the id of the post you want to like
        :param reblog_key: a string, the reblog key of the post

        :returns: a dict created from the JSON response
        """
        url = "/v2/user/like"
        params = {'id': id, 'reblog_key': reblog_key}
        return await self.send_api_request("post", url, params, ['id', 'reblog_key'])

    async def unlike(self, id, reblog_key):
        """
        Unlike the post of the given blog

        :param id: an int, the id of the post you want to like
        :param reblog_key: a string, the reblog key of the post

        :returns: a dict created from the JSON response
        """
        url = "/v2/user/unlike"
        params = {'id': id, 'reblog_key': reblog_key}
        return await self.send_api_request("post", url, params, ['id', 'reblog_key'])

    @validate_blogname
    async def create_photo(self, blogname, **kwargs):
        """
        Create a photo post or photoset on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param caption: a string, the caption that you want applied to the photo
        :param link: a string, the 'click-through' url you want on the photo
        :param source: a string, the photo source url
        :param data: a string or a list of the path of photo(s)

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "photo"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def create_text(self, blogname, **kwargs):
        """
        Create a text post on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param title: a string, the optional title of a post
        :param body: a string, the body of the text post

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "text"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def create_quote(self, blogname, **kwargs):
        """
        Create a quote post on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param quote: a string, the full text of the quote
        :param source: a string, the cited source of the quote

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "quote"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def create_link(self, blogname, **kwargs):
        """
        Create a link post on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param title: a string, the title of the link
        :param url: a string, the url of the link you are posting
        :param description: a string, the description of the link you are posting

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "link"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def create_chat(self, blogname, **kwargs):
        """
        Create a chat post on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param title: a string, the title of the conversation
        :param conversation: a string, the conversation you are posting

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "chat"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def create_audio(self, blogname, **kwargs):
        """
        Create a audio post on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param caption: a string, the caption for the post
        :param external_url: a string, the url of the audio you are uploading
        :param data: a string, the local filename path of the audio you are uploading

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "audio"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def create_video(self, blogname, **kwargs):
        """
        Create a audio post on a blog

        :param blogname: a string, the url of the blog you want to post to.
        :param state: a string, The state of the post.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param caption: a string, the caption for the post
        :param embed: a string, the emebed code that you'd like to upload
        :param data: a string, the local filename path of the video you are uploading

        :returns: a dict created from the JSON response
        """
        kwargs.update({"type": "video"})
        return await self._send_post(blogname, kwargs)

    @validate_blogname
    async def reblog(self, blogname, **kwargs):
        """
        Creates a reblog on the given blogname

        :param blogname: a string, the url of the blog you want to reblog to
        :param id: an int, the post id that you are reblogging
        :param reblog_key: a string, the reblog key of the post
        :param comment: a string, a comment added to the reblogged post

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/post/reblog".format(blogname)

        valid_options = ['id', 'reblog_key', 'comment'] + self._post_valid_options(kwargs.get('type', None))
        if 'tags' in kwargs and kwargs['tags']:
            # Take a list of tags and make them acceptable for upload
            kwargs['tags'] = ",".join(kwargs['tags'])
        return await self.send_api_request('post', url, kwargs, valid_options)

    @validate_blogname
    async def delete_post(self, blogname, id):
        """
        Deletes a post with the given id

        :param blogname: a string, the url of the blog you want to delete from
        :param id: an int, the post id that you want to delete

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/post/delete".format(blogname)
        return await self.send_api_request('post', url, {'id': id}, ['id'])

    @validate_blogname
    async def edit_post(self, blogname, **kwargs):
        """
        Edits a post with a given id

        :param blogname: a string, the url of the blog you want to edit
        :param state: a string, the state of the post. published, draft, queue, or private.
        :param tags: a list of tags that you want applied to the post
        :param tweet: a string, the customized tweet that you want
        :param date: a string, the GMT date and time of the post
        :param format: a string, sets the format type of the post. html or markdown
        :param slug: a string, a short text summary to the end of the post url
        :param id: an int, the post id that you want to edit

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/post/edit".format(blogname)

        if 'tags' in kwargs and kwargs['tags']:
            # Take a list of tags and make them acceptable for upload
            kwargs['tags'] = ",".join(kwargs['tags'])

        valid_options = ['id'] + self._post_valid_options(kwargs.get('type', None))
        return await self.send_api_request('post', url, kwargs, valid_options)

    @validate_blogname
    async def notes(self, blogname, id, **kwargs):
        """
        Gets the notes

        :param blogname: a string, the url of the blog that houses the post
        :param id: a string, the id of the post.
        :param mode: a string. Undocumented. Automatically added by tumblr but it's use is not yet known.
        :param before_timestamp: a string, retreives data before this timestamp

        :returns: a dict created from the JSON response
        """
        url = "/v2/blog/{}/notes".format(blogname)
        valid_options = ["id", "mode", "before_timestamp"]
        kwargs.update({"id":id})
        return await self.send_api_request('get', url, kwargs, valid_options)

    # Parameters valid for /post, /post/edit, and /post/reblog.
    def _post_valid_options(self, post_type=None):
        # These options are always valid
        valid = ['type', 'state', 'tags', 'tweet', 'date', 'format', 'slug']

        # Other options are valid on a per-post-type basis
        if post_type == 'text':
            valid += ['title', 'body']
        elif post_type == 'photo':
            valid += ['caption', 'link', 'source', 'data', 'photoset_layout']
        elif post_type == 'quote':
            valid += ['quote', 'source']
        elif post_type == 'link':
            valid += ['title', 'url', 'description', 'thumbnail']
        elif post_type == 'chat':
            valid += ['title', 'conversation']
        elif post_type == 'audio':
            valid += ['caption', 'external_url', 'data']
        elif post_type == 'video':
            valid += ['caption', 'embed', 'data']

        return valid

    async def _send_post(self, blogname, params):
        """
        Formats parameters and sends the API request off. Validates
        common and per-post-type parameters and formats your tags for you.

        :param blogname: a string, the blogname of the blog you are posting to
        :param params: a dict, the key-value of the parameters for the api request
        :param valid_options: a list of valid options that the request allows

        :returns: a dict parsed from the JSON response
        """
        url = "/v2/blog/{}/post".format(blogname)
        valid_options = self._post_valid_options(params.get('type', None))

        if len(params.get("tags", [])) > 0:
            # Take a list of tags and make them acceptable for upload
            params['tags'] = ",".join(params['tags'])

        return await self.send_api_request("post", url, params, valid_options)

    async def send_api_request(self, method, url, params={}, valid_parameters=[], needs_api_key=False):
        """
        Sends the url with parameters to the requested url, validating them
        to make sure that they are what we expect to have passed to us

        :param method: a string, the request method you want to make
        :param params: a dict, the parameters used for the API request
        :param valid_parameters: a list, the list of valid parameters
        :param needs_api_key: a boolean, whether or not your request needs an api key injected

        :returns: a dict parsed from the JSON response
        """
        if needs_api_key:
            params.update({'api_key': self.request.consumer_key})
            valid_parameters.append('api_key')

        files = {}
        if 'data' in params:
            if isinstance(params['data'], list):
                for idx, data in enumerate(params['data']):
                    files['data['+str(idx)+']'] =  open(params['data'][idx], 'rb')
            else:
                files = {'data': open(params['data'], 'rb')}
            del params['data']

        validate_params(valid_parameters, params)
        if method == "get":
            return await self.request.get(url, params)
        elif method == "delete":
            return await self.request.delete(url, params)
        else:
            return await self.request.post(url, params, files)
