%% -------------------------------------------------------------------
%%
%% Copyright (c) 2014 Basho Technologies, Inc.  All Rights Reserved.
%%
%%   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 http://mozilla.org/MPL/2.0/.
%%
%% -------------------------------------------------------------------


%% @doc API and behaviour for metrics instances
%%
%% <h2>Predefined templates</h2>
%%
%% It is possible to define a set of defaults for exometer.
%%
%% Example: Putting the following in a sys.config file,
%% <pre lang="erlang">
%% {exometer, [
%%          {defaults,
%%           [{['_'], function    , [{module, exometer_function}]},
%%            {['_'], counter     , [{module, exometer}]},
%%            {['_'], fast_counter, [{module, exometer}]},
%%            {['_'], gauge       , [{module, exometer}]},
%%            {['_'], histogram   , [{module, exometer_histogram}]},
%%            {['_'], spiral      , [{module, exometer_spiral}]}
%%           ]}
%%         ]}
%% </pre>
%% will define global defaults for the given metric types. The format is
%% `{NamePattern, Type, Options}'
%%
%% The options can be overridden by options given in the `new()' command.
%%
%% `NamePattern' is similar to that used in {@link find_entries/1}.
%% For more information, see {@link exometer_admin:set_default/3}.
%% @end
-module(exometer).

-export(
   [
    new/2, new/3,
    re_register/3, ensure/3,
    repair/1,
    propose/3,
    update/2, update_or_create/2, update_or_create/4,
    get_value/1, get_value/2, get_values/1,
    sample/1,
    delete/1,
    reset/1,
    setopts/2,
    find_entries/1,
    select/1, select/2, select_cont/1, select_count/1,
    aggregate/2,
    info/1, info/2,
    register_application/0,
    register_application/1
   ]).

-export([global_status/1]).

-export([create_entry/1]).  % called only from exometer_admin.erl

%% Convenience function for testing
-export([start/0, stop/0]).

-export_type([name/0, type/0, options/0, status/0, behaviour/0,
              entry/0, datapoint/0]).

-compile(inline).

-include("exometer.hrl").

-type name()        :: list().
-type type()        :: atom()
                     | {function, M :: atom(), F :: atom()}
                     | {function, M :: atom(), F :: atom(), ArgSpec :: list(),
                        Type :: atom(), DataPoints :: list()}
                     | {Type :: atom(), Arg :: any()}.
-type status()      :: enabled | disabled.
-type options()     :: [{atom(), any()}].
-type value()       :: any().
-type error()       :: {error, any()}.
-type behaviour()   :: probe | entry.
-type entry()       :: #exometer_entry{}.
-type datapoint()   :: atom() | integer().

-define(IS_ENABLED(St), St==enabled orelse St band 2#1 == 1).
-define(IS_DISABLED(St), St==disabled orelse St band 2#1 =/= 1).

-define(EVENT_ENABLED(St), St band 2#10 == 2#10).

%% @doc Start exometer and dependent apps (for testing).
start() ->
    {ok,_} = exometer_util:ensure_all_started(exometer_core),
    ok.

%% @doc Stop exometer and dependent apps (for testing).
stop() ->
    application:stop(exometer_core).

-spec new(name(), type()) -> ok.
%% @equiv new(Name, Type, [])
new(Name, Type) ->
    new(Name, Type, []).

-spec new(name(), type(), options()) -> ok.
%% @doc Create a new metrics entry.
%%
%% `Name' must be a list of terms (e.g. atoms). `Type' must be either one
%% of the built-in types, or match a predefined template.
%%
%% `Options' will be passed to the entry, but the framework will recognize
%% the following options:
%%
%% * `{cache, Lifetime}' - Cache the results of {@link get_value/1} for
%% the given number of milliseconds. Subsequent calls to {@link get_value/1}
%% will get the cached value, if found. Default is `0', which means no
%% caching will be performed.
%%
%% * `{status, enabled | disabled}' - Default is `enabled'. If the metric
%% is `disabled', calls to {@link get_value/1} will return `{ok, disabled}',
%% and calls to {@link update/2} and {@link sample/1} will return `ok' but
%% will do nothing.
%%
%% * `{snmp, [{DataPoint, ReportInterval}]}' - defines a link to SNMP reporting,
%% where the given data points are sampled at the given intervals, converted
%% to SNMP PDUs and transmitted via the `exometer_report_snmp' reporter.
%%
%% * `{snmp_syntax, [{DataPoint | {default}, SYNTAX}]}' - specifies a custom
%% SNMP type for a given data point. `SYNTAX' needs to be a binary or a string,
%% and corresponds to the SYNTAX definition in the generated SNMP MIB.
%%
%% * `{aliases, [{DataPoint, Alias}]}' - maps aliases to datapoints.
%%   See {@link exometer_alias:new/2}.
%%
%% * <code>{'--', Keys}</code> removes option keys from the applied template.
%% This can be used to clean up the options list when overriding the defaults
%% for a given namespace (if the default definition contains options that are
%% not applicable, or would even cause problems with the current entry.)
%%
%% For example, the default value for an exometer counter is `"Counter32"', which
%% expands to `SYNTAX Counter32' in the corresponding MIB object definition. If
%% a 64-bit counter (not supported by SNMPv1) is desired instead, the option
%% `{snmp_syntax, [{value, "Counter64"}]}' can be added to the counter entry
%% (note that `value' in this case is the name of the data point representing
%% the counter value).
%%
%% @end
new(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
    exometer_admin:new_entry(Name, Type, Opts).

-spec propose(name(), type(), options()) -> exometer_info:pp() | error().
%% @doc Propose a new exometer entry (no entry actually created).
%%
%% This function analyzes a proposed entry definition, applying templates
%% and processing options in the same way as {@link new/3}, but not actually
%% creating the entry. The return value, if successful, corresponds to
%% `exometer_info:pp(Entry)'.
%% @end
propose(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
    exometer_admin:propose(Name, Type, Opts).

-spec re_register(name(), type(), options()) -> ok.
%% @doc Create a new metrics entry, overwrite any old entry.
%%
%% This function behaves as {@link new/3}, but will not fail if an entry
%% with the same name already exists. Instead, the old entry will be replaced
%% by the new.
%% @end
re_register(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
    exometer_admin:re_register_entry(Name, Type, Opts).

-spec repair(name()) -> ok.
%% @doc Delete and re-create an entry.
%%
%% This function can be tried if a metric (e.g. a complex probe) has become
%% 'stuck' or otherwise isn't functioning properly. It fetches the stored
%% meta-data and then deletes and re-creates the metric.
%% @end
repair(Name) ->
    exometer_admin:repair_entry(Name).

-spec ensure(name(), type(), options()) -> ok | error().
%% @doc Ensure that metric exists and is of given type.
%%
%% This function is similar to re-register, but doesn't actually re-register
%% a metric if it already exists. If a matching entry is found, a check is
%% performed to verify that it is of the correct type. If it isn't, an
%% error tuple is returned.
%% @end
ensure(Name, Type, Opts) when is_list(Name), is_list(Opts) ->
    exometer_admin:ensure(Name, Type, Opts).

-dialyzer({no_match, update/2}).
-spec update(name(), value()) -> ok | error().
%% @doc Update the given metric with `Value'.
%%
%% The exact semantics of an update will vary depending on metric type.
%% For exometer's built-in counters, the counter instance on the current
%% scheduler will be incremented. For a plugin metric (e.g. a probe), the
%% corresponding callback module will be called. For a disabled metric,
%% `ok' will be returned without any other action being taken.
%% @end
update(Name, Value) ->
    case exometer_global:status() of
        enabled -> update_(Name, Value);
        _ -> ok
    end.

update_(Name, Value) when is_list(Name) ->
    case ets:lookup(Table = exometer_util:table(), Name) of
        [#exometer_entry{status = Status} = E]
          when ?IS_ENABLED(Status) ->
            case E of
                #exometer_entry{module = ?MODULE, type = counter} ->
                    ets:update_counter(
                      Table, Name, {#exometer_entry.value, Value});
                #exometer_entry{module = ?MODULE,
                                type = fast_counter, ref = {M, F}} ->
                    fast_incr(Value, M, F);
                #exometer_entry{module = ?MODULE, type = gauge} ->
                    ets:update_element(
                      ?EXOMETER_ENTRIES,
                      Name, [{#exometer_entry.value, Value}]);
                #exometer_entry{behaviour = probe,
                                type = T, ref = Pid} ->
                    exometer_probe:update(Name, Value, T, Pid);
                #exometer_entry{module = M, behaviour = entry,
                                type = Type, ref = Ref} ->
                    M:update(Name, Value, Type, Ref)
            end,
            update_ok(Status, E, Value);
        [] ->
            {error, not_found};
        _ ->
            ok
    end.

update_ok(St, #exometer_entry{name = Name}, Value)
  when St band 2#10 =:= 2#10 ->
    try begin
	    exometer_event ! {updated, Name, Value},
	    ok
	end
    catch
        _:_ -> ok
    end;
update_ok(_, _, _) ->
    ok.


-spec update_or_create(Name::name(), Value::value()) -> ok | error().
%% @doc Update existing metric, or create+update according to template.
%%
%% If the metric exists, it is updated (see {@link update/2}). If it doesn't,
%% exometer searches for a template matching `Name', picks the best
%% match and creates a new entry based on the template
%% (see {@link exometer_admin:set_default/3}). Note that fully wild-carded
%% templates (i.e. <code>['_']</code>) are ignored.
%% @end
update_or_create(Name, Value) ->
    case update(Name, Value) of
        {error, not_found} ->
            case exometer_admin:auto_create_entry(Name) of
                ok ->
                    update(Name, Value);
                Error ->
                    Error
            end;
        ok ->
            ok
    end.

update_or_create(Name, Value, Type, Opts) ->
    case update(Name, Value) of
        {error, not_found} ->
            ensure(Name, Type, Opts),
            update(Name, Value);
        ok ->
            ok
    end.

fast_incr(N, M, F) when N > 0 ->
    M:F(),
    fast_incr(N-1, M, F);
fast_incr(0, _, _) ->
    ok.


%% @doc Fetch the current value of the metric.
%%
%% For a built-in counter, the value returned is the sum of all counter
%% instances (one per scheduler). For plugin metrics, the callback module is
%% responsible for providing the value. If the metric has a specified
%% (non-zero) cache lifetime, and a value resides in the cache, the cached
%% value will be returned.
%% @end
-spec get_value(name()) -> {ok, value()} | {error, not_found}.

get_value(Name) when is_list(Name) ->
    get_value(Name, default).

-spec get_value(name(), datapoint() | [datapoint()]) ->
                       {ok, value()} | {error, not_found}.

get_value(Name, DataPoint) when is_list(Name), is_integer(DataPoint) ->
    get_value(Name, [DataPoint]);
get_value(Name, DataPoint) when is_list(Name), is_atom(DataPoint),
                                DataPoint=/=default ->
    get_value(Name, [DataPoint]);

%% Also covers DataPoints = default
get_value(Name, DataPoints) when is_list(Name) ->
    case ets:lookup(exometer_util:table(), Name) of
        [#exometer_entry{} = E] ->
            {ok, get_value_(E, DataPoints)};
        _ ->
            {error, not_found}
    end.

%% If the entry is disabled, just err out.
get_value_(#exometer_entry{ status = Status }, _DataPoints)
  when ?IS_DISABLED(Status) ->
    disabled;

%% If the value is cached, see if we can find it.
%% In the default case, call again with resolved data points.
get_value_(#exometer_entry{cache = Cache } = E,
           DataPoints) when Cache =/= 0 ->
    get_cached_value_(E, DataPoints);

get_value_(#exometer_entry{ module = ?MODULE,
                            type = Type} = E, default)
  when Type == counter; Type == fast_counter; Type == gauge ->
    get_value_(E, exometer_util:get_datapoints(E));

get_value_(#exometer_entry{ module = ?MODULE,
                            type = counter} = E, DataPoints0) ->
    DataPoints = datapoints(DataPoints0, E),
    [get_ctr_datapoint(E, D) || D <- DataPoints];

get_value_(#exometer_entry{ module = ?MODULE,
                            type = gauge, name = Name}, DataPoints0) ->
    [E] = ets:lookup(?EXOMETER_ENTRIES, Name),
    DataPoints = datapoints(DataPoints0, E),
    [get_gauge_datapoint(E, D) || D <- DataPoints];

