%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2010-2025. 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%
%%

%%
%% This file contains code that encode/decode modules generated by
%% diameter_codegen.erl calls to implement the functionality. This
%% code does most of the work, the generated code being kept simple.
%%

-module(diameter_gen).
-moduledoc false.

-compile({inline, [incr/8,
                   incr/4,
                   field/1,
                   setopts/4,
                   avp_arity/5,
                   set_failed/2,
                   set_strict/3]}).

-export([encode_avps/3,
         decode_avps/3,
         grouped_avp/4,
         empty_group/2,
         empty/2]).

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

-define(THROW(T), throw({?MODULE, T})).

-type parent_name()   :: atom().  %% parent = Message or AVP
-type parent_record() :: tuple() | avp_values() | map().
-type avp_name()   :: atom().
-type avp_record() :: tuple().
-type avp_values() :: [{avp_name(), term()}].

-type non_grouped_avp() :: #diameter_avp{}.
-type grouped_avp() :: nonempty_improper_list(#diameter_avp{}, [avp()]).
-type avp() :: non_grouped_avp() | grouped_avp().

%% The arbitrary arity returned from dictionary avp_arity functions.
-define(ANY, {0, '*'}).

%% ---------------------------------------------------------------------------
%% # encode_avps/3
%% ---------------------------------------------------------------------------

-spec encode_avps(parent_name(), parent_record(), map())
   -> iolist()
    | no_return().

encode_avps(Name, Vals, #{module := Mod} = Opts) ->
    Strict = mget(strict_arities, Opts, encode),
    try
        encode(Name, Vals, Opts, Strict, Mod)
    catch
        throw: {?MODULE, Reason} ->
            diameter_lib:log({encode, error},
                             ?MODULE,
                             ?LINE,
                             {Reason, Name, Vals, Mod}),
            erlang:error(list_to_tuple(Reason ++ [Name]));
        error: Reason: Stack ->
            diameter_lib:log({encode, failure},
                             ?MODULE,
                             ?LINE,
                             {Reason, Name, Vals, Mod, Stack}),
            erlang:error({encode_failure, Reason, Name, Stack})
    end.

%% encode/5

encode(Name, Vals, Opts, Strict, Mod)
  when is_list(Vals) ->
    case Opts of
        #{ordered_encode := false} ->
            lists:map(fun({F,V}) -> encode(Name, F, V, Opts, Strict, Mod) end,
                      Vals);
        _ ->
            Rec = Mod:'#set-'(Vals, newrec(Mod, Name)),
            encode(Name, Rec, Opts, Strict, Mod)
    end;

encode(Name, Map, Opts, Strict, Mod)
  when is_map(Map) ->
    [enc(F, A, V, Opts, Strict, Mod) || {F,A} <- Mod:avp_arity(Name),
                                        V <- [mget(F, Map, undefined)]];

encode(Name, Rec, Opts, Strict, Mod) ->
    [encode(Name, F, V, Opts, Strict, Mod) || {F,V} <- Mod:'#get-'(Rec)].

%% encode/6

encode(_, AvpName, Values, Opts, Strict, Mod)
  when Strict /= encode ->
    enc(AvpName, ?ANY, Values, Opts, Strict, Mod);

encode(Name, AvpName, Values, Opts, Strict, Mod) ->
    Arity = Mod:avp_arity(Name, AvpName),
    enc(AvpName, Arity, Values, Opts, Strict, Mod).

%% enc/6

enc(AvpName, Arity, Values, Opts, Strict, Mod)
  when Strict /= encode, Arity /= ?ANY ->
    enc(AvpName, ?ANY, Values, Opts, Strict, Mod);

enc(AvpName, 1, undefined, _, _, _) ->
    ?THROW([mandatory_avp_missing, AvpName]);

enc(AvpName, 1, Value, Opts, _, Mod) ->
    H = avp_header(AvpName, Mod),
    enc(AvpName, H, Value, Opts, Mod);

enc(_, {0,_}, [], _, _, _) ->
    [];

enc(_, _, undefined, _, _, _) ->
    [];

%% Be forgiving when a list of values is expected. If the value itself
%% is a list then the user has to wrap it to avoid each member from
%% being interpreted as an individual AVP value.
enc(AvpName, Arity, V, Opts, Strict, Mod)
  when not is_list(V) ->
    enc(AvpName, Arity, [V], Opts, Strict, Mod);

enc(AvpName, {Min, Max}, Values, Opts, Strict, Mod) ->
    H = avp_header(AvpName, Mod),
    enc(AvpName, H, Min, 0, Max, Values, Opts, Strict, Mod).

%% enc/9

enc(AvpName, H, Min, N, Max, Vs, Opts, Strict, Mod)
  when Strict /= encode;
       Max == '*', Min =< N ->
    [enc(AvpName, H, V, Opts, Mod) || V <- Vs];

enc(AvpName, _, Min, N, _, [], _, _, _)
  when N < Min ->
    ?THROW([repeated_avp_insufficient_arity, AvpName, Min, N]);

enc(_, _, _, _, _, [], _, _, _) ->
    [];

enc(AvpName, _, _, N, Max, _, _, _, _)
  when Max =< N ->
    ?THROW([repeated_avp_excessive_arity, AvpName, Max]);

enc(AvpName, H, Min, N, Max, [V|Vs], Opts, Strict, Mod) ->
    [enc(AvpName, H, V, Opts, Mod)
     | enc(AvpName, H, Min, N+1, Max, Vs, Opts, Strict, Mod)].

%% avp_header/2

avp_header('AVP', _) ->
    false;

avp_header(AvpName, Mod) ->
    {_,_,_} = Mod:avp_header(AvpName).

%% enc/5

enc('AVP', false, Value, Opts, Mod) ->
    enc_AVP(Value, Opts, Mod);

enc(AvpName, Hdr, Value, Opts, Mod) ->
    enc1(AvpName, Hdr, Value, Opts, Mod).

%% enc1/5

enc1(AvpName, {_,_,_} = Hdr, Value, Opts, Mod) ->
    diameter_codec:pack_data(Hdr, Mod:avp(encode, Value, AvpName, Opts)).

%% enc1/6

enc1(AvpName, {_,_,_} = Hdr, Value, Opts, Mod, Dict) ->
    diameter_codec:pack_data(Hdr, avp(encode, Value, AvpName, Opts, Mod, Dict)).

%% enc_AVP/3

%% No value: assume AVP data is already encoded. The normal case will
%% be when this is passed back from #diameter_packet.errors as a
%% consequence of a failed decode. Any AVP can be encoded this way
%% however, which side-steps any arity checks for known AVP's and
%% could potentially encode something unfortunate.
enc_AVP(#diameter_avp{value = undefined} = A, Opts, _) ->
    diameter_codec:pack_avp(A, Opts);

%% Encode a name/value pair using an alternate dictionary if need be ...
enc_AVP(#diameter_avp{name = AvpName, value = Value}, Opts, Mod) ->
    enc_AVP(AvpName, Value, Opts, Mod);
enc_AVP({AvpName, Value}, Opts, Mod) ->
    enc_AVP(AvpName, Value, Opts, Mod);

%% ... or with a specified dictionary.
enc_AVP({Dict, AvpName, Value}, Opts, Mod) ->
    enc1(AvpName, Dict:avp_header(AvpName), Value, Opts, Mod, Dict).

%% Don't guard against anything being sent as a generic 'AVP', which
%% allows arity restrictions to be abused.

%% enc_AVP/4

enc_AVP(AvpName, Value, Opts, Mod) ->
    try Mod:avp_header(AvpName) of
        H ->
            enc1(AvpName, H, Value, Opts, Mod)
    catch
        error: _ ->
            Dicts = mget(avp_dictionaries, Opts, []),
            enc_AVP(Dicts, AvpName, Value, Opts, Mod)
    end.

%% enc_AVP/5

enc_AVP([Dict | Rest], AvpName, Value, Opts, Mod) ->
    try Dict:avp_header(AvpName) of
        H ->
            enc1(AvpName, H, Value, Opts, Mod, Dict)
    catch
        error: _ ->
            enc_AVP(Rest, AvpName, Value, Opts, Mod)
    end;

enc_AVP([], AvpName, _, _, _) ->
    ?THROW([no_dictionary, AvpName]).

%% ---------------------------------------------------------------------------
%% # decode_avps/3
%% ---------------------------------------------------------------------------

-spec decode_avps(parent_name(), binary(), map())
   -> {parent_record() | parent_name(), [avp()], Failed}
 when Failed :: [{5000..5999, #diameter_avp{}}].

decode_avps(Name, Bin, #{module := Mod, decode_format := Fmt} = Opts) ->
    Strict = mget(strict_arities, Opts, decode),
    [AM, Avps, Failed | Rec]
        = decode(Bin, Name, Mod, Fmt, Strict, Opts, 0, #{}),
    %% AM counts the number of top-level AVPs, which missing/5 then
    %% uses when appending 5005 errors.
    {reformat(Name, Rec, Strict, Mod, Fmt),
     Avps,
     Failed ++ missing(Name, Strict, Mod, Opts, AM)}.

%% Append arity errors so that errors are reported in the order
%% encountered. Failed-AVP should typically contain the first
%% error encountered.

%% decode/8

decode(<<Code:32, V:1, M:1, P:1, _:5, Len:24, I:V/unit:32, Rest/binary>>,
       Name,
       Mod,
       Fmt,
       Strict,
       Opts,
       Idx,
       AM) ->
    decode(Rest,
           Code,
           if 1 == V -> I; true -> undefined end,
           Len - 8 - 4*V, %% possibly negative, causing case match to fail
           (4 - (Len rem 4)) rem 4,
           1 == M,
           1 == P,
           Name,
           Mod,
           Fmt,
           Strict,
           Opts,
           Idx,
           AM);

decode(<<>>, Name, Mod, Fmt, Strict, _, _, AM) ->
    [AM, [], [] | newrec(Fmt, Mod, Name, Strict)];

decode(Bin, Name, Mod, Fmt, Strict, _, Idx, AM) ->
    Avp = #diameter_avp{data = Bin, index = Idx},
    [AM, [Avp], [{5014, Avp}] | newrec(Fmt, Mod, Name, Strict)].

%% decode/14

decode(Bin, Code, Vid, DataLen, Pad, M, P, Name, Mod, Fmt, Strict, Opts0,
       Idx, AM0) ->
    case Bin of
        <<Data:DataLen/binary, _:Pad/binary, T/binary>> ->
            {NameT, Field, Arity, {I, AM}}
                = incr(Name, Code, Vid, M, Mod, Strict, Opts0, AM0),

            Opts = setopts(NameT, Name, M, Opts0),
            %% Not AvpName or else a failed Failed-AVP
            %% decode is packed into 'AVP'.

            Avp = #diameter_avp{code = Code,
                                vendor_id = Vid,
                                is_mandatory = M,
                                need_encryption = P,
                                data = Data,
                                name = name(NameT),
                                type = type(NameT),
                                index = Idx},

            Dec = dec(Data, Name, NameT, Mod, Fmt, Opts, Avp),
            Acc = decode(T, Name, Mod, Fmt, Strict, Opts0, Idx+1, AM),%% recurse
            acc(Acc, Dec, I, Field, Arity, Strict, Mod);
        _ ->
            {NameT, _Field, _Arity, {_, AM}}
                = incr(Name, Code, Vid, M, Mod, Strict, Opts0, AM0),

            Avp = #diameter_avp{code = Code,
                                vendor_id = Vid,
                                is_mandatory = M,
                                need_encryption = P,
                                data = Bin,
                                name = name(NameT),
                                type = type(NameT),
                                index = Idx},

            [AM, [Avp], [{5014, Avp}] | newrec(Fmt, Mod, Name, Strict)]
    end.

%% incr/8

incr(Name, Code, Vid, M, Mod, Strict, Opts, AM0) ->
    NameT = Mod:avp_name(Code, Vid), %% {AvpName, Type} | 'AVP'
    Field = field(NameT),            %% AvpName | 'AVP'
    Arity = avp_arity(Name, Field, Mod, Opts, M),
    if 0 == Arity, 'AVP' /= Field ->
            A = pack_arity(Name, Field, Opts, Mod, M),
            {NameT, 'AVP', A, incr('AVP', A, Strict, AM0)};
       true ->
            {NameT, Field, Arity, incr(Field, Arity, Strict, AM0)}
    end.

%% Data is a truncated header if command_code = undefined, otherwise
%% payload bytes. The former is padded to the length of a header if
%% the AVP reaches an outgoing encode.
%%
%% RFC 6733 says that an AVP returned with 5014 can contain a minimal
%% payload for the AVP's type, but don't always know the type.

setopts('AVP', _, _, Opts) ->
    Opts;

setopts({_, Type}, Name, M, Opts) ->
    set_failed(Name, set_strict(Type, M, Opts)).

%% incr/4

incr(_, A, SA, AM)
  when A == ?ANY;
       A == 0;
       SA /= decode ->
    {undefined, AM};

incr(AvpName, _, _, AM) ->
    case AM of
        #{AvpName := N} ->
            {N, AM#{AvpName => N+1}};
        _ ->
            {0, AM#{AvpName => 1}}
    end.

%% mget/3
%%
%% Measurably faster than maps:get/3.

mget(Key, Map, Def) ->
    case Map of
        #{Key := V} ->
            V;
        _ ->
            Def
    end.

%% name/1

name({Name, _}) ->
    Name;
name(_) ->
    undefined.

%% type/1

type({_, Type}) ->
    Type;
type(_) ->
    undefined.

%% missing/5

missing(Name, decode, Mod, Opts, AM) ->
    [{5005, empty_avp(N, Opts, Mod)} || {N,A} <- Mod:avp_arity(Name),
                                        N /= 'AVP',
                                        Mn <- [min_arity(A)],
                                        0 < Mn,
                                        mget(N, AM, 0) < Mn];

missing(_, _, _, _, _) ->
    [].

%% 3588/6733:
%%
%%   DIAMETER_MISSING_AVP               5005
%%      The request did not contain an AVP that is required by the Command
%%      Code definition.  If this value is sent in the Result-Code AVP, a
%%      Failed-AVP AVP SHOULD be included in the message.  The Failed-AVP
%%      AVP MUST contain an example of the missing AVP complete with the
%%      Vendor-Id if applicable.  The value field of the missing AVP
%%      should be of correct minimum length and contain zeros.

%% min_arity/1

min_arity(1) ->
    1;
min_arity({Mn,_}) ->
    Mn.

%% empty_avp/3

empty_avp('AVP', _, _) ->
    #diameter_avp{data = <<0:64>>};

empty_avp(Name, Opts, Mod) ->
    {Code, Flags, VId} = Mod:avp_header(Name),
    {Name, Type} = Mod:avp_name(Code, VId),
    #diameter_avp{name = Name,
                  code = Code,
                  vendor_id = VId,
                  is_mandatory = 0 /= (Flags band 2#01000000),
                  need_encryption = 0 /= (Flags band 2#00100000),
                  data = Mod:empty_value(Name, Opts),
                  type = Type}.

%% 3588, ch 7:
%%
%%   The Result-Code AVP describes the error that the Diameter node
%%   encountered in its processing.  In case there are multiple errors,
%%   the Diameter node MUST report only the first error it encountered
%%   (detected possibly in some implementation dependent order).  The
%%   specific errors that can be described by this AVP are described in
%%   the following section.

%% field/1

field({AvpName, _}) ->
    AvpName;
field(_) ->
    'AVP'.

%% dec/7

%% AVP not in dictionary: try an alternate.

dec(Data, Name, 'AVP', Mod, Fmt, Opts, Avp) ->
    dec_AVP(dicts(Mod, Opts), Data, Name, Mod, Fmt, Opts, Avp);

%% 6733, 4.4:
%%
%%   Receivers of a Grouped AVP that does not have the 'M' (mandatory)
%%   bit set and one or more of the encapsulated AVPs within the group
%%   has the 'M' (mandatory) bit set MAY simply be ignored if the
%%   Grouped AVP itself is unrecognized. The rule applies even if the
%%   encapsulated AVP with its 'M' (mandatory) bit set is further
%%   encapsulated within other sub-groups, i.e., other Grouped AVPs
%%   embedded within the Grouped AVP.
%%
%% The first sentence is slightly mangled, but take it to mean this:
%%
%%   An unrecognized AVP of type Grouped that does not set the 'M' bit
%%   MAY be ignored even if one of its encapsulated AVPs sets the 'M'
%%   bit.
%%
%% The text above is a change from RFC 3588, which instead says this:
%%
%%   Further, if any of the AVPs encapsulated within a Grouped AVP has
%%   the 'M' (mandatory) bit set, the Grouped AVP itself MUST also
%%   include the 'M' bit set.
%%
%% Both of these texts have problems. If the AVP is unknown then its
%% type is unknown since the type isn't sent over the wire, so the
%% 6733 text becomes a non-statement: don't know that the AVP not
%% setting the M-bit is of type Grouped, therefore can't know that its
%% data consists of encapsulated AVPs, therefore can't but ignore that
%% one of these might set the M-bit. It should be no worse if we know
%% the AVP to have type Grouped.
%%
%% Similarly, for the 3588 text: if we receive an AVP that doesn't set
%% the M-bit and don't know that the AVP has type Grouped then we
%% can't realize that its data contains an AVP that sets the M-bit, so
%% can't regard the AVP as erroneous on this account. Again, it should
%% be no worse if the type is known to be Grouped, but in this case
%% the RFC forces us to regard the AVP as erroneous. This is
%% inconsistent, and the 3588 text has never been enforced.
%%
%% So, if an AVP doesn't set the M-bit then we're free to ignore it,
%% regardless of the AVP's type. If we know the type to be Grouped
%% then we must ignore the M-bit on an encapsulated AVP. That means
%% packing such an encapsulated AVP into an 'AVP' field if need be,
%% not regarding the lack of a specific field as an error as is
%% otherwise the case. (The lack of an AVP-specific field being how we
%% defined the RFC's "unrecognized", which is slightly stronger than
%% "not defined".)

dec(Data, Name, {AvpName, Type}, Mod, Fmt, Opts, Avp) ->
    #{app_dictionary := AppMod, failed_avp := Failed}
        = Opts,

    %% Reset the dictionary for best-effort decode of Failed-AVP.
    Dict = if Failed -> AppMod;
              true   -> Mod
           end,

    dec(Data, Name, AvpName, Type, Mod, Dict, Fmt, Failed, Opts, Avp).

%% dicts/2

dicts(Mod, #{app_dictionary := Mod, avp_dictionaries := Dicts}) ->
    Dicts;

dicts(_, #{app_dictionary := Dict, avp_dictionaries := Dicts}) ->
    [Dict | Dicts];

dicts(Mod, #{app_dictionary := Mod}) ->
    [];

dicts(_, #{app_dictionary := Dict}) ->
    [Dict].

%% dec/10

dec(Data, Name, AvpName, Type, Mod, Dict, Fmt, Failed, Opts, Avp) ->
    try avp(decode, Data, AvpName, Opts, Mod, Dict) of
        V ->
            set(Type, Fmt, Avp, V)
    catch
        throw: {?MODULE, T} ->
            decode_error(Failed, Fmt, T, Avp);
        error: Reason: Stack ->
            decode_error(Failed, Reason, Stack, Name, Mod, Opts, Avp)
    end.

%% dec_AVP/7

dec_AVP([], _, _, _, _, _, Avp) ->
    Avp;

dec_AVP(Dicts, Data, Name, Mod, Fmt, Opts, #diameter_avp{code = Code,
                                                         vendor_id = Vid}
                                           = Avp) ->
    dec_AVP(Dicts, Data, Name, Mod, Fmt, Opts, Code, Vid, Avp).

%% dec_AVP/9
%%
%% Try to decode an AVP in the first alternate dictionary that defines
%% it.

dec_AVP([Dict | Rest], Data, Name, Mod, Fmt, Opts0, Code, Vid, Avp) ->
    case Dict:avp_name(Code, Vid) of
        {AvpName, Type} = NameT ->
            A = Avp#diameter_avp{name = AvpName,
                                 type = Type},
            #{failed_avp := Failed}
                = Opts
                = setopts(NameT, Name, Avp#diameter_avp.is_mandatory, Opts0),
            dec(Data, Name, AvpName, Type, Mod, Dict, Fmt, Failed, Opts, A);
        _ ->
            dec_AVP(Rest, Data, Name, Mod, Fmt, Opts0, Code, Vid, Avp)
    end;

dec_AVP([], _, _, _, _, _, _, _, Avp) ->
    Avp.

%% set/4
%%
%% A Grouped AVP is represented as a #diameter_avp{} list with AVP
%% as head and component AVPs as tail.

set('Grouped', Fmt, Avp, V) ->
    {Rec, As} = V,
    [set(Fmt, Avp, Rec) | As];

set(_, _, Avp, V) ->
    Avp#diameter_avp{value = V}.

%% decode_error/4
%%
%% Error when decoding a grouped AVP.

%% Ignoring errors in Failed-AVP.
decode_error(true, Fmt, {Rec, ComponentAvps, _Errors}, Avp) ->
    [set(Fmt, Avp, Rec) | ComponentAvps];

%% Or not. A faulty component is encoded by itself in Failed-AVP, as
%% suggested by 7.5 of RFC 6733 (quoted below), so that errors are
%% reported unambigiously.
decode_error(false, _, {_, ComponentAvps, [{RC,A} | _]}, Avp) ->
    {RC, [Avp | ComponentAvps], Avp#diameter_avp{data = [A]}}.

%% set/3

set(none, Avp, _Name) ->
    Avp;
set(_, Avp, Rec) ->
    Avp#diameter_avp{value = Rec}.

%% decode_error/7
%%
%% Error when decoding a non-grouped AVP.

decode_error(true, _, _, _, _, _, Avp) ->
    Avp;

decode_error(false, Reason, Stack, Name, Mod, Opts, Avp) ->
    Z = diameter_lib:stacktrace(Stack),
    diameter_lib:log(decode_error,
                     ?MODULE,
                     ?LINE,
                     {Reason, Name, Avp#diameter_avp.name, Mod, Z}),
    case Reason of
        {'DIAMETER', 5014 = RC, _} ->
            %% Length error communicated from diameter_types or a
            %% @custom_types/@codecs module.
            AvpName = Avp#diameter_avp.name,
            {RC, Avp#diameter_avp{data = Mod:empty_value(AvpName, Opts)}};
        _ ->
            {5004, Avp}
    end.

%% 3588/6733:
%%
%%   DIAMETER_INVALID_AVP_VALUE         5004
%%      The request contained an AVP with an invalid value in its data
%%      portion.  A Diameter message indicating this error MUST include
%%      the offending AVPs within a Failed-AVP AVP.

%% avp/6

avp(T, Data, AvpName, Opts, Mod, Mod) ->
    Mod:avp(T, Data, AvpName, Opts);

avp(T, Data, AvpName, Opts, _, Mod) ->
    Mod:avp(T, Data, AvpName, Opts#{module := Mod}).

%% set_strict/3
%%
%% Set false as soon as we see a Grouped AVP that doesn't set the
%% M-bit, to ignore the M-bit on an encapsulated AVP.

set_strict('Grouped', false = M, #{strict_mbit := true} = Opts) ->
    Opts#{strict_mbit := M};
set_strict(_, _, Opts) ->
    Opts.

%% set_failed/2
%%
%% Set true as soon as we see Failed-AVP. Matching on 'Failed-AVP'
%% assumes that this is the RFC AVP. Strictly, this doesn't need to be
%% the case.

set_failed('Failed-AVP', #{failed_avp := false} = Opts) ->
    Opts#{failed_avp := true};
set_failed(_, Opts) ->
    Opts.

%% acc/7

acc([AM | Acc], As, I, Field, Arity, Strict, Mod) ->
    [AM | acc1(Acc, As, I, Field, Arity, Strict, Mod)].

%% acc1/7

%% Faulty AVP, not grouped.
acc1(Acc, {_RC, Avp} = E, _, _, _, _, _) ->
    [Avps, Failed | Rec] = Acc,
    [[Avp | Avps], [E | Failed] | Rec];

%% Faulty component in grouped AVP.
acc1(Acc, {RC, As, Avp}, _, _, _, _, _) ->
    [Avps, Failed | Rec] = Acc,
    [[As | Avps], [{RC, Avp} | Failed] | Rec];

%% Grouped AVP ...
acc1([Avps | Acc], [Avp|_] = As, I, Field, Arity, Strict, Mod) ->
    [[As|Avps] | acc2(Acc, Avp, I, Field, Arity, Strict, Mod)];

%% ... or not.
acc1([Avps | Acc], Avp, I, Field, Arity, Strict, Mod) ->
    [[Avp|Avps] | acc2(Acc, Avp, I, Field, Arity, Strict, Mod)].

%% The component list of a Grouped AVP is discarded when packing into
%% the record (or equivalent): the values in an 'AVP' field are
%% diameter_avp records, not a list of records in the Grouped case,
%% and the decode into the value field is best-effort. The reason is
%% history more than logic: it would probably have made more sense to
%% retain the same structure as in diameter_packet.avps, but an 'AVP'
%% list has always been flat.

%% acc2/7

%% No errors, but nowhere to pack.
acc2(Acc, Avp, _, 'AVP', 0, _, _) ->
    [Failed | Rec] = Acc,
    [[{rc(Avp), Avp} | Failed] | Rec];

%% Relaxed arities.
acc2(Acc, Avp, _, Field, Arity, Strict, Mod)
  when Strict /= decode ->
    pack(Arity, Field, Avp, Mod, Acc);

%% No maximum arity.
acc2(Acc, Avp, _, Field, {_,'*'} = Arity, _, Mod) ->
    pack(Arity, Field, Avp, Mod, Acc);

%% Or check.
acc2(Acc, Avp, I, Field, Arity, _, Mod) ->
    Mx = max_arity(Arity),
    if Mx =< I ->
            [Failed | Rec] = Acc,
            [[{5009, Avp} | Failed] | Rec];
       true ->
            pack(Arity, Field, Avp, Mod, Acc)
    end.

%% 3588/6733:
%%
%%   DIAMETER_AVP_OCCURS_TOO_MANY_TIMES 5009
%%      A message was received that included an AVP that appeared more
%%      often than permitted in the message definition.  The Failed-AVP
%%      AVP MUST be included and contain a copy of the first instance of
%%      the offending AVP that exceeded the maximum number of occurrences

%% max_arity/1

max_arity(1) ->
    1;
max_arity({_,Mx}) ->
    Mx.

%% rc/1

rc(#diameter_avp{is_mandatory = M}) ->
    if M -> 5001; true -> 5008 end.

%% 3588:
%%
%%   DIAMETER_AVP_UNSUPPORTED           5001
%%      The peer received a message that contained an AVP that is not
%%      recognized or supported and was marked with the Mandatory bit.  A
%%      Diameter message with this error MUST contain one or more Failed-
%%      AVP AVP containing the AVPs that caused the failure.
%%
%%   DIAMETER_AVP_NOT_ALLOWED           5008
%%      A message was received with an AVP that MUST NOT be present.  The
%%      Failed-AVP AVP MUST be included and contain a copy of the
%%      offending AVP.

%% pack_arity/5

%% Give Failed-AVP special treatment since (1) it'll contain any
%% unrecognized mandatory AVP's and (2) the RFC 3588 grammar failed to
%% allow for Failed-AVP in an answer-message.

pack_arity(Name, AvpName, _, Mod, M)
  when Name == 'Failed-AVP';
       Name == 'answer-message', AvpName == 'Failed-AVP';
       not M ->
    Mod:avp_arity(Name, 'AVP');
%% Not testing just Name /= 'Failed-AVP' means we're changing the
%% packing of AVPs nested within Failed-AVP, but the point of
%% ignoring errors within Failed-AVP is to decode as much as
%% possible, and failing because a mandatory AVP couldn't be
%% packed into a dedicated field defeats that point.

pack_arity(Name, _, #{strict_mbit := Strict, failed_avp := Failed}, Mod, _)
  when not Strict;
       Failed ->
    Mod:avp_arity(Name, 'AVP');

pack_arity(_, _, _, _, _) ->
    0.

%% avp_arity/5

avp_arity(Name, 'AVP' = AvpName, Mod, Opts, M) ->
    pack_arity(Name, AvpName, Opts, Mod, M);

avp_arity(Name, AvpName, Mod, _, _) ->
    Mod:avp_arity(Name, AvpName).

%% pack/5

pack(Arity, F, Avp, Mod, [Failed | Rec]) ->
    [Failed | set(Arity, F, value(F, Avp), Mod, Rec)].

%% set/5

set(_, _, _, _, Name)
  when is_atom(Name) ->
    Name;

set(1, F, Value, _, Map)
  when is_map(Map) ->
    Map#{F => Value};

set(_, F, V, _, Map)
  when is_map(Map) ->
    maps:update_with(F, fun(Vs) -> [V|Vs] end, [V], Map);

set(1, F, Value, Mod, Rec) ->
    Mod:'#set-'({F, Value}, Rec);

set(_, F, V, Mod, Rec) ->
    Vs = Mod:'#get-'(F, Rec),
    Mod:'#set-'({F, [V|Vs]}, Rec).

%% value/2

value('AVP', Avp) ->
    Avp;

value(_, #diameter_avp{value = V}) ->
    V.

%% ---------------------------------------------------------------------------
%% # grouped_avp/3
%% ---------------------------------------------------------------------------

%% Note that Grouped is the only AVP type that doesn't just return a
%% decoded value, also returning the list of component diameter_avp
%% records.

-spec grouped_avp(decode, avp_name(), binary(), term())
   -> {avp_record(), [avp()]};
                 (encode, avp_name(), avp_record() | avp_values(), term())
   -> iolist()
    | no_return().

%% An error in decoding a component AVP throws the first faulty
%% component, which a catch wraps in the Grouped AVP in question. A
%% partially decoded record is only used when ignoring errors in
%% Failed-AVP.
grouped_avp(decode, Name, Bin, Opts) ->
    {Rec, Avps, Es} = T = decode_avps(Name, Bin, Opts),
    [] == Es orelse ?THROW(T),
    {Rec, Avps};

grouped_avp(encode, Name, Data, Opts) ->
    encode_avps(Name, Data, Opts).

%% 7.5.  Failed-AVP AVP

%%    In the case where the offending AVP is embedded within a Grouped AVP,
%%    the Failed-AVP MAY contain the grouped AVP, which in turn contains
%%    the single offending AVP.  The same method MAY be employed if the
%%    grouped AVP itself is embedded in yet another grouped AVP and so on.
%%    In this case, the Failed-AVP MAY contain the grouped AVP hierarchy up
%%    to the single offending AVP.  This enables the recipient to detect
%%    the location of the offending AVP when embedded in a group.

%% ---------------------------------------------------------------------------
%% # empty_group/2
%% ---------------------------------------------------------------------------

empty_group(Name, #{module := Mod} = Opts) ->
    list_to_binary([z(F, A, Opts, Mod) || {F,A} <- Mod:avp_arity(Name)]).

z(Name, 1, Opts, Mod) ->
    z(Name, Opts, Mod);
z(_, {0,_}, _, _) ->
    [];
z(Name, {Min, _}, Opts, Mod) ->
    binary:copy(z(Name, Opts, Mod), Min).

z('AVP', _, _) ->
    <<0:64>>;  %% minimal header
z(Name, Opts, Mod) ->
    Bin = diameter_codec:pack_data(Mod:avp_header(Name),
                                   Mod:empty_value(Name, Opts)),
    Sz = iolist_size(Bin),
    <<0:Sz/unit:8>>.

%% ---------------------------------------------------------------------------
%% # empty/2
%% ---------------------------------------------------------------------------

empty(Name, #{module := Mod} = Opts) ->
    Mod:avp(encode, zero, Name, Opts).

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

%% newrec/4

newrec(none, _, Name, _) ->
    Name;

newrec(record, Mod, Name, T)
  when T /= decode ->
    RecName = Mod:name2rec(Name),
    Sz = Mod:'#info-'(RecName, size),
    erlang:make_tuple(Sz, [], [{1, RecName}]);

newrec(record, Mod, Name, _) ->
    newrec(Mod, Name);

newrec(_, _, _, _) ->
    #{}.

%% newrec/2

newrec(Mod, Name) ->
    Mod:'#new-'(Mod:name2rec(Name)).

%% reformat/5

reformat(Name, Map, _Strict, Mod, list) ->
    [{F,V} || {F,_} <- Mod:avp_arity(Name), #{F := V} <- [Map]];

reformat(Name, Map, Strict, Mod, record_from_map) ->
    RecName = Mod:name2rec(Name),
    list_to_tuple([RecName | [mget(F, Map, def(A, Strict))
                              || {F,A} <- Mod:avp_arity(Name)]]);

reformat(_, Rec, _, _, _) ->
    Rec.

%% def/2

def(1, decode) ->
    undefined;

def(_, _) ->
    [].
