{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels      #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE ViewPatterns          #-}
module Colog.Message
       ( 
         Message (..)
       , log
       , logDebug
       , logInfo
       , logWarning
       , logError
       , logException
       , fmtMessage
         
       , FieldType
       , MessageField (..)
       , FieldMap
       , defaultFieldMap
       , RichMessage
       , fmtRichMessageDefault
       , upgradeMessageAction
       ) where
import Control.Concurrent (ThreadId, myThreadId)
import Control.Exception (displayException)
import Data.Time.Clock (UTCTime, getCurrentTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import Data.TypeRepMap (TypeRepMap)
import GHC.OverloadedLabels (IsLabel (..))
import GHC.Stack (SrcLoc (..))
import GHC.TypeLits (KnownSymbol, Symbol)
import System.Console.ANSI (Color (..), ColorIntensity (Vivid), ConsoleLayer (Foreground), SGR (..),
                            setSGRCode)
import Colog.Core (LogAction, Severity (..), cmap)
import Colog.Monad (WithLog, logMsg)
import qualified Data.TypeRepMap as TM
data Message = Message
    { messageSeverity :: !Severity
    , messageStack    :: !CallStack
    , messageText     :: !Text
    }
log :: WithLog env Message m => Severity -> Text -> m ()
log messageSeverity messageText =
    withFrozenCallStack (logMsg Message{ messageStack = callStack, .. })
logDebug :: WithLog env Message m => Text -> m ()
logDebug = withFrozenCallStack (log Debug)
logInfo :: WithLog env Message m => Text -> m ()
logInfo = withFrozenCallStack (log Info)
logWarning :: WithLog env Message m => Text -> m ()
logWarning = withFrozenCallStack (log Warning)
logError :: WithLog env Message m => Text -> m ()
logError = withFrozenCallStack (log Error)
logException :: forall e m env . (WithLog env Message m, Exception e) => e -> m ()
logException = withFrozenCallStack (logError . toText . displayException)
fmtMessage :: Message -> Text
fmtMessage Message{..} =
    showSeverity messageSeverity
 <> showSourceLoc messageStack
 <> messageText
showSeverity :: Severity -> Text
showSeverity = \case
    Debug   -> color Green  "[Debug]   "
    Info    -> color Blue   "[Info]    "
    Warning -> color Yellow "[Warning] "
    Error   -> color Red    "[Error]   "
 where
    color :: Color -> Text -> Text
    color c txt = toText (setSGRCode [SetColor Foreground Vivid c])
        <> txt
        <> toText (setSGRCode [Reset])
square :: Text -> Text
square t = "[" <> t <> "] "
showSourceLoc :: CallStack -> Text
showSourceLoc cs = square showCallStack
  where
    showCallStack :: Text
    showCallStack = case getCallStack cs of
        []                             -> "<unknown loc>"
        [(name, loc)]                  -> showLoc name loc
        (_, loc) : (callerName, _) : _ -> showLoc callerName loc
    showLoc :: String -> SrcLoc -> Text
    showLoc name SrcLoc{..} =
        toText srcLocModule <> "." <> toText name <> "#" <> show srcLocStartLine
type family FieldType (fieldName :: Symbol) :: Type
type instance FieldType "threadId" = ThreadId
type instance FieldType "utcTime"  = UTCTime
newtype MessageField (m :: Type -> Type) (fieldName :: Symbol) where
    MessageField
        :: forall fieldName m .
           { unMesssageField :: m (FieldType fieldName) }
        -> MessageField m fieldName
instance (KnownSymbol fieldName, a ~ m (FieldType fieldName))
      => IsLabel fieldName (a -> TM.WrapTypeable (MessageField m)) where
    fromLabel field = TM.WrapTypeable $ MessageField @fieldName field
extractField
    :: Applicative m
    => Maybe (MessageField m fieldName)
    -> m (Maybe (FieldType fieldName))
extractField = traverse unMesssageField
type FieldMap (m :: Type -> Type) = TypeRepMap (MessageField m)
defaultFieldMap :: MonadIO m => FieldMap m
defaultFieldMap = fromList
    [ #threadId (liftIO myThreadId)
    , #utcTime  (liftIO getCurrentTime)
    ]
data RichMessage (m :: Type -> Type) = RichMessage
    { richMessageMsg :: {-# UNPACK #-} !Message
    , richMessageMap :: {-# UNPACK #-} !(FieldMap m)
    }
fmtRichMessageDefault :: MonadIO m => RichMessage m -> m Text
fmtRichMessageDefault RichMessage{..} = do
    maybeThreadId <- extractField $ TM.lookup @"threadId" richMessageMap
    maybeUtcTime  <- extractField $ TM.lookup @"utcTime"  richMessageMap
    pure $ formatRichMessage maybeThreadId maybeUtcTime richMessageMsg
  where
    formatRichMessage :: Maybe ThreadId -> Maybe UTCTime -> Message -> Text
    formatRichMessage (maybe "" showThreadId -> thread) (maybe "" showTime -> time) Message{..} =
        showSeverity messageSeverity
     <> time
     <> showSourceLoc messageStack
     <> thread
     <> messageText
    showTime :: UTCTime -> Text
    showTime t = square $ toText $
          formatTime defaultTimeLocale "%H:%M:%S." t
       ++ take 3 (formatTime defaultTimeLocale "%q" t)
       ++ formatTime defaultTimeLocale " %e %b %Y %Z" t
    showThreadId :: ThreadId -> Text
    showThreadId = square . show
upgradeMessageAction
    :: forall m .
       FieldMap m
    -> LogAction m (RichMessage m)
    -> LogAction m Message
upgradeMessageAction fieldMap = cmap addMap
  where
    addMap :: Message -> RichMessage m
    addMap msg = RichMessage msg fieldMap