-- | A version of 'Development.Shake.newThrottle' that runs in IO, and can be called before calling 'Development.Shake.shake'.
--   Most people should use 'Development.Shake.newResource' instead.
newThrottleIO :: String -> Int -> Double -> IO Resource
newThrottleIO name count period = do
    when (count < 0) $
        error $ "You cannot create a throttle named " ++ name ++ " with a negative quantity, you used " ++ show count
    key <- resourceId
    lock <- newLock
    time <- offsetTime
    rep <- newVar $ Right [(0, count)]
    let s = Throttle lock rep time
    return $ Resource key shw (acquire s) (release s)
    where
        shw = "Throttle " ++ name

        release :: Throttle -> Int -> IO ()
        release Throttle{..} n = do
            t <- throttleTime
            modifyVar_ throttleVal $ \v -> case v of
                Left b -> signalBarrier b () >> return (Right [(t+period, n)])
                Right ts -> return $ Right $ ts ++ [(t+period, n)]

        acquire :: Throttle -> Int -> IO (Maybe (IO ()))
        acquire Throttle{..} want
            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
            | want > count = error $ "You cannot acquire more than " ++ show count ++ " of " ++ shw ++ ", requested " ++ show want
            | otherwise = do
                let grab t vs = do
                        let (a,b) = span ((<= t) . fst) vs
                        -- renormalise for clock skew, nothing can ever be > t+period away
                        return (sum $ map snd a, map (first $ min $ t+period) b)
                let push i vs = [(0,i) | i > 0] ++ vs

                -- attempt to grab without locking
                res <- withLockTry throttleLock $ do
                    modifyVar throttleVal $ \v -> case v of
                        Right vs -> do
                            t <- throttleTime
                            (got,vs) <- grab t vs
                            if got >= want then
                                return (Right $ push (got - want) vs, True)
                             else
                                return (Right $ push got vs, False)
                        _ -> return (v, False)
                if res == Just True then
                    return Nothing
                 else
                    return $ Just $ withLock throttleLock $ do
                        -- keep trying to acquire more resources until you have everything you need
                        let f want = join $ modifyVar throttleVal $ \v -> case v of
                                Left _ -> err "newThrottle, invariant failed, Left while holding throttleLock"
                                Right vs -> do
                                    t <- throttleTime
                                    (got,vs) <- grab t vs
                                    case vs of
                                        _ | got >= want -> return (Right $ push (got - want) vs, return ())
                                        [] -> do
                                            b <- newBarrier
                                            return (Left b, waitBarrier b >> f (want - got))
                                        (t2,n):vs -> do
                                            -- be robust to clock skew - only ever sleep for 'period' at most and always mark the next as good.
                                            return $ (,) (Right $ (0,n):vs) $ do
                                                sleep $ min period (t2-t)
                                                f $ want - got
                        f want
