{-# LANGUAGE OverloadedStrings, TemplateHaskell, RankNTypes, TypeFamilies #-}

module System.Log.Simple.Base (
        Level(..), level, level_,
        Component(..), Scope(..),
        Message(..),
        Converter, Consumer, consumer,
        LogHandler, handler,
        LogConfig(..), defCfg, logCfg, componentCfg, componentLevel,
        Log(..),
        newLog, rootLog, getLog, subLog, getLogConfig, updateLogConfig, updateLogHandlers, writeLog, stopLog,
        ) where

import Prelude.Unicode

import Control.Applicative
import Control.Arrow
import qualified Control.Exception as E
import Control.Concurrent
import qualified Control.Concurrent.Async as A
import Control.DeepSeq
import Control.Monad
import Control.Monad.Cont
import Data.Default
import Data.Function (fix)
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
import Data.String
import Lens.Micro.Platform
import Lens.Micro.Internal
import Text.Format


-- Helper function
splitBy  Char  Text  [Text]
splitBy _ "" = []
splitBy ch t = T.split ( ch) t



-- | Level of message
data Level = Trace | Debug | Info | Warning | Error | Fatal
        deriving (Eq, Ord, Read, Show, Enum, Bounded)

instance Default Level where
        def = Trace

instance Formattable Level where
        formattable Trace _ = "TRACE" `withFlags` ["gray"]
        formattable Debug _ = "DEBUG" `withFlags` ["yellow"]
        formattable Info _ = "INFO" `withFlags` ["blue"]
        formattable Warning _ = "WARN" `withFlags` ["darkyellow"]
        formattable Error _ = "ERROR" `withFlags` ["red"]
        formattable Fatal _ = "FATAL" `withFlags` ["bg=red"]

-- | Component — each one have separate log scopes and can have different politics
-- Child component's root politics inherits its parent root politics
-- Component name parts stored in reverse order
newtype Component = Component { componentPath  [Text] } deriving (Eq, Ord)

instance Show Component where
        show = T.unpack  T.intercalate "."  reverse  componentPath

instance Formattable Component

instance Read Component where
        readsPrec _ = return  flip (,) ""  Component  reverse  splitBy '.'  T.pack

instance IsString Component where
        fromString = read

instance Semigroup Component where
        Component l <> Component r = Component $ r ++ l

instance Monoid Component where
        mempty = Component []
        Component l `mappend` Component r = Component $ r ++ l

instance NFData Component where
        rnf (Component cs) = rnf cs

-- | Log scope, also stored in reverse order
newtype Scope = Scope { scopePath  [Text] } deriving (Eq, Ord)

instance Show Scope where
        show = T.unpack  T.intercalate "/"  reverse  scopePath

instance Formattable Scope

instance Read Scope where
        readsPrec _ = return  flip (,) ""  Scope  reverse  splitBy '/'  T.pack

instance IsString Scope where
        fromString = read

instance Semigroup Scope where
        Scope l <> Scope r = Scope $ r ++ l

instance Monoid Scope where
        mempty = Scope []
        Scope l `mappend` Scope r = Scope $ r ++ l

instance NFData Scope where
        rnf (Scope s) = rnf s

class HasParent a where
        getParent  a  Maybe a

instance HasParent Component where
        getParent (Component []) = Nothing
        getParent (Component (_:cs)) = Just $ Component cs

instance HasParent Scope where
        getParent (Scope []) = Nothing
        getParent (Scope (_:ps)) = Just $ Scope ps

-- | Parse level
level  Text  Maybe Level
level = flip M.lookup levels  T.toLower where
        levels = M.fromList [(T.toLower  T.pack  show $ l', l') | l'  [minBound .. maxBound]]

-- | Parse level, failing on invalid input
level_  Text  Level
level_ t = fromMaybe (error errMsg)  level $ t where
        errMsg = "invalid level: " ++ T.unpack t

-- | Log message
data Message = Message {
        messageTime  ZonedTime,
        messageLevel  Level,
        messageComponent  Component,
        messageScope  Scope,
        messageText  Text }
                deriving (Read, Show)

instance NFData Message where
        rnf (Message t l c s m) = t `seq` l  `seq` rnf c `seq` rnf s `seq` rnf m

type Converter a = Message  a

-- | Returns function which accepts consumed value
type Consumer a = ContT () IO (a  IO ())

-- | Make consumer
consumer  (((a  IO ())  IO ())  IO ())  Consumer a
consumer = ContT

-- | Message handler
type LogHandler = Consumer Message

handler  Converter a  Consumer a  Consumer Message
handler conv = fmap ( conv)

data LogConfig = LogConfig {
        _logConfigMap  Map Component Level }

makeLenses ''LogConfig

instance Default LogConfig where
        def = LogConfig mempty

instance Show LogConfig where
        show (LogConfig cfg) = unlines [show comp ++ ":" ++ show lev | (comp, lev)  M.toList cfg]

type instance Index LogConfig = Component
type instance IxValue LogConfig = Level

instance Ixed LogConfig where
        ix n = logConfigMap  ix n

instance At LogConfig where
        at n = logConfigMap  at n

-- | Default log config — info level
defCfg  LogConfig
defCfg = def

-- | Make log config by list of components and levels
logCfg  [(Component, Level)]  LogConfig
logCfg = LogConfig  M.fromList

-- | Component config level lens
componentCfg  Component  Lens' LogConfig (Maybe Level)
componentCfg comp = logConfigMap . at comp

-- | Get politics for specified component
componentLevel  LogConfig  Component  Level
componentLevel cfg comp = fromMaybe def $ (cfg ^. componentCfg comp) <|> (componentLevel cfg <$> getParent comp)

-- | Log
data Log = Log {
        -- | Current log component
        logComponent  Component,
        -- | Current log scope
        logScope  Scope,
        -- | Log message, it is low-level function, i.e. it doesn't take into account current component and scope and writes message as is
        logPost  Message  IO (),
        -- | Stop log and wait until it writes all
        logStop  IO (),
        -- | Log config
        logConfig  MVar LogConfig,
        -- | Handlers list
        logHandlers  MVar [LogHandler],
        -- | Restart all handlers
        logRestartHandlers  IO () }

type FChan a = Chan (Maybe a)

writeFChan  FChan a  a  IO ()
writeFChan ch = writeChan ch  Just

stopFChan  FChan a  IO ()
stopFChan ch = writeChan ch Nothing

-- | Create log, returns root logger for root component
--
-- Messages from distinct threads and components are splitted in several chans, where they are processed, and then messages combined back and sent to log-thread
--
newLog  LogConfig  [LogHandler]  IO Log
newLog cfg handlers = do
        ch  newChan  IO (FChan Message)
        cfgVar  newMVar cfg
        handlersVar  newMVar handlers
        handlersThread  newEmptyMVar

        let
                -- | Pass message firther if it passes config
                passMessage  (Message  IO ())  Message  IO ()
                passMessage fn msg = do
                        cfg'  readMVar cfgVar
                        when (componentLevel cfg' (messageComponent msg)  messageLevel msg) $ fn msg

                fatalMsg  String  IO Message
                fatalMsg s = do
                        tm  getZonedTime
                        return $ Message tm Fatal "*" "" $ fromString s

                -- | Perform log
                tryLog  (Message  IO ())  Message  IO ()
                tryLog logMsg m = E.handle onError (m `deepseq` logMsg m) where
                        onError  E.SomeException  IO ()
                        onError e = E.handle ignoreError $ fatalMsg ("Exception during logging message: " ++ show e) >>= logMsg
                        ignoreError  E.SomeException  IO ()
                        ignoreError _ = return ()

                -- | Consume messages and send to handlers
                runHandlers  FChan Message  [LogHandler]  ContT () IO ()
                runHandlers inCh hs = do
                        hs'  sequence $ map (fmap tryLog) hs
                        fix $ \loop'  do
                                msg  liftIO $ readChan inCh
                                case msg of
                                        Just msg'  liftIO (mapM_ ($ msg') hs') >> loop'
                                        Nothing  return ()

                -- | Start handlers thread
                startHandlers  IO (A.Async ())
                startHandlers = readMVar handlersVar >>= A.async  flip runContT return  runHandlers ch

                -- | Restart handlers thread
                restartHandlers  IO ()
                restartHandlers = modifyMVar_ handlersThread $ \th  A.cancel th >> startHandlers

                -- | Write message with myThreadId
                writeMessage  Message  IO ()
                writeMessage = passMessage (writeFChan ch)

                waitHandlers  IO ()
                waitHandlers = readMVar handlersThread >>= A.wait

        startHandlers >>= putMVar handlersThread
        return $ Log {
                logComponent = mempty,
                logScope = mempty,
                logPost = writeMessage,
                logStop = stopFChan ch >> waitHandlers,
                logConfig = cfgVar,
                logHandlers = handlersVar,
                logRestartHandlers = restartHandlers }

-- | Get root log, i.e. just drop current component and scope
rootLog  Log  Log
rootLog l = l { logComponent = mempty, logScope = mempty }

-- | Get log for specified component and scope
getLog  Component  Scope  Log  Log
getLog comp scope l = l { logComponent = comp, logScope = scope }

-- | Get sub-log
subLog  Component  Scope  Log  Log
subLog comp scope l = l {
        logComponent = logComponent l `mappend` comp,
        logScope = logScope l `mappend` scope }

-- | Read log config
getLogConfig  MonadIO m  Log  m LogConfig
getLogConfig l = liftIO $ readMVar (logConfig l)

-- | Modify log config
updateLogConfig  MonadIO m  Log  (LogConfig  LogConfig)  m LogConfig
updateLogConfig l update = liftIO $ modifyMVar (logConfig l) (return  (update &&& id))

-- | Update log handlers, this restarts handlers thread
updateLogHandlers  MonadIO m  Log  ([LogHandler]  [LogHandler])  m ()
updateLogHandlers l update = liftIO $ modifyMVar_ (logHandlers l) (return  update) >> logRestartHandlers l

-- | Write message to log for current component and scope
writeLog  MonadIO m  Log  Level  Text  m ()
writeLog l lev msg = liftIO $ do
        tm  getZonedTime
        logPost l $ Message tm lev (logComponent l) (logScope l) msg

-- | Wait log messages and stop log
stopLog  MonadIO m  Log  m ()
stopLog = liftIO  logStop