{-|
Module      : Hakyll.Process
Description : Common compilers and helpers for external executables.
Stability   : experimental
-}
module Hakyll.Process
    (
      newExtension
    , newExtOutFilePath
    , execName
    , execCompiler
    , execCompilerWith
    , unsafeExecCompiler
    , CompilerOut(..)
    , ExecutableArg(..)
    , ExecutableArgs
    , ExecutableName
    , OutFilePath(..)
    ) where

import qualified Data.ByteString.Lazy.Char8 as B
import           GHC.Conc                        (atomically)
import           Hakyll.Core.Item
import           Hakyll.Core.Compiler
import           System.Process.Typed

-- | Expected output from the external compiler.
data CompilerOut   =
  -- | Compiler uses stdout as the output type.
    CStdOut
  -- | Compiler outputs to a specific target file on the filesystem.
  | COutFile OutFilePath

-- | Arguments to provide to the process
data ExecutableArg =
  -- | Abstract representation of the path to the Hakyll item.
    HakFilePath
  -- | Literal argument to provide to the other process.
  | ProcArg String deriving (Read, Show)

-- | Specifies the output file path of a process.
data OutFilePath   =
  -- | A specific, known filepath.
    SpecificPath FilePath
  -- | Indicates that the output path is related to the input path.
  | RelativePath (FilePath -> FilePath)

-- | Name of the executable if in the PATH or path to it if not
newtype ExecutableName = ExecutableName  String    deriving (Read, Show)
-- | Arguments to pass to the executable. Empty if none.
type ExecutableArgs    = [ExecutableArg]

-- | Helper function to indicate that the output file name is the same as the input file name with a new extension
-- Note: like hakyll, assumes that no "." is present in the extension
newExtension ::
     String   -- ^ New file extension, excluding the leading "."
  -> FilePath -- ^ Original FilePath
  -> FilePath
newExtension ext f = (reverse . dropWhile (/= '.') . reverse $ f) <> ext

-- | Helper function to indicate that the output file name is the same as the input file name with a new extension
-- Note: like hakyll, assumes that no "." is present in the extension
newExtOutFilePath :: String -> CompilerOut
newExtOutFilePath ext = COutFile $ RelativePath (newExtension ext)

execName :: String -> ExecutableName
execName = ExecutableName

-- | Calls the external compiler with no arguments. Returns the output contents as a 'B.ByteString'.
--   If an error occurs this raises an exception.
--   May be useful if you already have build scripts for artifacts in your repository.
execCompiler     :: ExecutableName                   -> CompilerOut -> Compiler (Item B.ByteString)
execCompiler name out          = execCompilerWith name [] out

-- | Calls the external compiler with the provided arguments. Returns the output contents as a 'B.ByteString'.
--   If an error occurs this raises an exception.
execCompilerWith :: ExecutableName -> ExecutableArgs -> CompilerOut -> Compiler (Item B.ByteString)
execCompilerWith name exArgs out = do
  input   <- getResourceFilePath
  let args = fmap (hargToArg input) exArgs
  let outputReader = cOutToFileContents input out
  unsafeExecCompiler name args outputReader

-- | Primarily for internal use, occasionally useful when already building a compiler imperatively.
-- Allows the caller to opt out of the declarative components of 'execCompiler' and 'execCompilerWith'.
unsafeExecCompiler ::
       ExecutableName                    -- ^ Name or filepath of the executable
    -> [String]                          -- ^ Arguments to pass to the executable
    -> (B.ByteString -> IO B.ByteString) -- ^ Action to read the output of the compiler. Input is the stdout of the process.
    -> Compiler (Item B.ByteString)
unsafeExecCompiler (ExecutableName exName) args outputReader =
  do
    results <- unsafeCompiler $ procResults
    -- just using this to get at the item
    oldBody <- getResourceString
    pure $ itemSetBody results oldBody
  where
  procResults = withProcessWait procConf waitOutput
  procConf = setStdout byteStringOutput . proc exName $ args
  waitOutput process = do
    let stmProc = getStdout process
    out <- atomically stmProc
    checkExitCode process
    outputReader out

--                 input fpath                   stdout contents
cOutToFileContents :: FilePath -> CompilerOut -> B.ByteString -> IO B.ByteString
cOutToFileContents _      CStdOut out                  = pure out
cOutToFileContents _     (COutFile (SpecificPath f)) _ = B.readFile  f
cOutToFileContents input (COutFile (RelativePath f)) _ = B.readFile (f input)

hargToArg :: FilePath -> ExecutableArg -> String
hargToArg _ (ProcArg s) = s
hargToArg f HakFilePath = f