# Standard library imports
import contextlib
import math
import threading
import time
import sys

# Local imports
from uplink import decorators, utils
from uplink.clients.io import RequestTemplate, transitions

__all__ = ["ratelimit", "RateLimitExceeded"]

# Use monotonic time if available, otherwise fall back to the system clock.
now = time.monotonic if hasattr(time, "monotonic") else time.time


class RateLimitExceeded(RuntimeError):
    def __init__(self, calls, period):
        super(RateLimitExceeded, self).__init__(
            "Exceeded rate limit of [%s] calls every [%s] seconds."
            % (calls, period)
        )


class Limiter(object):
    _last_reset = _num_calls = None

    def __init__(self, max_calls, period, clock):
        self._max_calls = max_calls
        self._period = period
        self._clock = clock
        self._lock = threading.RLock()
        self._reset()

    @property
    def period_remaining(self):
        return self._period - (self._clock() - self._last_reset)

    @contextlib.contextmanager
    def check(self):
        with self._lock:
            if self.period_remaining <= 0:
                self._reset()
            yield self._max_calls > self._num_calls
            self._num_calls += 1

    def _reset(self):
        self._num_calls = 0
        self._last_reset = self._clock()


class RateLimiterTemplate(RequestTemplate):
    def __init__(self, limiter, create_limit_reached_exception):
        self._limiter = limiter
        self._create_limit_reached_exception = create_limit_reached_exception

    def before_request(self, request):
        with self._limiter.check() as ok:
            if ok:
                return transitions.send(request)
            elif self._create_limit_reached_exception is not None:
                raise self._create_limit_reached_exception()
            else:
                return transitions.sleep(self._limiter.period_remaining)


# noinspection PyPep8Naming
class ratelimit(decorators.MethodAnnotation):
    """
    A decorator that constrains a consumer method or an entire
    consumer to making a specified maximum number of requests within a
    defined time period (e.g., 15 calls every 15 minutes).

    By default, when the limit is reached, the client will wait until
    the current period is over before executing any subsequent
    requests. If you'd prefer the client to raise an exception when the
    limit is exceeded, set the ``raise_on_limit`` argument.

    Args:
        calls (int): The maximum number of allowed calls that the
            consumer can make within the time period.
        period (float): The duration of each time period in seconds.
        raise_on_limit (:class:`Exception` or bool): An exception to
            raise when the client exceeds the rate limit. If
            :obj:`True`, a :class:`uplink.ratelimit.RateLimitExceeded`
            exception is raised.
    """

    def __init__(self, calls=15, period=900, raise_on_limit=False, clock=now):
        self._max_calls = max(1, min(sys.maxsize, math.floor(calls)))
        self._period = period
        self._clock = clock
        self._limiter = None

        if utils.is_subclass(raise_on_limit, Exception) or isinstance(
            raise_on_limit, Exception
        ):
            self._create_limit_reached_exception = raise_on_limit
        elif raise_on_limit:
            self._create_limit_reached_exception = (
                self._create_rate_limit_exceeded
            )
        else:
            self._create_limit_reached_exception = None

    def _get_limiter_for_request(self, _):
        if self._limiter is None:
            self._limiter = Limiter(self._max_calls, self._period, self._clock)
        return self._limiter

    def modify_request(self, request_builder):
        limiter = self._get_limiter_for_request(request_builder)
        request_builder.add_request_template(
            RateLimiterTemplate(limiter, self._create_limit_reached_exception)
        )

    def _create_rate_limit_exceeded(self):
        return RateLimitExceeded(self._max_calls, self._period)
