%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 随机函数
%%% @end
%%% -------------------------------------------------------
-module(urand).
-export([
        rand/2                                      % 产生一个介于Min到Max之间的随机整数
        ,rand_with_weight/1                         % 从权值列表中随机一项
        ,rand_with_percent/2
        ,rand_with_percent_get_elem/3
        ,list_rand/1
        ,get_rand_list/2
        ,list_rand_by_weight/2
        ,list_rand_by_weight/3
        ,list_shuffle/1
        ,repeat_list_rand_by_weight/2
        ,rand_char10/0                              % 随机长度为10的字符串
    ]).

%% 产生一个介于Min到Max之间的随机整数
rand(Same, Same) -> Same;
rand(Min, Max) when Max < Min ->
    rand(Max, Min);
rand(Min, Max) ->
    case rand:export_seed() of
        undefined ->
            %% 首次调用crypto:strong_rand_bytes/1较慢，尤其Windows上很明显
            %% 但是对游戏服影响可忽略，所以启动时不作特殊调用一次的处理
            <<A:32, B:32, C:32>> = crypto:strong_rand_bytes(12),
            %% Erlang OTP 18之后，rand:uniform/1会自动调用seed
            %% 但是这里不使用默认精度的算法和自动创建的随机种子
            rand:seed(exs1024, {A, B, C});
        _ -> ignore
    end,
    M = Min - 1,
    if
        Max - M =< 0 ->
            0;
        true ->
            M + rand:uniform(Max - M)
    end.

%% rand_with_weight(WeightList) -> Elem | false
%% 输出带有权值的的随机项
%% WeightList = [{权值(Weight), 项(Elem)}, ...]
%% Weight = integer() > 0
rand_with_weight([]) -> false;
rand_with_weight(WeightList) ->
    Max  = lists:sum([Weight||{Weight, _Elem} <- WeightList]),
    Rand = rand(1, Max),
    rand_with_weight_get_elem(WeightList, Rand, 0).
%% 获取对应的随机项
rand_with_weight_get_elem([], _, _) -> false;
rand_with_weight_get_elem([{Weight, Elem}|T], Rand, WeightSum) ->
    case Rand =< Weight+WeightSum of
        true  -> Elem;
        false -> rand_with_weight_get_elem(T, Rand, Weight+WeightSum)
    end.

%% rand_with_percent(PercentList) -> Elem | false
%% PercentList: 固定概率列表 [{概率分子(Percent), 项(Elem)}, ...]
%% Sum:总概率（概率分母）
rand_with_percent([], _) -> false;
rand_with_percent(PercentList, Sum) ->
    Rand = rand(1, Sum),
    rand_with_percent_get_elem(PercentList, Rand, 0).
%% 获取对应的随机项
rand_with_percent_get_elem([], _, _) -> false;
rand_with_percent_get_elem([{Percent, Elem}|T], Rand, I) ->
    case Rand =< Percent+I of
        true  -> Elem;
        false -> rand_with_percent_get_elem(T, Rand, Percent+I)
    end.

%% 从一个list中随机取出一项
%% null | term()
list_rand([]) -> null;
list_rand([I]) -> I;
list_rand(List) ->
    Len = length(List),
    Index = rand(1, Len),
    get_term_from_list(List, Index).
get_term_from_list(List, 1) ->
    [Term|_R] = List,
    Term;
get_term_from_list(List, Index) ->
    [_H|R] = List,
    get_term_from_list(R, Index - 1).

%% @doc 从列表中不重复选择N个元素，返回选中元素构成的新列表
get_rand_list(N, List) ->
    Len = length(List),
    get_rand_list(N, List, Len, []).

get_rand_list(0, _List, _Len, AccList) -> AccList;
get_rand_list(_N, [], _Len, AccList) -> AccList;
get_rand_list(N, List, Len, AccList) ->
    Idx = rand(1, Len),
    Rand = get_term_from_list(List, Idx),
    get_rand_list(N - 1, lists:delete(Rand, List), Len - 1, [Rand | AccList]).

%% ---------------------------------------------------------------------------
%% @doc 从列表中根据权重概率随机出N个元素. 默认取出来不重复
-spec list_rand_by_weight(WeightList :: [{Weight :: integer(), term()}], N :: integer()) -> Elems :: [term()].
%% ---------------------------------------------------------------------------
list_rand_by_weight(WeightList, N) ->
    list_rand_by_weight(WeightList, N, false).

