%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2023-2026. All Rights Reserved.
%%
%% Licensed 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.
%%
%% %CopyrightEnd%
%%

-module(zstd).
-moduledoc """
Zstandard compression interface.

This module provides an API for the Zstandard library
([www.zstd.net](http://www.zstd.net)). It is used to compress and decompress data
and offers the same compression ratio as `m:zlib` but at a lower CPU cost.

Example:

```
1> Data = ~"my data to be compressed".
2> Compressed = zstd:compress(Data).
3> zstd:decompress(Compressed).
[~"my data to be compressed"]
```

If you are compressing or decompressing possibly large amounts of data,
it is also possible to do streamed compression/decompression.

Example:

```erlang
1> Compress = fun F(Ctx, D) ->
                      case file:read(D, 5) of
                          {ok, Data} ->
                              {continue, C} = zstd:stream(Ctx, Data),
                              [C|F(Ctx, D)];
                          eof ->
                              {done, C} = zstd:finish(Ctx, ""),
                              C
                      end
              end.
2> {ok, Ctx} = zstd:context(compress).
3> {ok, D} = file:open(File,[read,binary]).
4> Compressed = iolist_to_binary(Compress(Ctx, D)).
<<40,181,47,253,0,88,89,0,0,108,111,114,101,109,32,105,112,115,117,109>>
5> zstd:decompress(Compressed).
[~"lorem ipsum"]
```

In all functions errors can be thrown, where `Reason` describes the error.

Typical `Reason`s:

- **`badarg`** - Bad argument.
- **`zstd_error`** - An error generated by the Zstandard library.
- **`not_on_controlling_process`** - The context was used by a process that
did not create it.
""".
-moduledoc #{ since => "OTP 28.0" }.

-export_type([context/0,
              dict/0,
              compress_parameters/0,
              decompress_parameters/0]).

-doc """
A compression or decompression context that can be used
for streaming compression or decompression.

Only the process that created the context can use it.
""".
-doc #{ since => "OTP 28.0" }.
-opaque context() :: {compress | decompress, term()}.

-doc """
A compression or decompression dictionary.
""".
-doc #{ since => "OTP 28.0" }.
-opaque  dict() :: {cdict | ddict, term()}.

-doc """
The compression level.

Higher values mean better compression ratio at the sacrifice of
performance. A negative value sacrifices compression ratio in
favor of performance.

0 is a special value which represents the default compression level.
""".
-type compressionLevel() :: -22..22.

-doc """
The compression strategy.

The strategies are listed depending on which compression ratio they
give, that is the `fast` strategy is the fastest but also has the
worst compression ratio, while `btultra2` is the slowest but has
the best compression ratio.

`default` is a special strategy representing the current default strategy.

See the Zstandard documentation for details on each strategy.
""".
-type strategy() :: default | fast | dfast | greedy | lazy | lazy2 | btlazy2 |
                    btopt | btultra | btultra2.

