%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2017-2024. 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(prim_buffer).
-moduledoc false.

-export([on_load/0]).

%% This is a mutable binary buffer that helps break out buffering logic from
%% NIFs/drivers, which is often the only thing that prevents the C code from
%% being reduced to bare system call wrappers.
%%
%% All operations in this file are thread-unsafe and risk crashing the emulator
%% if you're not careful.

-export([new/0, size/1, wipe/1, read/2, read_iovec/2, write/2, skip/2]).

-export([find_byte_index/2]).

-export([try_lock/1, unlock/1]).

-nifs([new/0, size/1, peek_head/1, copying_read/2, write/2, skip/2,
       find_byte_index/2, try_lock/1, unlock/1]).

-type prim_buffer() :: term().

%% Controls when to copy rather than extract sub-binaries from the buffer,
%% reducing the risk of small reads keeping a large binary alive.
-define(COPYING_READ_LIMIT, 512).

%% Reads that fit into heap binaries are always copied since the cost of
%% peeking binaries that short is largely equivalent to copying.
-define(ERL_ONHEAP_BIN_LIMIT, 64).

on_load() ->
    case erlang:load_nif(atom_to_list(?MODULE), 0) of
        ok -> ok
    end.

-spec new() -> prim_buffer().
new() ->
    erlang:nif_error(undef).

-spec size(Buffer :: prim_buffer()) -> non_neg_integer().
size(_Buffer) ->
    erlang:nif_error(undef).

%% Reads data as a binary from the front of the buffer. This will almost always
%% result in copying so it should be avoided unless you absolutely must have a
%% binary.
-spec read(Buffer :: prim_buffer(), Size :: non_neg_integer()) -> binary().
read(Buffer, Size) when Size =< ?ERL_ONHEAP_BIN_LIMIT ->
    copying_read(Buffer, Size);
read(Buffer, Size) when Size > ?ERL_ONHEAP_BIN_LIMIT ->
    iolist_to_binary(read_iovec(Buffer, Size)).

%% Reads data as an erlang:iovec() binary from the front of the buffer,
%% avoiding copying if reasonable.
-spec read_iovec(Buffer, Size) -> IOVec when
      Buffer :: prim_buffer(),
      Size :: non_neg_integer(),
      IOVec :: erlang:iovec().
read_iovec(Buffer, Size) when Size =< ?ERL_ONHEAP_BIN_LIMIT ->
    [copying_read(Buffer, Size)];
read_iovec(Buffer, Size) when Size > ?ERL_ONHEAP_BIN_LIMIT ->
    Head = peek_head(Buffer),
    HeadSize = byte_size(Head),
    if
        (HeadSize - Size) > ?COPYING_READ_LIMIT, Size =< ?COPYING_READ_LIMIT ->
            [copying_read(Buffer, Size)];
        HeadSize > Size ->
            skip(Buffer, Size),
            {First, _Rest} = split_binary(Head, Size),
            [First];
        HeadSize < Size ->
            skip(Buffer, HeadSize),
            [Head | read_iovec(Buffer, Size - HeadSize)];
        HeadSize =:= Size ->
            skip(Buffer, Size),
            [Head]
    end.

%% Writes an erlang:iovec() to the back of the buffer.
-spec write(Buffer :: prim_buffer(), IOVec :: erlang:iovec()) -> ok.
write(_Buffer, _IOVec) ->
    erlang:nif_error(undef).

%% Removes data from the front of the buffer without reading it.
-spec skip(Buffer :: prim_buffer(), Size :: non_neg_integer()) -> ok.
skip(_Buffer, _Size) ->
    erlang:nif_error(undef).

-spec wipe(Buffer :: prim_buffer()) -> ok.
wipe(Buffer) ->
    skip(Buffer, prim_buffer:size(Buffer)).

%% Finds the start-index of the first occurrence of Needle, for implementing
%% read_line and similar.
-spec find_byte_index(Buffer, Needle) -> Result when
      Buffer :: prim_buffer(),
      Needle :: non_neg_integer(),
      Result :: {ok, non_neg_integer()} |
                not_found.
find_byte_index(_Buffer, _Needle) ->
    erlang:nif_error(undef).

%% Attempts to take a unique lock on the buffer. Failure handling is left to
%% the user.
-spec try_lock(Buffer :: prim_buffer()) -> acquired | busy.
try_lock(_Buffer) ->
    erlang:nif_error(undef).

-spec unlock(Buffer :: prim_buffer()) -> ok.
unlock(_Buffer) ->
    erlang:nif_error(undef).

%% Unexported helper functions:

%% Reads data from the front of the buffer, returning a copy of the data to
%% avoid holding references.
-spec copying_read(Buffer :: prim_buffer(), Size :: non_neg_integer()) -> binary().
copying_read(_Buffer, _Size) ->
    erlang:nif_error(undef).

%% Returns the binary at the front of the buffer without modifying the buffer.
-spec peek_head(Buffer :: prim_buffer()) -> binary().
peek_head(_Buffer) ->
    erlang:nif_error(undef).
