a cabal implementation in erlang
% SPDX-FileCopyrightText: 2023 Henry Bubert
%
% SPDX-License-Identifier: LGPL-2.1-or-later

-module(cabal_posts).
-export([decode/1, encode/3]).

% Encode

encode(KeyPair, Links, Post) ->
    {PostType, PostBody} = encode_body(Post),
    NumLinks = cabal_wire:encode_varint(length(Links)),
    LinksBin = iolist_to_binary(Links),
    Timestamp = cabal_wire:encode_varint(os:system_time(1000)),
    Payload = iolist_to_binary([NumLinks, LinksBin, PostType, Timestamp, PostBody]),
    #{public := Public, secret := Secret} = KeyPair,
    Signature = enacl:sign_detached(Payload, Secret),
    iolist_to_binary([Public, Signature, Payload]).

% TODO: delete
encode_body({text, Chan, Text}) ->
    ChanBin = unicode:characters_to_binary(Chan),
    ChanLen = cabal_wire:encode_varint(byte_size(ChanBin)),
    TextBin = unicode:characters_to_binary(Text),
    TextLen = cabal_wire:encode_varint(byte_size(TextBin)),
    {0, iolist_to_binary([ChanLen, ChanBin, TextLen, TextBin])};
encode_body({delete, Hashes}) ->
    NumHashes = cabal_wire:encode_varint(length(Hashes)),
    lists:foreach(fun(H) -> 32 = length(H) end, Hashes),
    {1, iolist_to_binary([1, NumHashes, Hashes])};
encode_body({info, {name, Name}}) ->
    NameBin = unicode:characters_to_binary(Name),
    NameLen = cabal_wire:encode_varint(byte_size(NameBin)),
    {2, iolist_to_binary([4, <<"name">>, NameLen, NameBin, 0])};
encode_body({topic, Chan, Topic}) ->
    ChanBin = unicode:characters_to_binary(Chan),
    ChanLen = cabal_wire:encode_varint(byte_size(ChanBin)),
    TopicData =
        case length(Topic) of
            0 ->
                [0];
            _ ->
                TopicBin = unicode:characters_to_binary(Topic),
                TopicLen = cabal_wire:encode_varint(byte_size(TopicBin)),
                [TopicLen, TopicBin]
        end,
    {3, iolist_to_binary([ChanLen, ChanBin] ++ TopicData)};
encode_body({JL, Chan}) when JL =:= join; JL =:= leave ->
    TypeCode =
        case JL of
            join -> 4;
            leave -> 5
        end,
    ChanBin = unicode:characters_to_binary(Chan),
    ChanLen = cabal_wire:encode_varint(byte_size(ChanBin)),
    {TypeCode, iolist_to_binary([ChanLen, ChanBin])}.

%%%%%%%%%%%%
%% Decode %%
%%%%%%%%%%%%

decode(Data) ->
    [Header, Body] = decode_post_header(Data),
    Decoded =
        case proplists:get_value(type, Header) of
            0 -> decode_post_text(Body);
            1 -> decode_post_delete(Body);
            2 -> decode_post_info(Body);
            3 -> decode_post_topic(Body);
            4 -> decode_post_join(Body);
            5 -> decode_post_leave(Body)
        end,
    [Header, Decoded].

decode_post_header(Data) ->
    <<PubKey:32/binary, Signature:64/binary, SignedData/binary>> = Data,
    true = enacl:sign_verify_detached(Signature, SignedData, PubKey),
    {NumLinks, Rest} = cabal_wire:decode_varint(SignedData),
    <<LinkData:(32 * NumLinks)/binary, Rest2/binary>> = Rest,
    Links = [Link || <<Link:32/binary>> <= LinkData],
    case length(Links) =:= NumLinks of
        false ->
            ErrMsg = io_lib:format("invalid num_links - expected ~p but got ~p", [
                NumLinks, length(Links)
            ]),
            erlang:error(lists:flatten(ErrMsg));
        true ->
            [PostType, Timestamp, PostBody] = cabal_wire:decode_varints(Rest2, 2),
            PostHash = enacl:generichash(32, Data),
            [
                [
                    {public_key, PubKey},
                    {links, Links},
                    {type, PostType},
                    {timestamp, Timestamp},
                    {hash, PostHash}
                ],
                PostBody
            ]
    end.

decode_post_text(Body) ->
    {ChannelLen, Rest} = cabal_wire:decode_varint(Body),
    <<Channel:(ChannelLen)/binary, Rest2/binary>> = Rest,
    {TextLen, Rest3} = cabal_wire:decode_varint(Rest2),
    <<Text:(TextLen)/binary>> = Rest3,
    [{channel, Channel}, {text, unicode:characters_to_binary(Text)}].

decode_post_delete(Body) ->
    {NumHashes, Rest} = cabal_wire:decode_varint(Body),
    <<HashData:(32 * NumHashes)/binary>> = Rest,
    Hashes = [Hash || <<Hash:32/binary>> <= HashData],
    [{hashes, Hashes}].

decode_post_info(Body) ->
    KVs = cabal_wire:decode_list_of_binaries(Body),
    [{infos, list_to_map(KVs)}].

decode_post_topic(Body) ->
    {ChannelLen, Rest} = cabal_wire:decode_varint(Body),
    <<Channel:(ChannelLen)/binary, Rest2/binary>> = Rest,
    {TopicLen, Rest3} = cabal_wire:decode_varint(Rest2),
    <<Topic:(TopicLen)/binary>> = Rest3,
    [{channel, Channel}, {topic, Topic}].

decode_post_join(Body) ->
    {ChannelLen, Rest} = cabal_wire:decode_varint(Body),
    <<Channel:(ChannelLen)/binary>> = Rest,
    [{channel, Channel}].

decode_post_leave(Body) ->
    {ChannelLen, Rest} = cabal_wire:decode_varint(Body),
    <<Channel:(ChannelLen)/binary>> = Rest,
    [{channel, Channel}].

%%%% helpers

% turn [A, B, C, D] into #{A => B, C => D}
% thanks https://social.coop/@rogerlipscombe@hachyderm.io/111212196457684597 !
list_to_map(Lst) -> list_to_map(Lst, #{}).
list_to_map([K, V | Rest], Acc) ->
    list_to_map(Rest, Acc#{K => V});
list_to_map([], Acc) ->
    Acc.