%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2021-2022. 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(gen_udp_socket).
-behaviour(gen_statem).

-compile({no_auto_import, [monitor/1]}).

%% gen_udp
-export([
	 open/2,
	 close/1,
	 connect/3,
	 controlling_process/2,
	 recv/2,recv/3,
         send/2, send/3, send/4, send/5
	]).

%% inet
-export([
         monitor/1, cancel_monitor/1,
         setopts/2, getopts/2,
         sockname/1, peername/1,
         socknames/1,
         getstat/2
        ]).

%% Utility
-export([info/1, which_sockets/0, socket_to_list/1]).

-ifdef(undefined).
-export([fdopen/2]).
-endif.

%% gen_statem callbacks
-export([init/1, callback_mode/0, terminate/3]).
-export([handle_event/4]).

-include("inet_int.hrl").

%% -define(DBG(T), erlang:display({{self(), ?MODULE, ?LINE, ?FUNCTION_NAME}, T})).
-define(RECBUF, 65536).

%% -define(ESOCK_VERBOSE_BADARG, true).
-ifdef(ESOCK_VERBOSE_BADARG).
-define(BADARG(Args), erlang:error(badarg, Args)).
-else.
-define(BADARG(_), exit(badarg)).
-endif.


%% -------------------------------------------------------------------------

%% Construct a "socket" as in this module's API
-define(MODULE_socket(Server, Socket),
        {'$inet', ?MODULE, {Server, Socket}}).

-define(badarg_exit(Error),
        case begin Error end of
            {error, badarg} -> exit(badarg);
            OTHER__ -> OTHER__
        end).

-define(badarg_einval_exit(Error),
        case begin Error end of
            {error, badarg} -> exit(badarg);
            {error, einval} -> exit(badarg);
            OTHER__ -> OTHER__
        end).

-define(socket_abort(Socket, SelectRef, Reason),
        {'$socket', (Socket), abort, {(SelectRef), (Reason)}}).
-define(socket_select(Socket, SelectRef),
        {'$socket', (Socket), select, (SelectRef)}).
-define(socket_counter_wrap(Socket, Counter),
        {'$socket', (Socket), counter_wrap, (Counter)}).
-define(select_info(SelectRef),
        {select_info, _, (SelectRef)}).


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

%% -- close ----------------------------------------------------------------

close(?MODULE_socket(Server, _Socket)) ->
    ?badarg_exit(close_server(Server)).


%% Helpers -------

close_server(Server) ->
    Result = call(Server, close),
    stop_server(Server),
    Result.


%% -- connect ----------------------------------------------------------------

connect(?MODULE_socket(_Server, Socket), Address, Port) ->
    Dest = dest2sockaddr({Address, Port}),
    case os:type() of
        {unix,linux} ->
            case socket:peername(Socket) of
                {error, enotconn} ->
                    socket:connect(Socket, Dest);
                {error, closed} = Error ->
                    Error;
                _ -> % Matches {ok, _} and unknown errors
                    _ = socket:connect(Socket, #{family => unspec}),
                    socket:connect(Socket, Dest)
            end;
        _ ->
            socket:connect(Socket, Dest)
    end.


%% -- open -----------------------------------------------------------------

open(Service, Opts) ->
    open_lookup(Service, Opts).


%% Helpers -------

open_lookup(Service, Opts0) ->
    %% ?DBG(['open lookup', {service, Service}, {opts, Opts0}]),
    {EinvalOpts, Opts_1} = setopts_split(einval, Opts0),
    EinvalOpts =:= [] orelse exit(badarg),
    {Mod, Opts_2} = inet:udp_module(Opts_1),
    Domain = domain(Mod),
    {StartOpts, Opts_3} = setopts_split(start, Opts_2),
    ErrRef = make_ref(),
    try
	begin
	    %% IPs    = val(ErrRef, Mod:getaddrs(Address, Domain)),
	    Port   = val(ErrRef, Mod:getserv(Service)),
	    %% Opts_4 = [{port, Port}, {buffer, ?RECBUF} | Opts_3],
	    Opts_4 = [{port, Port} | Opts_3],
	    #udp_opts{fd     = Fd,
		      ifaddr = BindIP,
		      port   = BindPort,
		      opts   = OpenOpts} =
		val(ErrRef, inet:udp_options(Opts_4, Mod)),
            %% ?DBG([{fd, Fd}, {bind_ip, BindIP}, {bind_port, BindPort},
            %%       {opts, OpenOpts}]),
            BindAddr = bind_addr(Domain, BindIP, BindPort, Fd),
            ExtraOpts = extra_opts(Fd),
            do_open(Mod, BindAddr, Domain, OpenOpts, StartOpts, ExtraOpts)
	end
    catch
        throw : {ErrRef, Reason} ->
            ?badarg_exit({error, Reason})
    end.

do_open(Mod, BindAddr, Domain, OpenOpts, Opts, ExtraOpts) ->

    %% ?DBG([{mod, Mod}, {bind_addr, BindAddr}, {domain, Domain},
    %%       {open_opts, OpenOpts}, {opts, Opts}, {extra_opts, ExtraOpts}]),

    %%
    %% The {netns, File} option is passed in Fd by inet:connect_options/2,
    %% and then over to ExtraOpts.
    %% The {debug, Bool} option is passed in Opts since it is
    %% subversively classified as both start and socket option.
    %%

    {SocketOpts, StartOpts} = setopts_split(socket, Opts),
    %% ?DBG(['try start server', {socket, SocketOpts}, {start, StartOpts}]),
    case start_server(Mod, Domain, start_opts(StartOpts), ExtraOpts) of
        {ok, Server} ->
            {SetOpts0, _} = setopts_split(#{socket       => [],
                                            server_read  => [],
                                            server_write => []},
                                          OpenOpts),
            SetOpts =
                default_active_true(
                  [{start_opts, StartOpts}] ++ SocketOpts ++ SetOpts0),

            ErrRef = make_ref(),
            try
                %% ?DBG(['try setopts',
                %%       {socket_opts, SocketOpts}, {set_opts, SetOpts}]),
                ok(ErrRef, call(Server, {setopts, SocketOpts ++ SetOpts})),

                %% ?DBG(['maybe try bind', {bind_addr, BindAddr}]),
                ok(ErrRef, call_bind(Server,
                                     default_any(Domain, ExtraOpts, BindAddr))),

                %% ?DBG(['try get-socket']),
                Socket = val(ErrRef, call(Server, get_socket)),

                {ok, ?MODULE_socket(Server, Socket)}
            catch
                throw : {ErrRef, Reason} ->
                    close_server(Server),
                    ?badarg_exit({error, Reason})
            end;
        {error, _} = Error ->
            ?badarg_exit(Error)
    end.

    
extra_opts(Fd) when is_integer(Fd) ->
    if
        Fd < 0 ->
            #{};
        true ->
            #{fd => Fd}
    end;
extra_opts(OpenOpts) when is_list(OpenOpts) ->
    %% This is an **ugly** hack.
    %% inet:{connect,listen,udp,sctp}_options/2 has the bad taste
    %% to use this for [{netns,BinNS}] if that option is used...
   maps:from_list(OpenOpts).


default_any(_Domain, #{fd := _}, _BindAddr) ->
    undefined;
default_any(Domain, _ExtraOpts, undefined = Undefined) ->
    if
        ((Domain =:= inet) orelse (Domain =:= inet6)) ->
            #{family => Domain,
              addr   => any,
              port   => 0};
        true ->
            Undefined
    end;
default_any(_Domain, _ExtraOpts, BindAddr) ->
    BindAddr.

bind_addr(_Domain, BindIP, BindPort, Fd)
  when ((BindIP =:= undefined) andalso (BindPort =:= 0)) orelse
       (is_integer(Fd) andalso (0 =< Fd)) ->
    %% Do not bind!
    undefined;
bind_addr(local = Domain, BindIP, _BindPort, _Fd) ->
    case BindIP of
	any ->
	    undefined;
	{local, Path} ->
	    #{family => Domain,
	      path   => Path}
    end;
bind_addr(Domain, BindIP, BindPort, _Fd)
  when (Domain =:= inet) orelse (Domain =:= inet6) ->
    Addr = if (BindIP =:= undefined) -> any; true -> BindIP end,
    #{family => Domain,
      addr   => Addr,
      port   => BindPort}.

call_bind(_Server, undefined) ->
    ok;
call_bind(Server, BindAddr) ->
    call(Server, {bind, BindAddr}).


default_active_true(Opts) ->
    case lists:keyfind(active, 1, Opts) of
        {active,_} ->
            Opts;
        _ ->
            [{active, true} | Opts]
    end.


%% -------------------------------------------------------------------------

%% Connected send

send(?MODULE_socket(_Server, Socket), Data) ->
    socket_send(Socket, Data);
send(_Socket, _Data) ->
    ?BADARG([_Socket, _Data]).


-compile({inline, [make_iov/1]}).
make_iov(Bin) when is_binary(Bin) -> [Bin];
make_iov(IOV) when is_list(IOV)   -> IOV.
    
-compile({inline, [dest2sockaddr/1]}).
dest2sockaddr({Addr, Port})
  when is_tuple(Addr) andalso (tuple_size(Addr) =:= 4) andalso
       is_integer(Port) ->
    #{family => inet,
      port   => Port,
      addr   => Addr};
dest2sockaddr({Addr, Port})
  when is_tuple(Addr) andalso (tuple_size(Addr) =:= 8) andalso
       is_integer(Port) ->
    #{family => inet6,
      port   => Port,
      addr   => Addr};
dest2sockaddr({local = Fam, Path})
  when is_binary(Path) orelse is_list(Path) ->
    #{family => Fam,
      path   => Path};
