{-# LANGUAGE OverloadedStrings, TemplateHaskell, Trustworthy #-}
{-|
  This module defines the 'Format' datatype that describes how to format a log line.
  It also defines a template function @$(format _) :: 'Format'@ that allows C-style string formatting.
  The format string is thus parsed at compile time.

  The format string may contain the following elements:

  * %m - The logged message

  * %s - The severity of the message

  * %t - The name of the logging thread

  * %d(FORMAT) - A timestamp, formatted with FORMAT.

  The datetime FORMAT is a UNIX style format string detailed in "Data.Time.Format". The only difference is that closing brackets \')\'  inside the datetime format must be escaped with a backslash.

  Example:

  @
logFormat = $(format \"%d(%T) (%s) %t: %m\")
  @

  Which when logging with @'System.Log.SLog.logI' \"Something\"@ will produce something like:

  @
14:49:06 (INFO   ) main: Something
  @


  Example for escaping \')\' in the datetime format string:

  @
logFormat = $(format \"%d((%F\\\\)(%T\\\\)) %m\")
  @

  Which when logging with @'System.Log.SLog.logI' \"Something\"@ will produce:

  @
(2013-10-02)(16:26:21) Something
  @

  Note how we need an additional \'\\' because of Haskell strings
-}
module System.Log.SLog.Format
    (
      -- *Format
      FormatElem(..)
    , Format
    , format )
where

import Control.Applicative
import Language.Haskell.TH
import Language.Haskell.TH.Lift
import qualified Data.Text as T
import qualified Data.Attoparsec.Text as A
import Data.Time.Format
import Data.Time.LocalTime
import System.Locale
import Data.String

-- An inefficient orphan Lift instance for Text. It is run at compile time so no worries
instance Lift T.Text where
    lift t = let s = T.unpack t in [| fromString s :: T.Text |]

-- | A 'Lift'able formatting element
data FormatElemTH
    = MessageElemTH
    | SeverityElemTH
    | StringElemTH T.Text
    | DateTimeElemTH String
    | ThreadElemTH
      deriving (Show)

type FormatTH = [FormatElemTH]

-- 'FormatElem' is the same as FormatElemTH, onlt this time it stores the datetime formatting closure instead of the format string
-- | A 'FormatElem' is a formatting element
data FormatElem
    = MessageElem
    | SeverityElem
    | StringElem T.Text
    | DateTimeElem (ZonedTime -> T.Text)
    | ThreadElem

-- | 'Format' is the type of a full format. It is simply a list of 'FormatElem's
type Format = [FormatElem]

-- | finaliseFormat finalises a format that represents datetime elements as a UNIX datetime format string and turns them into a formatting closure. This method also concatenates adjacent 'StringElem's
finaliseFormat :: FormatTH -> Format
finaliseFormat (StringElemTH s0 : StringElemTH s1 : rest) = finaliseFormat $ StringElemTH (T.append s0 s1) : rest
finaliseFormat fs = map for' fs
  where
    for' MessageElemTH = MessageElem
    for' SeverityElemTH = SeverityElem
    for' (StringElemTH s) = StringElem s
    for' (DateTimeElemTH s) = DateTimeElem $ T.pack . formatTime defaultTimeLocale s
    for' ThreadElemTH = ThreadElem


-- | formatParser parses a format string into FormatElemTHs
formatParser :: A.Parser [ExpQ] -- FormatTH
formatParser
    = A.endOfInput *> return [] <|> do
        e <- A.choice [ A.takeWhile1 (/= '%') >>= \t -> return [|StringElemTH t|]
                      , A.char '%' *> elemParser
                      ]
        (e :) <$> formatParser

-- | elemParser parses a single FormatElemTH
elemParser :: A.Parser ExpQ -- FormatElemTH
elemParser = A.choice [ A.char '%' *> return [|StringElemTH "%"|]
                      , A.char 'm' *> return [|MessageElemTH|]
                      , A.char 's' *> return [|SeverityElemTH|]
                      , A.char 'd' *> datetimeParser >>= \f -> return [|DateTimeElemTH f|]
                      , A.char 't' *> return [|ThreadElemTH|]
                      ]

-- | datetimeParser parses a datetime element
datetimeParser :: A.Parser String
datetimeParser = fmap T.unpack $ A.char '(' *> datetimeParser' <* A.char ')'
  where
    datetimeParser' :: A.Parser T.Text
    datetimeParser' = do
      s <- A.takeWhile (\c -> c /= '\\' && c /= ')')
      A.choice [ A.string "\\)" *> ((\r -> T.concat [s, ")", r]) <$> datetimeParser')
               , liftA3
                   (\a b c -> a `T.cons` b `T.cons` c)
                   (A.char '\\')
                   A.anyChar
                   datetimeParser'
               , return s
               ]


-- | @$('format' _) :: 'Format'@ is a template function that parses the passed in format string, then finalises it and returns a 'Format'. 
format :: String -> ExpQ
format s = [| finaliseFormat $(mat s) |]

-- | $(mat _) :: FormatTH is a template function that parses the passed in format string and returns a FormatTH.
mat :: String -> ExpQ
mat s = case A.parseOnly formatParser $ T.pack s of
          Left err -> fail err
          Right exps -> ListE <$> sequence exps