%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 跨服中心节点中，游戏服活跃度更新管理
%%% 负责收集各服的活跃度数据，计算出阵营活跃度，
%%% 用作跨服重新分区时的阵营划分依据
%%%
%%% 跨服中心节点工作机制
%%%  - 各服上传数据时，按服分别存储角色#power_role{}数据
%%%  - 定期通知各服上传数据（由跨服的crontab定义任务）
%%%  - 收到定期上传数据后（发出通知后30秒），重新计算阵营活跃数据进行缓存
%%% @end
%%% -------------------------------------------------------
-module(svr_vitality_center_mgr).

-behaviour(gen_server).

%% API
-export([
        start_link/0
        ,recv_upload/2
        ,timer_notice_upload/0
        ,get_zone_vitality/1
        ,async_zone_vitality/2
        ,set_zone_faction/1
    ]).

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

-include("common.hrl").
-include("svr_vitality.hrl").

-define(SERVER, ?MODULE).

-ifdef(DEV_SERVER).
-define(INIT_TIMEOUT, 30 * 1000).
-else.
-define(INIT_TIMEOUT, 180 * 1000).
-endif.

%% 进程状态数据
-record(svr_vitality_center_state, {
        server_ids = []                     % 存在数据的游戏服server_id（各服数据存储在进程字典）
        ,faction_list = []                  % 计算过数据的阵营（各阵营数据存储在进程字典）
        ,timer_calc = undefined             % 计算阵营数据的定时器
        ,zone_faction_svrs_map = #{}        % 分区阵营与游戏服映射
        ,zone_vitality = []                 % 分区各阵营活跃数据缓存（用于对比检测是否更新）
    }).

%% TODO 合服移除非主服的数据

%%%===================================================================
%%% API
%%%===================================================================

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

%% 数据实时性要求不高，所以是定期全量上传的
recv_upload(ServerId, UploadData) ->
    gen_server:cast(?SERVER, {recv_upload, ServerId, UploadData}).

%% 定时器通知上传数据
timer_notice_upload() ->
    erlang:send(?SERVER, timer_notice_upload).

%% 获取分区各阵营的活跃度信息
%% get_zone_vitality(ZoneIds | all) -> ZoneVitalityList :: [{ZoneId, FVitalityList}].
get_zone_vitality(ZoneIds) ->
    gen_server:call(?SERVER, {get_zone_vitality, ZoneIds}).

%% 异步获取分区各阵营活跃度信息，回调CallbackMFA
async_zone_vitality(ZoneIds, CallbackMFA = {_, _, _}) ->
    gen_server:cast(?SERVER, {async_zone_vitality, ZoneIds, CallbackMFA}).

%% 设置分区阵营信息（在异步回调/分区变更时使用）
set_zone_faction(Reply) ->
    gen_server:cast(?SERVER, {zone_faction_svrs_map, Reply}).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([]) ->
    process_flag(trap_exit, true),
    erlang:send_after(?INIT_TIMEOUT, self(), timer_notice_upload),
    {ok, #svr_vitality_center_state{}}.

handle_call(Request, From, State) ->
    try
        do_handle_call(Request, From, State)
    catch
        ?EXCEPTION(_ErrorClass, _Error, _Stacktrace) ->
            ?ERR("handle_call Request:~w, Error:~w, ~w", [Request, _Error, ?GET_STACK(_Stacktrace)]),
            {reply, error, State}
    end.

handle_cast(Msg, State) ->
    try
        do_handle_cast(Msg, State)
    catch
        ?EXCEPTION(_ErrorClass, _Error, _Stacktrace) ->
            ?ERR("handle_cast Msg:~w, Error:~w, ~w", [Msg, _Error, ?GET_STACK(_Stacktrace)]),
            {noreply, State}
    end.

handle_info(Info, State) ->
    try
        do_handle_info(Info, State)
    catch
        ?EXCEPTION(_ErrorClass, _Error, _Stacktrace) ->
            ?ERR("handle_info Info:~w, Error:~w, ~w", [Info, _Error, ?GET_STACK(_Stacktrace)]),
            {noreply, State}
    end.

terminate(_Reason, _State) ->
    ok.

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

%%%===================================================================
%%% handle_xxxx Internal functions
%%%===================================================================

do_handle_call({get_zone_vitality, ZoneIds}, _From, State) ->
    #svr_vitality_center_state{
        faction_list = FactionList
    } = State,
    ZoneVitalityList = proc_get_zone_vitality(ZoneIds, FactionList),
    {reply, ZoneVitalityList, State};

do_handle_call(_Request, _From, State) ->
    {reply, error, State}.

do_handle_cast({async_zone_vitality, ZoneIds, {M, F, A}}, State) ->
    #svr_vitality_center_state{
        faction_list = FactionList
    } = State,
    ZoneVitalityList = proc_get_zone_vitality(ZoneIds, FactionList),
    erlang:apply(M, F, [ZoneVitalityList | A]),
    {noreply, State};

