src/zotonic_notifier.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2017 Marc Worrell
%% @doc Extension system using notifications with fold, map and priorities.

%% Copyright 2017 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(zotonic_notifier).

-behaviour(application).

-export([
    start/0,
    start/2,
    stop/1,

    start_notifier/1,
    stop_notifier/1,

    observe/2, observe/3, observe/4, observe/5,
    detach/2, detach/3,
    detach_all/1, detach_all/2,
    get_observers/1, get_observers/2,
    notify_sync/3, notify_sync/4,
    notify/3, notify/4,
    notify1/3, notify1/4,
    first/3, first/4,
    map/3, map/4,
    foldl/4,  foldl/5,
    foldr/4, foldr/5,
    await/1, await/2, await/3, await/4,
    await_exact/2, await_exact/3, await_exact/4
]).

-type observer() :: pid()
                  | {module(), atom()}
                  | {module(), atom(), list()}.

-type event() :: term().

-type notifier() :: atom() | pid().

-export_type([
    observer/0,
    event/0,
    notifier/0
]).

-include_lib("zotonic_notifier.hrl").

%%====================================================================
%% API
%%====================================================================

start() ->
    ensure_started(zotonic_notifier).

start(_StartType, _StartArgs) ->
    zotonic_notifier_sup:start_link().

%%--------------------------------------------------------------------
stop(_State) ->
    ok.

-spec start_notifier(atom()) -> {ok, pid()} | {error, term()}.
start_notifier(Name) when is_atom(Name) ->
    case zotonic_notifier_sup:start_notifier(Name) of
        {error, {already_started, Pid}} -> {ok, Pid};
        Other -> Other
    end.

-spec stop_notifier(atom()) -> ok.
stop_notifier(Name) when is_atom(Name) ->
    zotonic_notifier_sup:stop_notifier(Name).

%% @doc Register an observer pid.
-spec observe(event(), pid()) -> ok | {error, term()}.
observe(Event, ObserverPid) when is_pid(ObserverPid) ->
    observe(Event, ObserverPid, ObserverPid).

%% @doc Register an observer with an owner pid.
-spec observe(event(), observer(), pid()) -> ok | {error, term()}.
observe(Event, Observer, OwnerPid) ->
    observe(?DEFAULT_NOTIFIER, Event, Observer, OwnerPid, prio(Observer)).

%% @doc Register an observer with the default notifier. Higher prio (lower nr) gets called earlier.
-spec observe(event(), observer(), pid(), integer()) -> ok | {error, term()}.
observe(Event, Observer, OwnerPid, Prio) ->
    observe(?DEFAULT_NOTIFIER, Event, Observer, OwnerPid, Prio).

%% @doc Register an observer. Higher prio (lower nr) gets called earlier.
-spec observe(notifier(), event(), observer(), pid(), integer()) -> ok | {error, term()}.
observe(Notifier, Event, Observer, OwnerPid, Prio) when is_pid(OwnerPid), is_integer(Prio) ->
    zotonic_notifier_worker:observe(Notifier, Event, Observer, OwnerPid, Prio).

%% @doc Unsubscribe an owner from an event
-spec detach(event(), pid()) -> ok | {error, term()}.
detach(Event, OwnerPid) when is_pid(OwnerPid) ->
    detach(?DEFAULT_NOTIFIER, Event, OwnerPid).

%% @doc Unsubscribe an owner from an event
-spec detach(notifier(), event(), pid()) -> ok | {error, term()}.
detach(Notifier, Event, OwnerPid) when is_pid(OwnerPid) ->
    zotonic_notifier_worker:detach(Notifier, Event, OwnerPid).

%% @doc Detach all observers owned by the pid
-spec detach_all(pid()) -> ok.
detach_all(OwnerPid) ->
    detach_all(?DEFAULT_NOTIFIER, OwnerPid).

%% @doc Detach all observers owned by the pid
-spec detach_all(notifier(), pid()) -> ok.
detach_all(Notifier, OwnerPid) when is_pid(OwnerPid) ->
    zotonic_notifier_worker:detach_all(Notifier, OwnerPid).

