src/erlzord.erl

-module(erlzord).
-author('Guilherme Andrade <erlzord(at)gandrade(dot)net>').

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

-export([config/3]).        -ignore_xref({config,3}).
-export([encode/2]).        -ignore_xref({encode,2}).
-export([decode/2]).        -ignore_xref({decode,2}).

-export([encode/4]).        -ignore_xref({encode,4}).
-export([decode/4]).        -ignore_xref({decode,4}).

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

-define(IS_UNSIGNED_INT(V), (is_integer((V)) andalso (V) >= 0)).

-define(IS_VALID_RANGE(Min, Max), (is_integer((Min)) andalso
                                   is_integer((Max)) andalso
                                   Max >= Min)).

-define(IS_OF_DIMENSION(Coordinates, Dimension),
        ((is_list((Coordinates)) andalso (length((Coordinates)) =:= (Dimension)))
            orelse
         (is_tuple((Coordinates)) andalso (size((Coordinates)) =:= (Dimension))))).

-define(IS_OF_BITSIZE(Value, Bitsize),
        (?IS_UNSIGNED_INT((Value)) andalso
         (Value < (1 bsl (Bitsize))))).

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

-opaque config() :: #{ dimension := non_neg_integer(),
                       min_coordinate_value := integer(),
                       max_coordinate_value := integer(),
                       coordinate_bitsize := non_neg_integer() }.

-type list_coordinates() :: [integer()].
-type tuple_coordinates() :: tuple().
-type coordinates() :: list_coordinates() | tuple_coordinates().

-export_type([config/0]).
-export_type([coordinates/0]).
-export_type([tuple_coordinates/0]).
-export_type([list_coordinates/0]).

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

-spec config(Dimension, MinCoordinateValue, MaxCoordinateValue) -> Config
        when Dimension :: non_neg_integer(),
             MinCoordinateValue :: integer(),
             MaxCoordinateValue :: integer(),
             Config :: config().

config(Dimension, MinCoordinateValue, MaxCoordinateValue)
  when ?IS_UNSIGNED_INT(Dimension),
       ?IS_VALID_RANGE(MinCoordinateValue, MaxCoordinateValue) ->
    Range = MaxCoordinateValue - MinCoordinateValue,
    CoordinateBitsize = unsigned_integer_bitsize(Range),
    #{ dimension => Dimension,
       min_coordinate_value => MinCoordinateValue,
       max_coordinate_value => MaxCoordinateValue,
       coordinate_bitsize => CoordinateBitsize }.


-spec encode(Coordinates, Config) -> Z
        when Coordinates :: coordinates(),
             Config :: config(),
             Z :: non_neg_integer().

encode(Coordinates, #{ dimension := Dimension } = Config)
  when ?IS_OF_DIMENSION(Coordinates, Dimension) ->
    #{ min_coordinate_value := MinCoordinateValue,
       max_coordinate_value := MaxCoordinateValue,
       coordinate_bitsize := CoordinateBitsize } = Config,
    encode_(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize).


-spec decode(Z, Config) -> Coordinates
        when Z :: non_neg_integer(),
             Config :: config(),
             Coordinates :: tuple_coordinates().

decode(Z, #{ dimension := Dimension, coordinate_bitsize := CoordinateBitsize } = Config)
  when ?IS_OF_BITSIZE(Z, Dimension * CoordinateBitsize) ->
    #{ min_coordinate_value := MinCoordinateValue,
       max_coordinate_value := MaxCoordinateValue } = Config,
    decode_(Z, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize).


-spec encode(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue) -> Z
        when Coordinates :: coordinates(),
             Dimension :: non_neg_integer(),
             MinCoordinateValue :: integer(),
             MaxCoordinateValue :: integer(),
             Z :: non_neg_integer().

encode(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue)
  when ?IS_OF_DIMENSION(Coordinates, Dimension),
       ?IS_UNSIGNED_INT(Dimension),
       ?IS_VALID_RANGE(MinCoordinateValue, MaxCoordinateValue) ->
    Range = MaxCoordinateValue - MinCoordinateValue,
    CoordinateBitsize = unsigned_integer_bitsize(Range),
    encode_(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize).


-spec decode(Z, Dimension, MinCoordinateValue, MaxCoordinateValue) -> Coordinates
        when Z :: non_neg_integer(),
             Dimension :: non_neg_integer(),
             MinCoordinateValue :: integer(),
             MaxCoordinateValue :: integer(),
             Coordinates :: tuple_coordinates().

decode(Z, Dimension, MinCoordinateValue, MaxCoordinateValue)
  when ?IS_UNSIGNED_INT(Z),
       ?IS_UNSIGNED_INT(Dimension),
       ?IS_VALID_RANGE(MinCoordinateValue, MaxCoordinateValue) ->
    Range = MaxCoordinateValue - MinCoordinateValue,
    CoordinateBitsize = unsigned_integer_bitsize(Range),
    (?IS_OF_BITSIZE(Z, Dimension * CoordinateBitsize) orelse exit({badarg,Z})),
    decode_(Z, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize).

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

