{-|
Module      : Control.Concurrent.Async.Timer.Internal
Description : Implementation of asynchronous Timers
Copyright   : (c) Moritz Clasmeier 2016, 2018
License     : BSD3
Maintainer  : mtesseract@silverratio.net
Stability   : experimental
Portability : POSIX

This module contains the internal implementation of asynchronous
timers.
-}

{-# LANGUAGE LambdaCase #-}

module Control.Concurrent.Async.Timer.Internal where

import qualified Control.Concurrent.Async as Async
import           Control.Exception.Safe
import           Control.Monad            (void)
import           Control.Monad.IO.Unlift
import           UnliftIO.Async
import           UnliftIO.Concurrent
import           UnliftIO.STM

-- | This is the type of timer handle, which will be provided to the
-- IO action to be executed within 'withAsyncTimer'. The user can use
-- 'timerWait' on this timer to delay execution until the next timer
-- synchronization event.
data Timer = Timer { timerMVar    :: MVar ()
                   , timerControl :: TBQueue TimerCommand }

-- | Timer commands that can be sent over a timer control channel to
-- an asynchronous timer.
data TimerCommand = TimerReset deriving (Show, Eq)

-- | Type of a timer configuration.
data TimerConf = TimerConf { _timerConfInitDelay :: Int
                           , _timerConfInterval  :: Int }

-- | Sleep 'dt' milliseconds.
millisleep :: MonadIO m => Int -> m ()
millisleep dt = threadDelay (dt * 10 ^ 3)

-- | Default timer configuration specifies no initial delay and an
-- interval delay of 1s.
defaultConf :: TimerConf
defaultConf = TimerConf { _timerConfInitDelay =    0
                        , _timerConfInterval  = 1000 }

-- | Set the initial delay in the provided timer configuration.
setInitDelay :: Int -> TimerConf -> TimerConf
setInitDelay n conf = conf { _timerConfInitDelay = n }

-- | Set the interval delay in the provided timer configuration.
setInterval :: Int -> TimerConf -> TimerConf
setInterval n conf = conf { _timerConfInterval = n }

-- | Timer loop to be executed within in a timer thread.
timerLoop
  :: MonadUnliftIO m
  => Int
  -> Int
  -> Timer
  -> m ()
timerLoop initDelay intervalDelay timer = go initDelay

  where go delay = do
          race (millisleep delay) readCmd >>= \ case
            Left () -> do
              wakeUp
              go intervalDelay
            Right cmd ->
              case cmd of
                TimerReset ->
                  go intervalDelay

        wakeUp   = putMVar (timerMVar timer) ()
        readCmd  = atomically $ readTBQueue (timerControl timer)

-- | Wait for the next synchronization event on the givem timer.
wait
  :: MonadUnliftIO m
  => Timer
  -> m ()
wait = void . takeMVar . timerMVar

-- | Reset the provided timer.
reset
  :: MonadUnliftIO m
  => Timer
  -> m ()
reset timer =
  atomically $ writeTBQueue (timerControl timer) TimerReset

-- | Spawn a timer thread based on the provided timer configuration
-- and then run the provided IO action, which receives the new timer
-- as an argument and call 'timerWait' on it for synchronization. When
-- the provided IO action has terminated, the timer thread will be
-- terminated also.
withAsyncTimer
  :: (MonadUnliftIO m, MonadMask m)
  => TimerConf
  -> (Timer -> m b)
  -> m b
withAsyncTimer conf io = do
  -- This MVar will be our synchronization mechanism.
  mVar <- newEmptyMVar
  controlChannel <- atomically $ newTBQueue 1
  let timer         = Timer { timerMVar = mVar
                            , timerControl = controlChannel }
      initDelay     = _timerConfInitDelay conf
      intervalDelay = _timerConfInterval  conf
  withAsync (timerLoop initDelay intervalDelay timer) $ \ asyncTimer -> do
    -- This guarantees that we will be informed right away if our
    -- timer thread disappears, for example because of an async
    -- exception:
    liftIO $ Async.link asyncTimer
    io timer