src/partisan_remote_ref.erl

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2016 Christopher Meiklejohn.  All Rights Reserved.
%% Copyright (c) 2022 Alejandro M. Ramallo. All Rights Reserved.
%%
%% This file is provided to you 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.
%%
%% -------------------------------------------------------------------

%% -----------------------------------------------------------------------------
%% @doc Remote references are Partisan's representation for remote process
%% identifiers (`pid()'), registered names and references (`reference()').
%%
%% Distributed Erlang (disterl) will transform the representation of process
%% identifiers, registered names and references when they are sent to a remote
%% node. This is done to disambiguate between remote and local instances.
%% Because Partisan doesn't use disterl it needs to implement this same
%% disambiguation mechanism somehow. As disterl's implementation is done by the
%% BEAM internally and not API is exposed, this module is required to
%% achieve a similar result.
%%
%% == Representation ==
%% In cases where lots of references are stored in process state, `ets' and
%% specially where those are uses as keys, a binary format is preferable to the
%% tuple format in order to save memory and avoid copying the term every time a
%% message is send between processes (by leveraging off-heap binary storage).
%%
%% For this reason, this module implements two alternative representations:
%% <ul>
%% <li>references as binary URIs</li>
%% <li>references as tuples</li>
%% </ul>
%%
%% The representation to use is controlled by the configuration option
%% `remote_ref_as_uri`. If `true' this module will generate references as
%% binary URIs. Otherwise it will generate them as tuples.r
%%
%% === URI Representation ===
%%
%% ```
%% 1> partisan_remote_ref:from_term(self()).
%% <<"partisan:pid:nonode@nohost:0.1062.0">>
%% '''
%%
%% ==== URI Padding ====
%%
%% For those cases where the resulting references are smaller than 64 bytes (
%% and thus will be stored on the process heap) this module can pad the
%% generated binary URIs to 65 bytes, thus forcing them to be stored off-heap.
%% This is controlled with the configuration option `remote_ref_binary_padding'.
%%
%% ```
%% 1> partisan_config:set(remote_ref_binary_padding, false).
%% 2> partisan_remote_ref:from_term(self()).
%% <<"partisan:pid:nonode@nohost:0.1062.0">>
%% 3> partisan_config:set(remote_ref_binary_padding, true).
%% ok
%% 4> partisan_remote_ref:from_term(self()).
%% <<"partisan:pid:nonode@nohost:0.1062.0:"...>>
%% '''
%%
%% === Tuple Representation ===
%%
%% ```
%% 1> partisan_remote_ref:from_term(self()).
%% {partisan_remote_reference,
%%    nonode@nohost,
%%    {partisan_process_reference,"<0.1062.0>"}}
%% '''
%%
%% == Issues and TODOs ==
%% As opposed to erlang pid encodintg (`NEW_PID_EXT`) our current
%% representation cannot distinguished between identifiers from old (crashed)
%% nodes from a new one. So maybe we need to adopt the `NEW_PID_EXT' `Creation'
%% attribute.
%% @end
%% -----------------------------------------------------------------------------
-module(partisan_remote_ref).

-include("partisan_logger.hrl").
-include("partisan.hrl").

-define(SEP, $:).
-define(PADDING_START, $\31).

-type t()               ::  p() | r() | n().
-type format()          ::  improper_list | tuple | uri.
-type p()               ::  [node()|binary()]
                            | tuple_ref(encoded_pid())
                            | uri().
-type r()               ::  [node()|binary()]
                            | tuple_ref(encoded_ref())
                            | uri().
-type n()               ::  [node()|binary()]
                            | tuple_ref(encoded_name())
                            | uri().
-type uri()             ::  <<_:64, _:_*8>>.
-type tuple_ref(T)      ::  {?MODULE, node(), T}.
-type target()          ::  encoded_pid() | encoded_ref() | encoded_name().
-type encoded_pid()     ::  {encoded_pid, list()}.
-type encoded_name()    ::  {encoded_name, list()}.
-type encoded_ref()     ::  {encoded_ref, list()}.



-export_type([t/0]).
-export_type([p/0]).
-export_type([r/0]).
-export_type([n/0]).
-export_type([encoded_pid/0]).
-export_type([encoded_name/0]).
-export_type([encoded_ref/0]).


-export([from_term/1]).
-export([from_term/2]).
-export([is_identical/2]).
-export([is_local/1]).
-export([is_local/2]).
-export([is_local_name/1]).
-export([is_local_name/2]).
-export([is_local_pid/1]).
-export([is_local_pid/2]).
-export([is_local_reference/1]).
-export([is_local_reference/2]).
-export([is_name/1]).
-export([is_name/2]).
-export([is_pid/1]).
-export([is_reference/1]).
-export([is_type/1]).
-export([node/1]).
-export([nodestring/1]).
-export([target/1]).
-export([to_name/1]).
-export([to_pid/1]).
-export([to_pid_or_name/1]).
-export([to_term/1]).



-compile({no_auto_import, [is_pid/1]}).
-compile({no_auto_import, [is_reference/1]}).
-compile({no_auto_import, [node/1]}).

-eqwalizer({nowarn_function, register_local_pid/1}).

-dialyzer([{nowarn_function, encode/3}, no_improper_lists]).


%% =============================================================================
%% API
%% =============================================================================



%% -----------------------------------------------------------------------------
%% @doc Returns the partisan-encoded representation of a process identifier,
%% reference, local or remote registered name (atom).
%%
%% In the case of a name, the function does not check `Name' is an actual
%% registered name.
%% @end
%% -----------------------------------------------------------------------------
-spec from_term(pid() | reference() | atom() | {atom(), node()}) ->
    t() | no_return().

from_term({Name, Node}) ->
    from_term(Name, Node);

from_term(Term)
when is_atom(Term); erlang:is_pid(Term); erlang:is_reference(Term) ->
    encode(Term, partisan:node()).


%% -----------------------------------------------------------------------------
%% @doc Returns the partisan-encoded representation of a registered name `Name'
%% at node `Node'.
%% The function does not check `Name' is an actual registered name,
%% @end
%% -----------------------------------------------------------------------------
-spec from_term(Term :: pid() | reference() | atom(), Node :: node()) ->
    t() | no_return().

from_term(Term, Node)
when is_atom(Node) andalso (
    erlang:is_pid(Term)
    orelse erlang:is_reference(Term) orelse is_atom(Term)) ->
    encode(Term, Node);

from_term(_, _) ->
    error(badarg).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec to_term(t()) -> pid() | reference() | atom() | no_return().

to_term(Ref) ->
    try
        decode_term(Ref)
    catch
        throw:badarg ->
            Info = #{
                cause => #{
                    1 =>
                        "should be a partisan_remote_ref representing a "
                        "local pid, local registered name, or "
                        "local reference."
                }
            },
            erlang:error(badarg, [Ref], [{error_info, Info}])
    end.


%% -----------------------------------------------------------------------------
%% @doc Calls {@link to_term/1} and returns the result if it is a local pid().
%% Otherwise fails with `badarg'
%% @end
%% -----------------------------------------------------------------------------
-spec to_pid(Arg :: p()) -> pid() | no_return().

to_pid(Arg) ->
    case to_term(Arg) of
        Term when erlang:is_pid(Term) ->
            Term;
        _ ->
            Info = #{cause => #{1 => "not a partisan_remote_ref:p()."}},
            erlang:error(badarg, [Arg], [{error_info, Info}])
    end.


%% -----------------------------------------------------------------------------
%% @doc Calls {@link to_term/1} and returns the result if it is an local
%% name i.e. atom(). Otherwise fails with `badarg'
%% @end
%% -----------------------------------------------------------------------------
-spec to_name(Arg :: n()) -> atom() | no_return().

to_name(Arg) ->
    case to_term(Arg) of
        Term when is_atom(Term) ->
            Term;
        _ ->
            Info = #{cause => #{1 => "not a pid partisan_remote_ref:n()."}},
            erlang:error(badarg, [Arg], [{error_info, Info}])
    end.


%% -----------------------------------------------------------------------------
%% @doc Calls {@link to_term/1} and returns the result if it is an local
%% name i.e. atom() or local pid(). Otherwise fails with `badarg'
%% @end
%% -----------------------------------------------------------------------------
-spec to_pid_or_name(Arg :: p() | n()) -> atom() | pid() | no_return().

to_pid_or_name(Arg) ->
    case to_term(Arg) of
        Term when is_atom(Term) ->
            Term;
        Term when erlang:is_pid(Term) ->
            Term;
        _ ->
            Info = #{cause => #{
                1 => "not a partisan_remote_ref:p() or partisan_remote_ref:n()."
            }},
            erlang:error(badarg, [Arg], [{error_info, Info}])
    end.


%% -----------------------------------------------------------------------------
%% @doc Calls {@link to_term/1} and returns the result if it is a local
%% reference(). Otherwise fails with `badarg'
%% @end
%% -----------------------------------------------------------------------------
-spec to_reference(Arg :: r()) -> reference() | no_return().

to_reference(Arg) ->
    case to_term(Arg) of
        Term when erlang:is_reference(Term) ->
            Term;
        _ ->
            Info = #{cause => #{1 => "not a partisan_remote_ref:r()."}},
            erlang:error(badarg, [Arg], [{error_info, Info}])
    end.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec target(PRef :: t()) -> target() | no_return().

target(Ref) ->
    decode_target(Ref).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec node(PRef :: t()) -> node() | no_return().

node([Node|<<"#Pid", _/binary>>]) when is_atom(Node) ->
    Node;

node([Node|<<"#Ref", _/binary>>]) when is_atom(Node) ->
    Node;

node([Node|<<"#Name", _/binary>>]) when is_atom(Node) ->
    Node;

node(<<"partisan:pid:", Rest/binary>>) ->
    get_node(Rest);

node(<<"partisan:ref:", Rest/binary>>) ->
    get_node(Rest);

node(<<"partisan:name:", Rest/binary>>) ->
    get_node(Rest);

node({?MODULE, Node, _}) when is_atom(Node) ->
    Node;

node(_) ->
    error(badarg).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec nodestring(PRef :: t()) -> binary() | no_return().

nodestring([Node|<<"#Pid", _/binary>>]) when is_atom(Node) ->
    atom_to_binary(Node, utf8);

nodestring([Node|<<"#Ref", _/binary>>]) when is_atom(Node) ->
    atom_to_binary(Node, utf8);

nodestring([Node|<<"#Name", _/binary>>]) when is_atom(Node) ->
    atom_to_binary(Node, utf8);

nodestring(<<"partisan:pid:", Rest/binary>>) ->
    get_nodestring(Rest);

nodestring(<<"partisan:ref:", Rest/binary>>) ->
    get_nodestring(Rest);

nodestring(<<"partisan:name:", Rest/binary>>) ->
    get_nodestring(Rest);

nodestring({?MODULE, Node, _}) ->
    atom_to_binary(Node, utf8);

nodestring(_) ->
    error(badarg).



%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local(PRef :: t()) -> boolean() | no_return().

is_local([Node|<<"#Pid", _/binary>>]) when is_atom(Node) ->
    Node =:= partisan:node();

is_local([Node|<<"#Ref", _/binary>>]) when is_atom(Node) ->
    Node =:= partisan:node();

is_local([Node|<<"#Name", _/binary>>]) when is_atom(Node) ->
    Node =:= partisan:node();

is_local(<<"partisan:pid:", Rest/binary>>) ->
    do_is_local(Rest);

is_local(<<"partisan:ref:", Rest/binary>>) ->
    do_is_local(Rest);

is_local(<<"partisan:name:", Rest/binary>>) ->
    do_is_local(Rest);

is_local({?MODULE, Node, _}) ->
    Node =:= partisan:node();

is_local(Arg) ->
    error({badarg, Arg}).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local_pid(PRef :: t()) -> boolean() | no_return().

is_local_pid([Node|<<"#Pid", _/binary>>]) when is_atom(Node) ->
    Node =:= partisan:node();

is_local_pid(<<"partisan:pid:", Rest/binary>>) ->
    do_is_local(Rest);

is_local_pid({?MODULE, Node, {encoded_pid, _}}) ->
    Node =:= partisan:node();

is_local_pid(_) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local_pid(PRef :: t(), Pid :: pid()) -> boolean() | no_return().

is_local_pid([Node|<<"#Pid", Rest/binary>>], Pid)
when is_atom(Node), erlang:is_pid(Pid) ->
    Node =:= partisan:node()
        andalso Rest =:= list_to_binary(pid_to_list(Pid));

is_local_pid(<<"partisan:pid:", Rest/binary>>, Pid)
when erlang:is_pid(Pid) ->
    PidAsBin = list_to_binary(pid_to_list(Pid)),
    do_is_local(Rest, partisan:nodestring(), PidAsBin);

is_local_pid({?MODULE, Node, {encoded_pid, PidAsList}}, Pid)
when erlang:is_pid(Pid) ->
    PidAsList =:= pid_to_list(Pid) andalso Node =:= partisan:node();

is_local_pid(Process, Pid) when erlang:is_pid(Pid) ->
    try to_term(Process) of
        Name when is_atom(Name) ->
            Pid =:= whereis(Name);
        _ ->
            false
    catch
        error:badarg ->
            false
    end.



%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local_reference(PRef :: t()) -> boolean() | no_return().

is_local_reference([Node|<<"#Ref", _/binary>>]) when is_atom(Node) ->
    Node =:= partisan:node();

is_local_reference(<<"partisan:ref:", Rest/binary>>) ->
    do_is_local(Rest);

is_local_reference({?MODULE, Node, {encoded_ref, _}}) ->
    Node =:= partisan:node();

is_local_reference(_) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local_reference(PRef :: t(), LocalRef :: reference()) ->
    boolean() | no_return().

is_local_reference([Node|<<"#Ref", _/binary>> = Bin], Ref)
when is_atom(Node), erlang:is_reference(Ref) ->
    Node =:= partisan:node()
        andalso Bin =:= list_to_binary(ref_to_list(Ref));

is_local_reference(<<"partisan:ref:", Rest/binary>>, Ref)
when erlang:is_reference(Ref) ->
    RefAsBin = list_to_binary(ref_to_list(Ref)),
    do_is_local(Rest, partisan:nodestring(), RefAsBin);

is_local_reference({?MODULE, Node, {encoded_ref, RefAsList}}, Ref)
when erlang:is_reference(Ref) ->
    RefAsList =:= ref_to_list(Ref) andalso Node =:= partisan:node();

is_local_reference(_, _) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local_name(PRef :: t()) -> boolean() | no_return().

is_local_name([Node|<<"#Name", _/binary>>]) when is_atom(Node) ->
    Node =:= partisan:node();

is_local_name(<<"partisan:name:", Rest/binary>>) ->
    do_is_local(Rest);

is_local_name({?MODULE, Node, {encoded_name, _}}) ->
    Node =:= partisan:node();

is_local_name(_) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_local_name(PRef :: t(), Name :: atom()) -> boolean() | no_return().

is_local_name([Node|<<"#Name", Rest/binary>>], Name)
when is_atom(Node), is_atom(Name) ->
    Node =:= partisan:node()
        andalso Rest =:= atom_to_binary(Name, utf8);

is_local_name(<<"partisan:name:", Rest/binary>>, Name) ->
    NameAsBin = atom_to_binary(Name, utf8),
    do_is_local(Rest, partisan:nodestring(), NameAsBin);

is_local_name({?MODULE, Node, {encoded_name, NameAsList}}, Name) ->
    NameAsList =:= atom_to_list(Name) andalso Node =:= partisan:node();

is_local_name(_, _) ->
    false.



%% -----------------------------------------------------------------------------
%% @doc Returns true if reference `Ref' is located in node `Node'.
%% @end
%% -----------------------------------------------------------------------------
-spec is_local(PRef :: t(), Node :: node()) -> boolean() | no_return().

is_local([Node|<<"#Pid", _/binary>>], OtherNode) when is_atom(OtherNode) ->
    Node =:= OtherNode;

is_local([Node|<<"#Ref", _/binary>>], OtherNode) when is_atom(OtherNode) ->
    Node =:= OtherNode;

is_local([Node|<<"#Name", _/binary>>], OtherNode) when is_atom(OtherNode) ->
    Node =:= OtherNode;

is_local(<<"partisan:pid:", Rest/binary>>, Node) when is_atom(Node) ->
    do_is_local(Rest, Node);

is_local(<<"partisan:ref:", Rest/binary>>, Node) when is_atom(Node) ->
    do_is_local(Rest, Node);

is_local(<<"partisan:name:", Rest/binary>>, Node) when is_atom(Node) ->
    do_is_local(Rest, Node);

is_local({?MODULE, Node, _}, OtherNode) when is_atom(OtherNode) ->
    Node =:= OtherNode;

is_local(_, _) ->
    error(badarg).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_type(any()) -> boolean().

is_type(Term) ->
    is_pid(Term) orelse
    is_reference(Term) orelse
    is_name(Term).


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_pid(any()) -> boolean().

is_pid([Node|<<"#Pid", _/binary>>]) when is_atom(Node) ->
    true;

is_pid(<<"partisan:pid:", _/binary>>) ->
    true;

is_pid({?MODULE, _Node, Term}) ->
    is_pid(Term);

is_pid({encoded_pid, _}) ->
    true;

is_pid(_) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_reference(any()) -> boolean().

is_reference([Node|<<"#Ref", _/binary>>]) when is_atom(Node) ->
    true;

is_reference(<<"partisan:ref:", _/binary>>) ->
    true;

is_reference({?MODULE, _, Term}) ->
    is_reference(Term);

is_reference({encoded_ref, _}) ->
    true;

is_reference(_) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_name(any()) -> boolean().

is_name([Node|<<"#Name", _/binary>>]) when is_atom(Node) ->
    true;

is_name(<<"partisan:name:", _/binary>>) ->
    true;

is_name({?MODULE, _, Term}) ->
    is_name(Term);

is_name({encoded_name, _}) ->
    true;

is_name(_) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec is_name(Ref :: any(), Name :: atom()) -> boolean().

is_name([Node|<<"#Name", Rest/binary>>], Name)
when is_atom(Node), is_atom(Name) ->
    Rest == atom_to_binary(Name, utf8);

is_name(<<"partisan:name:", Rest/binary>>, Name) when is_atom(Name) ->
    NameStr = atom_to_binary(Name, utf8),
    case binary:split(Rest, <<?SEP>>, [global]) of
        [_Node, NameStr] ->
            true;
        _ ->
            false
    end;

is_name({?MODULE, _, Term}, Name) when is_atom(Name) ->
    is_name(Term, Name);

is_name({encoded_name, NameStr}, Name) when is_atom(Name) ->
    NameStr == atom_to_list(Name);

is_name(_, _) ->
    false.


%% -----------------------------------------------------------------------------
%% @doc Checks two refs for identity. Two refs are identical if the are
%% equal or if one is a process reference and the other one is a registered
%% name reference of said process.
%% In the latter case the function uses `erlang:whereis/1' which means the check
%% can fail if the process has died (and thus is no longer registered).
%% @end
%% -----------------------------------------------------------------------------
-spec is_identical(A :: t(), B :: t()) -> boolean().

is_identical(A, A) ->
    true;

is_identical(A, B) ->
    case node(A) == node(B) of
        true ->
            case {to_term(A), to_term(B)} of
                {Term, Term} ->
                    true;
                {Pid, Name} when erlang:is_pid(Pid) andalso is_atom(Name) ->
                    Pid == whereis(Name);
                _ ->
                    false
            end;
        false ->
            false
    end.



%% =============================================================================
%% PRIVATE
%% =============================================================================



%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec encode(pid() | reference() | atom(), node()) -> t() | no_return().

encode(Term, Node) ->
    case partisan_config:get(remote_ref_format, improper_list) of
        Format when Format == improper_list; Format == tuple; Format == uri ->
            encode(Term, Node, Format);
        false ->
            error(badarg)
    end.


%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec encode(pid() | reference() | atom(), node(), format()) ->
    t() | no_return().

encode(Pid, Node, Format) when erlang:is_pid(Pid) ->
    PidNode = erlang:node(Pid),
    PidStr0 = pid_to_list(Pid),
    PidStr =
        case PidNode == Node of
            true ->
                _ = maybe_register_pid(Pid, PidNode, local),
                PidStr0;
            false ->
                PidStr1 = to_local_pid_list(PidStr0),
                LocalPid = list_to_pid(PidStr1),
                _ = maybe_register_pid(LocalPid, PidNode, remote),
                PidStr1
        end,

    case Format of
        improper_list ->
            %% eqwalizer:ignore improper_list
            [Node|list_to_binary("#Pid" ++ PidStr)];
        uri ->
            PidBin = untag(PidStr),
            Nodestr = atom_to_binary(Node, utf8),
            maybe_pad(<<"partisan:pid:", Nodestr/binary, ?SEP, PidBin/binary>>);
        tuple ->
            Target = {encoded_pid, PidStr},
            {?MODULE, Node, Target}
    end;

encode(Ref, Node, improper_list) when erlang:is_reference(Ref) ->
    Node =:= partisan:node() orelse error(badarg),
    %% eqwalizer:ignore improper_list
    [Node|list_to_binary(ref_to_list(Ref))];

encode(Ref, Node, uri) when erlang:is_reference(Ref) ->
    %% We do not support reference rewriting
    Node =:= partisan:node() orelse error(badarg),
    <<"#Ref", RefBin/binary>> = untag(ref_to_list(Ref)),
    Nodestring = atom_to_binary(Node, utf8),
    maybe_pad(<<"partisan:ref:", Nodestring/binary, ?SEP, RefBin/binary>>);

encode(Ref, Node, tuple) when erlang:is_reference(Ref) ->
    %% We do not support reference rewriting
    Node =:= partisan:node() orelse error(badarg),
    Target = {encoded_ref, erlang:ref_to_list(Ref)},
    {?MODULE, Node, Target};

encode(Name, Node, improper_list) when is_atom(Name) ->
    %% eqwalizer:ignore improper_list
    [Node|list_to_binary("#Name" ++ atom_to_list(Name))];

encode(Name, Node, uri) when is_atom(Name) ->
    NameBin = atom_to_binary(Name, utf8),
    Nodestring = atom_to_binary(Node, utf8),
    maybe_pad(<<"partisan:name:", Nodestring/binary, ?SEP, NameBin/binary>>);

encode(Name, Node, tuple) when is_atom(Name) ->
    Target = {encoded_name, atom_to_list(Name)},
    {?MODULE, Node, Target}.



%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec decode_term(t() | target()) -> pid() | reference() | atom() | no_return().

decode_term([Node|<<"#Pid", Rest/binary>>]) when is_atom(Node) ->
    Node == partisan:node() orelse throw(badarg),
    to_local_pid(binary_to_list(Rest));

decode_term([Node|<<"#Ref", _/binary>> = Bin]) when is_atom(Node) ->
    Node == partisan:node() orelse throw(badarg),
    list_to_ref(binary_to_list(Bin));

decode_term([Node|<<"#Name", Rest/binary>>]) when is_atom(Node) ->
    Node == partisan:node() orelse throw(badarg),
    binary_to_existing_atom(Rest, utf8);

decode_term(<<"partisan:", Rest0/binary>>) ->
    ThisNode = partisan:nodestring(),

    %% We remove padding
    Rest =
        case binary:split(Rest0, <<?PADDING_START>>, [global]) of
            [Rest0] ->
                Rest0;
            [Rest1 | _Padding] ->
                Rest1
        end,

    %% We ignore everything following the 3rd element.
    %% More elements are allowed because we plan to allow
    %% user-defined Uri schemes where the uri can have more elements
    %% e.g. to encode metadata (but we will provide functions to extract the
    %% metadata, here we don't need it)
    case binary:split(Rest, <<?SEP>>, [global]) of

        [<<"pid">>, Node, Term | _] when Node == ThisNode ->
            to_local_pid(tag(Term));

        [<<"ref">>, Node, Term | _] when Node == ThisNode ->
            list_to_ref("#Ref" ++ tag(Term));

        [<<"name">>, Node, Term | _] when Node == ThisNode ->
            binary_to_existing_atom(Term, utf8);

        _ ->
            throw(badarg)
    end;

decode_term({?MODULE, Node, Target}) ->
    Node =:= partisan:node() orelse throw(badarg),
    decode_term(Target);

decode_term({encoded_pid, Value}) ->
    list_to_pid(Value);

decode_term({encoded_ref, Value}) ->
    list_to_ref(Value);

decode_term({encoded_name, Value}) ->
    list_to_existing_atom(Value).




%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec decode_target(t() | target()) -> target() | no_return().

decode_target([Node|<<"#Pid", Rest/binary>>]) when is_atom(Node) ->
    {encoded_pid, binary_to_list(Rest)};

decode_target([Node|<<"#Ref", _/binary>> = Bin]) when is_atom(Node) ->
    {encoded_ref, binary_to_list(Bin)};

decode_target([Node|<<"#Name", Rest/binary>>]) when is_atom(Node) ->
    {encoded_name, binary_to_list(Rest)};

decode_target(<<"partisan:", Rest0/binary>>) ->
    %% We remove padding
    Rest =
        case binary:split(Rest0, <<?PADDING_START>>, [global]) of
            [Rest0] ->
                Rest0;
            [Rest1 | _Padding] ->
                Rest1
        end,

    %% We ignore everything following the 3rd element.
    %% More elements are allowed because we plan to allow
    %% user-defined Uri schemes where the uri can have more elements
    %% e.g. to encode metadata (but we will provide functions to extract the
    %% metadata, here we don't need it)
    case binary:split(Rest, <<?SEP>>, [global]) of

        [<<"pid">>, _, Term | _] ->
            {encoded_pid, tag(Term)};

        [<<"ref">>, _, Term | _] ->
            {encoded_ref, "#Ref" ++ tag(Term)};

        [<<"name">>, _, Term | _] ->
            {encoded_name, binary_to_list(Term)};


        _ ->
            throw(badarg)
    end;

decode_target({?MODULE, Node, {_, _} = Target}) ->
    Node =:= partisan:node() orelse throw(badarg),
    decode_target(Target);

decode_target({encoded_pid, _} = Target) ->
    Target;

decode_target({encoded_ref, _} = Target) ->
    Target;


decode_target({encoded_name, _} = Target) ->
    Target.


%% @private
get_node(Bin) ->
    Nodestring = get_nodestring(Bin),
    %% Using binary_to_existing_atom/2 is ideal but assumes all nodes know all
    %% other nodes, definitively not the case with partial views, so we need to
    %% take the risk.
    binary_to_atom(Nodestring, utf8).


%% @private
get_nodestring(Bin) ->
    case binary:split(Bin, <<?SEP>>) of
        [Node | _]  ->
            Node;
        _ ->
            error(badarg)
    end.


%% @private
do_is_local(Bin) ->
    do_is_local(Bin, partisan:nodestring(), undefined).


%% @private
do_is_local(Bin, Nodestring) ->
    do_is_local(Bin, Nodestring, undefined).


%% @private
do_is_local(Bin, Nodestring, undefined) ->
    Size = byte_size(Nodestring),
    case Bin of
        <<Nodestring:Size/binary, ?SEP, _/binary>> ->
            true;
        _ ->
            false
    end;

do_is_local(Bin, Nodestring, TargetAsBin) ->
    case Bin of
        <<
            Nodestring:(byte_size(Nodestring))/binary, ?SEP,
            TargetAsBin:(byte_size(TargetAsBin))/binary
        >> ->
            true;
        <<
            Nodestring:(byte_size(Nodestring))/binary, ?SEP,
            TargetAsBin:(byte_size(TargetAsBin))/binary, ?SEP,
            _/binary
        >> ->
            true;
        _ ->
            false
    end.


%% @private
-spec untag(string()) -> binary().

untag(String0) ->
    String1 = string:replace(String0, "<", ""),
    %% eqwalizer:ignore String1
    iolist_to_binary(string:replace(String1, ">", "")).


%% @private
-spec tag(binary() | list()) -> list().

tag(Bin) when is_binary(Bin) ->
    tag(binary_to_list(Bin));

tag(String) when is_list(String) ->
    lists:append(["<", String, ">"]).


%% @private
maybe_pad(Bin) when byte_size(Bin) < 65 ->
    %% Erlang Heap binaries are small binaries, up to 64 bytes, and are stored
    %% directly on the process heap. They are copied when the process is
    %% garbage-collected and when they are sent as a message.
    %% Erlang Refc binaries (short for reference-counted binaries) - they
    %% consist of a ProcBin (stored on the process heap) and the binary object
    %% itself stored on the VM outside of all processes.
    %% TODO we need to support additional components so that apps can add
    %% metadata
    %% So, use ?PADDING_START as separator between URI and padding and pad with zeros
    %% string:pad(<<Bin/binary, PADDING_START>>, 65, trailing, $0)
    %% when decoding, first split by PADDING_START, dropping padding,
    %% then split by ?SEP
    case partisan_config:get(remote_ref_binary_padding, false) of
        true ->
            iolist_to_binary(
                string:pad(<<Bin/binary, ?SEP>>, 65, trailing, ?PADDING_START)
            );
        false ->
            Bin
    end;

maybe_pad(Bin) ->
    Bin.


%% @private
to_local_pid_list("<0." ++ _ = Value) ->
    Value;

to_local_pid_list(Value) ->
    case string:split(Value, ".", all) of
        [_, B, C] ->
            "<0." ++ B ++ "." ++ C;
        _ ->
            error(badarg)
    end.


%% @private
to_local_pid(Value) ->
    list_to_pid(to_local_pid_list(Value)).


%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
maybe_register_pid(Pid, Node, Mode) ->
    case partisan_config:get(register_pid_for_encoding, false) of
        true when Mode == local ->
            register_local_pid(Pid);
        true when Mode == remote ->
            register_remote_pid(Pid, Node);
        false ->
            false
    end.


%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec register_local_pid(pid()) -> true.

register_local_pid(Pid) ->
    %% This is super dangerous.
    %% This code was in partisan_util in previous versions
    Unique = erlang:unique_integer([monotonic, positive]),

    Name = case process_info(Pid, registered_name) of
        {registered_name, Name0} ->
            ?LOG_DEBUG("unregistering pid: ~p name: ~p", [Pid, Name0]),
            %% TODO: Race condition on unregister/register.
            unregister(Name0),
            Name0;
        [] ->
            list_to_atom("partisan_registered_name_" ++ integer_to_list(Unique))
    end,

    ?LOG_DEBUG("registering pid: ~p name: ~p", [Pid, Name]),

    register(Name, Pid).


%% -----------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%% -----------------------------------------------------------------------------
register_remote_pid(Pid, Node) ->
    %% This is even more super dangerous.
    %% This code was in partisan_util in previous versions
    Unique = erlang:unique_integer([monotonic, positive]),
    NewName = "partisan_registered_name_" ++ integer_to_list(Unique),

    Register = fun() ->
        Name = case process_info(Pid, registered_name) of
            {registered_name, Name0} ->
                ?LOG_DEBUG("unregistering pid: ~p name: ~p", [Pid, Name0]),
                %% TODO: Race condition on unregister/register.
                unregister(Name0),
                Name0;
            [] ->
                list_to_atom(NewName)
        end,
        ?LOG_DEBUG(
            "registering pid: ~p name: ~p at node: ~p",
            [Pid, NewName, Node]
        ),
        erlang:register(Name, Pid)
    end,

    %% TODO: Race here unless we wait.
    _ = partisan_rpc:call(Node, erlang, spawn, [Register], 5000),
    true.


%% =============================================================================
%% EUNIT TESTS
%% =============================================================================



-ifdef(TEST).

-include_lib("eunit/include/eunit.hrl").

init_test() ->
    %% A hack to resolve node name
    partisan_config:init(),
    partisan_config:set(name, 'test@127.0.0.1'),
    partisan_config:set(nodestring, <<"test@127.0.0.1">>),
    ok.


local_pid_test_improper_list_test() ->
    partisan_config:set(remote_ref_format, improper_list),
    test_local_pid().

local_pid_test_tuple_test() ->
    partisan_config:set(remote_ref_format, tuple),
    test_local_pid().

local_pid_test_uri_test() ->
    partisan_config:set(remote_ref_format, uri),
    test_local_pid().


local_name_test_improper_list_test() ->
    partisan_config:set(remote_ref_format, improper_list),
    test_local_name().

local_name_test_tuple_test() ->
    partisan_config:set(remote_ref_format, tuple),
    test_local_name().

local_name_test_uri_test() ->
    partisan_config:set(remote_ref_format, uri),
    test_local_name().


local_ref_test_improper_list_test() ->
    partisan_config:set(remote_ref_format, improper_list),
    test_local_ref().

local_ref_test_tuple_test() ->
    partisan_config:set(remote_ref_format, tuple),
    test_local_ref().

local_ref_test_uri_test() ->
    partisan_config:set(remote_ref_format, uri),
    test_local_ref().


test_local_pid() ->
    UriRef = from_term(self()),
    ?assert(is_pid(UriRef)),
    ?assert(not is_reference(UriRef)),
    ?assert(not is_name(UriRef)),
    ?assert(is_type(UriRef)),
    ?assert(is_local(UriRef)),
    ?assert(is_local(UriRef), self()),
    ?assertEqual(partisan:node(), ?MODULE:node(UriRef)),
    ?assertEqual(partisan:nodestring(), ?MODULE:nodestring(UriRef)),
    ?assertEqual(
        self(),
        to_term(UriRef)
    ),
    ?assertEqual(
        {encoded_pid, pid_to_list(self())},
        target(UriRef)
    ).


test_local_name() ->
    Ref = from_term(foo),
    ?assert(not is_pid(Ref)),
    ?assert(not is_reference(Ref)),
    ?assert(is_name(Ref)),
    ?assert(is_type(Ref)),
    ?assert(is_local(Ref)),
    ?assert(is_local_name(Ref, foo), Ref),
    ?assertEqual(partisan:node(), ?MODULE:node(Ref)),
    ?assertEqual(partisan:nodestring(), ?MODULE:nodestring(Ref)),
    ?assertEqual(
        foo,
        to_term(Ref)
    ),
    ?assertEqual(
        {encoded_name, "foo"},
        target(Ref)
    ).


test_local_ref() ->
    ERef = make_ref(),
    Ref = from_term(ERef),
    ?assert(not is_pid(Ref)),
    ?assert(is_reference(Ref)),
    ?assert(not is_name(Ref)),
    ?assert(is_type(Ref)),
    ?assert(is_local(Ref)),
    ?assert(is_local(Ref), ERef),
    ?assertEqual(partisan:node(), ?MODULE:node(Ref)),
    ?assertEqual(partisan:nodestring(), ?MODULE:nodestring(Ref)),
    ?assertEqual(
        ERef,
        to_term(Ref)
    ).


remote_name_test() ->
    Ref = from_term(foo, 'othernode@127.0.0.1'),
    ?assertEqual(
        true,
        is_type(Ref)
    ),
    ?assertEqual(
        true,
        is_name(Ref)
    ).


non_local_target_test() ->
    Ref =  <<"partisan:pid:othernode@127.0.0.1:0.800.0">>,
    ?assertError(
        badarg,
        to_term(Ref)
    ),
    ?assertEqual(
        {encoded_pid, "<0.800.0>"},
        target(Ref)
    ).


identical_test() ->
    true = erlang:register(foo, self()),

    A = from_term(self()),
    B = from_term(foo),

    ?assert(
        is_identical(A, B)
    ),
    ?assert(
        is_identical(A, A)
    ),
    ?assert(
        is_identical(B, B)
    ),
    ?assertEqual(
        false,
        is_identical(A, from_term(bar))
    ).


is_name_test() ->
    ?assertEqual(
        true,
        is_name(from_term(foo), foo)
    ),
    ?assertEqual(
        false,
        is_name(from_term(foo), bar)
    ),
    ?assertEqual(
        false,
        is_name(from_term(self()), bar)
    ).


-endif.