%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 游戏服活跃度更新管理
%%% 实际上，维护的是一个近期内有在线过的角色的最高战力排行
%%%
%%% 游戏服节点工作机制
%%%  - 启动时从数据库load出足够的数据
%%%  - 在游戏服实时收集#power_role{}数据，
%%%    在玩家上下线、战力上限突破时实时更新#power_role{}数据
%%%    保留一定的备选数据，预防清理过期时空缺
%%%  - 定期将数据上传到跨服中心节点（例如每小时，由跨服主动通知上传）
%%%  - 每天特定时间检查一次数据（例如2点，由游戏服crontab任务触发），清理过期数据
%%% @end
%%% -------------------------------------------------------
-module(svr_vitality_local_mgr).

-behaviour(gen_server).

%% API
-export([
        start_link/0
        ,power_up/3
        ,online_update/3
        ,timer_clean_outdate/0
        ,notice_upload/0
        ,sort_power_roles/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).

-record(svr_vitality_local_state, {
        power_rank = [],
        rank_len = 0,
        min_power = 0
    }).

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

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

%% 在线突破历史最高战力
power_up(RoleId, HightPower, UpTime) ->
    gen_server:cast(?SERVER, {power_up, RoleId, HightPower, UpTime}).

%% 在线状态变更：上线/离线（非停服离线）通知更新数据
online_update(RoleId, HightPower, UpTime) ->
    gen_server:cast(?SERVER, {update_time, RoleId, HightPower, UpTime}).

%% 清理过期数据，由游戏服crontab任务触发
timer_clean_outdate() ->
    gen_server:cast(?SERVER, clean_outdate).

%% 上传数据（一般由跨服中心节点负责通知上传）
notice_upload() ->
    gen_server:cast(?SERVER, notice_upload).

%% 排行计算
sort_power_roles(PowerRoles) ->
    lists:sort(fun cmp_power_role/2, PowerRoles).

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

init([]) ->
    process_flag(trap_exit, true),
    PowerRoles = load_top_power_roles(),
    Min = min_power(PowerRoles),
    {ok, #svr_vitality_local_state{
            power_rank = PowerRoles,
            rank_len = length(PowerRoles),
            min_power = Min
        }}.

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

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) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

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

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

do_handle_cast({power_up, RoleId, HightPower, UpTime}, State) ->
    #svr_vitality_local_state{
        power_rank = PowerRoles,
        rank_len = RankLen,
        min_power = MinPower
    } = State,
    case check_up_rank(HightPower, MinPower, RankLen) of
        true  ->
            PowerRole = to_power_role(RoleId, HightPower, UpTime),
            {
                NewPowerRoles, NewMinPower, NewRankLen
            } = store_to_rank(PowerRole, PowerRoles, MinPower, RankLen),
            NewState = State#svr_vitality_local_state{
                power_rank = NewPowerRoles,
                rank_len = NewRankLen,
                min_power = NewMinPower
            },
            {noreply, NewState};
        false ->
            {noreply, State}
    end;

%% 理论上在线期间战力突破会更新，这里只需要检查更新时间参数，
%% 但是考虑到防止上线时数据产生变化引发的战力突破，
%% 所以暂时直接调用战力突破更新的处理
do_handle_cast({update_time, RoleId, HightPower, UpTime}, State) ->
    do_handle_cast({power_up, RoleId, HightPower, UpTime}, State);

%% 清理过期数据
do_handle_cast(clean_outdate, State) ->
    #svr_vitality_local_state{
        power_rank = PowerRoles
    } = State,
    NewPowerRoles = clean_outdate(PowerRoles),
    NewRankLen = length(NewPowerRoles),
    NewMinPower = min_power(NewPowerRoles),
    NewState = State#svr_vitality_local_state{
        power_rank = NewPowerRoles,
        rank_len = NewRankLen,
        min_power = NewMinPower
    },
    {noreply, NewState};

%% 上传数据
do_handle_cast(notice_upload, State) ->
    #svr_vitality_local_state{
        power_rank = PowerRoles
    } = State,
    %% 只取需要的长度（后面部分为备选数据不上传）
    SubRoles = lists:sublist(PowerRoles, ?SVR_POWER_RANK_LEN),
    ServerId = config:get_server_id(),
    SubRolesBin = term_to_binary(SubRoles),
    clusters_node:apply_cast(svr_vitality_center_mgr, recv_upload, [ServerId, SubRolesBin]),
    {noreply, State};

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

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

