{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, TypeFamilies, GeneralizedNewtypeDeriving, StandaloneDeriving, MultiParamTypeClasses, UndecidableInstances, AllowAmbiguousTypes, ScopedTypeVariables, FunctionalDependencies, FlexibleContexts, ConstraintKinds #-}
-- | This module contains implementation of @HasLogger@, @HasLogBackend@, @HasLogContext@ instances for IO monad.
-- This implementation uses thread-local storage, so each thread will have it's own logging state (context and logger).
--
-- This module is not automatically re-exported by System.Log.Heavy, because in many cases it is more convinient to maintain
-- logging state within monadic context, than in global variable.
--
-- Note: implementations of @HasLogger@, @HasLogBackend@, @HasLogContext@ for IO, provided by this module, work only inside
-- @withLoggingIO@ call. If you try to call logging functions outside, you will get runtime error.
--
module System.Log.Heavy.IO
  ( withLoggingIO
  ) where

import Control.Exception
import Data.IORef
import Data.TLS.GHC

import System.IO.Unsafe (unsafePerformIO)

import System.Log.Heavy.Types

-- | Logging state stored in TLS (thread-local storage)
data LoggingIOState = LoggingIOState {
    liosLogger :: SpecializedLogger
  , liosBackend :: AnyLogBackend
  , liosContext :: LogContext
  }

-- | This global variable stores logging state.
-- Nothing inside IORef means that logging state is not initialized yet
-- or is already deinitialized.
loggingTLS :: TLS (IORef (Maybe LoggingIOState))
loggingTLS = unsafePerformIO $ mkTLS $ do
    newIORef Nothing
{-# NOINLINE loggingTLS #-}

-- | Execute IO actions with logging.
--
-- Note 1: logging methods calls in IO monad are only valid inside @withLoggingIO@.
--         If you try to call them outside of this function, you will receive runtime error.
-- 
-- Note 2: if you will for some reason call @withLoggingIO@ inside @withLoggingIO@ within one
--         thread, you will receive runtime error.
-- 
-- Note 3: You can call @withLoggingIO@ syntactically inside @withLoggingIO@, but within other
--         thread. I.e., the construct like following is valid:
--
--         @
--         withLoggingIO settings $ do
--             \$info "message" ()
--             ...
--             forkIO $ do
--                 withLoggingIO settings $ do
--                     \$info "message" ()
--                     ...
--         @
--
withLoggingIO :: LoggingSettings -- ^ Settings of arbitrary logging backend
              -> IO a            -- ^ Actions to be executed with logging
              -> IO a
withLoggingIO (LoggingSettings settings) actions =
    bracket (init settings)
            (cleanup)
            (\tls -> withBackend tls actions)
  where
    init settings = do
      ioref <- getTLS loggingTLS
      mbState <- readIORef ioref
      case mbState of
        Just _ -> fail "Logging IO state is already initialized. withLoggingIO was called twice?"
        Nothing -> do
              backend <- initLogBackend settings
              let logger = makeLogger backend
              let st = LoggingIOState logger (AnyLogBackend backend) []
              writeIORef ioref (Just st)
              return st

    cleanup st = do
      ioref <- getTLS loggingTLS
      freeAllTLS loggingTLS
      writeIORef ioref Nothing

    withBackend st actions = actions

-- | Get current logging state. Fail if it is not initialized yet.
getLogginngIOState :: IO LoggingIOState
getLogginngIOState = do
  ioref <- getTLS loggingTLS
  mbState <- readIORef ioref
  case mbState of
    Nothing -> fail "get: Logging IO state is not initialized. See withLoggingIO."
    Just st -> return st

-- | Modify logging state with pure function.
-- Fail if logging state is not initialized yet.
modifyLoggingIOState :: (LoggingIOState -> LoggingIOState) -> IO ()
modifyLoggingIOState fn = do
  ioref <- getTLS loggingTLS
  modifyIORef ioref $ \mbState ->
    case mbState of
      Nothing -> error "modify: Logging IO state is not initialized. See withLoggingIO."
      Just st -> Just (fn st)

instance HasLogBackend AnyLogBackend IO where
  getLogBackend = do
    st <- getLogginngIOState
    return $ liosBackend st

instance HasLogger IO where
  getLogger = do
    st <- getLogginngIOState
    return $ liosLogger st

  localLogger logger actions = do
    oldLogger <- getLogger
    modifyLoggingIOState $ \st -> st {liosLogger = logger}
    result <- actions
    modifyLoggingIOState $ \st -> st {liosLogger = oldLogger}
    return result

instance HasLogContext IO where
  getLogContext = do
    st <- getLogginngIOState
    return $ liosContext st

  withLogContext frame actions = do
    oldContext <- getLogContext
    modifyLoggingIOState $ \st -> st {liosContext = frame:oldContext}
    result <- actions
    modifyLoggingIOState $ \st -> st {liosContext = oldContext}
    return result