newThrottleIO :: String -> Int -> Double -> IO Resource
newThrottleIO name count period = do
    when (count < 0) $
        error $ "You cannot create a throttle named " ++ name ++ " with a negative quantity, you used " ++ show count
    key <- resourceId
    lock <- newLock
    time <- offsetTime
    rep <- newVar $ Right [(0, count)]
    let s = Throttle lock rep time
    return $ Resource key shw (acquire s) (release s)
    where
        shw = "Throttle " ++ name

        release :: Throttle -> Int -> IO ()
        release Throttle{..} n = do
            t <- throttleTime
            modifyVar_ throttleVal $ \v -> case v of
                Left b -> signalBarrier b () >> return (Right [(t+period, n)])
                Right ts -> return $ Right $ ts ++ [(t+period, n)]

        acquire :: Throttle -> Int -> IO (Maybe (IO ()))
        acquire Throttle{..} want
            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
            | want > count = error $ "You cannot acquire more than " ++ show count ++ " of " ++ shw ++ ", requested " ++ show want
            | otherwise = do
                let grab t vs = do
                        let (a,b) = span ((<= t) . fst) vs
                        -- renormalise for clock skew, nothing can ever be > t+period away
                        return (sum $ map snd a, map (first $ min $ t+period) b)
                let push i vs = [(0,i) | i > 0] ++ vs

                -- attempt to grab without locking
                res <- withLockTry throttleLock $ do
                    modifyVar throttleVal $ \v -> case v of
                        Right vs -> do
                            t <- throttleTime
                            (got,vs) <- grab t vs
                            if got >= want then
                                return (Right $ push (got - want) vs, True)
                             else
                                return (Right $ push got vs, False)
                        _ -> return (v, False)
                if res == Just True then
                    return Nothing
                 else
                    return $ Just $ withLock throttleLock $ do
                        -- keep trying to acquire more resources until you have everything you need
                        let f want = join $ modifyVar throttleVal $ \v -> case v of
                                Left _ -> err "newThrottle, invariant failed, Left while holding throttleLock"
                                Right vs -> do
                                    t <- throttleTime
                                    (got,vs) <- grab t vs
                                    case vs of
                                        _ | got >= want -> return (Right $ push (got - want) vs, return ())
                                        [] -> do
                                            b <- newBarrier
                                            return (Left b, waitBarrier b >> f (want - got))
                                        (t2,n):vs -> do
                                            -- be robust to clock skew - only ever sleep for 'period' at most and always mark the next as good.
                                            return $ (,) (Right $ (0,n):vs) $ do
                                                sleep $ min period (t2-t)
                                                f $ want - got
                        f want
newThrottleIO name count period = do
    when (count < 0) $
        error $ "You cannot create a throttle named " ++ name ++ " with a negative quantity, you used " ++ show count
    key <- resourceId
    lock <- newLock
    time <- offsetTime
    rep <- newVar $ Right [(0, count)]
    let s = Throttle lock rep time
    return $ Resource key shw (acquire s) (release s)
    where
        shw = "Throttle " ++ name

        release :: Throttle -> Int -> IO ()
        release Throttle{..} n = do
            t <- throttleTime
            modifyVar_ throttleVal $ \v -> case v of
                Left b -> signalBarrier b () >> return (Right [(t+period, n)])
                Right ts -> return $ Right $ ts ++ [(t+period, n)]

        acquire :: Throttle -> Int -> IO (Maybe (IO ()))
        acquire Throttle{..} want
            | want < 0 = error $ "You cannot acquire a negative quantity of " ++ shw ++ ", requested " ++ show want
            | want > count = error $ "You cannot acquire more than " ++ show count ++ " of " ++ shw ++ ", requested " ++ show want
            | otherwise = do
                let grab t vs = do
                        let (a,b) = span ((<= t) . fst) vs
                        -- renormalise for clock skew, nothing can ever be > t+period away
                        return (sum $ map snd a, map (first $ min $ t+period) b)
                let push i vs = [(0,i) | i > 0] ++ vs

                -- attempt to grab without locking
                res <- withLockTry throttleLock $ do
                    modifyVar throttleVal $ \v -> case v of
                        Right vs -> do
                            t <- throttleTime
                            (got,vs) <- grab t vs
                            if got >= want then
                                return (Right $ push (got - want) vs, True)
                             else
                                return (Right $ push got vs, False)
                        _ -> return (v, False)
                if res == Just True then
                    return Nothing
                 else
                    return $ Just $ withLock throttleLock $ do
                        -- keep trying to acquire more resources until you have everything you need
                        let f want = join $ modifyVar throttleVal $ \v -> case v of
                                Left _ -> err "newThrottle, invariant failed, Left while holding throttleLock"
                                Right vs -> do
                                    t <- throttleTime
                                    (got,vs) <- grab t vs
                                    case vs of
                                        _ | got >= want -> return (Right $ push (got - want) vs, return ())
                                        [] -> do
                                            b <- newBarrier
                                            return (Left b, waitBarrier b >> f (want - got))
                                        (t2,n):vs -> do
                                            -- be robust to clock skew - only ever sleep for 'period' at most and always mark the next as good.
                                            return $ (,) (Right $ (0,n):vs) $ do
                                                sleep $ min period (t2-t)
                                                f $ want - got
                        f want