%%%----------------------------------------------------------------------
%%% File    : mod_adhoc_api.erl
%%% Author  : Badlop <badlop@process-one.net>
%%% Purpose : Frontend for ejabberd API Commands via XEP-0050 Ad-Hoc Commands
%%% Created : 21 Feb 2025 by Badlop <badlop@process-one.net>
%%%
%%%
%%% ejabberd, Copyright (C) 2002-2026   ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------

%%%% definitions
%% @format-begin

-module(mod_adhoc_api).

-behaviour(gen_mod).

-author('badlop@process-one.net').

%% gen_mod callbacks
-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
%% hooks
-export([adhoc_local_commands/4, adhoc_local_items/4, disco_local_features/5,
         disco_local_identity/5, disco_local_items/5]).

-include("ejabberd_commands.hrl").
-include("ejabberd_sm.hrl").
-include("logger.hrl").
-include("translate.hrl").

-include_lib("stdlib/include/ms_transform.hrl").
-include_lib("xmpp/include/xmpp.hrl").

-define(DEFAULT_API_VERSION, 1000000).

%%%==================================
%%%% gen_mod

start(_Host, _Opts) ->
    {ok,
     [{hook, adhoc_local_commands, adhoc_local_commands, 40},
      {hook, adhoc_local_items, adhoc_local_items, 40},
      {hook, disco_local_features, disco_local_features, 40},
      {hook, disco_local_identity, disco_local_identity, 40},
      {hook, disco_local_items, disco_local_items, 40}]}.

stop(_Host) ->
    ok.

reload(_Host, _NewOpts, _OldOpts) ->
    ok.

mod_opt_type(default_version) ->
    econf:either(
        econf:int(0, 3),
        econf:and_then(
            econf:binary(),
            fun(Binary) ->
               case binary_to_list(Binary) of
                   F when F >= "24.06" ->
                       2;
                   F when (F > "23.10") and (F < "24.06") ->
                       1;
                   F when F =< "23.10" ->
                       0
               end
            end)).

-spec mod_options(binary()) -> [{default_version, integer()}].
mod_options(_) ->
    [{default_version, ?DEFAULT_API_VERSION}].

depends(_Host, _Opts) ->
    [{mod_adhoc, hard}, {mod_last, soft}].

mod_doc() ->
    #{desc =>
          ?T("Execute (def:API commands) "
             "in a XMPP client using "
             "https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. "
             "This module requires _`mod_adhoc`_ (to execute the commands), "
             "and recommends _`mod_disco`_ (to discover the commands)."),
      note => "added in 25.03",
      opts =>
          [{default_version,
            #{value => "integer() | string()",
              desc =>
                  ?T("What API version to use. "
                     "If setting an ejabberd version, it will use the latest API "
                     "version that was available in that (def:c2s) ejabberd version. "
                     "For example, setting '\"24.06\"' in this option implies '2'. "
                     "The default value is the latest version.")}}],
      example =>
          ["acl:",
           "  admin:",
           "    user: jan@localhost",
           "",
           "api_permissions:",
           "  \"adhoc commands\":",
           "    from: mod_adhoc_api",
           "    who: admin",
           "    what:",
           "      - \"[tag:roster]\"",
           "      - \"[tag:session]\"",
           "      - stats",
           "      - status",
           "",
           "modules:",
           "  mod_adhoc_api:",
           "    default_version: 2"]}.

%%%==================================
%%%% Ad-Hoc Commands (copied from mod_configure)

-define(INFO_IDENTITY(Category, Type, Name, Lang),
        [#identity{category = Category,
                   type = Type,
                   name = tr(Lang, Name)}]).
-define(INFO_COMMAND(Name, Lang),
        ?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)).
-define(NODE(Name, Node),
        #disco_item{jid = jid:make(Server),
                    node = Node,
                    name = tr(Lang, Name)}).

-spec tokenize(binary()) -> [binary()].
tokenize(Node) ->
    str:tokens(Node, <<"/#">>).

-spec tr(binary(), binary()) -> binary().
tr(Lang, Text) ->
    translate:translate(Lang, Text).

%%%==================================
%%%% - disco identity

-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) ->
                              [identity()].
