{-# LANGUAGE OverloadedStrings, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, TypeFamilies, GeneralizedNewtypeDeriving, StandaloneDeriving, MultiParamTypeClasses, UndecidableInstances, AllowAmbiguousTypes, ScopedTypeVariables, FunctionalDependencies, FlexibleContexts, ConstraintKinds #-}

-- | This module contains generic types definition, along with some utilities.
module System.Log.Heavy.Types
  (
    -- * Data types
    LogSource, LogMessage (..), LogFilter, LogContextFrame (..), LogContext,
    IsLogBackend (..), LogBackendSettings (..), LoggingSettings (..),
    AnyLogBackend (..), LogContextFilter (..),
    include, exclude, noChange,
    Logger,SpecializedLogger,
    HasLogBackend (..), HasLogContext (..), HasLogging,
    HasLogger (..),
    -- * Main functions
    logMessage',
    applyBackend,
    defaultLogFilter,
    withLogVariable,
    -- * Utility functions
    splitString, splitDots,
  ) where

import Control.Monad.Reader
import Control.Monad.Logger (MonadLogger (..))
import Control.Monad.Trans.Control
import Control.Exception.Lifted (bracket)
import Language.Haskell.TH
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Format.Heavy as F
import System.Log.FastLogger

import System.Log.Heavy.Level

-- | Log message source. This is usually a list of program module names,
-- for example @[\"System\", \"Log\", \"Heavy\", \"Types\"]@.
type LogSource = [String]

-- | Log message structure. You usually will want to use some sort
-- of shortcut function to create messages. There are some provided
-- by this package:
--
-- * @System.Log.Heavy.Shortcuts@ module exports simple functions, that can be used
--   in simple cases, when you do not want to write or check message source.
--
-- * @System.Log.Heavy.TH@ module exports TH macros, which correctly fill message
--   source and location.
--
data LogMessage = forall vars. F.ClosedVarContainer vars => LogMessage {
    lmLevel :: Level    -- ^ Log message level
  , lmSource :: LogSource  -- ^ Log message source (module)
  , lmLocation :: Loc      -- ^ Log message source (exact location). You usually
                           --   will want to use TH quotes to fill this.
  , lmFormatString :: TL.Text -- ^ Log message string format (in @Data.Text.Format.Heavy@ syntax)
  , lmFormatVars :: vars   -- ^ Log message substitution variables. Use @()@ if you do not have variables.
  , lmContext :: LogContext -- ^ Logging context. Authomatically filled by @logMessage@.
  }

-- | Log messages filter by source and level.
--
-- Semantics under this is that @(source, severity)@ pair allows to write
-- messages from @source@ of @severity@ (and all more important messages) to log.
type LogFilter = [(LogSource, Level)]

-- | Default log messages filter. This says pass all messages
-- of level Info or higher.
defaultLogFilter :: LogFilter
defaultLogFilter = [([], info_level)]

-- | One frame in logging context stack.
data LogContextFrame = LogContextFrame {
      lcfVariables :: [(TL.Text, F.Variable)] -- ^ Context variables
    , lcfFilter :: LogContextFilter           -- ^ Context events filter
  }
  deriving (Show)

-- | Events filter for specific logging context.
data LogContextFilter =
  LogContextFilter {
      setInclude :: Maybe LogFilter  -- ^ Positive filter (include specified messages)
    , setExclude :: Maybe LogFilter  -- ^ Negative filter (exclude specified messages)
  }
  deriving (Eq, Show)

-- | Do not affect context filter settings
noChange :: LogContextFilter
noChange = LogContextFilter Nothing Nothing

-- | Create filter which includes only specified messages
include :: LogFilter -> LogContextFilter
include f = LogContextFilter (Just f) Nothing

-- | Create filter which just excludes specified messages
exclude :: LogFilter -> LogContextFilter
exclude f = LogContextFilter Nothing (Just f)

-- | Logging context stack
type LogContext = [LogContextFrame]

-- | Logging backend class.
class IsLogBackend b where
  -- | Logging backend settings data type
  data LogBackendSettings b

  -- | Create logger from backend
  makeLogger :: Logger b

  -- | Initialize logging backend from settings
  initLogBackend :: LogBackendSettings b -> IO b

  -- | Should return True if the specified message would be
  -- actually written to the log. Default implementation 
  -- always returns True.
  wouldWriteMessage :: b -> LogMessage -> IO Bool
  wouldWriteMessage _ _ = return True

  -- | Cleanup logging backend (release resources and so on)
  cleanupLogBackend :: b -> IO ()

  -- | Bracket function. Concrete implementations usually
  -- do not have to override default implementation.
  withLoggingB :: (MonadBaseControl IO m, MonadIO m)
            => LogBackendSettings b
            -> (b -> m a)
            -> m a
  withLoggingB settings actions = do
    bracket (liftIO $ initLogBackend settings)
            (liftIO . cleanupLogBackend)
            (actions)

-- | Container data type for representing arbitrary logging backend.
data AnyLogBackend = forall b. IsLogBackend b => AnyLogBackend b

instance IsLogBackend AnyLogBackend where
  data LogBackendSettings AnyLogBackend =
    AnyLogBackendSettings LoggingSettings

  makeLogger (AnyLogBackend backend) = makeLogger backend

  wouldWriteMessage (AnyLogBackend backend) msg =
    wouldWriteMessage backend msg

  initLogBackend (AnyLogBackendSettings (LoggingSettings settings)) =
    AnyLogBackend `fmap` initLogBackend settings

  cleanupLogBackend (AnyLogBackend backend) = cleanupLogBackend backend

-- | Constraint for monads in which it is possible to obtain logging backend.
class IsLogBackend b => HasLogBackend b m where
  getLogBackend :: m b

-- | A container for arbitrary logging backend.
-- You usually will use this similar to:
--
-- @
--  getLoggingSettings :: String -> LoggingSettings
--  getLoggingSettings "syslog" = LoggingSettings defaultsyslogsettings
-- @
data LoggingSettings = forall b. IsLogBackend b => LoggingSettings (LogBackendSettings b)

-- | Logging function
type Logger backend = backend -> LogMessage -> IO ()

-- | Logging function applied to concrete backend
type SpecializedLogger = LogMessage -> IO ()

-- | Type class for monads that can write logs
class Monad m => HasLogger m where
  getLogger :: m SpecializedLogger

  -- | Change logger to specified one locally
  localLogger :: SpecializedLogger -> m a -> m a

-- instance (Monad m, MonadReader SpecializedLogger m) => HasLogger m where
--   getLogger = ask
--   localLogger l = local (const l)

-- | Apply logging backend locally.
applyBackend :: (IsLogBackend b, HasLogger m) => b -> m a -> m a
applyBackend b actions = do
  let logger = makeLogger b
  localLogger logger actions

-- | Type class for monads that store logging context
class Monad m => HasLogContext m where
  -- | Execute actions within logging context frame
  withLogContext :: LogContextFrame -> m a -> m a

  -- | Obtain currently active logging context stack
  getLogContext :: m LogContext

-- GHC will not be able to select instance for LoggingT.
-- instance (Monad m, HasLogBackend b m) => HasLogBackend AnyLogBackend m where
--   getLogBackend = do
--     backend <- getLogBackend :: m b
--     return $ AnyLogBackend backend

-- | Convinience constraint synonym.
type HasLogging m = (HasLogger m, HasLogContext m)

-- | Shortcut function to execute actions within logging context frame,
-- which contains only one variable
withLogVariable :: (HasLogContext m, F.Formatable v)
                => TL.Text -- ^ Variable name
                -> v       -- ^ Variable value
                -> m a     -- ^ Actions to execute within context frame
                -> m a
withLogVariable name value =
  withLogContext (LogContextFrame [(name, F.Variable value)] noChange)

-- | Compatibility instance.
instance (Monad m, MonadIO m, HasLogging m) => MonadLogger m where
  monadLoggerLog loc src level msg = do
      logger <- getLogger
      context <- getLogContext
      liftIO $ logger $ LogMessage {
                          lmLevel = logLevelToLevel level,
                          lmSource = src',
                          lmLocation = loc,
                          lmFormatString = textFromLogStr msg,
                          lmFormatVars = (),
                          lmContext = context
                        }
    where
      src' = splitDots $ T.unpack src

      textFromLogStr :: ToLogStr str => str -> TL.Text
      textFromLogStr str = TL.fromStrict $ TE.decodeUtf8 $ fromLogStr $ toLogStr str

instance F.Formatable LogStr where
  formatVar fmt str = F.formatVar fmt $ fromLogStr str

-- | Simple implementation of splitting string by character.
splitString       :: Char -> String -> [String]
splitString _ ""  =  []
splitString c s   =  let (l, s') = break (== c) s
                 in  l : case s' of
                           []      -> []
                           (_:s'') -> splitString c s''

-- | Split string by dots
splitDots :: String -> [String]
splitDots = splitString '.'

-- | Log a message. This version is for monads that do not know about logging contexts.
logMessage' :: forall m. (HasLogger m, MonadIO m) => LogMessage -> m ()
logMessage' msg = do
  logger <- getLogger
  liftIO $ logger msg