
"""A very thin Python wrapper over the WaniKani API.

The aim of this module is to model the API as closely as possible while
ensuring the correct Python type is used for each field in the API. The syntax
for using the library has been made as simple as possible.

Usage
-----
1. Import the WaniKani class. `from crabigator.wanikani import WaniKani`

2. Instantiate a WaniKani instance with your API key.
   `wanikani = WaniKani('my_api_key')`

3. Get any endpoint specified by the API docs (dashes replaced with
   underscores).
  * wanikani.user_information
  * wanikani.study_queue
  * wanikani.level_progression
  * wanikani.srs_distribution
  * wanikani.recent_unlocks
  * wanikani.critical_items
  * wanikani.radicals
  * wanikani.kanji
  * wanikani.vocabulary

4. From any of the above endpoints in #3 the output fields in the
   required-information key can be retrieved. For example:
  * print(wanikani.user_information.username)
  * print(wanikani.study_queue.next_review_date)
  * print(wanikani.level_progression.kanji_total)
  * print(wanikani.srs_distribution.master.total)
  * print(wanikani.recent_unlocks[0])
  * print(wanikani.critical_items[0])
  * print(wanikani.radicals[0])
  * print(wanikani.kanji[0])
  * print(wanikani.vocabulary[0])

5. If you need to pass arguments to the API then the following functions will
   do that.
  * wanikani.get_recent_unlocks(limit=10)
  * wanikani.get_critical_items(percent=75)
  * wanikani.get_radicals(levels=[2, 3, 4])
  * wanikani.get_kanji(levels=[5, 6, 7])
  * wanikani.get_vocabulary(levels=[8, 9, 10])

Notes
-----
* Everything is a property of a Python object.
* All API responses can be easily printed for debugging.
* All dates in API responses are converted to datetime objects.
* All comma-separated lists in API responses are converted to arrays of strings
  for easier iteration over multiple readings and meanings.
"""

from datetime import datetime
from json import loads
try:
    # pylint: disable=no-name-in-module
    from urllib import urlopen
except ImportError:
    # pylint: disable=no-name-in-module, import-error
    from urllib.request import urlopen

__all__ = ['WaniKani', 'WaniKaniError', 'WaniKaniObject']

API_VERSION = '1.4'
URL_FORMAT = 'https://www.wanikani.com/api/v{vers}/user/{key}/{res}/{args}'


class WaniKani(object):
    """Main class for interacting with the WaniKani API.

    Each API endpoint has a property of the same name (with dashes replaced by
    underscores), and also a get_<endpoint> method which takes an optional
    argument specified by the API documentation.
    """

    def __init__(self, api_key, url_format=URL_FORMAT):
        (self.url_format, self.api_key) = (url_format, api_key)

    def get_user_information(self):
        """Retrieves the information in the user-information API endpoint."""
        return self._get('user-information', 'user_information', USER_INFO)
    user_information = property(get_user_information)

    def get_study_queue(self):
        """Retrieves the information in the study-queue API endpoint."""
        return self._get('study-queue', 'requested_information', STUDY_QUEUE)
    study_queue = property(get_study_queue)

    def get_level_progression(self):
        """Retrieves the information in the level-progression API endpoint."""
        return self._get('level-progression',
                         'requested_information',
                         LEVEL_PROG)
    level_progression = property(get_level_progression)

    def get_srs_distribution(self):
        """Retrieves the information in the srs-distribution API endpoint."""
        return self._get('srs-distribution',
                         'requested_information',
                         SRS_DISTS)
    srs_distribution = property(get_srs_distribution)

    def get_recent_unlocks(self, limit=None):
        """Retrieves the information in the recent-unlocks API endpoint."""
        return self._get_meta_items('recent-unlocks', limit)
    recent_unlocks = property(get_recent_unlocks)

    def get_critical_items(self, percent=None):
        """Retrieves the information in the critical-items API endpoint."""
        return self._get_meta_items('critical-items', percent)
    critical_items = property(get_critical_items)

    def get_radicals(self, levels=None):
        """Retrieves the information in the radicals API endpoint."""
        return self._get_items(levels, 'radicals', RADICAL)
    radicals = property(get_radicals)

    def get_kanji(self, levels=None):
        """Retrieves the information in the kanji API endpoint."""
        return self._get_items(levels, 'kanji', KANJI)
    kanji = property(get_kanji)

    def get_vocabulary(self, levels=None):
        """Retrieves the information in the vocabulary API endpoint."""
        return self._get_items(levels, 'vocabulary', VOCABULARY)
    vocabulary = property(get_vocabulary)

    def _get(self, resource, key, fields):
        """Gets an WaniKaniObject representing some object returned by the
        API.
        """
        return WaniKaniObject(self._raw_request(resource)[key], fields)

    def _get_items(self, levels, resource, fields):
        """Gets an array of WaniKaniObject representing radicals, kanji, or
        vocabulary.
        """
        arg = ','.join([str(x) for x in levels]) if levels is not None else ''
        response = self._raw_request(resource=resource, argument=arg)
        response = (response['requested_information']['general']
                    if 'general' in response['requested_information']
                    else response['requested_information'])
        item_list = []
        for item in response:
            item_list.append(WaniKaniObject(item, fields))
        return item_list

    def _get_meta_items(self, resource, argument):
        """Gets an array of WaniKaniObject representing radicals, kanji, and/or
        vocabulary.
        """
        argument = str(argument) if argument is not None else ''
        response = self._raw_request(resource, argument=argument)
        item_list = []
        for item in response['requested_information']:
            tmap = {'radical': RADICAL, 'kanji': KANJI,
                    'vocabulary': VOCABULARY}
            item_list.append(WaniKaniObject(item, tmap[item['type']]))
        return item_list

    def _raw_request(self, resource, argument='', version=API_VERSION):
        """Issue an HTTP GET request to the API and return the response."""
        url = self.url_format.format(vers=version, key=self.api_key,
                                     res=resource, args=argument)
        rsp = loads(urlopen(url).read().decode('utf-8'))
        if 'error' in rsp:
            error = rsp['error']
            code = error['code'] if 'code' in error else None
            message = error['message'] if 'message' in error else None
            raise WaniKaniError(code, message)
        return rsp