disco_local_identity(Acc, _From, #jid{lserver = LServer} = _To, Node, Lang) ->
    case tokenize(Node) of
        [<<"api-commands">>] ->
            ?INFO_COMMAND(?T("API Commands"), Lang);
        [<<"api-commands">>, CommandName] ->
            ?INFO_COMMAND(get_api_command_desc(CommandName, LServer), Lang);
        _ ->
            Acc
    end.

get_api_command_desc(NameAtom, Host) ->
    iolist_to_binary((get_api_command(NameAtom, Host))#ejabberd_commands.desc).

%%%==================================
%%%% - disco features

-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) ->
                              mod_disco:features_acc().
disco_local_features(Acc, _From, #jid{lserver = LServer} = _To, Node, _Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
        false ->
            Acc;
        _ ->
            case tokenize(Node) of
                [<<"api-commands">>] ->
                    {result, []};
                [<<"api-commands">>, _] ->
                    {result, [?NS_COMMANDS]};
                _ ->
                    Acc
            end
    end.

%%%==================================
%%%% - adhoc items

-spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) ->
                           mod_disco:items_acc().
adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) ->
    Items =
        case Acc of
            {result, Its} ->
                Its;
            empty ->
                []
        end,
    Nodes = recursively_get_local_items(From, global, LServer, <<"">>, Server, Lang),
    Nodes1 =
        lists:filter(fun(#disco_item{node = Nd}) ->
                        F = disco_local_features(empty, From, To, Nd, Lang),
                        case F of
                            {result, [?NS_COMMANDS]} ->
                                true;
                            _ ->
                                false
                        end
                     end,
                     Nodes),
    {result, Items ++ Nodes1}.

-spec recursively_get_local_items(jid(),
                                  global | vhost,
                                  binary(),
                                  binary(),
                                  binary(),
                                  binary()) ->
                                     [disco_item()].
recursively_get_local_items(From, PermLev, LServer, Node, Server, Lang) ->
    Items =
        case get_local_items2(From, {PermLev, LServer}, tokenize(Node), Server, Lang) of
            {result, Res} ->
                Res;
            {error, _Error} ->
                []
        end,
    lists:flatten(
        lists:map(fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
                     if (S /= Server) or (Nd == <<"">>) ->
                            [];
                        true ->
                            [Item,
                             recursively_get_local_items(From, PermLev, LServer, Nd, Server, Lang)]
                     end
                  end,
                  Items)).

%%%==================================
%%%% - disco items

-spec disco_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) ->
                           mod_disco:items_acc().
disco_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) ->
    case gen_mod:is_loaded(LServer, mod_adhoc) of
        false ->
            Acc;
        _ ->
            Items =
                case Acc of
                    {result, Its} ->
                        Its;
                    empty ->
                        [];
                    Other ->
                        Other
                end,
            case tokenize(Node) of
                LNode when (LNode == [<<"api-commands">>]) or (LNode == []) ->
                    case get_local_items2(From, {global, LServer}, LNode, jid:encode(To), Lang) of
                        {result, Res} ->
                            {result, Res};
                        {error, Error} ->
                            {error, Error}
                    end;
                _ ->
                    {result, Items}
            end
    end.

%%%==================================
%%%% - get_local_items2

-spec get_local_items2(jid(),
                       {global | vhost, binary()},
                       [binary()],
                       binary(),
                       binary()) ->
                          {result, [disco_item()]} | {error, stanza_error()}.
get_local_items2(_From, _Host, [], Server, Lang) ->
    {result, [?NODE(?T("API Commands"), <<"api-commands">>)]};
get_local_items2(From, {_, Host}, [<<"api-commands">>], _Server, Lang) ->
    {result, get_api_commands(From, Host, Lang)};
get_local_items2(_From, {_, _Host}, [<<"api-commands">>, _], _Server, _Lang) ->
    {result, []};
get_local_items2(_From, _Host, _, _Server, _Lang) ->
    {error, xmpp:err_item_not_found()}.