list_rand_by_weight(WeightList, N, IsRepeat) when is_list(WeightList) andalso N >=0 ->
    Length = length(WeightList),
    case N of
        0 -> [];
        Num1 when Num1 >= Length ->
            [Elem || {_Weight, Elem} <- WeightList];
        Num2 when Num2 < Length ->
            TotalWeight = lists:foldl(
                            fun({Weight, _Gain}, Sum) -> Weight + Sum end, 0, WeightList
                           ),
            list_rand_by_weight(WeightList, N, IsRepeat, TotalWeight, []);
        _ ->
            []
    end;
list_rand_by_weight(_WeightList, _N, _IsRepeat) -> [].

list_rand_by_weight(WeightList, N, IsRepeat, TotalWeight, Result) when N >= 1 ->
    RandWeight = urand:rand(1, TotalWeight),
    {Weight, Gain} = find_one_gain_by_rate(WeightList, RandWeight, 1, 0),
    NewWeightList = case IsRepeat of
        true -> WeightList;
        false -> lists:delete({Weight, Gain}, WeightList)
    end,
    list_rand_by_weight(NewWeightList, N-1, IsRepeat, TotalWeight-Weight, [Gain | Result]);
list_rand_by_weight(_WeightList, _N, _IsRepeat, _TotalWeight, Result) ->
    Result.

find_one_gain_by_rate(List, RandWeight, ArrayNth, LeftValue) ->
    {Weight, Gain} = lists:nth(ArrayNth, List),
    RightValue = LeftValue + Weight,
    case RandWeight > LeftValue andalso RandWeight =< RightValue of
        true ->
            {Weight, Gain};
        false ->
            find_one_gain_by_rate(List, RandWeight, ArrayNth + 1, RightValue)
    end.

%% 随机打乱list元素顺序
list_shuffle(L = [_, _ | _]) ->
    Len = length(L) + 10000,
    List1 = [{rand(1, Len), X} || X <- L],
    List2 = lists:sort(List1),
    [E || {_, E} <- List2];
list_shuffle(L) ->
    %% 空列表或一个元素，不需要打乱
    L.



%% ---------------------------------------------------------------------------
%% @doc 从列表中根据权重概率随机出N个元素(元素可能会重复出现)
-spec repeat_list_rand_by_weight(WeightList, N) -> Elems when
    WeightList     :: [{Weight :: integer(), term()}],   %% 权重列表
    N              :: integer(),                         %% 抽出的个数
    Elems          :: [term()].                          %% 抽出的元素列表
%% ---------------------------------------------------------------------------
repeat_list_rand_by_weight(WeightList, N) when is_list(WeightList) andalso N >=0 ->
    case N of
        0 ->
            [];
        _ ->
            TotalWeight = lists:foldl(
                fun({Weight, _Gain}, Sum) -> Weight + Sum end, 0, WeightList
            ),
            repeat_list_rand_by_weight(WeightList, N, TotalWeight, [])
    end.
repeat_list_rand_by_weight(WeightList, N, TotalWeight, Result) when N >= 1 ->
    RandWeight = rand(1, TotalWeight),
    {_Weight, Gain} = find_one_gain_by_rate(WeightList, RandWeight, 1, 0),
    repeat_list_rand_by_weight(WeightList, N-1, TotalWeight, [Gain | Result]);
repeat_list_rand_by_weight(_WeightList, _N, _TotalWeight, Result) ->
    Result.

%% 随机长度为10的字符串（二进制形式，基于0-9和a-z的字符串）
-spec rand_char10() -> BinStr :: binary().
%% 结果示例：<<"sb9e84m19t">>.
%% 说明：如果多次从36个字符中随机字母组成字符串，消耗比较大。
%% 而由于36进制数的表示正是通过字符0-9与a-z。所以在要求不高的情况下，
%% 可以用随机36进制数，输出对应宽度的方式来获取对应宽度的随机字符串。
rand_char10() ->
    iolist_to_binary(io_lib:format("~10.36.0b", [urand:rand(0, 36#zzzzzzzzzz)])).