do_handle_cast({recv_upload, ServerId, UploadData}, State) ->
    #svr_vitality_center_state{
        server_ids = ServerIds
    } = State,
    PowerRoles = binary_to_term(UploadData),
    ValidPowerRoles = valid_power_roles(PowerRoles),
    SvrPowerRoles = #svr_power_roles{
        server_id = ServerId,
        power_roles = ValidPowerRoles,
        update_time = utime:unixtime()
    },
    proc_put_svr_power_roles(SvrPowerRoles),
    NewServerIds =
    case lists:member(ServerId, ServerIds) of
        true  -> ServerIds;
        false -> [ServerId | ServerIds]
    end,
    NewState = State#svr_vitality_center_state{
        server_ids = NewServerIds
    },
    {noreply, NewState};

%% 更新阵营数据
do_handle_cast({zone_faction_svrs_map, ZoneFSvrsMap}, State) ->
    #svr_vitality_center_state{
        faction_list = OldFactionList,
        zone_vitality = OldZoneVitality
    } = State,
    {NewFactionList, NewZoneVitality} =
    faction_update(ZoneFSvrsMap, OldFactionList, OldZoneVitality),
    NewState = State#svr_vitality_center_state{
        zone_faction_svrs_map = ZoneFSvrsMap
        ,faction_list = NewFactionList
        ,zone_vitality = NewZoneVitality
    },
    {noreply, NewState};

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

do_handle_info(timer_notice_upload, State) ->
    #svr_vitality_center_state{
        timer_calc = OldTimer
    } = State,
    util:cancel_timer(OldTimer),
    %% 数据实时性要求不高，所以延迟以统一计算，减少每次上传数据时重新计算的操作
    %% 延迟时间标准：为获取阵营数据、各服上传数据预留充足时间即可，不宜太长
    Timer = erlang:start_timer(30 * 1000, self(), calc_vitality),
    %% 异步获取阵营数据
    CallbackMFA = {?MODULE, set_zone_faction, []},
    czone:async_get_faction_map(zone_faction_svrs_map, CallbackMFA),
    %% 通知各服上传数据
    clusters_center:apply_to_all_node(svr_vitality_local_mgr, notice_upload, []),
    NewState = State#svr_vitality_center_state{
        timer_calc = Timer
    },
    {noreply, NewState};

%% 达到重新计算活跃数据的时间
do_handle_info({timeout, _Timer, calc_vitality}, State) ->
    #svr_vitality_center_state{
        zone_faction_svrs_map = ZoneFSvrsMap,
        faction_list = OldFactionList,
        zone_vitality = OldZoneVitality
    } = State,
    {NewFactionList, NewZoneVitality} =
    faction_update(ZoneFSvrsMap, OldFactionList, OldZoneVitality),
    NewState = State#svr_vitality_center_state{
        faction_list = NewFactionList,
        zone_vitality = NewZoneVitality
    },
    {noreply, NewState};

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

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% 阵营数据更新时检测数据更新处理
faction_update(ZoneFSvrsMap, OldFactionList, OldZoneVitality) ->
    %% 计算阵营活跃数据（即计算阵营排名，再进行统计）
    {NewFactionList, NewZoneVitality} = calc_faction_vitality(ZoneFSvrsMap),
    ZoneFactionExpire = OldFactionList -- NewFactionList,
    clean_expired_faction(ZoneFactionExpire),
    %% 将更新数据通知到czone分区管理进程
    % NewZoneVitality = proc_get_zone_vitality(all, NewFactionList),
    ZoneVitalityUpdate = calc_zone_vitality_update(NewZoneVitality, OldZoneVitality),
    czone:set_zone_vitalitys(ZoneVitalityUpdate),
    {NewFactionList, NewZoneVitality}.

calc_zone_vitality_update(NewZoneVitality, OldZoneVitality) ->
    {_, UpdateList} =
    lists:foldl(fun(New = {ZoneId, FVList}, {ZFVList, UpdateAcc}) ->
                case lists:keytake(ZoneId, 1, ZFVList) of
                    {value, {_, OldFVList}, NewZFVList} ->
                        NewUpdateAcc =
                        case ulists:list_equal(OldFVList, FVList) of
                            true  ->
                                UpdateAcc;
                            false ->
                                [New | UpdateAcc]
                        end,
                        {NewZFVList, NewUpdateAcc};
                    _ ->
                        {ZFVList, [New | UpdateAcc]}
                end
        end, {OldZoneVitality, []}, NewZoneVitality),
    UpdateList.

%% ------------------------------------------------------------------
%% SvrPowerRoles在进程字典的存取管理
%% ------------------------------------------------------------------
proc_get_svr_power_roles(ServerId) ->
    case get({svr_power_roles, ServerId}) of
        #svr_power_roles{
        } = SvrPowerRoles -> SvrPowerRoles;
        _Undefined -> undefined
    end.

