-- | Flexible control of progress reporting for readCreateProcess and friends.

{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE UndecidableInstances #-}

module System.Process.Run
    ( 
    -- * Monad transformer
      RunT
    , runT
    , RunState(..)
    , OutputStyle(..)
    -- * Monad class
    , RunM
    -- * Modify moand RunM state parameters
    , echoStart
    , echoEnd
    , output
    , silent
    , dots
    , indent
    , vlevel
    , quieter
    , noisier
    , lazy
    , strict
    , message
    -- * Monadic process runner
    , run
    -- * Re-exports
    , module System.Process.ListLike
    ) where

#if __GLASGOW_HASKELL__ <= 709
import Data.Monoid (Monoid, mempty)
#endif
import Control.Monad (when)
import Control.Monad.State (evalState, evalStateT, get, modify, MonadState, put, StateT)
import Control.Monad.Trans (MonadIO, lift, liftIO)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as Lazy (ByteString)
import Data.Char (ord)
import Data.Default (Default(def))
import Data.ListLike as ListLike
    (break, fromList, head, hPutStr, length, ListLike, null, putStr, singleton, tail)
import Data.Monoid ((<>))
import Data.String (IsString, fromString)
import Data.Text (Text)
import Data.Word (Word8)
import qualified Data.Text.Lazy as Lazy (Text)
import System.IO (hPutStr, hPutStrLn, stderr)
import System.Process.ListLike

-- | This is the state record that controls the output style.
data RunState text
    = RunState
      { _output :: OutputStyle -- ^ Overall style of output
      , _outprefix :: text     -- ^ Prefix for lines of stdout
      , _errprefix :: text     -- ^ Prefix for lines of stderr
      , _echoStart :: Bool     -- ^ Echo command as process starts
      , _echoEnd :: Bool       -- ^ Echo command as process finishes
      , _verbosity :: Int      -- ^ A progression of progress modes
      , _lazy :: Bool          -- ^ Use the lazy or strict runner?
      , _message :: text       -- ^ Extra text for start/end message - e.g. the change root
      }

type RunT text m = StateT (RunState text) m

class (MonadState (RunState text) m,
       ProcessText text char,
       ListLikeProcessIO text char,
       MonadIO m, IsString text, Eq char, Dot char) =>
    RunM text char m

instance Dot Word8 where
    dot = fromIntegral (ord '.')

instance (MonadIO m, MonadState (RunState String) m) => RunM String Char m
instance (MonadIO m, MonadState (RunState Text) m) => RunM Text Char m
instance (MonadIO m, MonadState (RunState Lazy.Text) m) => RunM Lazy.Text Char m
instance (MonadIO m, MonadState (RunState ByteString) m) => RunM ByteString Word8 m
instance (MonadIO m, MonadState (RunState Lazy.ByteString) m) => RunM Lazy.ByteString Word8 m

runT :: forall m text char a. (MonadIO m, ProcessText text char) => RunT text m a -> m a
runT action = evalStateT action (def :: RunState text)

data OutputStyle
    = Dots Int  -- ^ Output one dot per n output characters
    | All       -- ^ send process stdout to console stdout and process stderr to console stderr
    | Indented  -- ^ Output with prefixes
    | Silent    -- ^ No output

instance ProcessText text char => Default (RunState text) where
    def = RunState { _outprefix = fromString "1> "
                   , _errprefix = fromString "2> "
                   , _output = All
                   , _echoStart = True
                   , _echoEnd = True
                   , _verbosity = 3
                   , _lazy = False
                   , _message = mempty }

{-
class (Monoid text, MonadIO m) => MonadRun m text where
    type Text m
    getRunState :: m (RunState text)
    putRunState :: RunState text -> m ()

instance Monoid text => MonadRun IO text where
    getRunState = return def
    putRunState _ = return ()

instance (MonadIO m, Monoid t, MonadState (RunState t) m) => MonadRun m t where
    getRunState = get
    putRunState = put
-}

noEcho :: (MonadState (RunState t) m) => m ()
noEcho = modify (\x -> x { _echoStart = False, _echoEnd = False })

echoStart :: (MonadState (RunState t) m) => m ()
echoStart = modify (\x -> x { _echoStart = True })

echoEnd :: (MonadState (RunState t) m) => m ()
echoEnd = modify (\x -> x { _echoEnd = True })

output :: (MonadState (RunState t) m) => m ()
output = modify (\x -> x { _output = All })

silent :: (MonadState (RunState t) m) => m ()
silent = modify (\x -> x { _output = Silent })

dots :: (MonadState (RunState t) m) => Int -> m ()
dots n = modify (\x -> x { _output = Dots n })

-- | Modify the indentation prefixes for stdout and stderr in the
-- progress monad.
indent :: (MonadState (RunState t) m, ListLike t char) => (t -> t) -> (t -> t) -> m ()
indent so se = modify $ \x ->
    let so' = so (_outprefix x)
        se' = se (_errprefix x) in
    x { _outprefix = so'
      , _errprefix = se'
      , _output = if ListLike.null so' &&
                     ListLike.null se' then _output x else Indented }

noIndent :: (MonadState (RunState text) m, ListLike text char) => m ()
noIndent = indent (const mempty) (const mempty)

-- | Set verbosity to a specific level from 0 to 3.
-- vlevel :: (MonadIO m, Monoid text, MonadState (RunState text) m) => Int -> m ()
-- vlevel :: forall m text char. (IsString text, ListLike text char, MonadIO m) => Int -> m ()
vlevel :: forall m text char.
          (IsString text, ListLike text char, MonadIO m, MonadState (RunState text) m) =>
          Int -> m ()
vlevel n = do
  modify (\x -> x {_verbosity = n})
  case n of
    _ | n <= 0 -> noEcho >> silent >> noIndent -- No output
    1 -> vlevel 0 >> echoStart                 -- Output command at start
    2 -> vlevel 1 >> echoEnd >> dots 100       -- Output command at start and end, dots to show output
    _ ->                                       -- echo command at start and end, and send all output
                                               -- to the console with channel prefixes 1> and 2>
          vlevel 2 >> output >> indent (const (fromString "1> ")) (const (fromString ("2> ")))

quieter :: RunM text char m => m ()
quieter = get >>= \x -> vlevel (_verbosity x - 1)

noisier :: RunM text char m => m ()
noisier = get >>= \x -> vlevel (_verbosity x + 1)

strict :: RunM text char m => m ()
strict = modify (\x -> x { _lazy = False })

lazy :: RunM text char m => m ()
lazy = modify (\x -> x { _lazy = True})

message :: RunM text char m => (text -> text) -> m ()
message f = modify (\x -> x { _message = f (_message x) })

class Dot c where
    dot :: c

instance Dot Char where
    dot = '.'

run' :: forall m maker text char.
        (RunM text char m,
         ProcessMaker maker) =>
        maker -> text -> m [Chunk text]
run' maker input = do
  st0 <- get
  when (_echoStart st0) (liftIO $ hPutStrLn stderr ("-> " ++ showProcessMakerForUser maker))
  result <- liftIO $ (if _lazy st0 then readCreateProcessLazy else readCreateProcess) maker input >>= doOutput st0
  when (_echoEnd st0) (liftIO $ hPutStrLn stderr ("<- " ++ showProcessMakerForUser maker))
  return result
    where
      doOutput :: RunState text -> [Chunk text] -> IO [Chunk text]
      doOutput (RunState {_output = Dots n}) cs = putDotsLn n cs
      doOutput (RunState {_output = Silent}) cs = return cs
      doOutput (RunState {_output = All}) cs = writeOutput cs
      doOutput (RunState {_output = Indented, _outprefix = outp, _errprefix = errp}) cs = writeOutputIndented outp errp cs

run :: forall m maker text char result.
       (RunM text char m,
        ProcessMaker maker,
        ProcessResult text result) =>
       maker -> text -> m result
run maker input = run' maker input >>= return . collectOutput

-- | Output the dotified text of a chunk list with a newline at EOF.
-- Returns the original list.
putDotsLn :: (ListLikeProcessIO text char, Dot char) =>
             Int -> [Chunk text] -> IO [Chunk text]
putDotsLn cpd chunks = putDots cpd chunks >>= \ r -> System.IO.hPutStr stderr "\n" >> return r

-- | Output the dotified text of a chunk list. Returns the original
-- (undotified) list.
putDots :: (ListLikeProcessIO text char, Dot char) => Int -> [Chunk text] -> IO [Chunk text]
putDots charsPerDot chunks =
    evalStateT (mapM (\ x -> dotifyChunk charsPerDot x >>= mapM_ (lift . putChunk) >> return x) chunks) 0

-- | dotifyChunk charsPerDot dot chunk - Replaces every charsPerDot
-- characters in the Stdout and Stderr chunks with one dot.  Runs in
-- the state monad to keep track of how many characters had been seen
-- when the previous chunk finished.  chunks.
dotifyChunk :: forall text char m. (Monad m, ListLike text char, Dot char) =>
               Int -> Chunk text -> StateT Int m [Chunk text]
dotifyChunk charsPerDot chunk =
    case chunk of
      Stdout x -> doChars (ListLike.length x)
      Stderr x -> doChars (ListLike.length x)
      _ -> return [chunk]
    where
      doChars :: Int -> StateT Int m [Chunk text]
      doChars count = do
        remaining <- get
        let (count', remaining') = divMod (remaining + count) (fromIntegral charsPerDot)
        put remaining'
        if (count' > 0) then return [Stderr (ListLike.fromList (replicate count' dot))] else return []

