{-# LANGUAGE GeneralizedNewtypeDeriving, DefaultSignatures, ScopedTypeVariables, NamedFieldPuns, OverloadedStrings, MultiParamTypeClasses, TemplateHaskell, FlexibleContexts, TypeFamilies, StandaloneDeriving, RecordWildCards, RankNTypes, Trustworthy, UndecidableInstances #-}
{-|
  SimpleLog is a library for convenient and configurable logging. It uses the usual monad transformer + associated class design: 'SLogT' and 'MonadSLog'.

  Example usage:

  @
import System.Log.SLog

main = simpleLog \"Example.log\" $ do
    logD \"Some debugging information\"
    logI \"This is some other information\"
    logW \"Something bad is about to happen\"
    logE \"Something bad happened\"
  @

  The above sample code when run will produce output similar to this:

  @
2013-10-02 14:17:40 | INFO    | [ThreadId 58] This is some other information
2013-10-02 14:17:40 | WARNING | [ThreadId 58] Something bad is about to happen
2013-10-02 14:17:40 | ERROR   | [ThreadId 58] Something bad happened
  @

  Note how the debug line is not displayed. This is because the default configuration ('defaultLogConfig') only logs to stdout when the severity is >= 'INFO'.
  The above code will also append the log lines to the file @\"Example.log\"@, including the debug line.

  The following example shows how one can fine tune SimpleLog as well as how to fork other logging threads.

  @
\-- Our log configuration specifies that no ANSI colouring should be used, all log lines
\-- should be written to the TChan, and >= INFO severity lines should be written to the
\-- stdout synchronously.
logConfig :: TChan LogLine -> LogConfig
logConfig tchan
    = LogConfig { ansiColours = False
                , loggers = [ (anySev, TChanLogger tchan)
                            , ((>= INFO), StdoutLogger Sync)
                            ] }


\-- Our custom logging format
logFormat :: Format
logFormat = $(format \"%d(%T) (%s) %t: %m\")

\-- The main thread will fork a child thread, then wait until everything is flushed, then
\-- count how many messages have been written in total to the TChan (which will be all
\-- messages as our filter passes through everything)
main :: IO ()
main = do
  tchan \<- newTChanIO
  (_, fkey) \<- runSLogT (logConfig tchan) logFormat \"main\" $ do
    logS \"Main thread started successfully\"
    logD \"This will not appear on stdout\"
    _ \<- forkSLog \"child\" $ do
      logS \"I am the child\"
      liftIO $ threadDelay 5000000
      logW \"CHILD SHUTTING DOWN\"
    logI \"Exiting main thread\"
  waitFlush fkey
  c \<- countTChan tchan
  putStrLn $ show c ++ \" messages have been logged in total\"

\-- Counts the number of elements in the TChan (and pops them all)
countTChan :: TChan a -> IO Int
countTChan tchan = do
  let count = do
        em \<- isEmptyTChan tchan
        if em then return 0
        else readTChan tchan >> (1 +) \<$> count
  atomically count
  @

  The above code when run will produce something like this:

  @
17:35:15 (SUCCESS) main: Main thread started successfully
17:35:15 (SUCCESS) child: I am the child, waiting for 5 seconds...
17:35:15 (INFO   ) main: Exiting main thread
17:35:20 (WARNING) child: CHILD SHUTTING DOWN
5 messages have been logged in total
  @

-}
module System.Log.SLog
    (
    -- * SLogT
      SLogT
    , SLog

    -- ** Running SLogT
    , runSLogT
    , simpleLog

    -- * FlushKey
    , FlushKey
    , waitFlush

    -- * MonadSLog
    , MonadSLog(..)

    -- ** Convenience log functions
    , logD
    , logI
    , logS
    , logW
    , logE

    -- * Loggers
    , Logger(..)
    , Sync(..)
    , LogLine(..)

    -- * Filters
    , Severity(..)
    , Filter
    , anySev

    -- * Configuration
    , LogConfig(..)
    , defaultLogConfig

    -- * Format
    , module System.Log.SLog.Format
    , defaultLogFormat

    -- * Utility functions
    , forkSLog
    , formatLine
    , unsafeUnliftSLogT
    )
where

import System.Log.SLog.Format

import Prelude hiding (log)
import Control.Applicative
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Error
import Control.Monad.Cont
import Control.Monad.Base
import Control.Monad.Trans.Resource
import Control.Monad.Trans.Control
import Data.Semigroup
import qualified Data.Map as Map
import Data.Time.LocalTime
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.IO
import System.Console.ANSI
import System.Directory
import Control.Concurrent
import Control.Concurrent.STM
import Control.Concurrent.ForkableT.Instances

-- | The type of severities with increasing importance
data Severity
    = DEBUG | INFO | SUCCESS | WARNING | ERROR
      deriving (Show, Read, Eq, Ord)

-- | The class of monads that can perform logging
class (MonadIO m) => MonadSLog m where
    -- | 'log' logs the specified 'T.Text' with the specified 'Severity'
    log :: Severity -> T.Text -> m ()
    -- | The default instance simply lifts through a monad transformer
    default log :: (MonadTrans t, MonadSLog m) => Severity -> T.Text -> t m ()
    log sev = lift . log sev

instance (MonadSLog m) => MonadSLog (StateT s m)
instance (MonadSLog m) => MonadSLog (ReaderT s m)
instance (MonadSLog m, Error e) => MonadSLog (ErrorT e m)
instance (MonadSLog m) => MonadSLog (ContT r m)
instance (MonadSLog m) => MonadSLog (ResourceT m)

-- | The default log format, which currently is @$(format \"%d(%F %T) | %s | [%t] %m\")@. See "System.Log.SLog.Format" for more details on format strings.
defaultLogFormat :: Format
defaultLogFormat = $(format "%d(%F %T) | %s | [%t] %m")

-- | Log a 'DEBUG' message
logD :: MonadSLog m => String -> m ()
logD = log DEBUG . T.pack

-- | Log a 'SUCCESS' message
logS :: MonadSLog m => String -> m ()
logS = log SUCCESS . T.pack

-- | Log an 'INFO' message
logI :: MonadSLog m => String -> m ()
logI = log INFO . T.pack

-- | Log a 'WARNING' message
logW :: MonadSLog m => String -> m ()
logW = log WARNING . T.pack

-- | Log an 'ERROR' message
logE :: MonadSLog m => String -> m ()
logE = log ERROR . T.pack

-- | SGR code for severity colours
sgr :: Severity -> [SGR]
sgr DEBUG = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid White]
sgr INFO = [SetColor Foreground Vivid White]
sgr SUCCESS = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Green]
sgr WARNING = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Yellow]
sgr ERROR = [SetConsoleIntensity BoldIntensity, SetBlinkSpeed SlowBlink, SetColor Foreground Vivid Red]

