{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeApplications #-}

module RON.Epoch (
    EpochClock,
    getCurrentEpochTime,
    localEpochTimeFromUnix,
    runEpochClock,
    runEpochClockFromCurrentTime,
) where

import           Control.Monad.IO.Class (MonadIO)
import           Control.Monad.Reader (ReaderT (ReaderT), reader, runReaderT)
import           Data.IORef (IORef, atomicModifyIORef', newIORef)
import           Data.Time.Clock.POSIX (getPOSIXTime)
import           Data.Word (Word64)

import           RON.Event (EpochEvent (EpochEvent), EpochTime,
                            LocalTime (TEpoch), ReplicaClock, ReplicaId,
                            advance, getEvents, getPid)
import           RON.Internal.Word (leastSignificant60, ls60, word60add)

-- | Real epoch clock.
-- Uses kind of global variable to ensure strict monotonicity.
newtype EpochClock a = EpochClock (ReaderT (ReplicaId, IORef EpochTime) IO a)
    deriving (Applicative, Functor, Monad, MonadIO)

instance ReplicaClock EpochClock where
    getPid = EpochClock $ reader fst

    advance time = EpochClock $ ReaderT $ \(_pid, timeVar) ->
        atomicModifyIORef' timeVar $ \t0 -> (max time t0, ())

    getEvents n0 = EpochClock $ ReaderT $ \(pid, timeVar) -> do
        let n = max n0 $ ls60 1
        realTime <- getCurrentEpochTime
        timeRangeStart <- atomicModifyIORef' timeVar $ \timeCur ->
            let timeRangeStart = max realTime $ succ timeCur
            in (timeRangeStart `word60add` pred n, timeRangeStart)
        pure
            [ EpochEvent t pid
            | t <- [timeRangeStart .. timeRangeStart `word60add` pred n]
            ]

-- | Run 'EpochClock' action with explicit time variable.
runEpochClock :: ReplicaId -> IORef EpochTime -> EpochClock a -> IO a
runEpochClock replicaId timeVar (EpochClock action) =
    runReaderT action (replicaId, timeVar)

-- | Like 'runEpochClock', but initialize time variable with current wall time.
runEpochClockFromCurrentTime :: ReplicaId -> EpochClock a -> IO a
runEpochClockFromCurrentTime replicaId clock = do
    time <- getCurrentEpochTime
    timeVar <- newIORef time
    runEpochClock replicaId timeVar clock

-- | Get current time in 'EpochTime' format (with 100 ns resolution).
-- Monotonicity is not guaranteed.
getCurrentEpochTime :: IO EpochTime
getCurrentEpochTime
    =   epochTimeFromUnix @Word64
    .   round
    .   (* 10000000)
    <$> getPOSIXTime

-- | Convert unix time in hundreds of milliseconds to RFC 4122 time.
epochTimeFromUnix :: Integral int => int -> EpochTime
epochTimeFromUnix
    =   leastSignificant60
    .   (+ 0x01B21DD213814000)
        -- the difference between Unix epoch and UUID epoch;
        -- the constant is taken from RFC 4122

-- | Convert unix time in hundreds of milliseconds to RFC 4122 time.
localEpochTimeFromUnix :: Integral int => int -> LocalTime
localEpochTimeFromUnix = TEpoch . epochTimeFromUnix