-----------------------------------------------------------------------------
-- |
-- 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`)