%% -------------------------------------------------------------------
%%
%% 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 Exometer utility functions.
%% @end
-module(cloudi_x_exometer_util).

-export(
   [
    timestamp/0,
    timestamp_to_datetime/1,
    get_opt/2,
    get_opt/3,
    get_env/2,
    tables/0,
    table/0,
    get_statistics/3,
    get_statistics2/4,
    pick_items/2,
    perc/2,
    histogram/1,
    histogram/2,
    drop_duplicates/1,
    report_type/3,
    get_datapoints/1,
    set_call_count/2, set_call_count/3,
    get_status/1,
    set_status/2,
    set_event_flag/2,
    clear_event_flag/2,
    test_event_flag/2,
    ensure_all_started/1,
    seed/0,
    seed0/0,
    seed/1,
    uniform/0,
    uniform/1
   ]).

-export_type([timestamp/0]).

-include("cloudi_x_exometer.hrl").

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

-type timestamp() :: non_neg_integer().

-spec timestamp() -> timestamp().
%% @doc Generate a millisecond-resolution timestamp.
%%
%% This timestamp format is used e.g. by the `cloudi_x_exometer_slide' and
%% `cloudi_x_exometer_histogram' implementations.
%% @end
timestamp() ->
    %% Invented epoc is {1258,0,0}, or 2009-11-12, 4:26:40
    %% Millisecond resolution
    {MS,S,US} = os:timestamp(),
    (MS-1258)*1000000000 + S*1000 + US div 1000.

-spec timestamp_to_datetime(timestamp()) ->
                                   {calendar:datetime(), non_neg_integer()}.
%% @doc Convert timestamp to a regular datetime.
%%
%% The timestamp is expected
timestamp_to_datetime(TS) when TS >= 0 ->
    %% Our internal timestamps are relative to Now = {1258,0,0}
    %% It doesn't really matter much how we construct a now()-like tuple,
    %% as long as the weighted sum of the three numbers is correct.
    S = TS div 1000,
    MS = TS rem 1000,
    %% return {Datetime, Milliseconds}
    {calendar:now_to_datetime({1258,S,0}), MS}.

get_env(Key, Default) ->
    case get_env1(cloudi_x_exometer, Key) of
        {ok, Value} ->
            Value;
        _ ->
            case get_env1(cloudi_x_exometer_core, Key) of
                {ok, CoreValue} ->
                    CoreValue;
                _ ->
                    Default
            end
    end.

get_env1(App, Key) ->
    case application:get_env(App, Key) of
        {ok, undefined} -> undefined;
        Other           -> Other
    end.

get_opt(K, Opts) ->
    case lists:keyfind(K, 1, Opts) of
        {_, V} -> V;
        false ->
            error({required, K})
    end.

get_opt(K, Opts, Default) ->
    case lists:keyfind(K, 1, Opts) of
        {_, V} -> V;
        false  ->
            if is_function(Default,0) -> Default();
               true -> Default
            end
    end.

tables() ->
    [table(S) || S <- lists:seq(1,erlang:system_info(schedulers))].

table() ->
    table(erlang:system_info(scheduler_id)).

table(1) -> exometer_1;
table(2) -> exometer_2;
table(3) -> exometer_3;
table(4) -> exometer_4;
table(5) -> exometer_5;
table(6) -> exometer_6;
table(7) -> exometer_7;
table(8) -> exometer_8;
table(9) -> exometer_9;
table(10) -> exometer_10;
table(11) -> exometer_11;
table(12) -> exometer_12;
table(13) -> exometer_13;
table(14) -> exometer_14;
table(15) -> exometer_15;
table(16) -> exometer_16;
table(17) -> exometer_17;
table(18) -> exometer_18;
table(19) -> exometer_19;
table(20) -> exometer_20;
table(21) -> exometer_21;
table(22) -> exometer_22;
table(23) -> exometer_23;
table(24) -> exometer_24;
table(25) -> exometer_25;
table(26) -> exometer_26;
table(27) -> exometer_27;
table(28) -> exometer_28;
table(29) -> exometer_29;
table(30) -> exometer_30;
table(31) -> exometer_31;
table(32) -> exometer_32;
table(33) -> exometer_33;
table(34) -> exometer_34;
table(35) -> exometer_35;
table(36) -> exometer_36;
table(37) -> exometer_37;
table(38) -> exometer_38;
table(39) -> exometer_39;
table(40) -> exometer_40;
table(41) -> exometer_41;
table(42) -> exometer_42;
table(43) -> exometer_43;
table(44) -> exometer_44;
table(45) -> exometer_45;
table(46) -> exometer_46;
table(47) -> exometer_47;
table(48) -> exometer_48;
table(49) -> exometer_49;
table(50) -> exometer_50;
table(51) -> exometer_51;
table(52) -> exometer_52;
table(53) -> exometer_53;
table(54) -> exometer_54;
table(55) -> exometer_55;
table(56) -> exometer_56;
table(57) -> exometer_57;
table(58) -> exometer_58;
table(59) -> exometer_59;
table(60) -> exometer_60;
table(61) -> exometer_61;
table(62) -> exometer_62;
table(63) -> exometer_63;
table(64) -> exometer_64;
table(N) when is_integer(N), N > 20 ->
    list_to_atom("exometer_" ++ integer_to_list(N)).