-- | Wrap a piece of text in some SGR configuration
withSgr :: [SGR] -> T.Text -> T.Text
withSgr sg s = T.concat [T.pack $ setSGRCode sg, s, T.pack $ setSGRCode []]

-- | 'Sync' is a type to specify whether a logger should log synchronously or asynchronously.
-- Syncronous logging means that the logging thread will block until the message has been written and flushed to the sink.
-- Asynchronous logging means that the logging thread will write to a work queue and move on. The work queue will be read by a dedicated thread that is forked for each sink.
data Sync
    = Sync | Async
  deriving (Eq)

-- | The 'Logger' type specifies the types of sinks we can log to.
data Logger
    -- | 'FileLogger' specifies a file to be logged in.
    -- Note that symbolic links will be resolved using 'canonicalizePath' when deciding whether two 'FileLogger's point to the same file.
    = FileLogger Sync FilePath
    -- | 'StdoutLogger' logs to the stdout
    | StdoutLogger Sync
    -- | 'StderrLogger' logs to the stderr
    | StderrLogger Sync
    -- | 'TChanLogger' logs to a specified 'TChan'.
    -- Note that 'LogLine's are written instead of the final formatted text. If you wish to use the final text use 'formatLine'.
    | TChanLogger (TChan LogLine)
      deriving (Eq)

