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

module Logging.Internal
  ( run
  , log
  , stderrHandler
  , stdoutHandler
  , defaultRoot
  ) where

import           Control.Exception           (SomeException, bracket_)
import           Control.Lens                (view)
import           Control.Monad               (forM_, void, when)
import           Control.Monad.IO.Class      (MonadIO (..))
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.LocalTime
import           GHC.Conc                    (setUncaughtExceptionHandler)
import           Prelude                     hiding (filter, log)
import           System.IO                   (stderr, stdout)
import           System.IO.Unsafe            (unsafePerformIO)

import           Logging.Types

{-# NOINLINE _mgr #-}
_mgr :: IORef Manager
_mgr = unsafePerformIO $ newIORef undefined


-- |Run a logging environment.
--
-- You should always write you application inside a logging environment.
--
-- 1. rename "main" function to "originMain" (or whatever you call it)
-- 2. write "main" as below
--
-- > main :: IO ()
-- > main = run manager originMain
-- > ...
--
run :: Manager -> IO a -> IO a
run mgr@Manager{..} io = do
    when catchUncaughtException $ setUncaughtExceptionHandler uceHandler
    bracket_ (atomicWriteIORef _mgr mgr >> start) shutdown io
  where
    unknownLoc = ("unknown file", "unknown package", "unknown module", 0)

    uceHandler :: SomeException -> IO ()
    uceHandler e = log "" "ERROR" (show e) unknownLoc

    allHandlers = map head $ group $
      concat [ handlers s | s <- (root : (elems sinks)) ]

    start :: IO ()
    start = forM_ allHandlers open

    shutdown :: IO ()
    shutdown = forM_ allHandlers close


-- |Low-level logging routine which creates a LogRecord and then calls
-- all the handlers of this logger to handle the record.
log :: MonadIO m
     => Logger -> Level -> String -> (String, String, String, Int) -> m ()
log logger level message location = liftIO $ do
    mgr@Manager{..} <- readIORef _mgr
    created <- getZonedTime

    let (file, package, modulename, lineno) = location

    when (not disabled) $ process logger mgr $
      LogRecord logger level message file package modulename lineno created
  where
    process :: Logger -> Manager -> LogRecord -> IO ()
    process logger mgr rcd =
      case lookupSink logger mgr of
        Just sink@Sink{..} -> do
          when (isSinkEnabledFor sink rcd) $ callHandlers handlers rcd
          let parentLogger = parent logger
              shouldPropagate = propagate && logger /= parentLogger
          when shouldPropagate $ process parentLogger mgr rcd
        Nothing -> process (parent logger) mgr rcd

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

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

    callHandlers :: [SomeHandler] -> LogRecord -> IO ()
    callHandlers handlers rcd = forM_ handlers $ \hdl ->
      when (isHandlerEnableFor hdl rcd) $ void $ 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

    isHandlerEnableFor :: SomeHandler -> LogRecord -> Bool
    isHandlerEnableFor hdl rcd@LogRecord{level=level'}
      | level' < (view (typed @Level) hdl) = False
      | otherwise = filter (view (typed @Filterer) hdl) rcd


-- |A 'StreamHandler' bound to 'stderr'
stderrHandler :: StreamHandler
stderrHandler = StreamHandler stderr def [] def

-- |A 'StreamHandler' bound to 'stdout'
stdoutHandler :: StreamHandler
stdoutHandler = StreamHandler stdout def [] def

{-# NOINLINE defaultRoot #-}
-- |Default root sink which is used by 'jsonToManager' when __root__ is missed.
--
-- You can use it when you make 'Manager' manually.
defaultRoot :: Sink
defaultRoot = Sink "" "DEBUG" [] [toHandler stderrHandler] False False