{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module System.Handsy
  ( Handsy
  , run

  -- * Commands
  , shell
  , command
  , readFile
  , writeFile
  , appendFile

  -- * Helpers
  , shell_
  , command_

  -- * Options
  , CommandOptions (..)
  , Options (..)

  -- * Re-exports
  , ExitCode (..)
  , def
  ) where

import           Prelude                        hiding (appendFile, readFile,
                                                 writeFile)

import           Data.Bool
import qualified Data.ByteString.Char8          as C8
import qualified Data.ByteString.Lazy           as B
import qualified Data.ByteString.Lazy.Char8     as C
import           System.Exit

import           Data.Default.Class
import           System.Process.ByteString.Lazy

import           Text.ShellEscape

import           System.Handsy.Internal         hiding (shell)
import qualified System.Handsy.Internal         as I

-- * Commands

-- | Runs a command
command :: FilePath     -- ^ Command to run
        -> [String]     -- ^ Arguments
        -> CommandOptions
        -> Handsy (ExitCode, B.ByteString, B.ByteString) -- ^ (status, stdout, stderr)
command cmd args opts = let cmd' = C8.unpack . C8.intercalate " " . map (bytes . bash . C8.pack) $ (cmd:args)
                        in  shell cmd' opts

{-| Executes the given string in shell. Example:

  > shell "ls" $~ def{cwd="/var/www"}
-}
shell :: String       -- ^ String to execute
      -> CommandOptions
      -> Handsy (ExitCode, B.ByteString, B.ByteString) -- ^ (ExitCode, Stdout, Stderr)
shell cmd opts = let esc = C8.unpack . bytes . bash . C8.pack
                     CommandOptions stdin' cwd' = opts
                 in  I.shell (bool ("cd " ++ esc cwd' ++ "; ") "" (null cwd') ++ cmd) stdin'

data CommandOptions =
  CommandOptions { stdin :: B.ByteString
                 , cwd   :: String
                 }
  deriving Show

instance Default CommandOptions where
  def = CommandOptions "" ""

-- | Reads a file and returns the contents of the file.
readFile :: FilePath -> Handsy B.ByteString
readFile fp = command "cat" [fp] def >>= \case
  (ExitSuccess, stdout, _) -> return stdout
  (_, _, stderr)           -> error $ "Error reading " ++ fp ++ "\nStderr was: " ++ C.unpack stderr

-- | @writeFile file str@ function writes the bytestring @str@, to the file @file@.
writeFile :: FilePath -> B.ByteString -> Handsy ()
writeFile fp s = command "dd" ["of=" ++ fp] def{stdin=s} >>= \case
  (ExitSuccess, _, _) -> return ()
  (_, _, stderr)      -> error $ "Error writing to " ++ fp  ++ "\nStderr was: " ++ C.unpack stderr

-- | @appendFile file str@ function appends the bytestring @str@, to the file @file@.
appendFile :: FilePath -> B.ByteString -> Handsy ()
appendFile fp s = command "dd" ["of=" ++ fp, "conv=notrunc", "oflag=append"] def{stdin=s} >>= \case
  (ExitSuccess, _, _) -> return ()
  (_, _, stderr)      -> error $ "Error appending to " ++ fp ++ "\nStderr was: " ++ C.unpack stderr

-- | Same as 'command', but ExitFailure is a runtime error.
command_ :: FilePath -> [String] -> CommandOptions -> Handsy (B.ByteString, B.ByteString)
command_ path args opts = command path args opts >>= \case
  (ExitFailure code, _, stderr) -> error ('`':path ++ ' ' : show args ++ "` returned " ++ show code
                                       ++ "\nStderr was: " ++ C.unpack stderr)
  (ExitSuccess, stdout, stderr) -> return (stdout, stderr)

-- | Same as 'shell', but ExitFailure is a runtime error.
shell_ :: String -> CommandOptions -> Handsy (B.ByteString, B.ByteString)
shell_ cmd opts = shell cmd opts >>= \case
  (ExitFailure code, _, stderr) -> error ('`':cmd ++ "` returned " ++ show code
                                       ++ "\nStderr was: " ++ C.unpack stderr)
  (ExitSuccess, stdout, stderr) -> return (stdout, stderr)

-- | Executes the actions locally
run :: Options -> Handsy a -> IO a
run = interpretSimple (\cmdline -> readProcessWithExitCode "bash" ["-c", cmdline])