-spec get_api_commands(jid(), binary(), binary()) -> [disco_item()].
get_api_commands(From, Server, Lang) ->
    ApiVersion = mod_adhoc_api_opt:default_version(Server),
    lists:map(fun({Name, _Args, _Desc}) ->
                 NameBin = list_to_binary(atom_to_list(Name)),
                 NiceNameBin = ejabberd_web_admin:nice_this(NameBin),
                 ?NODE(NiceNameBin, <<"api-commands/", NameBin/binary>>)
              end,
              ejabberd_commands:list_commands(ApiVersion, get_caller_info(From))).

%%%==================================
%%%% - adhoc commands

-define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang),
        adhoc_local_commands(From, To, Request)).

-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) ->
                              adhoc_command() | {error, stanza_error()}.
adhoc_local_commands(Acc, From, To, #adhoc_command{node = Node} = Request) ->
    case tokenize(Node) of
        [<<"api-commands">>, _CommandName] ->
            ?COMMANDS_RESULT(LServer, From, To, Request, Lang);
        _ ->
            Acc
    end.

-spec adhoc_local_commands(jid(), jid(), adhoc_command()) ->
                              adhoc_command() | {error, stanza_error()}.
adhoc_local_commands(From,
                     #jid{lserver = LServer} = _To,
                     #adhoc_command{lang = Lang,
                                    node = Node,
                                    sid = SessionID,
                                    action = Action,
                                    xdata = XData} =
                         Request) ->
    LNode = tokenize(Node),
    ActionIsExecute = Action == execute orelse Action == complete,
    if Action == cancel ->
           #adhoc_command{status = canceled,
                          lang = Lang,
                          node = Node,
                          sid = SessionID};
       XData == undefined, ActionIsExecute ->
           case get_form(LServer, LNode, Lang) of
               {result, Form} ->
                   xmpp_util:make_adhoc_response(Request,
                                                 #adhoc_command{status = executing, xdata = Form});
               {error, Error} ->
                   {error, Error}
           end;
       XData /= undefined, ActionIsExecute ->
           case set_form(From, LServer, LNode, Lang, XData) of
               {result, Res} ->
                   xmpp_util:make_adhoc_response(Request,
                                                 #adhoc_command{xdata = Res, status = completed});
               %%{'EXIT', _} -> {error, xmpp:err_bad_request()};
               {error, Error} ->
                   {error, Error}
           end;
       true ->
           {error, xmpp:err_bad_request(?T("Unexpected action"), Lang)}
    end.

-spec get_form(binary(), [binary()], binary()) ->
                  {result, xdata()} | {error, stanza_error()}.
get_form(Host, [<<"api-commands">>, CommandName], Lang) ->
    get_form_api_command(CommandName, Host, Lang);
get_form(_Host, _, _Lang) ->
    {error, xmpp:err_service_unavailable()}.

-spec set_form(jid(), binary(), [binary()], binary(), xdata()) ->
                  {result, xdata() | undefined} | {error, stanza_error()}.
set_form(From, Host, [<<"api-commands">>, Command], Lang, XData) ->
    set_form_api_command(From, Host, Command, XData, Lang);
set_form(_From, _Host, _, _Lang, _XData) ->
    {error, xmpp:err_service_unavailable()}.

%%%==================================
%%%% API Commands

get_api_command(Name, Host) when is_binary(Name) ->
    get_api_command(binary_to_existing_atom(Name, latin1), Host);
get_api_command(Name, Host) when is_atom(Name) ->
    ApiVersion = mod_adhoc_api_opt:default_version(Host),
    ejabberd_commands:get_command_definition(Name, ApiVersion).

get_caller_info(#jid{user = User, server = Server} = From) ->
    #{tag => <<>>,
      usr => {User, Server, <<"">>},
      caller_server => Server,
      ip => get_ip_address(From),
      caller_module => ?MODULE}.

get_ip_address(#jid{user = User,
                    server = Server,
                    resource = Resource}) ->
    case ejabberd_sm:get_user_ip(User, Server, Resource) of
        {IP, _Port} when is_tuple(IP) ->
            IP;
        _ ->
            error_ip_address
    end.

%%%==================================
%%%% - get form

get_form_api_command(NameBin, Host, _Lang) ->
    Def = get_api_command(NameBin, Host),
    Title = ejabberd_web_admin:nice_this(Def#ejabberd_commands.name),
    Instructions = get_instructions(Def),
    FieldsArgs =
        build_fields(Def#ejabberd_commands.args,
                     Def#ejabberd_commands.args_desc,
                     Def#ejabberd_commands.args_example,
                     Def#ejabberd_commands.policy,
                     get_replacements(Host),
                     true),
    FieldsArgsWithHeads =
        case FieldsArgs of
            [] ->
                [];
            _ ->
                [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
        end,
    NodeFields = build_node_fields(),
    {result,
     #xdata{title = Title,
            type = form,
            instructions = Instructions,
            fields = FieldsArgsWithHeads ++ NodeFields}}.

get_replacements(Host) ->
    [{user, <<"">>},
     {localuser, <<"">>},
     {host, Host},
     {localhost, Host},
     {password, <<"">>},
     {newpass, <<"">>},
     {service, mod_muc_admin:find_hosts(Host)}].

build_node_fields() ->
    build_node_fields([node() | nodes()]).

build_node_fields([_ThisNode]) ->
    [];
build_node_fields(AtomNodes) ->
    [ThisNode | _] = Nodes = [atom_to_binary(Atom, latin1) || Atom <- AtomNodes],
    Options = [#xdata_option{label = N, value = N} || N <- Nodes],
    [#xdata_field{type = fixed, label = ?T("Clustering")},
     #xdata_field{type = 'list-single',
                  label = <<"ejabberd node">>,
                  var = <<"mod_adhoc_api_target_node">>,
                  values = [ThisNode],
                  options = Options}].

%%%==================================
%%%% - set form

set_form_api_command(From, Host, CommandNameBin, XData, _Lang) ->
    %% Description
    Def = get_api_command(CommandNameBin, Host),
    Title = ejabberd_web_admin:nice_this(Def#ejabberd_commands.name),
    Instructions = get_instructions(Def),

    %% Arguments
    FieldsArgs0 = [Field || Field <- XData#xdata.fields, Field#xdata_field.type /= fixed],
    FieldsArgs1 =
        lists:map(fun(Arg) ->
                     case Arg#xdata_field.values of
                         [_] ->
                             Arg;
                         _ ->
                             Arg#xdata_field{type = 'text-multi'}
                     end
                  end,
                  FieldsArgs0),

    {Node, FieldsArgs} =
        case lists:keytake(<<"mod_adhoc_api_target_node">>, #xdata_field.var, FieldsArgs1) of
            {value, #xdata_field{values = [TargetNode]}, FAs} ->
                {binary_to_existing_atom(TargetNode, latin1), FAs};
            false ->
                {node(), FieldsArgs1}
        end,

    FieldsArgsWithHeads =
        case FieldsArgs of
            [] ->
                [];
            _ ->
                [#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
        end,

    %% Execute
    Arguments = api_extract_fields(FieldsArgs, Def#ejabberd_commands.args),
    ApiVersion = mod_adhoc_api_opt:default_version(Host),
    CallResult =
        execute(Def,
                [Node,
                 mod_http_api,
                 handle,
                 [binary_to_existing_atom(CommandNameBin, latin1),
                  get_caller_info(From),
                  Arguments,
                  ApiVersion]]),

    %% Command result
    FieldsResult2 =
        case CallResult of
            {200, RR} ->
                build_fields([Def#ejabberd_commands.result],
                             [Def#ejabberd_commands.result_desc],
                             [RR],
                             restricted,
                             [{host, Host}],
                             false);
            {Code, _ApiErrorCode, MessageBin} ->
                [#xdata_field{type = 'text-single',
                              label = <<"Error ", (integer_to_binary(Code))/binary>>,
                              values = encode(MessageBin, irrelevat_type),
                              var = <<"error">>}];
            {Code, MessageBin} ->
                [#xdata_field{type = 'text-single',
                              label = <<"Error ", (integer_to_binary(Code))/binary>>,
                              values = encode(MessageBin, irrelevat_type),
                              var = <<"error">>}]
        end,
    FieldsResultWithHeads =
        [#xdata_field{type = fixed, label = <<"">>},
         #xdata_field{type = fixed, label = ?T("Result")}
         | FieldsResult2],

    %% Result stanza
    {result,
     #xdata{title = Title,
            type = result,
            instructions = Instructions,
            fields = FieldsArgsWithHeads ++ FieldsResultWithHeads}}.

execute(Def, Arguments) ->
    case lists:member(async, Def#ejabberd_commands.tags) of
        true ->
            spawn(ejabberd_cluster, call, Arguments),
            {200, 0};
        false ->
            apply(ejabberd_cluster, call, Arguments)
    end.

api_extract_fields(Fields, ArgsDef) ->
    lists:map(fun(#xdata_field{values = Values, var = ANameBin}) ->
                 ArgDef = proplists:get_value(binary_to_existing_atom(ANameBin, latin1), ArgsDef),
                 V = case {Values, ArgDef} of
                         {Values, {list, {_ElementName, {tuple, ElementsDef}}}} ->
                             [parse_tuple(ElementsDef, Value) || Value <- Values];
                         {[Value], {tuple, ElementsDef}} ->
                             parse_tuple(ElementsDef, Value);
                         {[Value], _} ->
                             Value;
                         _ ->
                             Values
                     end,
                 {ANameBin, V}
              end,
              Fields).

parse_tuple(ElementsDef, Value) ->
    Values = str:tokens(Value, <<":">>),
    List1 =
        [{atom_to_binary(Name, latin1), Val}
         || {{Name, _Type}, Val} <- lists:zip(ElementsDef, Values)],
    maps:from_list(List1).

%%%==================================
%%%% - get instructions

get_instructions(Def) ->
    Note2 =
        case Def#ejabberd_commands.note of
            [] ->
                [];
            Note ->
                N = iolist_to_binary(Note),
                [<<"Note: ", N/binary>>]
        end,
    Tags2 =
        case Def#ejabberd_commands.tags of
            [] ->
                [];
            Tags ->
                T = str:join([atom_to_binary(Tag, latin1) || Tag <- Tags], <<", ">>),
                [<<"Tags: ", T/binary>>]
        end,
    Module2 =
        case Def#ejabberd_commands.definer of
            unknown ->
                [];
            DefinerAtom ->
                D = atom_to_binary(DefinerAtom, latin1),
                [<<"Module: ", D/binary>>]
        end,
    Version2 =
        case Def#ejabberd_commands.version of
            0 ->
                [];
            Version ->
                V = integer_to_binary(Version),
                [<<"API version: ", V/binary>>]
        end,
    get_instructions2([Def#ejabberd_commands.desc, Def#ejabberd_commands.longdesc]
                      ++ Note2
                      ++ Tags2
                      ++ Module2
                      ++ Version2).

get_instructions2(ListStrings) ->
    [re:replace(String, "[\t]*[  ]+", " ", [{return, binary}, global])
     || String <- ListStrings, String /= ""].

%%%==================================
%%%% - build fields

build_fields(NameTypes, none, Examples, Policy, Replacements, Required) ->
    build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
build_fields(NameTypes, Descs, none, Policy, Replacements, Required) ->
    build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
build_fields(NameTypes, [none], Examples, Policy, Replacements, Required) ->
    build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
build_fields(NameTypes, Descs, [none], Policy, Replacements, Required) ->
    build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
build_fields(NameTypes, Descs, Examples, Policy, Replacements, Required) ->
    {NameTypes2, Descs2, Examples2} =
        case Policy of
            user ->
                {[{user, binary}, {host, binary} | NameTypes],
                 ["Username", "Server host" | Descs],
                 ["tom", "example.com" | Examples]};
            _ ->
                {NameTypes, Descs, Examples}
        end,
    build_fields2(NameTypes2, Descs2, Examples2, Replacements, Required).

build_fields2([{_ArgName, {list, ArgNameType}}] = NameTypes,
              Descs,
              Examples,
              _Replacements,
              Required) ->
    ArgNameType2 =
        case ArgNameType of
            {jid, _} ->
                'jid-multi';
            {_, _} ->
                'text-multi'
        end,
    Args = lists_zip3_pad(NameTypes, Descs, Examples),
    lists:map(fun({{AName, AType}, ADesc, AExample}) ->
                 ANameBin = list_to_binary(atom_to_list(AName)),
                 #xdata_field{type = ArgNameType2,
                              label = ANameBin,
                              desc = list_to_binary(ADesc),
                              values = encode(AExample, AType),
                              required = Required,
                              var = ANameBin}
              end,
              Args);
build_fields2(NameTypes, Descs, Examples, Replacements, Required) ->
    Args = lists_zip3_pad(NameTypes, Descs, Examples),
    lists:map(fun({{AName, AType}, ADesc, AExample}) ->
                 ANameBin = list_to_binary(atom_to_list(AName)),
                 AValue = proplists:get_value(AName, Replacements, AExample),
                 Values = encode(AValue, AType),
                 Type =
                     case {AType, Values} of
                         {{list, _}, _} ->
                             'text-multi';
                         {string, [_, _ | _]} ->
                             'text-multi';
                         _ ->
                             'text-single'
                     end,
                 #xdata_field{type = Type,
                              label = ANameBin,
                              desc = make_desc(ADesc, AValue),
                              values = Values,
                              required = Required,
                              var = ANameBin}
              end,
              Args).

-ifdef(OTP_BELOW_26).

lists_zip3_pad(As, Bs, Cs) ->
    lists_zip3_pad(As, Bs, Cs, []).

lists_zip3_pad([A | As], [B | Bs], [C | Cs], Xs) ->
    lists_zip3_pad(As, Bs, Cs, [{A, B, C} | Xs]);
lists_zip3_pad([A | As], [B | Bs], Nil, Xs) when (Nil == none) or (Nil == []) ->
    lists_zip3_pad(As, Bs, [], [{A, B, ""} | Xs]);
lists_zip3_pad([A | As], Nil, [C | Cs], Xs) when (Nil == none) or (Nil == []) ->
    lists_zip3_pad(As, [], Cs, [{A, "", C} | Xs]);
lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
    lists_zip3_pad(As, [], [], [{A, "", ""} | Xs]);
lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
    lists:reverse(Xs).

-endif.

-ifndef(OTP_BELOW_26).

lists_zip3_pad(As, Bs, Cs) ->
    lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}).

-endif.

make_desc(ADesc, T) when is_tuple(T) ->
    T3 = string:join(tuple_to_list(T), " : "),
    iolist_to_binary([ADesc, " {", T3, "}"]);
make_desc(ADesc, M) when is_map(M) ->
    M2 = [binary_to_list(V) || V <- maps:keys(M)],
    M3 = string:join(M2, " : "),
    iolist_to_binary([ADesc, " {", M3, "}"]);
make_desc(ADesc, _M) ->
    iolist_to_binary(ADesc).

%%%==================================
%%%% - encode

encode({[T | _] = List}, Type) when is_tuple(T) ->
    encode(List, Type);
encode([T | _] = List, Type) when is_tuple(T) ->
    [encode(Element, Type) || Element <- List];
encode(T, _Type) when is_tuple(T) ->
    T2 = [x_to_binary(E) || E <- tuple_to_list(T)],
    T3 = str:join(T2, <<":">>),
    [T3];
encode(M, {tuple, Types}) when is_map(M) ->
    M2 = [x_to_list(maps:get(atom_to_binary(Key, latin1), M))
          || {Key, _ElementType} <- Types],
    M3 = string:join(M2, " : "),
    [iolist_to_binary(M3)];
encode([S | _] = SList, _Type) when is_list(S) ->
    [iolist_to_binary(A) || A <- SList];
encode([B | _] = BList, _Type) when is_binary(B) ->
    BList;
encode(I, _Type) when is_integer(I) ->
    [integer_to_binary(I)];
encode([M | _] = List, {list, {_Name, TupleType}}) when is_map(M) ->
    [encode(M1, TupleType) || M1 <- List];
encode(S, _Type) when is_list(S) ->
    [iolist_to_binary(S)];
encode(B, _Type) when is_binary(B) ->
    str:tokens(B, <<"\n">>).

x_to_list(B) when is_binary(B) ->
    binary_to_list(B);
x_to_list(I) when is_integer(I) ->
    integer_to_list(I);
x_to_list(L) when is_list(L) ->
    L.

x_to_binary(B) when is_binary(B) ->
    B;
x_to_binary(I) when is_integer(I) ->
    integer_to_binary(I);
x_to_binary(L) when is_list(L) ->
    iolist_to_binary(L).

%%%==================================

%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
