{-# LANGUAGE CPP #-}
{-# LANGUAGE BangPatterns #-}

module Di.Handle
 ( stderr
 , handle
 , blob
 , LineRenderer(LineRendererUtf8)
 , BlobRenderer(BlobRenderer)
 ) where

import qualified Control.Monad.Catch as Ex
import Control.Monad.IO.Class (MonadIO(liftIO))
import qualified Data.ByteString.Builder as BB
import Data.Monoid ((<>))
import qualified System.IO as IO

#ifdef VERSION_unix
import qualified System.Posix.Terminal
import qualified System.Posix.IO
#endif

import Di.Core (Log)

--------------------------------------------------------------------------------

-- | How to render a 'Log' as a line of text.
data LineRenderer level path msg
  = LineRendererUtf8 !(Bool -> Log level path msg -> BB.Builder)
  -- ^ The returned bytes must not contain a leading nor trailing newline.
  --
  -- The 'Bool' tells whether we are trying to write these bytes to a
  -- terminal that supports ANSI colors.

-- | How to render a 'Log' as a binary blob.
data BlobRenderer level path msg
  = BlobRenderer !(Log level path msg -> BB.Builder)

--------------------------------------------------------------------------------

-- | Like 'blob', but each 'Log' is rendered as text in its own line.
--
-- If the given 'IO.Handle' is associated to a TTY supporting ANSI colors, and
-- the given 'LineRenderer' supports rendering with colors, and you ask for
-- it, then you will get colorful output.
handle
  :: (MonadIO m, MonadIO n)
  => Maybe Bool
  -- ^ Whether to render with colors.
  --
  -- If 'Nothing', then we'll render with ANSI colors when the given
  -- 'IO.Handle' is a TTY. You probably want to use 'Nothing' here most of the
  -- time.
  -> IO.Handle  -- ^ Handle where to write 'Log's.
  -> LineRenderer level path msg -- ^ How to render each 'Log'.
  -> m (Log level path msg -> n ())
handle ywantColors h (LineRendererUtf8 render0) = liftIO $ do
  wantColors <- maybe (isTty h) pure ywantColors
  let !render1 = render0 wantColors
      !newline = BB.char7 '\n'
      render2 = \log' -> render1 log' <> newline
  blob h (BlobRenderer render2)

-- | Write 'Log's to a 'IO.Handle' as a binary blob.
blob
  :: (MonadIO m, MonadIO n)
  => IO.Handle -- ^ Handle where to write 'Log's.
  -> BlobRenderer level path msg -- ^ How to render each 'Log'.
  -> m (Log level path msg -> n ())
blob h (BlobRenderer render) = liftIO $ do
  IO.hSetBinaryMode h True
  pure $ \log_ ->
     liftIO (Ex.finally (BB.hPutBuilder h (render log_))
                        (IO.hFlush h))

-- | 'Log's are written to 'IO.stderr', one per line.
--
-- /WARNING/ Currently this always renders as UTF-8.
stderr
  :: (MonadIO m, MonadIO n)
  => LineRenderer level path msg -- ^ How to render each 'Log' line.
  -> m (Log level path msg -> n ())
stderr = handle Nothing IO.stderr

--------------------------------------------------------------------------------

isTty :: IO.Handle -> IO Bool
#ifdef VERSION_unix
isTty h
  | h == IO.stderr = q System.Posix.IO.stdError
  | h == IO.stdout = q System.Posix.IO.stdOutput
  | h == IO.stdin  = q System.Posix.IO.stdInput
  | otherwise = pure False  -- Is this good enough?
  where q = System.Posix.Terminal.queryTerminal
#else
isTty _ = pure False
#endif