{-# LANGUAGE ForeignFunctionInterface,ViewPatterns,RecordWildCards #-}
-- Various routines for dealing with temporary directories and files.
module Support.TempDir(
    getTempDir,
    createTempFile,
    fileInTempDir,
    cleanTempDir,
    setTempDir,
    addAtExit,
    withStackStatus,
    wrapMain
   ) where

import Control.Exception as E
import Control.Monad
import Data.IORef
import Data.Maybe
import System.Directory
import System.Exit
import System.FilePath as FP
import System.IO
import System.IO.Unsafe
import System.IO.Temp
import Text.Printf
import qualified Data.Set as Set
import GenUtil (iocatch)
import Support.CompatMingw32

data TempDir = TempDir {
    tempDirClean   :: Bool,  -- ^ whether to delete the directory afterwords.
    tempDirDump    :: Bool,
    tempDirPath    :: Maybe String,
    tempDirAtExit  :: [IO ()],
    tempDirCleanup :: Set.Set FilePath
    }

putLog :: String -> IO ()
--putLog = putStrLn
putLog _ = return ()

cleanTempDir :: Bool -> IO ()
cleanTempDir b = modifyIORef tdRef $ \x -> x { tempDirClean = b }

setTempDir :: FilePath -> IO ()
setTempDir (FP.normalise -> fp) = do
    TempDir {..} <- readIORef tdRef
    when (isJust $ tempDirPath) $ do
        fail $ printf
          "Cannot set temp directory to '%s'; it is already set to '%s'."
          fp (fromJust tempDirPath)
    putLog $ printf "Setting work directory to '%s'" fp
    createDirectoryIfMissing False fp
    writeIORef tdRef TempDir { tempDirPath = Just fp,  .. }
    cleanTempDir False

getTempDir :: IO FilePath
getTempDir = do
    td <- readIORef tdRef
    case tempDirPath td of
        Just fp -> return fp
        Nothing -> do
            tmpdir <- getTemporaryDirectory
            fp <- createTempDirectory tmpdir "jhc_"
            putLog $ printf "Created work directory '%s'" fp
            writeIORef tdRef td { tempDirPath = Just fp }
            return fp

addAtExit :: IO () -> IO ()
addAtExit action = do
    td <- readIORef tdRef
    writeIORef tdRef td { tempDirAtExit = action:tempDirAtExit td }

createTempFile :: FilePath -> IO (FilePath, Handle)
createTempFile (FP.normalise -> fp) = do
    unless (filePathSafe fp) $
        fail $ "createTempFile: unsafe path " ++ fp
    dir <- getTempDir
    (fp,h) <- openBinaryTempFile dir (if null fp then "temp.tmp" else fp)
    putLog $ printf "Created temporary file '%s'" fp
    addCleanup fp
    return (fp,h)

-- make sure nothing is sneaky about the file path
filePathSafe fp = FP.isRelative fp &&
        ".." `notElem` FP.splitPath fp && not (hasDrive fp)

fileInTempDir :: FilePath -> (FilePath -> IO ()) -> IO FilePath
fileInTempDir (FP.normalise -> fp) action = do
    unless (filePathSafe fp) $
        fail $ "fileinTempDir: unsafe path " ++ fp
    let (FP.normalise -> dpart,_) = FP.splitFileName fp
    tdir <- getTempDir
    let f ("./":ps) cp = f ps cp
        f (".":ps) cp = f ps cp
        f (p:ps) cp = do
            putLog $ printf "Creating directory '%s' '%s' '%s' '%s' '%s'" tdir cp p dpart fp
            createDirectoryIfMissing False (tdir </> cp </> p)
            let cp' = FP.normalise (cp </> p)
            addCleanup cp'
            f ps cp'
        f [] _ = return ()
    f (FP.splitPath dpart) ""
    --unless (null $ FP.normalise dpart) $
    --    fold (FP.splitPath dpart) $ addCleanup
    --    createDirectoryIfMissing True (tdir </> dpart)
    let nfp = FP.normalise (tdir </> fp)
    b <- addCleanup fp
    when b $ action nfp
    return $ noEscapePath nfp

cleanUp :: IO ()
cleanUp = do
    td <- readIORef tdRef
    sequence_ (tempDirAtExit td)
    if not (tempDirClean td) ||
        isNothing (tempDirPath td) then return () else do
    dir <- getTempDir
    forM_ (reverse . Set.toList $ tempDirCleanup td) $ \fp -> do
        putLog $ printf "Removing '%s'" (dir </> fp)
        ignoreError (removeDirectory $ dir </> fp)
        ignoreError (removeFile $ dir </> fp)
    putLog $ printf "Removing '%s'" dir
    ignoreError (removeDirectory dir)

addCleanup :: FilePath -> IO Bool
addCleanup fp = do
    td <- readIORef tdRef
    if fp `Set.member` tempDirCleanup td then return False else do
    writeIORef tdRef td { tempDirCleanup = fp `Set.insert` tempDirCleanup td }
    return True

wrapMain :: IO () -> IO ()
wrapMain main = E.catch (main >> cleanUp) f where
    panic = raiseSigIntCompat
    f (fromException -> Just code) = cleanUp >> exitWith code
    f (fromException -> Just UserInterrupt) = cleanUp >> panic
    f e = do
        ss <- readIORef stackRef
        td <- readIORef tdRef
        case tempDirPath td of
            Just td -> hPutStrLn stderr $
                printf "Exiting abnormally. Work directory is '%s'" td
            _ -> return ()
        unless (null ss) $
            forM_ ("Stack:":ss) (hPutStrLn stderr)
        throwIO e

-------------------
-- support routines
-------------------

ignoreError :: IO () -> IO ()
ignoreError action = iocatch action (\_ -> return ())

{-# NOINLINE tdRef #-}
tdRef :: IORef TempDir
tdRef = unsafePerformIO $ newIORef TempDir {
    tempDirClean   = True,
    tempDirDump    = False,
    tempDirPath    = Nothing,
    tempDirAtExit  = [],
    tempDirCleanup = Set.empty
    }

{-# NOINLINE stackRef #-}
stackRef :: IORef [String]
stackRef = unsafePerformIO $ newIORef []

withStackStatus :: String -> IO a -> IO a
withStackStatus s action = do
    cs <- readIORef stackRef
    writeIORef stackRef (s:cs)
    r <- action
    writeIORef stackRef cs
    return r