{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances, TypeOperators, ScopedTypeVariables, NamedFieldPuns #-}
{-# LANGUAGE GADTs, GeneralizedNewtypeDeriving #-}
#if __GLASGOW_HASKELL__ < 710
{-# LANGUAGE OverlappingInstances #-}
#endif
module Development.Shake.Command(
    command, command_, cmd, cmd_, unit, CmdArgument(..), CmdArguments(..), IsCmdArgument(..), (:->),
    Stdout(..), Stderr(..), Stdouterr(..), Exit(..), Process(..), CmdTime(..), CmdLine(..),
    CmdResult, CmdString, CmdOption(..),
    addPath, addEnv,
    ) where
import Data.Tuple.Extra
import Control.Monad.Extra
import Control.Monad.IO.Class
import Data.Char
import Data.Either.Extra
import Data.List.Extra
import Data.Maybe
import Data.Semigroup (Semigroup)
import Data.Monoid
import System.Directory
import System.Environment.Extra
import System.Exit
import System.IO.Extra hiding (withTempFile, withTempDir)
import System.Process
import System.Info.Extra
import System.Time.Extra
import System.IO.Unsafe(unsafeInterleaveIO)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import General.Extra
import General.Process
import Control.Applicative
import Prelude
import Development.Shake.Internal.CmdOption
import Development.Shake.Internal.Core.Action
import Development.Shake.Internal.Core.Types hiding (Result)
import Development.Shake.FilePath
import Development.Shake.Internal.FilePattern
import Development.Shake.Internal.Options
import Development.Shake.Internal.Rules.File
import Development.Shake.Internal.Derived
addPath :: MonadIO m => [String] -> [String] -> m CmdOption
addPath pre post = do
    args <- liftIO getEnvironment
    let (path,other) = partition ((== "PATH") . (if isWindows then upper else id) . fst) args
    return $ Env $
        [("PATH",intercalate [searchPathSeparator] $ pre ++ post) | null path] ++
        [(a,intercalate [searchPathSeparator] $ pre ++ [b | b /= ""] ++ post) | (a,b) <- path] ++
        other
addEnv :: MonadIO m => [(String, String)] -> m CmdOption
addEnv extra = do
    args <- liftIO getEnvironment
    return $ Env $ extra ++ filter (\(a,_) -> a `notElem` map fst extra) args
data Str = Str String | BS BS.ByteString | LBS LBS.ByteString | Unit deriving Eq
data Result
    = ResultStdout Str
    | ResultStderr Str
    | ResultStdouterr Str
    | ResultCode ExitCode
    | ResultTime Double
    | ResultLine String
    | ResultProcess PID
      deriving Eq
data PID = PID0 | PID ProcessHandle
instance Eq PID where _ == _ = True
commandExplicit :: String -> [CmdOption] -> [Result] -> String -> [String] -> Action [Result]
commandExplicit funcName oopts results exe args = do
    ShakeOptions
        {shakeCommandOptions,shakeRunCommands
        ,shakeLint,shakeLintInside,shakeLintIgnore} <- getShakeOptions
    let fopts = shakeCommandOptions ++ oopts
    let useShell = Shell `elem` fopts
    let useLint = shakeLint == Just LintFSATrace
    let useAutoDeps = AutoDeps `elem` fopts
    let opts = filter (/= Shell) fopts
    let skipper act = if null results && not shakeRunCommands then return [] else act
    let verboser act = do
            let cwd = listToMaybe $ reverse [x | Cwd x <- opts]
            putLoud $
                maybe "" (\x -> "cd " ++ x ++ "; ") cwd ++
                if useShell then unwords $ exe : args else showCommandForUser2 exe args
            verb <- getVerbosity
            
            (if verb >= Loud then quietly else id) act
    let tracer = case reverse [x | Traced x <- opts] of
            "":_ -> liftIO
            msg:_ -> traced msg
            _ | useShell -> traced $ takeFileName $ fst $ word1 exe
            [] -> traced $ takeFileName exe
    let tracker act
            | useLint = fsatrace act
            | useAutoDeps = autodeps act
            | useShell = shelled act
            | otherwise = act exe args
        shelled = runShell (unwords $ exe : args)
        ignore = map (?==) shakeLintIgnore
        ham cwd xs = [makeRelative cwd x | x <- map toStandard xs
                                         , any (`isPrefixOf` x) shakeLintInside
                                         , not $ any ($ x) ignore]
        fsaCmd act opts file
            | isMac = fsaCmdMac act opts file
            | useShell = runShell (unwords $ exe : args) $ \exe args -> act "fsatrace" $ opts : file : "--" : exe : args
            | otherwise = act "fsatrace" $ opts : file : "--" : exe : args
        fsaCmdMac act opts file = do
            let fakeExe e = liftIO $ do
                    me <- findExecutable e
                    case me of
                        Just re -> do
                            let isSystem = any (`isPrefixOf` re) [ "/bin"
                                                                 , "/usr"
                                                                 , "/sbin"
                                                                 ]
                            if isSystem
                                then do
                                    tmpdir <- getTemporaryDirectory
                                    let fake = tmpdir ++ "fsatrace-fakes" ++ re
                                    unlessM (doesFileExist fake) $ do
                                        createDirectoryRecursive $ takeDirectory fake
                                        copyFile re fake
                                    return fake
                                else return re
                        Nothing -> return e
            fexe <- fakeExe exe
            if useShell
                then do
                    fsh <- fakeExe "/bin/sh"
                    act "fsatrace" $ opts : file : "--" : fsh : "-c" : [unwords $ fexe : args]
                else act "fsatrace" $ opts : file : "--" : fexe : args
        fsatrace act = withTempFile $ \file -> do
            res <- fsaCmd act "rwm" file
            xs <- liftIO $ parseFSAT <$> readFileUTF8' file
            cwd <- liftIO getCurrentDirectory
            let reader (FSATRead x) = Just x; reader _ = Nothing
                writer (FSATWrite x) = Just x; writer (FSATMove x _) = Just x; writer _ = Nothing
                existing f = liftIO . filterM doesFileExist . nubOrd . mapMaybe f
            rs <- existing reader xs
            ws <- existing writer xs
            let reads = ham cwd rs
                writes = ham cwd ws
            when useAutoDeps $
                unsafeAllowApply $ needed reads
            trackRead reads
            trackWrite writes
            return res
        autodeps act = withTempFile $ \file -> do
            res <-  fsaCmd act "r" file
            pxs <- liftIO $ parseFSAT <$> readFileUTF8' file
            xs <- liftIO $ filterM doesFileExist [x | FSATRead x <- pxs]
            cwd <- liftIO getCurrentDirectory
            unsafeAllowApply $ need $ ham cwd xs
            return res
    skipper $ tracker $ \exe args -> verboser $ tracer $ commandExplicitIO funcName opts results exe args
runShell :: String -> (String -> [String] -> Action a) -> Action a
runShell x act | not isWindows = act "/bin/sh" ["-c",x] 
runShell x act = withTempDir $ \dir -> do
    let file = dir </> "s.bat"
    writeFile' file x
    act "cmd.exe" ["/d/q/c",file]
data FSAT
    = FSATWrite FilePath
    | FSATRead FilePath
    | FSATDelete FilePath
    | FSATMove FilePath FilePath
parseFSAT :: String -> [FSAT]
parseFSAT = mapMaybe f . lines
    where f ('w':'|':xs) = Just $ FSATWrite xs
          f ('r':'|':xs) = Just $ FSATRead xs
          f ('d':'|':xs) = Just $ FSATDelete xs
          f ('m':'|':xs) | (xs,'|':ys) <- break (== '|') xs = Just $ FSATMove xs ys
          f _ = Nothing
commandExplicitIO :: String -> [CmdOption] -> [Result] -> String -> [String] -> IO [Result]
commandExplicitIO funcName opts results exe args = do
    let (grabStdout, grabStderr) = both or $ unzip $ flip map results $ \r -> case r of
            ResultStdout{} -> (True, False)
            ResultStderr{} -> (False, True)
            ResultStdouterr{} -> (True, True)
            _ -> (False, False)
    optEnv <- resolveEnv opts
    let optCwd = let x = last $ "" : [x | Cwd x <- opts] in if x == "" then Nothing else Just x
    let optStdin = flip mapMaybe opts $ \x -> case x of
            Stdin x -> Just $ SrcString x
            StdinBS x -> Just $ SrcBytes x
            FileStdin x -> Just $ SrcFile x
            _ -> Nothing
    let optShell = Shell `elem` opts
    let optBinary = BinaryPipes `elem` opts
    let optAsync = ResultProcess PID0 `elem` results
    let optTimeout = listToMaybe $ reverse [x | Timeout x <- opts]
    let optWithStdout = last $ False : [x | WithStdout x <- opts]
    let optWithStderr = last $ True : [x | WithStderr x <- opts]
    let optFileStdout = [x | FileStdout x <- opts]
    let optFileStderr = [x | FileStderr x <- opts]
    let optEchoStdout = last $ (not grabStdout && null optFileStdout) : [x | EchoStdout x <- opts]
    let optEchoStderr = last $ (not grabStderr && null optFileStderr) : [x | EchoStderr x <- opts]
    let cmdline = showCommandForUser2 exe args
    let bufLBS f = do (a,b) <- buf $ LBS LBS.empty; return (a, (\(LBS x) -> f x) <$> b)
        buf Str{} | optBinary = bufLBS (Str . LBS.unpack)
        buf Str{} = do x <- newBuffer; return ([DestString x | not optAsync], Str . concat <$> readBuffer x)
        buf LBS{} = do x <- newBuffer; return ([DestBytes x | not optAsync], LBS . LBS.fromChunks <$> readBuffer x)
        buf BS {} = bufLBS (BS . BS.concat . LBS.toChunks)
        buf Unit  = return ([], return Unit)
    (dStdout, dStderr, resultBuild) :: ([[Destination]], [[Destination]], [Double -> ProcessHandle -> ExitCode -> IO Result]) <-
        fmap unzip3 $ forM results $ \r -> case r of
            ResultCode _ -> return ([], [], \_ _ ex -> return $ ResultCode ex)
            ResultTime _ -> return ([], [], \dur _ _ -> return $ ResultTime dur)
            ResultLine _ -> return ([], [], \_ _ _ -> return $ ResultLine cmdline)
            ResultProcess _ -> return ([], [], \_ pid _ -> return $ ResultProcess $ PID pid)
            ResultStdout    s -> do (a,b) <- buf s; return (a , [], \_ _ _ -> fmap ResultStdout b)
            ResultStderr    s -> do (a,b) <- buf s; return ([], a , \_ _ _ -> fmap ResultStderr b)
            ResultStdouterr s -> do (a,b) <- buf s; return (a , a , \_ _ _ -> fmap ResultStdouterr b)
    exceptionBuffer <- newBuffer
    po <- resolvePath ProcessOpts
        {poCommand = if optShell then ShellCommand $ unwords $ exe:args else RawCommand exe args
        ,poCwd = optCwd, poEnv = optEnv, poTimeout = optTimeout
        ,poStdin = [SrcBytes LBS.empty | optBinary && not (null optStdin)] ++ optStdin
        ,poStdout = [DestEcho | optEchoStdout] ++ map DestFile optFileStdout ++ [DestString exceptionBuffer | optWithStdout && not optAsync] ++ concat dStdout
        ,poStderr = [DestEcho | optEchoStderr] ++ map DestFile optFileStderr ++ [DestString exceptionBuffer | optWithStderr && not optAsync] ++ concat dStderr
        ,poAsync = optAsync
        }
    (dur,(pid,exit)) <- duration $ process po
    if exit == ExitSuccess || ResultCode ExitSuccess `elem` results then
        mapM (\f -> f dur pid exit) resultBuild
     else do
        exceptionBuffer <- readBuffer exceptionBuffer
        let captured = ["Stderr" | optWithStderr] ++ ["Stdout" | optWithStdout]
        cwd <- case optCwd of
            Nothing -> return ""
            Just v -> do
                v <- canonicalizePath v `catchIO` const (return v)
                return $ "Current directory: " ++ v ++ "\n"
        fail $
            "Development.Shake." ++ funcName ++ ", system command failed\n" ++
            "Command: " ++ cmdline ++ "\n" ++
            cwd ++
            "Exit code: " ++ show (case exit of ExitFailure i -> i; _ -> 0) ++ "\n" ++
            if null captured then "Stderr not captured because WithStderr False was used\n"
            else if null exceptionBuffer then intercalate " and " captured ++ " " ++ (if length captured == 1 then "was" else "were") ++ " empty"
            else intercalate " and " captured ++ ":\n" ++ unlines (dropWhile null $ lines $ concat exceptionBuffer)
resolveEnv :: [CmdOption] -> IO (Maybe [(String, String)])
resolveEnv opts
    | null env, null addEnv, null addPath, null remEnv = return Nothing
    | otherwise = Just . unique . tweakPath . (++ addEnv) . filter (flip notElem remEnv . fst) <$>
                  if null env then getEnvironment else return (concat env)
    where
        env = [x | Env x <- opts]
        addEnv = [(x,y) | AddEnv x y <- opts]
        remEnv = [x | RemEnv x <- opts]
        addPath = [(x,y) | AddPath x y <- opts]
        newPath mid = intercalate [searchPathSeparator] $
            concat (reverse $ map fst addPath) ++ [mid | mid /= ""] ++ concatMap snd addPath
        isPath x = (if isWindows then upper else id) x == "PATH"
        tweakPath xs | not $ any (isPath . fst) xs = ("PATH", newPath "") : xs
                     | otherwise = map (\(a,b) -> (a, if isPath a then newPath b else b)) xs
        unique = reverse . nubOrdOn (if isWindows then upper . fst else fst) . reverse
resolvePath :: ProcessOpts -> IO ProcessOpts
resolvePath po
    | Just e <- poEnv po
    , Just (_, path) <- find ((==) "PATH" . (if isWindows then upper else id) . fst) e
    , RawCommand prog args <- poCommand po
    = do
    let progExe = if prog == prog -<.> exe then prog else prog <.> exe
    
    pathOld <- unsafeInterleaveIO $ fromMaybe "" <$> lookupEnv "PATH"
    old <- unsafeInterleaveIO $ findExecutable prog
    new <- unsafeInterleaveIO $ findExecutableWith (splitSearchPath path) progExe
    old2 <- unsafeInterleaveIO $ findExecutableWith (splitSearchPath pathOld) progExe
    switch <- return $ case () of
        _ | path == pathOld -> False 
          | Nothing <- new -> False 
          | Nothing <- old -> True 
          | Just old <- old, Just new <- new, equalFilePath old new -> False 
          | Just old <- old, Just old2 <- old2, equalFilePath old old2 -> True 
          | otherwise -> False
    return $ case new of
        Just new | switch -> po{poCommand = RawCommand new args}
        _ -> po
resolvePath po = return po
findExecutableWith :: [FilePath] -> String -> IO (Maybe FilePath)
findExecutableWith path x = flip firstJustM (map (</> x) path) $ \s ->
    ifM (doesFileExist s) (return $ Just s) (return Nothing)
newtype Stdout a = Stdout {fromStdout :: a}
newtype Stderr a = Stderr {fromStderr :: a}
newtype Stdouterr a = Stdouterr {fromStdouterr :: a}
newtype Exit = Exit {fromExit :: ExitCode}
newtype Process = Process {fromProcess :: ProcessHandle}
newtype CmdTime = CmdTime {fromCmdTime :: Double}
newtype CmdLine = CmdLine {fromCmdLine :: String}
class CmdString a where cmdString :: (Str, Str -> a)
instance CmdString () where cmdString = (Unit, \Unit -> ())
instance CmdString String where cmdString = (Str "", \(Str x) -> x)
instance CmdString BS.ByteString where cmdString = (BS BS.empty, \(BS x) -> x)
instance CmdString LBS.ByteString where cmdString = (LBS LBS.empty, \(LBS x) -> x)
#if __GLASGOW_HASKELL__ >= 710
class Unit a
instance {-# OVERLAPPING #-} Unit b => Unit (a -> b)
instance {-# OVERLAPPABLE #-} a ~ () => Unit (m a)
#else
class Unit a
instance Unit b => Unit (a -> b)
instance a ~ () => Unit (m a)
#endif
class CmdResult a where
    
    
    cmdResult :: ([Result], [Result] -> a)
instance CmdResult Exit where
    cmdResult = ([ResultCode ExitSuccess], \[ResultCode x] -> Exit x)
instance CmdResult ExitCode where
    cmdResult = ([ResultCode ExitSuccess], \[ResultCode x] -> x)
instance CmdResult Process where
    cmdResult = ([ResultProcess PID0], \[ResultProcess (PID x)] -> Process x)
instance CmdResult ProcessHandle where
    cmdResult = ([ResultProcess PID0], \[ResultProcess (PID x)] -> x)
instance CmdResult CmdLine where
    cmdResult = ([ResultLine ""], \[ResultLine x] -> CmdLine x)
instance CmdResult CmdTime where
    cmdResult = ([ResultTime 0], \[ResultTime x] -> CmdTime x)
instance CmdString a => CmdResult (Stdout a) where
    cmdResult = let (a,b) = cmdString in ([ResultStdout a], \[ResultStdout x] -> Stdout $ b x)
instance CmdString a => CmdResult (Stderr a) where
    cmdResult = let (a,b) = cmdString in ([ResultStderr a], \[ResultStderr x] -> Stderr $ b x)
instance CmdString a => CmdResult (Stdouterr a) where
    cmdResult = let (a,b) = cmdString in ([ResultStdouterr a], \[ResultStdouterr x] -> Stdouterr $ b x)
instance CmdResult () where
    cmdResult = ([], \[] -> ())
instance (CmdResult x1, CmdResult x2) => CmdResult (x1,x2) where
    cmdResult = (a1++a2, \rs -> let (r1,r2) = splitAt (length a1) rs in (b1 r1, b2 r2))
        where (a1,b1) = cmdResult
              (a2,b2) = cmdResult
cmdResultWith :: forall b c. CmdResult b => (b -> c) -> ([Result], [Result] -> c)
cmdResultWith f = second (f .) cmdResult
instance (CmdResult x1, CmdResult x2, CmdResult x3) => CmdResult (x1,x2,x3) where
    cmdResult = cmdResultWith $ \(a,(b,c)) -> (a,b,c)
instance (CmdResult x1, CmdResult x2, CmdResult x3, CmdResult x4) => CmdResult (x1,x2,x3,x4) where
    cmdResult = cmdResultWith $ \(a,(b,c,d)) -> (a,b,c,d)
instance (CmdResult x1, CmdResult x2, CmdResult x3, CmdResult x4, CmdResult x5) => CmdResult (x1,x2,x3,x4,x5) where
    cmdResult = cmdResultWith $ \(a,(b,c,d,e)) -> (a,b,c,d,e)
command :: CmdResult r => [CmdOption] -> String -> [String] -> Action r
command opts x xs = b <$> commandExplicit "command" opts a x xs
    where (a,b) = cmdResult
command_ :: [CmdOption] -> String -> [String] -> Action ()
command_ opts x xs = void $ commandExplicit "command_" opts [] x xs
type a :-> t = a
cmd :: CmdArguments args => args :-> Action r
cmd = cmdArguments mempty
cmd_ :: (CmdArguments args, Unit args) => args :-> Action ()
cmd_ = cmd
newtype CmdArgument = CmdArgument [Either CmdOption String]
  deriving (Eq, Semigroup, Monoid, Show)
class CmdArguments t where
    
    cmdArguments :: CmdArgument -> t
instance (IsCmdArgument a, CmdArguments r) => CmdArguments (a -> r) where
    cmdArguments xs x = cmdArguments $ xs `mappend` toCmdArgument x
instance CmdResult r => CmdArguments (Action r) where
    cmdArguments (CmdArgument x) = case partitionEithers x of
        (opts, x:xs) -> let (a,b) = cmdResult in b <$> commandExplicit "cmd" opts a x xs
        _ -> error "Error, no executable or arguments given to Development.Shake.cmd"
instance CmdResult r => CmdArguments (IO r) where
    cmdArguments (CmdArgument x) = case partitionEithers x of
        (opts, x:xs) -> let (a,b) = cmdResult in b <$> commandExplicitIO "cmd" opts a x xs
        _ -> error "Error, no executable or arguments given to Development.Shake.cmd"
instance CmdArguments CmdArgument where
    cmdArguments = id
class IsCmdArgument a where
    
    toCmdArgument :: a -> CmdArgument
instance IsCmdArgument String where toCmdArgument = CmdArgument . map Right . words
instance IsCmdArgument [String] where toCmdArgument = CmdArgument . map Right
instance IsCmdArgument CmdOption where toCmdArgument = CmdArgument . return . Left
instance IsCmdArgument [CmdOption] where toCmdArgument = CmdArgument . map Left
instance IsCmdArgument a => IsCmdArgument (Maybe a) where toCmdArgument = maybe mempty toCmdArgument
showCommandForUser2 :: FilePath -> [String] -> String
showCommandForUser2 cmd args = unwords $ map (\x -> if safe x then x else showCommandForUser x []) $ cmd : args
    where
        safe xs = not (null xs) && not (any bad xs)
        bad x = isSpace x || (x == '\\' && not isWindows) || x `elem` "\"\'"