%% ------------------------------
%% desc     :幻光副本扫荡排行&恢复次数计时
%% module   :tower_dun_rank.erl
%% author   :hh
%% time     :21/05/11
%% ------------------------------

-module (tower_dun_rank).

-behaviour (gen_server).

-include ("common.hrl").
-include ("dungeon.hrl").
-include ("predefine.hrl").

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

-export ([
    start_link/0
    ,refresh_tower_rank/3
    ,calc_tower_star/3
    ,get_tower_dun_rank/5
    ,get_access_index_by_star/1
    ,load_tower_star/1
    ,add_role_recover/2
    ]).

-define (RANK_MAX, 100).

-define (REFRESH_RANK, 30).

-define (RANK_INTERVAL, 180000).

-define (SELECT_TOWER_RANK, 
    <<"SELECT `role_id`, `role_name`, `star`, `time` FROM role_tower_rank">>).
-define (REPLACE_TOWER_RANK, 
    <<"REPLACE INTO role_tower_rank SET `role_id` = ~p, `role_name` = ~ts, `star` = ~p, `time` = ~p">>).
-define (DELETE_TOWER_RANK, 
    <<"DELETE FROM role_tower_rank WHERE role_id = ~p">>).

-record (tower_rank_state, {
    rank = [],          %% #tower_rank_data{} 排行数据
    limit = 0,          %% 上榜限制
    last_time = 0,      %% 上次更新时间
    sort_ref = undefined,
    recover_ref = [], %% 
    need_rank = 0,      %% 是否需要刷新数据 0否|1是
    recover_list = []
    }).

-record (tower_rank_data, {
    id = 0,             %% 玩家id
    name = "",          %% 玩家名字
    star = 0,           %% 总星数
    time = 0            %% 上榜时间
    }).

-define(RECOVER_ROLE(RoleId), {recover_role, RoleId}).

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% 通关完成副本更新排行榜数据
refresh_tower_rank(RoleId, RoleName, Star) ->
    gen_server:cast(?MODULE, {refresh_tower_rank, RoleId, RoleName, Star}).

get_tower_dun_rank(RoleId, Sid, Start, Len, AllStar) ->
    gen_server:cast(?MODULE, {get_tower_dun_rank, RoleId, Sid, Start, Len, AllStar}).

add_role_recover(RoleId, EndTime) ->
    gen_server:cast(?MODULE, {add_role_recover, RoleId, EndTime}).