dest2sockaddr({Fam, {Addr, Port}})
  when is_atom(Fam) andalso
       ((Fam =:= inet) orelse (Fam =:= inet6)) andalso
       is_integer(Port) ->
    #{family => Fam,
      port   => Port,
      addr   => Addr};
dest2sockaddr(_Arg) ->
    ?BADARG([_Arg]).

-compile({inline, [make_cmsghdrs/1]}).
make_cmsghdrs(AncData) when is_list(AncData) ->
    [make_cmsghdr(AD) || AD <- AncData];
make_cmsghdrs(_Arg) ->
    ?BADARG([_Arg]).

make_cmsghdr({tos = Type, Byte}) ->
    #{level => ip, type => Type, data => Byte};
make_cmsghdr({tclass = Type, Byte}) ->
    #{level => ipv6, type => Type, data => Byte};
make_cmsghdr({ttl = Type, Byte}) ->
    #{level => ip, type => Type, data => Byte};
make_cmsghdr(_Arg) ->
    ?BADARG([_Arg]).
    

send(Socket, Destination, Data) ->
    do_sendto(Socket, dest2sockaddr(Destination), Data).

send(Socket, {_, _} = Destination, PortZero, Data)
  when (PortZero =:= 0) ->
    send(Socket, Destination, Data);
send(_Socket, {_,_} = _Destination, PortZero, _Data)
  when is_integer(PortZero) ->
    {error, einval};
send(Socket, {_,_} = Destination, AncData, Data) ->
    do_sendmsg(Socket,
	       dest2sockaddr(Destination),
	       make_iov(Data),
	       make_cmsghdrs(AncData));
send(Socket, Addr, Port, Data)
  when is_tuple(Addr) andalso
       ((tuple_size(Addr) =:= 4) orelse (tuple_size(Addr) =:= 8)) andalso
       is_integer(Port) ->
    send(Socket, {Addr, Port}, Data);
