-- Copyright (c) 2016-present, Facebook, Inc.
-- All rights reserved.
--
-- This source code is licensed under the BSD-style license found in
-- the LICENSE file in the root directory of this source tree. An
-- additional grant of patent rights can be found in the PATENTS file
-- in the same directory.

{-# LANGUAGE MagicHash #-}

-- |
-- Module      : Codec.Compression.Zstd.FFI
-- Copyright   : (c) 2016-present, Facebook, Inc. All rights reserved.
--
-- License     : BSD3
-- Maintainer  : bryano@fb.com
-- Stability   : experimental
-- Portability : GHC
--
-- Low-level bindings to the native zstd compression library.  These
-- bindings make almost no effort to provide any additional safety or
-- ease of use above that of the C library.  Unless you have highly
-- specialized needs, you should use the streaming or base APIs
-- instead.
--
-- To correctly use the functions in this module, you must read the
-- API documentation in the zstd library's @zstd.h@ include file.  It
-- would also be wise to search elsewhere in this package for uses of
-- the functions you are interested in.

module Codec.Compression.Zstd.FFI
    (
    -- * One-shot functions
      compress
    , compressBound
    , maxCLevel
    , decompress
    , getDecompressedSize

    -- ** Cheaper operations using contexts
    -- *** Compression
    , CCtx
    , createCCtx
    , freeCCtx
    , p_freeCCtx
    , compressCCtx

    -- *** Decompression
    , DCtx
    , createDCtx
    , freeDCtx
    , p_freeDCtx
    , decompressDCtx

    -- * Result and error checks
    , isError
    , getErrorName
    , checkError
    , checkAlloc

    -- * Streaming operations
    -- ** Streaming types
    , CStream
    , DStream
    , Buffer(..)
    , In
    , Out

    -- ** Streaming compression
    , cstreamInSize
    , cstreamOutSize
    , createCStream
    , freeCStream
    , p_freeCStream
    , initCStream
    , compressStream
    , endStream

    -- ** Streaming decompression
    , dstreamInSize
    , dstreamOutSize
    , createDStream
    , initDStream
    , decompressStream
    , freeDStream
    , p_freeDStream

    -- * Dictionary-based compression
    , trainFromBuffer
    , getDictID
    , compressUsingDict
    , decompressUsingDict

    -- ** Pre-digested dictionaries
    -- *** Compression
    , CDict
    , createCDict
    , freeCDict
    , p_freeCDict
    , compressUsingCDict

    -- *** Decompression
    , DDict
    , createDDict
    , freeDDict
    , p_freeDDict
    , decompressUsingDDict

    -- * Low-level code
    , c_maxCLevel
    ) where

import Codec.Compression.Zstd.FFI.Types
import Foreign.C.Types (CInt(..), CSize(..), CUInt(..), CULLong(..))
import Foreign.Ptr (FunPtr, nullPtr)
import GHC.CString (unpackCString#)
import GHC.IO.Exception
import GHC.Ptr (Ptr(..))

-- | Compress bytes from source buffer into destination buffer.
-- The destination buffer must be already allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_compress"
    compress :: Ptr dst         -- ^ Destination buffer.
             -> CSize           -- ^ Capacity of destination buffer.
             -> Ptr src         -- ^ Source buffer.
             -> CSize           -- ^ Size of source buffer.
             -> CInt            -- ^ Compression level.
             -> IO CSize

-- | Returns the maximum compression level supported by the library.
foreign import ccall unsafe "ZSTD_maxCLevel"
    c_maxCLevel :: CInt

-- | The maximum compression level supported by the library.
maxCLevel :: Int
maxCLevel = fromIntegral c_maxCLevel

-- | Compute the maximum compressed size of given source buffer.
foreign import ccall unsafe "ZSTD_compressBound"
    compressBound :: CSize -- ^ Size of input.
                  -> IO CSize

foreign import ccall unsafe "ZSTD_isError"
    c_isError :: CSize -> CUInt

-- | Indicates whether a return value is an error code.
isError :: CSize -> Bool
isError sizeOrError = c_isError sizeOrError /= 0

-- | Gives the description associated with an error code.
--
-- Always returns a valid pointer to a constant string.
foreign import ccall unsafe "ZSTD_getErrorName"
    c_getErrorName :: CSize -> Ptr a

-- | Gives the description associated with an error code.
getErrorName :: CSize -> String
getErrorName cs = unpackCString# (case c_getErrorName cs of Ptr a -> a)

-- | Decompress a buffer.  The destination buffer must be already
-- allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_decompress"
    decompress :: Ptr dst         -- ^ Destination buffer.
               -> CSize           -- ^ Capacity of destination buffer.
               -> Ptr src         -- ^ Source buffer.
               -> CSize
               -- ^ Size of compressed input.  This must be exact, so
               -- for example supplying the size of a buffer that is
               -- larger than the compressed input will cause a failure.
               -> IO CSize

-- | Returns the decompressed size of a compressed payload if known, 0
-- otherwise.
--
-- To discover precisely why a result is 0, follow up with
-- 'getFrameParams'.
foreign import ccall unsafe "ZSTD_getDecompressedSize"
    getDecompressedSize :: Ptr src
                        -> CSize
                        -> IO CULLong

-- | Allocate a compression context.
foreign import ccall unsafe "ZSTD_createCCtx"
    createCCtx :: IO (Ptr CCtx)

-- | Free a compression context.
foreign import ccall unsafe "ZSTD_freeCCtx"
    freeCCtx :: Ptr CCtx -> IO ()

-- | Free a compression context.  For use by a finalizer.
foreign import ccall unsafe "zstd.h &ZSTD_freeCCtx"
    p_freeCCtx :: FunPtr (Ptr CCtx -> IO ())

-- | Compress bytes from source buffer into destination buffer.
-- The destination buffer must be already allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_compressCCtx"
    compressCCtx :: Ptr CCtx    -- ^ Compression context.
                 -> Ptr dst     -- ^ Destination buffer.
                 -> CSize       -- ^ Capacity of destination buffer.
                 -> Ptr src     -- ^ Source buffer.
                 -> CSize       -- ^ Size of source buffer.
                 -> CInt        -- ^ Compression level.
                 -> IO CSize

-- | Compress bytes from source buffer into destination buffer, using
-- a prebuilt dictionary.  The destination buffer must be already
-- allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_compress_usingDict"
    compressUsingDict
        :: Ptr CCtx    -- ^ Compression context.
        -> Ptr dst     -- ^ Destination buffer.
        -> CSize       -- ^ Capacity of destination buffer.
        -> Ptr src     -- ^ Source buffer.
        -> CSize       -- ^ Size of source buffer.
        -> Ptr dict     -- ^ Dictionary.
        -> CSize       -- ^ Size of dictionary.
        -> CInt        -- ^ Compression level.
        -> IO CSize

-- | Compress bytes from source buffer into destination buffer, using
-- a pre-built, pre-digested dictionary.  The destination buffer must
-- be already allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_compress_usingCDict"
    compressUsingCDict
        :: Ptr CCtx    -- ^ Compression context.
        -> Ptr dst     -- ^ Destination buffer.
        -> CSize       -- ^ Capacity of destination buffer.
        -> Ptr src     -- ^ Source buffer.
        -> CSize       -- ^ Size of source buffer.
        -> Ptr CDict   -- ^ Dictionary.
        -> IO CSize

-- | Decompress a buffer, using a prebuilt dictionary.  The
-- destination buffer must be already allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_decompress_usingDict"
    decompressUsingDict
        :: Ptr DCtx        -- ^ Decompression context.
        -> Ptr dst         -- ^ Destination buffer.
        -> CSize           -- ^ Capacity of destination buffer.
        -> Ptr src         -- ^ Source buffer.
        -> CSize
        -- ^ Size of compressed input.  This must be exact, so
        -- for example supplying the size of a buffer that is
        -- larger than the compressed input will cause a failure.
        -> Ptr dict        -- ^ Dictionary.
        -> CSize           -- ^ Size of dictionary.
        -> IO CSize

-- | Decompress a buffer, using a pre-built, pre-digested dictionary.
-- The destination buffer must be already allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_decompress_usingDDict"
    decompressUsingDDict
        :: Ptr DCtx        -- ^ Decompression context.
        -> Ptr dst         -- ^ Destination buffer.
        -> CSize           -- ^ Capacity of destination buffer.
        -> Ptr src         -- ^ Source buffer.
        -> CSize
        -- ^ Size of compressed input.  This must be exact, so
        -- for example supplying the size of a buffer that is
        -- larger than the compressed input will cause a failure.
        -> Ptr DDict       -- ^ Dictionary.
        -> IO CSize

-- | Allocate a decompression context.
foreign import ccall unsafe "ZSTD_createDCtx"
    createDCtx :: IO (Ptr DCtx)

-- | Free a decompression context.
foreign import ccall unsafe "ZSTD_freeDCtx"
    freeDCtx :: Ptr DCtx -> IO ()

-- | Free a decompression context.  For use by a finalizer.
foreign import ccall unsafe "zstd.h &ZSTD_freeDCtx"
    p_freeDCtx :: FunPtr (Ptr DCtx -> IO ())

-- | Decompress a buffer.  The destination buffer must be already
-- allocated.
--
-- Returns the number of bytes written into destination buffer, or an
-- error code if it fails (which can be tested using 'isError').
foreign import ccall unsafe "ZSTD_decompressDCtx"
    decompressDCtx :: Ptr DCtx  -- ^ Decompression context.
                   -> Ptr dst   -- ^ Destination buffer.
                   -> CSize     -- ^ Capacity of destination buffer.
                   -> Ptr src   -- ^ Source buffer.
                   -> CSize
                   -- ^ Size of compressed input.  This must be exact, so
                   -- for example supplying the size of a buffer that is
                   -- larger than the compressed input will cause a failure.
                   -> IO CSize

-- | Recommended size for input buffer.
foreign import ccall unsafe "ZSTD_CStreamInSize"
    cstreamInSize :: CSize

-- | Recommended size for output buffer.
foreign import ccall unsafe "ZSTD_CStreamOutSize"
    cstreamOutSize :: CSize

-- | A context for streaming compression.
data CStream

-- | Create a streaming compression context.  This must be freed using
-- 'freeCStream', or if using a finalizer, with 'p_freeCStream'.
foreign import ccall unsafe "ZSTD_createCStream"
    createCStream :: IO (Ptr CStream)

-- | Free a 'CStream' value.
foreign import ccall unsafe "ZSTD_freeCStream"
    freeCStream :: Ptr CStream -> IO ()

-- | Free a 'CStream' value.  For use by a finalizer.
foreign import ccall unsafe "zstd.h &ZSTD_freeCStream"
    p_freeCStream :: FunPtr (Ptr CStream -> IO ())

-- | Begin a new compression operation.
foreign import ccall unsafe "ZSTD_initCStream"
    initCStream :: Ptr CStream
                -> CInt         -- ^ Compression level.
                -> IO CSize

-- | Consume part or all of an input.
foreign import ccall unsafe "ZSTD_compressStream"
    compressStream :: Ptr CStream -> Ptr (Buffer Out) -> Ptr (Buffer In)
                   -> IO CSize

-- | End a compression stream. This performs a flush and writes a
-- frame epilogue.
foreign import ccall unsafe "ZSTD_endStream"
    endStream :: Ptr CStream -> Ptr (Buffer Out) -> IO CSize

-- | Recommended size for input buffer.
foreign import ccall unsafe "ZSTD_DStreamInSize"
    dstreamInSize :: CSize

-- | Recommended size for output buffer.
foreign import ccall unsafe "ZSTD_DStreamOutSize"
    dstreamOutSize :: CSize

-- | A context for streaming decompression.
data DStream

-- | Create a streaming decompression context.  This must be freed using
-- 'freeDStream', or if using a finalizer, with 'p_freeDStream'.
foreign import ccall unsafe "ZSTD_createDStream"
    createDStream :: IO (Ptr DStream)

-- | Begin a new streaming decompression operation.
foreign import ccall unsafe "ZSTD_initDStream"
    initDStream :: Ptr DStream -> IO CSize

-- | Consume part or all of an input.
foreign import ccall unsafe "ZSTD_decompressStream"
    decompressStream :: Ptr DStream -> Ptr (Buffer Out) -> Ptr (Buffer In)
                     -> IO CSize

-- | Free a 'CStream' value.
foreign import ccall unsafe "ZSTD_freeDStream"
    freeDStream :: Ptr DStream -> IO ()

-- | Free a 'CStream' value.  For use by a finalizer.
foreign import ccall unsafe "zstd.h &ZSTD_freeDStream"
    p_freeDStream :: FunPtr (Ptr DStream -> IO ())

-- | Train a dictionary from a collection of samples.
-- Returns the number size of the resulting dictionary.
foreign import ccall unsafe "ZDICT_trainFromBuffer"
    trainFromBuffer :: Ptr dict
                    -- ^ Preallocated dictionary buffer.
                    -> CSize
                    -- ^ Capacity of dictionary buffer.
                    -> Ptr samples
                    -- ^ Concatenated samples.
                    -> Ptr CSize
                    -- ^ Array of sizes of samples.
                    -> CUInt
                    -- ^ Number of samples.
                    -> IO CSize

-- | Return the identifier for the given dictionary, or zero if
-- not a valid dictionary.
foreign import ccall unsafe "ZDICT_getDictID"
    getDictID :: Ptr dict
              -- ^ Dictionary.
              -> CSize
              -- ^ Size of dictionary.
              -> IO CUInt

-- | Allocate a pre-digested dictionary.
foreign import ccall unsafe "ZSTD_createCDict"
    createCDict :: Ptr dict
                -- ^ Dictionary.
                -> CSize
                -- ^ Size of dictionary.
                -> CInt
                -- ^ Compression level.
                -> IO (Ptr CDict)

-- | Free a pre-digested dictionary.
foreign import ccall unsafe "ZSTD_freeCDict"
    freeCDict :: Ptr CDict -> IO ()

-- | Free a pre-digested dictionary.
foreign import ccall unsafe "zstd.h &ZSTD_freeCDict"
    p_freeCDict :: FunPtr (Ptr CDict -> IO ())

-- | Allocate a pre-digested dictionary.
foreign import ccall unsafe "ZSTD_createDDict"
    createDDict :: Ptr dict
                -- ^ Dictionary.
                -> CSize
                -- ^ Size of dictionary.
                -> IO (Ptr DDict)

-- | Free a pre-digested dictionary.
foreign import ccall unsafe "ZSTD_freeDDict"
    freeDDict :: Ptr DDict -> IO ()

-- | Free a pre-digested dictionary.
foreign import ccall unsafe "zstd.h &ZSTD_freeDDict"
    p_freeDDict :: FunPtr (Ptr DDict -> IO ())

-- | Check that an allocating operation is successful.  If it fails,
-- throw an 'IOError'.
checkAlloc :: String -> IO (Ptr a) -> IO (Ptr a)
checkAlloc name act = do
  addr <- act
  if addr == nullPtr
    then ioError (IOError Nothing ResourceExhausted name
                  "out of memory" Nothing Nothing)
    else return addr

-- | Check whether a 'CSize' has an error encoded in it (yuck!), and
-- report success or failure more safely.
checkError :: IO CSize -> IO (Either String CSize)
checkError act = do
  ret <- act
  return $! if isError ret
            then Left (getErrorName ret)
            else Right ret