%% @doc
%% `drop_duplicates/1' will drop all duplicate elements from a list of tuples identified by their first element.
%% Elements which are not tuples will be dropped as well.
%% If called with a non-list argument, the argument is returned as is.
%% @end
-spec drop_duplicates(List0 :: [tuple()]) -> [tuple()].
drop_duplicates(List0) when is_list(List0) ->
    List1 = lists:foldl(
              fun
                  (Elem, Acc) when is_tuple(Elem) ->
                               case lists:keymember(element(1, Elem), 1, Acc) of
                                   true ->
                                       Acc;
                                   false ->
                                       [Elem | Acc]
                               end;
                  (_, Acc) ->
                               Acc
                       end, [], List0),
    lists:reverse(List1);
drop_duplicates(Any) ->
    Any.

histogram(Values) ->
    Sorted = lists:sort(Values),
    Len = length(Sorted),
    Total = lists:foldl(fun(I,Acc) -> Acc + I end, 0, Sorted),
    get_statistics(Len, Total, Sorted).

histogram(Values, default) ->
    histogram(Values);
histogram(Values, DataPoints) ->
    H = histogram(Values),
    [DP || {K,_} = DP <- H,
           lists:member(K, DataPoints)].

-spec get_statistics(Length::non_neg_integer(),
                     Total::non_neg_integer(),
                     Sorted::list()) -> [{atom(), number()}].
%% @doc Calculate statistics from a sorted list of values.
%%
%% This function assumes that you have already sorted the list, and
%% now the number and sum of the elements in the list.
%%
%% The stats calculated are min, max, mean, median and the 50th,
%% 75th, 90th, 95th, 99th, and 99.9th percentiles (note that the
%% 99.9th percentile is labeled 999).
%%
%% This function is similar to `cloudi_x_bear:get_statistics_subset/2'.
%% `mean' refers to the arithmetic mean.
%%
%% Fulpatchad med min/max av Magnus Feuer.
%% @end

get_statistics(_, _, []) ->
    [];
get_statistics(L, Total, Sorted) ->
    get_statistics2(L, Sorted, Total, Total / L).

%%
%% Special case when we get called with an empty histogram.
get_statistics2(_L, [], _Total, _Mean) ->
    [];

get_statistics2(L, Sorted, Total, Mean) ->
    P50 = perc(0.5, L),
    Items = [{min,1}, {50, P50}, {median, P50}, {75, perc(0.75,L)},
             {90, perc(0.9,L)}, {95, perc(0.95,L)}, {99, perc(0.99,L)},
             {999, perc(0.999,L)}, {max,L}],
    [{n,L}, {mean, Mean}, {total, Total} | pick_items(Sorted, Items)].

-spec pick_items([number()], [{atom() | integer(), integer()}]) ->
                        [{atom(), number()}].
%% @doc Pick values from specified positions in a sorted list of numbers.
%%
%% This function is used to extract datapoints (usually percentiles) from
%% a sorted list of values. `Items' is a list of `{Datapoint, Position}'
%% entries.
%% @end
pick_items(Vals, Items) ->
    pick_items(Vals, 1, Items).

pick_items([H|_] = L, P, [{Tag,P}|Ps]) ->
    [{Tag,H} | pick_items(L, P, Ps)];

pick_items([_|T], P, Ps) ->
    pick_items(T, P+1, Ps);

pick_items([], _, Ps) ->
    [{Tag, 0.0} || {Tag,_} <- Ps].

perc(P, Len) when P > 1.0 ->
    round((P / 10) * Len);

perc(P, Len) ->
    round(P * Len).


report_type(Key, Extra, TypeMap) when is_list(Extra) ->
    case lists:keyfind(report_type, 1, Extra) of
        {_, Type} -> {ok, Type};
        false     -> check_type(Key, TypeMap)
    end;
report_type(Key, _, TypeMap) ->
    check_type(Key, TypeMap).

check_type(K, [{K, Type}|_]) ->
    {ok, Type};
