{-# LANGUAGE OverloadedStrings #-}
module Data.Conduit.LogFile
    ( RotatingLog
    , openRotatingLog
    , addChunk
    , close
    , defaultMaxTotal
    , dummy
    ) where

import           Control.Concurrent             (forkIO)
import           Control.Concurrent.STM         (atomically)
import           Control.Concurrent.STM.TBQueue
import           Control.Concurrent.STM.TVar
import           Control.Exception              (bracket, bracketOnError,
                                                 finally)
import           Control.Monad                  (void, when)
import qualified Data.ByteString                as S
import           Data.Time                      (UTCTime, getCurrentTime)
import           Data.Word                      (Word)
import           System.Directory               (createDirectoryIfMissing,
                                                 doesFileExist, renameFile)
import           System.FilePath                ((<.>), (</>))
import qualified System.IO                      as SIO
import           System.IO.Unsafe               (unsafePerformIO)
import           System.Mem.Weak                (addFinalizer)

data Command = AddChunk !S.ByteString
             | Close

-- | Represents a folder used for totating log files.
--
-- Since 0.2.1
data RotatingLog = RotatingLog !(TVar State)
-- Use a data instead of a newtype so that we can attach a finalizer.

-- | A @RotatingLog@ which performs no logging.
--
-- Since 0.2.1
dummy :: RotatingLog
dummy = RotatingLog $! unsafePerformIO $! newTVarIO Closed

data State = Closed
           | Running !SIO.Handle !(TBQueue Command)

queue :: Command -> RotatingLog -> IO ()
queue cmd (RotatingLog ts) = atomically $ do
    s <- readTVar ts
    case s of
        Closed -> return ()
        Running _ q -> writeTBQueue q cmd

addChunk :: RotatingLog -> S.ByteString -> IO ()
addChunk lf bs = queue (AddChunk bs) lf

close :: RotatingLog -> IO ()
close = queue Close

-- | Create a new @RotatingLog@.
--
-- Since 0.2.1
openRotatingLog :: FilePath -- ^ folder to contain logs
                -> Word -- ^ maximum log file size, in bytes
                -> IO RotatingLog
openRotatingLog dir maxTotal = do
    createDirectoryIfMissing True dir
    bracketOnError (moveCurrent dir) SIO.hClose $ \handle -> do
        queue' <- newTBQueueIO 5
        let s = Running handle queue'
        ts <- newTVarIO s
        void $ forkIO $ loop dir ts maxTotal
        let rl = RotatingLog ts
        addFinalizer rl (atomically (writeTBQueue queue' Close))
        return rl

current :: FilePath -- ^ folder containing logs
        -> FilePath
current = (</> "current.log")

moveCurrent :: FilePath -- ^ folder containing logs
            -> IO SIO.Handle -- ^ new handle
moveCurrent dir = do
    let curr = current dir
    x <- doesFileExist curr
    when x $ do
        now <- getCurrentTime
        renameFile curr $ dir </> suffix now
    SIO.openFile curr SIO.WriteMode

suffix :: UTCTime -> FilePath
suffix now =
    (concatMap fix $ takeWhile (/= '.') $ show now) <.> "log"
  where
    fix ' ' = "_"
    fix c | '0' <= c && c <= '9' = [c]
    fix _ = ""

loop :: FilePath -- ^ folder containing logs
     -> TVar State
     -> Word -- ^ maximum total log size
     -> IO ()
loop dir ts maxTotal =
    go 0 `finally` (closeCurrentHandle `finally` atomically (writeTVar ts Closed))
  where
    closeCurrentHandle = bracket
        (atomically $ do
            s <- readTVar ts
            case s of
                Closed -> return Nothing
                Running h _ -> return $! Just h)
        (maybe (return ()) SIO.hClose)
        (const $ return ())

    go total = do
        res <- atomically $ do
            s <- readTVar ts
            case s of
                Closed -> return Nothing
                Running handle queue' -> do
                    cmd <- readTBQueue queue'
                    case cmd of
                        Close -> return Nothing
                        AddChunk bs -> return $! Just (handle, queue', bs)
        case res of
            Nothing -> return ()
            Just (handle, queue', bs) -> do
                let total' = total + fromIntegral (S.length bs)
                S.hPut handle bs
                SIO.hFlush handle
                if total' > maxTotal
                    then do
                        bracket
                            (SIO.hClose handle >> moveCurrent dir)
                            (\handle' -> atomically $ writeTVar ts $ Running handle' queue')
                            (const $ return ())
                        go 0
                    else go total'

defaultMaxTotal :: Word
defaultMaxTotal = 5 * 1024 * 1024 -- 5 MB