{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
module Log.Logger (
    Logger
  , mkLogger
  , mkBulkLogger
  , execLogger
  , waitForLogger
  ) where

import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.IORef
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.IO as T

import Log.Data
import Log.Internal.Logger

-- | Make 'Logger' that consumes one queued message at a time.
mkLogger :: T.Text -> (LogMessage -> IO ()) -> IO Logger
mkLogger = mkLoggerImpl
  newTQueueIO isEmptyTQueue readTQueue writeTQueue $ return ()

-- | Make 'Logger' that consumes all queued messages once per second.
mkBulkLogger :: T.Text -> ([LogMessage] -> IO ()) -> IO Logger
mkBulkLogger = mkLoggerImpl
  newSQueueIO isEmptySQueue readSQueue writeSQueue $ threadDelay 1000000

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

-- | Simple STM based queue.
newtype SQueue a = SQueue (TVar [a])

-- | Create an instance of 'SQueue'.
newSQueueIO :: IO (SQueue a)
newSQueueIO = SQueue <$> newTVarIO []

-- | Check if an 'SQueue' is empty.
isEmptySQueue :: SQueue a -> STM Bool
isEmptySQueue (SQueue queue) = null <$> readTVar queue

-- | Read all the values stored in an 'SQueue'.
readSQueue :: SQueue a -> STM [a]
readSQueue (SQueue queue) = do
  elems <- readTVar queue
  when (null elems) retry
  writeTVar queue []
  return $ reverse elems

-- | Write a value to an 'SQueue'.
writeSQueue :: SQueue a -> a -> STM ()
writeSQueue (SQueue queue) a = modifyTVar queue (a :)

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

mkLoggerImpl :: IO queue
             -> (queue -> STM Bool)
             -> (queue -> STM msgs)
             -> (queue -> LogMessage -> STM ())
             -> IO ()
             -> T.Text
             -> (msgs -> IO ())
             -> IO Logger
mkLoggerImpl newQueue isQueueEmpty readQueue writeQueue afterExecDo name exec = do
  (queue, inProgress) <- (,) <$> newQueue <*> newTVarIO False
  finalizer <- newIORef ()
  mask $ \release -> do
    tid <- forkIO . (`finally` printLoggerTerminated) . release . forever $ do
      msgs <- atomically $ do
        writeTVar inProgress True
        readQueue queue
      exec msgs
      atomically $ writeTVar inProgress False
      afterExecDo
    let waitForWrite = do
          isEmpty <- isQueueEmpty queue
          isInProgress <- readTVar inProgress
          when (not isEmpty || isInProgress) retry
    void . mkWeakIORef finalizer $ do
      atomically waitForWrite
      killThread tid
    return Logger {
      loggerWriteMessage = atomically . writeQueue queue
    , loggerWaitForWrite = waitForWrite
    , loggerFinalizers = [finalizer]
    }
  where
    printLoggerTerminated = T.putStrLn $ name <> ": logger thread terminated"