%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2018-2020 VMware, Inc. or its affiliates.  All rights reserved.
%%

%% @author The RabbitMQ team
%% @copyright 2018-2020 VMware, Inc. or its affiliates.
%%
%% @doc
%% This module exposes the API of the {@link rabbit_feature_flags}
%% registry. The feature flags registry is an Erlang module, compiled at
%% runtime, storing all the informations about feature flags: which are
%% supported, which are enabled, etc.
%%
%% Because it is compiled at runtime, the initial source code is mostly
%% an API reference. What the initial module does is merely ask {@link
%% rabbit_feature_flags} to generate the real registry.

-module(rabbit_ff_registry).

-export([get/1,
         list/1,
         states/0,
         is_supported/1,
         is_enabled/1,
         is_registry_initialized/0,
         is_registry_written_to_disk/0]).

-ifdef(TEST).
-on_load(on_load/0).
-endif.

-spec get(rabbit_feature_flags:feature_name()) ->
    rabbit_feature_flags:feature_props() | undefined.
%% @doc
%% Returns the properties of a feature flag.
%%
%% Only the informations stored in the local registry is used to answer
%% this call.
%%
%% @param FeatureName The name of the feature flag.
%% @returns the properties of the specified feature flag.

get(FeatureName) ->
    rabbit_feature_flags:initialize_registry(),
    %% Initially, is_registry_initialized/0 always returns `false`
    %% and this ?MODULE:get(FeatureName) is always called. The case
    %% statement is here to please Dialyzer.
    case is_registry_initialized() of
        false -> ?MODULE:get(FeatureName);
        true  -> undefined
    end.

-spec list(all | enabled | disabled) -> rabbit_feature_flags:feature_flags().
%% @doc
%% Lists all, enabled or disabled feature flags, depending on the argument.
%%
%% Only the informations stored in the local registry is used to answer
%% this call.
%%
%% @param Which The group of feature flags to return: `all', `enabled' or
%% `disabled'.
%% @returns A map of selected feature flags.

list(Which) ->
    rabbit_feature_flags:initialize_registry(),
    %% See get/1 for an explanation of the case statement below.
    case is_registry_initialized() of
        false -> ?MODULE:list(Which);
        true  -> #{}
    end.

-spec states() -> rabbit_feature_flags:feature_states().
%% @doc
%% Returns the states of supported feature flags.
%%
%% Only the informations stored in the local registry is used to answer
%% this call.
%%
%% @returns A map of feature flag states.

states() ->
    rabbit_feature_flags:initialize_registry(),
    %% See get/1 for an explanation of the case statement below.
    case is_registry_initialized() of
        false -> ?MODULE:states();
        true  -> #{}
    end.

-spec is_supported(rabbit_feature_flags:feature_name()) -> boolean().
%% @doc
%% Returns if a feature flag is supported.
%%
%% Only the informations stored in the local registry is used to answer
%% this call.
%%
%% @param FeatureName The name of the feature flag to be checked.
%% @returns `true' if the feature flag is supported, or `false'
%%   otherwise.

is_supported(FeatureName) ->
    rabbit_feature_flags:initialize_registry(),
    %% See get/1 for an explanation of the case statement below.
    case is_registry_initialized() of
        false -> ?MODULE:is_supported(FeatureName);
        true  -> false
    end.

-spec is_enabled(rabbit_feature_flags:feature_name()) -> boolean() | state_changing.
%% @doc
%% Returns if a feature flag is supported or if its state is changing.
%%
%% Only the informations stored in the local registry is used to answer
%% this call.
%%
%% @param FeatureName The name of the feature flag to be checked.
%% @returns `true' if the feature flag is supported, `state_changing' if
%%   its state is transient, or `false' otherwise.

is_enabled(FeatureName) ->
    rabbit_feature_flags:initialize_registry(),
    %% See get/1 for an explanation of the case statement below.
    case is_registry_initialized() of
        false -> ?MODULE:is_enabled(FeatureName);
        true  -> false
    end.

-spec is_registry_initialized() -> boolean().
%% @doc
%% Indicates if the registry is initialized.
%%
%% The registry is considered initialized once the initial Erlang module
%% was replaced by the copy compiled at runtime.
%%
%% @returns `true' when the module is the one compiled at runtime,
%%   `false' when the module is the initial one compiled from RabbitMQ
%%   source code.

is_registry_initialized() ->
    always_return_false().

-spec is_registry_written_to_disk() -> boolean().
%% @doc
%% Indicates if the feature flags state was successfully persisted to disk.
%%
%% Note that on startup, {@link rabbit_feature_flags} tries to determine
%% the state of each supported feature flag, regardless of the
%% information on disk, to ensure maximum consistency. However, this can
%% be done for feature flags supporting it only.
%%
%% @returns `true' if the state was successfully written to disk and
%%   the registry can be initialized from that during the next RabbitMQ
%%   startup, `false' if the write failed and the node might loose feature
%%   flags state on restart.

is_registry_written_to_disk() ->
    always_return_true().

always_return_true() ->
    %% This function is here to trick Dialyzer. We want some functions
    %% in this initial on-disk registry to always return `true` or
    %% `false`. However the generated registry will return actual
    %% booleans. The `-spec()` correctly advertises a return type of
    %% `boolean()`. But in the meantime, Dialyzer only knows about this
    %% copy which, without the trick below, would always return either
    %% `true` (e.g. in is_registry_written_to_disk/0) or `false` (e.g.
    %% is_registry_initialized/0). This obviously causes some warnings
    %% where the registry functions are used: Dialyzer believes that
    %% e.g. matching the return value of is_registry_initialized/0
    %% against `true` will never succeed.
    %%
    %% That's why this function makes a call which we know the result,
    %% but not Dialyzer, to "create" that hard-coded `true` return
    %% value.
    erlang:get({?MODULE, always_undefined}) =:= undefined.

always_return_false() ->
    not always_return_true().

-ifdef(TEST).
on_load() ->
     _ = (catch rabbit_log_feature_flags:debug(
                  "Feature flags: Loading initial (uninitialized) registry "
                  "module (~p)",
                  [self()])),
    ok.
-endif.
