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

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


import           Control.Concurrent
import           Control.Exception           (SomeException, bracket_)
import           Control.Monad
import           Control.Monad.IO.Class      (MonadIO (..))
import           Control.Monad.Reader
import           Data.Aeson
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           Lens.Micro.Extras           (view)
import           Prelude                     hiding (filter, log)
import           System.FilePath
import           System.IO                   (stderr, stdout)
import           System.IO.Unsafe            (unsafePerformIO)

import           Logging.Class
import           Logging.Level
import           Logging.Logger
import           Logging.Manager
import           Logging.Prelude
import           Logging.Record
import           Logging.Sink


type LoggingT m a = ReaderT Manager m a


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


runIO :: MonadIO m => IO a -> ReaderT Manager m a
runIO = lift . liftIO


log :: (MonadIO m, IsMessage s, ToJSON c)
    => Logger -> Level -> s -> c -> (String, String, String, Int)
    -> LoggingT m ()
log logger level msg ctx location = do
    manager@Manager{..} <- ask
    utctime <- runIO getCurrentTime
    thread <- runIO myThreadId

    let (pathname, pkgname, modulename, lineno) = location
        filename = takeFileName pathname
        asctime = utcToZonedTime timezone utctime
        diffTime = utcTimeToPOSIXSeconds utctime
        created = timestamp diffTime
        msecs = milliseconds diffTime - (seconds diffTime * 1000)
        message = toMessage msg
        context = toJSON ctx

    when (not disabled) $ runIO $ process logger manager LogRecord{..}
  where
    process :: Logger -> Manager -> LogRecord -> IO ()
    process logger manager rcd =
      case lookupSink logger manager of
        Just sink@Sink{..} -> do
          when (filter 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 (`handle` rcd)