-spec encode_(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize) -> Z
        when Coordinates :: coordinates(),
             Dimension :: non_neg_integer(),
             MinCoordinateValue :: integer(),
             MaxCoordinateValue :: integer(),
             CoordinateBitsize :: non_neg_integer(),
             Z :: non_neg_integer().

encode_(CoordinatesTuple, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize)
  when is_tuple(CoordinatesTuple) ->
    Coordinates = tuple_to_list(CoordinatesTuple),
    encode_(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize);
encode_(Coordinates, Dimension, MinCoordinateValue, MaxCoordinateValue, CoordinateBitsize) ->
    % Cull values and make them unsigned
    NormalizedCoordinates =
        [cull(V, MinCoordinateValue, MaxCoordinateValue) - MinCoordinateValue
         || V <- Coordinates],
    interleave(NormalizedCoordinates, Dimension, CoordinateBitsize).


-spec interleave(Values, Dimension, Bitsize) -> Interleaved
        when Values :: list_coordinates(),
             Dimension :: non_neg_integer(),
             Bitsize :: non_neg_integer(),
             Interleaved :: non_neg_integer().

interleave(Values, Dimension, Bitsize) ->
    interleave_recur(Values, Dimension, Bitsize, Bitsize, 0).


-spec interleave_recur(Values, Dimension, TotalBitsize, BitIndex, Acc) -> Interleaved
        when Values :: list_coordinates(),
             Dimension :: non_neg_integer(),
             TotalBitsize :: non_neg_integer(),
             BitIndex :: non_neg_integer(),
             Acc :: non_neg_integer(),
             Interleaved :: non_neg_integer().

interleave_recur(_Values, _Dimension, _TotalBitsize, 0 = _BitIndex, Acc) ->
    Acc;
interleave_recur(Values, Dimension, TotalBitsize, BitIndex, Acc) ->
    {Conjugated, NewValues} = conjugate_values(Values),
    ShiftAmount = Dimension * (TotalBitsize - BitIndex),
    ShiftedConjugated = Conjugated bsl ShiftAmount,
    NewAcc = Acc bor ShiftedConjugated,
    interleave_recur(NewValues, Dimension, TotalBitsize, BitIndex - 1, NewAcc).


-spec conjugate_values(Values) -> {Conjugated, NewValues}
        when Values :: [non_neg_integer()],
             Conjugated :: non_neg_integer(),
             NewValues :: [non_neg_integer()].

conjugate_values(Values) ->
    conjugate_values_recur(Values, 0, 0, []).


-spec conjugate_values_recur(Values, DimensionIndex, AccConjugated, AccNewValues)
    -> {Conjugated, NewValues}
        when Values :: list_coordinates(),
             DimensionIndex :: non_neg_integer(),
             AccConjugated :: non_neg_integer(),
             AccNewValues :: list_coordinates(),
             Conjugated :: non_neg_integer(),
             NewValues :: list_coordinates().

conjugate_values_recur([], _DimensionIndex, AccConjugated, AccNewValues) ->
    {AccConjugated, lists:reverse(AccNewValues)};
conjugate_values_recur([H | T], DimensionIndex, AccConjugated, AccNewValues) ->
    Bit = (H band 1) bsl DimensionIndex,
    NewH = H bsr 1,
    conjugate_values_recur(T, DimensionIndex + 1, Bit bor AccConjugated, [NewH | AccNewValues]).


-spec decode_(Z, Dimension, MinCoordinateValue, MaxCoordinateValue,
              Bitsize) -> Coordinates
        when Z :: non_neg_integer(),
             Dimension :: non_neg_integer(),
             MinCoordinateValue :: integer(),
             MaxCoordinateValue :: integer(),
             Bitsize :: non_neg_integer(),
             Coordinates :: tuple_coordinates().

decode_(Z, Dimension, MinCoordinateValue, MaxCoordinateValue, Bitsize) ->
    {NewZ, NormalizedCoordinates} = revert_interleave(Z, Dimension, Bitsize),

    (NewZ =:= 0 orelse throw({badarg,Z})), % some higher bits weren't processed
    Coordinates =
        lists:map(
          fun (NormalizedValue) ->
                  Value = MinCoordinateValue + NormalizedValue,
                  (Value =< MaxCoordinateValue orelse throw({badarg, Z})), % out of range
                  Value
          end,
          NormalizedCoordinates),

    list_to_tuple(Coordinates).


