{-# LANGUAGE FlexibleInstances, TypeSynonymInstances,
             TypeFamilies, ExistentialQuantification #-}

-- | This module is a wrapper for the module "Shelly".
-- The only difference is a main type 'Sh'. In this module
-- 'Sh' contains a list of results. Actual definition of the type 'Sh' is:
--
-- > import qualified Shelly as S
-- >
-- > newtype Sh a = Sh { unSh :: S.Sh [a] }
--
-- This definition can simplify some filesystem commands.
-- A monad bind operator becomes a pipe operator and we can write
--
-- > findExt ext = findWhen (pure . hasExt ext)
-- >
-- > main :: IO ()
-- > main = shs $ do
-- >     mkdir "new"
-- >     findExt "hs"  "." >>= flip cp "new"
-- >     findExt "cpp" "." >>= rm_f
-- >     liftIO $ putStrLn "done"
--
-- Documentation in this module mostly just reference documentation from
-- the main "Shelly" module.
--
-- > {-# LANGUAGE OverloadedStrings #-}
-- > {-# LANGUAGE ExtendedDefaultRules #-}
-- > {-# OPTIONS_GHC -fno-warn-type-defaults #-}
-- > import Shelly
-- > import Data.Text as T
-- > default (T.Text)

module Shelly.Pipe
       (
         -- * Entering Sh
         Sh, shs, shelly, shellyFailDir, shsFailDir, sub, silently, verbosely, escaping, print_stdout, print_commands, tracing, errExit, log_stdout_with, log_stderr_with
         -- * List functions
         , roll, unroll, liftSh
         -- * Running external commands
         , FoldCallback
         , run, run_, runFoldLines, cmd
         , (-|-), lastStderr, setStdin, lastExitCode
         , command, command_, command1, command1_
         , sshPairs, sshPairs_

         -- * Modifying and querying environment
         , setenv, get_env, get_env_text, get_env_def, appendToPath, prependToPath

         -- * Environment directory
         , cd, chdir, pwd

         -- * Printing
         , echo, echo_n, echo_err, echo_n_err, inspect, inspect_err
         , tag, trace, show_command

         -- * Querying filesystem
         , ls, lsT, test_e, test_f, test_d, test_s, which

         -- * Filename helpers
         , absPath, (</>), (<.>), canonic, canonicalize, relPath, relativeTo
         , hasExt

         -- * Manipulating filesystem
         , mv, rm, rm_f, rm_rf, cp, cp_r, mkdir, mkdir_p, mkdirTree

         -- * reading/writing Files
         , readfile, readBinary, writefile, appendfile, touchfile, withTmpDir

         -- * exiting the program
         , exit, errorExit, quietExit, terror

         -- * Exceptions
         , catchany, catch_sh, finally_sh
         , ShellyHandler(..), catches_sh
         , catchany_sh

         -- * convert between Text and FilePath
         , toTextIgnore, toTextWarn, S.fromText

         -- * Utilities
         , (<$>), whenM, unlessM, time

         -- * Re-exported for your convenience
         , liftIO, when, unless, FilePath

         -- * internal functions for writing extensions
         , get, put

         -- * find functions
         , find, findWhen, findFold
         , findDirFilter, findDirFilterWhen, findFoldDirFilter
         , followSymlink
         ) where

import qualified Shelly as S

import Shelly
    ( (</>), (<.>), hasExt
    , whenM, unlessM, toTextIgnore
    , catchany
    , FoldCallback
    )
import Shelly.Base (State)

import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Exception

import Data.Maybe      ( fromMaybe )
import Data.ByteString ( ByteString )
import Data.Tree       ( Tree )
import Data.Text as T hiding (concat, all, find, cons)

-- | This type is a simple wrapper for a type @Shelly.Sh@.
-- 'Sh' contains a list of results.

newtype Sh a = Sh { unSh :: S.Sh [a] }

instance Functor Sh where
    fmap f = Sh . fmap (fmap f) . unSh

instance Applicative Sh where
    pure  = Sh . pure . pure
    (<*>) = ap
    a *> b  = Sh $ unSh a *> unSh b

instance Monad Sh where
    return  = pure
    a >>= f = Sh $ fmap concat $ mapM (unSh . f) =<< unSh a
    (>>)    = (*>)

instance Alternative Sh where
    empty = mzero
    (<|>) = mplus

instance MonadPlus Sh where
    mzero = Sh $ return []
    mplus a b = Sh $ liftA2 (++) (unSh a) (unSh b)

instance MonadIO Sh where
    liftIO = sh1 liftIO

-------------------------------------------------------
-- converters

sh0 :: S.Sh a -> Sh a
sh0 = Sh . fmap return

sh1 :: (a -> S.Sh b) -> (a -> Sh b)
sh1 f = \a -> sh0 (f a)

sh2 :: (a1 -> a2 -> S.Sh b) -> (a1 -> a2 -> Sh b)
sh2 f = \a b -> sh0 (f a b)

sh3 :: (a1 -> a2 -> a3 -> S.Sh b) -> (a1 -> a2 -> a3 -> Sh b)
sh3 f = \a b c -> sh0 (f a b c)

sh4 :: (a1 -> a2 -> a3 -> a4 -> S.Sh b) -> (a1 -> a2 -> a3 -> a4 -> Sh b)
sh4 f = \a b c d -> sh0 (f a b c d)

sh0s :: S.Sh [a] -> Sh a
sh0s = Sh

sh1s :: (a -> S.Sh [b]) -> (a -> Sh b)
sh1s f = \a -> sh0s (f a)

{-  Just in case ...
sh2s :: (a1 -> a2 -> S.Sh [b]) -> (a1 -> a2 -> Sh b)
sh2s f = \a b -> sh0s (f a b)

sh3s :: (a1 -> a2 -> a3 -> S.Sh [b]) -> (a1 -> a2 -> a3 -> Sh b)
sh3s f = \a b c -> sh0s (f a b c)
-}

lift1 :: (S.Sh a -> S.Sh b) -> (Sh a -> Sh b)
lift1 f = Sh . (mapM (f . return) =<< ) . unSh

lift2 :: (S.Sh a -> S.Sh b -> S.Sh c) -> (Sh a -> Sh b -> Sh c)
lift2 f a b = Sh $ join $ liftA2 (mapM2 f') (unSh a) (unSh b)
    where f' = \x y -> f (return x) (return y)

mapM2 :: Monad m => (a -> b -> m c)-> [a] -> [b] -> m [c]
mapM2 f as bs = sequence $ liftA2 f as bs

-----------------------------------------------------------

-- | Unpack list of results.
unroll :: Sh a -> Sh [a]
unroll = Sh . fmap return . unSh

-- | Pack list of results. It performs @concat@ inside 'Sh'.
roll :: Sh [a] -> Sh a
roll = Sh . fmap concat . unSh

-- | Transform result as list. It can be useful for filtering.
liftSh :: ([a] -> [b]) -> Sh a -> Sh b
liftSh f = Sh . fmap f . unSh

------------------------------------------------------------------
-- Entering Sh

-- | see 'S.shelly'
shelly :: MonadIO m => Sh a -> m [a]
shelly = S.shelly . unSh

-- | Performs 'shelly' and then an empty action @return ()@.
shs :: MonadIO m => Sh () -> m ()
shs x = shelly x >> return ()

-- | see 'S.shellyFailDir'
shellyFailDir :: MonadIO m => Sh a -> m [a]
shellyFailDir = S.shellyFailDir . unSh

-- | Performs 'shellyFailDir' and then an empty action @return ()@.
shsFailDir :: MonadIO m => Sh () -> m ()
shsFailDir x = shellyFailDir x >> return ()

-- | see 'S.sub'
sub :: Sh a -> Sh a
sub = lift1 S.sub

-- See 'S.siliently'
silently :: Sh a -> Sh a
silently = lift1 S.silently

-- See 'S.verbosely
verbosely :: Sh a -> Sh a
verbosely = lift1 S.verbosely

-- | see 'S.escaping'
escaping :: Bool -> Sh a -> Sh a
escaping b = lift1 (S.escaping b)

-- | see 'S.log_stdout_with'
log_stdout_with :: (Text -> IO ()) -> Sh a -> Sh a
log_stdout_with logger = lift1 (S.log_stdout_with logger)

-- | see 'S.log_stderr_with'
log_stderr_with :: (Text -> IO ()) -> Sh a -> Sh a
log_stderr_with logger = lift1 (S.log_stdout_with logger)

-- | see 'S.print_stdout'
print_stdout :: Bool -> Sh a -> Sh a
print_stdout b = lift1 (S.print_stdout b)

-- | see 'S.print_commands
print_commands :: Bool -> Sh a -> Sh a
print_commands b = lift1 (S.print_commands b)

-- | see 'S.tracing'
tracing :: Bool -> Sh a -> Sh a
tracing b = lift1 (S.tracing b)

-- | see 'S.errExit'
errExit :: Bool -> Sh a -> Sh a
errExit b = lift1 (S.errExit b)

-- | see 'S.followSymlink'
followSymlink :: Bool -> Sh a -> Sh a
followSymlink b = lift1 (S.followSymlink b)

-- | see 'S.run'
run :: FilePath -> [Text] -> Sh Text
run a b = sh0 $ S.run a b

-- | see 'S.run_'
run_ :: FilePath -> [Text] -> Sh ()
run_ a b = sh0 $ S.run_ a b

-- | see 'S.runFoldLines'
runFoldLines :: a -> FoldCallback a -> FilePath -> [Text] -> Sh a
runFoldLines a cb fp ts = sh0 $ S.runFoldLines a cb fp ts

-- | see 'S.-|-'
(-|-) :: Sh Text -> Sh b -> Sh b
(-|-) = lift2 (S.-|-)

-- | see 'S.lastStderr'
lastStderr :: Sh Text
lastStderr = sh0 S.lastStderr

-- | see 'S.setStdin'
setStdin :: Text -> Sh ()
setStdin = sh1 S.setStdin

-- | see 'S.lastExitCode'
lastExitCode :: Sh Int
lastExitCode = sh0 S.lastExitCode

-- | see 'S.command'
command :: FilePath -> [Text] -> [Text] -> Sh Text
command = sh3 S.command

-- | see 'S.command_'
command_ :: FilePath -> [Text] -> [Text] -> Sh ()
command_ = sh3 S.command_


-- | see 'S.command1'
command1 :: FilePath -> [Text] -> Text -> [Text] -> Sh Text
command1 = sh4 S.command1

-- | see 'S.command1_'
command1_ :: FilePath -> [Text] -> Text -> [Text] -> Sh ()
command1_ = sh4 S.command1_

-- | see 'S.sshPairs'
sshPairs :: Text -> [(FilePath, [Text])] -> Sh Text
sshPairs = sh2 S.sshPairs

-- | see 'S.sshPairs_'
sshPairs_ :: Text -> [(FilePath, [Text])] -> Sh ()
sshPairs_ = sh2 S.sshPairs_

-- | see 'S.setenv'
setenv :: Text -> Text -> Sh ()
setenv = sh2 S.setenv

-- | see 'S.get_env'
get_env :: Text -> Sh (Maybe Text)
get_env = sh1 S.get_env

-- | see 'S.get_env_text'
get_env_text :: Text -> Sh Text
get_env_text = sh1 S.get_env_text

-- | see 'S.get_env_def'
get_env_def :: Text -> Text -> Sh Text
get_env_def a d = sh0 $ fmap (fromMaybe d) $ S.get_env a
{-# DEPRECATED get_env_def "use fromMaybe DEFAULT get_env" #-}

-- | see 'S.appendToPath'
appendToPath :: FilePath -> Sh ()
appendToPath = sh1 S.appendToPath

-- | see 'S.prependToPath'
prependToPath :: FilePath -> Sh ()
prependToPath = sh1 S.prependToPath

-- | see 'S.cd'
cd :: FilePath -> Sh ()
cd = sh1 S.cd

-- | see 'S.chdir'
chdir :: FilePath -> Sh a -> Sh a
chdir p = lift1 (S.chdir p)

-- | see 'S.pwd'
pwd :: Sh FilePath
pwd = sh0 S.pwd

-----------------------------------------------------------------
-- Printing

-- | Echo text to standard (error, when using _err variants) output. The _n
-- variants do not print a final newline.
echo, echo_n_err, echo_err, echo_n :: Text -> Sh ()

echo        = sh1 S.echo
echo_n_err  = sh1 S.echo_n_err
echo_err    = sh1 S.echo_err
echo_n      = sh1 S.echo_n

-- | see 'S.inspect'
inspect :: Show s => s -> Sh ()
inspect = sh1 S.inspect

-- | see 'S.inspect_err'
inspect_err :: Show s => s -> Sh ()
inspect_err = sh1 S.inspect_err

-- | see 'S.tag'
tag :: Sh a -> Text -> Sh a
tag a t = lift1 (flip S.tag t) a

-- | see 'S.trace'
trace :: Text -> Sh ()
trace = sh1 S.trace

-- | see 'S.show_command'
show_command :: FilePath -> [Text] -> Text
show_command = S.show_command

------------------------------------------------------------------
-- Querying filesystem

-- | see 'S.ls'
ls :: FilePath -> Sh FilePath
ls = sh1s S.ls

-- | see 'S.lsT'
lsT :: FilePath -> Sh Text
lsT = sh1s S.lsT

-- | see 'S.test_e'
test_e :: FilePath -> Sh Bool
test_e = sh1 S.test_e

-- | see 'S.test_f'
test_f :: FilePath -> Sh Bool
test_f = sh1 S.test_f

-- | see 'S.test_d'
test_d :: FilePath -> Sh Bool
test_d = sh1 S.test_d

-- | see 'S.test_s'
test_s :: FilePath -> Sh Bool
test_s = sh1 S.test_s

-- | see 'S.which
which :: FilePath -> Sh (Maybe FilePath)
which = sh1 S.which

---------------------------------------------------------------------
-- Filename helpers

-- | see 'S.absPath'
absPath :: FilePath -> Sh FilePath
absPath = sh1 S.absPath

-- | see 'S.canonic'
canonic :: FilePath -> Sh FilePath
canonic = sh1 S.canonic

-- | see 'S.canonicalize'
canonicalize :: FilePath -> Sh FilePath
canonicalize = sh1 S.canonicalize

-- | see 'S.relPath'
relPath :: FilePath -> Sh FilePath
relPath = sh1 S.relPath

-- | see 'S.relativeTo'
relativeTo :: FilePath -- ^ anchor path, the prefix
           -> FilePath -- ^ make this relative to anchor path
           -> Sh FilePath
relativeTo = sh2 S.relativeTo

-------------------------------------------------------------
-- Manipulating filesystem

-- | see 'S.mv'
mv :: FilePath -> FilePath -> Sh ()
mv = sh2 S.mv

-- | see 'S.rm'
rm :: FilePath -> Sh ()
rm = sh1 S.rm

-- | see 'S.rm_f'
rm_f :: FilePath -> Sh ()
rm_f = sh1 S.rm_f

-- | see 'S.rm_rf'
rm_rf :: FilePath -> Sh ()
rm_rf = sh1 S.rm_rf

-- | see 'S.cp'
cp :: FilePath -> FilePath -> Sh ()
cp = sh2 S.cp

-- | see 'S.cp_r'
cp_r :: FilePath -> FilePath -> Sh ()
cp_r = sh2 S.cp_r

-- | see 'S.mkdir'
mkdir :: FilePath -> Sh ()
mkdir = sh1 S.mkdir

-- | see 'S.mkdir_p'
mkdir_p :: FilePath -> Sh ()
mkdir_p = sh1 S.mkdir_p

-- | see 'S.mkdirTree'
mkdirTree :: Tree FilePath -> Sh ()
mkdirTree = sh1 S.mkdirTree

-- | see 'S.readFile'
readfile :: FilePath -> Sh Text
readfile = sh1 S.readfile

-- | see 'S.readBinary'
readBinary :: FilePath -> Sh ByteString
readBinary = sh1 S.readBinary

-- | see 'S.writeFile'
writefile :: FilePath -> Text -> Sh ()
writefile = sh2 S.writefile

-- | see 'S.touchFile'
touchfile :: FilePath -> Sh ()
touchfile = sh1 S.touchfile

-- | see 'S.appendFile'
appendfile :: FilePath -> Text -> Sh ()
appendfile = sh2 S.appendfile

-- | see 'S.withTmpDir'
withTmpDir :: (FilePath -> Sh a) -> Sh a
withTmpDir f = Sh $ S.withTmpDir (unSh . f)

-----------------------------------------------------------------
-- find

-- | see 'S.find'
find :: FilePath -> Sh FilePath
find = sh1s S.find

-- | see 'S.findWhen'
findWhen :: (FilePath -> Sh Bool) -> FilePath -> Sh FilePath
findWhen p a = Sh $ S.findWhen (fmap and . unSh . p) a

-- | see 'S.findFold'
findFold :: (a -> FilePath -> Sh a) -> a -> FilePath -> Sh a
findFold cons nil a = Sh $ S.findFold cons' nil' a
    where nil'  = return nil
          cons' as dir = unSh $ roll $ mapM (flip cons dir) as

-- | see 'S.findDirFilter'
findDirFilter :: (FilePath -> Sh Bool) -> FilePath -> Sh FilePath
findDirFilter p a = Sh $ S.findDirFilter (fmap and . unSh . p) a

-- | see 'S.findDirFilterWhen'
findDirFilterWhen :: (FilePath -> Sh Bool) -- ^ directory filter
                  -> (FilePath -> Sh Bool) -- ^ file filter
                  -> FilePath -- ^ directory
                  -> Sh FilePath
findDirFilterWhen dirPred filePred a =
    Sh $ S.findDirFilterWhen
            (fmap and . unSh . dirPred)
            (fmap and . unSh . filePred)
            a


-- | see 'S.findFoldDirFilterWhen'
findFoldDirFilter :: (a -> FilePath -> Sh a) -> a -> (FilePath -> Sh Bool) -> FilePath -> Sh a
findFoldDirFilter cons nil p a = Sh $ S.findFoldDirFilter cons' nil' p' a
    where p'    = fmap and . unSh . p
          nil'  = return nil
          cons' as dir = unSh $ roll $ mapM (flip cons dir) as

-----------------------------------------------------------
-- exiting the program

-- | see 'S.exit'
exit :: Int -> Sh ()
exit = sh1 S.exit

-- | see 'S.errorExit'
errorExit :: Text -> Sh ()
errorExit = sh1 S.errorExit

-- | see 'S.quietExit'
quietExit :: Int -> Sh ()
quietExit = sh1 S.quietExit

-- | see 'S.terror'
terror :: Text -> Sh a
terror = sh1 S.terror

------------------------------------------------------------
-- Utilities

-- | see 'S.catch_sh'
catch_sh :: (Exception e) => Sh a -> (e -> Sh a) -> Sh a
catch_sh a f = Sh $ S.catch_sh (unSh a) (unSh . f)

-- | see 'S.catchany_sh'
catchany_sh :: Sh a -> (SomeException -> Sh a) -> Sh a
catchany_sh = catch_sh


-- | see 'S.finally_sh'
finally_sh :: Sh a -> Sh b -> Sh a
finally_sh = lift2 S.finally_sh

-- | see 'S.time'
time :: Sh a -> Sh (Double, a)
time = lift1 S.time

-- | see 'S.ShellyHandler'
data ShellyHandler a = forall e . Exception e => ShellyHandler (e -> Sh a)

-- | see 'S.catches_sh'
catches_sh :: Sh a -> [ShellyHandler a] -> Sh a
catches_sh a hs = Sh $ S.catches_sh (unSh a) (fmap convert hs)
    where convert :: ShellyHandler a -> S.ShellyHandler [a]
          convert (ShellyHandler f) = S.ShellyHandler (unSh . f)

------------------------------------------------------------
-- convert between Text and FilePath

-- | see 'S.toTextWarn'
toTextWarn :: FilePath -> Sh Text
toTextWarn = sh1 S.toTextWarn

-------------------------------------------------------------
-- internal functions for writing extension

get :: Sh State
get = sh0 S.get

put :: State -> Sh ()
put = sh1 S.put

--------------------------------------------------------
-- polyvariadic vodoo

-- | Converter for the variadic argument version of 'run' called 'cmd'.
class ShellArg a where toTextArg :: a -> Text
instance ShellArg Text     where toTextArg = id
instance ShellArg FilePath where toTextArg = toTextIgnore


-- Voodoo to create the variadic function 'cmd'
class ShellCommand t where
    cmdAll :: FilePath -> [Text] -> t

instance ShellCommand (Sh Text) where
    cmdAll fp args = run fp args

instance (s ~ Text, Show s) => ShellCommand (Sh s) where
    cmdAll fp args = run fp args

-- note that Sh () actually doesn't work for its case (_<- cmd) when there is no type signature
instance ShellCommand (Sh ()) where
    cmdAll fp args = run_ fp args

instance (ShellArg arg, ShellCommand result) => ShellCommand (arg -> result) where
    cmdAll fp acc = \x -> cmdAll fp (acc ++ [toTextArg x])

-- | see 'S.cmd'
cmd :: (ShellCommand result) => FilePath -> result
cmd fp = cmdAll fp []