init([]) ->
    erlang:send_after(1000, self(), {base_init}),
    {ok, #tower_rank_state{}}.

handle_call(_Request, _From, State) ->
    {reply, ok, State}.

handle_cast(Msg, State) ->
    try do_handle_cast(Msg, State) of
    Reply -> Reply
    catch ?EXCEPTION(Class, Reason, StackTrace) ->
        Error = {Class, Reason, ?GET_STACK(StackTrace)},
        ?ERR("tower dun rank ~w:state error:~p~n", [self(), Error]),
        {noreply, State}
    end.

handle_info(Msg, State) ->
    try do_handle_info(Msg, State) of
    Reply -> Reply
    catch ?EXCEPTION(Class, Reason, StackTrace) ->
        Error = {Class, Reason, ?GET_STACK(StackTrace)},
        ?ERR("tower dun rank ~w:state error:~p~n", [self(), Error]),
        {noreply, State}
    end.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

do_handle_cast({refresh_tower_rank, RoleId, RoleName, Star}, State) ->
    #tower_rank_state{rank = RankData, last_time = LastTime, limit = LimitLevel} = State,
    Now = utime:unixtime(),
    RestTime = Now - LastTime,
    RankRole = #tower_rank_data{id = RoleId, name = RoleName, star = Star, time = Now},
    {NewRankData, MinLimit} = update_tower_rank(RankRole, RankData, LimitLevel),
    if
        RestTime >= ?REFRESH_RANK ->
            {SortRankData, MinLimit1} = sort_rank_info(NewRankData),
            NewState = State#tower_rank_state{rank = SortRankData, limit = MinLimit1, last_time = Now, need_rank = 0},
            {noreply, NewState};
        true ->
            NewState = State#tower_rank_state{rank = NewRankData, limit = MinLimit, need_rank = 1},
            {noreply, NewState}
    end;

do_handle_cast({get_tower_dun_rank, RoleId, Sid, Start, Len, AllStar}, State) ->
    %% 获取排行数据前先检查是否需要更新排序数据
    #tower_rank_state{rank = RankList, need_rank = NeedRank, limit = OldMinLimit} = State,
    if NeedRank == 1 ->
            {NewRankList, MinLimit} = sort_rank_info(RankList);
        true ->
            NewRankList = RankList, MinLimit = OldMinLimit
    end,
    Sum = length(NewRankList),
    NewState =
    case Start > Sum orelse Start =< 0 orelse Len =< 0 of
        true ->
            % Rank = calc_role_rank(RoleId, NewRankList),
            RankInfos   = pack_rank_list(NewRankList, 1),
            case lists:keyfind(RoleId, 3, RankInfos) of
                {Rank, _, _, _, _} -> ok;
                _ -> Rank = 0
            end,
            lib_server_send:send_to_sid(Sid, pt_611, 61116, [[], AllStar, Rank, Sum]),
            if NeedRank == 1 ->
                    State#tower_rank_state{rank = NewRankList, limit = MinLimit, need_rank = 0, last_time = utime:unixtime()};
                true ->
                    State
            end;
        _ ->
            RankInfos   = pack_rank_list(NewRankList, 1),
            SubRankInfos = lists:sublist(RankInfos, Start, Len),
            case lists:keyfind(RoleId, 3, RankInfos) of
                {Rank, _, _, _, _} -> ok;
                _ -> Rank = 0
            end,
            % ?ERR("~p ~p", [RankInfos, Rank]),
            lib_server_send:send_to_sid(Sid, pt_611, 61116, [SubRankInfos, AllStar, Rank, Sum]),
            if NeedRank == 1 ->
                    State#tower_rank_state{rank = NewRankList, limit = MinLimit, need_rank = 0, last_time = utime:unixtime()};
                true ->
                    State
            end
    end,
    {noreply, NewState};

do_handle_cast({add_role_recover, RoleId, EndTime}, State) ->
    #tower_rank_state{recover_list = RecoverList, recover_ref = RecoverRef} = State,
    Now = utime:unixtime(),
    if 
        EndTime == 0 -> %% 0表示该玩家次数已恢复满,无需定时通知
            NewRecoverList =
            case lists:keymember(RoleId, 1, RecoverList) of
                true -> lists:keydelete(RoleId, 1, RecoverList);
                _    -> RecoverList
            end,
            NewRecoverRef =
            case lists:keyfind(RoleId, 1, RecoverRef) of
                {_, Ref} -> util:cancel_timer(Ref), [];
                _    -> RecoverRef
            end;
        true ->
            case lists:keymember(RoleId, 1, RecoverRef) of
                true -> %% 等待旧次数恢复
                    NewRecoverList = RecoverList,
                    NewRecoverRef = RecoverRef;
                _    ->
                    NewRecoverList = lists:keystore(RoleId, 1, RecoverList, {RoleId, EndTime}),
                    NewRecoverRef = RecoverRef
            end
    end,
    if RecoverRef == [] ->
            SortRecoverList = lists:keysort(2, NewRecoverList),
            {NewRecoverList1, NewRecoverRef1} = calc_next_recover(SortRecoverList);
        true ->
            [{MinRoleId, OldRef}|_] = RecoverRef,
            Now = utime:unixtime(),
            if is_reference(OldRef) ->
                    LastTime = erlang:read_timer(OldRef),
                    LastTime1 = LastTime div 1000,
                    RemTime = LastTime rem 1000,
                    if RemTime > 0 -> ExtraTime = 1;
                        true -> ExtraTime = 0
                    end,
                    RoleLastTime = EndTime - Now,
                    if LastTime1 > RoleLastTime ->
                            NewRecoverList0 = lists:keystore(MinRoleId, 1, NewRecoverList, {MinRoleId, Now+LastTime1+ExtraTime}),
                            SortRecoverList = lists:keysort(2, NewRecoverList0),
                            {NewRecoverList1, NewRecoverRef1} = calc_next_recover(SortRecoverList);
                        true ->
                            NewRecoverList1 = NewRecoverList,
                            NewRecoverRef1   = NewRecoverRef
                    end;
                true ->
                    NewRecoverList1 = NewRecoverList,
                    NewRecoverRef1 = NewRecoverRef
            end
    end,
    % ?ERR("add role recover ~p ~p ~p ~p", [RoleId, EndTime, NewRecoverList1, NewRecoverRef1]),
    NewState = State#tower_rank_state{recover_list = NewRecoverList1, recover_ref = NewRecoverRef1},
    {noreply, NewState};

do_handle_cast(_Msg, State) ->
    {noreply, State}.

do_handle_info({base_init}, State) ->
    NewState =
    case db:get_all(?SELECT_TOWER_RANK) of
        DataList = [_|_] ->
            {RankData, LimitLevel} = make_rank_info(DataList),
            Now = utime:unixtime(),
            State#tower_rank_state{rank = RankData, last_time = Now, limit = LimitLevel};
        _ ->
            State
    end,
    Ref = erlang:send_after(?RANK_INTERVAL, self(), {sort_rank_info}),
    {noreply, NewState#tower_rank_state{sort_ref = Ref}};

do_handle_info({sort_rank_info}, State) ->
    #tower_rank_state{rank = RankData, last_time = LastTime, sort_ref = OldRef} = State,
    Now = utime:unixtime(),
    RestTime = Now - LastTime,
    if 
        RestTime >= ?REFRESH_RANK -> %% 超过30秒,刷新一波
            {SortRankData, MinLimit} = sort_rank_info(RankData),
            NewRef = util:send_after(OldRef, ?RANK_INTERVAL, self(), {sort_rank_info}),
            NewState = State#tower_rank_state{rank = SortRankData, sort_ref = NewRef, last_time = Now, limit = MinLimit, need_rank = 0};
        true ->
            NewRef = util:send_after(OldRef, ?RANK_INTERVAL, self(), {sort_rank_info}),
            NewState = State#tower_rank_state{sort_ref = NewRef}
    end,
    {noreply, NewState};

do_handle_info({recover_time_out, RoleId, _EndTime}, State) ->
    #tower_rank_state{recover_list = RecoverList, recover_ref = RecoverRef} = State,
    case RecoverRef of
        [{_, Ref}|_] -> util:cancel_timer(Ref);
        _ -> skip
    end,
    lib_player:apply_cast(RoleId, ?APPLY_CAST_STATUS, lib_dungeon_tower, role_recover_time_out, []),
    SortRecoverList = lists:keysort(2, RecoverList),
    {NewRecoverList, NewRecoverRef} = calc_next_recover(SortRecoverList),
    NewState = State#tower_rank_state{recover_list = NewRecoverList, recover_ref = NewRecoverRef},
    {noreply, NewState};

do_handle_info(_Msg, State) ->
    {noreply, State}.

make_rank_info(DataList) ->
    DataList1 = make_rank_info(DataList, []),
    sort_rank_info(DataList1).

make_rank_info([], AccRank) -> AccRank;
make_rank_info([[RoleId, RoleName, Star, Time|_]|Rest], AccRank) ->
    RankInfo = #tower_rank_data{
        id = RoleId,
        name = RoleName,        
        star = Star,
        time = Time
    },
    make_rank_info(Rest, [RankInfo|AccRank]).

sort_rank_info(RankList) ->
    SortFun = fun(#tower_rank_data{star = S1, time = T1}, 
        #tower_rank_data{star = S2, time = T2}) -> 
            if
                S1 > S2  -> true;
                S1 == S2 -> T1 =< T2;
                true -> false 
            end
    end,
    SortRankData = lists:sort(SortFun, RankList),
    RankLen = length(SortRankData),
    if
        RankLen >= ?RANK_MAX ->
            {SortRankData1, DeleteList} = ulists:sublist(SortRankData, ?RANK_MAX),
            %% 定期删除数据
            DeleteList =/= [] andalso delete_old_rank_data(DeleteList),
            #tower_rank_data{star = MinLimit} = lists:last(SortRankData1),
            {SortRankData1, MinLimit};
        true ->
            if SortRankData == [] -> {SortRankData, 0};
                true -> 
                    #tower_rank_data{star = MinLimit} = lists:last(SortRankData),
                    {SortRankData, MinLimit}
            end
    end.

delete_old_rank_data(RankList) ->
    [db:execute(?DELETE_TOWER_RANK, [RoleId]) || #tower_rank_data{id = RoleId} <- RankList],
    ok.

db_replace_rank_role(RankRole) ->
    #tower_rank_data{id = RoleId, name = RoleName, star = Star, time = Time} = RankRole,
    db:execute(?REPLACE_TOWER_RANK, [RoleId, RoleName, Star, Time]),
    ok.

update_tower_rank(RankRole, RankData, LimitStar) ->
    RankLen = length(RankData),
    #tower_rank_data{id = RoleId, star = Star} = RankRole,
    if
        RankLen >= ?RANK_MAX ->
            if Star < LimitStar ->
                    {RankData, LimitStar};
                true ->
                    case lists:keyfind(RoleId, #tower_rank_data.id, RankData) of
                        #tower_rank_data{star = OldStar} when Star > OldStar ->
                            NewRankData = lists:keystore(RoleId, #tower_rank_data.id, RankData, RankRole),
                            db_replace_rank_role(RankRole),
                            {NewRankData, min(LimitStar, Star)};
                        false ->
                            NewRankData = lists:keystore(RoleId, #tower_rank_data.id, RankData, RankRole),
                            db_replace_rank_role(RankRole),
                            {NewRankData, min(LimitStar, Star)};
                        _ ->
                            {RankData, LimitStar}                      
                    end
            end;
        true ->
            case lists:keyfind(RoleId, #tower_rank_data.id, RankData) of
                #tower_rank_data{star = OldStar} when Star > OldStar ->
                    NewRankData = lists:keystore(RoleId, #tower_rank_data.id, RankData, RankRole),
                    db_replace_rank_role(RankRole),
                    {NewRankData, min(LimitStar, Star)};
                false ->
                    NewRankData = lists:keystore(RoleId, #tower_rank_data.id, RankData, RankRole),
                    db_replace_rank_role(RankRole),
                    {NewRankData, min(LimitStar, Star)};
                _ ->
                    {RankData, LimitStar}                      
            end        
    end.

calc_tower_star(DunId, Score, DunRecs) ->
    DiffcultDuns = data_tower_dun:get_all_diffcult_dun(),
    AllStar = lists:foldl(fun(CfgDunId, AccStar) ->
        if CfgDunId == DunId ->
                AccStar + Score;
            true ->
                DunRec = maps:get(CfgDunId, DunRecs, []),
                case lists:keyfind(?DUNGEON_REC_MAX_STARS, 1, DunRec) of
                    {_, StarList} ->
                        AccStar + length(StarList);
                    _ -> AccStar
                end
        end
    end, 0, DiffcultDuns),
    AllStar.

get_access_index_by_star(Star) ->
    AllStarIndex = data_tower_dun:get_all_star(),
    lists:foldl(fun({Index, CfgStar}, Acc) -> 
        if CfgStar =< Star -> [Index|Acc];
            true -> Acc
        end
    end, [], AllStarIndex).

pack_rank_list(SubRankList, Index) ->
    {_, _, RankInfos} = lists:foldl(fun(RankData, {AccIndex, {PreIndex, PreStar}, AccRankInfo}) -> 
        #tower_rank_data{
            id = RoleId,
            name = RoleName,
            star = Star,
            time = Time
        } = RankData,
        if Star == PreStar ->
                {AccIndex+1, {PreIndex, PreStar}, [{PreIndex, RoleName, RoleId, Star, Time}|AccRankInfo]};
            true ->
                {AccIndex+1, {AccIndex, Star}, [{AccIndex, RoleName, RoleId, Star, Time}|AccRankInfo]}
        end
    end, {Index, {0, 0}, []}, SubRankList),
    lists:reverse(RankInfos).

% calc_role_rank(RoleId, RankList) ->
%     {_, Rank} =
%     lists:foldl(fun(#tower_rank_data{id = RoleIdX}, {Index, OldIndex}) -> 
%         if RoleId == RoleIdX -> {Index+1, Index};
%             true -> {Index+1, OldIndex}
%         end
%     end, {1, 0}, RankList),
%     Rank.

load_tower_star(RoleId) ->
    DiffcultDuns = data_tower_dun:get_all_diffcult_dun(),
    lists:foldl(fun(DunId, Acc) -> 
        case catch db:get_one(<<"SELECT `data` FROM dungeon_best_record WHERE `player_id` = ~p AND `dun_id` = ~p">>, [RoleId, DunId]) of
            DataBin when is_binary(DataBin) orelse is_bitstring(DataBin) ->
                DataList = util:bitstring_to_term(DataBin),
                case lists:keyfind(?DUNGEON_REC_MAX_STARS, 1, DataList) of
                    {_, StarList} ->
                        Acc + length(StarList);
                    _ -> Acc
                end;
            _ -> Acc
        end
    end, 0, DiffcultDuns).

calc_next_recover([]) -> {[], []};
calc_next_recover([{RoleId, EndTime}|NewRecoverList]) ->
    Now = utime:unixtime(),
    if Now < EndTime ->
            NextTime = (EndTime - Now)*1000,
            NewRecoverRef = util:send_after(undefined, NextTime, self(), {recover_time_out, RoleId, EndTime}),
            {NewRecoverList, [{RoleId, NewRecoverRef}]};
        true -> %% Now >= EndTime
            lib_player:apply_cast(RoleId, ?APPLY_CAST_STATUS, lib_dungeon_tower, role_recover_time_out, []),
            calc_next_recover(NewRecoverList)
    end.
