    -- | Interface to Fixed-length DBM. See also,
    -- <http://tokyocabinet.sourceforge.net/spex-en.html#tcfdbapi> for details
    module Database.TokyoCabinet.FDB
        (
         -- $doc
         -- * Constructors
          FDB
        , ECODE(..)
        , OpenMode(..)
        , ID(..)
        -- * Basic API (tokyocabinet.idl compliant)
        , new
        , delete
        , ecode
        , errmsg
        , tune
        , open
        , close
        , put
        , putkeep
        , putcat
        , out
        , get
        , vsiz
        , iterinit
        , iternext
        , range
        , fwmkeys
        , addint
        , adddouble
        , sync
        , optimize
        , vanish
        , copy
        , path
        , rnum
        , fsiz
        ) where

    import Database.TokyoCabinet.Error
    import Database.TokyoCabinet.FDB.C
    import Database.TokyoCabinet.FDB.Key
    import Database.TokyoCabinet.Internal
    import Database.TokyoCabinet.Sequence
    import Database.TokyoCabinet.Storable

    import Foreign.Ptr
    import Foreign.ForeignPtr
    import Foreign.C.Types
    import Foreign.Storable (peek)
    import Foreign.Marshal (alloca, free)
    import Foreign.Marshal.Array (peekArray)
    import Foreign.Marshal.Utils (maybePeek)

    import Data.Int
    import Data.Word

    import Control.Exception

    -- $doc
    -- Example
    --
    -- @
    --    import Control.Monad
    --    import Database.TokyoCabinet.FDB
    -- @
    --
    -- @
    --    main = do fdb <- new
    --              -- open the database
    --              open fdb \"casket.tcf\" [OWRITER, OCREAT] >>= err fdb
    --              -- store records
    --              puts fdb [(1, \"one\"), (12, \"twelve\"), (144, \"one forty four\")] >>=
    --                        err fdb . (all id)
    --              -- retrieve records
    --              get fdb (1 :: Int) >>= maybe (error \"something goes wrong\") putStrLn
    --              -- close the database
    --              close fdb >>= err fdb
    --        where
    --          puts :: FDB -> [(Int, String)] -> IO [Bool]
    --          puts fdb = mapM (uncurry $ put fdb)
    -- @
    --
    -- @
    --          err :: FDB -> Bool -> IO ()
    --          err fdb = flip unless $ ecode fdb >>= error . show
    -- @
    --

    data FDB = FDB { unTCFDB :: !(ForeignPtr FDB') }

    -- | Create a Fixed-length database object.
    new :: IO FDB
    new = FDB `fmap` (c_tcfdbnew >>= newForeignPtr tcfdbFinalizer)

    -- | Free FDB resource forcibly.
    -- FDB is kept by ForeignPtr, so Haskell runtime GC cleans up memory for
    -- almost situation. Most always, you don't need to call this.
    -- After call this, you must not touch FDB object. Its behavior is undefined.
    delete :: FDB -> IO ()
    delete fdb = finalizeForeignPtr $ unTCFDB fdb

    -- | Return the last happened error code.
    ecode :: FDB -> IO ECODE
    ecode fdb =
        withForeignPtr (unTCFDB fdb) $ \fdb' ->
            cintToError `fmap` c_tcfdbecode fdb'

    -- | Set the tuning parameters.
    tune :: FDB     -- ^ FDB object.
         -> Int32   -- ^ the width of the value of each record.
         -> Int64   -- ^ the limit size of the database file.
         -> IO Bool -- ^ if successful, the return value is True.
    tune fdb width limsiz =
        withForeignPtr (unTCFDB fdb) $ \fdb' -> c_tcfdbtune fdb' width limsiz

    -- | Open FDB database file.
    open :: FDB -> String -> [OpenMode] -> IO Bool
    open = openHelper c_tcfdbopen unTCFDB combineOpenMode

    -- | Close the database file.
    close :: FDB -> IO Bool
    close fdb = withForeignPtr (unTCFDB fdb) c_tcfdbclose

    type FunPut' = Ptr FDB' -> Int64 -> Ptr Word8 -> CInt -> IO Bool
    putHelper' :: (Key k, Storable v) => FunPut' -> FDB -> k -> v -> IO Bool
    putHelper' func fdb key val =
        withForeignPtr (unTCFDB fdb) $ \fdb' ->
            withPtrLen val $ \(vbuf, vsize) -> do
              key' <- keyToInt key
              func fdb' key' vbuf vsize

    -- | Stora a record (key-value pair) on FDB.  Key type must be
    -- instance of Key class. Value type must be instance of Storable.
    put :: (Key k, Storable v) => FDB -> k -> v -> IO Bool
    put = putHelper' c_tcfdbput

    -- | Store a new record. If a record with the same key exists in the
    -- database, this function has no effect.
    putkeep :: (Key k, Storable v) => FDB -> k -> v -> IO Bool
    putkeep = putHelper' c_tcfdbputkeep

    -- | Concatenate a value at the end of the existing record.
    putcat :: (Key k, Storable v) => FDB -> k -> v -> IO Bool
    putcat =  putHelper' c_tcfdbputcat

    -- | Delete a record.
    out :: (Key k) => FDB -> k -> IO Bool
    out fdb key =
        withForeignPtr (unTCFDB fdb) $ \fdb' ->
            c_tcfdbout fdb' =<< keyToInt key

    -- | Return the value of record.
    get :: (Key k, Storable v) => FDB -> k -> IO (Maybe v)
    get fdb key =
        withForeignPtr (unTCFDB fdb) $ \fdb' ->
            alloca $ \sizbuf -> do
              key' <- keyToInt key
              vbuf  <- c_tcfdbget fdb' key' sizbuf
              vsize <- peek sizbuf
              flip maybePeek vbuf $ \vbuf' -> peekPtrLen (vbuf', vsize)

    -- | Return the byte size of value in a record.
    vsiz :: (Key k) => FDB -> k -> IO (Maybe Int)
    vsiz fdb key =
        withForeignPtr (unTCFDB fdb) $ \fdb' -> do
          vsize <- c_tcfdbvsiz fdb' =<< keyToInt key
          return $ if vsize == (-1)
                     then Nothing
                     else Just (fromIntegral vsize)

    -- | Initialize the iterator of a FDB object.
    iterinit :: FDB -> IO Bool
    iterinit fdb = withForeignPtr (unTCFDB fdb) c_tcfdbiterinit

    -- | Return the next key of the iterator of a FDB object.
    iternext :: (Key k) => FDB -> IO (Maybe k)
    iternext fdb =
        withForeignPtr (unTCFDB fdb) $ \fdb' -> do
          i <-  c_tcfdbiternext fdb'
          return $ if i == 0
                     then Nothing
                     else Just (fromID $ ID i)

    -- | Return list of keys in the specified range.
    range :: (Key k1, Key k2) =>
             FDB     -- ^ FDB object
          -> k1      -- ^ the lower limit of the range.
          -> k1      -- ^ the upper limit of the range.
          -> Int     -- ^ the maximum number of keys to be fetched.
          -> IO [k2] -- ^ keys in the specified range.
    range fdb lower upper maxn =
        withForeignPtr (unTCFDB fdb) $ \fdb' ->
            alloca $ \sizbuf -> do
              [l, u] <- mapM keyToInt [lower, upper]
              rp <- c_tcfdbrange fdb' l u (fromIntegral maxn) sizbuf
              size <- fromIntegral `fmap` peek sizbuf
              keys <- peekArray size rp
              free rp
              return $ map (fromID . ID) keys

    -- | Return list of forward matched keys.
    fwmkeys :: (Storable k1, Storable k2, Sequence q) =>
               FDB -> k1 -> Int -> IO (q k2)
    fwmkeys fdb k maxn = smap fromString =<< fwmkeys' fdb k maxn
        where fwmkeys' = fwmHelper c_tcfdbrange4 unTCFDB

    -- | Increment the corresponding value. (The value specified by a key
    -- is treated as integer.)
    addint :: (Key k) => FDB -> k -> Int -> IO (Maybe Int)
    addint fdb key num =
        withForeignPtr (unTCFDB fdb) $ \fdb' -> do
          key' <- keyToInt key
          sumval <- c_tcfdbaddint fdb' key' (fromIntegral num)
          return $ if sumval == cINT_MIN
                     then Nothing
                     else Just $ fromIntegral sumval

    -- | Increment the corresponding value. (The value specified by a key
    -- is treated as double.)
    adddouble :: (Key k) => FDB -> k -> Double -> IO (Maybe Double)
    adddouble fdb key num =
        withForeignPtr (unTCFDB fdb) $ \fdb' -> do
          key' <- keyToInt key
          sumval <- c_tcfdbadddouble fdb' key' (realToFrac num)
          return $ if isNaN sumval
                     then Nothing
                     else Just $ realToFrac sumval

    -- | Synchronize updated contents of a database object with the file
    -- and the device.
    sync :: FDB -> IO Bool
    sync fdb = withForeignPtr (unTCFDB fdb) c_tcfdbsync

    -- |  Optimize the file of a Hash database object.
    optimize :: FDB -> Int32 -> Int64 -> IO Bool
    optimize fdb width limsiz =
        withForeignPtr (unTCFDB fdb) $ \fdb' -> c_tcfdboptimize fdb' width limsiz

    -- | Delete all records.
    vanish :: FDB -> IO Bool
    vanish fdb = withForeignPtr (unTCFDB fdb) c_tcfdbvanish

    -- | Copy the database file.
    copy :: FDB -> String -> IO Bool
    copy = copyHelper c_tcfdbcopy unTCFDB

    -- | Return the file path of currentry opened database.
    path :: FDB -> IO (Maybe String)
    path = pathHelper c_tcfdbpath unTCFDB

    -- | Return the number of records in the database.
    rnum :: FDB -> IO Word64
    rnum fdb = withForeignPtr (unTCFDB fdb) c_tcfdbrnum

    -- | Return the size of the database file.
    fsiz :: FDB -> IO Word64
    fsiz fdb = withForeignPtr (unTCFDB fdb) c_tcfdbfsiz

    keyToInt :: (Key k) => k -> IO Int64
    keyToInt i = catchJust selector (evaluate (unID . toID $ i)) handler
        where
          selector :: ErrorCall -> Maybe ()
          selector e = if show e == "Prelude.read: no parse"
                         then Just ()
                         else Nothing
          handler _ = error "Database.TokyoCabinet.FDB: invalid key"