{-# LANGUAGE CPP #-}

module Sos.Job
  ( Job(..)
  , ShellCommand
  , runJob
  ) where

import Sos.FileEvent
import Sos.Utils

import Control.Concurrent.MVar (readMVar)
import Control.Exception
import Data.Function (on)
import Data.List.NonEmpty (NonEmpty)
import System.Exit
import System.IO
import System.IO.Error (tryIOError)
import System.Posix.Process (getProcessGroupID, getProcessGroupIDOf)
import System.Posix.Signals
  (Handler(Ignore), Signal, installHandler, sigTERM, sigTTOU,
    signalProcessGroup)
import System.Posix.Terminal (setTerminalProcessGroupID)
import System.Posix.Types (ProcessGroupID)
import System.Process
import System.Process.Internals (ProcessHandle__(OpenHandle), phandle)
import Text.Printf

import qualified Data.List.NonEmpty as NonEmpty

type ShellCommand = String

-- | A 'Job' is a list of shell commands to run, along with the 'FileEvent' that
-- triggered the job.
data Job = Job
  { jobEvent    :: FileEvent             -- ^ Event that triggered this job.
  , jobCommands :: NonEmpty ShellCommand -- ^ The list of shell commands to run.
  }

-- | Non-stanard Eq instance: Job equality compares only the shell commands it's
-- associated with.
instance Eq Job where
  (==) = (==) `on` jobCommands

-- | Run a Job's list of shell commands sequentially. If a command returns
-- ExitFailure, or an exception is thrown, propagate the exception.
runJob :: Job -> IO ()
runJob (NonEmpty.toList . jobCommands -> cmds0) = go 1 cmds0
 where
  go :: Int -> [ShellCommand] -> IO ()
  go _ [] = pure ()
  go n (cmd:cmds) = do
    putStrLn (magenta (printf "[%d/%d] " n (length cmds0)) <> cmd)

    let flushStdin :: IO ()
        flushStdin =
          hReady stdin >>= \case
            True -> getLine >> flushStdin
            False -> pure ()

    flushStdin

    try (runForegroundProcess (shell cmd)) >>= \case
      Left (ex :: SomeException) -> do
        case fromException ex of
          Just ThreadKilled -> do
            putStrLn (yellow "Job interrupted ✗")
            throwIO ThreadKilled
          _ -> do
            putStrLn (red (show ex))
            throwIO ex

      Right ExitSuccess -> do
        putStrLn (green "Success ✓")
        go (n+1) cmds

      Right (ExitFailure c) ->
        throwIO (ExitFailure c)

#ifdef mingw32_HOST_OS

runForegroundProcess :: CreateProcess -> IO ExitCode
runForegroundProcess c =
  bracket acquire release waitForProcess
 where
  acquire :: IO ProcessHandle
  acquire = do
    (_, _, _, ph) <- createProcess c { create_group = True }
    pure ph

  release :: ProcessHandle -> IO ()
  release ph = do
    _ <- tryIOError (interruptProcessGroupOf ph)
    terminateProcess ph

#else

runForegroundProcess :: CreateProcess -> IO ExitCode
runForegroundProcess c =
  bracket acquire release (\(ph, _) -> waitForProcess ph)
 where
  -- Create a process (inheriting all file descriptors) in a new process group
  -- and give it terminal access.
  acquire :: IO (ProcessHandle, ProcessGroupID)
  acquire = do
    (_, _, _, ph) <- createProcess (c { create_group = True })
    readMVar (phandle ph) >>= \case
      OpenHandle pid -> do
        pgid <- getProcessGroupIDOf pid
        setTerminalProcessGroupID 0 pgid
        pure (ph, pgid)
      _ -> error "Sos.Job.runForegroundProcess: unexpected process handle"

  -- Terminate a process and take back control of the terminal.
  release :: (ProcessHandle, ProcessGroupID) -> IO ()
  release (_, pgid) = do
    _ <- tryIOError (signalProcessGroup sigTERM pgid)
    getProcessGroupID >>= ignoring sigTTOU . setTerminalProcessGroupID 0

  ignoring :: Signal -> IO a -> IO a
  ignoring sig act =
    bracket
      (installHandler sig Ignore Nothing)
      (\handler -> installHandler sig handler Nothing)
      (\_ -> act)

#endif