send(?MODULE_socket(_, ESock) = Socket,
     Host, Service, Data) when is_list(Host) orelse is_atom(Host) ->
    case socket:getopt(ESock, {otp, domain}) of
	{ok, Domain} ->
	    case inet:getaddr(Host, Domain) of
		{ok, Addr} ->
                    {ok, #{mod := Mod}} = socket:getopt(ESock, {otp, meta}),
		    case Mod:getserv(Service) of
			{ok, Port} ->
			    send(Socket, {Addr, Port}, Data);
			{error, einval} ->
			    exit(badarg);
			{error, _} = ERROR ->
			    ERROR
		    end;
		{error, einval} ->
		    exit(badarg);
		{error, _} = ERROR ->
		    ERROR
	    end;
	ERROR ->
	    ERROR
    end;
send(_Socket, _Arg1, _Arg2, _Arg3) ->
    ?BADARG([_Socket, _Arg1, _Arg2, _Arg3]).


send(Socket, Addr, Port, AncData, Data)
  when is_tuple(Addr) andalso
       ((tuple_size(Addr) =:= 4) orelse (tuple_size(Addr) =:= 8)) andalso
       is_integer(Port) ->
    send(Socket, {Addr, Port}, AncData, Data);
send(?MODULE_socket(_, ESock) = Socket,
     Host, Service, AncData, Data) when is_list(Host) orelse is_atom(Host) ->
    case socket:getopt(ESock, {otp, domain}) of
	{ok, Domain} ->
	    case inet:getaddr(Host, Domain) of
		{ok, Addr} ->
                    {ok, #{mod := Mod}} = socket:getopt(ESock, {otp, meta}),
		    case Mod:getserv(Service) of
			{ok, Port} ->
			    send(Socket, {Addr, Port}, AncData, Data);
			{error, einval} ->
			    exit(badarg);
			{error, _} = ERROR ->
			    ERROR
		    end;
		{error, einval} ->
		    exit(badarg);
		{error, _} = ERROR ->
		    ERROR
	    end;
	ERROR ->
	    ERROR
    end;
send(_Socket, _Arg1, _Arg2, _Arg3, _Arg4) ->
    ?BADARG([_Socket, _Arg1, _Arg2, _Arg3, _Arg4]).



do_sendto(?MODULE_socket(_Server, Socket), Dest, Data) ->
    case socket_sendto(Socket, Dest, Data) of
        {error, {invalid, _}} ->
            exit(badarg);
        Any ->
            Any
    end.

do_sendmsg(?MODULE_socket(_Server, Socket), SockAddr, IOV, Ctrl)
  when is_list(IOV) andalso is_list(Ctrl) ->
    MsgHdr = #{addr => SockAddr,
	       iov  => IOV,
	       ctrl => Ctrl},
    case socket_sendmsg(Socket, MsgHdr) of
        {error, {invalid, _}} ->
            exit(badarg);
        Any ->
            Any
    end.

    
%% -------------------------------------------------------------------------

recv(Socket, Length) ->
    recv(Socket, Length, infinity).

%% This is a bit weird. If two calls to recv is made with short intervals
%% from different processes, with say timeout 10s and then 5s. The second
%% call will be postponed until the first has been processed, which can
%% take up to 10s. If the first times out (after 10s) the second is 
%% supposed to have been timed out already by the time it gets processed.
%% Can we postpone? And can we make conditional postpone?

recv(?MODULE_socket(Server, _Socket), Length, Timeout) ->
    ?badarg_exit(call(Server, {recv, Length, Timeout})).


%% -------------------------------------------------------------------------

controlling_process(?MODULE_socket(Server, _Socket) = S, NewOwner)
  when is_pid(NewOwner) ->
    case call(Server, {controlling_process, NewOwner}) of
        ok -> ok;
        transfer -> controlling_process(S, NewOwner, Server);
        {error, _} = Error -> Error
    end.

%%
%% Helpers -------
%%
%% Transfer all queued socket messages to new owner
controlling_process(S, NewOwner, Server) ->
    receive
        {udp, S, _Data} = Msg ->
            controlling_process(S, NewOwner, Server, Msg);
        {udp_closed, S} = Msg ->
            controlling_process(S, NewOwner, Server, Msg);
        {S, {data, _Data}} = Msg ->
            controlling_process(S, NewOwner, Server, Msg)
    after 0 ->
            call(Server, controlling_process)
    end.

%% Loop
controlling_process(S, NewOwner, Server, Msg) ->
    NewOwner ! Msg,
    controlling_process(S, NewOwner, Server).


%% -------------------------------------------------------------------------
%% Module inet backends
%% -------------------------------------------------------------------------

monitor(?MODULE_socket(_Server, ESock) = Socket) ->
    %% The socket that is part of the down message:
    case socket_registry:monitor(ESock, #{msocket => Socket}) of
	{error, Reason} ->
	    erlang:error({invalid, Reason});
	MRef when is_reference(MRef) ->
	    MRef
    end;
monitor(Socket) ->
    erlang:error(badarg, [Socket]).

cancel_monitor(MRef) when is_reference(MRef) ->
    socket:cancel_monitor(MRef);
cancel_monitor(MRef) ->
    erlang:error(badarg, [MRef]).


%% -------------------------------------------------------------------------

setopts(?MODULE_socket(Server, _Socket), Opts) when is_list(Opts) ->
    call(Server, {setopts, Opts}).


%% -------------------------------------------------------------------------

getopts(?MODULE_socket(Server, _Socket), Opts) when is_list(Opts) ->
    call(Server, {getopts, Opts}).


%% -------------------------------------------------------------------------

sockname(?MODULE_socket(_Server, Socket)) ->
    case socket:sockname(Socket) of
        {ok, SockAddr} -> {ok, address(SockAddr)};
        {error, _} = Error -> Error
    end.


%% -------------------------------------------------------------------------

socknames(Socket) ->
    case sockname(Socket) of
        {ok, Addr} -> {ok, [Addr]};
        {error, _} = Error -> Error
    end.


%% -------------------------------------------------------------------------

peername(?MODULE_socket(_Server, Socket)) ->
    case socket:peername(Socket) of
        {ok, SockAddr} -> {ok, address(SockAddr)};
        {error, _} = Error -> Error
    end.

%% -------------------------------------------------------------------------

getstat(?MODULE_socket(Server, _Socket), What) when is_list(What) ->
    call(Server, {getstat, What}).


%% -------------------------------------------------------------------------

info(?MODULE_socket(Server, _Socket)) ->
    case call(Server, info) of
        {error, closed} ->
            #{rstates => [closed], wstates => [closed]};
        Other ->
            Other
    end.


%% -------------------------------------------------------------------------

socket_to_list(?MODULE_socket(_Server, Socket)) ->
    "#Socket" ++ Id = socket:to_list(Socket),
    "#InetSocket" ++ Id;
socket_to_list(Socket) ->
    erlang:error(badarg, [Socket]).


which_sockets() ->
    which_sockets(socket:which_sockets(udp)).

which_sockets(Socks) ->
    which_sockets(Socks, []).

which_sockets([], Acc) ->
    Acc;
which_sockets([Sock|Socks], Acc) ->
    case socket:getopt(Sock, {otp, meta}) of
	{ok, undefined} ->
	    which_sockets(Socks, Acc);
	{ok, _Meta} ->
	    %% One of ours - try to recreate the compat socket.
	    %% Currently we don't have the 'owner' in meta,
	    %% so we need to look it up...
	    #{owner := Owner} = socket:info(Sock),
	    MSock = ?MODULE_socket(Owner, Sock),
	    which_sockets(Socks, [MSock|Acc]);
	_ ->
	    which_sockets(Socks, Acc)
    end.


%%% ========================================================================
%%% Socket glue code
%%%

-compile({inline, [socket_recvfrom/2]}).
socket_recvfrom(Socket, Length) ->
    socket:recvfrom(Socket, Length, nowait).

-compile({inline, [socket_recvmsg/2]}).
socket_recvmsg(Socket, Length) ->
    socket:recvmsg(Socket, Length, 0, nowait).


-compile({inline, [socket_send/2]}).
socket_send(Socket, Data) ->
    socket:send(Socket, Data).

-compile({inline, [socket_sendto/3]}).
socket_sendto(Socket, Dest, Data) ->
    %% <OBSERVE>
    %%
    %% Argument swap!
    %%
    %% </OBSERVE>
    %% socket:setopt(Socket, otp, debug, true),
    Res = socket:sendto(Socket, Data, Dest),
    %% socket:setopt(Socket, otp, debug, false),
    Res.

-compile({inline, [socket_sendmsg/2]}).
socket_sendmsg(Socket, MsgHdr) ->
    socket:sendmsg(Socket, MsgHdr).


-compile({inline, [socket_close/1]}).
socket_close(Socket) ->
    %% XXX Should we set the meta option to closed here,
    %% for the send operation to detect without calling
    %% the NIF???
    case socket:close(Socket) of
        ok -> ok;
        {error, closed} -> ok
    end.


-compile({inline, [socket_cancel/2]}).
socket_cancel(Socket, SelectInfo) ->
    case socket:cancel(Socket, SelectInfo) of
        ok              -> ok;
        {error, closed} -> ok
    end.



%%% ========================================================================
%%% API Helpers
%%%

%% Deep return helpers

ok(_ErrRef, ok) -> ok;
ok(ErrRef, {error, Reason}) -> throw({ErrRef, Reason}).

val(_ErrRef, {ok, Val}) -> Val;
val(ErrRef, {error, Reason}) -> throw({ErrRef, Reason}).


%% -------------------------------------------------------------------------

-compile({inline, [domain/1]}).
domain(Mod) ->
    case Mod of
        inet_udp  -> inet;
        inet6_udp -> inet6;
        local_udp -> local
    end.


%% -------------------------------------------------------------------------

address(SockAddr) ->
    case SockAddr of
        #{family := Family, addr := IP, port := Port}
	when (Family =:= inet) orelse (Family =:= inet6) ->
            {IP, Port};
        #{family := local, path := Path} ->
            {local, Path}
    end.


%% -------------------------------------------------------------------------

setopts_split(FilterTags, Opts) ->
    setopts_split(FilterTags, Opts, [], []).

%%
setopts_split(_FilterTags, [], True, False) ->
    %% ?DBG(['done', {true, True}, {false, False}]),
    {reverse(True), reverse(False)};
setopts_split(FilterTags, [Opt | Opts], True, False) ->
    %% ?DBG([{ftags, FilterTags}, {opt, Opt}]),
    Opt_1 = conv_setopt(Opt),
    case member(FilterTags, setopt_categories(Opt_1)) of
        true ->
            setopts_split(FilterTags, Opts, [Opt_1 | True], False);
        false ->
            setopts_split(FilterTags, Opts, True, [Opt_1 | False])
    end.


%% Set operation on atom sets that are atoms or maps with atom tags.
%% Returns true if sets have at least one common member, false otherwise.
%% X is atom() or map(), Y is map().
member(X, Y) when is_atom(X) andalso is_map(Y) ->
    case Y of
        #{X := _} -> true;
        #{} -> false
    end;
member(X, Y) when is_map(X) andalso is_map(Y) ->
    maps:fold(
      fun (_, _, true) -> true;
          (Key, _, false) -> maps:is_key(Key, Y)
      end, false, X).


conv_setopt(binary) -> {mode, binary};
conv_setopt(list)   -> {mode, list};
conv_setopt(inet)   -> {udp_module, inet_udp};
conv_setopt(inet6)  -> {udp_module, inet6_udp};
conv_setopt(local)  -> {udp_module, local_udp};
conv_setopt(Other)  -> Other.

%% Socket options

socket_setopt(Socket, {raw, Level, Key, Value}) ->
    socket:setopt_native(Socket, {Level,Key}, Value);
socket_setopt(Socket, {raw, {Level, Key, Value}}) ->
    socket:setopt_native(Socket, {Level,Key}, Value);
socket_setopt(Socket, {Tag, Value}) ->
    %% ?DBG({Tag, Value}),
    case socket_opt() of
        #{Tag := {Domain, _} = Opt} when is_atom(Domain) ->
            %% ?DBG(Opt),
            socket_setopt_opt(Socket, Opt, Tag, Value);

        #{Tag := Opts} when is_list(Opts) ->
            %% ?DBG(Opts),
            socket_setopt_opts(Opts, Socket, Tag, Value);

        #{} ->
            {error, einval}
    end.

socket_setopt_opt(Socket, Opt, Tag, Value) ->
    %% _ = socket:setopt(Socket, otp, debug, true),
    Val = socket_setopt_value(Tag, Value, Opt),
    %% ?DBG(Val),
    Res = socket:setopt(Socket, Opt, Val),
    %% ?DBG(Res),
    %% _ = socket:setopt(Socket, otp, debug, false),
    Res.
    

socket_setopt_opts([], _Socket, _Tag, _Value) ->
    ok;

%% Its possible for *one* option to be mapped to several 'socket' options.
%% For instance, when *setting* the the option recbuf, we actually set two
%% 'socket' options: {otp, rcvbuf} and {socket, rcvbuf}.
socket_setopt_opts([{_Level, _OptKey} = Opt|Opts], Socket, Tag, Value) ->
    %% _ = socket:setopt(Socket, otp, debug, true),
    Res = socket:setopt(Socket, Opt, socket_setopt_value(Tag, Value, Opt)),
    %% _ = socket:setopt(Socket, otp, debug, false),
    case Res of
        ok ->
            socket_setopt_opts(Opts, Socket, Tag, Value);
        {error, _Reason} ->
            {error, einval}
    end;

%% We need to lookup the domain of the socket,
%% so we can select which one to use.
socket_setopt_opts(Opts, Socket, Tag, Value) ->
    case socket:getopt(Socket, otp, domain) of
        {ok, Domain} ->
            case lists:keysearch(Domain, 1, Opts) of
                {value, {Domain, Level, OptKey}} ->
                    Opt = {Level, OptKey},
                    %% _ = socket:setopt(Socket, otp, debug, true),
                    Res = socket:setopt(Socket, Opt,
                                        socket_setopt_value(Tag, Value, Opt)),
                    %% _ = socket:setopt(Socket, otp, debug, false),
                    Res;
                false ->
                    {error, einval}
            end;
        {error, _} ->
            {error, einval}
    end.



%% This happens when we set the recbuf options.
%% As a side effect, we also set our internal buffer,
%% but with a limiter.
socket_setopt_value(recbuf, Value, {otp, rcvbuf}) 
  when (Value > ?RECBUF) ->
    ?RECBUF;
socket_setopt_value(_Tag, Value, _) ->
    Value.

socket_getopt(Socket, {raw, Level, Key, ValueSpec}) ->
    socket:getopt_native(Socket, {Level,Key}, ValueSpec);
socket_getopt(Socket, {raw, {Level, Key, ValueSpec}}) ->
    socket:getopt_native(Socket, {Level,Key}, ValueSpec);
socket_getopt(Socket, Tag) when is_atom(Tag) ->
    case socket_opt() of
        #{Tag := {Domain, _} = Opt} when is_atom(Domain) ->
            %% ?DBG(Opt),
            socket_getopt_opt(Socket, Opt, Tag);

        #{Tag := Opts} when is_list(Opts) ->
            %% ?DBG(Opts),
            socket_getopt_opts(Opts, Socket, Tag);

        #{} = __X__ ->
            %% ?DBG({'socket_getopt - no match - 2', Tag, __X__}),
            {error, einval}
    end.


socket_getopt_opt(Socket, Opt, Tag) ->
    %% _ = socket:setopt(Socket, otp, debug, true),
    Res = socket:getopt(Socket, Opt),
    %% ?DBG([{'socket_getopt - result', Res}]),
    %% _ = socket:setopt(Socket, otp, debug, false),
    socket_getopt_value(Tag, Res).

%% socket_getopt_opts([], _Socket, _Tag) ->
%%     {error, einval};

%% Its possible for *one* option to be mapped to several 'socket' options.
%% But, its *always* the first element in th elist that is the "real"
%% option. This is the one used when *reading*. The other elements in the list
%% is basically side effect options, which is not used when reading.
socket_getopt_opts([{_Domain, _} = Opt|_], Socket, Tag) ->
    socket_getopt_opt(Socket, Opt, Tag);

socket_getopt_opts(Opts, Socket, Tag) ->
    case socket:getopt(Socket, otp, domain) of
        {ok, Domain} ->
            %% ?DBG([{'domain', Domain}]),
            case lists:keysearch(Domain, 1, Opts) of
                {value, {Domain, Level, OptKey}} ->
                    %% ?DBG(['ok domain', {level, Level}, {optkey, OptKey}]),
                    Opt  = {Level, OptKey},
                    %% _ = socket:setopt(Socket, otp, debug, true),
                    Res = socket:getopt(Socket, Opt),
                    %% _ = socket:setopt(Socket, otp, debug, false),
                    %% ?DBG([{'result', Res}]),
                    socket_getopt_value(Tag, Res);
                false ->
                    %% ?DBG({'invalid domain', Tag, Domain, Opts}),
                    {error, einval}
            end;
        {error, _DReason} ->
            %% ?DBG([{'unknown domain', _DReason}]),
            {error, einval}
    end.

    
    
%% socket_getopt_value(pktoptions, {ok, PktOpts0}) when is_list(PktOpts0) ->
%%     PktOpts = [{Type, Value} || #{type := Type, value := Value} <- PktOpts0],
%%     {ok, PktOpts};
socket_getopt_value(_Tag, {ok, _Value} = Ok) -> Ok;
socket_getopt_value(_Tag, {error, _} = Error) -> Error.

start_opts([{sys_debug, D} | Opts]) ->
    [{debug, D} | start_opts(Opts)];
start_opts([Opt | Opts]) ->
    [Opt | start_opts(Opts)];
start_opts([]) -> [].


%% Categories: socket, ignore, start, server_read, server_write, einval
%% returns a maps set

setopt_categories(Opt) ->
    case Opt of
        {raw, _, _, _}   -> #{socket => []};
        {raw, {_, _, _}} -> #{socket => []};
        {Tag, _}         -> opt_categories(Tag);
        _                -> ignore
    end.

getopt_categories(Opt) ->
    case Opt of
        {raw, _, _, _}   -> #{socket => []};
        {raw, {_, _, _}} -> #{socket => []};
        _                -> opt_categories(Opt)
    end.

%% setopt and getopt category
opt_categories(Tag) when is_atom(Tag) ->
    case Tag of
        sys_debug -> #{start => []};
        debug     -> #{socket => [], start => []};

        %% Some options may trigger us to choose recvmsg (instead of recvfrom)
        %% Or trigger us to choose recvfrom *if* was previously selected
        
        _ when (Tag =:= recvtos) orelse
               (Tag =:= recvttl) orelse
               (Tag =:= recvtclass) ->
            #{socket => [], recv_method => []};

        _ ->
            case maps:is_key(Tag, socket_opt()) of
                true -> #{socket => []};
                false ->
                    case maps:is_key(Tag, ignore_opt()) of
                        true ->
                            #{ignore => []};
                        false ->
                            maps:merge(
                              case maps:is_key(Tag, server_read_opts()) of
                                  true ->
                                      #{server_read => []};
                                  false ->
                                      #{}
                              end,
                              case maps:is_key(Tag, server_write_opts()) of
                                  true ->
                                      #{server_write => []};
                                  false ->
                                      #{}
                              end)
                    end
            end
    end.

-compile({inline, [ignore_opt/0]}).
ignore_opt() ->
    #{
      %% Handled by inet:udp_module/2
      udp_module => [],
      %% Handled by inet:udp_options/2
      ip => [],
      %% XXX Some of these must probably be handled one day...
      %% These are options for the 'port', which we don't have, so...
      high_msgq_watermark => [],
      low_msgq_watermark  => []
     }.

%% Category 'socket'
%%
%% Translation to 'level' and 'opt'
%% The map-values are one of the following:
%%    {Level, Key} | [{Level, Key}] | [{Domain, Level, Key}]
%% * The first is the "normal" way
%% * The second is basically a way to have side effects.
%%   For example, recbuf is nominally mapped to {socket, rcvbuf}
%%   but as a side effect our (socket nif) internal buffer is also
%%   set to the same value, but limited to at most 65536.
%% * The third is an even more special case, for the non-standard
%%   option pktoptions, which has different 'level' depending on the
%%   domain.
-compile({inline, [socket_opt/0]}).
socket_opt() ->
    #{
      %% Level: otp
      buffer => {otp, rcvbuf},
      debug  => {otp, debug},
      fd     => {otp, fd},

      %%
      %% Level: socket
      broadcast      => {socket, broadcast},
      bind_to_device => {socket, bindtodevice},
      dontroute      => {socket, dontroute},
      keepalive      => {socket, keepalive},
      %% linger         => {socket, linger},
      %% low_watermark  => {socket, rcvlowat},
      priority       => {socket, priority},
      %% Note that its the *first* that is the actual option!
      %% The second can be seen as a side effect...
      recbuf         => [{socket, rcvbuf}, {otp, rcvbuf}],
      reuseaddr      => {socket, reuseaddr},
      sndbuf         => {socket, sndbuf},

      %%
      %% Level: udp
      %% nodelay => {udp, nodelay},

      %%
      %% Level: ip
      recvtos         => {ip, recvtos},
      recvttl         => {ip, recvttl},
      tos             => {ip, tos},
      ttl             => {ip, ttl},
      add_membership  => {ip, add_membership},
      drop_membership => {ip, drop_membership},
      multicast_if    => {ip, multicast_if},
      multicast_ttl   => {ip, multicast_ttl},
      multicast_loop  => {ip, multicast_loop},

      %%
      %% Level: ipv6
      recvtclass  => {ipv6, recvtclass},
      ipv6_v6only => {ipv6, v6only},
      tclass      => {ipv6, tclass},

      %%
      %% Special cases
      %% These are options that cannot be mapped as above,
      %% as they, for instance, "belong to" several domains.
      %% So, we select which level to use based on the domain
      %% of the socket.

      %% This is a special case.
      %% Only supported on Linux and then only actually for IPv6,
      %% but unofficially also for ip...barf...
      %% In both cases this is *no longer valid* as the RFC which 
      %% introduced this, RFC 2292, is *obsoleted* by RFC 3542, where
      %% this "feature" *does not exist*...
      pktoptions  => [{inet, ip, pktoptions}, {inet6, ipv6, pktoptions}]
      }.

%% -compile({inline, [socket_inherit_opts/0]}).
%% socket_inherit_opts() ->
%%     [priority].

-compile({inline, [server_read_write_opts/0]}).
server_read_write_opts() ->
    %% Common for read and write side
    %% mod is unknown at this time, but will be properly "initiated"
    %% before 'meta' is written...
    #{mod => undefined}.
-compile({inline, [server_read_opts/0]}).
server_read_opts() ->
    %% Read side only opts
    maps:merge(
      #{active => false, % inet_drv also has this default
        mode => list,
        header => 0,
        deliver => term,
        %% WHAT DO WE DO ABOUT THIS!!!
        %% This option, read_packets, is *currently* not used,
        %% but accepted for backward compatibillity reasons.
	read_packets => 5,
        start_opts => [], % Just to make it settable
        %% XXX not implemented yet
        exit_on_close => true},
      server_read_write_opts()).