%% @doc Return the list of all observers for a specific event
-spec get_observers(event()) -> list().
get_observers(Event) ->
    get_observers(?DEFAULT_NOTIFIER, Event).

-spec get_observers(notifier(), event()) -> list().
get_observers(Notifier, '_') ->
    zotonic_notifier_worker:get_observers(Notifier);
get_observers(Notifier, Event) ->
    zotonic_notifier_worker:get_observers(Notifier, Event).


%% @doc Notify observers. Pids async, M:F sync.
-spec notify_sync(event(), term(), term()) -> ok | {error, term()}.
notify_sync(Event, Msg, ContextArg) ->
    notify_sync(?DEFAULT_NOTIFIER, Event, Msg, ContextArg).

%% @doc Notify observers. Pids async, M:F sync.
-spec notify_sync(notifier(), event(), term(), term()) -> ok | {error, term()}.
notify_sync(Notifier, Event, Msg, ContextArg) ->
    zotonic_notifier_worker:notify_sync(Notifier, Event, Msg, ContextArg).


%% @doc Notify observers async. Start separate process for the notification.
-spec notify(event(), term(), term()) -> ok | {error, term()}.
notify(Event, Msg, ContextArg) ->
    notify(?DEFAULT_NOTIFIER, Event, Msg, ContextArg).

%% @doc Notify observers async. Start separate process for the notification.
-spec notify(notifier(), event(), term(), term()) -> ok | {error, term()}.
notify(Notifier, Event, Msg, ContextArg) ->
    zotonic_notifier_worker:notify_async(Notifier, Event, Msg, ContextArg).


%% @doc Notify the first observer. Pids async, M:F sync.
-spec notify1(event(), term(), term()) -> ok | {error, term()}.
notify1(Event, Msg, ContextArg) ->
    notify1(?DEFAULT_NOTIFIER, Event, Msg, ContextArg).

%% @doc Notify the first observer. Pids async, M:F sync.
-spec notify1(notifier(), event(), term(), term()) -> ok | {error, term()}.
notify1(Notifier, Event, Msg, ContextArg) ->
    zotonic_notifier_worker:notify1(Notifier, Event, Msg, ContextArg).

%% @doc Return the result of the first observer returning something else than 'undefined'.
%%      Return 'undefined' if none.
-spec first(event(), term(), term()) -> term() | undefined.
first(Event, Msg, ContextArg) ->
    first(?DEFAULT_NOTIFIER, Event, Msg, ContextArg).

%% @doc Return the result of the first observer returning something else than 'undefined'.
%%      Return 'undefined' if none.
-spec first(notifier(), event(), term(), term()) -> term() | undefined.
first(Notifier, Event, Msg, ContextArg) ->
    zotonic_notifier_worker:first(Notifier, Event, Msg, ContextArg).


%% @doc Call all observers, return a list of all return values.
-spec map(event(), term(), term()) -> list( term() ).
map(Event, Msg, ContextArg) ->
    map(?DEFAULT_NOTIFIER, Event, Msg, ContextArg).

-spec map(notifier(), event(), term(), term()) -> list( term() ).
map(Notifier, Event, Msg, ContextArg) ->
    zotonic_notifier_worker:map(Notifier, Event, Msg, ContextArg).


-spec foldl(event(), term(), term(), term()) -> term().
foldl(Event, Msg, Value, ContextArg) ->
    foldl(?DEFAULT_NOTIFIER, Event, Msg, Value, ContextArg).

-spec foldl(notifier(), event(), term(), term(), term()) -> term().
foldl(Notifier, Event, Msg, Value, ContextArg) ->
    zotonic_notifier_worker:foldl(Notifier, Event, Msg, Value, ContextArg).

-spec foldr(event(), term(), term(), term()) -> term().
foldr(Event, Msg, Value, ContextArg) ->
    foldr(?DEFAULT_NOTIFIER, Event, Msg, Value, ContextArg).