-- | 'Filter' is the type of logging filters. 'Filter's may only depend on the 'Severity'.
type Filter = Severity -> Bool

-- | 'anySev' allows all lines to be logged.
anySev :: Filter
anySev = const True

-- | 'LogLine' is a log message together with the severity, time of logging and the logging thread's name.
data LogLine
    = LogLine { logSeverity :: Severity
              , logMessage :: T.Text
              , logTimestamp :: ZonedTime
              , logThread :: T.Text
              }

-- | 'LogConfig' is the configuration of 'SLogT'
data LogConfig
    = LogConfig {
        -- | Specifies whether ANSI colouring should be used when logging to stdout/stderr
        ansiColours :: Bool
        -- | The list of loggers together with the associated filters
      , loggers :: [(Filter, Logger)]
      }

-- | 'defaultLogConfig' is the default log configuration.
-- It writes all non-DEBUG messages to the stdout synchronously and all messages to a specified file asynchronously.
defaultLogConfig :: FilePath -> LogConfig
defaultLogConfig fName
    = LogConfig { ansiColours = True
                , loggers = [ ((>= INFO), StdoutLogger Sync)
                            , (anySev, FileLogger Async fName)
                            ]
                }

-- | The internal representation of Loggers with open handles, locks and 'TChan's.
data LoggerInternal
    = SyncLoggerInternal Handle (MVar ()) Bool
    | AsyncLoggerInternal (TChan T.Text) Bool
    | TChanLoggerInternal (TChan LogLine)

-- | The environment of 'SLogT'
data SLogEnv
    = SLogEnv {
        -- | The current thread's name
        threadName :: T.Text
        -- | The list of internal loggers together with associated filters
      , loggerInternals :: [(Filter, LoggerInternal)]
        -- | Same as the user-specified 'ansiColours'
      , logColours :: Bool
        -- | The 'Format' of logging
      , logFormat :: Format
      }

-- | The SLogT monad transformer is simply a 'ResourceT' with an environment
newtype SLogT m a
    = SLogT { unSLogT :: ReaderT SLogEnv (ResourceT m) a }
      deriving ( Functor, Monad, MonadIO, Applicative
               , MonadThrow )
deriving instance (MonadBase IO m) => MonadBase IO (SLogT m)
deriving instance (MonadBase IO m, MonadThrow m, MonadIO m) => MonadResource (SLogT m)

instance MonadTransControl SLogT where
    type StT SLogT a = StT ResourceT a
    liftWith f = SLogT . ReaderT $ \r ->
                   liftWith $ \lres ->
                     f $ \(SLogT t) ->
                       lres $ runReaderT t r
    restoreT = SLogT . lift . restoreT

instance (MonadBaseControl IO m) => MonadBaseControl IO (SLogT m) where
    type StM (SLogT m) a = ComposeSt SLogT m a
    liftBaseWith = defaultLiftBaseWith
    restoreM = defaultRestoreM

instance MonadTrans SLogT where
    lift = SLogT . lift . lift

-- | This is a simple monad for the bottom of one's monad stack.
type SLog = SLogT IO

-- | A 'FlushKey' is returned when an 'SLogT' is run. You may wait on it with 'waitFlush'.
newtype FlushKey = FlushKey (TVar Bool)

-- | 'waitFlush' will only return when all resources have been released and all streams have been flushed. Note that this includes resources allocated by the user using the exposed 'MonadResource' instance.
-- 
-- All threads internally accounted for are signaled to exit (they will first finish processing of all remaining jobs) when the 'SLogT' is run, however it is the user's responsibility to shut down threads forked with 'forkSLog' or 'fork' before 'waitFlush' can return.
waitFlush :: FlushKey -> IO ()
waitFlush (FlushKey tvar)
    = atomically $ do
        b <- readTVar tvar
        unless b retry