-doc """
Compression parameters.

Zstandard has many parameters that can be tuned. Setting some parameters will
fail when set to an incorrect value, while others will be silently adjusted to
the closest valid value.

* `dictionary` - Sets the compression dictionary for the context. The dictionary
  can be either a `t:binary()` representing the dictionary or a compression `t:dict/0`.
  When a `t:dict/0` is attached to a context it will be kept alive until
  either the context is closed or it is replaced by another dictionary.

  To reset the context to not use any dictionary use the empty dictionary, that is `<<>>`.
* `pledgedSrcSize` - When using `stream/2` to do streaming compression, the decompressed
  size is not known when the header of the Zstandard frame is emitted. Setting this
  parameter on the context lets the compressor know the expected size of the data to
  compress. If the size is not correct when `finish/2` is called an exception will be
  generated. Using [`compress/1,2`](`compress/2`) will automatically set this value.
* `compressionLevel` - Sets the `t:compressionLevel/0`.
* `windowLog` | `hashLog` | `chainLog` | `searchLog` | `minMatch` |
  `targetLength` | `targetCBlockSize` - Set the corresponding parameter.
  See the Zstandard documentation for more details.
* `strategy` - Sets the compression `t:strategy/0`.
* `enableLongDistanceMatching` - Whether to enable Long Distance Matching or not.
  LDM is useful when compressing large datasets and is enabled by using a high
  `compressionLevel`.
* `ldmHashLog` | `ldmMinMatch` | `ldmBucketSizeLog` | `ldmHashRateLog` -
  Set the corresponding LDM parameter.
  See the Zstandard documentation for more details.
* `contentSizeFlag` - Whether to include the contentSize or not.
* `checksumFlag` - Whether to include the checksum or not.
* `dictIDFlag` - Whether to include the dictionary ID or not.

The Zstandard documentation contains more details about each parameter.
""".
-type compress_parameters() ::
        #{
          %% function calls
          dictionary => binary() | dict(),
          pledgedSrcSize => non_neg_integer(),

          %% Parameters
          compressionLevel => compressionLevel(),
          windowLog => non_neg_integer(),
          hashLog => non_neg_integer(),
          chainLog => non_neg_integer(),
          searchLog => non_neg_integer(),
          minMatch => non_neg_integer(),
          targetLength => non_neg_integer(),
          strategy => strategy(),
          targetCBlockSize => non_neg_integer(),

          %% LDM (Long distance matching parameter)
          enableLongDistanceMatching => boolean(),
          ldmHashLog => non_neg_integer(),
          ldmMinMatch => non_neg_integer(),
          ldmBucketSizeLog => non_neg_integer(),
          ldmHashRateLog => non_neg_integer(),

          %% frame parameters
          contentSizeFlag => boolean(),
          checksumFlag => boolean(),
          dictIDFlag => boolean()

         }.

-doc """
Decompression parameters.

Zstandard has many parameters that can be tuned. Setting some parameters will
fail when set to an incorrect value, while others will be silently adjusted to
the closest valid value.

* `dictionary` - Sets the decompression dictionary for the context. The dictionary
  can be either a `t:binary()` representing the dictionary or a decompression `t:dict/0`.
  When a `t:dict/0` is attached to a context it will be kept alive until
  either the context is closed or it is replaced by another dictionary.

To reset the context to not use any dictionary use the empty dictionary, that is `<<>>`.
* `windowLogMax` - Set the corresponding parameter. See the Zstandard documentation for more details.
""".
-type decompress_parameters() ::
        #{ dictionary => binary() | dict(),

           %% Parameters
           windowLogMax => non_neg_integer()

         }.

-on_load(on_load/0).

%% Basic API
-export([compress/1, compress/2, decompress/1, decompress/2]).

%% Streaming API
-export([context/1, context/2, stream/2, flush/1, finish/2, set_parameter/3,
         get_parameter/2, reset/1, close/1]).

%% Dict API
-export([dict/2, dict/3, get_dict_id/1]).

%% Frame API
-export([get_frame_header/1]).

-nifs([
       init_compress_nif/0, load_compress_dictionary_nif/2,
       ref_compress_dictionary_nif/2,
       set_compress_parameter_nif/3, get_compress_parameter_nif/2,
       set_pledged_src_size_nif/2, compress_stream_nif/3,
       compress_reset_nif/1, compress_close_nif/1,

       init_decompress_nif/0, load_decompress_dictionary_nif/2,
       ref_decompress_dictionary_nif/2,
       set_decompress_parameter_nif/3, get_decompress_parameter_nif/2,
       decompress_stream_nif/3, decompress_reset_nif/1,
       decompress_close_nif/1,

       create_cdict_nif/2, create_ddict_nif/1,
       getDictId_fromCDict_nif/1, getDictId_fromDDict_nif/1,
       getDictId_fromDict_nif/1, getDictId_fromFrame_nif/1,

       get_frame_header_nif/1
      ]).

%% ZSTD_EndDirective.
-define(ZSTD_e_continue, 0).
-define(ZSTD_e_flush,    1).
-define(ZSTD_e_end,      2).

