# -*- coding: utf-8 -*-
"""
    ranking
    ~~~~~~~

    :class:`Ranking` and various strategies for assigning rankings.

    :copyright: (c) 2012-2020 by Heungsub Lee
    :license: BSD, see LICENSE for more details.
"""
import itertools


__version__ = '0.3.2'
__all__ = ['Ranking', 'COMPETITION', 'MODIFIED_COMPETITION', 'DENSE',
           'ORDINAL', 'FRACTIONAL']


try:
    next
except NameError:
    # for Python 2.5
    next = lambda it: it.next()


def strategy(f):
    class Strategy(object):
        def __call__(self, start, length):
            return f(start, length)
        def __repr__(self):
            return f.__name__
    Strategy.__name__ = f.__name__
    return Strategy()


@strategy
def COMPETITION(start, length):
    """Standard competition ranking ("1224" ranking)"""
    for x in xrange(length):
        yield start
    yield start + length


@strategy
def MODIFIED_COMPETITION(start, length):
    """Modified competition ranking ("1334" ranking)"""
    for x in xrange(length):
        yield start + length - 1
    yield start + length


@strategy
def DENSE(start, length):
    """Dense ranking ("1223" ranking)"""
    for x in xrange(length):
        yield start
    yield start + 1


@strategy
def ORDINAL(start, length):
    """Ordinal ranking ("1234" ranking)"""
    return xrange(start, start + length + 1)


@strategy
def FRACTIONAL(start, length):
    """Fractional ranking ("1 2.5 2.5 4" ranking)"""
    avg = (2 * start + length - 1) / 2.
    for x in xrange(length):
        yield avg
    yield start + length


class Ranking(object):
    """This class looks like `enumerate` but generates a `tuple` containing a
    rank and value instead.

    >>> scores = [100, 80, 80, 70, None]
    >>> list(Ranking(scores))
    [(0, 100), (1, 80), (1, 80), (3, 70), (None, None)]

    :param sequence: sorted score sequence. `None` in the sequence means that
                     no score.
    :param strategy: a strategy for assigning rankings. Defaults to
                     :func:`COMPETITION`.
    :param start: a first rank. Defaults to 0.
    :param key: (keyword-only) a function to get score from a value
    :param reverse: (keyword-only) `sequence` is in ascending order if `True`,
                    descending otherwise. Defaults to `False`.
    :param no_score: (keyword-only) a value for representing "no score".
                     Defaults to `None`.
    """

    def __init__(self, sequence, strategy=COMPETITION, start=0, **kwargs):
        self.sequence = sequence
        self.strategy = strategy
        self.start = start
        self.key = kwargs.pop('key', None)
        self.reverse = kwargs.pop('reverse', False)
        self.no_score = kwargs.pop('no_score', None)
        if kwargs:
            s = 's' if len(kwargs) == 2 else ''
            params = ', '.join(repr(key) for key in kwargs.iterkeys())
            raise TypeError('Unexpected keyword argument%s: %s' % (s, params))

    def __iter__(self):
        rank, drawn, tie_started, final = self.start, [], None, object()
        iterator = iter(self.sequence)
        try:
            right = next(iterator)
        except StopIteration:
            return
        right_score = right if self.key is None else self.key(right)
        for value in itertools.chain(iterator, [final]):
            left, right = right, value
            left_score = right_score
            if value is not final:
                right_score = right if self.key is None else self.key(right)
            if left_score == self.no_score:
                yield None, left
                continue
            elif right_score == self.no_score or value is final:
                compared = 1
            else:
                compared = (0 if left_score == right_score else
                            -1 if left_score < right_score else 1)
                if self.reverse:
                    compared = -compared
            if compared < 0: # left is less than right
                raise ValueError('%r should be behind of %r' % \
                                 (left_score, right_score))
            elif compared == 0: # same scores
                if tie_started is None:
                    tie_started = rank
                drawn.append(left)
                continue
            elif drawn:
                drawn.append(left)
                for rank in self.strategy(tie_started, len(drawn)):
                    try:
                        yield rank, drawn.pop(0)
                    except IndexError:
                        pass
                tie_started = None
                continue
            yield rank, left
            rank += 1

    def ranks(self):
        """Generates only ranks."""
        for rank, value in self:
            yield rank

    def rank(self, value):
        """Finds the rank of the value.

        :raises ValueError: the value isn't ranked in the ranking
        """
        for rank, other in self:
            if value == other:
                return rank
        raise ValueError('%r is not ranked' % value)
