content: -- Licensed to the Apache Software Foundation (ASF) under one
  -- or more contributor license agreements.  See the NOTICE file
  -- distributed with this work for additional information
  -- regarding copyright ownership.  The ASF licenses this file
  -- to you under the Apache License, Version 2.0 (the
  -- "License"); you may not use this file except in compliance
  -- with the License.  You may obtain a copy of the License at
  --
  --   http://www.apache.org/licenses/LICENSE-2.0
  --
  -- Unless required by applicable law or agreed to in writing,
  -- software distributed under the License is distributed on an
  -- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  -- KIND, either express or implied.  See the License for the
  -- specific language governing permissions and limitations
  -- under the License.
  
  -- |
  -- Module      : OpenDAL
  -- Description : Haskell bindings for Apache OpenDAL
  -- Copyright   : (c) 2023 Apache OpenDAL
  -- License     : Apache-2.0
  -- Maintainer  : OpenDAL Contributors <dev@opendal.apache.org>"
  -- Stability   : experimental
  -- Portability : non - portable (GHC extensions)
  --
  -- This module provides Haskell bindings for Apache OpenDAL.
  module OpenDAL
    ( -- * Operator APIs
  
      -- ** Types
      OperatorConfig (..),
      Operator,
      Lister,
      Writer,
      WriterOption (..),
      OpenDALError (..),
      ErrorCode (..),
      EntryMode (..),
      Metadata (..),
      OperatorT (..),
      MonadOperation (..),
  
      -- ** Functions
      runOp,
      newOperator,
  
      -- * Writer Option Functions
      defaultWriterOption,
      appendWriterOption,
  
      -- * Lister APIs
      nextLister,
  
      -- * Writer APIs
      writerOpRaw,
      writerWrite,
      writerClose,
  
      -- * Operator Raw APIs
      -- $raw-operations
      readOpRaw,
      writeOpRaw,
      isExistOpRaw,
      createDirOpRaw,
      copyOpRaw,
      renameOpRaw,
      deleteOpRaw,
      statOpRaw,
      listOpRaw,
      scanOpRaw,
      operatorInfoRaw,
      removeAllOpRaw,
    )
  where
  
  import Colog (LogAction, Message, Msg (Msg), (<&))
  import Control.Monad.Except (ExceptT, MonadError, MonadTrans, runExceptT, throwError)
  import Control.Monad.Reader (MonadIO, MonadReader, ReaderT, ask, liftIO, runReaderT)
  import Control.Monad.Trans (MonadTrans (lift))
  import Data.ByteString (ByteString)
  import qualified Data.ByteString as BS
  import Data.HashMap.Strict (HashMap)
  import qualified Data.HashMap.Strict as HashMap
  import Data.String (IsString (fromString))
  import Data.Text (pack)
  import Data.Time (UTCTime, parseTimeM, zonedTimeToUTC)
  import Data.Time.Format (defaultTimeLocale)
  import Foreign
  import Foreign.C.String
  import GHC.Stack (emptyCallStack)
  import OpenDAL.FFI
  
  -- | `OperatorConfig` is the configuration for an `Operator`.
  -- We recommend using `OverloadedStrings` to construct a default config.
  --
  -- For example:
  --
  -- default config
  --
  -- @
  -- newOperator "memory"
  -- @
  --
  -- custom services config
  --
  -- @
  -- newOperator "memory" {ocConfig = HashMap.fromList [("root", "/tmp")]}
  -- @
  --
  -- enable logging
  --
  -- @
  -- newOperator "memory" {ocLogAction = Just simpleMessageAction}
  -- @
  data OperatorConfig = OperatorConfig
    { -- | The scheme of the operator. For example, "s3" or "gcs".
      ocScheme :: String,
      -- | The config for the operator services. For example, "endpoint" and "access_key_id" for s3. The config is same as rust core.
      ocConfig :: HashMap String String,
      -- | The log action for the operator. If it's `Nothing`, the operator won't enable logging.
      ocLogAction :: Maybe (LogAction IO Message)
    }
  
  instance IsString OperatorConfig where
    fromString s = OperatorConfig s HashMap.empty Nothing
  
  -- | `Operator` is the entry for all public blocking APIs.
  -- Create an `Operator` with `newOperator`.
  newtype Operator = Operator (ForeignPtr RawOperator)
  
  -- | `Lister` is designed to list entries at given path in a blocking manner.
  -- Users can construct Lister by `listOp` or `scanOp`.
  newtype Lister = Lister (ForeignPtr RawLister)
  
  -- | `Writer` is designed to write bytes into given path.
  newtype Writer = Writer (ForeignPtr RawWriter)
  
  -- | Options for creating a Writer.
  data WriterOption = WriterOption
    { -- | Whether to append to the file instead of overwriting it.
      woAppend :: Bool
    }
    deriving (Eq, Show)
  
  -- | Represents the possible error codes that can be returned by OpenDAL.
  data ErrorCode
    = -- | An error occurred in the FFI layer.
      FFIError
    | -- | OpenDAL don't know what happened here, and no actions other than just returning it back. For example, s3 returns an internal service error.
      Unexpected
    | -- | Underlying service doesn't support this operation.
      Unsupported
    | -- | The config for backend is invalid.
      ConfigInvalid
    | -- | The given path is not found.
      NotFound
    | -- | The given path doesn't have enough permission for this operation.
      PermissionDenied
    | -- | The given path is a directory.
      IsADirectory
    | -- | The given path is not a directory.
      NotADirectory
    | -- | The given path already exists thus we failed to the specified operation on it.
      AlreadyExists
    | -- | Requests that sent to this path is over the limit, please slow down.
      RateLimited
    | -- | The given file paths are same.
      IsSameFile
    deriving (Eq, Show)
  
  -- | Represents an error that can occur when using OpenDAL.
  data OpenDALError = OpenDALError
    { -- | The error code.
      errorCode :: ErrorCode,
      -- | The error message.
      message :: String
    }
    deriving (Eq, Show)
  
  -- | Represents the mode of an entry in a storage system (e.g., file or directory).
  data EntryMode = File | Dir | Unknown deriving (Eq, Show)
  
  -- | Represents metadata for an entry in a storage system.
  data Metadata = Metadata
    { -- | The mode of the entry.
      mMode :: EntryMode,
      -- | The cache control of the entry.
      mCacheControl :: Maybe String,
      -- | The content disposition of the entry.
      mContentDisposition :: Maybe String,
      -- | The content length of the entry.
      mContentLength :: Integer,
      -- | The content MD5 of the entry.
      mContentMD5 :: Maybe String,
      -- | The content type of the entry.
      mContentType :: Maybe String,
      -- | The ETag of the entry.
      mETag :: Maybe String,
      -- | The last modified time of the entry.
      mLastModified :: Maybe UTCTime
    }
    deriving (Eq, Show)
  
  -- | @newtype@ wrapper 'ReaderT' that keeps 'Operator' in its context.
  newtype OperatorT m a = OperatorT
    {runOperatorT :: ReaderT Operator (ExceptT OpenDALError m) a}
    deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Operator, MonadError OpenDALError)
  
  instance MonadTrans OperatorT where
    lift = OperatorT . lift . lift
  
  -- | A type class for monads that can perform OpenDAL operations.
  class (Monad m) => MonadOperation m where
    -- | Read the whole path into a bytes.
    readOp :: String -> m ByteString
  
    -- | Write bytes into given path.
    writeOp :: String -> ByteString -> m ()
  
    -- | Check if this path exists or not.
    isExistOp :: String -> m Bool
  
    -- | Create a dir at given path.
    createDirOp :: String -> m ()
  
    -- | Copy a file from srcPath to dstPath.
    copyOp :: String -> String -> m ()
  
    -- | Rename a file from srcPath to dstPath.
    renameOp :: String -> String -> m ()
  
    -- | Delete given path.
    deleteOp :: String -> m ()
  
    -- | Get given path's metadata without cache directly.
    statOp :: String -> m Metadata
  
    -- | List current dir path.
    -- This function will create a new handle to list entries.
    -- An error will be returned if path doesn't end with /.
    listOp :: String -> m Lister
  
    -- | List dir in flat way.
    -- Also, this function can be used to list a prefix.
    -- An error will be returned if given path doesn't end with /.
    scanOp :: String -> m Lister
  
    -- | Remove all files and directories recursively.
    removeAllOp :: String -> m ()
  
  instance (MonadIO m) => MonadOperation (OperatorT m) where
    readOp path = do
      op <- ask
      result <- liftIO $ readOpRaw op path
      either throwError return result
    writeOp path byte = do
      op <- ask
      result <- liftIO $ writeOpRaw op path byte
      either throwError return result
    isExistOp path = do
      op <- ask
      result <- liftIO $ isExistOpRaw op path
      either throwError return result
    createDirOp path = do
      op <- ask
      result <- liftIO $ createDirOpRaw op path
      either throwError return result
    copyOp src dst = do
      op <- ask
      result <- liftIO $ copyOpRaw op src dst
      either throwError return result
    renameOp src dst = do
      op <- ask
      result <- liftIO $ renameOpRaw op src dst
      either throwError return result
    deleteOp path = do
      op <- ask
      result <- liftIO $ deleteOpRaw op path
      either throwError return result
    statOp path = do
      op <- ask
      result <- liftIO $ statOpRaw op path
      either throwError return result
    listOp path = do
      op <- ask
      result <- liftIO $ listOpRaw op path
      either throwError return result
    scanOp path = do
      op <- ask
      result <- liftIO $ scanOpRaw op path
      either throwError return result
    removeAllOp path = do
      op <- ask
      result <- liftIO $ removeAllOpRaw op path
      either throwError return result
  
  -- helper functions
  
  byteSliceToByteString :: ByteSlice -> IO ByteString
  byteSliceToByteString (ByteSlice bsDataPtr len) = BS.packCStringLen (bsDataPtr, fromIntegral len)
  
  parseErrorCode :: Int -> ErrorCode
  parseErrorCode 1 = FFIError
  parseErrorCode 2 = Unexpected
  parseErrorCode 3 = Unsupported
  parseErrorCode 4 = ConfigInvalid
  parseErrorCode 5 = NotFound
  parseErrorCode 6 = PermissionDenied
  parseErrorCode 7 = IsADirectory
  parseErrorCode 8 = NotADirectory
  parseErrorCode 9 = AlreadyExists
  parseErrorCode 10 = RateLimited
  parseErrorCode 11 = IsSameFile
  parseErrorCode _ = FFIError
  
  parseEntryMode :: Int -> EntryMode
  parseEntryMode 0 = File
  parseEntryMode 1 = Dir
  parseEntryMode _ = Unknown
  
  parseCString :: CString -> IO (Maybe String)
  parseCString value | value == nullPtr = return Nothing
  parseCString value = do
    value' <- peekCString value
    free value
    return $ Just value'
  
  parseString :: CString -> IO String
  parseString value = do
    value' <- peekCString value
    free value
    return value'
  
  parseTime :: String -> Maybe UTCTime
  parseTime time = zonedTimeToUTC <$> parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" time
  
  parseFFIMetadata :: FFIMetadata -> IO Metadata
  parseFFIMetadata (FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified) = do
    let mode' = parseEntryMode $ fromIntegral mode
    cacheControl' <- parseCString cacheControl
    contentDisposition' <- parseCString contentDisposition
    let contentLength' = toInteger contentLength
    contentMD5' <- parseCString contentMD5
    contentType' <- parseCString contentType
    eTag' <- parseCString eTag
    lastModified' <- (>>= parseTime) <$> parseCString lastModified
    return $
      Metadata
        { mMode = mode',
          mCacheControl = cacheControl',
          mContentDisposition = contentDisposition',
          mContentLength = contentLength',
          mContentMD5 = contentMD5',
          mContentType = contentType',
          mETag = eTag',
          mLastModified = lastModified'
        }
  
  -- Exported functions
  
  -- | Default WriterOption for writing (overwriting).
  defaultWriterOption :: WriterOption
  defaultWriterOption = WriterOption {woAppend = False}
  
  -- | WriterOption for appending to a file.
  appendWriterOption :: WriterOption
  appendWriterOption = WriterOption {woAppend = True}
  
  -- |  Runner for 'OperatorT' monad.
  -- This function will run given 'OperatorT' monad with given 'Operator'.
  --
  -- Let's see an example:
  --
  -- @
  -- operation :: MonadOperation m => m ()
  -- operation = __do__
  --    writeOp op "key1" "value1"
  --    writeOp op "key2" "value2"
  --    value1 <- readOp op "key1"
  --    value2 <- readOp op "key2"
  -- @
  --
  -- You can run this operation with 'runOp' function:
  --
  -- @
  -- runOp operator operation
  -- @
  runOp :: Operator -> OperatorT m a -> m (Either OpenDALError a)
  runOp op = runExceptT . flip runReaderT op . runOperatorT
  {-# INLINE runOp #-}
  
  -- | Creates a new OpenDAL operator via `OperatorConfig`.
  newOperator :: OperatorConfig -> IO (Either OpenDALError Operator)
  newOperator (OperatorConfig scheme hashMap maybeLogger) = do
    let keysAndValues = HashMap.toList hashMap
    withCString scheme $ \cScheme ->
      withMany withCString (map fst keysAndValues) $ \cKeys ->
        withMany withCString (map snd keysAndValues) $ \cValues ->
          allocaArray (length keysAndValues) $ \cKeysPtr ->
            allocaArray (length keysAndValues) $ \cValuesPtr ->
              alloca $ \ffiResultPtr -> do
                logFnPtr <- case maybeLogger of
                  Just logger -> wrapLogFn (logFn logger)
                  Nothing -> return nullFunPtr
                pokeArray cKeysPtr cKeys
                pokeArray cValuesPtr cValues
                c_via_map_ffi cScheme cKeysPtr cValuesPtr (fromIntegral $ length keysAndValues) logFnPtr ffiResultPtr
                ffiResult <- peek ffiResultPtr
                if ffiCode ffiResult == 0
                  then do
                    op <- Operator <$> newForeignPtr c_free_operator (dataPtr ffiResult)
                    return $ Right op
                  else do
                    let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
                    errMsg <- peekCString (errorMessage ffiResult)
                    return $ Left $ OpenDALError code errMsg
    where
      logFn logger enumSeverity cStr = do
        str <- peekCString cStr
        logger <& Msg (toEnum (fromIntegral enumSeverity)) emptyCallStack (pack str)
  
  -- $raw-operations
  -- Functions for performing raw OpenDAL operations are defined below.
  -- These functions are not meant to be used directly in most cases.
  -- Instead, use the high-level interface provided by the 'MonadOperation' type class.
  
  -- | Read the whole path into a bytes.
  readOpRaw :: Operator -> String -> IO (Either OpenDALError ByteString)
  readOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_read opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then do
            byteslice <- peek $ dataPtr ffiResult
            byte <- byteSliceToByteString byteslice
            c_free_byteslice (bsData byteslice) (bsLen byteslice)
            return $ Right byte
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | Write bytes into given path.
  writeOpRaw :: Operator -> String -> ByteString -> IO (Either OpenDALError ())
  writeOpRaw (Operator op) path byte = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      BS.useAsCStringLen byte $ \(cByte, len) ->
        alloca $ \ffiResultPtr -> do
          c_blocking_write opptr cPath cByte (fromIntegral len) ffiResultPtr
          ffiResult <- peek ffiResultPtr
          if ffiCode ffiResult == 0
            then return $ Right ()
            else do
              let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
              errMsg <- peekCString (errorMessage ffiResult)
              return $ Left $ OpenDALError code errMsg
  
  -- | Check if this path exists or not.
  isExistOpRaw :: Operator -> String -> IO (Either OpenDALError Bool)
  isExistOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_is_exist opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then do
            val <- peek $ dataPtr ffiResult
            let isExist = val /= 0
            return $ Right isExist
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | Create a dir at given path.
  createDirOpRaw :: Operator -> String -> IO (Either OpenDALError ())
  createDirOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_create_dir opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then return $ Right ()
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | Copy a file from srcPath to dstPath.
  copyOpRaw :: Operator -> String -> String -> IO (Either OpenDALError ())
  copyOpRaw (Operator op) srcPath dstPath = withForeignPtr op $ \opptr ->
    withCString srcPath $ \cSrcPath ->
      withCString dstPath $ \cDstPath ->
        alloca $ \ffiResultPtr -> do
          c_blocking_copy opptr cSrcPath cDstPath ffiResultPtr
          ffiResult <- peek ffiResultPtr
          if ffiCode ffiResult == 0
            then return $ Right ()
            else do
              let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
              errMsg <- peekCString (errorMessage ffiResult)
              return $ Left $ OpenDALError code errMsg
  
  -- | Rename a file from srcPath to dstPath.
  renameOpRaw :: Operator -> String -> String -> IO (Either OpenDALError ())
  renameOpRaw (Operator op) srcPath dstPath = withForeignPtr op $ \opptr ->
    withCString srcPath $ \cSrcPath ->
      withCString dstPath $ \cDstPath ->
        alloca $ \ffiResultPtr -> do
          c_blocking_rename opptr cSrcPath cDstPath ffiResultPtr
          ffiResult <- peek ffiResultPtr
          if ffiCode ffiResult == 0
            then return $ Right ()
            else do
              let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
              errMsg <- peekCString (errorMessage ffiResult)
              return $ Left $ OpenDALError code errMsg
  
  -- | Delete given path.
  deleteOpRaw :: Operator -> String -> IO (Either OpenDALError ())
  deleteOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_delete opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then return $ Right ()
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | Get given path's metadata without cache directly.
  statOpRaw :: Operator -> String -> IO (Either OpenDALError Metadata)
  statOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_stat opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then do
            ffimatadata <- peek $ dataPtr ffiResult
            metadata <- parseFFIMetadata ffimatadata
            return $ Right metadata
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | List current dir path.
  -- This function will create a new handle to list entries.
  -- An error will be returned if path doesn't end with /.
  listOpRaw :: Operator -> String -> IO (Either OpenDALError Lister)
  listOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_list opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then do
            ffilister <- peek $ dataPtr ffiResult
            lister <- Lister <$> newForeignPtr c_free_lister ffilister
            return $ Right lister
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | List dir in flat way.
  -- Also, this function can be used to list a prefix.
  -- An error will be returned if given path doesn't end with /.
  scanOpRaw :: Operator -> String -> IO (Either OpenDALError Lister)
  scanOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_scan opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then do
            ffilister <- peek $ dataPtr ffiResult
            lister <- Lister <$> newForeignPtr c_free_lister ffilister
            return $ Right lister
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | Get operator info (scheme).
  operatorInfoRaw :: Operator -> IO (Either OpenDALError String)
  operatorInfoRaw (Operator op) = withForeignPtr op $ \opptr ->
    alloca $ \ffiResultPtr -> do
      c_operator_info opptr ffiResultPtr
      ffiResult <- peek ffiResultPtr
      if ffiCode ffiResult == 0
        then do
          val <- peek $ dataPtr ffiResult
          Right <$> parseString val
        else do
          let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
          errMsg <- peekCString (errorMessage ffiResult)
          return $ Left $ OpenDALError code errMsg
  
  -- | Get next entry path from `Lister`.
  nextLister :: Lister -> IO (Either OpenDALError (Maybe String))
  nextLister (Lister lister) = withForeignPtr lister $ \listerptr ->
    alloca $ \ffiResultPtr -> do
      c_lister_next listerptr ffiResultPtr
      ffiResult <- peek ffiResultPtr
      if ffiCode ffiResult == 0
        then do
          val <- peek $ dataPtr ffiResult
          Right <$> parseCString val
        else do
          let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
          errMsg <- peekCString (errorMessage ffiResult)
          return $ Left $ OpenDALError code errMsg
  
  
  -- | Writes bytes into given writer.
  writerWrite :: Writer -> ByteString -> IO (Either OpenDALError ())
  writerWrite (Writer writer) byte = withForeignPtr writer $ \writerptr ->
    BS.useAsCStringLen byte $ \(cByte, len) ->
      alloca $ \ffiResultPtr -> do
        c_writer_write writerptr cByte (fromIntegral len) ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then return $ Right ()
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  -- | Closes given writer.
  writerClose :: Writer -> IO (Either OpenDALError Metadata)
  writerClose (Writer writer) = withForeignPtr writer $ \writerptr ->
    alloca $ \ffiResultPtr -> do
      c_writer_close writerptr ffiResultPtr
      ffiResult <- peek ffiResultPtr
      if ffiCode ffiResult == 0
        then do
          ffimatadata <- peek $ dataPtr ffiResult
          metadata <- parseFFIMetadata ffimatadata
          return $ Right metadata
        else do
          let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
          errMsg <- peekCString (errorMessage ffiResult)
          return $ Left $ OpenDALError code errMsg
  
  -- | Create a new writer for given path with specified options.
  writerOpRaw :: Operator -> String -> WriterOption -> IO (Either OpenDALError Writer)
  writerOpRaw (Operator op) path writerOption = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        if woAppend writerOption
          then c_blocking_writer_append opptr cPath ffiResultPtr
          else c_blocking_writer opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then do
            ffiwriter <- peek $ dataPtr ffiResult
            writer <- Writer <$> newForeignPtr c_free_writer ffiwriter
            return $ Right writer
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  
  
  
  -- | Remove all files and directories recursively.
  removeAllOpRaw :: Operator -> String -> IO (Either OpenDALError ())
  removeAllOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
    withCString path $ \cPath ->
      alloca $ \ffiResultPtr -> do
        c_blocking_remove_all opptr cPath ffiResultPtr
        ffiResult <- peek ffiResultPtr
        if ffiCode ffiResult == 0
          then return $ Right ()
          else do
            let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
            errMsg <- peekCString (errorMessage ffiResult)
            return $ Left $ OpenDALError code errMsg
  