%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 跨服游戏服分区管理公共接口
%%% @end
%%% @since 2018-12-10
%%% -------------------------------------------------------
-module(cross_zone_api).
%% 分区计算相关API
-export([
        calc_zones_by_time/1                    % 按开服时间计算分区
        ,calc_zones_by_time/2
        ,calc_zones_by_time/3
        ,conv_zone_svr/1                        % 将游戏节点#game_node{}转换为#zone_svr{}

        ,check_node_filter/2                    % 判断节点是否可以参与跨服小分区
        ,check_nodes/2                          % 按Filter过滤节点列表
        ,filter_open_day/2
        ,split_zones/2
    ]).
%% 数据更新API
-export([
        store_to_servers/2                      % 保存/替换#zone_svr{}数据到ServerList
        ,store_to_zone_info/2                   % 保存/替换#zone_svr{}数据到#zone_info{}
    ]).
%% #zone_info{}数据查找API
-export([
        find_zone/2
        ,find_svr_zone/3
        ,find_zone_id/2
    ]).
%% #zone_svr{}数据查找API
-export([
        find_server/3
        ,find_server/2
    ]).
%% 跨服小分区模板 回调管理相关接口
-export([
        init/0
        ,terminate/0
        ,center_connected/1
        ,center_disconnected/1
        ,insert_worker/4
        ,delete_worker/1
    ]).
-include("common.hrl").
-include("clusters.hrl").
-include("cross_zone.hrl").

%% 按开服时间计算分区
calc_zones_by_time(N) ->
    GameNodes = clusters_center:get_nodes_info(),
    calc_zones_by_time(N, undefined, 0, GameNodes).

calc_zones_by_time(N, Filter) ->
    GameNodes = clusters_center:get_nodes_info(),
    calc_zones_by_time(N, Filter, 0, GameNodes).

calc_zones_by_time(N, Filter, JoinDay) ->
    GameNodes = clusters_center:get_nodes_info(),
    calc_zones_by_time(N, Filter, JoinDay, GameNodes).