proc_put_svr_power_roles(SvrPowerRoles = #svr_power_roles{server_id = ServerId}) ->
    put({svr_power_roles, ServerId}, SvrPowerRoles).

% 需要补充合服清理功能时才用到，暂时注释掉，去除warning
% proc_erase_svr_power_roles(ServerId) ->
%     erase({svr_power_roles, ServerId}).
%% ------------------------------------------------------------------
%% FactionVitality在进程字典的存取管理
%% ------------------------------------------------------------------
proc_get_faction_vitality(ZoneId, Faction) ->
    case get({faction_vitality, ZoneId, Faction}) of
        #faction_vitality{} = FV -> FV;
        _Undefined -> undefined
    end.

proc_put_faction_vitality(FVitality = #faction_vitality{zone_id = ZoneId, faction = Faction}) ->
    put({faction_vitality, ZoneId, Faction}, FVitality).

proc_erase_faction_vitality(ZoneId, Faction) ->
    erase({faction_vitality, ZoneId, Faction}).
%% ------------------------------------------------------------------

%% 校验数据是否有效
valid_power_roles([PowerRole | PowerRoles]) ->
    case valid_power_role(PowerRole) of
        true  ->
            [PowerRole | valid_power_roles(PowerRoles)];
        false ->
            valid_power_roles(PowerRoles)
    end;
valid_power_roles([]) ->
    [].

valid_power_role(PowerRole) ->
    is_record(PowerRole, power_role).

% to_zone_faction_list(ZoneFSvrsMap) ->
%     maps:fold(fun(ZoneId, FSvrsMap, ZFAcc) ->
%                 maps:fold(fun(Faction, _ServerIds, Acc) ->
%                             [{ZoneId, Faction} | Acc]
%                     end, ZFAcc, FSvrsMap)
%         end, [], ZoneFSvrsMap).

%% 计算各阵营数据（遍历Zone）
calc_faction_vitality(ZoneFSvrsMap) ->
    NowTime = utime:unixtime(),
    maps:fold(fun(ZoneId, FSvrsMap, {ZFAcc, ZVAcc}) ->
                {AddZFList, AddZV} = calc_faction_vitality_2(ZoneId, FSvrsMap, NowTime),
                NewZFAcc = AddZFList ++ ZFAcc,
                NewZVAcc = [AddZV | ZVAcc],
                {NewZFAcc, NewZVAcc}
        end, {[], []}, ZoneFSvrsMap).

%% 计算各阵营数据（同一ZoneId下，遍历Faction）
calc_faction_vitality_2(ZoneId, FSvrsMap, NowTime) ->
    {ZFList, FVList} =
    maps:fold(fun(Faction, FServerIds, {ZFAcc, FVAcc}) ->
                Vitality = count_faction_vitality(FServerIds),
                FVitality = #faction_vitality{
                    zone_id = ZoneId
                    ,faction = Faction
                    ,vitality = Vitality
                    ,update_time = NowTime
                },
                proc_put_faction_vitality(FVitality),
                NewZFAcc = [{ZoneId, Faction} | ZFAcc],
                NewFVAcc = [{Faction, Vitality} | FVAcc],
                {NewZFAcc, NewFVAcc}
        end, {[], []}, FSvrsMap),
    {ZFList, {ZoneId, FVList}}.

count_faction_vitality(FServerIds = [_|_]) ->
    Rank = lists:foldl(fun(ServerId, RankAcc) ->
                case proc_get_svr_power_roles(ServerId) of
                    #svr_power_roles{power_roles = PowerRoles} ->
                        SortedList = svr_vitality_local_mgr:sort_power_roles(PowerRoles ++ RankAcc),
                        lists:sublist(SortedList, ?CENTER_FACTION_VITALITY_ROLE_LEN);
                    _ ->
                        RankAcc
                end
        end, [], FServerIds),
    PowerList = [Power || #power_role{power = Power} <- Rank],
    case PowerList of
        [_|_] -> lists:sum(PowerList);
        _ -> 0
    end.

%% 清理过期的阵营数据
clean_expired_faction([{ZoneId, Faction} | Others]) ->
    proc_erase_faction_vitality(ZoneId, Faction),
    clean_expired_faction(Others);
clean_expired_faction([]) ->
    ok.

proc_get_zone_vitality(all, FactionList) ->
    ZoneIds = lists:foldl(fun(ZoneIdX, AccList) ->
                case lists:member(ZoneIdX, AccList) of
                    true  -> AccList;
                    false -> [ZoneIdX | AccList]
                end
        end, [], FactionList),
    proc_get_zone_vitality(ZoneIds, FactionList);

proc_get_zone_vitality([ZoneId | ZoneIds], FactionList) ->
    FVitalityList = proc_get_zone_vitality_2(ZoneId, FactionList),
    [{ZoneId, FVitalityList} | proc_get_zone_vitality(ZoneIds, FactionList)];
proc_get_zone_vitality([], _FactionList) ->
    [].

proc_get_zone_vitality_2(ZoneId, [{ZoneId, Faction} | FactionList]) ->
    Vitality =
    case proc_get_faction_vitality(ZoneId, Faction) of
        #faction_vitality{
            vitality = VitalityX
        } ->
            VitalityX;
        _ ->
            0
    end,
    [{Faction, Vitality} | proc_get_zone_vitality_2(ZoneId, FactionList)];
proc_get_zone_vitality_2(ZoneId, [_|FactionList]) ->
    proc_get_zone_vitality_2(ZoneId, FactionList);
proc_get_zone_vitality_2(_ZoneId, []) ->
    [].


