{-# LANGUAGE FlexibleContexts #-}

module Control.Concurrent.Timer.Lifted
( Timer
, TimerIO

, oneShotTimer
, oneShotRestart

, repeatedTimer
, repeatedRestart

, newTimer
, stopTimer
) where

------------------------------------------------------------------------------
import           Control.Applicative
import           Control.Concurrent.Lifted         (ThreadId, fork, killThread)
import           Control.Concurrent.MVar.Lifted    (newMVar, tryTakeMVar, putMVar, modifyMVar_)
import           Control.Concurrent.Suspend.Lifted (Delay, suspend)
import           Control.Monad
import           Control.Monad.Base                (MonadBase)
import           Control.Monad.Trans.Control       (MonadBaseControl)
------------------------------------------------------------------------------
import           Control.Concurrent.Timer.Types (Timer(..))
------------------------------------------------------------------------------

-- | Attempts to restart (or start) a timer making it a one shot timer.
--
-- Returns True if the restrat was successful,
-- otherwise (e.g. other thread is attempting to restart the timer) returns False.
oneShotRestart :: MonadBaseControl IO m
               => Timer m
               -> m Bool
oneShotRestart (Timer action delay threadID) = do
    mtid <- tryTakeMVar threadID
    case mtid of
         Just (Just tid) -> do
             killThread tid
             ntid <- Just <$> oneShotAction delay action
             putMVar threadID ntid
             return True
         Just (Nothing)  -> do
             ntid <- Just <$> oneShotAction delay action
             putMVar threadID ntid
             return True
         Nothing -> return False
{-# INLINEABLE oneShotRestart #-}

-- | Attempts to restart (or start) a timer making it a repeated timer.
--
-- Returns True if the restrat was successful,
-- otherwise (e.g. other thread is attempting to restart the timer) returns False.
repeatedRestart :: MonadBaseControl IO m
                => Timer m
                -> m Bool
repeatedRestart (Timer action delay threadID) = do
    mtid <- tryTakeMVar threadID
    case mtid of
         Just (Just tid) -> do
             killThread tid
             ntid <- Just <$> repeatedAction delay action
             putMVar threadID ntid
             return True
         Just (Nothing)  -> do
             ntid <- Just <$> repeatedAction delay action
             putMVar threadID ntid
             return True
         Nothing -> return False
{-# INLINEABLE repeatedRestart #-}

-- | Executes the the given action once after the given delay elapsed, no sooner, maybe later.
oneShotTimer :: MonadBaseControl IO m
              => Delay      -- ^ The (minimal) time until the execution in microseconds.
             -> m ()        -- ^ The action to be executed.
             -> m (Timer m)
oneShotTimer d action = do
    tid <- oneShotAction d action >>= newMVar . Just
    return Timer { timerAction   = action
                 , timerDelay    = d
                 , timerThreadID = tid
                 }
{-# INLINEABLE oneShotTimer #-}

-- | Executes the the given action repeatedly with at least the given delay between executions.
repeatedTimer :: MonadBaseControl IO m
              => Delay   -- ^ The (minimal) delay between executions.
              -> m ()    -- ^ The action to be executed.
              -> m (Timer m)
repeatedTimer d action = do
    tid <- repeatedAction d action >>= newMVar . Just
    return Timer { timerAction   = action
                 , timerDelay    = d
                 , timerThreadID = tid
                 }
{-# INLINEABLE repeatedTimer #-}

-- | This function is blocking. It waits until it can stop the timer
-- (until there is a value in the threadID MVar), then it kill the thread.
stopTimer :: MonadBaseControl IO m
          => Timer m
          -> m ()
stopTimer (Timer _ _ threadID) = modifyMVar_ threadID $
    maybe (return Nothing)
          (\tid -> killThread tid >> return Nothing)

-- | Creates a new timer. This does not start the timer.
newTimer :: MonadBase IO m
         => Delay   -- ^ The (minimal) delay between executions.
         -> m ()    -- ^ The action to be executed.
         -> m (Timer m)
newTimer d action = Timer action d <$> newMVar Nothing
{-# INLINE newTimer #-}

------------------------------------------------------------------------------
-- | Utility

type TimerIO = Timer IO

-- | Forks a new thread that runs the supplied action
-- (at least) after the given delay.
oneShotAction :: MonadBaseControl IO m
              => Delay
              -> m ()
              -> m ThreadId
oneShotAction d action = fork (suspend d >> action)
{-# INLINE oneShotAction #-}

-- | Forks a new thread that repeats the supplied action
-- with (at least) the given delay between each execution.
repeatedAction :: MonadBaseControl IO m
               => Delay
               -> m ()
               -> m ThreadId
repeatedAction d action = fork (forever $ suspend d >> action)
{-# INLINE repeatedAction #-}