%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==CPG Remote Node Monitor Process.==
%%% Handle remote node pid monitors to reduce the messages the cpg
%%% scope process needs to manage.
%%% @end
%%%
%%% MIT License
%%%
%%% Copyright (c) 2017-2020 Michael Truog <mjtruog at protonmail dot com>
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a
%%% copy of this software and associated documentation files (the "Software"),
%%% to deal in the Software without restriction, including without limitation
%%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%%% and/or sell copies of the Software, and to permit persons to whom the
%%% Software is furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%%% DEALINGS IN THE SOFTWARE.
%%%
%%% @author Michael Truog <mjtruog at protonmail dot com>
%%% @copyright 2017-2020 Michael Truog
%%% @version 1.8.1 {@date} {@time}
%%%------------------------------------------------------------------------

-module(cpg_node_monitor).
-author('mjtruog at protonmail dot com').

%% external interface
-export([start_link/1,
         stop_link/1,
         died/1,
         add/2,
         remove/2]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         code_change/3, terminate/2]).

-record(state,
    {
        parent :: pid(),
        monitors :: #{pid() := reference()}
    }).

-type process() :: pid().
-export_type([process/0]).

-include("cpg_logging.hrl").
-include("cpg_constants.hrl").

%%%------------------------------------------------------------------------
%%% External interface functions
%%%------------------------------------------------------------------------

-spec start_link(Pid :: pid()) ->
    {ok, pid()} | {error, any()}.

start_link(Pid) ->
    gen_server:start_link(?MODULE, [self(), Pid], []).

-spec stop_link(Process :: process()) ->
    ok.

stop_link(Process) ->
    true = erlang:unlink(Process),
    true = erlang:exit(Process, normal),
    ok.

-spec died(Process :: process()) ->
    {list(pid()), list(nonempty_list({pid(), {exit, any()}}))}.

died(Process) ->
    Pids = gen_server:call(Process, died),
    {Pids, monitors_flush([])}.

-spec add(Process :: process(),
          Pid :: pid()) ->
    ok.

add(Process, Pid) ->
    gen_server:cast(Process, {add, Pid}).

-spec remove(Process :: process(),
             Pid :: pid()) ->
    ok.

remove(Process, Pid) ->
    gen_server:cast(Process, {remove, Pid}).

%%%------------------------------------------------------------------------
%%% Callback functions from gen_server
%%%------------------------------------------------------------------------

init([Parent, Pid]) ->
    MonitorRef = erlang:monitor(process, Pid),
    {ok, #state{parent = Parent,
                monitors = #{Pid => MonitorRef}}}.

handle_call(died, {Client, _},
            #state{parent = Parent,
                   monitors = Monitors} = State) ->
    true = Client =:= Parent,
    {stop, normal, maps:keys(Monitors), State};

handle_call(Request, _, State) ->
    {stop, lists:flatten(io_lib:format("Unknown call \"~w\"", [Request])),
     error, State}.

handle_cast({add, Pid},
            #state{monitors = Monitors} = State) ->
    case maps:find(Pid, Monitors) of
        {ok, _} ->
            {noreply, State};
        error ->
            MonitorRef = erlang:monitor(process, Pid),
            {noreply,
             State#state{monitors = maps:put(Pid, MonitorRef, Monitors)}}
    end;

handle_cast({remove, Pid},
            #state{monitors = Monitors} = State) ->
    case maps:find(Pid, Monitors) of
        {ok, MonitorRef} ->
            true = erlang:demonitor(MonitorRef, [flush]),
            {noreply, State#state{monitors = maps:remove(Pid, Monitors)}};
        error ->
            {noreply, State}
    end;

handle_cast(Request, State) ->
    {stop, lists:flatten(io_lib:format("Unknown cast \"~w\"", [Request])),
     State}.

handle_info({'DOWN', _, process, _, _} = DOWN, State) ->
    {noreply, monitors_send(DOWN, [], State)};

handle_info(Request, State) ->
    {stop, lists:flatten(io_lib:format("Unknown info \"~w\"", [Request])),
     State}.

terminate(_, #state{}) ->
    ok.

code_change(_, State, _) ->
    {ok, State}.

%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------

monitors_send({'DOWN', _MonitorRef, process, Pid, Info}, PidReasons,
              #state{parent = Parent,
                     monitors = Monitors} = State) ->
    PidReasonsNew = [{Pid, {exit, Info}} | PidReasons],
    StateNew = State#state{monitors = maps:remove(Pid, Monitors)},
    receive
        {'DOWN', _, process, _, _} = DOWN ->
            monitors_send(DOWN, PidReasonsNew, StateNew)
    after
        ?MONITORS_SEND_DELAY ->
            Parent ! {'DOWNS', PidReasonsNew},
            StateNew
    end.

monitors_flush(PidReasonsL) ->
    receive
        {'DOWNS', PidReasons} ->
            monitors_flush([PidReasons | PidReasonsL])
    after
        ?MONITORS_FLUSH_DELAY ->
            lists:reverse(PidReasonsL)
    end.