-- Internally we use two ResourceTs. The inner one is used to keep
-- track of open files, make sure everything gets flushed and release
-- the FlushKey.
-- The outer one is used to signal completion. In particular when the
-- outer ResourceT is run all threads that are internally accounted
-- for will receive a signal to finish processing, thus running the
-- internal ResourceT (see forkCleanUp)
-- Note that this means all SLog threads forked by the user need to
-- finish before the FlushKey releases
-- | 'runSLogT' runs an 'SLogT' given a 'LogConfig', 'Format' and the current thread's name.
-- It returns a 'FlushKey' besides the usual return value.
runSLogT :: (MonadResource m, MonadBaseControl IO m) => LogConfig -> Format -> String -> SLogT m a -> m (a, FlushKey)
runSLogT LogConfig{..} lf tName (SLogT r)
    = runResourceT $ do
        (_, tvar) <- allocate (newTVarIO False) (\t -> atomically $ writeTVar t True)
        runResourceT $ do
          internals <- initLoggers loggers
          a <- lift $ runReaderT r SLogEnv{ threadName = T.pack tName
                                          , loggerInternals = internals
                                          , logColours = ansiColours
                                          , logFormat = lf }
          return (a, FlushKey tvar)

-- | 'simpleLog' uses the default configuration with the specified log file name. It also waits using 'waitFlush' until all resources have been released.
simpleLog :: (MonadResource m, MonadBaseControl IO m) => FilePath -> SLogT m a -> m a
simpleLog fName s = do
  tName <- show <$> liftIO myThreadId
  (a, fkey) <- runSLogT (defaultLogConfig fName) defaultLogFormat tName s
  liftIO $ waitFlush fkey
  return a

-- | initLoggers initialises the user specified 'Logger's and returns the internal representation of them.
-- 
-- This includes first aggregating the 'Logger's resolving any ambiguities, then opening the logging streams.
initLoggers :: (MonadResource m, Applicative m) => [(Filter, Logger)] -> ResourceT (ResourceT m) [(Filter, LoggerInternal)]
initLoggers fls = do
  InitState{..} <- liftIO $ aggregateLoggers fls

  let stdHandle (Just ini) h = do
        _ <- lift $ register (hFlush h)
        return [(h, ini, True)]
      stdHandle Nothing _ = return []

      createHandle (fname, ini) = do
        (_, h) <- allocate
                  (openFile fname AppendMode)
                  (\h -> hFlush h >> hClose h)
        return (h, ini, False)

  sout <- stdHandle stdoutInit stdout
  serr <- stdHandle stderrInit stderr
  files <- lift . mapM createHandle $ Map.toList fileInitMap

  let toInternal (h, InitSync f, c) = do
        lock <- liftIO $ newMVar ()
        return [(f, SyncLoggerInternal h lock c)]
      toInternal (h, InitAsync f, c) = do
        tchan <- liftIO newTChanIO
        _ <- forkCleanUp $ lift . asyncLogger Nothing h tchan
        return [(f, AsyncLoggerInternal tchan c)]
      toInternal (h, Both fs fa, c) = do
        lock <- liftIO $ newMVar ()
        tchan <- liftIO newTChanIO
        _ <- forkCleanUp $ lift . asyncLogger (Just lock) h tchan
        return [(fs, SyncLoggerInternal h lock c), (fa, AsyncLoggerInternal tchan c)]

      toInternalTChan (f, tchan) = (f, TChanLoggerInternal tchan)

  nonTChan <- join <$> mapM toInternal (sout ++ serr ++ files)

  return $ nonTChan ++ map toInternalTChan tchanInit

-- | An internal datatype used for aggregation of filters as well as keeping track of synchronous and asynchronous logging.
-- This means for example that it is possible to log from two threads into the same file, one synchronously, one asynchronously, using different filters.
data InitLogger
    = InitSync Filter
    | InitAsync Filter
    | Both Filter Filter

