{-# LANGUAGE CPP, ScopedTypeVariables #-}

-- |Random and Binary IO with generic Iteratees, using File Descriptors for IO.
-- when available, these are the preferred functions for performing IO as they
-- run in constant space and function properly with sockets, pipes, etc.

module Data.Iteratee.IO.Fd(
#if defined(USE_POSIX)
  -- * File enumerators
  -- ** FileDescriptor based enumerators
  enumFd
  ,enumFdFollow
  ,enumFdRandom
  -- * Iteratee drivers
  ,fileDriverFd
  ,fileDriverFollowFd
  ,fileDriverRandomFd
#endif
)

where

#if defined(USE_POSIX)
import Data.Iteratee.Base.StreamChunk (ReadableChunk (..))
import Data.Iteratee.Base
import Data.Iteratee.Binary()
import Data.Iteratee.IO.Base

import Control.Monad
import Control.Monad.IO.Class

import Foreign.Ptr
import Foreign.ForeignPtr
import Foreign.Storable

import System.IO (SeekMode(..))

import System.Posix hiding (FileOffset)
import GHC.Conc

-- ------------------------------------------------------------------------
-- Binary Random IO enumerators

-- |The enumerator of a POSIX File Descriptor.  This version enumerates
-- over the entire contents of a file, in order, unless stopped by
-- the iteratee.  In particular, seeking is not supported.
enumFd :: forall s el m a.(ReadableChunk s el, MonadIO m) =>
  Fd ->
  EnumeratorGM s el m a
enumFd fd iter' =
  liftIO (mallocForeignPtrBytes (fromIntegral buffer_size)) >>= loop iter'
  where
    buffer_size = fromIntegral $ 4096 - mod 4096 (sizeOf (undefined :: el))
    loop iter fp = do
      s <- liftIO . withForeignPtr fp $ \p -> do
        liftIO $ GHC.Conc.threadWaitRead fd
        n <- myfdRead fd (castPtr p) buffer_size
        case n of
          Left _errno -> return $ Left "IO error"
          Right 0 -> return $ Right Nothing
          Right n' -> liftM (Right . Just) $ readFromPtr p (fromIntegral n')
      checkres fp iter s
    checkres fp iter = either (flip enumErr iter)
                              (maybe (return iter)
                                     (check fp <=< runIter iter . Chunk))
    check _p (Done x _) = return . return $ x
    check p  (Cont i Nothing) = loop i p
    check _p (Cont _ (Just e)) = return $ throwErr e

-- |The enumerator of a POSIX File Descriptor: a variation of enumFd
-- that follows the tail of growing input.
enumFdFollow :: forall s el a.(ReadableChunk s el) =>
  Fd ->
  EnumeratorGM s el IO a
enumFdFollow fd iter' =
  liftIO (mallocForeignPtrBytes (fromIntegral buffer_size)) >>= loop iter'
  where
    buffer_size = fromIntegral $ 4096 - mod 4096 (sizeOf (undefined :: el))
    loop iter fp = do
      s <- readFollow iter fp
      checkres fp iter s
    readFollow iter fp = do
        liftIO . withForeignPtr fp $ \p -> do
          liftIO $ GHC.Conc.threadWaitRead fd
          n <- myfdRead fd (castPtr p) buffer_size
          case n of
            Left _errno -> return $ Left "IO error"
            Right 0 -> do liftIO $ threadDelay (250 * 1000)
                          readFollow iter fp
            Right n' -> liftM (Right . Just) $ readFromPtr p (fromIntegral n')
    checkres fp iter = either (flip enumErr iter)
                              (maybe (return iter)
                                     (check fp <=< runIter iter . Chunk))
    check _p (Done x _) = return . return $ x
    check p  (Cont i Nothing) = loop i p
    check _p (Cont _ (Just e)) = return $ throwErr e

-- |The enumerator of a POSIX File Descriptor: a variation of enumFd that
-- supports RandomIO (seek requests)
enumFdRandom :: forall s el m a.(ReadableChunk s el, MonadIO m) =>
  Fd ->
  EnumeratorGM s el m a
