{-# OPTIONS_GHC -fno-warn-orphans #-}
module Yam.Logger(
  -- * Logger Function
    withLogger
  , getLogger
  , extensionLogKey
  , LogConfig(..)
  , HasLogger
  , LogFuncHolder(..)
  , VaultHolder(..)
  ) where
import           Control.Monad.IO.Unlift
import           Control.Monad.Logger.CallStack
import qualified Data.Vault.Lazy                as L
import           Data.Word
import           Salak
import           System.IO.Unsafe               (unsafePerformIO)
import           System.Log.FastLogger
import           Yam.Prelude

instance FromEnumProp LogLevel where
  fromEnumProp "debug" = Right   LevelDebug
  fromEnumProp "info"  = Right   LevelInfo
  fromEnumProp "warn"  = Right   LevelWarn
  fromEnumProp "error" = Right   LevelError
  fromEnumProp _       = Right $ LevelOther "fatal"

{-# INLINE toStr #-}
toStr :: LogLevel -> LogStr
toStr LevelDebug     = "DEBUG"
toStr LevelInfo      = " INFO"
toStr LevelWarn      = " WARN"
toStr LevelError     = "ERROR"
toStr (LevelOther l) = toLogStr l

-- | Logger config
data LogConfig = LogConfig
  { bufferSize    :: Word16   -- ^ Logger buffer size.
  , file          :: FilePath -- ^ Logger file path.
  , maxSize       :: Word32   -- ^ Max logger file size.
  , rotateHistory :: Word16   -- ^ Max number of logger files should be reserved.
  , level         :: LogLevel -- ^ Log level to show.
  } deriving (Eq, Show)

instance Default LogConfig where
  def = LogConfig 4096 "" 10485760 256 LevelInfo

instance FromProp LogConfig where
  fromProp = LogConfig
    <$> "buffer-size" .?: bufferSize
    <*> "file"        .?: file
    <*> "max-size"    .?: maxSize
    <*> "max-history" .?: rotateHistory
    <*> "level"       .?: level

newLogger :: Text -> IO LogConfig -> IO (LogFunc, IO ())
newLogger name lc = do
  LogConfig{..} <- lc
  tc            <- newTimeCache "%Y-%m-%d %T"
  let ft = if file == ""
            then LogStdout $ fromIntegral bufferSize
            else LogFile (FileLogSpec file (toInteger maxSize) (fromIntegral rotateHistory)) $ fromIntegral bufferSize
      ln = " [" <> toLogStr name <> "] "
  (l,close) <- newTimedFastLogger tc ft
  return (toLogger ln l, close)
  where
    toLogger xn f Loc{..} _ ll s = do
      c <- lc
      when (level c <= ll) $ f $ \t ->
        let locate = if ll /= LevelError then "" else " @" <> toLogStr loc_filename <> toLogStr (show loc_start)
        in toLogStr t <> " " <> toStr ll <> xn <> toLogStr loc_module <> locate <> " - " <> s <> "\n"

withLogger :: (MonadUnliftIO m) => Text -> IO LogConfig -> (LogFunc -> LoggingT m a) -> m a
withLogger n lc action = do
  f <- askRunInIO
  liftIO $ bracket (newLogger n lc) (snd) $ f . runLoggingT (askLoggerIO >>= action) . fst

addTrace :: LogFunc -> Text -> LogFunc
addTrace f tid a b c d = let p = "[" <> toLogStr tid <> "] " in f a b c (p <> d)

{-# NOINLINE extensionLogKey #-}
extensionLogKey :: L.Key Text
extensionLogKey = unsafePerformIO L.newKey

getLogger :: Maybe VaultHolder -> LogFuncHolder -> LogFunc
getLogger (Just (VH vault)) (LF logger) =
  let {-# INLINE nlf #-}
      nlf x (Just t) = addTrace x t
      nlf x _        = x
  in nlf logger $ L.lookup extensionLogKey vault
getLogger _ (LF logger) = logger

-- | Holder for 'LogFunc'
newtype LogFuncHolder = LF LogFunc
-- | Holder for 'Vault'
newtype VaultHolder   = VH L.Vault

-- | Context with logger.
type HasLogger cxt = (HasContextEntry cxt LogFuncHolder, TryContextEntry cxt VaultHolder)