#!/usr/bin/env escript

-mode(compile).
-include("./ansi.hrl").

% ho from pimp pov
-record(pho,
        {pid   = none  :: none | pid(),
         ref   = none  :: none | reference(),
         taken = false :: boolean(),
         name  = none  :: none | string()}).

%% pimp state
-record(ps,
        {lsock    = none            :: gen_tcp:socket(),
         hos      = []              :: [#pho{}],
         ho_names = init_ho_names() :: [string()]}).


%% ho state
-record(hs,
        {name  = none :: string(),
         daddy = none :: pid(),
         asock = none :: gen_tcp:socket(),
         nsock = none :: enoise:noise_socket()}).


main([]) ->
    p_init(6969);
main([PortNum]) ->
    p_init(list_to_integer(PortNum)).


% static keypair is static, ephemeral is ephemeral
noise_options() ->
    Secret = crypto:hash(sha3_256, <<"alice">>),
    Public = <<116, 159, 91, 248, 138, 250, 73, 40, 231, 50, 81, 110,
               137, 163, 44, 76, 48, 130, 225, 95, 168, 121, 93, 44,
               148, 42, 180, 103, 11, 40, 168, 96>>,
    SKP = enoise_keypair:new(dh25519, Secret, Public),
    EKP = enoise_keypair:new(dh25519),
    [{noise, "Noise_NK_25519_ChaChaPoly_BLAKE2b"},
     {s, SKP},
     {e, EKP}].


% pimp process startup
p_init(Port) ->
    p_logln("pimp startup (p_init)"),
    Opts = [binary, {packet, 0}, {active, true}],
    case gen_tcp:listen(Port, Opts) of
        {ok, LSock} ->
            ok = p_logfln("pimp starting: ~tp", [self()]),
            PS_I = #ps{lsock = LSock},
            PS_II = p_spawn_ho(PS_I),
            p_loop(PS_II);
        {error, closed} ->
            %% dont be pimp
            p_logln("listen socket closed")
    end.

p_loop(PS = #ps{hos = Hos}) ->
    receive
        {ho, HoPid, taken} ->
            NewPS = p_taken(HoPid, PS),
            p_loop(NewPS);
        {ho, HoPid, sent, Data} ->
            HoName = p_ho_name(HoPid, PS),
            SendMessage = io_lib:format("~ts> ~ts\r\n", [HoName, Data]),
            ok = p_echo_except(HoPid, HoName, SendMessage, Hos),
            p_loop(PS);
        Down = {'DOWN', _, _, _, _} ->
            NewPS = p_ho_down(Down, PS),
            p_loop(NewPS);
        Unknown ->
            ok = p_logfln("unknown message: ~tw", [Unknown]),
            p_loop(PS)
    end.

p_taken(Pid, PS = #ps{hos = Hos}) ->
    Hos_II = mark_taken(Pid, Hos, []),
    PS_II  = PS#ps{hos = Hos_II},
    PS_III = p_spawn_ho(PS_II),
    PS_III.

mark_taken(Pid, [Ho = #pho{pid = Pid} | Rest], Acc) ->
    NewHo = Ho#pho{taken = true},
    lists:reverse(Acc) ++ [NewHo | Rest];
mark_taken(Pid, [WrongHo | Rest], Acc) ->
    mark_taken(Pid, Rest, [WrongHo | Acc]);
mark_taken(_, [], Acc) ->
    lists:reverse(Acc).

% given pid of ho, get her name
p_ho_name(HoPid, #ps{hos = Hos}) ->
    case lists:keyfind(HoPid, #pho.pid, Hos) of
        #pho{name = Name} -> Name;
        false             -> "JaneDoe"
    end.

% pimp spawns new ho
p_spawn_ho(PS = #ps{lsock = LSock, hos = Hos, ho_names = Names}) ->
    % new name for ho, initial ho state
    {N, NewNames} = recycle(Names),
    InitHS = #hs{name = N, daddy = self()},
    % spawn the new ho
    ok = p_logfln("spawning new ho: ~ts", [N]),
    SpawnHo = fun() -> h_init(LSock, InitHS) end,
    {HPid, HRef} = erlang:spawn_monitor(SpawnHo),
    % new ho record from Pimp POV
    NewHo = #pho{pid = HPid, ref = HRef, name = N},
    ok = p_logfln("spawned new ho: ~tw", [NewHo]),
    NewPS = PS#ps{hos = [NewHo | Hos], ho_names = NewNames},
    NewPS.

% ho down, remove her from roster
p_ho_down({'DOWN', _Ref, process, Pid, Info}, PS = #ps{hos = Hos}) ->
    Ho = lists:keyfind(Pid, #pho.pid, Hos),
    ok = p_logfln("ho down: ~ts; reason: ~tw", [Ho#pho.name, Info]),
    NewHos = lists:keydelete(Pid, #pho.pid, Hos),
    NewPS = PS#ps{hos = NewHos},
    NewPS.


% echo message from one ho to all the rest
% skip if equal
p_echo_except(SrcPid, SrcName, Data, [#pho{pid = SrcPid} | Rest]) ->
    p_echo_except(SrcPid, SrcName, Data, Rest);
% skip if not taken
p_echo_except(SrcPid, SrcName, Data, [#pho{taken = false} | Rest]) ->
    p_echo_except(SrcPid, SrcName, Data, Rest);
p_echo_except(SrcPid, SrcName, Data, [#pho{pid = DstPid, taken = true} | Rest]) ->
    DstPid ! {pimp, send, Data},
    p_echo_except(SrcPid, SrcName, Data, Rest);
p_echo_except(_, _, _, []) ->
    ok.

%% runs in ho context
h_init(LSock, HS = #hs{name = Name, daddy = Daddy, asock = none, nsock = none}) ->
    h_logln(Name, "h_init"),
    % this is key: accept needs to run in ho context, otherwise she
    % doesn't own the acceptor socket
    %
    % blocks until client connects
    case gen_tcp:accept(LSock) of
        {ok, ASock} ->
            h_logln(Name, "taken"),
            % tell daddy to spawn the next ho
            Daddy ! {ho, self(), taken},
            {ok, NSock, _} = enoise:accept(ASock, noise_options()),
            Welcome = io_lib:format("HI! I'm ~ts. What's your name?\r\n",
                                    [Name]),
            ok = h_send(Name, NSock, Welcome),
            HS_II = HS#hs{asock = ASock, nsock = NSock},
            h_loop(HS_II);
        {error, closed} ->
            h_logln(Name, "didn't get taken")
    end.


h_loop(HS = #hs{name = Name, daddy = Daddy, asock = ASock, nsock = NSock}) ->
    %ok = inet:setopts(ASock, [{active, once}]),
    receive
        {noise, NSock, Data} ->
            Data_II = string:chomp(Data),
            ok = h_rcvd(Name, Data_II),
            Daddy ! {ho, self(), sent, Data_II},
            h_loop(HS);
        {pimp, send, Data} = M ->
            io:format("~p received ~p~n", [self(), M]),
            ok = h_send(Name, NSock, Data),
            h_loop(HS);
        {tcp_closed, ASock} ->
            exit(its_fine);
        Unknown ->
            ok = h_logfln(Name, "unknown message: ~tw", [Unknown]),
            h_loop(HS)
    end.


% pimp log in bold
p_logln(LogStr) ->
    Msg = io_lib:format("Daddy: ~ts~n", [LogStr]),
    PimpChars = ?ANSI_BOLD([Msg]),
    io:put_chars(PimpChars).

p_logfln(LogStr, Args) ->
    p_logln(io_lib:format(LogStr, Args)).


% ho log dimmed
h_logln(HoName, LogStr) ->
    FMsg = io_lib:format("~ts: ~ts~n", [HoName, LogStr]),
    HoChars = ?ANSI_DIM([FMsg]),
    io:put_chars(HoChars).

h_logfln(HoName, MsgStr, Args) ->
    h_logln(HoName, io_lib:format(MsgStr, Args)).

% normal: john messages
h_rcvd(Name, MsgStr) ->
    Msg = io_lib:format("~ts< ~ts~n", [Name, string:chomp(MsgStr)]),
    io:put_chars(Msg).

h_send(Name, NSock, Msg) ->
    FMsg = io_lib:format("~ts> ~ts~n", [Name, string:chomp(Msg)]),
    HoChars = [?ANSI_DIM(FMsg)],
    ok = io:put_chars(HoChars),
    Send = unicode:characters_to_binary([string:chomp(Msg), "\r\n"]),
    ok = enoise:send(NSock, Send).


init_ho_names() ->
    shuffle(plain_ho_names()).

plain_ho_names() ->
    ["Crystal", "Tiffany", "Amber", "Brandy", "Lola", "Angel",
     "Ginger", "Candy", "Charity", "Anastasia", "Cherry", "Kitty",
     "Jade", "Destiny", "Devon", "Chastity", "Raven", "Scarlett",
     "Bambi", "Star", "Paris", "Dallas", "Diamond", "Skye",
     "Trinity", "Tawny", "Layla", "Lexie", "Roxy", "Porsche",
     "Nevaeh", "Ashlynn", "Aspen", "Chyna", "Lexus", "Unique",
     "Chardonnay", "Houston", "London", "Coco", "Luscious",
     "Delight", "Capri", "Trixie", "Cinnamon"].


recycle([N | Ns]) ->
    {N, Ns ++ [N]}.


%% @private
%% naive shuffle because we aren't cool enough to have
%% `rand:shuffle/1' yet`()'
%%
%% for list of length `N', there there are `N!' permutations
%%
%% ```
%% [<N options>, <N-1 options>, ..., <2 options>, <1 options>]
%% '''
%%
%% so we simply do the stupid
%%
%% conceptually: pick a sequence of integers at random
%%
%% ```
%% [(1..N), (1..N-1), (1..N-2), ... (1..2), 1]
%% '''
%%
%% which determines a permuation of the list

shuffle([]) ->
    [];
shuffle([X]) ->
    [X];
shuffle(Items) ->
    N = rand:uniform(length(Items)),
    {Nth,  Rest} = inverse_nth(N, Items),
    [Nth | shuffle(Rest)].

inverse_nth(N, Items) ->
    inverse_nth([], N, Items).

inverse_nth(Stk, 1, [X | Rest]) ->
    {X, lists:reverse(Stk, Rest)};
inverse_nth(Stk, N, [X | Rest]) when N > 1 ->
    inverse_nth([X | Stk], N-1, Rest).


%% haven't thought about this, but faster because doesn't involve any
%% lists:reverse calls
% fshuf([])  -> [];
% fshuf([X]) -> [X];
% fshuf(Items) -> [X];
%     N = rand:uniform(length(Items)),
%     fshuf_ii(N, Items)
%     shuffle(Items) ->
