{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}

module Logging.Monad.Internal
  ( LoggingT
  , runLoggingT
  , log
  ) where


import           Control.Exception           (SomeException, bracket_)
import           Control.Lens                (view)
import           Control.Monad
import           Control.Monad.IO.Class      (MonadIO (..))
import           Control.Monad.Reader
import           Data.Default
import           Data.Generics.Product.Typed
import           Data.IORef
import           Data.List                   (dropWhileEnd, group)
import           Data.Map.Lazy               (elems, (!?))
import           Data.Time.Clock
import           Data.Time.Clock.POSIX
import           Data.Time.LocalTime
import           GHC.Conc                    (setUncaughtExceptionHandler)
import           Prelude                     hiding (filter, log)
import           System.FilePath
import           System.IO                   (stderr, stdout)
import           System.IO.Unsafe            (unsafePerformIO)

import           Logging.Prelude
import           Logging.Types


type LoggingT m a = ReaderT Manager m a


runLoggingT :: MonadIO m => LoggingT m a -> Manager -> m a
runLoggingT = runReaderT


log :: MonadIO m
    => Logger -> Level -> String -> (String, String, String, Int)
    -> LoggingT m ()
log logger level message location = do
    manager@Manager{..} <- ask
    asctime <- lift $ liftIO $ getZonedTime

    let (pathname, pkgname, modulename, lineno) = location
        filename = takeFileName pathname
        utctime = zonedTimeToUTC asctime
        diffTime = utcTimeToPOSIXSeconds utctime
        created = timestamp diffTime
        msecs = microseconds diffTime

    when (not disabled) $ lift $ liftIO $ process logger manager $
      LogRecord logger level message pathname filename pkgname modulename
                lineno asctime utctime created msecs
  where
    process :: Logger -> Manager -> LogRecord -> IO ()
    process logger manager rcd =
      case lookupSink logger manager of
        Just sink@Sink{..} -> do
          when (isSinkEnabledFor sink rcd) $ callHandlers handlers rcd
          let parentLogger = parent logger
              shouldPropagate = propagate && logger /= parentLogger
          when shouldPropagate $ process parentLogger manager rcd
        Nothing -> process (parent logger) manager rcd

    parent :: Logger -> Logger
    parent = dropWhileEnd (== '.') . dropWhileEnd (/= '.')

    lookupSink :: Logger -> Manager -> Maybe Sink
    lookupSink logger manager@Manager{root=root@Sink{logger=rootLogger}, ..}
      | logger `elem` ["", rootLogger] = Just root
      | otherwise = sinks !? logger

    callHandlers :: [SomeHandler] -> LogRecord -> IO ()
    callHandlers handlers rcd = forM_ handlers $ \hdl ->
      Logging.Types.handle hdl rcd

    isSinkEnabledFor :: Sink -> LogRecord -> Bool
    isSinkEnabledFor sink@Sink{..} rcd@LogRecord{level=level'}
      | disabled = False
      | level' < level = False
      | otherwise = filter sink rcd