{-# LANGUAGE ForeignFunctionInterface #-}

-- | This module creates new files and directories with unique names.
-- Its functionality is similary to C's mkstemp() and mkdtemp()
-- functions.
module LIO.TmpFile (-- * The high level interface
                    mkTmpFile
                   , mkTmpDir
                   , mkTmpDir'
                   -- * Some lower-level helper functions
                   , mkTmp, openFileExclusive
                   -- * Functions for generating unique names
                   , tmpName, nextTmpName, serializele, unserializele
                   -- * For flushing temp files before rename
                   , hSync
                   )where

import LIO.Armor

import Prelude hiding (catch)
import Control.Exception (throwIO, catch)
-- import qualified Control.Exception as IO
import Data.Bits (shiftL, shiftR, (.|.))
import qualified Data.ByteString.Lazy as L
import Data.Word (Word8)
import Foreign.C.Error
import Foreign.C.Types
import System.Directory (createDirectory)
import System.FilePath ((</>))
import System.Posix.IO (OpenMode(..), OpenFileFlags(..)
                       , defaultFileFlags , openFd, fdToHandle)
import qualified System.IO as IO
import qualified System.IO.Error as IO
import System.Time (ClockTime(..), getClockTime)

import Data.Typeable
import GHC.IO.FD (FD(..))
import GHC.IO.Handle.Types (Handle__(..))
import GHC.IO.Handle.Internals (wantWritableHandle)

foreign import ccall "unistd.h fsync" c_fsync :: CInt -> IO CInt

--
-- Temporary file name based on time in 1/16 of a microsecond, then
-- step until unused file name found.
--

-- | Serialize an Integer into an array of bytes, in little-endian
-- order.
serializele :: Int              -- ^ Minimum number of bytes to return
            -> Integer          -- ^ The Integer to serialize
            -> [Word8]
serializele n i | n <= 0 && i <= 0 = []
serializele n i = (fromInteger i):serializele (n - 1) (i `shiftR` 8)

-- | Take an array of bytes containing an Integer serialized in
-- little-endian order, and return the Integer.
unserializele       :: [Word8] -> Integer
unserializele []    = 0
unserializele (c:s) = (fromIntegral c) .|. (unserializele s `shiftL` 8)

-- | Return a temorary file name, based on the value of the current
-- time of day clock.
tmpName :: IO String
tmpName = do
  (TOD sec psec) <- getClockTime
  return $ armor32 $ L.pack $
          serializele 3 (psec `shiftR` 16) ++ serializele 4 sec

-- | When the file name returned by 'tmpName' already exists,
-- @nextTmpName@ modifies the file name to generate a new one.
nextTmpName :: String -> String
nextTmpName s =
    let val = unserializele $ L.unpack $ dearmor32 s
    in armor32 $ L.pack $ serializele 7 (1 + val)

-- | Opens a file in exclusive mode, throwing AlreadyExistsError if
-- the file name is already in use.
openFileExclusive     :: IO.IOMode -> FilePath -> IO IO.Handle
openFileExclusive m p = do
  let dom = defaultFileFlags { exclusive = True }
      (om, fm) = case m of
                   IO.WriteMode     -> (WriteOnly, dom)
                   IO.AppendMode    -> (WriteOnly, dom { append = True })
                   IO.ReadWriteMode -> (ReadWrite, dom)
                   IO.ReadMode      ->
                       error "openFileExclusive: ReadMode is illegal"
  fd <- openFd p om (Just $ toEnum 0o666) fm
  fdToHandle fd

-- | Executes a function on temporary file names until the function
-- does not throw AlreadyExistsError.  For example, 'mkTmpFile' is
-- defined as:
--
-- > mkTmpFile m d s = mkTmp (openFileExclusive m) d s
--
mkTmp       :: (FilePath -> IO a) -- ^The function to execute (@f@)
            -> FilePath           -- ^Directory to prepend to temp file names
            -> String             -- ^Suffix for new file name
            -> IO (a, FilePath)   -- ^The result of @f@ and the
                                  -- FilePath on which it finally
                                  -- succeeded.
mkTmp f dir suffix = tmpName >>= loop
    where
      ff n = case dir </> n of path -> do a <- f path; return (a, path)
      loop name = ff (name ++ suffix) `catch` reloop name
      reloop name e = if IO.isAlreadyExistsError e
                      then loop $ nextTmpName name
                      else throwIO e


-- | Creates a new file with a unique name in a particular directory
mkTmpFile :: IO.IOMode          -- ^@WriteMode@, @AppendMode@, or
                                -- @ReadWriteMode@ (It is an error to
                                -- use @ReadMode@.)
          -> FilePath           -- ^Directory in which to create file
          -> String             -- ^Suffix for new file name
          -> IO (IO.Handle, FilePath) -- ^Returns open handle to new
                                      -- file, along with pathname of
                                      -- new file
mkTmpFile m d s = mkTmp (openFileExclusive m) d s

-- | Creates a new subdirectory with uniqe file name.  Returns the
-- pathname of the new directory as the second element of a pair, just
-- for consistency with the interface to 'mkTmpFile'.  See
-- `mkTmpDir'` if you don't want this behavior.
mkTmpDir     :: FilePath -- ^Directory in which to create subdirectory
             -> String   -- ^Suffix to append to new directory name
             -> IO ((), FilePath) -- ^Returns full path to new directory
mkTmpDir d s = mkTmp createDirectory d s

-- | Like 'mkTmpDir', but just returns the pathname of the new directory.
mkTmpDir'     :: FilePath -- ^Directory in which to create subdirectory
              -> String   -- ^Suffix to append to new directory name
              -> IO FilePath    -- ^Returns full path to new directory
mkTmpDir' d s = fmap snd $ mkTmpDir d s

-- | Flushes a Handle to disk with fsync()
hSync :: IO.Handle -> IO ()
hSync h = do
  IO.hFlush h
  wantWritableHandle "hSync" h $ fsyncH
    where
      fsyncH Handle__ {haDevice = dev} = maybe (return ()) fsyncD $ cast dev
      fsyncD FD {fdFD = fd} = throwErrnoPathIfMinus1_ "fsync" (show h)
                              (c_fsync fd)