src/locus_shared_bitarray.erl

%% Copyright (c) 2021-2024 Guilherme Andrade
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy  of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.
%%
%% locus is an independent project and has not been authorized, sponsored,
%% or otherwise approved by MaxMind.

%% @private
-module(locus_shared_bitarray).

-include_lib("stdlib/include/assert.hrl").

%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------

-export([new/1,
         set/2,
         is_set/2,
         get_positions_set_at_cell/2]).

%% ------------------------------------------------------------------
%% Macro Definitions
%% ------------------------------------------------------------------

-define(BITSHIFT, 6).

%% ------------------------------------------------------------------
%% Type Definitions
%% ------------------------------------------------------------------

-opaque t() :: atomics:atomics_ref().
-export_type([t/0]).

%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------

-spec new(pos_integer()) -> t().
new(Length) ->
    Size = ceil(Length / (1 bsl ?BITSHIFT)),
    atomics:new(Size, [{signed, false}]).

-spec set(t(), non_neg_integer()) -> ok.
set(Array, Position) ->
    OneBasedIndex = (Position bsr ?BITSHIFT) + 1,
    Offset = Position band ((1 bsl ?BITSHIFT) - 1),
    UpdateMask = 1 bsl Offset,
    Cell = atomics:get(Array, OneBasedIndex),
    set_recur(Array, Cell, OneBasedIndex, UpdateMask).

-spec is_set(t(), non_neg_integer()) -> boolean().
is_set(Array, Position) ->
    OneBasedIndex = (Position bsr ?BITSHIFT) + 1,
    Offset = Position band ((1 bsl ?BITSHIFT) - 1),
    ReadMask = 1 bsl Offset,
    try atomics:get(Array, OneBasedIndex) of
        Cell ->
            (Cell band ReadMask) =/= 0
    catch
        error:badarg when is_reference(Array), is_integer(Position), Position >= 0 ->
            throw({position_out_of_bounds, Position})
    end.

-spec get_positions_set_at_cell(t(), non_neg_integer()) -> [non_neg_integer(), ...].
get_positions_set_at_cell(Array, Index) ->
    try atomics:get(Array, _OneBasedIndex = Index + 1) of
        Cell ->
            BasePosition = Index bsl ?BITSHIFT,
            positions_set_at_cell(BasePosition, Cell)
    catch
        error:badarg ->
            #{size := Size} = atomics:info(Array),
            ?assertMatch({true, _, _}, {Index >= Size, Index, Size}),
            throw({index_out_of_bounds, Index})
    end.

%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------

set_recur(Array, Cell, OneBasedIndex, UpdateMask) ->
    case Cell bor UpdateMask of
        Cell ->
            ok;
        NewCell ->
            case atomics:exchange(Array, OneBasedIndex, NewCell) of
                Cell ->
                    ok;
                UpdatedCell ->
                    set_recur(Array, UpdatedCell, OneBasedIndex, UpdateMask)
            end
    end.

positions_set_at_cell(BasePosition, Cell)
  when Cell =/= 0 ->
    case Cell band 1 of
        1 ->
            [BasePosition | positions_set_at_cell(BasePosition + 1,
                                                  Cell bsr 1)];
        _ ->
            positions_set_at_cell(BasePosition + 1, Cell bsr 1)
    end;
positions_set_at_cell(_BasePosition, _Cell) ->
    [].