instance Semigroup InitLogger where
    InitSync f <> InitSync f' = InitSync $ liftM2 (||) f f'
    InitSync f <> InitAsync f' = Both f f'
    InitSync f <> Both f' f'' = Both (liftM2 (||) f f') f''
    InitAsync f <> InitSync f' = Both f f'
    InitAsync f <> InitAsync f' = InitAsync $ liftM2 (||) f f'
    InitAsync f <> Both f' f'' = Both f' (liftM2 (||) f f'')
    Both f' f'' <> InitSync f = Both (liftM2 (||) f f') f''
    Both f' f'' <> InitAsync f = Both f' (liftM2 (||) f f'')
    Both f f' <> Both f'' f''' = Both (liftM2 (||) f f') (liftM2 (||) f'' f''')

-- | canonExist makes sure the specified file exists by creating it if it doesn't, then returns canonicalizePath.
canonExist :: String -> IO String
canonExist f = appendFile f "" >> canonicalizePath f

-- | 'forkCleanup' forks a ResIO thread that will get an exit signal through a TVar when the outer ResourceT is run.
-- The forked off ResourceT is the inner one
forkCleanUp :: (MonadResource m) =>
               (TVar Bool -> ResIO ()) -> ResourceT (ResourceT m) ThreadId
forkCleanUp io = do
  (_, exitSignal) <- allocate (newTVarIO False) (\t -> atomically $ writeTVar t True)
  st <- lift . liftWith $ \unliftRes -> liftIO . unliftRes . fork $ io exitSignal
  lift . restoreT $ return st

-- | An internal datatype used when initialising 'Logger's. It includes all information that the user passed in with the list of loggers.
data InitState = InitState { fileInitMap :: Map.Map FilePath InitLogger
                           , stdoutInit :: Maybe InitLogger
                           , stderrInit :: Maybe InitLogger
                           , tchanInit :: [(Filter, TChan LogLine)]
                           }

-- | This method aggregates the specified loggers in a meaningful way
-- Ambiguous cases arise when several loggers specify the same file/stream
--  * First off we use canonicalizePath to resolve symlinks
--  * Second, we create a disjunction of the attached filters
--  * Lastly we keep track of synchrony: if there are two loggers
--    specifying the same file, one synchronous the other asynchronous
--    then we keep both filters and will use the a lock in each case.
aggregateLoggers :: [(Filter, Logger)] -> IO InitState
aggregateLoggers fls = execStateT
                       (mapM_ (uncurry initLogger) fls)
                       InitState { fileInitMap = Map.empty
                                 , stdoutInit = Nothing
                                 , stderrInit = Nothing
                                 , tchanInit = [] }
    where
      initLogger :: Filter -> Logger -> StateT InitState IO ()
      initLogger f (FileLogger sync fname) = do
        trueFname <- liftIO $ canonExist fname
        s@InitState{..} <- get
        let ini = case sync of ; Sync -> InitSync f ; Async -> InitAsync f
        put s { fileInitMap = Map.alter (<> Just ini) trueFname fileInitMap }
      initLogger f (StdoutLogger sync) = do
        s@InitState{..} <- get
        let ini = case sync of ; Sync -> InitSync f ; Async -> InitAsync f
        put s { stdoutInit = stdoutInit <> Just ini }
      initLogger f (StderrLogger sync) = do
        s@InitState{..} <- get
        let ini = case sync of ; Sync -> InitSync f ; Async -> InitAsync f
        put s { stderrInit = stderrInit <> Just ini }
      initLogger f (TChanLogger tchan) =
        modify $ \s@InitState{..} -> s { tchanInit = (f, tchan) : tchanInit }

-- | asyncLogger is the thread forked for each 'Async' logger.
-- It may be passed a lock to use when logging in case a 'Sync'hronous logger for the same handle also exists.
asyncLogger :: Maybe (MVar ()) -> Handle -> TChan T.Text -> TVar Bool -> IO ()
asyncLogger mlock h tchan exitSignal = flip runContT return $
  callCC $ \exit -> forever $ do
    m <- liftIO . atomically $
           (Just <$> readTChan tchan)
             `orElse`
           (readTVar exitSignal >>= check >> return Nothing)
    case m of
      Just str -> liftIO $ case mlock of
                    Nothing -> T.hPutStrLn h str
                    Just lock -> withMVar lock $ \_ -> T.hPutStrLn h str
      Nothing -> do
        liftIO $ hFlush h
        exit ()