-compile({inline, [server_write_opts/0]}).
server_write_opts() ->
    %% Write side only opts
    maps:merge(#{}, server_read_write_opts()).
%% Category 'server'
%%
%% Default values
-compile({inline, [server_opts/0]}).
server_opts() ->
    maps:merge(server_read_opts(), server_write_opts()).

-compile({inline, [meta/1]}).
meta(D) -> maps:with(maps:keys(server_write_opts()), D).


%%% ========================================================================
%%% State Machine
%%%

%% State Machine Engine Call Interface

%% Start for open - create a socket
start_server(Mod, Domain, StartOpts, ExtraOpts) ->
    Owner = self(),
    Arg   = {Mod, Domain, ExtraOpts, Owner},
    case gen_statem:start(?MODULE, Arg, StartOpts) of
        {ok, _} = OK                -> OK;
        {error, {shutdown, Reason}} -> {error, Reason};
        {error, _} = Error          -> Error
    end.


call(Server, Call) ->
    try gen_statem:call(Server, Call)
    catch exit:{noproc, {gen_statem, call, _Args}} -> {error, closed}
    end.

stop_server(Server) ->
    try gen_statem:stop(Server) of
        _ -> ok
    catch
        _:_ -> ok
    end.

%% reply(From, Reply) ->
%%     gen_statem:reply(From, Reply).

%% -------------------------------------------------------------------------
%% Statem Machine Engine Callbacks

callback_mode() -> handle_event_function.

%% States:

%% 'controlling_process'
%% A super state that encapsulates any other state and
%% postpones all events but get_socket, get_server_opts/0
%% and Owner 'DOWN'.
%% Handling of controlling_process is divided up into two
%% parts. One part is handled by the state machine and the
%% other by the function controlling_process/2.
-record(controlling_process,
        {owner :: pid(),
         state :: term()}).


%% 'open'

%% 'reading'
-record(recv,
        {info :: socket:select_info()}).

%% 'closed_read' | 'closed_read_write'
%% 'closed' % Socket is closed or not created

-record(params,
        {socket        :: undefined | socket:socket(),
         owner         :: pid(),
         owner_mon     :: reference(),
         %% This comes from the options recvtos | recvttl | recvtclass
         %% If any of them are set then the method will change to msg
         %%    RecvMethod =:= [] => recvfrom
         %%    RecvMethod =/= [] => recvmsg
         recv_method = []}).


init({Mod, Domain, ExtraOpts, Owner}) ->

    %% ?DBG([{init, open},
    %%       {domain, Domain}, {extraopts, ExtraOpts}, {owner, Owner}]),

    process_flag(trap_exit, true),
    OwnerMon  = erlang:monitor(process, Owner),
    Proto     = if (Domain =:= local) -> default; true -> udp end,
    Extra     = #{}, % #{debug => true},
    case socket_open(Domain, Proto, ExtraOpts, Extra) of
        {ok, Socket} ->
            D0  = server_opts(),
            D   = D0#{mod => Mod},
            ok = socket:setopt(Socket, {otp,iow},  true),
            ok = socket:setopt(Socket, {otp,meta}, meta(D)),
            P  = #params{socket    = Socket,
                         owner     = Owner,
                         owner_mon = OwnerMon},
            State = open,
            Data  = {P, D},
            {ok, State, Data};
        {error, Reason} ->
	    %% ?DBG({open_failed, Reason}),
	    {stop, {shutdown, Reason}}
    end;
