-- ----------------------------------------------------------------------------

{- |
  Module     : Holumbus.Common.Threading
  Copyright  : Copyright (C) 2008 Stefan Schmidt
  License    : MIT

  Maintainer : Stefan Schmidt (stefanschmidt@web.de)
  Stability  : experimental
  Portability: portable
  Version    : 0.1

  Operations to start and stop threads which will not be killed when a regular
  exception occurs. In this case, the thread will continue working. Such a 
  thread can only be killed by the stop-method. This whole thing is a wrapper
  around the normal lightweight thread functions.
  
  The created threads execute a function in an infinite loop. This is the
  normal usecase for message dispatcher threads.
  
-}

-- ----------------------------------------------------------------------------

module Holumbus.Common.Threading
(
  Thread
, newThread
, setThreadDelay
, setThreadAction
, setThreadErrorHandler

, startThread
, stopThread
)
where

{- 6.8
import qualified Control.Exception      as E
import           Data.Typeable
-}
import           Control.Exception      ( AsyncException(..)
                                        , catchJust
                                        )
import           Control.Concurrent

import           Data.Maybe

import           System.Log.Logger

import           Holumbus.Common.Utils  ( handleAll )

localLogger :: String
localLogger = "Holumbus.Common.Threading"


-- ----------------------------------------------------------------------------
-- Thread-Control
-- ----------------------------------------------------------------------------

-- | Exception to stop a thread, so we can distinguish this request from
--   "real" exceptions.

{- 6.8
data KillThreadException = KillThreadException ThreadId
  deriving (Typeable, Show)
-}

-- | The data needed to access and control the thread. 
data ThreadData = ThreadData {
    thd_Id      :: Maybe ThreadId
  , thd_Running :: Bool
  , thd_Delay   :: Maybe Int
  , thd_Action  :: (IO ())
  , thd_Error   :: (IO ())
  }
  
  
-- | The thread datatype
type Thread = MVar ThreadData 


-- | Creates a new thread object. The thread will not be running.
newThread :: IO Thread
newThread
  = do 
    newMVar $ ThreadData Nothing False defaultDelay noAction noAction
    where
    noAction     = return ()
    defaultDelay = Nothing


-- | Sets the delay between two loop cycles. Default value: no delay.
setThreadDelay :: Int -> Thread -> IO ()
setThreadDelay d thread
  = do
    modifyMVar thread $ \thd -> return (thd {thd_Delay = Just d},())
    
    
-- | Sets the action function, which will be executed in each cycle
setThreadAction :: (IO ()) -> Thread -> IO ()
setThreadAction f thread
  = do
    modifyMVar thread $ \thd -> return (thd {thd_Action = f},())


-- | Sets the error handler. It is activated, when the action function
--   will raise an exception.
setThreadErrorHandler :: (IO ()) -> Thread -> IO ()
setThreadErrorHandler e thread
  = do
    modifyMVar thread $ \thd -> return (thd {thd_Error = e},())


-- | Starts the thread.
startThread :: Thread -> IO ()
startThread th
  = modifyMVar th $
      \thd ->
      do
      -- check, if thread is running (it has a threadId)
      servId' <- case (thd_Id thd) of
        i@(Just _) -> return i
        (Nothing) ->
          do
          -- if not running, start the loop
          i <- forkIO $ doAction th
          return (Just i)
      return (thd {thd_Id = servId', thd_Running = True},())
    where    
    -- the action loop
    doAction thread
      = do
        thd <- readMVar thread
        if (thd_Running thd)
          -- if thread is still running 
          then do
            {- 6.8 E.handle -}
            handleAll
             ( \ e -> do
                      -- if a normal exception occurs, the error handler should be excuted
                      warningM localLogger $ show e
                      thd_Error thd
                      doAction thread
             ) $
              do
              -- catch the exception which tells us to kill the dispatcher and kill it
              {- 6.8 E.catchDyn -}
              catchJust isThreadKilledException
               ( do
                 -- wait for next watch-cycle
                 if (isJust (thd_Delay thd)) 
                    then do threadDelay $ fromJust (thd_Delay thd)
                    else do return ()
                 -- do the action
                 thd_Action thd
                 -- and again
                 doAction thread
               )
               killHandler
          else do
            debugM localLogger $ "thread normally closed by himself"
            deleteThreadId thread
	where
        isThreadKilledException      		:: AsyncException -> Maybe ()
        isThreadKilledException ThreadKilled    = Just ()
        isThreadKilledException _               = Nothing

        killHandler :: () -> IO ()
        killHandler _
          = do
            debugM localLogger $ "thread normally closed by other thread "
            deleteThreadId thread
{- 6.8
        killHandler :: KillThreadException -> IO ()
        killHandler (KillThreadException i)
          = do
            debugM localLogger $ "thread normally closed by other thread " ++ show i
            deleteThreadId thread
-}
        deleteThreadId :: Thread -> IO ()
        deleteThreadId t
          = modifyMVar t $ \thd -> return (thd {thd_Id = Nothing, thd_Running = False},())


-- | Stops the thread. If the thread itself wants to stop from within the action
--   function, the current cycle will be executed till the end. So statements
--   after this function will still be executed.
stopThread :: Thread -> IO ()
stopThread thread
  = do
    me <- myThreadId
    him <- withMVar thread $ \thd -> return $ thd_Id thd
    if (isJust him) 
      then do
        let he = fromJust him
        if (me == he) 
          -- if stop is called from the thread, we want to finish our work
          then do 
            modifyMVar thread $ \thd -> return (thd {thd_Running = False},())
            -- else we kill it the unfriendly way...
          else do
            {- 6.8: E.throwDynTo he (KillThreadException me) -}
            killThread he
      else do
        return ()