get_value_(#exometer_entry{module = ?MODULE,
                           type = fast_counter} = E, DataPoints0) ->
    DataPoints = datapoints(DataPoints0, E),
    [get_fctr_datapoint(E, D) || D <- DataPoints ];

get_value_(#exometer_entry{behaviour = entry,
                           module = Mod,
                           name = Name,
                           type = Type,
                           ref = Ref} = E, DataPoints0) ->
    Mod:get_value(Name, Type, Ref, datapoints(DataPoints0, E));

get_value_(#exometer_entry{behaviour = probe,
                           name = Name,
                           type = Type,
                           ref = Ref} = E, DataPoints0) ->

    exometer_probe:get_value(Name, Type, Ref, datapoints(DataPoints0, E)).


get_cached_value_(E, default) ->
    get_cached_value_(E, exometer_util:get_datapoints(E));

get_cached_value_(#exometer_entry{name = Name,
                                  cache = CacheTTL } = E,
                  DataPoints) ->

    %% Dig through all the data points and check for cache hit.
    %% Store all cached KV pairs, and all keys that must be
    %% read and cached.
    { Cached, Uncached } =
        lists:foldr(fun(DataPoint, {Cached1, Uncached1}) ->
                            case exometer_cache:read(Name, DataPoint) of
                                not_found ->
                                    { Cached1, [DataPoint | Uncached1] };
                                {_, Value } ->
                                    { [{ DataPoint, Value } | Cached1], Uncached1 }
                            end
                    end, {[],[]}, DataPoints),

    if Uncached == [] ->
            %% We are done, return
            Cached;
       true ->
            %% Go through all cache misses and retreive their actual values.
            case get_value_(E#exometer_entry { cache = 0 }, Uncached) of
                {error, unavailable} = Result ->
                    %% a function entry returns this in exception case.
                    %% see `exometer_function:get_value/4`
                    Result;
                Result ->
                    %% Update the cache with all the shiny new values retrieved.
                    [ exometer_cache:write(Name, DataPoint1, Value1, CacheTTL)
                      || { DataPoint1, Value1 } <- Result],
                    All = Result ++ Cached,
                    [{_,_} = lists:keyfind(DP, 1, All) || DP <- DataPoints,
                                                          lists:keymember(DP,1,All)]
            end
    end.

-spec delete(name()) -> ok | error().
%% @doc Delete the metric
delete(Name) when is_list(Name) ->
    exometer_admin:delete_entry(Name).

-spec sample(name()) -> ok | error().
%% @doc Tells the metric (mainly probes) to take a sample.
%%
%% Probes often take care of data sampling using a configured sample
%% interval. This function provides a way to explicitly tell a probe to
%% take a sample. The operation is asynchronous. For other metrics, the
%% operation likely has no effect, and will return `ok'.
%% @end
sample(Name)  when is_list(Name) ->
    case ets:lookup(exometer_util:table(), Name) of
        [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) ->
            case E of
                #exometer_entry{module = ?MODULE, type = counter} -> ok;
                #exometer_entry{module = ?MODULE, type = fast_counter} -> ok;
                #exometer_entry{behaviour = probe,
                                type = Type,
                                ref = Ref} ->
                    exometer_probe:sample(Name, Type, Ref),
                    ok;
                #exometer_entry{behaviour = entry,
                                module = M, type = Type, ref = Ref} ->
                    M:sample(Name, Type, Ref)
            end;
        [_] -> disabled;
        [] ->
            {error, not_found}
    end.


-dialyzer({no_match, reset/1}).
-spec reset(name()) -> ok | error().
%% @doc Reset the metric.
%%
%% For a built-in counter, the value of the counter is set to zero. For other
%% types of metric, the callback module will define exactly what happens
%% when a reset() is requested. A timestamp (`os:timestamp()') is saved in
%% the exometer entry, which can be recalled using {@link info/2}, and will
%% indicate the time that has passed since the metric was last reset.
%% @end
reset(Name) ->
    case exometer_global:status() of
        enabled -> reset_(Name);
        _ -> ok
    end.

reset_(Name)  when is_list(Name) ->
    case ets:lookup(exometer_util:table(), Name) of
        [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) ->
            case E of
                #exometer_entry{module = ?MODULE, type = counter} ->
                    TS = exometer_util:timestamp(),
                    [ets:update_element(T, Name, [{#exometer_entry.value, 0},
                                                  {#exometer_entry.timestamp, TS}])
                     || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
                    ok;
                #exometer_entry{module = ?MODULE, type = fast_counter,
                                ref = {M, F}} ->
                    TS = exometer_util:timestamp(),
                    set_call_count(M, F, true),
                    [ets:update_element(T, Name, [{#exometer_entry.timestamp, TS}])
                     || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
                    ok;
                #exometer_entry{module = ?MODULE, type = gauge} ->
                    TS = exometer_util:timestamp(),
                    [ets:update_element(T, Name, [{#exometer_entry.value, 0},
                                                  {#exometer_entry.timestamp, TS}])
                     || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
                    ok;
                #exometer_entry{behaviour = probe, type = Type, ref = Ref} ->
                    [exometer_cache:delete(Name, DataPoint) ||
                        DataPoint <- exometer_util:get_datapoints(E)],
                    exometer_probe:reset(Name, Type, Ref),
                    ok;
                #exometer_entry{behaviour = entry,
                                module = M, type = Type, ref = Ref} ->
                    [exometer_cache:delete(Name, DataPoint) ||
                        DataPoint <- exometer_util:get_datapoints(E)],
                    M:reset(Name, Type, Ref)
            end;
        [] ->
            {error, not_found};
        _ ->
            ok
    end.


-spec setopts(name(), options()) -> ok | error().
%% @doc Change options for the metric.
%%
%% Valid options are whatever the metric type supports, plus:
%%
%% * `{cache, Lifetime}' - The cache lifetime (0 for no caching).
%%
%% * `{status, enabled | disabled}' - the operational status of the metric.
%%
%%
%% Note that if the metric is disabled, setopts/2 will fail unless the options
%% list contains `{status, enabled}', which will enable the metric and cause
%% other options to be processed.
%% @end
setopts(Name, Options) when is_list(Name), is_list(Options) ->
    case ets:lookup(exometer_util:table(), Name) of
        [#exometer_entry{module = ?MODULE,
                         type = Type,
                         status = Status} = E] ->
            if ?IS_DISABLED(Status) ->
                    case lists:keyfind(status, 1, Options) of
                        {_, enabled} ->
                            case Type of
                                fast_counter -> setopts_fctr(E, Options);
                                counter      -> setopts_ctr(E, Options);
                                gauge        -> setopts_gauge(E, Options)
                            end,
                            reporter_setopts(E, Options, enabled);
                        _ ->
                            {error, disabled}
                    end;
               ?IS_ENABLED(Status) ->
                    case lists:keyfind(status, 1, Options) of
                        {_, disabled} ->
                            {_, Elems} = process_setopts(E, Options),
                            update_entry_elems(Name, Elems),
                            reporter_setopts(E, Options, disabled);
                        R when R==false; R=={status,disabled} ->
                            case Type of
                                fast_counter -> setopts_fctr(E, Options);
                                counter      -> setopts_ctr(E, Options);
                                gauge        -> setopts_gauge(E, Options)
                            end,
                            reporter_setopts(E, Options, enabled)
                    end
            end;
        [#exometer_entry{status = Status} = E] when ?IS_ENABLED(Status) ->
            NewStatus = proplists:get_value(status, Options, enabled),
            {_, Elems} = process_setopts(E, Options),
            update_entry_elems(Name, Elems),
            module_setopts(E, Options, NewStatus);

        [#exometer_entry{status = Status} = E] when ?IS_DISABLED(Status) ->
            case lists:keyfind(status, 1, Options) of
                {_, enabled} ->
                    {_, Elems} = process_setopts(E, Options),
                    update_entry_elems(Name, Elems),
                    module_setopts(E, Options, enabled);
                false ->
                    {error, disabled}
            end;
        [] ->
            {error, not_found}
    end.

module_setopts(#exometer_entry{behaviour = probe,
                               name = Name,
                               status = St0} = E, Options, NewStatus) ->
    reporter_setopts(E, Options, NewStatus),
    case NewStatus of
        disabled when ?IS_ENABLED(St0) ->
            exometer_cache:delete_name(Name),
            exometer_probe:stop_probe(E),
            update_entry_elems(Name, [{#exometer_entry.ref, undefined}]);
        enabled when ?IS_DISABLED(St0) ->
            %% Previously, probes were not stopped when disabled
            OldRef = E#exometer_entry.ref,
            case is_pid(OldRef) andalso is_process_alive(OldRef) of
                true -> ok;
                false ->
                    {ok, Ref} = exometer_probe:start_probe(E),
                    update_entry_elems(Name, [{#exometer_entry.ref, Ref}])
            end;
        _ ->
            exometer_probe:setopts(E, Options, NewStatus)
    end,
    ok;

module_setopts(#exometer_entry{behaviour = entry,
                               module=M} = E, Options, NewStatus) ->
    case [O || {K, _} = O <- Options,
               not lists:member(K, [status, cache, ref])] of
        [] ->
            ok;
        [_|_] = UserOpts ->
            case M:setopts(E, UserOpts, NewStatus) of
                ok ->
                    reporter_setopts(E, Options, NewStatus),
                    ok;
                {error,_} = Error ->
                    Error
            end
    end.

reporter_setopts(#exometer_entry{} = E, Options, Status) ->
    exometer_report:setopts(E, Options, Status).

setopts_fctr(#exometer_entry{name = Name,
                             ref = OldRef,
                             status = OldStatus} = E, Options) ->
    {#exometer_entry{status = NewStatus}, Elems} =
        process_setopts(E, Options),
    Ref = case lists:keyfind(function, 1, Options) of
              {_, {M, F} = NewRef} when is_atom(M), is_atom(F),
                                        M =/= '_', F =/= '_' -> NewRef;
              false -> OldRef
          end,
    if Ref =/= OldRef ->
            set_call_count(OldRef, false),
            set_call_count(Ref, NewStatus == enabled);
       true ->
            if OldStatus =/= NewStatus ->
                    set_call_count(Ref, NewStatus == enabled);
               true ->
                    %% Setting call_count again will reset the counter
                    %% so don't do it unnecessarily
                    ok
            end
    end,
    Elems1 = add_elem(ref, Ref, Elems),
    update_entry_elems(Name, Elems1),
    ok.

setopts_ctr(#exometer_entry{name = Name} = E, Options) ->
    {_, Elems} = process_setopts(E, Options),
    update_entry_elems(Name, Elems),
    ok.

setopts_gauge(E, Options) ->
    setopts_ctr(E, Options).  % same logic as for counter

%% cache(0, _, Value) ->
%%     Value;
%% cache(TTL, Name, Value) when TTL > 0 ->
%%     exometer_cache:write(Name, Value, TTL),
%%     Value.

update_entry_elems(Name, Elems) ->
    [ets:update_element(T, Name, Elems) || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
    ok.

-type info() :: name | type | module | value | cache
              | status | timestamp | options | ref | datapoints | entry.
-spec info(name(), info()) -> any().
%% @doc Retrieves information about a metric.
%%
%% Supported info items:
%%
%% * `name' - The name of the metric
%% * `type' - The type of the metric
%% * `module' - The callback module used
%% * `value' - The result of `get_value(Name)'
%% * `cache' - The cache lifetime
%% * `status' - Operational status: `enabled' or `disabled'
%% * `timestamp' - When the metric was last reset/initiated
%% * `datapoints' - Data points available for retrieval with get_value()
%% * `options' - Options passed to the metric at creation (or via setopts())
%% * `ref' - Instance-specific reference; usually a pid (probe) or undefined
%% @end
info(#exometer_entry{status = Status} = E, Item) ->
    info_(E, Item, info_status(Status));
info(Name, Item) ->
    case ets:lookup(exometer_util:table(), Name) of
        [#exometer_entry{status = Status0} = E] ->
            Status = info_status(Status0),
            info_(E, Item, Status);
        _ ->
            undefined
    end.

info_(E, Item, Status) ->
    case {Item, Status} of
        {name,             _} -> E#exometer_entry.name;
        {type,             _} -> E#exometer_entry.type;
        {module,           _} -> E#exometer_entry.module;
        {value,      enabled} -> get_value_(E, exometer_util:get_datapoints(E));
        {cache,      enabled} -> E#exometer_entry.cache;
        {status,           _} -> Status;
        {timestamp,  enabled} -> E#exometer_entry.timestamp;
        {options,          _} -> E#exometer_entry.options;
        {ref,              _} -> E#exometer_entry.ref;
        {entry,            _} -> E;
        {datapoints, enabled} -> exometer_util:get_datapoints(E);
        _ -> undefined
    end.

info_status(S) when ?IS_ENABLED(S) ->
    enabled;
info_status(_) ->
    disabled.

datapoints(default, _E) ->
    default;
datapoints(D, _) when is_atom(D) ->
    [D];
datapoints(D, _) when is_integer(D) ->
    [D];
datapoints(D, _) when is_list(D) ->
    D.

-spec info(name()) -> [{info(), any()}] | undefined.
%% @doc Returns a list of info items for Metric, see {@link info/2}.
info(Name) ->
    case ets:lookup(exometer_util:table(), Name) of
        [#exometer_entry{status = Status0} = E] ->
            Status = info_status(Status0),
            Flds = record_info(fields, exometer_entry),
            lists:map(fun(Item) -> {Item, info_(E, Item, Status)} end, Flds);
        _ ->
            undefined
    end.

-spec find_entries([any() | '_']) -> [{name(), type(), status()}].
%% @doc Find metrics based on a name prefix pattern.
%%
%% This function will find and return metrics whose name matches the given
%% prefix. For example `[kvdb, kvdb_conf, Table]' would match any metrics
%% tied to the given table in the `kvdb_conf' database.
%%
%% It is possible to insert wildcards:
%% <code>[kvdb, kvdb_conf, '_', write]</code> would match
%% `write'-related metrics in all tables of the `kvdb_conf' database.
%%
%% The format of the returned metrics is `[{Name, Type, Status}]'.
%% @end
find_entries(Path) ->
    Pat = Path ++ '_',
    ets:select(?EXOMETER_ENTRIES,
               [ { #exometer_entry{name = Pat, _ = '_'}, [],
                   [{{ {element, #exometer_entry.name, '$_'},
                       {element, #exometer_entry.type, '$_'},
                       status_body_pattern()
                     }}] } ]).

get_values(Path) ->
    Entries = find_entries(Path),
    lists:foldr(
      fun({Name, _Type, enabled}, Acc) ->
              case get_value(Name) of
                  {ok, V} ->
                      [{Name, V}|Acc];
                  {error,not_found} ->
                      Acc
              end
      end, [], Entries).

-spec select(ets:match_spec()) -> list().
%% @doc Perform an `ets:select()' on the set of metrics.
%%
%% This function operates on a virtual structure representing the metrics,
%% but otherwise works as a normal `select()'. The representation of the
%% metrics is `{Name, Type, Status}'.
%% @end
select(Pattern) ->
    ets:select(?EXOMETER_ENTRIES, [pattern(P) || P <- Pattern]).

-spec select_count(ets:match_spec()) -> non_neg_integer().
%% @doc Corresponds to {@link ets:select_count/1}.
%% @end
select_count(Pattern) ->
    ets:select_count(?EXOMETER_ENTRIES, [pattern(P) || P <- Pattern]).

-spec select(ets:match_spec(), pos_integer() | infinity) -> {list(), _Cont}.
%% @doc Perform an `ets:select()' with a Limit on the set of metrics.
%%
%% This function is equivalent to {@link select/1}, but also takes a limit.
%% After `Limit' number of matches, the function returns the matches and a
%% continuation, which can be passed to {@link select_cont/1}.
%% @end
select(Pattern, Limit) ->
    ets:select(?EXOMETER_ENTRIES, [pattern(P) || P <- Pattern], Limit).

-spec select_cont('$end_of_table' | tuple()) ->
                         '$end_of_table'
                             | {[{name(), type(), status()}], _Cont}.
%% @equiv ets:select(Cont)
%%
select_cont('$end_of_table') -> '$end_of_table';
select_cont(Cont) ->
    ets:select(Cont).

-spec aggregate(ets:match_spec(), [datapoint()]) -> list().
%% @doc Aggregate datapoints of matching entries.
%%
%% This function selects metric entries based on the given match spec, and
%% summarizes the given datapoint values.
%%
%% Note that the match body of the match spec will be overwritten, to produce
%% only the value for each entry matching the head and guard pattern(s).
%%
%% The function can for example be used inside a function metric:
%%
%% <pre lang="erlang"><![CDATA[
%% 1> exometer:start().
%% ok
%% 2> exometer:new([g,1], gauge, []).
%% ok
%% 3> exometer:new([g,2], gauge, []).
%% ok
%% 4> exometer:new([g,3], gauge, []).
%% ok
%% 5> [exometer:update(N,V) || {N,V} <- [{[g,1],3}, {[g,2],4}, {[g,3],5}]].
%% [ok,ok,ok]
%% 6> exometer:new([g], {function,exometer,aggregate,
%%                       [ [{{[g,'_'],'_','_'},[],[true]}], [value] ],
%%                       value, [value]}, []).
%% ok
%% 7> exometer:get_value([g], [value]).
%% {ok,[{value,12}]}
%% ]]></pre>
%% @end
aggregate(Pattern, DataPoints) ->
    case aggr_select(Pattern) of
        [] -> [];
        Found ->
            aggregate(Found, DataPoints, orddict:new())
    end.

aggr_select(Pattern) ->
    select([setelement(3,P,[{element,1,'$_'}]) || P <- Pattern]).

aggregate([N|Ns], DPs, Acc) when is_list(N) ->
    case get_value(N, DPs) of
        {ok, Vals} ->
            aggregate(Ns, DPs, aggr_acc(Vals, Acc));
        _ ->
            aggregate(Ns, DPs, Acc)
    end;
aggregate([], _, Acc) ->
    Acc.

aggr_acc([{D,V}|T], Acc) ->
    if is_integer(V) ->
            aggr_acc(T, orddict:update(D, fun(Val) ->
                                                  Val + V
                                          end, V, Acc));
       true ->
            aggr_acc(T, Acc)
    end;
aggr_acc([], Acc) ->
    Acc.

global_status(St) when St==enabled; St==disabled ->
    Prev = exometer_global:status(),
    if St =:= Prev -> ok;
       true ->
            parse_trans_mod:transform_module(
              exometer_global, fun(Forms,_) -> pt(Forms, St) end, [])
    end,
    Prev.

pt(Forms, St) ->
    parse_trans:plain_transform(
      fun(F) ->
              plain_pt(F, St)
      end, Forms).

plain_pt({function, L, status, 0, [_]}, St) ->
    {function, L, status, 0,
     [{clause, L, [], [], [{atom, L, St}]}]};
plain_pt(_, _) ->
    continue.


%% Perform variable replacement in the ets select pattern.
%% We want to project the entries as a set of {Name, Type, Status} tuples.
%% This means we need to perform variable substitution in both guards and
%% match bodies. Note that the 'status' attribute is now a bit map, but we
%% want the disabled|enabled 'status' to be presented (and also to be
%% matchable).
pattern({'_', Gs, Prod}) ->
    {'_', repl(Gs, g_subst(['$_'])), repl(Prod, p_subst(['$_']))};
pattern({KP, Gs, Prod}) when is_atom(KP) ->
    {KP, repl(Gs, g_subst([KP,'$_'])), repl(Prod, p_subst([KP,'$_']))};
pattern({{N,T,S}, Gs, Prod}) ->
    %% Remember the match head variables, at least if they are simple
    %% dollar variables, so that we can perform substitution on them
    %% later on.
    Tail = match_tail(N,T,S),
    {S1, Gs1} = repl_status(S, repl(Gs, g_subst(['$_'|Tail]))),
    {#exometer_entry{name = N, type = T, status = S1, _ = '_'},
     Gs1, repl(Prod, p_subst(['$_'|Tail]))}.

%% The "named variables" are for now undocumented.
repl('$name'       ,_) -> {element, #exometer_entry.name, '$_'};
repl('$type'       ,_) -> {element, #exometer_entry.type, '$_'};
repl('$options'    ,_) -> {element, #exometer_entry.options, '$_'};
repl('$status'     ,_) -> status_body_pattern();
repl('$status_bits',_) -> {element, #exometer_entry.status, '$_'};
repl('$ref'        ,_) -> {element, #exometer_entry.ref, '$_'};
repl('$behaviour'  ,_) -> {element, #exometer_entry.behaviour, '$_'};
repl('$cache'      ,_) -> {element, #exometer_entry.cache, '$_'};
repl('$timestamp'  ,_) -> {element, #exometer_entry.timestamp, '$_'};
repl(P, Subst) when is_atom(P) ->
    %% Check if P is one of the match variables we've saved.
    case lists:keyfind(P, 1, Subst) of
        {_, Repl} -> Repl;
        false     -> P
    end;
repl(T, Subst) when is_tuple(T) ->
    list_to_tuple(repl(tuple_to_list(T), Subst));
repl([H|T], Subst) ->
    [repl(H, Subst)|repl(T, Subst)];
repl(X, _) ->
    X.

repl_status(S, Gs) when S==enabled; S==disabled ->
    {'_', [{'=:=', status_body_pattern(), S}|Gs]};
repl_status(S, Gs) ->
    {S, Gs}.


g_subst(Ks) ->
    [g_subst_(K) || K <- Ks].

%% Potentially save Name, Type and Status match head patterns
match_tail(N,T,S) ->
    case is_dollar(N) of true ->
            [{N, {element,#exometer_entry.name,'$_'}}|match_tail(T,S)];
        false -> match_tail(T, S)
    end.
match_tail(T,S) ->
    case is_dollar(T) of true ->
            [{T, {element,#exometer_entry.type,'$_'}}|match_tail(S)];
        false -> match_tail(S)
    end.
match_tail(S) ->
    case is_dollar(S) of true ->
            [{S, status_element_pattern(S)}];
        false -> []
    end.

is_dollar(A) when is_atom(A) ->
    case atom_to_list(A) of
        [$$ | Rest] ->
            try _ = list_to_integer(Rest), true
            catch error:_ -> false
            end;
        _ -> false
    end;
is_dollar(_) -> false.


g_subst_({_,_}=X) -> X;
g_subst_(K) when is_atom(K) ->
    {K, {{{element,#exometer_entry.name,'$_'},
          {element,#exometer_entry.type,'$_'},
          status_element_pattern({element,#exometer_entry.status,'$_'})}}}.


%% The status attribute: bit 1 indicates enabled (1) or disabled (0)
status_element_pattern(S) ->
    %% S is the match head pattern corresponding to the status bits
    {element, {'+',{'band',S,1},1}, {{disabled,enabled}}}.

status_body_pattern() ->
    {element,
     {'+',{'band',
           {element,#exometer_entry.status,'$_'},1},1},
     {{disabled,enabled}}}.

p_subst(Ks) ->
    [p_subst_(K) || K <- Ks].
p_subst_({_,_}=X) -> X;
p_subst_(K) when is_atom(K) ->
    {K, {{{element,#exometer_entry.name,'$_'},
          {element,#exometer_entry.type,'$_'},
          status_body_pattern()}}}.

%% This function returns a list of elements for ets:update_element/3,
%% to be used for updating the #exometer_entry{} instances.
%% The options attribute is always updated, replacing old matching
%% options in the record.
process_setopts(#exometer_entry{
                   name = Name, ref = Ref, type = Type,
                   module = M, options = OldOpts} = Entry, Options) ->
    case M =/= exometer andalso
        erlang:function_exported(M, preprocess_setopts, 5) of
        true ->
            Options1 = M:preprocess_setopts(Name, Options, Type, Ref, OldOpts),
            process_setopts_(Entry, Options1);
        false ->
            process_setopts_(Entry, Options)
    end.

process_setopts_(#exometer_entry{options = OldOpts} = Entry, Options) ->
    {E1, Elems} =
        lists:foldr(
          fun
              ({cache, Val},
               {#exometer_entry{cache = Cache0} = E, Elems} = Acc) ->
                           if is_integer(Val), Val >= 0 ->
                                   if Val =/= Cache0 ->
                                           {E#exometer_entry{cache = Val},
                                            add_elem(cache, Val, Elems)};
                                      true ->
                                           Acc
                                   end;
                              true -> error({illegal, {cache, Val}})
                           end;
              ({status, Status}, {#exometer_entry{status = Status0} = E, Elems} = Acc) ->
                           case is_status_change(Status, Status0) of
                               {true, Status1} ->
                                   {E#exometer_entry{status = Status1},
                                    add_elem(status, Status1, Elems)};
                               false ->
                                   Acc
                           end;
              ({update_event, UE},
               {#exometer_entry{status = Status0} = E, Elems} = Acc)
          when is_boolean(UE) ->
                           case (exometer_util:test_event_flag(update, Status0) == UE) of
                               true -> Acc;
                               false ->
                                   %% value changed
                                   if UE ->
                                           Status = exometer_util:set_event_flag(
                                                      update, E#exometer_entry.status),
                                           {E#exometer_entry{status = Status},
                                            add_elem(status, Status, Elems)};
                                      true ->
                                           Status = exometer_util:clear_event_flag(
                                                      update, E#exometer_entry.status),
                                           {E#exometer_entry{status = Status},
                                            add_elem(status, Status, Elems)}
                                   end
                           end;
              ({ref,R}, {E, Elems}) ->
                           {E#exometer_entry{ref = R}, add_elem(ref, R, Elems)};
              ({_,_}, Acc) ->
                           Acc
                   end, {Entry, []}, Options),
    {E1, [{#exometer_entry.options, update_opts(Options, OldOpts)}|Elems]}.

is_status_change(enabled, St) ->
    if ?IS_ENABLED(St) -> false;
       true -> {true, exometer_util:set_status(enabled, St)}
    end;
is_status_change(disabled, St) ->
    if ?IS_DISABLED(St) -> false;
       true -> {true, exometer_util:set_status(disabled, St)}
    end.


add_elem(K, V, Elems) ->
    P = pos(K),
    lists:keystore(P, 1, Elems, {P, V}).

pos(cache) -> #exometer_entry.cache;
pos(status) -> #exometer_entry.status;
pos(ref) -> #exometer_entry.ref.

update_opts(New, Old) ->
    type_arg_first(lists:foldl(
                     fun({K,_} = Opt, Acc) ->
                             lists:keystore(K, 1, Acc, Opt)
                     end, Old, New)).

type_arg_first([{arg,_}|_] = Opts) ->
    Opts;

type_arg_first(Opts) ->
    case lists:keyfind(arg, 1, Opts) of
        false ->
            Opts;
        Arg ->
            [Arg|Opts -- [Arg]]
    end.



%% Retrieve individual data points for the counter maintained by
%% the exometer record itself.
get_ctr_datapoint(#exometer_entry{name = Name}, value) ->
    {value, counter_sum(Name)};
get_ctr_datapoint(#exometer_entry{name = Name}, value16) ->
    {value16, counter_sum(Name) rem 16#FFFF};
get_ctr_datapoint(#exometer_entry{name = Name}, value32) ->
    {value32, counter_sum(Name) rem 16#FFFFFFFF};
get_ctr_datapoint(#exometer_entry{name = Name}, value64) ->
    {value64, counter_sum(Name) rem 16#FFFFFFFFFFFFFFFF};
get_ctr_datapoint(#exometer_entry{timestamp = TS}, ms_since_reset) ->
    {ms_since_reset, exometer_util:timestamp() - TS};
get_ctr_datapoint(#exometer_entry{}, Undefined) ->
    {Undefined, undefined}.

counter_sum(Name) ->
    lists:sum([ets:lookup_element(T, Name, #exometer_entry.value)
               || T <- exometer_util:tables()]).

get_gauge_datapoint(#exometer_entry{value = Value}, value) ->
    {value, Value};
get_gauge_datapoint(#exometer_entry{timestamp = TS}, ms_since_reset) ->
    {ms_since_reset, exometer_util:timestamp() - TS};
get_gauge_datapoint(#exometer_entry{}, Undefined) ->
    {Undefined, undefined}.

get_fctr_datapoint(#exometer_entry{ref = Ref}, value) ->
    case Ref of
        {M, F} ->
            {call_count, Res} = erlang:trace_info({M, F, 0}, call_count),
            case Res of
                C when is_integer(C) ->
                    {value, C};
                _ ->
                    {value, 0}
            end;
        _ -> {value, 0}
    end;
get_fctr_datapoint(#exometer_entry{timestamp = TS }, ms_since_reset) ->
    {ms_since_reset, exometer_util:timestamp() - TS };
get_fctr_datapoint(#exometer_entry{ }, Undefined) ->
    {Undefined, undefined}.


create_entry(#exometer_entry{module = exometer,
                             type = Type} = E) when Type == counter;
                                                    Type == gauge ->
    E1 = E#exometer_entry{value = 0, timestamp = exometer_util:timestamp()},
    insert_aliases(E1),
    [ets:insert(T, E1) || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
    ok;

create_entry(#exometer_entry{module = exometer,
                             status = Status,
                             type = fast_counter, options = Opts} = E) ->
    case lists:keyfind(function, 1, Opts) of
        false ->
            error({required, function});
        {_, {M,F}} when is_atom(M), M =/= '_',
                        is_atom(F), M =/= '_' ->
            code:ensure_loaded(M),  % module must be loaded for trace_pattern
            E1 = E#exometer_entry{ref = {M, F}, value = 0,
                                  timestamp = exometer_util:timestamp()},
            set_call_count(M, F, ?IS_ENABLED(Status)),
            insert_aliases(E1),
            [ets:insert(T, E1) ||
                T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
            ok;
        Other ->
            error({badarg, {function, Other}})
    end;


create_entry(#exometer_entry{module = Module,
                             type = Type,
                             name = Name,
                             status = Status,
                             options = Opts} = E) ->
    case
        case Module:behaviour() of
            probe ->
                if ?IS_ENABLED(Status) ->
                        {probe, exometer_probe:new(
                                  Name, Type, [{ arg, Module} | Opts ])};
                   ?IS_DISABLED(Status) ->
                        %% Don't start probe if disabled
                        {probe, ok}
                end;
            entry ->
                {entry, Module:new(Name, Type, Opts) };

            Other -> Other
        end
    of
        {Behaviour, ok }->
            insert_aliases(E),
            [ets:insert(T, E#exometer_entry { behaviour = Behaviour })
             || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
            ok;
        {Behaviour, {ok, Ref}} ->
            insert_aliases(E),
            [ets:insert(T, E#exometer_entry{ref=Ref, behaviour=Behaviour})
             || T <- [?EXOMETER_ENTRIES|exometer_util:tables()]],
            ok;
        Other1 ->
            Other1
    end;
create_entry(_Other) ->
    {error, unknown_argument}.

insert_aliases(#exometer_entry{name = Name, options = Options}) ->
    case lists:keyfind(aliases, 1, Options) of
        {_, Aliases} ->
            case lists:all(fun({DP, Alias}) ->
                                   (is_atom(DP) orelse is_integer(DP))
                                       andalso (is_atom(Alias) orelse is_binary(Alias));
                              (_) -> false
                           end, Aliases) of
                true ->
                    [exometer_alias:new(Alias, Name, DP) || {DP, Alias} <- Aliases],
                    ok;
                false ->
                    erlang:error({invalid_aliases, Aliases})
            end;
        false ->
            ok
    end.

set_call_count({M, F}, Bool) ->
    set_call_count(M, F, Bool).

set_call_count(M, F, Bool) when is_atom(M), is_atom(F), is_boolean(Bool) ->
    erlang:trace_pattern({M, F, 0}, Bool, [call_count]).

-spec register_application() -> ok | error().
%% @equiv register_application(current_application())
%%
register_application() ->
    case application:get_application() of
        {ok, App} ->
            register_application(App);
        Other ->
            {error, Other}
    end.

-spec register_application(_Application::atom()) -> ok | error().
%% @doc Registers statically defined entries with exometer.
%%
%% This function can be used e.g. as a start phase hook or during upgrade.
%% It will check for the environment variables `exometer_defaults' and
%% `exometer_predefined' in `Application', and apply them as if it had
%% when exometer was first started. If the function is called again,
%% the settings are re-applied. This can be used e.g. during upgrade,
%% in order to change statically defined settings.
%%
%% If exometer is not running, the function does nothing.
%% @end
register_application(App) ->
    exometer_admin:register_application(App).