init(Arg) ->
    error_logger:error_report([{badarg, {?MODULE, init, [Arg]}}]),
    error(badarg, [Arg]).

                        
socket_open(Domain, Proto, #{fd := FD} = ExtraOpts, Extra) ->
    Opts =
        (maps:merge(Extra, maps:remove(fd, ExtraOpts)))
        #{dup      => false,
          domain   => Domain,
          type     => dgram,
          protocol => Proto},
    %% ?DBG([{fd, FD}, {opts, Opts}]),
    case socket:open(FD, Opts) of
        {ok, Socket} = OK ->
            %% ?DBG(['open ok', {socket, Socket}]),
            case socket:info(Socket) of
                #{ctype    := fromfd,
                  domain   := Domain,
                  type     := dgram,
                  protocol := _SelectedProto} ->
                    OK;
                _INVALID ->
                    %% ?DBG(['info invalid', {invalid, _INVALID}]),
                    (catch socket:close(Socket)),
                    {error, einval}
            end;
        {error, _Reason} = ERROR ->
            %% ?DBG(['open error', {reason, _Reason}]),
            ERROR
    end;
socket_open(Domain, Proto, ExtraOpts, Extra) ->
    %% ?DBG([{domain, Domain}, {proto, Proto},
    %%       {extra_opts, ExtraOpts}, {extra, Extra}]),
    Opts = maps:merge(Extra, ExtraOpts),
    socket:open(Domain, dgram, Proto, Opts).


