{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE MultiWayIf                #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TemplateHaskell           #-}
{-# LANGUAGE TupleSections             #-}

{- |
   Module     : System.Log.Logger
   Copyright  : Copyright (C) 2004-2011 John Goerzen
   License    : BSD3

   Maintainer : John Goerzen <jgoerzen@complete.org>
   Stability  : provisional
   Portability: portable

Haskell Logging Framework, Primary Interface

Written by John Goerzen, jgoerzen\@complete.org

This module is a modification of "System.Log.Logger" of 'hslogger'
library. Unless proper description is written here, please use the
original documentation available on hackage/hslogger.
-}

module System.Wlog.IOLogger
       (
         -- * Basic Types
         Logger
         -- ** Re-Exported from System.Wlog
       , Severity(..)

         -- * Logging Messages
         -- ** Basic
       , logM
       , logMCond
         -- ** Utility Functions
       , removeAllHandlers

         -- * Logger Manipulation
         -- ** Finding ∨ Creating Loggers
       , getLogger, getRootLogger, rootLoggerName
         -- ** Modifying Loggers
       , addHandler, removeHandler, setHandlers
       , getLevel, setLevel, clearLevel
         -- ** Severity settings
       , setSeverities, setSeveritiesMaybe
         -- ** Saving Your Changes
       , saveGlobalLogger
       , updateGlobalLogger
       , setPrefix
       , retrieveLogContent
       ) where

import Universum

import Control.Concurrent.MVar (modifyMVar, modifyMVar_, withMVar)
import Data.Maybe (fromJust)
import Lens.Micro.Platform (makeLenses)
import System.FilePath ((</>))
import System.IO.Unsafe (unsafePerformIO)

import System.Wlog.LoggerName (LoggerName (..))
import System.Wlog.LogHandler (LogHandler (getTag), LogHandlerTag (HandlerFilelike), close,
                               readBack)
import System.Wlog.Severity (LogRecord (..), Severities, Severity (..), warningPlus)


import qualified Data.Map as M
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Text.IO as TIO

import qualified System.Wlog.LogHandler (logHandlerMessage)

---------------------------------------------------------------------------
-- Basic logger types
---------------------------------------------------------------------------

data HandlerT = forall a. LogHandler a => HandlerT a

data Logger = Logger
    { _lLevel    :: Maybe Severities
    , _lHandlers :: [HandlerT]
    , _lName     :: LoggerName
    } deriving (Generic)

makeLenses ''Logger

type LogTree = Map LoggerName Logger

data LogInternalState = LogInternalState
    { liTree   :: LogTree
    , liPrefix :: Maybe FilePath
    } deriving (Generic)

---------------------------------------------------------------------------
-- Utilities
---------------------------------------------------------------------------

-- | The name of the root logger, which is always defined and present
-- on the system.
rootLoggerName :: LoggerName
rootLoggerName = mempty

---------------------------------------------------------------------------
-- Logger Tree Storage
---------------------------------------------------------------------------

-- | The log tree. Initialize it with a default root logger.
{-# NOINLINE logInternalState #-}
logInternalState :: MVar LogInternalState
-- note: only kick up tree if handled locally
logInternalState = unsafePerformIO $ do
    let liTree = M.singleton rootLoggerName
                 Logger { _lLevel = Just warningPlus
                        , _lName = ""
                        , _lHandlers = []}
        liPrefix = Nothing
    newMVar LogInternalState {..}

{-
Given a name, return all components of it, starting from the root.

Example return value:

λ> componentsOfName (LoggerName "a.b.c")
[LoggerName {getLoggerName = ""},
 LoggerName {getLoggerName = "a"},
 LoggerName {getLoggerName = "a.b"},
 LoggerName {getLoggerName = "a.b.c"}]

-}
componentsOfName :: LoggerName -> [LoggerName]
componentsOfName (LoggerName name) =
    rootLoggerName : (LoggerName <$> joinComp (T.splitOn "." name))
  where
    joinComp :: [Text] -> [Text]
    joinComp = map (T.intercalate ".") . drop 1 . inits

---------------------------------------------------------------------------
-- Logging With Location
---------------------------------------------------------------------------

-- | Log a message using the given logger at a given priority.
logM :: MonadIO m
     => LoggerName -- ^ Name of the logger to use
     -> Severity   -- ^ Severity of this message
     -> Text       -- ^ The log text itself
     -> m ()
logM logname sev msg = do
    l <- getLogger logname
    handle l (LR sev msg) (const True)

logMCond :: MonadIO m => LoggerName -> Severity -> Text -> (LogHandlerTag -> Bool) -> m ()
logMCond logname sev msg cond = do
    l <- getLogger logname
    handle l (LR sev msg) cond

---------------------------------------------------------------------------
-- Public Logger Interaction Support
---------------------------------------------------------------------------

-- | Returns the logger for the given name. If no logger with that
-- name exists, creates new loggers and any necessary parent loggers,
-- with no connected handlers.
getLogger :: MonadIO m => LoggerName -> m Logger
getLogger lname = liftIO $ modifyMVar logInternalState $ \lt@LogInternalState{..} ->
    case M.lookup lname liTree of
      Just x ->  return (lt, x) -- A logger exists; return it and leave tree
      Nothing -> do
          -- Add logger(s).  Then call myself to retrieve it.
          let newlt = createLoggers liTree
          let result = fromJust $ M.lookup lname newlt
          return (LogInternalState newlt liPrefix, result)
  where
    createLoggers :: LogTree -> LogTree
    createLoggers lt = foldl' addLoggerToTree lt (componentsOfName lname)

    addLoggerToTree ::  LogTree -> LoggerName -> LogTree
    addLoggerToTree lt x = if M.member x lt then lt else M.insert x (Logger Nothing [] x) lt

-- | Returns the root logger.
getRootLogger :: MonadIO m => m Logger
getRootLogger = getLogger rootLoggerName

-- | Handle a log request.
--
-- 1. Find the deepest logger that has non-zero handlers to handle log message.
-- 2. Validate if message severity matches this logger severity
-- 3. Handle it by all parent handlers.
handle :: forall m. MonadIO m => Logger -> LogRecord -> (LogHandlerTag -> Bool) -> m ()
handle l lr@(LR sev _) handlerFilter =
    traverseAndLog False =<< parentLoggers nm
  where
    nm :: LoggerName
    nm = view lName l

    -- Returns all loggers, root logger last
    parentLoggers :: LoggerName -> m [Logger]
    parentLoggers = fmap reverse . mapM getLogger . componentsOfName

    -- Tries to log the message into handlers. sevFiltPassed variable
    -- denotes the "has log message passed through the first severity
    -- filter". We only apply severity filter once, the first time we
    -- encounter it.
    traverseAndLog :: Bool -> [Logger] -> m ()
    traverseAndLog sevFiltPassed lgs = whenNotNull lgs $ \(x:|xs) -> do
        let doLog n = do
                forM_ (x ^. lHandlers) callHandler
                traverseAndLog n xs
        if sevFiltPassed
        then doLog sevFiltPassed
        else case x ^. lLevel of
           -- We haven't yet met severity filter, so we still traverse
           Nothing   -> doLog sevFiltPassed
           -- If we didn't pass the first encountered filter check, we
           -- don't proceed with logging. If we pass, we set
           -- sevFiltPassed to true for next iterations.
           (Just lp) -> when (sev `Set.member` lp) $ doLog True

    callHandler :: HandlerT -> m ()
    callHandler (HandlerT x) =
        when (handlerFilter $ getTag x) $
            System.Wlog.LogHandler.logHandlerMessage x lr nm

-- | Sets file prefix to 'LogInternalState'.
setPrefix :: MonadIO m => Maybe FilePath -> m ()
setPrefix p = liftIO
            $ modifyMVar_ logInternalState
            $ \li -> pure $ li { liPrefix = p }

-- | Add handler to 'Logger'.  Returns a new 'Logger'.
addHandler :: LogHandler a => a -> Logger -> Logger
addHandler h = lHandlers %~ (HandlerT h:)

-- | Remove a handler from the 'Logger'.  Handlers are removed in the reverse
-- order they were added, so the following property holds for any 'LogHandler'
-- @h@:
--
-- > removeHandler . addHandler h = id
--
-- If no handlers are associated with the 'Logger', it is returned unchanged.
--
-- The root logger's default handler that writes every message to stderr can
-- be removed by using this function before any handlers have been added
-- to the root logger:
--
-- > updateGlobalLogger rootLoggerName removeHandler
removeHandler :: Logger -> Logger
removeHandler = lHandlers %~ drop 1

-- | Set the 'Logger'\'s list of handlers to the list supplied.
-- All existing handlers are removed first.
setHandlers :: LogHandler a => [a] -> Logger -> Logger
setHandlers hl = lHandlers .~ map HandlerT hl

-- | Returns the "level" of the logger.  Items beneath this
-- level will be ignored.
getLevel :: Logger -> Maybe Severities
getLevel = _lLevel

-- | Sets the "level" of the 'Logger'.  Returns a new
-- 'Logger' object with the new level.
setLevel :: Severities -> Logger -> Logger
setLevel p = lLevel .~ Just p

-- | Clears the "level" of the 'Logger'.  It will now inherit the level of
-- | its parent.
clearLevel :: Logger -> Logger
clearLevel = lLevel .~ Nothing

-- | Set severities for given logger. By default parent's severities are used.
setSeverities :: MonadIO m => LoggerName -> Severities -> m ()
setSeverities name = updateGlobalLogger name . setLevel

-- | Set or clear severities.
setSeveritiesMaybe
    :: MonadIO m
    => LoggerName -> Maybe Severities -> m ()
setSeveritiesMaybe name Nothing  = updateGlobalLogger name clearLevel
setSeveritiesMaybe n    (Just x) = setSeverities n x

-- | Updates the global record for the given logger to take into
-- account any changes you may have made.
saveGlobalLogger :: MonadIO m => Logger -> m ()
saveGlobalLogger l = liftIO $
    modifyMVar_ logInternalState $ \LogInternalState{..} ->
    pure $ LogInternalState (M.insert (view lName l) l liTree) liPrefix

-- | Helps you make changes on the given logger.  Takes a function
-- that makes changes and writes those changes back to the global
-- database.  Here's an example from above (\"s\" is a 'LogHandler'):
--
-- > updateGlobalLogger "MyApp.BuggyComponent"
-- >                    (setLevel DEBUG . setHandlers [s])
updateGlobalLogger
    :: MonadIO m
    => LoggerName         -- ^ Logger name
    -> (Logger -> Logger) -- ^ Function to call
    -> m ()
updateGlobalLogger ln func = do
    l <- getLogger ln
    saveGlobalLogger (func l)

-- | Allow graceful shutdown. Release all opened files/handlers/etc.
removeAllHandlers :: MonadIO m => m ()
removeAllHandlers = liftIO $
    modifyMVar_ logInternalState $ \LogInternalState{..} -> do
        let allHandlers = M.foldr (\l r -> r ++ view lHandlers l) [] liTree
        mapM_ (\(HandlerT h) -> close h) allHandlers
        let newTree = map (lHandlers .~ []) liTree
        return $ LogInternalState newTree liPrefix

----------------------------------------------------------------------------
-- Retrieving logs ad-hoc
----------------------------------------------------------------------------

-- | Retrieves content of log file(s) given path (w/o '_lcFilePrefix',
-- as specified in your config). Example: there's @component.log@ in
-- config, but this function will return @[component.log.122,
-- component.log.123]@ if you want to. Content is file lines newest
-- first.
--
-- FYI: this function is implemented to avoid the following problem:
-- log-warper holds open handles to files, so trying to open log file
-- for read would result in 'IOException'.
retrieveLogContent :: (MonadIO m) => FilePath -> Maybe Int -> m [Text]
retrieveLogContent filePath linesNum =
    liftIO $ withMVar logInternalState $ \LogInternalState{..} -> do
        let filePathFull = fromMaybe "" liPrefix </> filePath
        let appropriateHandlers =
                filter (\(HandlerT h) -> getTag h == HandlerFilelike filePathFull) $
                concatMap _lHandlers $
                M.elems liTree
        let takeMaybe = maybe id take linesNum
        case appropriateHandlers of
            [HandlerT h] -> liftIO $ readBack h 12345 -- all of them
            []  -> takeMaybe . reverse . T.lines <$> TIO.readFile filePathFull
            xs  -> error $ "Found more than one (" <> show (length xs) <>
                           "handle with the same filePath tag, impossible."