check_type(K, [{KPat, Type}|T]) ->
    case key_match(KPat, K) of
        true  -> {ok, Type};
        false -> check_type(K, T)
    end;
check_type(_, []) ->
    error.

key_match([H|T], [H|T1]) ->
    key_match(T, T1);
key_match(['_'|T], [_|T1]) ->
    key_match(T, T1);
key_match([], []) -> true;
key_match('_', _) -> true;
key_match(_, _)   -> false.


get_datapoints(#cloudi_x_exometer_entry{module = cloudi_x_exometer,
                               type = T}) when T==counter;
                                               T==fast_counter;
                                               T==gauge ->
    [value, ms_since_reset];
get_datapoints(#cloudi_x_exometer_entry{behaviour = entry,
                               name = Name, module = M,
                               type = Type, ref = Ref}) ->
    M:get_datapoints(Name, Type, Ref);
get_datapoints(#cloudi_x_exometer_entry{behaviour = probe,
                               name = Name, type = Type, ref = Ref}) ->
    cloudi_x_exometer_probe:get_datapoints(Name, Type, Ref).

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]).

get_status(enabled) -> enabled;
get_status(disabled) -> disabled;
get_status(St) when is_integer(St) ->
    if St band 2#1 == 1 ->
            enabled;
       true ->
            disabled
    end.

set_status(enabled , enabled ) -> 1;
set_status(enabled , disabled) -> 1;
set_status(enabled , St      ) -> St bor 2#1;
set_status(disabled, enabled ) -> 0;
set_status(disabled, disabled) -> 0;
set_status(disabled, St      ) -> St band 2#11111110.

set_event_flag(update, St) when is_integer(St) ->
    St bor 2#10;
set_event_flag(update, enabled ) -> 2#11;
set_event_flag(update, disabled) -> 2#10.


clear_event_flag(update, St) when is_integer(St) ->
    St band 2#11111101;
clear_event_flag(update, enabled ) -> 1;
clear_event_flag(update, disabled) -> 0.

test_event_flag(update, St) when St band 2#10 =:= 2#10 -> true;
test_event_flag(update, _) -> false.

%% This implementation is originally from Basho's Webmachine. On
%% older versions of Erlang, we don't have
%% application:ensure_all_started, so we use this wrapper function to
%% either use the native implementation or our own version, depending
%% on what's available.
-spec ensure_all_started(atom()) -> {ok, [atom()]} | {error, term()}.
ensure_all_started(App) ->
    %% Referencing application:ensure_all_started/1 will anger Xref
    %% in earlier R16B versions of OTP
    ensure_all_started(App, []).

-ifdef(rand_module).
seed() ->
    {0, 0, 0}.

seed0() ->
    {0, 0, 0}.

seed({A, B, C}) ->
    %% rand in the erlang version 18 or above, use a different seed methond, does not use the seed like that any more
    {A, B, C}.

uniform() ->
    rand:uniform().

uniform(N) ->
    rand:uniform(N).

-else.
seed() ->
    random:seed().

seed0() ->
    random:seed0().

seed({A, B, C}) ->
    random:seed({A, B, C}).

uniform() ->
    random:uniform().

uniform(N) ->
    random:uniform(N).

-endif.

%% This implementation is originally from Basho's
%% Webmachine. Reimplementation of ensure_all_started. NOTE this does
%% not behave the same as the native version in all cases, but as a
%% quick hack it works well enough for our purposes. Eventually I
%% assume we'll drop support for older versions of Erlang and this can
%% be eliminated.
ensure_all_started(App, Apps0) ->
    case application:start(App) of
        ok ->
            {ok, lists:reverse([App | Apps0])};
        {error,{already_started,App}} ->
            {ok, lists:reverse(Apps0)};
        {error,{not_started,BaseApp}} ->
            {ok, Apps} = ensure_all_started(BaseApp, Apps0),
            ensure_all_started(App, [BaseApp|Apps])
    end.


%% EUnit tests
-ifdef(TEST).

key_match_test() ->
    {ok,yes} = report_type([a,b,c], [], [{'_',yes}]),
    {ok,yes} = report_type([a,b,c], [], [{[a,b], no},
                                         {[a,b,c], yes},
                                         {[a,b,c], no}]), % match on first
    {ok,yes} = report_type([a,b,c], [], [{[a,b,'_'], yes}]),
    {ok,yes} = report_type([a,b,c], [], [{[a,'_',c], yes}]),
    {ok,yes} = report_type([a,b,c], [], [{[a,b|'_'], yes}]),
    {ok,yes} = report_type([a,b,c], [{report_type,yes}], [{[a,b,c], no}]),
    ok.

-endif.
