{-# LANGUAGE DeriveDataTypeable #-}
module Distribution.Client.Cron
  ( cron
  , Signal(..)
  , ReceivedSignal(..)
  , rethrowSignalsAsExceptions
  ) where

import Control.Monad (forM_)
import Control.Exception (Exception)
import Control.Concurrent (myThreadId, threadDelay, throwTo)
import System.Random (randomRIO)
import Data.Time.Format (defaultTimeLocale, formatTime)
import Data.Time.Clock (UTCTime, getCurrentTime, addUTCTime)
import Data.Time.LocalTime (getCurrentTimeZone, utcToZonedTime)
import Data.Typeable (Typeable)

import qualified System.Posix.Signals as Posix

import Distribution.Verbosity (Verbosity)
import Distribution.Simple.Utils hiding (warn)

data ReceivedSignal = ReceivedSignal Signal UTCTime
  deriving (Show, Typeable)

data Signal = SIGABRT
            | SIGINT
            | SIGQUIT
            | SIGTERM
  deriving (Show, Typeable)

instance Exception ReceivedSignal


-- | "Re"throw signals as exceptions to the invoking thread
rethrowSignalsAsExceptions :: [Signal] -> IO ()
rethrowSignalsAsExceptions signals = do
  tid <- myThreadId
  forM_ signals $ \s ->
    let handler = do
          time <- getCurrentTime
          throwTo tid (ReceivedSignal s time)
    in Posix.installHandler (toPosixSignal s) (Posix.Catch handler) Nothing

toPosixSignal :: Signal -> Posix.Signal
toPosixSignal SIGABRT = Posix.sigABRT
toPosixSignal SIGINT  = Posix.sigINT
toPosixSignal SIGQUIT = Posix.sigQUIT
toPosixSignal SIGTERM = Posix.sigTERM

-- | @cron verbosity interval act@ runs @act@ over and over with
-- the specified interval.
cron :: Verbosity -> Int -> (a -> IO a) -> (a -> IO ())
cron verbosity interval action x = do
    x' <- action x

    interval' <- pertabate interval
    logNextSyncMessage interval'
    wait interval'
    cron verbosity interval action x'

  where
    -- to stop all mirror clients hitting the server at exactly the same time
    -- we randomly adjust the wait time by +/- 10%
    pertabate i = let deviation = i `div` 10
                   in randomRIO (i + deviation, i - deviation)

    -- Annoyingly, threadDelay takes an Int number of microseconds, so we cannot
    -- wait much longer than an hour. So have to wait repeatedly. Sigh.
    wait minutes | minutes > 60 = do threadDelay (60 * 60 * 1000000)
                                     wait (minutes - 60)
                 | otherwise    = threadDelay (minutes * 60 * 1000000)

    logNextSyncMessage minutes = do
      now       <- getCurrentTime
      tz        <- getCurrentTimeZone
      let nextSync = addUTCTime (fromIntegral (60 * minutes)) now
      notice verbosity $
          "Next try will be in " ++ show minutes ++ " minutes, at "
       ++ formatTime defaultTimeLocale "%R %Z" (utcToZonedTime tz nextSync)
