   2014 - 2016   <  >
%%
%% @doc Sink Applicable Condition
%% @end
-module(logi_condition).

%%----------------------------------------------------------------------------------------------------------------------
%% Exported API
%%----------------------------------------------------------------------------------------------------------------------
-export([is_condition/1]).
-export([normalize/1]).

-export_type([condition/0]).
-export_type([severity_condition/0]).
-export_type([location_condition/0]).
-export_type([normalized_condition/0]).

%%----------------------------------------------------------------------------------------------------------------------
%% Types
%%----------------------------------------------------------------------------------------------------------------------
-type condition() :: severity_condition() | location_condition().
%% The condition to determine which messages to be consumed by a sink.

-type severity_condition() :: (Min :: logi:severity())
                            | {Min :: logi:severity(), Max :: logi:severity()}
                            | (Severities :: [logi:severity()]).
%% `Min':
%% - The messages with `Min' or higher severity will be consumed.
%%
  ` { Min ,  } ' :
  - The messages with severity between ` Min ' and `  ' will be consumed .
%%
  ` Severities ' :
  - The messages with severity included in ` Severities ' will be consumed .
%%
%% === EXAMPLE ===
%% <pre lang="erlang">
%% > [emergency,alert]     = logi_condition:normalize(alert).                % level
%% > [warning,notice,info] = logi_condition:normalize({info, warning}).      % range
  > [ alert , debug , info ]     = logi_condition :  , info , alert ] ) . % list
%% </pre>

-type location_condition() ::
        #{
           severity    => severity_condition(),
           application => logi_location:application() | [logi_location:application()],
           module      => module() | [module()]
         }.