enumFdRandom fd iter' =
 liftIO (mallocForeignPtrBytes (fromIntegral buffer_size)) >>= loop (0,0) iter'
 where
  -- this can be usefully varied.  Values between 512 and 4096 seem
  -- to provide the best performance for most cases.
  buffer_size = fromIntegral $ 4096 - mod 4096 (sizeOf (undefined :: el))
  -- the first argument of loop is (off,len), describing which part
  -- of the file is currently in the buffer 'fp'
  loop :: (FileOffset,Int) ->
          IterateeG s el m a ->
          ForeignPtr el ->
          m (IterateeG s el m a)
    -- Thanks to John Lato for the strictness annotation
    -- Otherwise, the `off + fromIntegral len' below accumulates thunks
  loop (off,len) _iter _fp | off `seq` len `seq` False = undefined
  loop (off,len) iter fp = do
    s <- liftIO . withForeignPtr fp $ \p -> do
      liftIO $ GHC.Conc.threadWaitRead fd
      n <- myfdRead fd (castPtr p) buffer_size
      case n of
        Left _errno -> return $ Left "IO error"
        Right 0 -> return $ Right Nothing
        Right n' -> liftM
          (Right . Just . (,) (off + fromIntegral len, fromIntegral n'))
          (readFromPtr p (fromIntegral n'))
    checkres fp iter s
  seekTo pos@(off, len) off' iter fp
    | off <= off' && off' < off + fromIntegral len =   -- Seek within buffer
    do
    let local_off = fromIntegral $ off' - off
    s <- liftIO $ withForeignPtr fp $ \p ->
                    readFromPtr (p `plusPtr` local_off) (len - local_off)
    igv <- runIter iter (Chunk s)
    check pos fp igv
  seekTo _pos off iter fp = do                         -- Seek outside buffer
    off' <- liftIO $ myfdSeek fd AbsoluteSeek (fromIntegral off)
    case off' of
      Left _errno -> enumErr "IO error" iter
      Right off'' -> loop (off'',0) iter fp
  checkres fp iter = either
                       (flip enumErr iter)
                       (maybe (return iter) (uncurry $ runS fp iter))
  runS fp iter o s = runIter iter (Chunk s) >>= check o fp
  check _ _fp (Done x _)                 = return . return $ x
  check o fp  (Cont i Nothing)           = loop o i fp
  check o fp  (Cont i (Just (Seek off))) = seekTo o off i fp
  check _ _fp (Cont _ (Just e))          = return $ throwErr e

-- |Process a file using the given IterateeGM.  This function wraps
-- enumFd as a convenience.
fileDriverFd :: (MonadIO m, ReadableChunk s el) =>
  IterateeG s el m a ->
  FilePath ->
  m a
fileDriverFd iter filepath = do
  fd <- liftIO $ openFd filepath ReadOnly Nothing defaultFileFlags
  result <- enumFd fd iter >>= run
  liftIO $ closeFd fd
  return result

-- |Process a file using the given IterateeGM.  This function wraps
-- enumFdFollow as a convenience.
-- The first iteratee is used to scan through to the end of the file, using
-- enumFd. The second iteratee is used from then onwards on the growing tail
-- of the file, using enumFdFollow.
fileDriverFollowFd :: (ReadableChunk s el) =>
  IterateeG s el IO a ->
  (a -> IterateeG s el IO b) ->
  FilePath ->
  IO b
fileDriverFollowFd scanIter followIter filepath = do
  fd <- liftIO $ openFd filepath ReadOnly Nothing defaultFileFlags
  state <- enumFd fd scanIter >>= run
  result <- enumFdFollow fd (followIter state) >>= run
  liftIO $ closeFd fd
  return result

-- |Process a file using the given IterateeGM.  This function wraps
-- enumFdRandom as a convenience.
fileDriverRandomFd :: (MonadIO m, ReadableChunk s el) =>
  IterateeG s el m a ->
  FilePath ->
  m a
fileDriverRandomFd iter filepath = do
  fd <- liftIO $ openFd filepath ReadOnly Nothing defaultFileFlags
  result <- enumFdRandom fd iter >>= run
  liftIO $ closeFd fd
  return result

#endif