-- | This modules contains support for logging.
--
-- @since 0.5.65
module B9.B9Logging
  ( Logger(..)
  , CommandIO
  , LoggerReader
  , withLogger
  , b9Log
  , traceL
  , dbgL
  , infoL
  , errorL
  , errorExitL
  )
where

import           B9.B9Config
import           B9.B9Error
import           Control.Eff
import           Control.Eff.Reader.Lazy
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Control    ( MonadBaseControl
                                                , liftBaseWith
                                                , restoreM
                                                )
import           Data.Maybe
import           Data.Time.Clock
import           Data.Time.Format
import qualified System.IO                     as SysIO
import           Text.Printf

-- | The logger to write log messages to.
--
-- @since 0.5.65
newtype Logger = MkLogger
  { logFileHandle :: Maybe SysIO.Handle
  }

-- | Effect that reads a 'Logger'.
--
-- @since 0.5.65
type LoggerReader = Reader Logger

-- | Lookup the selected 'getLogVerbosity' and '_logFile' from the 'B9Config'
-- and open it.
--
-- Then run the given action; if the action crashes, the log file will be closed.
--
-- @since 0.5.65
withLogger
  :: (MonadBaseControl IO (Eff e), MonadIO (Eff e), Member B9ConfigReader e)
  => Eff (LoggerReader ': e) a
  -> Eff e a
withLogger action = do
  lf       <- _logFile <$> getB9Config
  effState <- liftBaseWith $ \runInIO ->
    let fInIO = runInIO . flip runReader action . MkLogger
    in  maybe (fInIO Nothing)
              (\logf -> SysIO.withFile logf SysIO.AppendMode (fInIO . Just))
              lf
  restoreM effState

-- | Convenience type alias for 'Eff'ects that have a 'B9Config', a 'Logger', 'MonadIO' and 'MonadBaseControl'.
--
-- @since 0.5.65
type CommandIO e
  = ( MonadBaseControl IO (Eff e)
  , MonadIO (Eff e)
  , Member LoggerReader e
  , Member B9ConfigReader e
  )

traceL :: CommandIO e => String -> Eff e ()
traceL = b9Log LogTrace

dbgL :: CommandIO e => String -> Eff e ()
dbgL = b9Log LogDebug

infoL :: CommandIO e => String -> Eff e ()
infoL = b9Log LogInfo

errorL :: CommandIO e => String -> Eff e ()
errorL = b9Log LogError

errorExitL :: (CommandIO e, Member ExcB9 e) => String -> Eff e a
errorExitL e = b9Log LogError e >> throwB9Error e

b9Log :: CommandIO e => LogLevel -> String -> Eff e ()
b9Log level msg = do
  lv  <- getLogVerbosity
  lfh <- logFileHandle <$> ask
  liftIO $ logImpl lv lfh level msg

logImpl :: Maybe LogLevel -> Maybe SysIO.Handle -> LogLevel -> String -> IO ()
logImpl minLevel mh level msg = do
  lm <- formatLogMsg level msg
  when (isJust minLevel && level >= fromJust minLevel) (putStr lm)
  when (isJust mh) $ do
    SysIO.hPutStr (fromJust mh) lm
    SysIO.hFlush (fromJust mh)

formatLogMsg :: LogLevel -> String -> IO String
formatLogMsg l msg = do
  u <- getCurrentTime
  let time = formatTime defaultTimeLocale "%H:%M:%S" u
  return $ unlines $ printf "[%s] %s - %s" (printLevel l) time <$> lines msg

printLevel :: LogLevel -> String
printLevel l = case l of
  LogNothing -> "NOTHING"
  LogError   -> " ERROR "
  LogInfo    -> " INFO  "
  LogDebug   -> " DEBUG "
  LogTrace   -> " TRACE "