%% The messages which satisfy `severity' (default is `debug') and are sent from the specified location will be consumed.
%%
%% The location is specified by `application' and `module' (OR condition).
%%
%% NOTE: The modules which does not belong to any application are forbidden.
%%
%% === EXAMPLE ===
%% <pre lang="erlang">
%% > logi_condition:is_condition(#{application => stdlib}).                          % application
%% > logi_condition:is_condition(#{application => [stdlib, kernel]}).                % applications
%% > logi_condition:is_condition(#{module => lists}).                                % module
%% > logi_condition:is_condition(#{module => [lists, dict]}).                        % modules
%% > logi_condition:is_condition(#{application => kernel, module => [lists, dict]}). % application and modules
%% > logi_condition:is_condition(#{severity => [info, alert], module => lists}).     % severity and module
%% </pre>

-type normalized_condition() :: [logi:severity() |
                                 {logi:severity(), logi_location:application()} |
                                 {logi:severity(), logi_location:application(), module()}].
%% The normalized form of a `condition/0'.
%%
%% <pre lang="erlang">
%% > Normalize = fun (C) -> lists:sort(logi_condition:normalize(C)) end.
%%
%% > Normalize(info).
%% [alert,critical,emergency,error,info,notice,warning]
%%
%% > Normalize({info, alert}).
%% [alert,critical,error,info,notice,warning]
%%
%% > Normalize(#{severity => [info], application => [kernel, stdlib]}).
%% [{info,kernel},{info,stdlib}]
%%
%% > Normalize(#{severity => [info], module => [lists, logi]}).
  [ { info , logi ,  , stdlib , lists } ]
%%
%% > Normalize(#{severity => [info], application => kernel, module => [lists, logi]}).
  [ { info , kernel},{info ,  ,  , stdlib , lists } ]
%% </pre>

%%----------------------------------------------------------------------------------------------------------------------
%% Exported Functions
%%----------------------------------------------------------------------------------------------------------------------
%% @doc Returns `true' if `X' is a valid `condition()' value, otherwise `false'
-spec is_condition(X :: (condition() | term())) -> boolean().
is_condition(X) when is_map(X) -> is_location_condition(X);
is_condition(X)                -> is_severity_condition(X).

%% @doc Returns a normalized form of `Condition'
-spec normalize(Condition :: condition()) -> normalized_condition().
normalize(C) when is_map(C) -> normalize_location_condition(C);
normalize(C)                -> normalize_severity_condition(C).

%%----------------------------------------------------------------------------------------------------------------------
%% Internal Functions
%%----------------------------------------------------------------------------------------------------------------------
-spec is_severity_condition(severity_condition() | term()) -> boolean().
is_severity_condition({Min, Max})                          -> logi:is_severity(Min) andalso logi:is_severity(Max);
is_severity_condition(Severities) when is_list(Severities) -> lists:all(fun logi:is_severity/1, Severities);
is_severity_condition(Min)                                 -> logi:is_severity(Min).

-spec is_location_condition(location_condition() | term()) -> boolean().
is_location_condition(Condition = #{severity := Severity}) ->
    is_location_condition(Severity) andalso is_location_condition(maps:without([severity], Condition));
is_location_condition(Condition = #{application := Application}) ->
    Valid =
        case is_list(Application) of
            false -> is_atom(Application);
            true  -> lists:all(fun erlang:is_atom/1, Application)
        end,
    Valid andalso is_location_condition(maps:without([application], Condition));
is_location_condition(Condition = #{module := Module}) ->
    Valid =
        case is_list(Module) of
            false -> is_module(Module);
            true  -> lists:all(fun is_module/1, Module)
        end,
    Valid andalso is_location_condition(maps:without([module], Condition));
is_location_condition(_) ->
    true.

-spec is_module(module() | term()) -> boolean().
is_module(Module) when is_atom(Module) -> logi_location:guess_application(Module) =/= undefined;
is_module(_)                           -> false.

-spec normalize_location_condition(location_condition()) -> normalized_condition().
normalize_location_condition(C) ->
    #{severity := Severity, application := Application, module := Module} =
        maps:merge(#{severity => debug, application => [], module => []}, C),
    Applications = case is_list(Application) of true -> Application; false -> [Application] end,
    Modules = case is_list(Module) of true -> Module; false -> [Module] end,
    lists:usort(
      lists:append(
        [begin
             [{S, A} || A <- Applications] ++
             [{S, logi_location:guess_application(M), M} || M <- Modules]
         end || S <- normalize_severity_condition(Severity)])).

-spec normalize_severity_condition(severity_condition()) -> normalized_condition().
normalize_severity_condition(C) ->
    L = fun (Severity) -> logi:severity_level(Severity) end,
    case C of
        {Min, Max}        -> lists:sublist(logi:severities(), L(Max), max(0, 1 + L(Min) - L(Max)));
        _ when is_atom(C) -> lists:sublist(logi:severities(), 1, L(C));
        _                 -> lists:usort(C)
    end.


 @doc Sink Applicable Condition
 @end
----------------------------------------------------------------------------------------------------------------------
 Exported API
----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------
 Types
----------------------------------------------------------------------------------------------------------------------
 The condition to determine which messages to be consumed by a sink.
 `Min':
 - The messages with `Min' or higher severity will be consumed.



 === EXAMPLE ===
 <pre lang="erlang">
 > [emergency,alert]     = logi_condition:normalize(alert).                % level
 > [warning,notice,info] = logi_condition:normalize({info, warning}).      % range
 list
 </pre>
 The messages which satisfy `severity' (default is `debug') and are sent from the specified location will be consumed.

 The location is specified by `application' and `module' (OR condition).

 NOTE: The modules which does not belong to any application are forbidden.

 === EXAMPLE ===
 <pre lang="erlang">
 > logi_condition:is_condition(#{application => stdlib}).                          % application
 > logi_condition:is_condition(#{application => [stdlib, kernel]}).                % applications
 > logi_condition:is_condition(#{module => lists}).                                % module
 > logi_condition:is_condition(#{module => [lists, dict]}).                        % modules
 > logi_condition:is_condition(#{application => kernel, module => [lists, dict]}). % application and modules
 > logi_condition:is_condition(#{severity => [info, alert], module => lists}).     % severity and module
 </pre>
 The normalized form of a `condition/0'.

 <pre lang="erlang">
 > Normalize = fun (C) -> lists:sort(logi_condition:normalize(C)) end.

 > Normalize(info).
 [alert,critical,emergency,error,info,notice,warning]

 > Normalize({info, alert}).
 [alert,critical,error,info,notice,warning]

 > Normalize(#{severity => [info], application => [kernel, stdlib]}).
 [{info,kernel},{info,stdlib}]

 > Normalize(#{severity => [info], module => [lists, logi]}).

 > Normalize(#{severity => [info], application => kernel, module => [lists, logi]}).
 </pre>
----------------------------------------------------------------------------------------------------------------------
 Exported Functions
----------------------------------------------------------------------------------------------------------------------
 @doc Returns `true' if `X' is a valid `condition()' value, otherwise `false'
 @doc Returns a normalized form of `Condition'
----------------------------------------------------------------------------------------------------------------------
 Internal Functions
----------------------------------------------------------------------------------------------------------------------
   2014 - 2016   <  >
-module(logi_condition).

-export([is_condition/1]).
-export([normalize/1]).

-export_type([condition/0]).
-export_type([severity_condition/0]).
-export_type([location_condition/0]).
-export_type([normalized_condition/0]).

-type condition() :: severity_condition() | location_condition().

-type severity_condition() :: (Min :: logi:severity())
                            | {Min :: logi:severity(), Max :: logi:severity()}
                            | (Severities :: [logi:severity()]).
  ` { Min ,  } ' :
  - The messages with severity between ` Min ' and `  ' will be consumed .
  ` Severities ' :
  - The messages with severity included in ` Severities ' will be consumed .

-type location_condition() ::
        #{
           severity    => severity_condition(),
           application => logi_location:application() | [logi_location:application()],
           module      => module() | [module()]
         }.

-type normalized_condition() :: [logi:severity() |
                                 {logi:severity(), logi_location:application()} |
                                 {logi:severity(), logi_location:application(), module()}].
  [ { info , logi ,  , stdlib , lists } ]
  [ { info , kernel},{info ,  ,  , stdlib , lists } ]

-spec is_condition(X :: (condition() | term())) -> boolean().
is_condition(X) when is_map(X) -> is_location_condition(X);
is_condition(X)                -> is_severity_condition(X).

-spec normalize(Condition :: condition()) -> normalized_condition().
normalize(C) when is_map(C) -> normalize_location_condition(C);
normalize(C)                -> normalize_severity_condition(C).

-spec is_severity_condition(severity_condition() | term()) -> boolean().
is_severity_condition({Min, Max})                          -> logi:is_severity(Min) andalso logi:is_severity(Max);
is_severity_condition(Severities) when is_list(Severities) -> lists:all(fun logi:is_severity/1, Severities);
is_severity_condition(Min)                                 -> logi:is_severity(Min).

-spec is_location_condition(location_condition() | term()) -> boolean().
is_location_condition(Condition = #{severity := Severity}) ->
    is_location_condition(Severity) andalso is_location_condition(maps:without([severity], Condition));
is_location_condition(Condition = #{application := Application}) ->
    Valid =
        case is_list(Application) of
            false -> is_atom(Application);
            true  -> lists:all(fun erlang:is_atom/1, Application)
        end,
    Valid andalso is_location_condition(maps:without([application], Condition));
is_location_condition(Condition = #{module := Module}) ->
    Valid =
        case is_list(Module) of
            false -> is_module(Module);
            true  -> lists:all(fun is_module/1, Module)
        end,
    Valid andalso is_location_condition(maps:without([module], Condition));
is_location_condition(_) ->
    true.

-spec is_module(module() | term()) -> boolean().
is_module(Module) when is_atom(Module) -> logi_location:guess_application(Module) =/= undefined;
is_module(_)                           -> false.

-spec normalize_location_condition(location_condition()) -> normalized_condition().
normalize_location_condition(C) ->
    #{severity := Severity, application := Application, module := Module} =
        maps:merge(#{severity => debug, application => [], module => []}, C),
    Applications = case is_list(Application) of true -> Application; false -> [Application] end,
    Modules = case is_list(Module) of true -> Module; false -> [Module] end,
    lists:usort(
      lists:append(
        [begin
             [{S, A} || A <- Applications] ++
             [{S, logi_location:guess_application(M), M} || M <- Modules]
         end || S <- normalize_severity_condition(Severity)])).

-spec normalize_severity_condition(severity_condition()) -> normalized_condition().
normalize_severity_condition(C) ->
    L = fun (Severity) -> logi:severity_level(Severity) end,
    case C of
        {Min, Max}        -> lists:sublist(logi:severities(), L(Max), max(0, 1 + L(Min) - L(Max)));
        _ when is_atom(C) -> lists:sublist(logi:severities(), 1, L(C));
        _                 -> lists:usort(C)
    end.
