{-# LANGUAGE RecordWildCards #-}
module Hans.Tcp.Timers where

import           Hans.Config
import qualified Hans.HashTable as HT
import           Hans.Lens
import           Hans.Tcp.Tcb
import           Hans.Tcp.Output
import           Hans.Tcp.SendWindow
import           Hans.Time (toUSeconds)
import           Hans.Types

import Control.Concurrent (threadDelay)
import Control.Monad (when)
import Data.IORef (atomicModifyIORef')
import Data.Time.Clock (getCurrentTime,diffUTCTime)


-- | Process the slow and fast tcp timers. The fast timer runs four times a
-- second, while the slow timer runs two times a second.
tcpTimers :: NetworkStack -> IO ()
tcpTimers ns = loop True
  where
  loop runSlow =
    do start <- getCurrentTime

       HT.mapHashTableM_ (\_ -> updateActive ns runSlow) (view tcpActive ns)

       -- delay to the next 250ms boundary
       end <- getCurrentTime
       let delay = 0.250 - diffUTCTime end start
       when (delay > 0) (threadDelay (toUSeconds delay))

       loop $! not runSlow


-- | The body of the fast and slow tick handlers. The boolean indicates whether
-- or not the slow tick should also be run.
updateActive :: NetworkStack -> Bool -> Tcb -> IO ()
updateActive ns runSlow tcb@Tcb { .. } =
  do -- the slow timer
     when runSlow $
       do ts <- atomicModifyIORef' tcbTimers updateTimers
          handleRTO  ns tcb ts
          handle2MSL ns tcb ts

     -- the fast timer
     shouldAck <- atomicModifyIORef' tcbNeedsDelayedAck (\ b -> (False,b))
     when shouldAck (sendAck ns tcb)


-- | Handle the retransmit timer. When the timer expires, if there is anything
-- in the send window, retransmit the left-most segment.
handleRTO :: NetworkStack -> Tcb -> TcpTimers -> IO ()
handleRTO ns Tcb { .. } TcpTimers { .. }
  | ttRetransmitValid && ttRetransmit <= 0 =
    do mbSeg <- atomicModifyIORef' tcbSendWindow retransmitTimeout
       case mbSeg of
         Just (hdr,body) ->
           do atomicModifyIORef' tcbTimers retryRetransmit
              _ <- sendTcp ns tcbRouteInfo tcbRemote hdr body
              return ()

         Nothing ->
              return ()

  | otherwise =
    return ()


-- | Make sure that the connection is still active. The Idle timer is checked
-- when the 2MSL timer expires.
handle2MSL :: NetworkStack -> Tcb -> TcpTimers -> IO ()
handle2MSL ns tcb@Tcb { .. } TcpTimers { .. }

  | tt2MSL <= 0 =
       -- why do we only check the idle timer when 2MSL goes off?
       if ttIdle >= ttMaxIdle
          then closeActive ns tcb
          else atomicModifyIORef' tcbTimers (reset2MSL (view config ns))

  | otherwise =
    return ()