{-# LANGUAGE CPP               #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  Yi.Debug
-- License     :  GPL-2
-- Maintainer  :  yi-devel@googlegroups.com
-- Stability   :  experimental
-- Portability :  portable
--
-- Debug utilities used throughout Yi.

module Yi.Debug ( initDebug, trace, traceM, traceM_, logPutStrLn
                , logError, logStream, Yi.Debug.error ) where

import Control.Concurrent
    ( dupChan, getChanContents, forkIO, myThreadId, Chan )
import Control.Monad.Base ( liftBase, MonadBase )
import Data.IORef ( readIORef, writeIORef, IORef, newIORef )
import Data.Monoid ( (<>) )
import qualified Data.Text as T ( pack, snoc, unpack, Text )
import GHC.Conc ( labelThread )
import System.IO
    ( hFlush, hPutStrLn, IOMode(WriteMode), openFile, Handle )
import System.IO.Unsafe ( unsafePerformIO )

#if __GLASGOW_HASKELL__ < 710
import Data.Time (formatTime, getCurrentTime)
import System.Locale (defaultTimeLocale)
#else
import Data.Time (formatTime, getCurrentTime, defaultTimeLocale)
#endif

dbgHandle :: IORef (Maybe Handle)
dbgHandle = unsafePerformIO $ newIORef Nothing
{-# NOINLINE dbgHandle #-}

-- | Set the file to which debugging output should be written. Though this
-- is called /init/Debug.
-- Debugging output is not created by default (i.e., if this function
-- is never called.)
-- The target file can not be changed, nor debugging disabled.
initDebug :: FilePath -> IO ()
initDebug f = do
  hndl <- readIORef dbgHandle
  case hndl of
    Nothing -> do openFile f WriteMode >>= writeIORef dbgHandle . Just
                  logPutStrLn "Logging initialized."
    Just _ -> logPutStrLn "Attempt to re-initialize the logging system."


-- | Outputs the given string before returning the second argument.
trace :: T.Text -> a -> a
trace s e = unsafePerformIO $ logPutStrLn s >> return e
{-# NOINLINE trace #-}

error :: T.Text -> a
error s = unsafePerformIO $ logPutStrLn s >> Prelude.error (T.unpack s)

logPutStrLn :: MonadBase IO m => T.Text -> m ()
logPutStrLn s = liftBase $
  readIORef dbgHandle >>= \case
    Nothing -> return ()
    Just h -> do
      time <-  getCurrentTime
      tId <- myThreadId
      let m = show tId ++ " " ++ T.unpack s
      hPutStrLn h $ formatTime defaultTimeLocale rfc822DateFormat' time ++ m
      hFlush h
  where
    -- A bug in rfc822DateFormat makes us use our own format string
    rfc822DateFormat' = "%a, %d %b %Y %H:%M:%S %Z"

logError :: MonadBase IO m => T.Text -> m ()
logError s = logPutStrLn $ "error: " <> s

logStream :: Show a => T.Text -> Chan a -> IO ()
logStream msg ch = do
  logPutStrLn $ "Logging stream " <> msg
  logThreadId <- forkIO $ logStreamThread msg ch
  labelThread logThreadId "LogStream"

logStreamThread :: Show a => T.Text -> Chan a -> IO ()
logStreamThread msg ch = do
  stream <- getChanContents =<< dupChan ch
  mapM_ logPutStrLn [ msg `T.snoc` '(' <> T.pack (show i) `T.snoc` ')'
                     <> T.pack (show event)
                    | (event, i) <- zip stream [(0::Int)..]
                    ]

-- | Traces @x@ and returns @y@.
traceM :: Monad m => T.Text -> a -> m a
traceM x y = trace x $ return y

-- | Like traceM, but returns ().
traceM_ :: Monad m => T.Text -> m ()
traceM_ x = traceM x ()