module RawFilePath.Process.Common
    ( Process(..)
    , ProcessConf(..)
    , proc
    , processStdin
    , processStdout
    , processStderr
    , StreamType
    , mbFd
    , willCreateHandle
    , CreatePipe(..)
    , Inherit(..)
    , NoStream(..)
    , UseHandle(..)
    , setStdin
    , setStdout
    , setStderr

    , PHANDLE
    , ProcessHandle__(..)
    , modifyProcessHandle
    , withProcessHandle
    , fdStdin
    , fdStdout
    , fdStderr
    , mbPipe
    ) where

import RawFilePath.Import

-- extra modules

import System.Posix.Internals (FD)
import qualified GHC.IO.FD as FD

-- Original declarations

-- | The process configuration that is needed for creating new processes. Use
-- 'proc' to make one.
data ProcessConf stdin stdout stderr = ProcessConf
    { cmdargs :: [ByteString]
    -- ^ Executable & arguments, or shell command
    , cwd :: Maybe RawFilePath
    -- ^ Optional path to the working directory for the new process
    , env :: Maybe [(ByteString, ByteString)]
    -- ^ Optional environment (otherwise inherit from the current process)
    , cfgStdin :: stdin
    -- ^ How to determine stdin
    , cfgStdout :: stdout
    -- ^ How to determine stdout
    , cfgStderr :: stderr
    -- ^ How to determine stderr
    , closeFds :: Bool
    -- ^ Close all file descriptors except stdin, stdout and stderr in the new
    -- process
    , createGroup :: Bool
    -- ^ Create a new process group
    , delegateCtlc :: Bool
    -- ^ Delegate control-C handling. Use this for interactive console
    -- processes to let them handle control-C themselves (see below for
    -- details).
    , createNewConsole :: Bool
    -- ^ Use the windows CREATE_NEW_CONSOLE flag when creating the process;
    -- does nothing on other platforms.
    --
    -- Default: @False@
    , newSession :: Bool
    -- ^ Use posix setsid to start the new process in a new session; does nothing on other platforms.
    , childGroup :: Maybe GroupID
    -- ^ Use posix setgid to set child process's group id.
    --
    -- Default: @Nothing@
    , childUser :: Maybe UserID
    -- ^ Use posix setuid to set child process's user id.
    --
    -- Default: @Nothing@
    }

-- | Create a process configuration with the default settings.
proc
    :: RawFilePath -- ^ Command to run
    -> [ByteString] -- ^ Arguments to the command
    -> ProcessConf Inherit Inherit Inherit
proc cmd args = ProcessConf
    { cmdargs = cmd : args
    , cwd = Nothing
    , env = Nothing
    , cfgStdin = Inherit
    , cfgStdout = Inherit
    , cfgStderr = Inherit
    , closeFds = False
    , createGroup = False
    , delegateCtlc = False
    , createNewConsole = False
    , newSession = False
    , childGroup = Nothing
    , childUser = Nothing
    }

-- | Control how the standard input of the process will be initialized.
setStdin
    :: (StreamType newStdin)
    => ProcessConf oldStdin stdout stderr
    -> newStdin
    -> ProcessConf newStdin stdout stderr
setStdin p newStdin = p { cfgStdin = newStdin }
infixl 4 `setStdin`

-- | Control how the standard output of the process will be initialized.
setStdout
    :: (StreamType newStdout)
    => ProcessConf stdin oldStdout stderr
    -> newStdout
    -> ProcessConf stdin newStdout stderr
setStdout p newStdout = p { cfgStdout = newStdout }
infixl 4 `setStdout`

-- | Control how the standard error of the process will be initialized.
setStderr
    :: (StreamType newStderr)
    => ProcessConf stdin stdout oldStderr
    -> newStderr
    -> ProcessConf stdin stdout newStderr
setStderr p newStderr = p { cfgStderr = newStderr }
infixl 4 `setStderr`

-- | The process type. The three type variables denote how its standard
-- streams were initialized.
data Process stdin stdout stderr = Process
    { procStdin         :: Maybe Handle
    , procStdout        :: Maybe Handle
    , procStderr        :: Maybe Handle
    , phandle           :: !(MVar ProcessHandle__)
    , mbDelegateCtlc    :: !Bool
    , waitpidLock       :: !(MVar ())
    }

