{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TemplateHaskell #-}

module Happstack.Server.Internal.TimeoutTable
  ( TimeoutTable
  , new
  , null
  , insert
  , delete
  , killAll
  , killOlderThan
  , waitForActivity
  )
where

------------------------------------------------------------------------------
import           Control.Concurrent
import           Control.Monad
import           Data.Bits
import qualified Data.PSQueue as PSQ
import           Data.PSQueue (PSQ)
import           Data.Time.Clock.POSIX(POSIXTime)
import qualified Data.Vector as V
import           Data.Vector (Vector)
import           Data.Word
import           GHC.Conc (numCapabilities)
import           Prelude hiding (null)
------------------------------------------------------------------------------
import           Data.Concurrent.HashMap (nextHighestPowerOf2)


type TT = PSQ ThreadId POSIXTime


data TimeoutTable = TimeoutTable {
      _maps     :: !(Vector (MVar TT))
    , _activity :: !(MVar ())
}


defaultNumberOfLocks :: Word
defaultNumberOfLocks = nextHighestPowerOf2 $ toEnum $ 8 * numCapabilities


hashToBucket :: Word -> Word
hashToBucket x = x .&. (defaultNumberOfLocks-1)


new :: IO TimeoutTable
new = do
    vector <- V.replicateM (fromEnum defaultNumberOfLocks) (newMVar PSQ.empty)
    act    <- newEmptyMVar
    return $ TimeoutTable vector act


null :: TimeoutTable -> IO Bool
null (TimeoutTable maps _) = do
    nulls <- V.mapM (\mv -> withMVar mv $ return . PSQ.null) maps
    return $ V.and nulls


insert :: Word -> ThreadId -> POSIXTime -> TimeoutTable -> IO ()
insert thash tid time (TimeoutTable maps act) = do
    modifyMVar_ psqMv $ \psq -> do
        let !psq' = PSQ.insert tid time psq
        return $! psq'

    tryPutMVar act ()
    return ()

  where
    bucket = hashToBucket thash
    psqMv  = V.unsafeIndex maps $ fromEnum bucket


delete :: Word -> ThreadId -> TimeoutTable -> IO ()
delete thash tid (TimeoutTable maps act) = do
    modifyMVar_ psqMv $ \psq -> do
        let !psq' = PSQ.delete tid psq
        return $! psq'

    tryPutMVar act ()
    return ()

  where
    bucket = hashToBucket thash
    psqMv  = V.unsafeIndex maps $ fromEnum bucket


killAll :: TimeoutTable -> IO ()
killAll (TimeoutTable maps _) = do
    V.mapM_ k maps

  where
    k psqMV = modifyMVar_ psqMV $ \psq -> do
        mapM_ killThread $ PSQ.keys psq
        return PSQ.empty


killOlderThan :: POSIXTime -> TimeoutTable -> IO ()
killOlderThan time (TimeoutTable maps _) = do
    V.mapM_ processPSQ maps

  where
    processPSQ psqMV = modifyMVar_ psqMV $ \psq -> do
        let (psq', threads) = findOlder psq []
        mapM_ killThread threads
        return psq'

    findOlder psq l =
        let mmin = PSQ.findMin psq
        in maybe (psq,l)
                 (\m -> if PSQ.prio m <= time
                          then findOlder (PSQ.deleteMin psq) ((PSQ.key m):l)
                          else (psq,l))
                 mmin


waitForActivity :: TimeoutTable -> IO ()
waitForActivity t@(TimeoutTable _ act) = do
    takeMVar act
    b <- null t

    -- if the table is not empty, put the activity mvar back
    unless b $ (tryPutMVar act () >> return ())

    threadDelay 2500000