-spec on_load() -> ok.
on_load() ->
    ok = erlang:load_nif("zstd", zstd).

-doc """
Set a parameter on a `context/0`.

See `t:compress_parameters/0` and `t:decompress_parameters/0` for details on
which parameters are available and what each parameter does.

Returns `ok` on success, raises an error on failure.

Example:

```
1> {ok, CCtx} = zstd:context(compress).
{ok, _}
2> ok = zstd:set_parameter(CCtx, compressionLevel, 15).
ok
3> zstd:stream(CCtx, "abc").
{continue, _}
4> catch zstd:set_parameter(CCtx, dictionary, "abc").
{'EXIT', {{zstd_error, <<"Operation not authorized at current processing stage">>}, _}}
```
""".
-doc #{ since => "OTP 28.0" }.
-spec set_parameter(Ctx :: context(), Key :: term(), Value :: term()) -> ok.
set_parameter({compress, Ctx}, dictionary, {cdict, Dict}) when is_reference(Dict) ->
    ref_compress_dictionary_nif(Ctx, Dict);
set_parameter({compress, Ctx}, dictionary, Dict) ->
    load_compress_dictionary_nif(Ctx, Dict);
set_parameter({compress, Ctx}, pledgedSrcSize, Size) ->
    set_pledged_src_size_nif(Ctx, Size);
set_parameter({decompress, Ctx}, dictionary, {ddict, Dict}) when is_reference(Dict) ->
    ref_decompress_dictionary_nif(Ctx, Dict);
set_parameter({decompress, Ctx}, dictionary, Dict) ->
    load_decompress_dictionary_nif(Ctx, Dict);
set_parameter({compress, Ctx}, Key, Value) ->
    set_compress_parameter_nif(Ctx, Key, Value);
set_parameter({decompress, Ctx}, Key, Value) ->
    set_decompress_parameter_nif(Ctx, Key, Value);
set_parameter(Ctx, _Key, _Value) ->
    error({badarg, {invalid_context, Ctx}}).

-doc """
Get a parameter from a `t:context/0`.

See `t:compress_parameters/0` and `t:decompress_parameters/0` for details on
which parameters are available and what each parameter does.

Note that it is not possible to get the `dictionary` and `pledgedSrcSize`
parameters using this API. Instead you can use `get_dict_id/1` on the
`t:context/0` to get the id of the dictionary used. There is no way to
get the `pledgedSrcSize`.

Returns `ok` on success, raises an error on failure.

Example:

```
1> {ok, CCtx} = zstd:context(compress).
{ok, _}
2> zstd:get_parameter(CCtx, compressionLevel).
3
3> zstd:set_parameter(CCtx, compressionLevel, 15).
ok
4> zstd:get_parameter(CCtx, compressionLevel).
15
```
""".
-doc #{ since => "OTP 28.0" }.
-spec get_parameter(Ctx :: context(), Key :: term()) -> Value :: term().
get_parameter({compress, Ctx}, Key) ->
    get_compress_parameter_nif(Ctx, Key);
get_parameter({decompress, Ctx}, Key) ->
    get_decompress_parameter_nif(Ctx, Key);
get_parameter(Ctx, _Key) ->
    error({badarg, {invalid_context, Ctx}}).

