-- | 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 "