content: -----------------------------------------------------------------------------
  -- |
  -- Module      :  Language.C.Wrapper.Preprocess
  -- Copyright   :  (c) 2008 Benedikt Huber
  -- License     :  BSD-style
  -- Maintainer  :  benedikt.huber@gmail.com
  -- Stability   :  experimental
  -- Portability :  portable
  --
  -- Invoking external preprocessors.
  -----------------------------------------------------------------------------
  module Language.C.System.Preprocess (
      Preprocessor(..),
      CppOption(..),
      CppArgs(..),rawCppArgs,addCppOption,addExtraOption,
      runPreprocessor,
      isPreprocessed,
  )
  where
  import Language.C.Data.InputStream
  import System.Exit
  import System.Directory
  import System.FilePath
  import System.Environment
  import System.IO
  import Control.Exception
  import Control.Monad
  import Data.List
  
  -- | 'Preprocessor' encapsulates the abstract interface for invoking C preprocessors
  class Preprocessor cpp where
      -- | parse the given command line arguments, and return a pair of parsed and ignored arguments
      parseCPPArgs :: cpp -> [String] -> Either String (CppArgs, [String])
      -- | run the preprocessor
      runCPP :: cpp -> CppArgs -> IO ExitCode
  
  -- | file extension of a preprocessed file
  preprocessedExt :: String
  preprocessedExt = ".i"
  
  -- | Generic Options for the preprocessor
  data CppOption =
          IncludeDir FilePath
        | Define String String
        | Undefine String
        | IncludeFile FilePath
  
  -- | Generic arguments for the preprocessor
  data CppArgs = CppArgs {
          cppOptions :: [CppOption],
          extraOptions :: [String],
          cppTmpDir  :: Maybe FilePath,
          inputFile  :: FilePath,
          outputFile :: Maybe FilePath
      }
  
  -- | Cpp arguments that only specify the input file name.
  cppFile :: FilePath -> CppArgs
  cppFile input_file = CppArgs { cppOptions = [], extraOptions = [], cppTmpDir = Nothing, inputFile = input_file, outputFile = Nothing }
  
  -- | use the given preprocessor arguments without analyzing them
  rawCppArgs :: [String] -> FilePath -> CppArgs
  rawCppArgs opts input_file =
      CppArgs { inputFile = input_file, cppOptions = [], extraOptions = opts, outputFile = Nothing, cppTmpDir = Nothing }
  
  -- | add a typed option to the given preprocessor arguments
  addCppOption :: CppArgs -> CppOption -> CppArgs
  addCppOption cpp_args opt =
      cpp_args { cppOptions = opt : (cppOptions cpp_args) }
  
  -- | add a string option to the given preprocessor arguments
  addExtraOption :: CppArgs -> String -> CppArgs
  addExtraOption cpp_args extra =
      cpp_args { extraOptions = extra : (extraOptions cpp_args) }
  
  -- | run the preprocessor and return an 'InputStream' if preprocesssing succeeded
  runPreprocessor :: (Preprocessor cpp) => cpp -> CppArgs -> IO (Either ExitCode InputStream)
  runPreprocessor cpp cpp_args = do
      bracket
          getActualOutFile
          -- remove outfile if it was temporary
          removeTmpOutFile
          -- invoke preprocessor
          invokeCpp
      where
      getActualOutFile :: IO FilePath
      getActualOutFile = maybe (mkOutputFile (cppTmpDir cpp_args) (inputFile cpp_args)) return (outputFile cpp_args)
      invokeCpp actual_out_file = do
          exit_code <- runCPP cpp (cpp_args { outputFile = Just actual_out_file})
          case exit_code of
              ExitSuccess   -> liftM Right (readInputStream actual_out_file)
              ExitFailure _ -> return $ Left exit_code
      removeTmpOutFile out_file = maybe (removeFile out_file) (\_ -> return ()) (outputFile cpp_args)
  
  -- | create an output file, given  @Maybe tmpdir@ and @inputfile@
  mkOutputFile :: Maybe FilePath -> FilePath -> IO FilePath
  mkOutputFile tmp_dir_opt input_file =
      do tmpDir <- getTempDir tmp_dir_opt
         mkTmpFile tmpDir (getOutputFileName input_file)
      where
      getTempDir (Just tmpdir) = return tmpdir
      getTempDir Nothing       = getTemporaryDirectory
  
  -- | compute output file name from input file name
  getOutputFileName :: FilePath -> FilePath
  getOutputFileName fp | hasExtension fp = replaceExtension filename preprocessedExt
                       | otherwise       = addExtension filename preprocessedExt
      where
      filename = takeFileName fp
  
  -- | create a temporary file
  mkTmpFile :: FilePath -> FilePath -> IO FilePath
  mkTmpFile tmp_dir file_templ = do
      -- putStrLn $ "TmpDir: "++tmp_dir
      -- putStrLn $ "FileTempl: "++file_templ
      (path,file_handle) <- openTempFile tmp_dir file_templ
      hClose file_handle
      return path
  
  -- | guess whether a file is preprocessed (file end with .i)
  isPreprocessed :: FilePath -> Bool
  isPreprocessed = (".i" `isSuffixOf`)
  
  