-doc #{ equiv => dict(Mode, Dict, #{}) }.
-doc #{ since => "OTP 28.0" }.
-spec dict(Mode :: compress | decompress, Dict :: binary()) -> {ok, dict()}.
dict(Mode, Dict) -> dict(Mode, Dict, #{}).

-doc """
Create a compression or decompression dictionary.

A compression dictionary can be used as a `t:compress_parameters/0` to use
a dictionary for compression. Dictionaries allow good compression ratios
even for small amounts of data.

A decompression dictionary can be used as a `t:decompress_parameters/0` to use
a dictionary for decompression. The same dictionary has to be used for
compression as decompression. To verify that the same dictionary is used
you can use `get_dict_id/1` on the dictionary and compressed data, or just
try to decompress as decompression will raise and exception if an incorrect
dictionary is given.

The `compressionLevel` set on a dictionary will override the `compressionLevel`
set in the `t:context/0`.

Example:

```
1> {ok, CDict} = zstd:dict(compress, Dict).
2> Data = lists:duplicate(100, 1).
[1, 1, 1 | _]
3> iolist_size(zstd:compress(Data)).
17
4> iolist_size(zstd:compress(Data, #{ dictionary => CDict, dictIDFlag => false })).
16
```

As loading a dictionary can be a heavy operations, it is possible to create
only a single `t:dict/0` and provide it to multiple `t:context/0`.

There is no API exposed in `m:zstd` to create a dictionary, instead use the
`zstd` command line tool.
""".
-doc #{ since => "OTP 28.0" }.
-spec dict(compress, Dict :: binary(), #{ compressionLevel => compressionLevel() }) -> {ok, dict()};
          (decompress, Dict :: binary(), #{ }) -> {ok, dict()}.
dict(compress, Dict, #{ compressionLevel := Level }) -> {ok, {cdict, create_cdict_nif(Dict, Level)}};
dict(compress, Dict, Params = #{ }) -> dict(compress, Dict, Params#{ compressionLevel => 0 });
dict(decompress, Dict, #{ }) -> {ok, {ddict, create_ddict_nif(Dict)}}.

-doc """
Get the dictionary ID of a dictionary or a frame.

The dictionary ID 0 represents no dictionary.

Example:

```
1> {ok, CDict} = zstd:dict(compress, Dict).
2> zstd:get_dict_id(CDict).
1850243626
3> zstd:get_dict_id(zstd:compress("abc")).
0
```
""".
-doc #{ since => "OTP 28.0" }.
-spec get_dict_id(DictOrFrame :: dict() | binary()) -> non_neg_integer().
get_dict_id({cdict, Dict}) ->
    getDictId_fromCDict_nif(Dict);
get_dict_id({ddict, Dict}) ->
    getDictId_fromDDict_nif(Dict);
get_dict_id(DictOrFrame) ->
    case get_frame_header(DictOrFrame) of
        {error, _Reason} ->
            getDictId_fromDict_nif(DictOrFrame);
        {ok, _} ->
            getDictId_fromFrame_nif(DictOrFrame)
    end.

-doc """
Get header of a Zstandard compressed frame.

A compressed Zstandard stream can consist of multiple frames. This
function will read metadata from the first frame. This information
can be useful when debugging corrupted Zstandard streams.

Example:

```
1> Compressed = zstd:compress(~"abc").
2> zstd:get_frame_header(Compressed).
{ok,#{frameContentSize => 3,windowSize => 3,blockSizeMax => 3,
      frameType => 'ZSTD_frame',headerSize => 6,
      dictID => 0, checksumFlag => false}}
```
""".
-doc #{ since => "OTP 28.0" }.
-spec get_frame_header(Frame :: iodata()) ->
          {ok, #{ blockSizeMax => non_neg_integer(),
                  checksumFlag => boolean(),
                  dictID => non_neg_integer(),
                  frameContentSize => non_neg_integer(),
                  frameType => 'ZSTD_frame' | 'ZSTD_skippableFrame',
                  headerSize => non_neg_integer(),
                  windowSize => non_neg_integer()
                }} |
          {error, unicode:chardata()}.
get_frame_header(Frame) ->
    try get_frame_header_nif(Frame)
    catch error:{zstd_error, Reason} ->
            {error, Reason}
    end.

-doc #{ equiv => context(Mode, #{}) }.
-doc #{ since => "OTP 28.0" }.
-spec context(compress | decompress) -> {ok, context()}.
context(Mode) when Mode =:= compress; Mode =:= decompress ->
    context(Mode, #{}).

-doc """
context(Mode, Options)

Create a compression or decompression context.

A context can be used to do streaming compression/decompression and allows
re-using parameters for multiple compressions/decompressions.
""".
-doc #{ since => "OTP 28.0" }.
-spec context(compress, Options :: compress_parameters()) -> {ok, context()};
             (decompress, Options :: decompress_parameters()) -> {ok, context()}.
context(compress, Options) ->
    Ref = {compress, init_compress_nif()},
    [ ok = set_parameter(Ref, Key, Value) || Key := Value <- Options ],
    {ok, Ref};
context(decompress, Options) ->
    Ref = {decompress, init_decompress_nif()},
    [ ok = set_parameter(Ref, Key, Value) || Key := Value <- Options ],
    {ok, Ref}.

-doc """
Compress or decompress a stream of data. The last stream of data should be called
with `finish/2` to complete the compression/decompression.

Example:

```
1> {ok, CCtx} = zstd:context(compress).
2> {continue, C1} = zstd:stream(CCtx, ~"a").
3> {done, C2} = zstd:finish(CCtx, ~"b").
4> Compressed = iolist_to_binary([C1, C2]).
<<40,181,47,253,0,88,17,0,0,97,98>>
5> zstd:decompress(Compressed).
[<<"ab">>]
```
""".
-doc #{ since => "OTP 28.0" }.
-spec stream(Ctx :: context(), Data :: iodata()) -> Result when
      Result :: {continue, Remainder :: erlang:iovec(), Output :: binary()} |
                {continue, Output :: binary()}.
stream(Ctx, Data) when is_list(Data) ->
    %% the stream_nif functions only take binaries as arguments,
    %% so we deal with iodata here.
    try erlang:iolist_to_iovec(Data) of
        [] ->
            stream(Ctx, <<>>);
        [H] ->
            stream(Ctx, H);
        [H|T] ->
            case stream(Ctx, H) of
                {continue, Rem, Out} ->
                    {continue, [Rem | T], Out};
                {continue, Out} ->
                    {continue, T, Out}
            end
    catch _:_ ->
            error(badarg)
    end;
stream({compress, Ref}, Data) ->
    compress_stream_nif(Ref, Data, ?ZSTD_e_continue);
stream({decompress, Ref}, Data) ->
    decompress_stream_nif(Ref, Data, ?ZSTD_e_continue).

-doc """
Flush the compression stream.

This flushes all pending compressed data without ending the frame,
allowing the compressed data to be read immediately while keeping
the context open for further compression.

Example:

```
1> {ok, CCtx} = zstd:context(compress).
2> {continue, C1} = zstd:stream(CCtx, ~"hello").
3> {continue, C2} = zstd:flush(CCtx).
4> zstd:decompress([C1, C2]).
[<<"hello">>]
```
""".
-doc #{ since => "OTP 29.0" }.
-spec flush(Ctx :: context()) -> Result when
      Result :: {continue, Output :: binary()}.
flush({compress, Ref} = Ctx) ->
    %% Note that we don't (yet) support providing arbitrary data here, as it's
    %% unclear what happens when ?ZSTD_e_flush is called multiple times with
    %% additional data each time. Adding a flush/2 variant later on is trivial
    %% and can be done in a minor release.
    case compress_stream_nif(Ref, <<>>, ?ZSTD_e_flush) of
        {continue, _} = Res ->
            Res;
        {done, Output} ->
            {continue, Output};
        {flush, Output0} ->
            %% This is a corner case encountered only with the debug emulator,
            %% which has a much narrower internal buffer.
            {continue, Output} = flush(Ctx),
            {continue, iolist_to_binary([Output0, Output])}
    end;
flush(Ctx={decompress, _}) ->
    error({badarg, {invalid_context, Ctx}}).

-doc """
Finish compressing/decompressing data.

This flushes all output buffers and resets the `t:context/0` so
that it can be used for compressing/decompressing again.

Example:

```
1> {ok, DCtx} = zstd:context(decompress).
2> {continue, D1} = zstd:stream(DCtx, <<40,181,47,253,32>>).
3> {done, D2} = zstd:finish(DCtx, <<2,17,0,0,97,98>>).
4> iolist_to_binary([D1,D2]).
<<"ab">>
```
""".
-doc #{ since => "OTP 28.0" }.
-spec finish(Ctx :: context(), Data :: iodata()) -> Result when
      Result :: {done, erlang:iovec()}.
finish({compress, Ref}, Data) when is_binary(Data) ->
    finish_1(fun compress_stream_nif/3, Ref, Data);
finish({decompress, Ref} = DCtx, Data) when is_binary(Data) ->
    Result = finish_1(fun decompress_stream_nif/3, Ref, Data),
    reset(DCtx),
    Result;
finish(Ctx, []) ->
    finish(Ctx, <<>>);
finish(Ctx, [Data]) when is_binary(Data) ->
    finish(Ctx, Data);
%% the stream_nif functions only take binaries as arguments,
%% so we deal with iodata here.
finish(Ctx, Data) ->
    case stream(Ctx, Data) of
        {continue, Remain, Out} ->
            case finish(Ctx, Remain) of
                {done, Tail} ->
                    {done, [Out | Tail]}
            end;
        {continue, Out} ->
            case finish(Ctx, <<>>) of
                {done, Tail} ->
                    {done, [Out | Tail]}
            end
    end.

finish_1(Codec, Ref, Data) when is_binary(Data) ->
    case Codec(Ref, Data, ?ZSTD_e_end) of
        {continue, Remainder, Output} ->
            case finish_1(Codec, Ref, Remainder) of
                {done, Tail} ->
                    {done, [Output | Tail]}
            end;
        {flush, Output} ->
            case finish_1(Codec, Ref, <<>>) of
                {done, Tail} ->
                    {done, [Output | Tail]}
            end;
        {done, Output} ->
            {done, [Output]}
    end.

-doc """
Reset a context while streaming data, returning it to its original state
but keeping all parameters set.

By resetting the state, the context can be re-used for other operations even
if it is in the middle of a (de)compression stream.

Example:

```
1> {ok, CCtx} = zstd:context(compress).
2> zstd:stream(CCtx, "a").
{continue, _}
3> zstd:reset(CCtx).
ok
4> {done, Compressed} = zstd:finish(CCtx, "b").
5> zstd:decompress(Compressed).
[~"b"]
```
""".
-doc #{ since => "OTP 28.0" }.
-spec reset(Ctx :: context()) -> ok.
reset({compress, Ref}) ->
    compress_reset_nif(Ref);
reset({decompress, Ref}) ->
    decompress_reset_nif(Ref).

-doc """
Close a `t:context/0`, releasing all referenced resources. After a `t:context/0`
is closed it is no longer possible to use it.

A `t:context/0` is automatically closed when GC:ed, so the only reason to call
this function is to make the resources attached to the context be released
before the next GC.
""".
-doc #{ since => "OTP 28.0" }.
-spec close(Ctx :: context()) -> ok.
close({compress, Ref}) ->
    compress_close_nif(Ref);
close({decompress, Ref}) ->
    decompress_close_nif(Ref).

-doc #{ equiv => compress(Data, #{}) }.
-doc #{ since => "OTP 28.0" }.
-spec compress(iodata()) -> iodata().
compress(Data) ->
    compress(Data, #{}).

-doc """
compress(Data, CtxOrOptions)

Compress `Data` using the given `t:compress_parameters/0` or the `t:context/0`.

Example:

```
1> zstd:compress("abc").
2> zstd:compress("abc", #{ compressionLevel => 20 }).
```
""".
-doc #{ since => "OTP 28.0" }.
-spec compress(Data :: iodata(), Options :: compress_parameters()) -> iodata();
              (Data :: iodata(), Ctx :: context()) -> iodata().
compress(Data, Options) when is_map(Options) ->
    {ok, Ref} = context(compress, Options),
    try
        compress(Data, Ref)
    after
        close(Ref)
    end;
compress(Data, {compress, Ref}) ->
    IOV = case erlang:iolist_to_iovec(Data) of
        %% Empty data must still become an empty compressed frame.
        [] -> [<<>>];
        IOV0 -> IOV0
    end,
    ok = set_pledged_src_size_nif(Ref, erlang:iolist_size(IOV)),
    codec_loop(fun compress_stream_nif/3,
               Ref,
               IOV).

-doc #{ equiv => decompress(Data, #{}) }.
-doc #{ since => "OTP 28.0" }.
-spec decompress(iodata()) -> iodata().
decompress(Data) ->
    decompress(Data, #{}).

-doc """
decompress(Data, CtxOrOptions)

Decompress `Data` using the given `t:decompress_parameters/0` or the `t:context/0`.

Example:

```
1> Compressed = zstd:compress("abc").
2> zstd:decompress(Compressed).
[~"abc"]
```
""".
-doc #{ since => "OTP 28.0" }.
-spec decompress(Data :: iodata(), Options :: decompress_parameters()) -> iodata();
                (Data :: iodata(), Ctx :: context()) -> iodata().
decompress(Data, Options) when is_map(Options) ->
    {ok, DCtx} = context(decompress, Options),
    try
        decompress(Data, DCtx)
    after
        close(DCtx)
    end;
decompress(Data, {decompress, Ref}) ->
    codec_loop(fun decompress_stream_nif/3,
               Ref,
               erlang:iolist_to_iovec(Data)).

codec_loop(Codec, Ref, [Data | Next]) ->
    EndDirective = case Next of
        [] -> ?ZSTD_e_end;
        _ -> ?ZSTD_e_continue
    end,
    case Codec(Ref, Data, EndDirective) of
        {continue, Remainder, Output} ->
            [Output | codec_loop(Codec, Ref, [Remainder | Next])];
        {continue, <<>>} ->
            codec_loop(Codec, Ref, Next);
        {continue, Output} ->
            [Output | codec_loop(Codec, Ref, Next)];
        {flush, Output} ->
            %% We have some remaining data that wouldn't fit into the final
            %% output buffer, give it another go with empty input.
            [Output | codec_loop(Codec, Ref, [<<>>])];
        {done, Output} ->
            [Output]
    end.

%%

init_compress_nif() ->
    erlang:nif_error(undef).
load_compress_dictionary_nif(_Ctx, _Dict) ->
    erlang:nif_error(undef).
ref_compress_dictionary_nif(_Ctx, _Dict) ->
    erlang:nif_error(undef).
set_compress_parameter_nif(_Ctx, _Key, _Value) ->
    erlang:nif_error(undef).
get_compress_parameter_nif(_Ctx, _Key) ->
    erlang:nif_error(undef).
set_pledged_src_size_nif(_Ctx, _Size) ->
    erlang:nif_error(undef).
compress_stream_nif(_, _, _) ->
    erlang:nif_error(undef).
compress_reset_nif(_) ->
    erlang:nif_error(undef).
compress_close_nif(_) ->
    erlang:nif_error(undef).

init_decompress_nif() ->
    erlang:nif_error(undef).
load_decompress_dictionary_nif(_Ctx, _Dict) ->
    erlang:nif_error(undef).
ref_decompress_dictionary_nif(_Ctx, _Dict) ->
    erlang:nif_error(undef).
set_decompress_parameter_nif(_Ctx, _Key, _Value) ->
    erlang:nif_error(undef).
get_decompress_parameter_nif(_Ctx, _Key) ->
    erlang:nif_error(undef).
decompress_stream_nif(_, _, _) ->
    erlang:nif_error(undef).
decompress_reset_nif(_) ->
    erlang:nif_error(undef).
decompress_close_nif(_) ->
    erlang:nif_error(undef).

create_cdict_nif(_Dict, _Level) ->
    erlang:nif_error(undef).
create_ddict_nif(_Dict) ->
    erlang:nif_error(undef).
getDictId_fromCDict_nif(_Dict) ->
    erlang:nif_error(undef).
getDictId_fromDDict_nif(_Dict) ->
    erlang:nif_error(undef).
getDictId_fromDict_nif(_Dict) ->
    erlang:nif_error(undef).
getDictId_fromFrame_nif(_Dict) ->
    erlang:nif_error(undef).

get_frame_header_nif(_Frame) ->
    erlang:nif_error(undef).
