-- | Interface to @fd_set@. See @select(2)@.  
--
-- The type 'I.FdSet' is opaque, but is implemented internally as a
-- pointer to an @fd_set@. All operations on 'I.FdSet's must adhere to
-- the requirements of @FD_CLR@, @FD_ISSET@, @FD_SET@ and @FD_ZERO@
-- (see @select(2)@). This includes requiring /valid/ file descriptors
-- for all operations. Most functions in this module are kept in the
-- 'IO' monad to make it easier to guarantee validity of the file
-- descriptors, but since invalid ones seem to work fine in practice
-- (at least on Linux), the module
-- "System.Posix.IO.Select.FdSet.Unsafe" provides a non-'IO'
-- interface.
--
-- Functions that return an 'I.FdSet', such as 'insert', copy the
-- underlying @fd_set@ in order to be referentially transparent.
--
-- In the documentation that follows, a file descriptor is said to be
-- /in range/ if it is non-negative and strictly smaller than the
-- system-defined @FD_SETSIZE@. Many functions silently ignore file
-- descriptors that are not in range.
module System.Posix.IO.Select.FdSet (I.FdSet(), fromList, insert, insertList, empty, elem,
                                     remove, removeList, inList, inRange, bound, duplicate) where

import Prelude hiding (elem)
import qualified System.Posix.IO.Select.FdSet.Internal as I
import Foreign
import System.Posix.Types
import Misc
import Control.Monad

-- | Create an 'FdSet' from a list of file descriptors. File
-- descriptors not in range (see above) are silently ignored.
fromList :: [Fd] -> IO I.FdSet
fromList fds = I.allocate' >>= \ptr ->
               withForeignPtr ptr I.c_fd_zero_wrapper >>
               mapM_ ((flip I.insert') ptr) (filter inRange fds) >>
               return (I.FdSet ptr (maximum (0:fds)))

-- | Insert a file descriptor.
insert :: Fd -> I.FdSet -> IO I.FdSet 
insert fd = insertList [fd]

-- | Insert multiple file descriptors. This is more efficient than
-- multiple 'insert's (only a single copy of the set is made).
insertList :: [Fd] -> I.FdSet -> IO I.FdSet
insertList fds (I.FdSet ptr l) = 
    I.duplicate' ptr >>= \newPtr ->
    mapM_ ((flip I.insert') newPtr) (filter inRange fds) >>
    return (I.FdSet newPtr (max l (maximum (0:fds))))

-- | An empty 'FdSet'.
empty :: IO I.FdSet
empty = I.allocate' >>= \ptr ->
        withForeignPtr ptr I.c_fd_zero_wrapper >>
        return (I.FdSet ptr 0)

-- | Test for membership. Recall that POSIX allows undefined behavior
-- if the file descriptor is not valid (it does, however, seem to work
-- fine on Linux).
elem :: Fd -> I.FdSet -> IO Bool
elem fd (I.FdSet ptr _) = I.elem' fd ptr >>= (return . cTrue)

-- | Remove a file descriptor.
remove :: Fd -> I.FdSet -> IO I.FdSet
remove fd = removeList [fd]

-- | Remove multiple file descriptors. This is more efficient than
-- multiple 'remove's (only a single copy of the set is made).
removeList :: [Fd] -> I.FdSet -> IO I.FdSet
removeList fds (I.FdSet ptr l) = 
    I.duplicate' ptr >>= \newPtr ->
    mapM_ ((flip I.remove') newPtr) (filter inRange fds) >>
    return (I.FdSet newPtr l) -- We don't actually shrink the maximum fd here. Should be ok!

-- | @'inList' fds fdset@ gives a list of all file descriptors in
-- @fd@ that are in @fdset@.
inList :: [Fd] -> I.FdSet -> IO [Fd]
inList fds (I.FdSet ptr l) = filterM (\fd -> I.elem' fd ptr >>= (return . cTrue)) fds'
    where
      fds' = filter (\fd -> fd <= l && inRange fd) fds

-- | Test if a file descriptor is in range (see introduction).
inRange :: Fd -> Bool
inRange fd = fd >= 0 && fd < I.c_FD_SETSIZE

-- | This file descriptor is at least as large as the largest in the
-- set. If no file descriptors have ever been removed from the set,
-- the value is /the largest/ in the set, but this assumption may not
-- hold after removals or other operations.
bound :: I.FdSet -> Fd
bound (I.FdSet _ l) = l

-- | Copy an 'FdSet'.
duplicate :: I.FdSet -> IO I.FdSet
duplicate (I.FdSet ptr l) = I.duplicate' ptr >>= \newPtr ->
                            return (I.FdSet newPtr l)