-- | Helper method for choosing coloured vs. non-coloured strings
chs :: Bool -> a -> a -> a
chs False a _ = a
chs True  _ b = b

-- | logger performs the actual logging.
logger :: LoggerInternal -> LogLine -> T.Text -> T.Text -> IO ()
logger (AsyncLoggerInternal tchan c) _ ns s = atomically . writeTChan tchan $ chs c ns s
logger (SyncLoggerInternal h lock c) _ ns s = withMVar lock $ \_ -> do
                                                T.hPutStrLn h (chs c ns s)
                                                hFlush h
logger (TChanLoggerInternal tchan) l _ _ = atomically $ writeTChan tchan l

-- | 'formatLine' formats the given 'LogLine' using the specified 'Format'. The 'Bool'ean determines whether 'formatLine' should insert ANSI colour codes or not.
formatLine :: Bool -> Format -> LogLine -> T.Text
formatLine isColour les ll = T.concat $ map (formatLine' ll) les
    where
      formatLine' LogLine{logMessage} MessageElem    = logMessage
      formatLine' LogLine{logSeverity} SeverityElem  =
          let sev = padS 7 . T.pack $ show logSeverity
          in if isColour then withSgr (sgr logSeverity) sev else sev
      formatLine' _ (StringElem str)                 = str
      formatLine' LogLine{logTimestamp} (DateTimeElem f) = f logTimestamp
      formatLine' LogLine{logThread} ThreadElem      = logThread

instance (MonadBaseControl IO m, MonadIO m) => Forkable (SLogT m) (SLogT m) where
    fork (SLogT (ReaderT f)) = SLogT . ReaderT $ \env ->
                                 fork $ do
                                   tid <- liftIO myThreadId
                                   f env { threadName = T.pack $ show tid }

-- | 'forkSLog' forks an 'SLogT' thread with the specified thread name.
forkSLog :: (MonadBaseControl IO m, MonadIO m) => String -> SLogT m () -> SLogT m ThreadId
forkSLog tname (SLogT m) = SLogT . local (\e -> e { threadName = T.pack tname }) $ fork m

-- | helper method for padding with spaces
padS :: Int -> T.Text -> T.Text
padS n t = t `T.append` T.replicate (n - T.length t) " "

-- | 'unsafeUnliftSLog' gives you an unsafe unlift of an SLogT by assuming that any unlifted computation will finish earlier than the runSLogT of the calling thread.
-- It is unsafe because if the unlifted computation doesn't finish earlier then it may access deallocated resources.
-- This is useful when a library is implicitly forking but we still need to log in the forked threads, and we know that the child threads will finish earlier than the parent. An example is Network.WebSockets
unsafeUnliftSLogT :: forall m b. (Monad m, MonadBaseControl IO m) =>
                    ((forall a. SLogT m a -> m a) -> SLogT m b) -> SLogT m b
unsafeUnliftSLogT f = do
  env <- SLogT ask
  let unlift :: SLogT m c -> m c
      unlift s = runResourceT $ runReaderT (unSLogT s) env
  f unlift

instance (MonadIO m) => MonadSLog (SLogT m) where
    log sev s = do
      SLogEnv{..} <- SLogT ask
      liftIO $ do
        timestamp <- getZonedTime
        let logLine = LogLine { logMessage = s
                              , logSeverity = sev
                              , logTimestamp = timestamp
                              , logThread = threadName
                              }
            nonColoured = formatLine False logFormat logLine
            coloured = if logColours
                       then formatLine True logFormat logLine
                       else nonColoured
        mapM_ (\(fter, l) -> when (fter sev) $
                             logger l logLine nonColoured coloured) loggerInternals