-spec revert_interleave(Interleaved, Dimension, Bitsize) -> {NewInterLeaved, Values}
        when Interleaved :: non_neg_integer(),
             Dimension :: non_neg_integer(),
             Bitsize :: non_neg_integer(),
             NewInterLeaved :: non_neg_integer(),
             Values :: [non_neg_integer()].

revert_interleave(Interleaved, Dimension, Bitsize) ->
    Acc0 = repeat(0, Dimension),
    revert_interleave_recur(Interleaved, Dimension, Bitsize, 0, Acc0).


-spec revert_interleave_recur(Interleaved, Dimension, Bitsize, BitIndex,
                              Acc) -> {NewInterLeaved, FinalAcc}
        when Interleaved :: non_neg_integer(),
             Dimension :: non_neg_integer(),
             Bitsize :: non_neg_integer(),
             BitIndex :: non_neg_integer(),
             Acc :: [non_neg_integer()],
             NewInterLeaved :: non_neg_integer(),
             FinalAcc :: [non_neg_integer()].

revert_interleave_recur(Interleaved, _Dimension, Bitsize, BitIndex, Acc)
  when BitIndex >= Bitsize ->
    {Interleaved, Acc};
revert_interleave_recur(Interleaved, Dimension, Bitsize, BitIndex, Acc) ->
    {NewInterLeaved, NewAcc} = unchain_values(Interleaved, Bitsize, BitIndex, Acc),
    revert_interleave_recur(NewInterLeaved, Dimension, Bitsize,
                            BitIndex + 1, NewAcc).


-spec unchain_values(Conjugated, Bitsize, BitIndex,
                     ValuesAcc) -> {NewConjugated, NewValuesAcc}
        when Conjugated :: non_neg_integer(),
             Bitsize :: non_neg_integer(),
             BitIndex :: non_neg_integer(),
             ValuesAcc :: [non_neg_integer()],
             NewConjugated :: non_neg_integer(),
             NewValuesAcc :: [non_neg_integer()].

unchain_values(Conjugated, Bitsize, BitIndex, ValuesAcc) ->
    unchain_values_recur(Conjugated, Bitsize, BitIndex, ValuesAcc, []).


-spec unchain_values_recur(Conjugated, Bitsize, BitIndex,
                           ValuesAcc, NewValuesAcc) -> {NewConjugated, FinalValuesAcc}
        when Conjugated :: non_neg_integer(),
             Bitsize :: non_neg_integer(),
             BitIndex :: non_neg_integer(),
             ValuesAcc :: [non_neg_integer()],
             NewConjugated :: non_neg_integer(),
             NewValuesAcc :: [non_neg_integer()],
             FinalValuesAcc :: [non_neg_integer()].

unchain_values_recur(Conjugated, _Bitsize, _BitIndex, [], NewValuesAcc) ->
    {Conjugated, lists:reverse(NewValuesAcc)};
unchain_values_recur(Conjugated, Bitsize, BitIndex, [H|T], NewValuesAcc) ->
    Bit = Conjugated band 1,
    NewConjugated = Conjugated bsr 1,
    NewH = H bor (Bit bsl BitIndex),
    unchain_values_recur(NewConjugated, Bitsize, BitIndex, T, [NewH | NewValuesAcc]).


-spec cull(Value, Min, Max) -> CulledValue
        when Value :: integer(),
             Min :: integer(),
             Max :: integer(),
             CulledValue :: integer().

cull(Value, Min, Max) when is_integer(Value) ->
    if Value > Max -> Max;
       Value < Min -> Min;
       true -> Value
    end.


-spec unsigned_integer_bitsize(Value) -> Bitsize
        when Value :: non_neg_integer(),
             Bitsize :: non_neg_integer().

unsigned_integer_bitsize(Value) ->
    unsigned_integer_bitsize_recur(Value, 0).


-spec unsigned_integer_bitsize_recur(Value, Acc) -> Bitsize
        when Value :: non_neg_integer(),
             Acc :: non_neg_integer(),
             Bitsize :: non_neg_integer().

unsigned_integer_bitsize_recur(Value, Acc) when Value < 1 ->
    Acc;
unsigned_integer_bitsize_recur(Value, Acc) ->
    unsigned_integer_bitsize_recur(Value bsr 1, Acc + 1).


-spec repeat(V, N) -> FinalAcc
        when V :: ValueT,
             N :: non_neg_integer(),
             FinalAcc :: [ValueT, ...],
             ValueT :: 0. % sigh

repeat(V, N) ->
    repeat_recur(V, N, []).


-spec repeat_recur(V, N, Acc) -> FinalAcc
        when V :: ValueT,
             N :: non_neg_integer(),
             Acc :: [ValueT],
             FinalAcc :: [ValueT, ...],
             ValueT :: 0.

repeat_recur(_V, N, Acc) when N < 1->
    Acc;
repeat_recur(V, N, Acc) ->
    repeat_recur(V, N - 1, [V | Acc]).