{-# LANGUAGE DeriveAnyClass, DeriveGeneric, OverloadedStrings #-}

{- |
   Module      : System.JBI.Commands.Tool
   Description : Common tooling commands
   Copyright   : (c) Ivan Lazar Miljenovic
   License     : MIT
   Maintainer  : Ivan.Miljenovic@gmail.com



 -}
module System.JBI.Commands.Tool where

import System.JBI.Tagged

import Control.Applicative          (liftA2)
import Data.Aeson                   (ToJSON(toJSON))
import Data.Char                    (isDigit)
import Data.Maybe                   (listToMaybe)
import Data.String                  (IsString(..))
import Data.Version                 (Version, parseVersion)
import GHC.Generics                 (Generic)
import System.Directory             (findExecutable)
import System.Exit                  (ExitCode(ExitSuccess))
import System.IO                    (IOMode(WriteMode), withFile)
import System.Process               (CreateProcess(..),
                                     StdStream(Inherit, UseHandle), proc,
                                     readProcessWithExitCode, waitForProcess,
                                     withCreateProcess)
import Text.ParserCombinators.ReadP (eof, readP_to_S)

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

class Tool t where
  commandName :: Tagged t CommandName

  commandVersion :: Tagged t CommandPath -> IO (Maybe (Tagged t Version))
  commandVersion = withTaggedF tryFindVersion

commandPath :: (Tool t) => IO (Maybe (Tagged t CommandPath))
commandPath = withTaggedF findExecutable commandName

commandInformation :: (Tool t) => IO (Maybe (Installed t))
commandInformation = commandPath >>= mapM getVersion
  where
    getVersion :: (Tool t') => Tagged t' CommandPath -> IO (Installed t')
    getVersion tcp = Installed tcp <$> commandVersion tcp

data GHC

instance Tool GHC where
  commandName = "ghc"

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

newtype CommandName = CommandName { nameOfCommand :: String }
  deriving (Eq, Ord, Show, Read)

instance IsString CommandName where
  fromString = CommandName

newtype CommandPath = CommandPath { pathToCommand :: FilePath }
  deriving (Eq, Ord, Show, Read)

instance ToJSON CommandPath where
  toJSON = toJSON . pathToCommand

instance IsString CommandPath where
  fromString = CommandPath

data Installed t = Installed
  { path    :: !(Tagged t CommandPath)
  , version :: !(Maybe (Tagged t Version))
               -- ^ Try and determine the version.  Only a factor in
               --   case any features are version-specific.
  } deriving (Eq, Ord, Show, Read, Generic, ToJSON)

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

-- | Attempt to find the version of the provided command, by assuming
--   it's contained in the first line of the output of @command
--   --version@.
tryFindVersion :: FilePath -> IO (Maybe Version)
tryFindVersion = tryFindVersionBy findVersion
  where
    findVersion str = takeVersion (dropWhile (not . isDigit) str)

-- | If we're at the start of a Version, take all of it.
takeVersion :: String -> String
takeVersion = takeWhile (liftA2 (||) isDigit (=='.'))

tryFindVersionBy :: (String -> String) -> FilePath -> IO (Maybe Version)
tryFindVersionBy findVersion cmd =
  fmap (>>= parseVer) (tryRunOutput cmd ["--version"])
  where
    parseVer ver = case readP_to_S (parseVersion <* eof) (findVersion ver) of
                     [(v,"")] -> Just v
                     _        -> Nothing

type Args = [String]

-- | Only return the stdout if the process was successful and had no stderr.
tryRunOutput :: FilePath -> Args -> IO (Maybe String)
tryRunOutput cmd args = do
  res <- readProcessWithExitCode cmd args ""
  return $ case res of
             (ExitSuccess, out, "" ) -> Just out
             -- Some tools (e.g. Stack) put output to stderr
             (ExitSuccess, "",  err) -> Just err
             _                       -> Nothing

-- | As with 'tryRunOutput' but only return the first line (if any).
tryRunLine :: FilePath -> Args -> IO (Maybe String)
tryRunLine cmd = fmap (>>= listToMaybe . lines) . tryRunOutput cmd

-- | Returns success of call.
tryRun :: Tagged t CommandPath -> Args -> IO ExitCode
tryRun cmd args = withCreateProcess cp $ \_ _ _ ph ->
                    waitForProcess ph
  where
    cmd' = stripTag cmd

    cp = (proc cmd' args) { std_in  = Inherit
                          , std_out = Inherit
                          , std_err = Inherit
                          }

tryRunToFile :: FilePath -> Tagged t CommandPath -> Args -> IO ExitCode
tryRunToFile file cmd args = withFile file WriteMode $ \h ->
                               withCreateProcess (cp h) $ \_ _ _ ph ->
                                 waitForProcess ph
  where
    cmd' = stripTag cmd

    cp h = (proc cmd' args) { std_in  = Inherit
                            , std_out = UseHandle h
                            , std_err = Inherit
                            }

-- | Equivalent to chaining all the calls with @&&@ in bash, etc.
--
--   Argument order to make it easier to feed it into a 'Tagged'-based
--   pipeline.
tryRunAll :: [Args] -> Tagged t CommandPath -> IO ExitCode
tryRunAll argss cmd = allSuccess $ map (tryRun cmd) argss

(.&&.) :: (Monad m) => m ExitCode -> m ExitCode -> m ExitCode
m1 .&&. m2 = do ec1 <- m1
                case ec1 of
                  ExitSuccess -> m2
                  _           -> return ec1

infixr 3 .&&.

(.||.) :: (Monad m) => m ExitCode -> m ExitCode -> m ExitCode
m1 .||. m2 = do ec1 <- m1
                case ec1 of
                  ExitSuccess -> return ec1
                  _           -> m2

infixr 2 .||.

tryCommand :: String -> IO ExitCode -> IO ExitCode -> IO ExitCode
tryCommand msg tryWith run = run .||. tryAgain
  where
    tryAgain = do
      putStrLn (makeBox msg)
      tryWith .&&. run

makeBox :: String -> String
makeBox msg = unlines [ border
                      , "* " ++ msg ++ " *"
                      , border
                      ]
  where
    msgLen = length msg
    boxLen = msgLen + 4 -- asterisk + space on either side

    border = replicate boxLen '*'

allSuccess :: (Monad m, Foldable t) => t (m ExitCode) -> m ExitCode
allSuccess = foldr (.&&.) (return ExitSuccess)

-- | Monad version of 'all', aborts the computation at the first @False@ value
allM :: Monad m => (a -> m Bool) -> [a] -> m Bool
allM _ []     = return True
allM f (b:bs) = f b >>= (\bv -> if bv then allM f bs else return False)