%% ---------------------------------------------------------------------------
%% @doc 跨服竞榜活动管理
%% @author zhaoyu
%% @since  2018-11-29
%% ---------------------------------------------------------------------------
-module(race_rank_act).


-behaviour(gen_server).

%% API
-export([
        start_link/0
        ,call/1                     %% call本进程
        ,cast/1                     %% cast本进程
        ,info/1                     %% 给进程发送消息
        ,reload_config/0            %% 重新加载配置
        ,apply_for_opening_list/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("race_rank.hrl").
-include("clusters.hrl").

%% 等待游戏服连接跨服等待时间
-ifdef(DEV_SERVER).
-define(WAITING_TIME, 20000).
-else.
-define(WAITING_TIME, (60 * 1000)).
-endif.


%% ---------------------------------------------------------------------------
%% @doc Starts the server
-spec start_link() -> {ok, Pid} | ignore | {error, Error} when
    Pid :: pid(),
    Error :: term().
%% ---------------------------------------------------------------------------
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).


%% ---------------------------------------------------------------------------
%% @doc call进程
-spec call(Msg) -> term() | {error, Reason} when
    Msg     :: term(),
    Reason  :: any().
%% ---------------------------------------------------------------------------
call(Msg) ->
    try gen_server:call(?MODULE, Msg) of
    Reply -> Reply
    catch Class:Reason ->
        Error = {Class, Reason, erlang:get_stacktrace()},
        ?ERR("~w:handle_cast error:~p~n", [?MODULE, Error]),
        {error, Reason}
    end.


%% ---------------------------------------------------------------------------
%% @doc cast进程
-spec cast(Msg) -> ok when
    Msg     :: term().
%% ---------------------------------------------------------------------------
cast(Msg) ->
    gen_server:cast(?MODULE, Msg).


%% ---------------------------------------------------------------------------
%% @doc 给进程发送消息
-spec info(Msg) -> ok when
    Msg     :: term().
%% ---------------------------------------------------------------------------
info(Msg) ->
    erlang:send(?MODULE, Msg).


%% ---------------------------------------------------------------------------
%% @doc 重新加载配置(热更data_race_rank.erl文件时会调用)。
-spec reload_config() -> ok.
%% ---------------------------------------------------------------------------
reload_config() ->
    cast({reload_config}).


%% ---------------------------------------------------------------------------
%% @doc 游戏服连接到跨服后，发送开启中的活动列表给游戏服
-spec apply_for_opening_list(Node) -> ok when
    Node :: atom().
%% ---------------------------------------------------------------------------
apply_for_opening_list(Node) ->
    cast({apply_for_opening_list, Node}).


%% ---------------------------------------------------------------------------
%% @doc Initializes the server
-spec init(Args) ->
    {ok, State}
    | {ok, State, Timeout}
    | ignore
    | {stop, Reason} when
    Args    :: term() ,
    State   :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity,
    Reason  :: term().
%% ---------------------------------------------------------------------------
init(_) ->
    process_flag(trap_exit, true),
    OpeningList = load_acts_info(),
    Now = utime:unixtime(),
    %% 根据时间筛选所有活动配置
    calc_all_act(Now),
    State = #race_rank_act_info{opening = OpeningList},
    %% specially:进程启动10分钟后等游戏服连接过来了再进行活动开始/结束/展示到期定时。
    %% 因为活动结束进行结算的时候发奖励需要游戏服连接着跨服。
    Ref = erlang:send_after(?WAITING_TIME, self(), {act_check_msg}),
    {ok, State#race_rank_act_info{ref = Ref}}.


%% ---------------------------------------------------------------------------
%% @doc Handling call messages
-spec handle_call(Request, From, State) ->
    {reply, Reply, State}
    | {reply, Reply, State, Timeout}
    | {noreply, State}
    | {noreply, State, Timeout}
    | {stop, Reason, Reply, State}
    | {stop, Reason, State} when
    Request :: term(),
    From :: pid(),
    State :: #race_rank_act_info{},
    Reply :: term(),
    State :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity.
%% ---------------------------------------------------------------------------
handle_call(Request, From, State) ->
    try handle_call2(Request, From, State) of
        Result ->
            Result
    catch Class:Reason ->
        Error = {Class, Reason, erlang:get_stacktrace()},
        ?ERR("~w:handle_cast error:~p~n", [?MODULE, Error]),
        {reply, error, State}
    end.


%% ---------------------------------------------------------------------------
%% @doc Handling cast messages
-spec handle_cast(Msg, State) ->
    {noreply, State}
    | {noreply, State, Timeout}
    | {stop, Reason, State} when
    Msg :: term(),
    State :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity,
    Reason :: term().
%% ---------------------------------------------------------------------------
handle_cast(Msg, State) ->
    try handle_cast2(Msg, State) of
        Result ->
            Result
    catch Class:Reason ->
        Error = {Class, Reason, erlang:get_stacktrace()},
        ?ERR("~w:handle_cast error:~p~n", [?MODULE, Error]),
        {noreply, State}
    end.


%% ---------------------------------------------------------------------------
%% @doc Handling all non call/cast messages
-spec handle_info(Info, State) ->
    {noreply, State}
    | {noreply, State, Timeout}
    | {stop, Reason, State} when
    Info :: term(),
    State :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity,
    Reason :: term().
%% ---------------------------------------------------------------------------
handle_info(Info, State) ->
    try handle_info2(Info, State) of
        Result ->
            Result
    catch Class:Reason ->
        Error = {Class, Reason, erlang:get_stacktrace()},
        ?ERR("~w:handle_cast error:~p~n", [?MODULE, Error]),
        {noreply, State}
    end.



%% ---------------------------------------------------------------------------
%% @doc Handling call messages
-spec handle_call2(Request, From, State) ->
    {reply, Reply, State}
    | {reply, Reply, State, Timeout}
    | {noreply, State}
    | {noreply, State, Timeout}
    | {stop, Reason, Reply, State}
    | {stop, Reason, State} when
    Request :: term(),
    From :: pid(),
    State :: #race_rank_act_info{},
    Reply :: term(),
    State :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity.
%% ---------------------------------------------------------------------------
handle_call2(_Request, _From, State) ->
    {reply, ok, State}.



%% ---------------------------------------------------------------------------
%% @doc Handling cast messages
-spec handle_cast2(Msg, State) ->
    {noreply, State}
    | {noreply, State, Timeout}
    | {stop, Reason, State} when
    Msg :: term(),
    State :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity,
    Reason :: term().
%% ---------------------------------------------------------------------------
%% 重新加载配置
handle_cast2({reload_config}, State) ->
    Now = utime:unixtime(),
    %% 根据时间筛选所有活动配置
    calc_all_act(Now),
    NewState = act_check(State),
    ?INFO("-----race_rank_act reload_config-------"),
    {noreply, NewState};
%% 游戏服连接到跨服后，发送开启中的活动列表给游戏服
handle_cast2({apply_for_opening_list, Node}, State) ->
    #race_rank_act_info{opening = OpenList} = State,
    Mfa = {race_rank_act_local, cast, [{reset_opening_act_list, OpenList}]},
    clusters_center:apply_cast(Node, Mfa),
    {noreply, State};
%% 玩家上榜
handle_cast2({hit_rank, RankRole}, State) ->
    #rr_rank_role{act_id = ActId} = RankRole,
    #race_rank_act_info{opening = OpenList} = State,
    case lists:keyfind(ActId, #rr_act_info.act_id, OpenList) of
        #rr_act_info{status = ?NOT_SETTLEMENT} -> %% 活动还没结算,可以上榜
            race_rank_mgr:cast({hit_rank, RankRole});
        _ ->
            race_rank_mgr:cast({hit_rank, RankRole}),
            ok
    end,
    {noreply, State};
%% gm结算排行榜-禁止调用
handle_cast2({gm_settlement_act, ActId}, State) ->
    #race_rank_act_info{opening = OpenList} = State,
    OpenList2 = case lists:keytake(ActId, #rr_act_info.act_id, OpenList) of
        {value, Act, LessOpenList} ->
            Now = utime:unixtime(),
            Act2 = settlement_act(Act, Now),
            [Act2 | LessOpenList];
        _ ->
            OpenList
    end,
    NewState = State#race_rank_act_info{opening = OpenList2},
    info({act_check_msg}),
    clusters_center:apply_to_all_node(
        race_rank_act_local, cast, [{reset_opening_act_list, OpenList2}]
    ),
    {noreply, NewState};
handle_cast2(_Msg, State) ->
    {noreply, State}.


%% ---------------------------------------------------------------------------
%% @doc Handling all non call/cast messages
-spec handle_info2(Info, State) ->
    {noreply, State}
    | {noreply, State, Timeout}
    | {stop, Reason, State} when
    Info :: term(),
    State :: #race_rank_act_info{},
    Timeout :: non_neg_integer() | infinity,
    Reason :: term().
%% ---------------------------------------------------------------------------
%% 活动开始/结束/展示到期定时器
handle_info2({act_check_msg}, State) ->
    NewState = act_check(State),
    {noreply, NewState};
handle_info2(_Info, State) ->
    {noreply, State}.


%% ---------------------------------------------------------------------------
%% @doc called by a gen_server when it is about to terminate
-spec terminate(Reason, State) -> ok when
    Reason :: term(),
    State  :: #race_rank_act_info{}.
%% ---------------------------------------------------------------------------
terminate(_Reason, _State) ->
    ok.


%% ---------------------------------------------------------------------------
%% @doc Convert process state when code is changed
-spec code_change(OldVsn, State, Extra) -> {ok, NewState} when
    OldVsn :: term(),
    State :: #race_rank_act_info{},
    Extra :: term(),
    NewState :: #race_rank_act_info{}.
%% ---------------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


%% ---------------------------------------------------------------------------
%% @doc 加载活动信息
-spec load_acts_info()-> Opening when
    Opening    :: [#rr_act_info{}].
%% ---------------------------------------------------------------------------
load_acts_info()->
    Opening = case db:get_all(?RR_ACT_SELECT) of
        DbList when is_list(DbList) ->
            Now = utime:unixtime(),
            check_on_load(DbList, Now, []);
        _ ->
            []
    end,
    Opening.

check_on_load([], _Now, Opening)->
    Opening;
check_on_load([[ActId, Status] | T], Now, Opening)->
    Act = #rr_act_info{act_id  = ActId, status = Status},
    NewOpening = case data_race_rank:get_race_rank_act(ActId) of
        #race_rank_act{start_time = STime, end_time = ETime, show_time = ShowTime} ->
            Act2 = Act#rr_act_info{start_time = STime, end_time = ETime},
            if
                Now < STime -> %% 关服期间配置进行了修改。活动已配置为未开启活动
                    case Status of
                        ?NOT_SETTLEMENT -> %% 活动之前没有结算。
                            %% 活动改为不在开启中，后续也不清理排行榜数据。
                            Opening;
                        _ -> %% 已经结算了。尝试展示一下。
                            ShowEndTime = min(Status + ShowTime, STime - 60 * 10),
                            Act3 = Act2#rr_act_info{show_end_time = ShowEndTime},
                            [Act3 | Opening]
                    end;
                STime =< Now andalso Now < ETime ->
                    case Status of
                        ?NOT_SETTLEMENT -> %% 活动之前没有结算,活动继续
                            [Act2 | Opening];
                        _ -> %% 已经结算了。。展示一下。
                            ShowEndTime = Status + ShowTime,
                            Act3 = Act2#rr_act_info{show_end_time = ShowEndTime},
                            [Act3 | Opening]
                    end;
                Now >= ETime ->
                    case Status of
                        ?NOT_SETTLEMENT -> %% 活动之前没有结算,待结算
                            [Act2 | Opening];
                        _ -> %% 已经结算了。。
                            ShowEndTime = ETime + ShowTime,
                            Act3 = Act2#rr_act_info{show_end_time = ShowEndTime},
                            [Act3 | Opening]
                    end;
                true ->
                    Opening
            end;
        _ -> %% 没有配置了。
            %% 活动改为不在开启中，后续也不清理排行榜数据。
            Opening
    end,
    check_on_load(T, Now, NewOpening).


%% ---------------------------------------------------------------------------
%% @doc 批量保存活动信息
-spec save_acts_info(Acts) -> ok when
    Acts  :: [#rr_act_info{}].
%% ---------------------------------------------------------------------------
save_acts_info(Acts) ->
    SqlArgs = [
        [ActId, Status] ||
        #rr_act_info{act_id  = ActId, status = Status} <- Acts
    ],
    SqlArgs =/= [] andalso db:insert_batch(?RR_ACT_BATCH, SqlArgs),
    ok.


%% ---------------------------------------------------------------------------
%% @doc 删除ActId活动信息
-spec del_act(ActId) -> ok when
    ActId       :: integer().
%% ---------------------------------------------------------------------------
del_act(ActId) ->
    db:execute(io_lib:format(?RR_ACT_DEL, [ActId])),
    ok.


%% %% ---------------------------------------------------------------------------
%% %% @doc 删除活动信息
%% -spec delete_acts() -> ok.
%% %% ---------------------------------------------------------------------------
%% delete_acts() ->
%%     db:execute(?RR_ACT_DEL_ALL),
%%     ok.



%% ---------------------------------------------------------------------------
%% @doc 根据时间筛选所有活动配置。
-spec calc_all_act(Now) -> Return when
    Now         :: integer(),
    Return      :: [#race_rank_act{}].
%% ---------------------------------------------------------------------------
calc_all_act(Now) ->
    ActIdList = data_race_rank:get_race_rank_act_list(),
    calc_all_act(ActIdList, Now, []).

calc_all_act([], _Now, ActList) ->
    set_act_list(ActList),
    ActList;
calc_all_act([ActId | T], Now, ActList) ->
    Act = #race_rank_act{
        start_time = StartTime, end_time = EndTime
    } = data_race_rank:get_race_rank_act(ActId),
    case Now >= EndTime of
        true -> %% 已结束,不再开
            calc_all_act(T, Now, ActList);
        _ when StartTime >= EndTime -> %% 活动开启时间不合理
            calc_all_act(T, Now, ActList);
        _->
            calc_all_act(T, Now, [Act | ActList])
    end.


%% ---------------------------------------------------------------------------
%% @doc 获取活动配置列表
-spec get_act_list() -> ActList when
    ActList      :: [#race_rank_act{}].
%% ---------------------------------------------------------------------------
get_act_list() ->
    case erlang:get({race_rank_act_list}) of
        ActList when is_list(ActList) -> ActList;
        _ -> []
    end.


%% ---------------------------------------------------------------------------
%% @doc 更新活动配置列表
-spec set_act_list(ActList) -> ok when
    ActList      :: [#race_rank_act{}].
%% ---------------------------------------------------------------------------
set_act_list(ActList) ->
    erlang:put({race_rank_act_list}, ActList),
    ok.


%% ---------------------------------------------------------------------------
%% @doc 活动开始/结束/展示到期定时器
-spec act_check(State) -> NewState when
    State       :: #race_rank_act{},
    NewState    :: #race_rank_act{}.
%% ---------------------------------------------------------------------------
act_check(State) ->
    #race_rank_act_info{opening = OpeningList, ref = Ref} = State,
    util:cancel_timer(Ref),
    Now = utime:unixtime(),
    OpeningList2 = try_settlement_act(OpeningList, Now, []),
    ActList = get_act_list(),
    {
        OpeningList3, NextStartTime
    } = new_opening_act_and_update(Now, OpeningList2, ActList, false),
    NextCheckTime = next_check_time(Now, OpeningList3, NextStartTime),
    TimeOut = (NextCheckTime - Now) * 1000,
    NewRef = erlang:send_after(TimeOut, self(), {act_check_msg}),
    case OpeningList3 of
        OpeningList ->
            ok;
        _ ->
            clusters_center:apply_to_all_node(
                race_rank_act_local, cast, [{reset_opening_act_list, OpeningList3}]
            )
    end,
    State#race_rank_act_info{opening = OpeningList3, ref = NewRef}.

%% 下次检查时间
next_check_time(Now, [], NextCheckTime) ->
    % DateTime = utime:unixdate(Now),
    % NextMidnight = DateTime + ?ONE_DAY_SECONDS,
    NextMidnight = utime:tomorrow(Now),
    case is_integer(NextCheckTime) of
        true ->
            min(NextCheckTime, NextMidnight);
        false ->
            NextMidnight
    end;
next_check_time(Now, [Act | T], NextCheckTime) ->
    #rr_act_info{
        status = Status, end_time = EndTime, show_end_time = ShowEndTime
    } = Act,
    NextCheckTime2 = case Status of
        ?NOT_SETTLEMENT -> %% 活动还没有结算
            min(NextCheckTime, EndTime);
        _ -> %% 活动已经结算过了
            min(NextCheckTime, ShowEndTime)
    end,
    next_check_time(Now, T, NextCheckTime2).


%% 判断是否有活动活动时间到了
try_settlement_act([], _Now, OpeningList) ->
    OpeningList;
try_settlement_act([Act | T], Now, OpeningList) ->
    #rr_act_info{
        status = Status, end_time = EndTime, show_end_time = ShowEndTime
    } = Act,
    case Status of
        ?NOT_SETTLEMENT -> %% 活动还没有结算
            case Now >= EndTime of
                true -> %% 活动时间结束
                    Act2 = settlement_act(Act, Now),
                    OpeningList2 = [Act2 | OpeningList],
                    try_settlement_act(T, Now, OpeningList2);
                false ->
                    try_settlement_act(T, Now, [Act | OpeningList])
            end;
        _ -> %% 活动已经结算过了
            case Now >= ShowEndTime of
                true -> %% 活动展示时间结束
                    act_show_end(Act, Now),
                    try_settlement_act(T, Now, OpeningList);
                false ->
                    try_settlement_act(T, Now, [Act | OpeningList])
            end
    end.

%% 判断是否有活动到时间开启了，并更新正在开启中的活动的时间
new_opening_act_and_update(_Now, OpeningList, [], NextStartTime) ->
    {OpeningList, NextStartTime};
new_opening_act_and_update(Now, OpeningList, [Act | T], NextStartTime) ->
    #race_rank_act{
        act_id = ActId, start_time = StartTime, end_time = EndTime
    } = Act,
    KeyPos = #rr_act_info.act_id,
    case lists:keyfind(ActId, KeyPos, OpeningList) of
        #rr_act_info{
            start_time = StartTime, end_time = EndTime
        } -> %% 已开启了,并且开始和结束时间跟配置一样
            new_opening_act_and_update(Now, OpeningList, T, NextStartTime);
        AInfo = #rr_act_info{} -> %% 已开启了,但是开始或者结束时间跟配置不一样了
            %% 更新下开始和结束时间
            AInfo2 = AInfo#rr_act_info{start_time = StartTime, end_time = EndTime},
            OpeningList2 = lists:keystore(ActId, KeyPos, OpeningList, AInfo2),
            new_opening_act_and_update(Now, OpeningList2, T, NextStartTime);
        _ when StartTime =< Now andalso Now < EndTime ->
            ActInfo = #rr_act_info{
                act_id = ActId, start_time = StartTime, end_time = EndTime
            },
            NewActInfo = start_act(ActInfo),
            OpeningList2 = [NewActInfo | OpeningList],
            new_opening_act_and_update(Now, OpeningList2, T, NextStartTime);
        _ ->
            NextStartTime2 = case Now < StartTime of
                true -> min(NextStartTime, StartTime);
                false -> NextStartTime
            end,
            new_opening_act_and_update(Now, OpeningList, T, NextStartTime2)
    end.


%% ---------------------------------------------------------------------------
%% @doc 开启活动
-spec start_act(ActInfo) -> NewActInfo when
    ActInfo         :: #rr_act_info{},
    NewActInfo      :: #rr_act_info{}.
%% ---------------------------------------------------------------------------
start_act(ActInfo) ->
    save_acts_info([ActInfo]),
    ActInfo.

%% ---------------------------------------------------------------------------
%% @doc 结算活动
-spec settlement_act(Act, Now) -> NewAct when
    Act         :: #rr_act_info{},
    Now         :: integer(),
    NewAct      :: #rr_act_info{}.
%% ---------------------------------------------------------------------------
settlement_act(Act, Now) ->
    #rr_act_info{start_time=StartTime,end_time=EndTime,act_id = ActId} = Act,
    ShowTime = case data_race_rank:get_race_rank_act(ActId) of
        #race_rank_act{show_time = STime} -> STime;
        _ -> 0
    end,
    ShowEndTime = Now + ShowTime,
    NewAct = Act#rr_act_info{status = Now, show_end_time = ShowEndTime},
    %% 结算排行榜
    race_rank_mgr:cast({settlement_rank, ActId,StartTime,EndTime}),
    save_acts_info([NewAct]),
    NewAct.


%% ---------------------------------------------------------------------------
%% @doc 活动排行榜展示结束
-spec act_show_end(Act, Now) -> ok when
    Act         :: #rr_act_info{},
    Now         :: integer().
%% ---------------------------------------------------------------------------
act_show_end(Act, _Now) ->
    #rr_act_info{act_id = ActId} = Act,
    del_act(ActId),
    %% 删除排行榜数据
    race_rank_mgr:cast({act_show_end, ActId}),
    %% 通知游戏服删除数据
    clusters_center:apply_to_all_node(
        race_rank, del_role_race_rank_act_data, [ActId]
    ),
    ok.
