 *
 * As far as I can tell, AWCP isn't formally defined anywhere. I
 * figured this out by fuzzing the messaging protocol. So I suppose
 * this is a candidate for a formal definition.
 *
 * This currently does not implement the full kitchen sink
 * functionality, only what is needed for the limited functionality that
 * sidekick provides.  That said, the framework and design pattern laid
 * out here can easily be extended to implement the entire kitchen sink.
 *
 * The pattern here is to define all of the types involved. At the end,
 * an interface called `AWCP_Aepp` is defined. This enumerates all of
 * the functions that you an aepp needs to have defined in order to do
 * stuff with a waellet.
 *
 * The functions are listed in the order that they are used in practice.
 * So for instance,
 *
 *  - before you can `connection.open` with the waellet, you must wait
 *    for the waellet to `connection.announcePresence`
 *  - before you can `address.subscribe` the waellet, you must wait
 *    for the waellet to `connection.open`
 *
 * An implementation of `AWCP_Aepp` is given in the `msgr.ts` file in
 * this directory. In particular, msgr implements the "selective ignore"
 * special behavior needed to deal with `connection.announcePresence`.
 *
 * `skylight.ts` (parent directory) includes some convenience functions
 * wrapped on top of msgr. In particular, it black-boxes away things
 * like "increment the message id each time you send a new message"
 *
 * Moreover, `skylight.ts` includes some subset of porcelain (dwim)
 * functions like "just connect to the wallet, do what I mean", which
 * does the "wait for `connection.announcePresence`, then do
 * `connection.open`, then do `address.subscribe`" dance.
 *
 * Crucially, skylight only contains porcelain functions that are of the
 * flavor of black-boxing away complexity related to talking to the
 * waellet. For instance, Skylight will never directly communicate with
 * a node.
 *
 * This "design pattern" of "define the types for a messaging protocol,
 * and separately implement it, then black-box away the complexity in a
 * porcelain module" will probably also be done for talking to a node
 * and talking to a compiler.
 *
 * sidekick.ts (parent directory) includes programmer-facing porcelain
 * functions such as "I just want to perform a transaction". In other
 * words, sidekick.ts black-boxes away the complexity in coordinating
 * between the compiler, the node, and the waellet.
 * This is entirely types and type definitions
 *
 * See:
 *  - JSON RPC 2.0 definition: https://www.jsonrpc.org/specification
 *  - Typescript generics: https://www.typescriptlang.org/docs/handbook/2/generics.html
 *
 * @module
 */


%       Every operation, calculation, and concept, no matter how
%       arbitrarily complex, reduces to adding integers together.
%       There are no new concepts in QAnal. Everything is just
%       putting lipstick on adding integers together.
%
%   -- Dr. Ajay Kumar PHD, The Founder
%
% a word is the smallest unit in a reduced sum. In for instance
% 1 + a + ab, the words are 1, a, and ab, which are represented as
% the sets {}, {a}, and {a, b}, respectively.
%
% - a word is a tuple {w, SetOfWFChars}
%   - the empty set means 1
%
% - a wfchar is a Binary
%   - if you wish to use pf/1, the binary must be string-formattable
%
% in WF algebra, anything times itself equals itself, therefore we
% don't need to keep track of exponents. That is why the set
% representation makes sense.
%
% with a word, multiplication is implied
% with a sentence, summation is implied
-module(wfc_word).
-vsn("1.0.0").

-export_type([
    wfchar/0,
    word/0
]).
-export([
    one/0,
    is_one/1,
    is_valid_word/1,
    from_binary/1,
    from_list/1,
    to_list/1,
    times/1,
    times/2,
    pf/1,
    pp/1
]).

-type wfchar() :: binary().
-type word()   :: {w, sets:set(wfchar())}.


%%% API


-spec one() -> word().
% @doc The word corresponding to the concept "1"; it is a tagged
% tuple of {w, EmptySet}.

one() ->
    {w, sets:new()}.



-spec is_one(term()) -> boolean().
% @doc a word is one if it {w, EmptySet}.

is_one(Word) ->
    Word =:= one().



-spec is_valid_word(term()) -> boolean().
% @doc
% a word is valid if exactly one of these conditions are true:
%
%   - is empty
%   - contains only valid wfchars
%
% return false on anything failing to pattern match {w, Set}

is_valid_word({w, Set}) ->
    Chars = sets:to_list(Set),
    lists:all(fun is_valid_char/1, Chars);
is_valid_word(_) ->
    false.

is_valid_char(X) ->
    is_binary(X).



-spec from_binary(binary()) -> word().
% @doc
% Convert a binary into a word

from_binary(Bin) when is_binary(Bin) ->
    Set = sets:from_list([Bin]),
    Word = {w, Set},
    true = is_valid_word(Word),
    Word.



-spec from_list([binary()]) -> word().
% @doc
% Given a list of binaries, take their "product" and put it into a
% word.

from_list(Binaries) ->
    Set = sets:from_list(Binaries),
    ResultWord = {w, Set},
    true = is_valid_word(ResultWord),
    ResultWord.



-spec to_list(word()) -> [binary()].
% @doc
% pull out the set in the tagged tuple, convert it to a list, and
% return the SORTED list of BINARIES
% @end

to_list({w, Set}) ->
    Chars = sets:to_list(Set),
    lists:sort(Chars).



-spec times([word()]) -> word().
% @doc product of a list of words

times(Words) ->
    Result = times_acc(Words, one()),
    true = is_valid_word(Result),
    Result.


times_acc([], FinalAcc) ->
    FinalAcc;
times_acc([W | Ws], Acc) ->
    NewAcc = times(W, Acc),
    times_acc(Ws, NewAcc).



-spec times(word(), word()) -> word().
% @doc
% Multiply two words. This amounts to just taking the union of the
% characters contained in the words
% @end

times({w, L}, {w, R}) ->
    % take the unions of the things it contains
    LR = sets:union(L, R),
    Word = {w, LR},
    true = is_valid_word(Word),
    Word.



-spec pp(word()) -> ok.
% @doc pretty print a word (wraps an io:format/2 call around pf/1).

pp(Word) ->
    io:format("~ts~n", [pf(Word)]).



-spec pf(word()) -> iolist().
% @doc
% returns iolist
%
% "(*)"         if word is 1
% "(* a b c)"   if word is the set containing {a,b,c}

pf(Word) ->
    true = is_valid_word(Word),
    Chars = to_list(Word),
    Strs = pf_wfchars(Chars, []),
    ["(*", Strs, ")"].

pf_wfchars([Binary | Rest], Accum) when is_binary(Binary) ->
    BinStr = io_lib:format("~s", [Binary]),
    NewAccum = [Accum, " ", BinStr],
    pf_wfchars(Rest, NewAccum);
pf_wfchars([], Accum) ->
    Accum.