-spec foldr(notifier(), event(), term(), term(), term()) -> term().
foldr(Notifier, Event, Msg, Value, ContextArg) ->
    zotonic_notifier_worker:foldr(Notifier, Event, Msg, Value, ContextArg).


-spec await(atom()|tuple()) ->
        {ok, tuple()|atom()} |
        {ok, {pid(), reference()}, tuple()|atom()} |
        {error, timeout}.
await(Event) ->
    await(?DEFAULT_NOTIFIER, Event, Event, 5000).

-spec await(term(), atom()|tuple()) ->
        {ok, tuple()|atom()} |
        {ok, {pid(), reference()}, tuple()|atom()} |
        {error, timeout}.
await(Event, Msg) ->
    await(?DEFAULT_NOTIFIER, Event, Msg, 5000).

-spec await(term(), atom()|tuple(), pos_integer()) ->
        {ok, tuple()|atom()} |
        {ok, {pid(), reference()}, tuple()|atom()} |
        {error, timeout}.
await(Event, Msg, Timeout) ->
    await(?DEFAULT_NOTIFIER, Event, Msg, Timeout).

-spec await(notifier(), term(), atom()|tuple(), pos_integer()) ->
        {ok, tuple()|atom()} |
        {ok, {pid(), reference()}, tuple()|atom()} |
        {error, timeout}.
await(Notifier, Event, Msg, Timeout) when is_atom(Event); is_tuple(Event) ->
    zotonic_notifier_worker:await(Notifier, Event, Msg, Timeout).


-spec await_exact(event(), term()) ->
        {ok, term()} |
        {ok, {pid(), reference()}, term()} |
        {error, timeout}.
await_exact(Event, Msg) ->
    await_exact(?DEFAULT_NOTIFIER, Event, Msg, 5000).

-spec await_exact(event(), term(), pos_integer()) ->
        {ok, term()} |
        {ok, {pid(), reference()}, term()} |
        {error, timeout}.
await_exact(Event, Msg, Timeout) ->
    await_exact(?DEFAULT_NOTIFIER, Event, Msg, Timeout).

-spec await_exact(notifier(), event(), term(), pos_integer()) ->
        {ok, term()} |
        {ok, {pid(), reference()}, term()} |
        {error, timeout}.
await_exact(Notifier, Event, Msg, Timeout) ->
    zotonic_notifier_worker:await_exact(Notifier, Event, Msg, Timeout).


%%====================================================================
%% Internal functions
%%====================================================================

-spec prio(observer()) -> integer().
prio({M, _F}) -> module_prio(M);
prio({M, _F, _A}) -> module_prio(M);
prio(Pid) when is_pid(Pid) -> ?NOTIFIER_DEFAULT_PRIORITY.

-spec module_prio(module()) -> integer().
module_prio(Module) ->
    try
        Info = erlang:get_module_info(Module, attributes),
        case proplists:get_value(mod_prio, Info) of
            [Prio] when is_integer(Prio) -> Prio;
            _ -> ?NOTIFIER_DEFAULT_PRIORITY
        end
    catch
        _M:_E -> ?NOTIFIER_DEFAULT_PRIORITY
    end.


-spec ensure_started(atom()) -> ok | {error, term()}.
ensure_started(App) ->
    case application:start(App) of
        ok ->
            ok;
        {error, {not_started, Dep}} ->
            case ensure_started(Dep) of
                ok -> ensure_started(App);
                {error, _} = Error -> Error
            end;
        {error, {already_started, App}} ->
            ok;
        {error, {Tag, Msg}} when is_list(Tag), is_list(Msg) ->
            {error, lists:flatten(io_lib:format("~s: ~s", [Tag, Msg]))};
        {error, {bad_return, {{M, F, Args}, Return}}} ->
            A = string:join([io_lib:format("~p", [A])|| A <- Args], ", "),
            {error, lists:flatten(
                        io_lib:format("~s failed to start due to a bad return value from call ~s:~s(~s):~n~p",
                                      [App, M, F, A, Return]))};
        {error, Reason} ->
            {error, Reason}
    end.