terminate(_Reason, State, {_P, _} = P_D) ->
    %% ?DBG({_P#params.socket, State, _Reason}),
    case State of
        #controlling_process{state = OldState} ->
            terminate(OldState, P_D);
        _ ->
            terminate(State, P_D)
    end.

%%
terminate(State, {#params{socket = Socket} = P, D}) ->
    %% ?DBG(['terminate', {socket, Socket}, {state, State}]),
    case State of
        'closed' ->
	    ok;
        'closed_read' ->
            _ = socket_close(Socket),
            ok;
        'closed_read_write' ->
            _ = socket_close(Socket),
            ok;
        _ ->
	    _ = socket_close(Socket),
            {_D_1, ActionsR} =
                case State of
                    #controlling_process{state = OldState} ->
                        cleanup_close_read(P, D, OldState, closed);
                    _ ->
                        cleanup_close_read(P, D, State, closed)
                end,
            [gen_statem:reply(Reply)
             || {reply, _From, _Msg} = Reply <- reverse(ActionsR)],
            ok
    end,
    void.


%% -------------------------------------------------------------------------
%% Helpers

%% Construct a "socket" as in this module's API
module_socket(#params{socket = Socket}) ->
    ?MODULE_socket(self(), Socket).


%% -------------------------------------------------------------------------
%% Event Handler (callback)

%% Any state:

%% Call: get_socket/0 - internal
handle_event({call, From}, get_socket, _State,
             {#params{socket = Socket}, _D}) ->
    {keep_state_and_data,
     [{reply, From, {ok, Socket}}]};

%% Call: get_server_opts/0
handle_event({call, From}, get_server_opts, _State, {_P, D}) ->
    ServerOpts = maps:with(maps:keys(server_opts()), D),
    {keep_state_and_data,
     [{reply, From, {ok, ServerOpts}}]};

%% Event: Owner 'DOWN'
handle_event(info,
	     {'DOWN', OwnerMon, _, _, Reason} = _DOWN,
	     _State,
	     {#params{owner_mon = OwnerMon} = _P, _D} = P_D) ->
    %% ?DBG([{event, info}, {down, _DOWN}, {state, _State}]),
    {stop, {shutdown, Reason}, P_D};

%% Event: ?socket_counter_wrap/2
handle_event(info,
	     ?socket_counter_wrap(Socket, Counter),
	     'open' = _State,
	     {#params{socket = Socket} = P, D}) ->
    %% ?DBG([{state, _State}, {counter, Counter}]),
    {keep_state, {P, wrap_counter(Counter, D)}};

handle_event(info,
	     ?socket_counter_wrap(Socket, Counter),
	     #recv{} = _State,
	     {#params{socket = Socket} = P, D}) ->
    %% ?DBG([{state, _State}, {counter, Counter}]),
    {keep_state, {P, wrap_counter(Counter, D)}};

handle_event(info,
	     ?socket_counter_wrap(_Socket, _Counter),
	     _State,
	     _P_D) ->
    %% ?DBG([{state, _State}, {counter, _Counter}]),
    {keep_state_and_data, [postpone]};

%% Call: controlling_process/1
handle_event({call, {Caller, _} = From},
	     {controlling_process, NewOwner},
	     State,
	     {P, _D} = P_D) ->
    case P of
        #params{owner = NewOwner} ->
            {keep_state_and_data,
             [{reply, From, ok}]};
        #params{owner = Caller} ->
            {next_state,
             #controlling_process{owner = NewOwner, state = State},
             P_D,
             [{reply, From, transfer}]};
        #params{} ->
            {keep_state_and_data,
             [{reply, From, {error, not_owner}}]}
    end;

%%
%% State: #controlling_process{}
%%
%% Call: controlling_process/0
handle_event({call, {Owner, _} = From},
	     controlling_process,
	     #controlling_process{owner = NewOwner, state = State},
	     {#params{owner = Owner, owner_mon = OwnerMon} = P, D}) ->
    NewOwnerMon = erlang:monitor(process, NewOwner),
    true = erlang:demonitor(OwnerMon, [flush]),
    {next_state, State,
     {P#params{owner = NewOwner, owner_mon = NewOwnerMon}, D},
     [{reply, From, ok}]};

%%
%% Postpone all events but the ones above controlling_process/1
%% until the controlling process has been changed
handle_event(_Type,
	     _Content,
	     #controlling_process{},
	     _StateData) ->
    {keep_state_and_data, [postpone]};
%% Handled state: #controlling_process{}

%% Call: close/0
handle_event({call, From},
	     close,
	     State,
	     {P, D} = P_D) ->
    %% ?DBG([{socket, P#params.socket}, {state, State}]),
    case State of
        'closed_read' ->
            {next_state, 'closed', P_D,
             [{reply, From, socket_close(P#params.socket)}]};
        'closed_read_write' ->
            {next_state, 'closed', P_D,
             [{reply, From, socket_close(P#params.socket)}]};
        'closed' ->
            {keep_state_and_data,
             [{reply, From, ok}]};
        _ ->
            next_state(P,
		       cleanup_close_read(P,
					  D#{active := false},
					  State,
					  closed),
		       'closed',
		       [{reply, From, socket_close(P#params.socket)}])
    end;

%% Call: getopts/1
handle_event({call, From},
	     {getopts, Opts},
	     State,
	     {P, D}) ->
    %% ?DBG({call, getopts, Opts, State, D}),
    Result = state_getopts(P, D, State, Opts),
    %% ?DBG({call, getopts_result, Result}),
    {keep_state_and_data,
     [{reply, From, Result}]};

%% Call: setopts/1
handle_event({call, From},
	     {setopts, Opts},
	     State,
	     {P, D}) ->
    %% ?DBG([{setopts, Opts}, {state, State}, {d, D}]),
    {Result, {P_1, D_1}} = state_setopts(P, D, State, Opts),
    %% ?DBG([{result, Result}, {p1, P_1}, {d1, D_1}]),
    case Result of
	{error, einval} ->
	    %% If we get this error, either the options where crap or
	    %% the socket is in a "bad state" (maybe its closed).
	    %% So, if that is the case we accept that we may not be
	    %% able to update the meta data.
	    _ = socket:setopt(P_1#params.socket, {otp,meta}, meta(D_1)),
	    ok;
	_ ->
	    %% We should really handle this better. stop_and_reply?
	    ok = socket:setopt(P_1#params.socket, {otp,meta}, meta(D_1))
    end,
    Reply = {reply, From, Result},
    handle_reading(State, P_1, D_1, [Reply]);
    

%% Call: getstat/2
handle_event({call, From},
	     {getstat, What},
	     State,
	     {P, D}) ->
    case State of
        'closed' ->
            {keep_state_and_data, [{reply, From, {error, closed}}]};
        _ ->
            {D_1, Result} = getstat(P#params.socket, D, What),
            {keep_state, {P, D_1}, [{reply, From, {ok, Result}}]}
    end;

%% Call: info/1
handle_event({call, From},
	     info,
	     State,
	     {P, D}) ->
    case State of
        'closed' ->
            {keep_state_and_data, [{reply, From, #{rstates => [closed],
                                                   wstates => [closed]}}]};
        _ ->
            {D_1, Result} =
                handle_info(State, P#params.socket, P#params.owner, D),
            {keep_state, {P, D_1}, [{reply, From, Result}]}
    end;

%% State: 'closed' - what is not handled above
handle_event(Type, Content, 'closed' = State, P_D) ->
    handle_closed(Type, Content, State, P_D);
%% Handled state: 'closed'

%% Call: shutdown/1
handle_event({call, From}, {shutdown, How} = _SHUTDOWN, State, {P, D}) ->
    %% ?DBG({P#params.socket, _SHUTDOWN, State}),
    case State of
        'closed_read' when (How =:= read) ->
            %% ?DBG('already closed-read'),
            {keep_state_and_data, [{reply, From, ok}]};
        'closed_read_write' when (How =:= read_write) ->
            %% ?DBG('already closed-read-write'),
            {keep_state_and_data, [{reply, From, ok}]};
        _ ->
            %% ?DBG({'handle shutdown', How, State}),
            case handle_shutdown(P, State, How) of
                {keep, SRes} ->
                    %% ?DBG({'shutdown result', SRes, keep}),
                    {keep_state_and_data, [{reply, From, SRes}]};
                {NextState, SRes} ->
                    next_state(P,
			       cleanup_close_read(P,
						  D#{active := false},
						  State,
						  closed),
			       NextState,
			       [{reply, From, SRes}])
            end
    end;

%% State: 'closed_read' | 'closed_read_write' - what is not handled in
%%        close/0 and shutdown/1 above
handle_event(Type, Content, State, P_D)
  when (State =:= 'closed_read') orelse (State =:= 'closed_read_write') ->
    handle_closed(Type, Content, State, P_D);


%% ------- Socket is defined from here on -----------------------------------

%% Call: bind/1
handle_event({call, From}, {bind, BindAddr} = _BIND, _State, {P, _D}) ->
    %% ?DBG(['try bind',
    %%       {handle_event, call}, {bind_addr, BindAddr}, {state, _State}]),
    Result = socket:bind(P#params.socket, BindAddr),
    %% ?DBG([{bind_result, Result}] ++ 
    %%     case Result of
    %%         ok ->
    %%             case socket:sockname(P#params.socket) of
    %%                 {ok, SockAddr} ->
    %%                     [{sockaddr, SockAddr}];
    %%                 {error, SAReason} ->
    %%                     [{sockaddr_reason, SAReason}]
    %%             end;
    %%         {error, BReason} ->
    %%             [{bind_reason, BReason}]
    %%     end),
    {keep_state_and_data, [{reply, From, Result}]};


%% Call: recv/2,3 - active socket
handle_event({call, From},
	     {recv, _Length, _Timeout},
	     _State,
	     {_P, #{active := Active} = _D})
  when (Active =/= false) ->
    {keep_state_and_data, [{reply, From, {error, einval}}]};


%% 'open' | #recv{}

%% Call: recv/2,3
handle_event(
  {call, From}, {recv, Length, Timeout}, State, {P, D}) ->
    %% ?DBG([recv, {length, Length}, {timeout, Timeout}, {state, State}]),
    case State of
        'open' -> % idle
            handle_recv_start(P, D, From, Length, Timeout);
        #recv{} ->
            %% Receive already in progress
            {keep_state_and_data, [postpone]}
    end;

%% State: #recv{}
%%
%% Handle select done - try recv again
handle_event(
  info, ?socket_select(Socket, SelectRef),
  #recv{info = ?select_info(SelectRef)},
  {#params{socket = Socket} = P, D}) ->
    %% ?DBG(['info socket select', {socket, Socket}, {ref, SelectRef}, {p, P}, {d, D}]),
    handle_recv(P, D, []);

%%
handle_event(
  info, ?socket_abort(Socket, SelectRef, Reason),
  #recv{info = ?select_info(SelectRef)},
  {#params{socket = Socket} = P, D}) ->
    %% ?DBG(['socket abort', {reason, Reason}, {p, P}, {d, D}]),
    handle_reading(P, cleanup_recv_reply(P, D, [], Reason));

%%
%% Timeout on recv in non-active mode
handle_event(
  {timeout, recv}, recv, #recv{} = State, {P, D}) ->
    %% ?DBG({timeout, recv}),
    handle_reading(P, cleanup_recv(P, D, State, timeout));

%% Catch-all
handle_event(Type, Content, State, P_D) ->
    handle_unexpected(Type, Content, State, P_D).

%% End of event handler
%% -------------------------------------------------------------------------
%% Event handler helpers


%% We only accept/perform shutdown when socket is 'connected'.
%% This is done to be "compatible" with the inet-driver!

handle_shutdown(#params{socket = Socket},
                open = _State,
                write = How) ->
    {keep, socket:shutdown(Socket, How)};
handle_shutdown(#params{socket = Socket},
                #recv{} = _State,
                write = How) ->
    {keep, socket:shutdown(Socket, How)};
handle_shutdown(#params{socket = Socket},
                open = _State,
                read = How) ->
    handle_shutdown2(Socket, closed_read, How);
handle_shutdown(#params{socket = Socket},
                #recv{} = _State,
                read = How) ->
    handle_shutdown2(Socket, closed_read, How);
handle_shutdown(#params{socket = Socket},
                open = _State,
                read_write = How) ->
    handle_shutdown2(Socket, closed_read_write, How);
handle_shutdown(#params{socket = Socket},
                #recv{} = _State,
                read_write = How) ->
    handle_shutdown2(Socket, closed_read_write, How);
handle_shutdown(_Params, State, _How) ->
    {keep, {error, {invalid_state, State}}}.

handle_shutdown2(Socket, NextState, How) ->
    case socket:shutdown(Socket, How) of
        ok ->
            {NextState, ok};
        Error ->
            {keep, Error}
    end.


handle_unexpected(Type, Content, State, {P, _D}) ->
    error_logger:warning_report(
      [{module, ?MODULE}, {socket, P#params.socket},
       {unknown_event, {Type, Content}}, {state, State}]),
    case Type of
        {call, From} ->
            {keep_state_and_data,
             [{reply, From, {error, einval}}]};
        _ ->
            keep_state_and_data
    end.

handle_closed(Type, Content, State, {P, _D}) ->
    case Type of
        {call, From} ->
            {keep_state_and_data,
             [{reply, From, {error, closed}}]};
        _ ->
            error_logger:warning_report(
              [{module, ?MODULE}, {socket, P#params.socket},
               {unknown_event, {Type, Content}}, {state, State}]),
            keep_state_and_data
    end.

%% State transition helpers -------

%% The socket was "activated" => Start reading
handle_reading('open' = _State,
               P,
               #{active := Active} = D,
               ActionsR)
  when (Active =/= false) ->
    %% ?DBG(['open', {p, P}, {d, D}, {actions_r, ActionsR}]),
    handle_recv(P, recv_start(D), ActionsR);

%% The socket was "deactivated" (made passive) => Stop reading
handle_reading(#recv{info = SelectInfo} = _State,
               #params{socket = Socket} = P,
               #{active := Active} = D,
               ActionsR)
  when (Active =:= false) ->
    %% ?DBG(['recv', {select_info, SelectInfo},
    %%       {p, P}, {d, D}, {actions_r, ActionsR}]),
    socket_cancel(Socket, SelectInfo),
    {D2, ActionsR2} = cleanup_recv_reply(P, D, ActionsR, normal),
    {next_state, 'open',
     {P, recv_stop(D2#{active := false})}, reverse(ActionsR2)};

%% No state change (either still passive, still reading,
%% or was "closed" already)
handle_reading(_State, P, D, ActionsR) ->
    {keep_state, {P, D}, ActionsR}.
    

handle_reading(P, {D, ActionsR}) ->
    handle_reading(P, D, ActionsR).

handle_reading(P, D, ActionsR) ->
    %% ?DBG([{p, P}, {d, D}, {actions_r, ActionsR}]),
    case D of
        #{active := false} ->
            {next_state, 'open', {P, D}, reverse(ActionsR)};
        #{active := _} ->
            %% ?DBG([{p, P}, {d, D}, {actions_r, ActionsR}]),
            handle_recv(P, recv_start(D), ActionsR)
    end.

handle_recv_start(P, D, From, Length, Timeout) ->
    %% ?DBG([{length, Length}, {timeout, Timeout}]),
    handle_recv(P,
                D#{recv_length => Length, recv_from => From},
                [{{timeout, recv}, Timeout, recv}]).

handle_recv(#params{socket = Socket, recv_method = []} = P,
            #{recv_length := Length} = D, ActionsR) ->
    %% ?DBG(['try recvfrom', {socket, Socket}, {length, Length}]),
    case socket_recvfrom(Socket, Length) of
        {ok, {Source, <<Data/binary>>}} ->
            %% ?DBG(['recvfrom ok', {source, Source},
            %%       {'data sz', byte_size(Data)}]),
            handle_recv_deliver(P, D, ActionsR, {Source, Data});
        {select, ?select_info(_) = SelectInfo} ->
            %% ?DBG(['recvfrom select', {socket_info, SelectInfo}]),
            {next_state,
             #recv{info = SelectInfo},
             {P, D},
             reverse(ActionsR)};
        {error, Reason} ->
            %% ?DBG(['recvfrom error', {reason, Reason}]),
            handle_recv_error(P, D, ActionsR, Reason)
    end;
handle_recv(#params{socket = Socket} = P,
            #{recv_length := Length} = D, ActionsR) ->
    %% ?DBG(['try recvmsg', {socket, Socket}, {length, Length}]),
    case socket_recvmsg(Socket, Length) of
        {ok, MsgHdr} ->
            handle_recv_deliver(P, D, ActionsR, MsgHdr);
        {select, ?select_info(_) = SelectInfo} ->
            %% ?DBG(['recvmsg select', {socket_info, SelectInfo}]),
            {next_state,
             #recv{info = SelectInfo},
             {P, D},
             reverse(ActionsR)};
        {error, Reason} ->
            %% ?DBG(['recvmsg error', {reason, Reason}]),
            handle_recv_error(P, D, ActionsR, Reason)
    end.


handle_recv_deliver(P, D, ActionsR, Data) ->
    handle_reading(P, recv_data_deliver(P, D, ActionsR, Data)).

handle_recv_error(P, D, ActionsR, Reason) ->
    %% ?DBG({P#params.socket, Reason}),
    {D_1, ActionsR_1} = cleanup_recv_reply(P, D, ActionsR, Reason),
    case Reason of
        closed ->
            {next_state, 'closed_read', {P, D_1}, reverse(ActionsR_1)};
        emsgsize ->
            {next_state, 'open',
             {P, recv_stop(D#{active := false})}, reverse(ActionsR_1)};
        _ ->
            %% Temporary ... need something better here...maybe
            {next_state, 'open',
             {P, recv_stop(D#{active := false})}, reverse(ActionsR_1)}
    end.

%% -------------------------------------------------------------------------
%% Callback Helpers

next_state(P, {D, ActionsR}, State, Actions) ->
    {next_state, State, {P, D}, reverse(ActionsR, Actions)}.

cleanup_close_read(P, D, State, Reason) ->
    cleanup_recv(P, D, State, Reason).

cleanup_recv(P, D, State, Reason) ->
    %% ?DBG([{socket, P#params.socket}, {state, State}, {reason, Reason}]),    
    case State of
        #recv{info = SelectInfo} ->
            socket_cancel(P#params.socket, SelectInfo),
            cleanup_recv_reply(P, D, [], Reason);
        _ ->
            cleanup_recv_reply(P, D, [], Reason)
    end.

cleanup_recv_reply(P, D, ActionsR, Reason) ->
    case D of
        #{active := false} -> ok;
        #{active := _} ->
            ModuleSocket = module_socket(P),
            Owner        = P#params.owner,
            case Reason of
                timeout ->
                    Owner ! {udp_error, ModuleSocket, Reason},
                    ok;
                closed ->
                    Owner ! {udp_closed, ModuleSocket},
                    ok;
                emsgsize ->
                    Owner ! {udp_error, ModuleSocket, Reason},
                    ok;
                _ ->
                    Owner ! {udp_error, ModuleSocket, Reason},
                    Owner ! {udp_closed, ModuleSocket},
                    ok
            end
    end,
    {recv_stop(D#{active := false}),
     case D of
         #{recv_from := From} ->
             [{reply, From, {error, Reason}}, {{timeout, recv}, cancel} |
              ActionsR];
         #{} ->
             ActionsR
     end}.

%% Initialize packet recv state
recv_start(D) ->
    D#{recv_length => 0}.

recv_stop(D) ->
    maps:without([recv_from, recv_length], D).


%% Deliver data and update the active state
%% -> {NewD, NewActionsR}
recv_data_deliver(
  #params{owner = Owner} = P,
  #{mode := Mode, deliver := Deliver} = D,
  ActionsR, Data) ->

    %% ?DBG([{owner, Owner}, {mode, Mode}, {deliver, Deliver}]), 

    {IP, Port, AncData, DeliverData} = deliver_data(Data, Mode),
    case D of
        #{recv_from := From} ->
            {recv_stop(D),
             [{reply, From, {ok, mk_recv_reply(IP, Port, AncData, DeliverData)}},
              {{timeout, recv}, cancel}
              | ActionsR]};
        #{active := false} ->
            {recv_stop(D), ActionsR};
        #{active := Active} ->
            %% ?DBG({active, Active}),
            ModuleSocket = module_socket(P),
            _ = deliver_recv_msg(Owner, Active, Deliver,
                                 ModuleSocket, IP, Port, AncData, DeliverData),
            %% ?DBG('package delivered'),
            case Active of
                true ->
                    {recv_start(D), ActionsR};
                once ->
                    {recv_stop(D#{active => false}), ActionsR};
                1 ->
                    Owner ! {udp_passive, ModuleSocket},
                    {recv_stop(D#{active => false}), ActionsR};
                N when is_integer(N) ->
                    {recv_start(D#{active => Active - 1}), ActionsR}
            end
    end.

mk_recv_reply(IP, Port, undefined = _AncData, Data) ->
    {IP, Port, Data};
mk_recv_reply(IP, Port, AncData, Data) ->
    {IP, Port, AncData, Data}.

deliver_recv_msg(Pid, Active, Deliver, Socket, IP, Port, AncData, Data) ->
    %% ?DBG(['deliver packet', {pid, Pid}]),
    Pid ! mk_recv_msg(Active, Deliver, Socket, IP, Port, AncData, Data).

mk_recv_msg(true = _Active, port = _Deliver,
            Socket, IP, Port, undefined, Data) ->
    {Socket, {data, [IP, Port, Data]}};
mk_recv_msg(true = _Active, port = _Deliver,
            Socket, IP, Port, AncData, Data) ->
    {Socket, {data, [IP, Port, AncData, Data]}};
mk_recv_msg(_Active, _Deliver,
            Socket, IP, Port, undefined, Data) ->
    {udp, Socket, IP, Port, Data};
mk_recv_msg(_Active, _Deliver,
            Socket, IP, Port, AncData, Data) ->
    {udp, Socket, IP, Port, AncData, Data}.


%% {Source :: sockaddr(), Data :: binary()}
%% #{addr := SockAddr :: sockaddr(),
%%   iov  := IOV      :: iovec(),
%%   ctrl := Ctrl     :: list()}
deliver_data({#{family := local = Fam, path := Path}, <<Data/binary>>}, Mode) ->
    {{Fam, Path}, 0, undefined, deliver_data_mode(Data, Mode)};
deliver_data({#{family := Fam,
                addr   := Addr,
                port   := Port}, <<Data/binary>>}, Mode)
  when ((Fam =:= inet) orelse (Fam =:= inet6)) ->
    {Addr, Port, undefined, deliver_data_mode(Data, Mode)};
deliver_data(#{addr := #{family := local = Fam, path := Path},
               iov  := IOV,
               ctrl := Ctrl}, Mode) ->
    Data  = deliver_data_mode(IOV, Mode),
    Ctrl2 = ctrl2ancdata(Ctrl),
    {{Fam, Path}, 0, Ctrl2, Data};
deliver_data(#{addr := #{family := Fam, addr := Addr, port := Port},
               iov  := IOV,
               ctrl := Ctrl}, Mode) 
  when ((Fam =:= inet) orelse (Fam =:= inet6)) ->
    Data  = deliver_data_mode(IOV, Mode),
    Ctrl2 = ctrl2ancdata(Ctrl),
    {Addr, Port, Ctrl2, Data};
deliver_data({#{family := unspec, addr := Addr}, <<Data/binary>>}, Mode)
  when is_binary(Addr) ->
    {{unspec, Addr}, 0, undefined, deliver_data_mode(Data, Mode)};
deliver_data({Unspec, <<Data/binary>>}, Mode) when is_binary(Unspec) ->
    {{unspec, Unspec}, 0, undefined, deliver_data_mode(Data, Mode)}.
%% deliver_data(_Arg1, _Arg2) ->
%%     ?BADARG([_Arg1, _Arg2]).


deliver_data_mode(Data, list)   when is_binary(Data) -> binary_to_list(Data);
deliver_data_mode(Data, binary) when is_binary(Data) -> Data;
deliver_data_mode(IOV,  list)   when is_list(IOV) -> deliver_data_mode_bin(IOV);
deliver_data_mode(IOV,  binary) when is_list(IOV) -> iolist_to_binary(IOV).

%% Make some optimizations
deliver_data_mode_bin([Bin]) ->
    Bin;
deliver_data_mode_bin([Bin1, Bin2]) ->
    <<Bin1/binary, Bin2/binary>>;
deliver_data_mode_bin([Bin1, Bin2, Bin3]) ->
    <<Bin1/binary, Bin2/binary, Bin3/binary>>;
deliver_data_mode_bin([Bin1, Bin2, Bin3, Bin4]) ->
    <<Bin1/binary, Bin2/binary, Bin3/binary, Bin4/binary>>;
deliver_data_mode_bin(IOV) ->
    deliver_data_mode_bin(IOV, <<>>).

deliver_data_mode_bin([], Acc) ->
    Acc;
deliver_data_mode_bin([Bin|IOV], Acc) ->
    deliver_data_mode_bin(IOV, <<Bin/binary, Acc/binary>>).


ctrl2ancdata(CTRL) ->
    ctrl2ancdata(CTRL, []).

ctrl2ancdata([], AncData) ->
   lists:reverse(AncData);
ctrl2ancdata([#{level := ip,
                type  := tos,
                value := TOS,
                data  := _Data}| CTRL],
             AncData) ->
    ctrl2ancdata(CTRL, [{tos, TOS}|AncData]);
ctrl2ancdata([#{level := ip,
                type  := ttl,
                value := TTL,
                data  := _Data}| CTRL],
             AncData) ->
    ctrl2ancdata(CTRL, [{ttl, TTL}|AncData]);
ctrl2ancdata([#{level := ipv6,
                type  := tclass,
                value := TClass,
                data  := _Data}| CTRL],
             AncData) ->
    ctrl2ancdata(CTRL, [{tclass, TClass}|AncData]);
ctrl2ancdata([_|CTRL], AncData) ->
    ctrl2ancdata(CTRL, AncData).


%% -> {ok, NewD} | {{error, Reason}, D}
state_setopts(P, D, _State, []) ->
    {ok, {P, D}};
state_setopts(P, D, State, [Opt | Opts]) ->
    %% ?DBG([{state, State}, {opt, Opt}]),
    Opt_1 = conv_setopt(Opt),
    %% ?DBG([{'option converted', Opt_1}]),
    case setopt_categories(Opt_1) of
        %% This is a special case for options that trigger us to choose 
        %% the recv "method"; either recvfrom or recvmsg.
        %% If the recv_method list is empty => recvfrom otherwise recvmsg.
        #{socket := _, recv_method := _} ->
            RecvMethod = P#params.recv_method,
            RecvMethod2 =
                case Opt_1 of
                    {Tag, true} -> %% Add if not already added
                        M = lists:member(Tag, RecvMethod),
                        if (M) -> RecvMethod; true -> [Tag|RecvMethod] end;
                    {Tag, false} -> %% Remove
                        lists:delete(Tag, RecvMethod)
                end,
            P_1 = P#params{recv_method = RecvMethod2},
            case P_1#params.socket of
                undefined ->
                    {{error, closed}, {P, D, State}};
                Socket ->
                    case socket_setopt(Socket, Opt_1) of
                        ok ->
                            state_setopts(P_1, D, State, Opts);
                        {error, _} = Error ->
                            {Error, {P_1, D}}
                    end
            end;

        #{socket := _} ->
            %% ?DBG(socket),
            case P#params.socket of
                undefined ->
                    {{error, closed}, {P, D}};
                Socket ->
                    case socket_setopt(Socket, Opt_1) of
                        ok ->
                            state_setopts(P, D, State, Opts);
                        {error, _} = Error ->
                            {Error, {P, D}}
                    end
            end;

        %%
        #{server_write := _} when State =:= 'closed' ->
            %% ?DBG('server write when state closed'),
            {{error, einval}, {P, D}};
        #{server_write := _} ->
            %% ?DBG('server write'),
            state_setopts_server(P, D, State, Opts, Opt_1);

        %%
        #{server_read := _} when State =:= 'closed' ->
            %% ?DBG('server read when state closed'),
            {{error, einval}, {P, D}};
        #{server_read := _} when (State =:= 'closed_read') orelse
                                 (State =:= 'closed_read_write') ->
            %% ?DBG('server read when state closed-read or closed-read-write'),
            {{error, einval}, {P, D}};
        #{server_read := _} ->
            %% ?DBG('server read'),
            state_setopts_server(P, D, State, Opts, Opt_1);

        %%
        #{ignore := _} ->
            %% ?DBG(ignore),
            state_setopts(P, D, State, Opts);
        #{} = _EXTRA -> % extra | einval
            %% ?DBG({extra, _EXTRA}),
            {{error, einval}, {P, D}}
    end.

state_setopts_server(P, D, State, Opts, {Tag, Value}) ->
    case Tag of
        active ->
	    %% ?DBG(['active', {value, Value}]),
            state_setopts_active(P, D, State, Opts, Value);
        _ ->
	    %% ?DBG([{tag, Tag}, {value, Value}]),
            state_setopts(P, D#{Tag => Value}, State, Opts)
    end.

state_setopts_active(P, D, State, Opts, Active) ->
    if
        Active =:= once;
        Active =:= true ->
            %% ?DBG(['active', {state, State}, {active, Active}]),
            state_setopts(P, D#{active := Active}, State, Opts);
        Active =:= false ->
            %% ?DBG(['passive', {state, State}]),
            case D of
                #{active := OldActive} when is_integer(OldActive) ->
                    P#params.owner ! {udp_passive, module_socket(P)},
                    ok;
                _ ->
                    ok
                end,
            state_setopts(P, D#{active := Active}, State, Opts);
        is_integer(Active), -32768 =< Active, Active =< 32767 ->
            %% ?DBG(['active', {state, State}, {active, Active}]),
            N =
                case D of
                    #{active := OldActive} when is_integer(OldActive) ->
                        OldActive + Active;
                    #{active := _OldActive} ->
                        Active
                end,
            %% ?DBG(['active', {'N', N}]),
            if
                32767 < N ->
                    {{error, einval}, {P, D}};
                N =< 0 ->
                    P#params.owner ! {udp_passive, module_socket(P)},
                    state_setopts(P, D#{active := false}, State, Opts);
                true ->
                    state_setopts(P, D#{active := N}, State, Opts)
            end;
        true ->
            %% ?DBG(['error', {state, State}, {active, Active}]),
            {{error, einval}, {P, D}}
    end.

%% -> {ok, [Options]} | {error, einval}
state_getopts(P, D, State, Opts) ->
    state_getopts(P, D, State, Opts, []).
%%
state_getopts(_P, _D, _State, [], Acc) ->
    {ok, reverse(Acc)};
state_getopts(P, D, State, [Tag | Tags], Acc) ->
    case getopt_categories(Tag) of
        #{socket := _} ->
            case P#params.socket of
                undefined ->
                    {error, closed};
                Socket ->
                    case socket_getopt(Socket, Tag) of
                        {ok, Value} ->
                            state_getopts(
                              P, D, State, Tags, [{Tag, Value} | Acc]);
                        {error, _Reason} ->
                            state_getopts(P, D, State, Tags, Acc)
                    end
              end;
        #{server_write := _} when State =:= 'closed' ->
            {error, einval};
        #{server_write := _} ->
            Value = maps:get(Tag, D),
            state_getopts(P, D, State, Tags, [{Tag, Value} | Acc]);
        #{server_read := _} when State =:= 'closed' ->
            {error, einval};
        #{server_read := _} when (State =:= 'closed_read') orelse
                                 (State =:= 'closed_read_write') ->
            {error, einval};
        #{server_read := _} ->
            Value = maps:get(Tag, D),
            state_getopts(P, D, State, Tags, [{Tag, Value} | Acc]);
        #{} = _EXTRA -> % extra | einval
            {error, einval}
    end.

handle_info(State, Socket, Owner, #{active := Active} = D) ->
    %% ?DBG([{state, State}, {active, Active}]),
    %% Read counters
    Counters_1 = socket_info_counters(Socket),
    %% Check for recent wraps
    {D_1, Wrapped} = receive_counter_wrap(Socket, D, []),
    %%
    %% Assumption: a counter that we just now got a wrap message from
    %% will not wrap again before we read the updated value
    %%
    %% Update wrapped counters
    Info = #{counters := Counters_2} = socket:info(Socket),
    Counters_3 = maps:merge(Counters_1, maps:with(Wrapped, Counters_2)),
    %% Go ahead with wrap updated counters
    Counters_4 = maps:from_list(getstat_what(D_1, Counters_3)),
    SimpleState = simplify_state(State),
    {D_1, Info#{counters => Counters_4,
                istate   => SimpleState,
                owner    => Owner,
                active   => Active}}.

simplify_state(#recv{}) ->
    recv;
simplify_state(State) ->
    State.

    
getstat(Socket, D, What) ->
    %% Read counters
    Counters_1 = socket_info_counters(Socket),
    %% Check for recent wraps
    {D_1, Wrapped} = receive_counter_wrap(Socket, D, []),
    %%
    %% Assumption: a counter that we just now got a wrap message from
    %% will not wrap again before we read the updated value
    %%
    %% Update wrapped counters
    Counters_2 = socket_info_counters(Socket),
    Counters_3 = maps:merge(Counters_1, maps:with(Wrapped, Counters_2)),
    %% Go ahead with wrap updated counters
    {D_1, getstat_what(What, D_1, Counters_3)}.

getstat_what(D, C) ->
    getstat_what(inet:stats(), D, C).

getstat_what([], _D, _C) -> [];
getstat_what([Tag | What], D, C) ->
    Val =
        case Tag of
            recv_oct ->
                counter_value(read_byte, D, C);
            recv_cnt ->
                counter_value(read_pkg, D, C);
            recv_max ->
                getstat_avg(read_byte, D, C, read_pkg);
            recv_avg ->
                getstat_avg(read_byte, D, C, read_pkg);
            recv_dvi -> 0;
            %%
            send_oct ->
                counter_value(write_byte, D, C);
            send_cnt ->
                counter_value(write_pkg, D, C);
            send_max ->
                getstat_avg(write_byte, D, C, write_pkg);
            send_avg ->
                getstat_avg(write_byte, D, C, write_pkg);
            send_pend -> 0
        end,
    [{Tag, Val} | getstat_what(What, D, C)].

getstat_avg(SumTag, D, C, CntTag) ->
    Cnt = counter_value(CntTag, D, C),
    if
        Cnt =:= 0 ->
            counter_value(SumTag, D, C);
        true ->
            round(counter_value(SumTag, D, C) / Cnt)
    end.

socket_info_counters(Socket) ->
    #{counters := Counters} = socket:info(Socket),
    Counters.

receive_counter_wrap(Socket, D, Wrapped) ->
    receive
        ?socket_counter_wrap(Socket, Counter) ->
	    %% ?DBG([{counter, Counter}]),
            receive_counter_wrap(
              Socket, wrap_counter(Counter, D), [Counter | Wrapped])
    after 0 ->
            {D, Wrapped}
    end.

wrap_counter(Counter, D) ->
    case D of
        #{Counter := N} ->
            D#{Counter := N + 1};
        #{} ->
            D#{Counter => 1}
    end.

-define(COUNTER_BITS, 32).
counter_value(Counter, D, Counters) ->
    case D of
        #{Counter := Wraps} ->
            (Wraps bsl ?COUNTER_BITS) + maps:get(Counter, Counters);
        #{} ->
            maps:get(Counter, Counters)
    end.



-compile({inline, [reverse/1]}).
reverse([]) -> [];
reverse([_] = L) -> L;
reverse([A, B]) -> [B, A];
reverse(L) -> lists:reverse(L).

-compile({inline, [reverse/2]}).
reverse([], L) -> L;
reverse([A], L) -> [A | L];
reverse([A, B], L) -> [B, A | L];
reverse(L1, L2) -> lists:reverse(L1, L2).


%% -------------------------------------------------------------------------
-ifdef(undefined).

%% Better, Leaner, Faster, Smarter, than inet:timeout

end_time(infinity) -> infinity;
end_time(Timeout) when is_integer(Timeout), 0 =< Timeout ->
    erlang:monotonic_time(millisecond) + Timeout.

timeout(infinity) -> infinity;
timeout(EndTime) ->
    Time = erlang:monotonic_time(millisecond),
    if
        Time < EndTime ->
            EndTime - Time;
        true -> 0
    end.

-endif.