%% 加载活跃的高战玩家数据
load_top_power_roles() ->
    Len = ?RANK_REAL_LEN,
    Time = utime:that_day_before(?VITALITY_DAYS),
    Sql = util:fmt_bin(?SQL_GET_TOP_POWER_ROLES, [Time, Time, Len]),
    DbDataList = db:get_all(Sql),
    [db_data_to_power_role(DbData) || DbData <- DbDataList].

db_data_to_power_role([Id, LastLogin, LastLogout, Power]) ->
    %% 最后的在线时间，粗略以登入或登出计，
    %% 代价比较大，所以活跃度的计算不需要如此精准
    LastOnline = max(LastLogin, LastLogout),
    to_power_role(Id, Power, LastOnline).

to_power_role(RoleId, HightPower, Time) ->
    #power_role{
        role_id = RoleId
        ,power = HightPower
        ,last_online = Time
    }.

%% 获取最小战力
% min_power(PowerRoles) ->
%     case ulists:keymin(#power_role.power, PowerRoles) of
%         Min when is_integer(Min) ->
%             Min;
%         _ ->
%             0
%     end.
%% 获取最小战力
%% 由于这里是有序的，所以直接取最后一名，不需要ulists:keymin/2去比较获取
min_power(PowerRoles = [_|_]) ->
    LastPowerRole = lists:last(PowerRoles),
    #power_role{power = Power} = LastPowerRole,
    Power;
min_power([]) ->
    0.

%% 有数据更新请求时，检查是否需要更新排行榜
%% 历史最高战力的特点是只增不减，
%% 所以不用考虑像其他排行榜可能变小而下榜的可能
check_up_rank(Power, MinPower, RankLen) ->
    Power >= MinPower orelse
    RankLen < ?RANK_REAL_LEN.

%% -> {NewPowerRoles, NewMinPower, NewRankLen}
store_to_rank(PowerRole, PowerRoles, MinPower, RankLen) ->
    #power_role{role_id = RoleId, power = Power} = PowerRole,
    case lists:keyfind(RoleId, #power_role.role_id, PowerRoles) of
        #power_role{
            power = OldPower
        } ->
            NewPowerRoles = lists:keystore(RoleId, #power_role.role_id, PowerRoles, PowerRole),
            case OldPower =:= Power of
                true  ->
                    %% 战力不变，应该是上下线时间数据更新
                    {NewPowerRoles, MinPower, RankLen};
                false ->
                    %% 战力变化
                    SortedPowerRoles = sort_power_roles(NewPowerRoles),
                    NewMinPower =
                    if
                        OldPower =:= MinPower ->
                            %% 如果原来是最小战力的人，需要重算最小战力
                            min_power(SortedPowerRoles);
                        Power < MinPower ->
                            %% 理论上不可能的情况（因为最高战力正常只会变大）
                            Power;
                        true ->
                            MinPower
                    end,
                    {SortedPowerRoles, NewMinPower, RankLen}
            end;
        _ ->
            SortedPowerRoles = sort_power_roles([PowerRole | PowerRoles]),
            {NewPowerRoles, NewRankLen} =
            case RankLen >= ?RANK_REAL_LEN of
                true  ->
                    {
                        lists:sublist(SortedPowerRoles, ?RANK_REAL_LEN),
                        ?RANK_REAL_LEN
                    };
                false ->
                    {
                        SortedPowerRoles,
                        RankLen + 1
                    }
            end,
            NewMinPower =
            case Power > MinPower of
                true  ->
                    min_power(NewPowerRoles);
                false ->
                    MinPower
            end,
            {NewPowerRoles, NewMinPower, NewRankLen}
    end.

cmp_power_role(PowerRoleA, PowerRoleB) ->
    #power_role{power = PowerA, last_online = LastOnlineA} = PowerRoleA,
    #power_role{power = PowerB, last_online = LastOnlineB} = PowerRoleB,
    if
        PowerA > PowerB -> true;
        %% 实际上Power相等的情况应该极少，所以不用太担心因为时间变化导致的顺序变动
        LastOnlineA < LastOnlineB -> true;
        true ->
            false
    end.

clean_outdate(PowerRoles) ->
    OutdateTime = utime:that_day_before(3),
    [
        PowerRole ||
        PowerRole = #power_role{last_online = LastOnline} <- PowerRoles,
        LastOnline >= OutdateTime
    ].

