{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

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

-- |
-- Module      :  Distribution.Simple.Program.Run
-- Copyright   :  Duncan Coutts 2009
--
-- Maintainer  :  cabal-devel@haskell.org
-- Portability :  portable
--
-- This module provides a data type for program invocations and functions to
-- run them.
module Distribution.Simple.Program.Run
  ( ProgramInvocation (..)
  , IOEncoding (..)
  , emptyProgramInvocation
  , simpleProgramInvocation
  , programInvocation
  , programInvocationCwd
  , multiStageProgramInvocation
  , runProgramInvocation
  , getProgramInvocationOutput
  , getProgramInvocationLBS
  , getProgramInvocationOutputAndErrors
  , getProgramInvocationLBSAndErrors
  , getEffectiveEnvironment
  , getFullEnvironment
  ) where

import Distribution.Compat.Prelude
import Prelude ()

import Distribution.Compat.Environment
import Distribution.Simple.Errors
import Distribution.Simple.Program.Types
import Distribution.Simple.Utils
import Distribution.Utils.Generic
import Distribution.Utils.Path
import Distribution.Verbosity

import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map as Map

-- | Represents a specific invocation of a specific program.
--
-- This is used as an intermediate type between deciding how to call a program
-- and actually doing it. This provides the opportunity to the caller to
-- adjust how the program will be called. These invocations can either be run
-- directly or turned into shell or batch scripts.
data ProgramInvocation = ProgramInvocation
  { progInvokePath :: FilePath
  , progInvokeArgs :: [String]
  , progInvokeEnv :: [(String, Maybe String)]
  , progInvokeCwd :: Maybe FilePath
  , progInvokeInput :: Maybe IOData
  , progInvokeInputEncoding :: IOEncoding
  -- ^ TODO: remove this, make user decide when constructing 'progInvokeInput'.
  , progInvokeOutputEncoding :: IOEncoding
  , progInvokeWhen :: IO Bool
  }

data IOEncoding
  = IOEncodingText -- locale mode text
  | IOEncodingUTF8 -- always utf8

encodeToIOData :: IOEncoding -> IOData -> IOData
encodeToIOData _ iod@(IODataBinary _) = iod
encodeToIOData IOEncodingText iod@(IODataText _) = iod
encodeToIOData IOEncodingUTF8 (IODataText str) = IODataBinary (toUTF8LBS str)

emptyProgramInvocation :: ProgramInvocation
emptyProgramInvocation =
  ProgramInvocation
    { progInvokePath = ""
    , progInvokeArgs = []
    , progInvokeEnv = []
    , progInvokeCwd = Nothing
    , progInvokeInput = Nothing
    , progInvokeInputEncoding = IOEncodingText
    , progInvokeOutputEncoding = IOEncodingText
    , progInvokeWhen = pure True
    }

simpleProgramInvocation
  :: FilePath
  -> [String]
  -> ProgramInvocation
simpleProgramInvocation path args =
  emptyProgramInvocation
    { progInvokePath = path
    , progInvokeArgs = args
    }

programInvocation
  :: ConfiguredProgram
  -> [String]
  -> ProgramInvocation
programInvocation prog args =
  emptyProgramInvocation
    { progInvokePath = programPath prog
    , progInvokeArgs =
        programDefaultArgs prog
          ++ args
          ++ programOverrideArgs prog
    , progInvokeEnv = programOverrideEnv prog
    }

programInvocationCwd
  :: forall to
   . Maybe (SymbolicPath CWD (Dir to))
  -> ConfiguredProgram
  -> [String]
  -> ProgramInvocation
programInvocationCwd mbWorkDir prog args =
  (programInvocation prog args)
    { progInvokeCwd = fmap getSymbolicPath mbWorkDir
    }

runProgramInvocation :: Verbosity -> ProgramInvocation -> IO ()
runProgramInvocation
  verbosity
  ProgramInvocation
    { progInvokePath = path
    , progInvokeArgs = args
    , progInvokeEnv = []
    , progInvokeCwd = Nothing
    , progInvokeInput = Nothing
    } =
    rawSystemExit verbosity Nothing path args
runProgramInvocation
  verbosity
  ProgramInvocation
    { progInvokePath = path
    , progInvokeArgs = args
    , progInvokeEnv = envOverrides
    , progInvokeCwd = mcwd
    , progInvokeInput = Nothing
    } = do
    menv <- getEffectiveEnvironment envOverrides
    maybeExit $
      rawSystemIOWithEnv
        verbosity
        path
        args
        mcwd
        menv
        Nothing
        Nothing
        Nothing
runProgramInvocation
  verbosity
  ProgramInvocation
    { progInvokePath = path
    , progInvokeArgs = args
    , progInvokeEnv = envOverrides
    , progInvokeCwd = mcwd
    , progInvokeInput = Just inputStr
    , progInvokeInputEncoding = encoding
    } = do
    menv <- getEffectiveEnvironment envOverrides
    (_, errors, exitCode) <-
      rawSystemStdInOut
        verbosity
        path
        args
        mcwd
        menv
        (Just input)
        IODataModeBinary
    when (exitCode /= ExitSuccess) $
      dieWithException verbosity $
        RunProgramInvocationException path errors
    where
      input = encodeToIOData encoding inputStr

getProgramInvocationOutput :: Verbosity -> ProgramInvocation -> IO String
getProgramInvocationOutput verbosity inv = do
  (output, errors, exitCode) <- getProgramInvocationOutputAndErrors verbosity inv
  when (exitCode /= ExitSuccess) $
    die' verbosity $
      "'" ++ progInvokePath inv ++ "' exited with an error:\n" ++ errors
  return output

getProgramInvocationLBS :: Verbosity -> ProgramInvocation -> IO LBS.ByteString
getProgramInvocationLBS verbosity inv = do
  (output, errors, exitCode) <- getProgramInvocationIODataAndErrors verbosity inv IODataModeBinary
  when (exitCode /= ExitSuccess) $
    dieWithException verbosity $
      GetProgramInvocationLBSException (progInvokePath inv) errors
  return output

getProgramInvocationOutputAndErrors
  :: Verbosity
  -> ProgramInvocation
  -> IO (String, String, ExitCode)
getProgramInvocationOutputAndErrors verbosity inv = case progInvokeOutputEncoding inv of
  IOEncodingText -> do
    (output, errors, exitCode) <- getProgramInvocationIODataAndErrors verbosity inv IODataModeText
    return (output, errors, exitCode)
  IOEncodingUTF8 -> do
    (output', errors, exitCode) <- getProgramInvocationIODataAndErrors verbosity inv IODataModeBinary
    return (normaliseLineEndings (fromUTF8LBS output'), errors, exitCode)

getProgramInvocationLBSAndErrors
  :: Verbosity
  -> ProgramInvocation
  -> IO (LBS.ByteString, String, ExitCode)
getProgramInvocationLBSAndErrors verbosity inv =
  getProgramInvocationIODataAndErrors verbosity inv IODataModeBinary

getProgramInvocationIODataAndErrors
  :: KnownIODataMode mode
  => Verbosity
  -> ProgramInvocation
  -> IODataMode mode
  -> IO (mode, String, ExitCode)
getProgramInvocationIODataAndErrors
  verbosity
  ProgramInvocation
    { progInvokePath = path
    , progInvokeArgs = args
    , progInvokeEnv = envOverrides
    , progInvokeCwd = mcwd
    , progInvokeInput = minputStr
    , progInvokeInputEncoding = encoding
    }
  mode = do
    menv <- getEffectiveEnvironment envOverrides
    rawSystemStdInOut verbosity path args mcwd menv input mode
    where
      input = encodeToIOData encoding <$> minputStr

-- | Return the current environment extended with the given overrides.
-- If an entry is specified twice in @overrides@, the second entry takes
-- precedence.
--
-- getEffectiveEnvironment returns 'Nothing' when there are no overrides.
-- It returns an argument that is suitable to pass directly to 'CreateProcess' to
-- override the environment.
-- If you need the full environment to manipulate further, even when there are no overrides,
-- then call 'getFullEnvironment'.
getEffectiveEnvironment
  :: [(String, Maybe String)]
  -> IO (Maybe [(String, String)])
getEffectiveEnvironment [] = return Nothing
getEffectiveEnvironment overrides =
  fmap (Just . Map.toList . apply overrides . Map.fromList) getEnvironment
  where
    apply os env = foldl' (flip update) env os
    update (var, Nothing) = Map.delete var
    update (var, Just val) = Map.insert var val

-- | Like 'getEffectiveEnvironment', but when no overrides are specified,
-- returns the full environment instead of 'Nothing'.
getFullEnvironment
  :: [(String, Maybe String)]
  -> IO [(String, String)]
getFullEnvironment overrides = do
  menv <- getEffectiveEnvironment overrides
  case menv of
    Just env -> return env
    Nothing -> getEnvironment

-- | Like the unix xargs program. Useful for when we've got very long command
-- lines that might overflow an OS limit on command line length and so you
-- need to invoke a command multiple times to get all the args in.
--
-- It takes four template invocations corresponding to the simple, initial,
-- middle and last invocations. If the number of args given is small enough
-- that we can get away with just a single invocation then the simple one is
-- used:
--
-- > $ simple args
--
-- If the number of args given means that we need to use multiple invocations
-- then the templates for the initial, middle and last invocations are used:
--
-- > $ initial args_0
-- > $ middle  args_1
-- > $ middle  args_2
-- >   ...
-- > $ final   args_n
multiStageProgramInvocation
  :: ProgramInvocation
  -> (ProgramInvocation, ProgramInvocation, ProgramInvocation)
  -> [String]
  -> [ProgramInvocation]
multiStageProgramInvocation simple (initial, middle, final) args =
  let argSize inv =
        length (progInvokePath inv)
          + foldl' (\s a -> length a + 1 + s) 1 (progInvokeArgs inv)
      fixedArgSize = maximum (map argSize [simple, initial, middle, final])
      chunkSize = maxCommandLineSize - fixedArgSize
   in case splitChunks chunkSize args of
        [] -> [simple]
        [c] -> [simple `appendArgs` c]
        (c : c2 : cs)
          | (xs, x) <- unsnocNE (c2 :| cs) ->
              [initial `appendArgs` c]
                ++ [middle `appendArgs` c' | c' <- xs]
                ++ [final `appendArgs` x]
  where
    appendArgs :: ProgramInvocation -> [String] -> ProgramInvocation
    inv `appendArgs` as = inv{progInvokeArgs = progInvokeArgs inv ++ as}

    splitChunks :: Int -> [[a]] -> [[[a]]]
    splitChunks len = unfoldr $ \s ->
      if null s
        then Nothing
        else Just (chunk len s)

    chunk :: Int -> [[a]] -> ([[a]], [[a]])
    chunk len (s : _) | length s >= len = error toolong
    chunk len ss = chunk' [] len ss

    chunk' :: [[a]] -> Int -> [[a]] -> ([[a]], [[a]])
    chunk' acc len (s : ss)
      | len' < len = chunk' (s : acc) (len - len' - 1) ss
      where
        len' = length s
    chunk' acc _ ss = (reverse acc, ss)

    toolong =
      "multiStageProgramInvocation: a single program arg is larger "
        ++ "than the maximum command line length!"

-- FIXME: discover this at configure time or runtime on unix
-- The value is 32k on Windows and posix specifies a minimum of 4k
-- but all sensible unixes use more than 4k.
-- we could use getSysVar ArgumentLimit but that's in the unix lib
--
maxCommandLineSize :: Int
maxCommandLineSize = 30 * 1024