class WaniKaniError(Exception):
    """Errors returned by the WaniKani API itself.

    Contains all of the information in the "error" field returned by the API.
    """

    def __init__(self, code, msg):
        super(WaniKaniError, self).__init__('{c} - {m}'.format(c=code, m=msg))
        (self.code, self.message) = (code, msg)


# Disable the too few public methods warning from pylint. Pylint may have a
# point, but ignore it on purpose...
# pylint: disable=too-few-public-methods
class WaniKaniObject(object):
    """Generic object representing anything returned by the WaniKani API.

    This really just acts like a dict, but has a little nicer syntax. The
    fields are specified by the constants below. Each field has a name and a
    function used to retrieve the name from a provided JSON object.
    """

    def __init__(self, info, fields):
        for field, fun in fields:
            if info is not None and field in info and info[field] is not None:
                setattr(self, field, fun(info[field]))
            else:
                setattr(self, field, None)

    def __repr__(self):
        return str(self.__dict__)

USER_INFO = [('username', lambda x: x),
             ('gravatar', lambda x: x),
             ('level', lambda x: x),
             ('title', lambda x: x),
             ('about', lambda x: x),
             ('twitter', lambda x: x),
             ('topics_count', lambda x: x),
             ('posts_count', lambda x: x),
             ('creation_date', datetime.utcfromtimestamp),
             ('vacation_date', datetime.utcfromtimestamp)]
STUDY_QUEUE = [('lessons_available', lambda x: x),
               ('reviews_available', lambda x: x),
               ('reviews_available_next_hour', lambda x: x),
               ('reviews_available_next_day', lambda x: x),
               ('next_review_date', datetime.utcfromtimestamp)]
LEVEL_PROG = [('radicals_progress', lambda x: x),
              ('radicals_total', lambda x: x),
              ('kanji_progress', lambda x: x),
              ('kanji_total', lambda x: x)]
SRS_DIST = [('radicals', lambda x: x),
            ('kanji', lambda x: x),
            ('vocabulary', lambda x: x),
            ('total', lambda x: x)]
SRS_DISTS = [('apprentice', lambda x: WaniKaniObject(x, SRS_DIST)),
             ('guru', lambda x: WaniKaniObject(x, SRS_DIST)),
             ('master', lambda x: WaniKaniObject(x, SRS_DIST)),
             ('enlighten', lambda x: WaniKaniObject(x, SRS_DIST)),
             ('burned', lambda x: WaniKaniObject(x, SRS_DIST))]
USER_SPEC = [('srs', lambda x: x),
             ('srs_numeric', lambda x: x),
             ('unlocked_date', datetime.utcfromtimestamp),
             ('available_date', datetime.utcfromtimestamp),
             ('burned', lambda x: x),
             ('burned_date', datetime.utcfromtimestamp),
             ('meaning_correct', lambda x: x),
             ('meaning_incorrect', lambda x: x),
             ('meaning_max_streak', lambda x: x),
             ('meaning_current_streak', lambda x: x),
             ('reading_correct', lambda x: x),
             ('reading_incorrect', lambda x: x),
             ('reading_max_streak', lambda x: x),
             ('reading_current_streak', lambda x: x),
             ('meaning_note', lambda x: x),
             ('user_synonyms', lambda x: x),
             ('reading_note', lambda x: x)]
RADICAL = [('character', lambda x: x),
           ('meaning', lambda x: x.split(', ')),
           ('image', lambda x: x),
           ('level', lambda x: x),
           ('user_specific', lambda x: WaniKaniObject(x, USER_SPEC))]
KANJI = [('character', lambda x: x),
         ('meaning', lambda x: x.split(', ')),
         ('onyomi', lambda x: x.split(', ')),
         ('kunyomi', lambda x: x.split(', ')),
         ('nanori', lambda x: x.split(', ')),
         ('important_reading', lambda x: x),
         ('level', lambda x: x),
         ('user_specific', lambda x: WaniKaniObject(x, USER_SPEC))]
VOCABULARY = [('character', lambda x: x),
              ('kana', lambda x: x.split(', ')),
              ('meaning', lambda x: x.split(', ')),
              ('level', lambda x: x),
              ('user_specific', lambda x: WaniKaniObject(x, USER_SPEC))]