%% 按开服时间计算分区 -> {MaxZoneId, SvrZoneMap, ZoneList}.
%% 符合JoinDay，且Filter计算结果为true，才允许加入到分区
calc_zones_by_time(N, Filter, JoinDay, GameNodes) when is_integer(N) andalso N > 0 ->
    RemainNodes  = check_join_day(JoinDay, GameNodes),
    NewGameNodes = check_nodes(Filter, RemainNodes),
    SortedNodes = lists:keysort(#game_node.open_time, NewGameNodes),
    split_zones(N, SortedNodes).

%% 将排序好的游戏节点数据按每分区游戏服数量参数，进行分区计算
-spec split_zones(N, SortedNodes) -> {MaxZoneId, SvrZoneMap, ZoneList} when
    N :: non_neg_integer(), SortedNodes :: [#game_node{}],
    MaxZoneId :: non_neg_integer(), SvrZoneMap :: map(),
    ZoneList :: [#zone_info{}].
split_zones(N, SortedNodes) ->
    split_zones(N, SortedNodes, 1, [], []).
split_zones(_N, [], Order, SvrZoneList, Zones) ->
    SvrZoneMap = maps:from_list(SvrZoneList),
    MaxZoneId = case Order =:= 1 of
        true  -> 1;
        false -> Order - 1
    end,
    {MaxZoneId, SvrZoneMap, Zones};
split_zones(N, SortedNodes, Order, SvrZoneList, Zones) when N > 0 ->
    {PreList, TailList} = ulists:split(N, SortedNodes),
    ZoneInfo = #zone_info{
        zone_id = Order
        ,servers = [conv_zone_svr(GameNode) || GameNode <- PreList]
    },
    NewSvrZoneList = [{ServerId, Order} || #game_node{server_id = ServerId} <- PreList] ++ SvrZoneList,
    NewZones = [ZoneInfo | Zones],
    split_zones(N, TailList, Order + 1, NewSvrZoneList, NewZones).

%% #game_node{} 转换为 #zone_svr{}.
conv_zone_svr(#game_node{
        server_id = ServerId
        ,server_num = ServerNum
        ,node = Node
    }) ->
    #zone_svr{
        server_id = ServerId
        ,server_num = ServerNum
        ,node = Node
    }.

%% 保存/替换#zone_svr{}到ServerList
store_to_servers(ZoneSvr = #zone_svr{server_id = ServerId}, ServerList) when is_list(ServerList) ->
    lists:keystore(ServerId, #zone_svr.server_id, ServerList, ZoneSvr).

%% 保存/替换#zone_svr{}到#zone_info{}
store_to_zone_info(ZoneSvr = #zone_svr{}, ZoneInfo = #zone_info{servers = ServerList}) ->
    NewServerList = store_to_servers(ZoneSvr, ServerList),
    ZoneInfo#zone_info{servers = NewServerList}.

%% 通过ZoneId，查找对应的分区信息#zone_info{}.
-spec find_zone(ZoneId, Zones) -> Zone :: #zone_info{} | false when
    ZoneId :: non_neg_integer(), Zones :: [#zone_info{}].
find_zone(ZoneId, Zones) ->
    lists:keyfind(ZoneId, #zone_info.zone_id, Zones).

%% 通过ServerId，查找对应的分区数据#zone_info{}.
-spec find_svr_zone(ServerId, Zones, SvrZoneMap) -> Zone :: #zone_info{} | false when
    ServerId :: non_neg_integer(),
    Zones :: [ #zone_info{} ],
    SvrZoneMap :: map().
find_svr_zone(ServerId, Zones, SvrZoneMap) ->
    ZoneId = find_zone_id(ServerId, SvrZoneMap),
    case ZoneId > 0 of
        true  ->
            find_zone(ZoneId, Zones);
        false ->
            false
    end.

%% 在SvrZoneMap查找ServerId对应的ZoneId
-spec find_zone_id(ServerId, SvrZoneMap) -> ZoneId when
    ServerId :: non_neg_integer(),          % 游戏服ServerId
    SvrZoneMap :: map(),                    % #{ServerId => ZoneId}
    ZoneId :: non_neg_integer().            % 分区编号，0表示未找到分区编号
find_zone_id(ServerId, SvrZoneMap) ->
    maps:get(ServerId, SvrZoneMap, 0).

%% 从所有Zones中查找对应ServerId的分区节点信息
-spec find_server(ServerId, Zones, SvrZoneMap) -> ZoneSvr when
    ServerId :: non_neg_integer(),
    Zones :: [#zone_info{}],
    SvrZoneMap :: map(),
    ZoneSvr :: #zone_svr{} | false.
find_server(ServerId, Zones, SvrZoneMap) ->
    case find_svr_zone(ServerId, Zones, SvrZoneMap) of
        #zone_info{servers = Servers} ->
            find_server(ServerId, Servers);
        _False ->
            false
    end.

%% 从单个Zone中，查找对应ServerId的分区节点信息
-spec find_server(ServerId, ZoneInfo) -> ZoneSvr when
    ServerId :: non_neg_integer(),
    ZoneInfo :: #zone_info{} | [ #zone_svr{} ],
    ZoneSvr :: #zone_svr{} | false.
find_server(ServerId, Servers = [_|_]) ->
    case lists:keyfind(ServerId, #zone_svr.server_id, Servers) of
        ZoneSvr = #zone_svr{} -> ZoneSvr;
        _False -> false
    end;
find_server(ServerId, #zone_info{servers = Servers}) ->
    find_server(ServerId, Servers);
find_server(_ServerId, _) ->
    false.

%% 判断节点是否可以参与跨服小分区
-spec check_node_filter(Filter, GameNode :: #game_node{}) -> Ret when
    Filter :: {M, F, A} | undefined, M :: atom(), F :: atom(), A :: list(),
    Ret :: boolean().
check_node_filter({M, F, Args}, GameNode = #game_node{}) ->
    erlang:apply(M, F, [GameNode | Args]);
check_node_filter(undefined, #game_node{}) ->
    true;
check_node_filter(_, _) ->
    false.

%% 按Filter过滤节点列表
-spec check_nodes(Filter, GameNodes) -> NewGameNodes when
    Filter :: {M, F, A} | undefined, M :: atom(), F :: atom(), A :: list(),
    GameNodes :: [GameNode], NewGameNodes :: [GameNode],
    GameNode :: #game_node{}.
check_nodes({M, F, Args}, GameNodes = [_|_]) ->
    lists:filter(
        fun (GameNode = #game_node{}) ->
                erlang:apply(M, F, [GameNode | Args]);
            (_) -> false
        end, GameNodes
    );
check_nodes(_, GameNodes = [_|_]) ->
    GameNodes;
check_nodes(_, _) ->
    [].

check_join_day(JoinDay, GameNodes) when JoinDay =< 1 ->
    GameNodes;
check_join_day(JoinDay, GameNodes) when is_integer(JoinDay) ->
    %% JoinDay是特殊的Filter
    Filter = {?MODULE, filter_open_day, [JoinDay]},
    check_nodes(Filter, GameNodes);
check_join_day(_, GameNodes) ->
    GameNodes.

%% 按开服天数限制节点的Filter
filter_open_day(_GameNode = #game_node{}, MinOpenDay) when MinOpenDay =< 1 ->
    true;
filter_open_day(#game_node{open_time = OpenTime}, MinOpenDay) ->
    NowTime = utime:unixtime(),
    OpenDay = utime:day_span(NowTime, OpenTime),
    OpenDay >= MinOpenDay;
filter_open_day(_, _) ->
    false.

%% 通知跨服小分区进程回调
center_connected(GameNode) ->
    ZoneWorkers = ets:tab2list(?CROSS_ZONE_WORKER),
    SimpleGameNode = GameNode#game_node{merge_server_ids = []},
    lists:foreach(
        fun (#cross_zone_worker{pid = Pid, later_join = true, proc_state = ?STATE_RUNNING}) when is_pid(Pid) ->
                % ?INFO("before send add node:~p", [SimpleGameNode]),
                cross_zone:add_node(Pid, SimpleGameNode, normal);
            (_) ->
                ignore
        end, ZoneWorkers
    ).

%% 通知跨服小分区进程回调
center_disconnected(GameNode) ->
    ZoneWorkers = ets:tab2list(?CROSS_ZONE_WORKER),
    SimpleGameNode = GameNode#game_node{merge_server_ids = []},
    lists:foreach(
        fun (#cross_zone_worker{pid = Pid, proc_state = ?STATE_RUNNING}) when is_pid(Pid) ->
                cross_zone:node_off(Pid, SimpleGameNode);
            (_) ->
                ignore
        end, ZoneWorkers
    ).

%% 跨服小分区管理进程信息ETS表创建
init() ->
    ets:new(?CROSS_ZONE_WORKER,  [named_table, public, set, {keypos, #cross_zone_worker.pid}, {read_concurrency, true}]),
    ok.

%% 停服回调，需要等待workers关闭再删除ets表
terminate() ->
    %% Wait ZoneWorkers shutdown
    ZoneWorkers = ets:tab2list(?CROSS_ZONE_WORKER),
    [cross_zone:stop(Pid) || #cross_zone_worker{pid = Pid} <- ZoneWorkers, is_pid(Pid)],
    %% Need wait for shutdown over, but it's unimportant, ignore it.
    ok.

%% 注册跨服小分区管理进程
%% 这里只记录信息，不检验Pid与ProcName的对应关系
%% Pid为小分区管理进程，ProcName为进程注册名，LaterJoin表示节点连接跨服后是否可以加入后面的分区
%% Pid :: pid(),
%% ProcName :: {local, atom()} | {global, term()} | undefined,
%% LaterJoin :: boolean(),
insert_worker(Pid, ProcName, LaterJoin, ProcState) ->
    case is_pid(Pid) andalso is_boolean(LaterJoin) of
        true  ->
            Worker = #cross_zone_worker{
                pid = Pid
                ,name = ProcName
                ,later_join = LaterJoin
                ,proc_state = ProcState
            },
            ets:insert(?CROSS_ZONE_WORKER, Worker);
        false ->
            error
    end.

%% 删除跨服小分区管理进程信息
delete_worker(Pid) when is_pid(Pid) ->
    ets:delete(?CROSS_ZONE_WORKER, Pid).


