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

module System.Log.Heavy.Util
  (
    -- * Functions of common use
    logMessage,
    -- * Utilities for backends implementation
    checkLogLevel, checkLogLevel',
    checkContextFilter, checkContextFilter', checkContextFilterM
  ) where

import Control.Monad (when)
import Control.Monad.Trans
import Data.List (isPrefixOf)

import System.Log.Heavy.Types
import System.Log.Heavy.Level

-- | Check if message source and level passes specified filters.
--
-- The message is passed if:
--
-- * No @include@ filters are defined in context stack, OR the message conforms to ANY of @include@ filters;
--
-- * AND the message does not conform to any of @exclude@ filters in the stack.
--
checkContextFilter' :: [LogContextFilter] -> LogSource -> Level -> Bool
checkContextFilter' filters source level =
  let includeFilters = [fltr | LogContextFilter (Just fltr) _ <- filters]
      excludeFilters = [fltr | LogContextFilter _ (Just fltr) <- filters]
      includeOk = null includeFilters || or [checkLogLevel' fltr source level | fltr <- includeFilters]
      excludeOk = or [checkLogLevel' fltr source level | fltr <- excludeFilters]
  in  includeOk && not excludeOk

-- | Check if message matches filters from logging context.
--
-- The message is passed if:
--
-- * No @include@ filters are defined in context stack, OR the message conforms to ANY of @include@ filters;
--
-- * AND the message does not conform to any of @exclude@ filters in the stack.
--
checkContextFilter :: LogContext -> LogMessage -> Bool
checkContextFilter context msg =
  checkContextFilter' (map lcfFilter context) (lmSource msg) (lmLevel msg)

-- | Check if message matches filters from logging context.
-- This function is similar to @checkContextFilter@, but uses current context
-- from monadic state.
checkContextFilterM :: HasLogContext m => LogMessage -> m Bool
checkContextFilterM msg = do
  context <- getLogContext
  return $ checkContextFilter context msg

-- | Check if message level matches given filter.
checkLogLevel :: LogFilter -> LogMessage -> Bool
checkLogLevel fltr m =
    checkLogLevel' fltr (lmSource m) (lmLevel m)

-- | Check if message level matches given filter.
checkLogLevel' :: LogFilter -> LogSource -> Level -> Bool
checkLogLevel' fltr source level =
    case lookup (bestMatch source (map fst fltr)) fltr of
      Nothing -> False
      Just min -> level <= min
  where
    bestMatch :: LogSource -> [LogSource] -> LogSource
    bestMatch src list = go [] src list

    go :: LogSource -> LogSource -> [LogSource] -> LogSource
    go best src [] = best
    go best src (x:xs)
      | src == x = x
      | (x `isPrefixOf` src) && (length x > length best) = go x src xs
      | otherwise = go best src xs

-- | Log a message. This will add current context to context specified
-- in the message.
-- This function checks current context filter.
logMessage :: forall m. (HasLogging m, MonadIO m) => LogMessage -> m ()
logMessage msg = do
  ok <- checkContextFilterM msg
  when ok $ do
    context <- getLogContext
    logger <- getLogger
    liftIO $ logger $ msg {lmContext = context ++ lmContext msg}