-- | Take a process and return its standard input handle.
processStdin :: Process CreatePipe stdout stderr -> Handle
processStdin Process{..} = fromMaybe err procStdin
  where
    err = error "This can't happen: stdin is CreatePipe but missing"

-- | Take a process and return its standard output handle.
processStdout :: Process stdin CreatePipe stderr -> Handle
processStdout Process{..} = fromMaybe err procStdout
  where
    err = error "This can't happen: stdout is CreatePipe but missing"

-- | Take a process and return its standard error handle.
processStderr :: Process stdin stdout CreatePipe -> Handle
processStderr Process{..} = fromMaybe err procStderr
  where
    err = error "This can't happen: stderr is CreatePipe but missing"

-- | Create a new pipe for the stream. You get a new 'Handle'.
data CreatePipe = CreatePipe deriving Show
-- | Inherit the parent (current) process handle. The child will share the
-- stream. For example, if the child writes anything to stdout, it will all go
-- to the parent's stdout.
data Inherit = Inherit deriving Show
-- | No stream handle will be passed. Use when you don't want to communicate
-- with a stream. For example, to run something silently.
data NoStream = NoStream deriving Show
-- | Use the supplied 'Handle'.
data UseHandle = UseHandle Handle deriving Show

-- | The class of types that determine the standard stream of a sub-process.
-- You can decide how to initialize the standard streams (stdin, stdout, and
-- stderr) of a sub-process with the instances of this class.
class StreamType c where
    mbFd :: FD -> c -> IO FD
    willCreateHandle :: c -> Bool
#if __GLASGOW_HASKELL__ >= 780
    mbFd = undefined
    willCreateHandle = undefined
    {-# MINIMAL #-}
#endif
instance StreamType CreatePipe where
    mbFd _ _ = return (-1)
    willCreateHandle _ = True
instance StreamType Inherit where
    mbFd std _ = return std
    willCreateHandle _ = False
instance StreamType NoStream where
    mbFd _ _ = return (-2)
    willCreateHandle _ = False
instance StreamType UseHandle where
    mbFd _std (UseHandle hdl) =
        withHandle "" hdl $ \Handle__{haDevice=dev,..} -> case cast dev of
            Just fd -> do
                -- clear the O_NONBLOCK flag on this FD, if it is set, since
                -- we're exposing it externally (see #3316 of 'process')
                fd' <- FD.setNonBlockingMode fd False
                return (Handle__{haDevice=fd',..}, FD.fdFD fd')
            Nothing -> ioError $ mkIOError illegalOperationErrorType
                "createProcess" (Just hdl) Nothing
                `ioeSetErrorString` "handle is not a file descriptor"
    willCreateHandle _ = False

-- Declarations from the process package (modified)

type PHANDLE = CPid

data ProcessHandle__ = OpenHandle PHANDLE
                     | OpenExtHandle PHANDLE PHANDLE PHANDLE
                     | ClosedHandle ExitCode

modifyProcessHandle
    :: Process stdin stdout stderr
    -> (ProcessHandle__ -> IO (ProcessHandle__, a))
    -> IO a
modifyProcessHandle p = modifyMVar (phandle p)

withProcessHandle
    :: Process stdin stdout stderr -> (ProcessHandle__ -> IO a) -> IO a
withProcessHandle p = withMVar (phandle p)

fdStdin, fdStdout, fdStderr :: FD
fdStdin  = 0
fdStdout = 1
fdStderr = 2

mbPipe :: StreamType c => c -> Ptr FD -> IOMode -> IO (Maybe Handle)
mbPipe streamConf pfd  mode = if willCreateHandle streamConf
    then fmap Just (pfdToHandle pfd mode)
    else return Nothing

pfdToHandle :: Ptr FD -> IOMode -> IO Handle
pfdToHandle pfd mode = do
  fd <- peek pfd
  let filepath = "fd:" ++ show fd
  (fD,fd_type) <- FD.mkFD (fromIntegral fd) mode
                       (Just (Stream,0,0)) -- avoid calling fstat()
                       False {-is_socket-}
                       False {-non-blocking-}
  fD' <- FD.setNonBlockingMode fD True -- see #3316
#if __GLASGOW_HASKELL__ >= 704
  enc <- getLocaleEncoding
#else
  let enc = localeEncoding
#endif
  mkHandleFromFD fD' fd_type filepath mode False {-is_socket-} (Just enc)