-- | Utilities for repeatedly running @'IO'@ actions
-- at a specific frequency.
module TimeStep
  ( -- * Repeating actions
    repeatedly
  , repeatedly'
    -- * Repeating actions in another thread
  , asyncRepeatedly
  , asyncRepeatedly'
    -- * Implementation details
  , TimeStepClock
  , newClock
  , loopUsing
    -- * Reexports (timekeeping)
  , module Time.Flick
  , Clock(..)
  ) where

import Control.Concurrent
import Control.Monad
import Data.Bifunctor
import Data.Fixed (divMod')
import Data.Int
import Data.IORef
import Data.Ratio

import Control.Concurrent.Async
import System.Clock

import Time.Flick

-- | Internal state of the stepper. Created using @'newClock'@.
data TimeStepClock = TimeStepClock
  { tscLastTick :: IORef Flicks
  , tscDesiredTickTime :: Flicks
  , tscClockType :: Clock
  } deriving Eq

-- | Run an action repeatedly at the specified frequency.
-- Uses the 'Monotonic' clock for timing.
repeatedly :: Rational -> IO a -> IO void
repeatedly freq = repeatedly' freq Monotonic

-- | Run an action repeatedly at the specified frequency,
-- using the given clock type.
repeatedly' :: Rational -> Clock -> IO a -> IO void
repeatedly' freq clockTy act = do
  clock <- newClock freq clockTy
  loopUsing clock act

-- | Run an action repeatedly at the specified frequency, in a separate thread.
-- Uses the 'Monotonic' clock for timing.
asyncRepeatedly :: Rational -> IO a -> IO (Async void)
asyncRepeatedly freq = asyncRepeatedly' freq Monotonic

-- | Run an action repeatedly at the specified frequency, in a separate thread,
-- using the given clock type.
asyncRepeatedly' :: Rational -> Clock -> IO a -> IO (Async void)
asyncRepeatedly' freq clockTy act = do
  clock <- newClock freq clockTy
  async (loopUsing clock act)

-- | Initialise a new clock structure for the given frequency and using
-- the given clock type.
newClock :: Rational -> Clock -> IO TimeStepClock
newClock freq clockTy = do
  leftoverRef <- newIORef =<< flicksNow clockTy
  pure TimeStepClock
    { tscLastTick = leftoverRef
    , tscDesiredTickTime = periodForFreq freq
    , tscClockType = clockTy
    }

-- | Repeat the given action using the information in the clock structure.
loopUsing :: TimeStepClock -> IO a -> IO void
loopUsing (TimeStepClock lastTickRef tickTime clockTy) act = forever $ do
  now <- flicksNow clockTy
  lastTick <- readIORef lastTickRef
  let elapsed = now - lastTick
      (ticksToRun, leftover) = elapsed `divMod` tickTime
  replicateM_ (fromIntegral ticksToRun) act
  writeIORef lastTickRef now
  now' <- flicksNow clockTy
  threadDelayFlicks (tickTime + now - now')