-- | Simple rate limiting combinator.

module NationStates.RateLimit (
    RateLimit(),
    newRateLimit,
    rateLimit,
    setDelay,
    ) where


import Control.Concurrent
import Control.Exception
import System.Clock


data RateLimit = RateLimit {
    _rateLock :: !(MVar TimeSpec),
    _rateDelay :: !TimeSpec
    }


-- | Create a new rate limiter with the specified delay.
--
-- The rate limiter is thread-safe, and can be shared between threads.
newRateLimit
    :: Rational
        -- ^ Delay, in seconds
    -> IO RateLimit
newRateLimit delay' = do
    lock <- newMVar $! negate delay
    return $ RateLimit lock delay
  where
    delay = fromSeconds delay'


-- | Run the given action, pausing as necessary to keep under the rate limit.
rateLimit :: RateLimit -> IO a -> IO a
rateLimit (RateLimit lock delay) action =
    mask $ \restore -> do
        prev <- takeMVar lock
        now <- getTime Monotonic
        threadDelay' (prev + delay - now) `onException` putMVar lock prev
        restore action `finally` (putMVar lock =<< getTime Monotonic)


threadDelay' :: TimeSpec -> IO ()
threadDelay' t = threadDelay . fromInteger $ timeSpecAsNanoSecs t `div` 1000


-- | Create a new rate limiter with the same lock but a different delay.
setDelay :: Rational -> RateLimit -> RateLimit
setDelay delay' (RateLimit lock _) = RateLimit lock (fromSeconds delay')


fromSeconds :: Rational -> TimeSpec
fromSeconds n = fromInteger . ceiling $ n * 1000 * 1000 * 1000