{-# LANGUAGE Safe #-}
{-# LANGUAGE OverloadedStrings #-}

-- | A library for enumerating OS process input and output with IterIO's stream transformers.

module Data.IterIO.Process
  ( enumProcess
  , cmd
  ) where

import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as L
import System.Process hiding (readProcess)
import System.IO(hClose, hFlush, Handle)
import System.Exit(ExitCode(ExitFailure))
import Control.Monad.IO.Class(liftIO, MonadIO)
import Data.IterIO
import Control.Exception(ErrorCall(ErrorCall))

-- | Create an Onum that starts and executable at the given FilePath,
--   passing it args, and routing the process's stdout to the Onum's
--   output stream.    If the executable fails, an exception is 
--   thrown with the contents of stderr.
enumProcess :: FilePath -> [String] -> Onum ByteString IO a
enumProcess p args = inumBracket (mkProc p args) cleanup procInum_

-- | Create an Inum that starts and executable at the given FilePath,
--   and passing it args.  The Inum's input stream is routed to stdin,
--   and stdout goes to the Inum's output stream.  If the executable
--   fails, an exception is thrown with the contents of stderr.
cmd :: MonadIO m => FilePath -> [String] -> Inum ByteString ByteString m a
cmd p args = inumBracket (mkProc p args) cleanup procInum


-- Make the process object
mkProc :: MonadIO m => FilePath -> [String] -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
mkProc p args = liftIO $ do
    createProcess (proc p args){
      std_in  = CreatePipe
    , std_out = CreatePipe
    , std_err = CreatePipe
    }

-- Cleanup routine for process inums
cleanup :: MonadIO m => (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> m ()
cleanup (Just i, Just o, Just e, _) = liftIO $ do
    hClose i
    hClose o
    hClose e
cleanup _ = error "the impossible happened"

-- Process Inum for an existing process
procInum :: (MonadIO m) => (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> Inum ByteString ByteString m a
procInum (Just i, Just o, Just e, pid) = mkInumM loop
  where
    loop = do
        -- Grab any data we have for stidin
        Chunk t eof <- chunkI
        liftIO $ L.hPut i t

        -- Close stdin if no more data.  This will allow the process exit.
        if eof
          then liftIO $ hClose i
          else liftIO $ hFlush i

        done <- feedStdout i o e pid
        if done
          then return L.empty
          else loop
procInum _ = error "unsupported configuration"


-- Process Onum for an existing process
procInum_ :: (MonadIO m) => (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> Onum ByteString m a
procInum_ (Just i, Just o, Just e, pid) = mkInumM loop
  where
    loop = do
        done <- feedStdout i o e pid
        if done
          then return L.empty
          else loop
procInum_ _ = error "unsupported configuration"


-- TODO: InumState is not exposed by the Inum module.  Is it possible to add a type signature here?
--feedStdout :: (MonadIO m, ChunkData t) => Handle -> Handle -> Handle -> ProcessHandle -> Iter t (IterStateT (InumState t ByteString m a) m) Bool
feedStdout i o e pid = do
    -- Close stdin if no more data.  This will allow the process exit.
    liftIO $ hClose i

    -- After stdin exits, the exe will write to stdout and then close it.
    maybeExitCode <- liftIO $ getProcessExitCode pid

    case maybeExitCode of
      Just (ExitFailure _) -> do
          msg <- liftIO $ B.hGetContents e
          throwI (ErrorCall (B8.unpack msg))

      _ -> return ()

    -- Grab any data we have on stdout
    output <- liftIO $ L.hGetNonBlocking o 1024

    -- Push proc's stdout to the inum's output stream
    notListening <- ifeed output

    -- Debate our future
    return $ notListening || maybeExitCode /= Nothing