-- | Write the Stdout chunks to stdout and the Stderr chunks to stderr.
putChunk :: ListLikeProcessIO text char => Chunk text -> IO ()
putChunk (Stdout x) = ListLike.putStr x
putChunk (Stderr x) = ListLike.hPutStr stderr x
putChunk _ = return ()

writeOutputIndented :: (ListLikeProcessIO text char, Eq char, IsString text) =>
                       text -> text -> [Chunk text] -> IO [Chunk text]
writeOutputIndented outp errp chunks =
    mapM (\(c, cs) -> mapM_ writeChunk cs >> return c) (indentChunks outp errp chunks)

-- | Pure function to indent the text of a chunk list.
indentChunks :: forall text char. (ListLikeProcessIO text char, Eq char, IsString text) =>
                text -> text -> [Chunk text] -> [(Chunk text, [Chunk text])]
indentChunks outp errp chunks =
    evalState (mapM (indentChunk nl outp errp) chunks) BOL
    where
      nl :: char
      nl = ListLike.head (fromString "\n" :: text)

-- | The monad state, are we at the beginning of a line or the middle?
data BOL = BOL | MOL deriving (Eq)

-- | Indent the text of a chunk with the prefixes given for stdout and
-- stderr.  The state monad keeps track of whether we are at the
-- beginning of a line - when we are and more text comes we insert one
-- of the prefixes.
indentChunk :: forall m text char.
               (Eq char, ListLike text char, MonadState BOL m) =>
               char -> text -> text -> Chunk text -> m (Chunk text, [Chunk text])
indentChunk nl outp errp chunk =
    case chunk of
      Stdout x -> doText Stdout outp x >>= return . (chunk,)
      Stderr x -> doText Stderr errp x >>= return . (chunk,)
      _ -> return (chunk, [chunk])
    where
      -- doText :: (a -> Chunk a) -> a -> a -> StateT BOL m [Chunk a]
      doText con pre x = do
        let (hd, tl) = ListLike.break (== nl) x
        hd' <- doHead con pre hd
        tl' <- doTail con pre tl
        return $ hd' <> tl'
      -- doHead :: (a -> Chunk a) -> a -> a -> StateT BOL m [Chunk a]
      doHead _ _ x | ListLike.null x = return []
      doHead con pre x = do
        bol <- get
        case bol of
          BOL -> put MOL >> return [con (pre <> x)]
          MOL -> return [con x]
      -- doTail :: (a -> Chunk a) -> a -> a -> StateT BOL m [Chunk a]
      doTail _ _ x | ListLike.null x = return []
      doTail con pre x = do
        bol <- get
        put BOL
        tl <- doText con pre (ListLike.tail x)
        return $ (if bol == BOL then [con pre] else []) <> [con (singleton nl)] <> tl
