%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 通用跨服小分区模块
%%%     各跨服小分区数据保存在进程字典中，
%%%     如有需要，可以在主进程中调用
%%%     进程内部接口（Process internal API）查询分区数据
%%%
%%% 本模块主要有两种用法：
%%% 如无特殊的需要，通常不需要设置回调模块，
%%% 以独立模块封装一些接口进行调用即可使用
%%%
%%% 特殊的分区可能需要自行定义分区方法、特殊事件处理，
%%% 以及特殊的玩法功能管理，则需要独立的回调模块
%%% 注意回调模块必须实现handle_event/3，其他回调接口可选
%%%
%%% 回调接口参考 -callback 和 -optional_callbacks 标签
%%%
%%% （PS: 实际上，玩法需要分区可以基于cross_zone behaviour，
%%% 直接实现玩法管理+分区功能，不用分两个进程做管理）
%%%
%%% @end
%%% @since 2018-12-15
%%% -------------------------------------------------------
-module(cross_zone).
-behaviour(gen_server).

%% API
-export([
        start_link/1                        % 开启进程
        ,start_link/2                       % 开启进程
        ,start_link/3                       % 开启进程
        ,stop/1                             % 关闭进程
        ,call/2                             % 发送消息，等待返回，类似gen_server:call/2
        ,cast/2                             % 发送cast异步消息，类似gen_server:cast/2
        ,info/2                             % 发送info异步消息，实质即erlang:send/2
        ,set_waiting/1                      % 设置暂停处理，清除分区信息
        ,add_node/3                         % 节点连接后回调
        ,node_off/2                         % 节点断开后回调
        ,recalc_zone/1                      % 重新计算分区
        ,recalc_zone/2                      % 重新计算分区
        ,apply_cast/2                       % 异步式管理进程执行MFA
        ,apply_cast_with_data/2             % 异步式管理进程执行MFA(带有补充的状态参数)
        ,apply_call/2                       % 同步式管理进程执行MFA，取得返回结果
        ,apply_call_with_data/2             % 同步式管理进程执行MFA，取得返回结果(执行时带有补充的状态参数)
        ,apply_to_zone/3                    % 异步到指定的分区编号对应的所有游戏服节点执行MFA
        ,get_zone_servers/2                 % 获取指定ZoneId对应的服务器数据
        ,get_all_zone_ids/1                 % 取得所有的分区编号
        ,get_server_zone/2                  % 取得游戏服对应跨服分区号
        ,get_server_zone_map/1              % 取得游戏服与跨服分区号的映射关系

        ,base_init/1                        % 基础的初始化参数解析接口
    ]).
%% be careful to use, always for gm use
-export([
        get_all_zones/1                     % 获取所有分区数据（慎用，通常只用于GM调试）
        ,get_the_zone/2                     % 获取指定ServerId对应分区数据（慎用，通常只用于GM调试）
    ]).
%% Process internal API
-export([
        proc_get_zone/1                     % 进程内部适用，获取分区数据
        ,proc_get_zones/0                   % 进程内部适用，获取所有分区数据
        ,proc_get_zones/1                   % 进程内部适用，获取指定列表分区数据
        ,proc_put_zone/1                    % 进程内部适用，设置分区数据，自动维护ZoneIds
        ,proc_put_zones/1                   % 进程内部适用，设置分区数据列表，自动维护ZoneIds
        ,proc_put_zone_only/1               % 进程内部适用，设置分区数据，不自动维护ZoneIds
        ,proc_put_zones_only/1              % 进程内部适用，设置分区数据列表，不自动维护ZoneIds
        ,proc_get_zone_ids/0                % 进程内部适用，获取分区id列表
        ,proc_put_zone_ids/1                % 进程内部适用，设置分区id列表
        ,proc_erase_zones/1                 % 进程内部适用，清除分区id列表数据，自动维护ZoneIds
        ,proc_get_svr_zone/2                % 进程内部适用，获取指定ServerId对应分区的数据
        ,proc_get_server/2                  % 进程内部适用，获取指定ServerId对应节点的数据
        ,proc_apply_to_zone/2               % 进程内部适用，异步到指定分区对应的所有节点或指定游戏服节点执行MFA
        ,get_add_nodes/6                    % 进程内部适用，用于新一天对比获取新的符合条件的节点列表
    ]).
%% only for test
-export([
        zone_test/0
        ,gm_add_server/2
        ,gm_add_server_callback/3
        ,gm_get_zone_sys/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("clusters.hrl").
-include("cross_zone.hrl").

%% 内置的特殊事件或事件标签，回调模块不能用
%% （为便于与普通的其他消息区分，统一使用大写）

%% 重新计算分区                 'CALC_ZONE'
%% 加入新节点/节点重连          'ADD_NODE'
%% 节点离线                     'NODE_OFF'
%% 获取所有分区数据             'GET_ALL_ZONES'
%% 获取指定分区数据             'GET_THE_ZONE'
%% 设置等待状态                 'SET_WAITING'
%% 终止事件                     'STOP'
%% 异步到分区或游戏服执行MFA    'APPLY_TO_ZONE'
%% 同步/异步到分区进程执行MFA   'DO_MFA'
%% 设置监听事件                 'LISTENER'
%% 每日定时器                   'DAILY_CHECK'

%% 分区方式分类
-define(TYPE_DEFAULT, open_time).

%% 进程字典KEY值
-define(ZONE(ZoneId), {zone, ZoneId}).                  % 跨服小分区数据的进程字典Key
-define(CALC_ZONE_TIMER, calc_zone_timer).              % 自动延迟分区的定时器进程字典Key

%% 管理器状态
-record(zone_proc_state, {
        zone_type       = undefined         % 分区方式（open_time / czone / 其他自定义...）
        ,num_per_zone   = 4                 % 每分区游戏服数量
        ,callback_mod   = undefined         % 回调模块
        ,module_state   = undefined         % 回调模块的其他状态数据
        ,zone_sys       = undefined         % 在特殊分区管理逻辑中，用于存储自定义的分区运算逻辑相关核心数据
        ,later_join     = true              % 是否允许后面接入的游戏服加入分区
        ,proc_name      = undefined         % 进程注册名称
        ,proc_state     = ?STATE_WAITING    % 状态
        ,max_zone_id    = 1                 % 最后一个分区id
        ,svr_zone_map   = #{}               % 游戏服到分区的映射关系#{ServerId => ZoneId}
        ,filter         = undefined         % 检查节点是否符合参与分区的条件的接口 {M, F, A}|undefined
        ,daily_timer    = undefined         % 定时检查加入初始分区时不够条件的分区（open_time + LaterJoin）
        ,join_day       = 0                 % 允许加入分区的开服天数
    }).

%% 只定义最简单常见的3种形式，其他不常见的不加入，例如
%% {'via', RegMod :: module(), ViaName :: term()}
%% {Name :: atom(), Node :: atom()}
-type server_ref() :: pid() | (LocalName :: atom()) | {global, GlobalName :: term()}.

%%%===================================================================
%%% 回调模块接口
%%% 如果有设置回调模块，则必须实现的回调接口为handle_event/3
%%% 其他几个回调接口为可选实现，当找不到实现的接口时，会按默认的处理方式进行数据处理
%%%===================================================================

%% init返回值#zone_init_ret{}，详细说明查看结构定义
-callback init(Args :: term()) -> #zone_init_ret{} | {error, Reason :: term()}.

%% 只有EventType为{call, From}时，返回结果tuple中reply才有实际意义（其他类型消息会忽略reply）
%% 对于call类消息的处理，有两类做法：
%% 1. 返回结果为 {reply, Reply, NewData} 或 {reply, Reply}，本模块内部会自动回复给调用者进程
%% 2. 返回结果为其他值，handle_event中，自行调用 gen_server:reply/2 向调用者进程回复消息
-callback handle_event(EventType, EventContent, ZoneState) -> Ret when
    EventType :: info | cast | {call, From :: pid()},   % 消息类型
    EventContent :: term(),                             % 消息体正文
    ZoneState :: #zone_state{},                         % 分区部分数据
    Ret :: {reply, Reply}
    | {reply, Reply, NewModuleState} | {ok, NewModuleState} % 返回#zone_state.module_state更新后的数据，一般情况下使用
    | {reply, Reply, NewZoneState} | {ok, NewZoneState}     % 返回#zone_state{}，通常仅特殊分区方法才需要
    | {stop,Reason} | ok | Other,
    Reply :: term(),                                    % 只有call事件才能reply
    NewModuleState :: term(),
    NewZoneState :: #zone_state{},
    Other :: term(),
    Reason :: term().

%% 定制特殊的分区方法（通常不需要）
-callback calc_zone(NumPerZone, ZoneType, Filter, JoinDay) -> Ret when
    Ret :: {
        MaxZoneId :: integer(),                         % 当前最后使用的ZoneId（最大）
        SvrZoneMap :: map(),                            % #{ServerId => ZoneId}
        Zones :: [ #zone_info{} ]                       % 分区列表数据
    },
    NumPerZone :: non_neg_integer(),
    ZoneType :: term(),                                 % 标识分区方式（自定义），默认为open_time
    JoinDay :: non_neg_integer(),                       % 允许加入分区的开服天数要求
    Filter :: {M, F, Args} | undefined,                 % 即init中指定的Filter（与JoinDay共同生效）
    M :: atom(), F :: atom(), Args :: list().

%% 重新分区前回调
-callback before_calc(ZoneState) -> NewModuleState when
    ZoneState :: #zone_state{},             % 同handle_event/3中的ZoneState
    NewModuleState :: term().               % 只返回自定义的ModuleState

%% 重新分区后回调
-callback after_calc(ZoneState) -> NewModuleState when
    ZoneState :: #zone_state{},             % 同handle_event/3中的ZoneState
    NewModuleState :: term().               % 只返回自定义的ModuleState

%% 节点连接时回调
-callback add_node(IsNewZoneId, ZoneSvr, ZoneInfo, OldModuleState) -> NewModuleState when
    IsNewZoneId :: boolean(),               % 是否开辟了新的分区
    ZoneSvr :: #zone_svr{},                 % 添加的节点
    ZoneInfo :: #zone_info{},               % 添加的节点对应的分区（新增或修改原有分区）
    OldModuleState :: term(), NewModuleState :: term().

%% 节点离线时回调
-callback node_off(ZoneSvr, ZoneInfo, OldModuleState) -> NewModuleState when
    ZoneSvr :: #zone_svr{},                 % 删除的节点
    ZoneInfo :: #zone_info{},               % 删除的节点对应的分区
    OldModuleState :: term(), NewModuleState :: term().

%% 终止服务回调清理
-callback terminate(Reason, ModuleState) -> Void when
    Reason :: term(),
    ModuleState :: term(),
    Void :: term().

-optional_callbacks([
        init/1
        ,calc_zone/4
        ,before_calc/1
        ,after_calc/1
        ,add_node/4
        ,node_off/3
        ,terminate/2
    ]).

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

%% start_link/1,2,3的InitArgs，
%% 在无回调模块时，传递给base_init/1调用，
%%      传入参数支持：map()、proplists:proplist()和#zone_init_ret{}
%% 在有回调模块时，除了支持以上类型，还可以自定义其他结构，
%%      但是回调接口返回值必须是 #zone_init_ret{}.

%% 不带回调模块的启动方式
-spec start_link(InitArgs) -> Ret when
    InitArgs :: map() | proplists:proplist() | #zone_init_ret{} | term(),
    Ret :: {ok, Pid :: pid()} | {error, {already_started, Pid :: pid()}} | {error, Reason :: term()}.
start_link(InitArgs) ->
    start_link(undefined, InitArgs).

%% 带回调模块，不注册名字的启动方式
-spec start_link(CallbackMod, InitArgs) -> Ret when
    CallbackMod :: atom() | undefined,
    InitArgs :: map() | proplists:proplist() | #zone_init_ret{} | term(),
    Ret :: {ok, Pid :: pid()} | {error, {already_started, Pid :: pid()}} | {error, Reason :: term()}.
start_link(CallbackMod, InitArgs) when is_atom(CallbackMod) ->
    gen_server:start_link(?MODULE, {undefined, CallbackMod, InitArgs}, []).

%% 带回调模块，指定注册名字的启动方式
-spec start_link(ServerName, CallbackMod, InitArgs) -> Ret when
    ServerName :: {local, LocalName :: atom()} | {global, GlobalName :: term()} | undefined,
    CallbackMod :: atom() | undefined,
    InitArgs :: map() | proplists:proplist() | #zone_init_ret{} | term(),
    Ret :: {ok, Pid :: pid()} | {error, {already_started, Pid :: pid()}} | {error, Reason :: term()}.
start_link(ServerName, CallbackMod, InitArgs) when is_atom(CallbackMod) ->
    {CheckNameRet, ProcName} =
    case ServerName of
        undefined ->
            {true, undefined};
        {global, _GlobalName} ->
            {true, ServerName};
        {local, LocalName} when is_atom(LocalName) ->
            {true, LocalName};
        _ ->
            {false, undefined}
    end,
    case CheckNameRet of
        true  -> gen_server:start_link(ServerName, ?MODULE, {ProcName, CallbackMod, InitArgs}, []);
        false -> {error, proc_name}
    end.

stop(ZoneMgr) ->
    gen_server:cast(ZoneMgr, 'STOP').

%% 通过GM命令添加对应ServerId列表的节点列表到分区管理器
gm_add_server(ZoneMgr, ServerIds = [_|_]) ->
    ZonePid = case ZoneMgr of
        _ when is_pid(ZoneMgr) -> ZoneMgr;
        _ when is_atom(ZoneMgr) -> whereis(ZoneMgr);
        {global, GlobalName} -> global:whereis_name(GlobalName);
        _ -> error
    end,
    case is_pid(ZonePid) of
        true  ->
            MFA = {?MODULE, gm_add_server_callback, [ServerIds, ZonePid]},
            clusters_center:proc_apply(MFA);
        false ->
            ignore
    end.

gm_add_server_callback(AllNodes, ServerIds, ZoneMgrPid) ->
    AddNodes = [
        GameNode ||
        GameNode = #game_node{server_id = ServerId} <- AllNodes,
        lists:member(ServerId, ServerIds)
    ],
    lists:foreach(fun(GameNode) ->
                add_node(ZoneMgrPid, GameNode#game_node{merge_server_ids = []}, gm)
        end, AddNodes).

%% 获取特定分区的ZoneSys数据
%% （方便其他模块的GM命令编写使用，其他模块没有本模块进程的结构）
gm_get_zone_sys(ZoneMgr) ->
    case sys:get_state(ZoneMgr) of
        #zone_proc_state{zone_sys = ZoneSys} -> ZoneSys;
        _Error -> _Error
    end.

%% -----------------------------------------------------------------------
%% 以下接口类似gen_server:call/2, gen_server:cast/2, erlang:send/2，
%%
%% 除了特殊的消息
%% （'CALC_ZONE'、'ADD_NODE'、'GET_ALL_ZONES'、...等等，具体见check_handle_event/3的处理）
%% 通过这几个接口调用的请求将交由CallbackMod回调handle_event/3处理
%% -----------------------------------------------------------------------
-spec call(ZoneMgr, EventContent) -> Reply | ok | {error, event} when
    ZoneMgr   :: server_ref(),      %% 进程Pid或注册名字
    EventContent :: term(),         %% 发送的消息体
    Reply :: term().
call(ZoneMgr, EventContent) ->
    gen_server:call(ZoneMgr, EventContent, 3000).

-spec cast(ZoneMgr, EventContent) -> ok when
    ZoneMgr :: server_ref(),
    EventContent :: term().
cast(ZoneMgr, EventContent) ->
    gen_server:cast(ZoneMgr, EventContent).

-spec info(ZoneMgr, EventContent) -> ok when
    ZoneMgr :: server_ref(),
    EventContent :: term().
info(ZoneMgr, EventContent) ->
    erlang:send(ZoneMgr, EventContent),
    ok.
%% -----------------------------------------------------------------------

-spec apply_cast(ZoneMgr :: server_ref(), MFA) -> ok when
    MFA :: {Mod, Fun, Args} | {Fun2, Args},
    Mod :: atom(), Fun :: atom(), Fun2 :: function(), Args :: list().
apply_cast(ZoneMgr, MFA) ->
    gen_server:cast(ZoneMgr, {'DO_MFA', false, MFA}).
apply_cast_with_data(ZoneMgr, MFA) ->
    gen_server:cast(ZoneMgr, {'DO_MFA', true,  MFA}).

apply_call(ZoneMgr, MFA) ->
    gen_server:call(ZoneMgr, {'DO_MFA', false, MFA}, 3000).
apply_call_with_data(ZoneMgr, MFA) ->
    gen_server:call(ZoneMgr, {'DO_MFA', true,  MFA}, 3000).

%% 异步到指定的分区（或全部分区）对应的所有游戏服节点执行
-spec apply_to_zone(ZoneMgr, ZoneId, MFA) -> ok | error when
    ZoneMgr :: server_ref(),
    ZoneId :: non_neg_integer() | all,
    MFA :: {Mod :: atom(), Func :: atom(), Args} | {Func :: function(), Args},
    Args :: list().
apply_to_zone(ZoneMgr, ZoneId, MFA) ->
    CheckMFARet = util:valid_mfa(MFA),
    case CheckMFARet of
        true ->
            case is_integer(ZoneId) orelse ZoneId =:= all of
                true  -> gen_server:cast(ZoneMgr, {'APPLY_TO_ZONE', ZoneId, MFA});
                false -> error
            end;
        false -> error
    end.

%% 获取指定ZoneId对应的服务器数据
get_zone_servers(ZoneMgr, ZoneId) ->
    gen_server:call(ZoneMgr, {'GET_ZONE_SERVERS', ZoneId}, 3000).

%% 获取所有分区数据
%% PS: 通常尽量不要使用，分区数据可能是个很大的结构体，产生大消息
%%     主要是调试时查看数据使用
get_all_zones(ZoneMgr) ->
    gen_server:call(ZoneMgr, 'GET_ALL_ZONES', 3000).

%% 获取指定ServerId对应的ZoneInfo数据
get_the_zone(ZoneMgr, ServerId) ->
    gen_server:call(ZoneMgr, {'GET_THE_ZONE', ServerId}, 3000).

%% 获取所有的ZoneIds
get_all_zone_ids(ZoneMgr)  ->
    MFA = {fun() -> {reply, proc_get_zone_ids()} end, []},
    apply_call(ZoneMgr, MFA).

%% 设置小分区管理进程状态
%% set_waiting时，表示清空分区数据（如有），进入等待状态（不分区）
set_waiting(ZoneMgr) ->
    gen_server:cast(ZoneMgr, 'SET_WAITING').

%% 添加节点
add_node(ZoneMgr, GameNode = #game_node{}, Type) when is_pid(ZoneMgr) ->
    lists:member(Type, [normal, gm]) andalso erlang:send(ZoneMgr, {'ADD_NODE', GameNode, Type}).

%% 节点下线
%% 只是标记不在线，有可能还会重连回来，
%% 标记之后广播时才有判断依据，实际上不处理也可以，因为节点设置为不自动连接了
node_off(ZoneMgr, #game_node{server_id = ServerId, node = Node}) when is_pid(ZoneMgr) ->
    erlang:send(ZoneMgr, {'NODE_OFF', ServerId, Node}).

%% 重新分区
-spec recalc_zone(ZoneMgr :: server_ref()) -> Void::any().
recalc_zone(ZoneMgr) ->
    info(ZoneMgr, 'CALC_ZONE').

%% 重新分区，并修改一些启动时的参数
%% 支持的参数查询接口reset_params，Params是Key-Value列表或map()
-spec recalc_zone(ZoneMgr, Params) -> Void::any() when
    ZoneMgr :: server_ref(),
    Params :: proplists:proplist() | map().
recalc_zone(ZoneMgr, Params) when is_list(Params) orelse is_map(Params) ->
    info(ZoneMgr, {'CALC_ZONE', Params}).

%% 获取游戏服对应的跨服分区编号
get_server_zone(ZoneMgr, ServerId) ->
    Fun = fun
        (#zone_state{svr_zone_map = SvrZoneMap}) ->
            ZoneId = maps:get(ServerId, SvrZoneMap, error),
            {reply, ZoneId};
        (_) ->
            {reply, error}
    end,
    apply_call_with_data(ZoneMgr, {Fun, []}).

%% 获取游戏服与跨服分区编号的关系
get_server_zone_map(ZoneMgr) ->
    Fun = fun
        (#zone_state{svr_zone_map = SvrZoneMap}) ->
            {reply, SvrZoneMap};
        (_) ->
            {reply, #{}}
    end,
    apply_call_with_data(ZoneMgr, {Fun, []}).

%% 基础初始化接口，可供外部调用再制自己的初始化接口
%% 无回调模块时，即调用此接口进行参数初始化解析
-spec base_init(InitArgs) -> Ret when
    InitArgs :: map() | proplists:proplist() | #zone_init_ret{},
    Ret :: #zone_init_ret{} | {error, badarg}.
base_init(InitArgs) when is_map(InitArgs) ->
    ArgList = maps:to_list(InitArgs),
    base_init(ArgList);

base_init(InitArgs) when is_list(InitArgs) ->
    BaseRet = #zone_init_ret{},
    init_params(BaseRet, InitArgs);

base_init(ZoneInitRet = #zone_init_ret{}) ->
    ZoneInitRet;

base_init(_) ->
    {error, badarg}.

%% 测试基本的分区、添加节点及获取所有节点
zone_test() ->
    ProcName = zone_test,
    OldTestPid = whereis(ProcName),
    case is_pid(OldTestPid) of
        true  ->
            unlink(OldTestPid),
            unregister(ProcName),
            exit(OldTestPid, normal);
        false -> ignore
    end,
    cross_zone:start_link({local, ProcName}, undefined, #{num_per_zone => 2, delay_time => false}),
    recalc_zone(ProcName),
    %% 延时让新创建的测试进程先完成分区，
    %% 这里虚拟的节点连接不会体现在正常'CALC_ZONE'中，
    %% 所以需要保证状态变化完成，才能测试到'ADD_NODE'
    timer:sleep(50),
    NowTime = utime:unixtime(),
    AddNodeList = [
        #game_node{node = 'testnode11@127.0.0.1', server_id = 11, server_num = 31, open_time = NowTime - 86400*54}
        ,#game_node{node = 'testnode12@127.0.0.1', server_id = 12, server_num = 32, open_time = NowTime - 86400*53}
        ,#game_node{node = 'testnode13@127.0.0.1', server_id = 13, server_num = 33, open_time = NowTime - 86400*52}
        ,#game_node{node = 'testnode14@127.0.0.1', server_id = 14, server_num = 34, open_time = NowTime - 86400*51}
        ,#game_node{node = 'testnode15@127.0.0.1', server_id = 15, server_num = 35, open_time = NowTime - 86400*50}
        ,#game_node{node = 'testnode16@127.0.0.1', server_id = 16, server_num = 36, open_time = NowTime - 86400*49}
    ],
    [
        cross_zone_api:center_connected(AddNode) ||
        AddNode <- AddNodeList
    ],
    Ret = cross_zone:get_all_zones(ProcName),
    ?INFO_TTY("zone_test Pid:~w~nzone ret:~p", [whereis(ProcName), Ret]),
    ok.

%%%===================================================================
%%% Process internal API
%%% 进程内部接口，仅供回调模块在原始进程中调用
%%%===================================================================

proc_get_zone_ids() ->
    case get(zone_ids) of
        [_|_] = ZoneIds -> ZoneIds;
        _ -> []
    end.

proc_put_zone_ids(ZoneIds = [_|_]) ->
    put(zone_ids, ZoneIds);
proc_put_zone_ids([]) ->
    put(zone_ids, []).

%% 清理zones数据
%% 内部已经维护zone_ids的更新
proc_erase_zones(ZoneIds = [_|_]) ->
    AllZoneIds = proc_get_zone_ids(),
    erase_zones(ZoneIds),
    NewAllZoneIds = AllZoneIds -- ZoneIds,
    proc_put_zone_ids(NewAllZoneIds);
proc_erase_zones(ZoneId) when is_integer(ZoneId) ->
    proc_erase_zones([ZoneId]);
proc_erase_zones(_) ->
    ignore.

proc_get_zones() ->
    ZoneIds = proc_get_zone_ids(),
    proc_get_zones(ZoneIds).

proc_get_zones(ZoneIds = [_|_]) ->
    lists:filtermap(fun(ZoneId) ->
                case proc_get_zone(ZoneId) of
                    false -> false;
                    Zone -> {true, Zone}
                end
        end, ZoneIds);
proc_get_zones(SvrZoneMap) when is_map(SvrZoneMap) ->
    ZoneIds = lists:usort(maps:values(SvrZoneMap)),
    proc_get_zones(ZoneIds);
proc_get_zones(_) ->
    [].

proc_get_zone(ZoneId) ->
    case get(?ZONE(ZoneId)) of
        Zone = #zone_info{} -> Zone;
        _ -> false
    end.

%% 更新Zone: #zone_info{}数据
%% 内部已经维护zone_ids的更新
%% proc_put_zone(Zone) -> ok | error.
proc_put_zone(Zone) ->
    case proc_put_zone_only(Zone) of
        error ->
            error;
        ZoneId ->
            ZoneIds = proc_get_zone_ids(),
            case lists:member(ZoneId, ZoneIds) of
                true  -> ok;
                false -> proc_put_zone_ids([ZoneId | ZoneIds])
            end
    end.

%% 更新Zones: [#zone_info{}]数据
%% 内部已经维护zone_ids的更新
proc_put_zones(Zones) ->
    ZoneIds = proc_get_zone_ids(),
    [IsUpdate, NewZoneIds] = proc_put_zones_2(Zones, false, ZoneIds),
    case IsUpdate of
        true  -> proc_put_zone_ids(NewZoneIds);
        false -> ok
    end.

%% 更新Zone: #zone_info{}数据，不自动更新zone_ids数据
proc_put_zone_only(Zone = #zone_info{zone_id = ZoneId}) ->
    put(?ZONE(ZoneId), Zone),
    ZoneId;
proc_put_zone_only(_) ->
    error.

%% 更新Zones: [#zone_info{}]数据，不自动更新zone_ids数据
proc_put_zones_only([Zone | Zones]) ->
    proc_put_zone_only(Zone),
    proc_put_zones_only(Zones);
proc_put_zones_only([]) ->
    ok.

-spec proc_get_svr_zone(ServerId, SvrZoneMap) -> Ret when
    ServerId :: non_neg_integer(),
    SvrZoneMap :: map(),
    Ret :: #zone_info{} | false.
proc_get_svr_zone(ServerId, SvrZoneMap) ->
    ZoneId = cross_zone_api:find_zone_id(ServerId, SvrZoneMap),
    case ZoneId > 0 of
        true  ->
            proc_get_zone(ZoneId);
        false ->
            false
    end.

-spec proc_get_server(ServerId, SvrZoneMap) -> Ret when
    ServerId :: non_neg_integer(),
    SvrZoneMap :: map(),
    Ret :: #zone_svr{} | false.
proc_get_server(ServerId, SvrZoneMap) ->
    case proc_get_svr_zone(ServerId, SvrZoneMap) of
        #zone_info{
            servers = ServerList
        } ->
            cross_zone_api:find_server(ServerId, ServerList);
        _ ->
            false
    end.

%% ZoneInfo: all表示所有分区的所有节点都会执行MFA，
%% 其他则为指定的对应分区对应节点（或指定的节点），执行MFA
-spec proc_apply_to_zone(ZoneInfo, MFA) -> Void :: term() when
    ZoneInfo :: ZoneId | ZoneIds | Zone | Zones | ZoneSvrs | ZoneSvr | all,
    ZoneIds :: [ZoneId], ZoneId :: non_neg_integer(),
    Zones :: [Zone], Zone :: #zone_info{},
    ZoneSvrs :: [ZoneSvr], ZoneSvr :: #zone_svr{},
    MFA :: {M, F, A} | {Func, A},
    M :: atom(), F :: atom(), A :: list(),
    Func :: function().
proc_apply_to_zone(ZoneInfo, MFA) ->
    CheckRet = util:valid_mfa(MFA),
    case CheckRet of
        true  ->
            ZoneInfos =
            if
                ZoneInfo =:= all                -> proc_get_zone_ids();
                is_integer(ZoneInfo)            -> ZoneInfo;    % ZoneInfo为integer时，表示ZoneId
                is_record(ZoneInfo, zone_info)  -> ZoneInfo;
                is_record(ZoneInfo, zone_svr)   -> ZoneInfo;
                is_list(ZoneInfo)               -> ZoneInfo;
                true -> false
            end,
            do_apply_to_zone(ZoneInfos, MFA);
        false -> ignore
    end.

%% 根据JoinDay、Filter等参数，确定可新增到分区的节点，
%% 再通知分区管理进程添加相关节点
get_add_nodes(AllNodes, NowServerIds, JoinDay, Filter, ZoneMgr, Type) ->
    RemainNodes = [
        GameNode || GameNode = #game_node{server_id = ServerId} <- AllNodes,
        not lists:member(ServerId, NowServerIds) andalso
        cross_zone_api:filter_open_day(GameNode, JoinDay)
    ],
    NodesAfterFilter = cross_zone_api:check_nodes(Filter, RemainNodes),
    %% 暂时只管理open_time类型的分区，如有其他需要再补充参数进行区分
    AddNodes = lists:keysort(#game_node.open_time, NodesAfterFilter),
    lists:foreach(fun(GameNode) ->
                add_node(ZoneMgr, GameNode#game_node{merge_server_ids = []}, Type)
        end, AddNodes).

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

init({ProcName, CallbackMod, InitArgs}) ->
    process_flag(trap_exit, true),
    case cb_check_init(CallbackMod, InitArgs) of
        #zone_init_ret{
            zone_type       = ZoneType
            ,num_per_zone   = NumPerZone
            ,delay_time     = DelayTime
            ,later_join     = LaterJoin
            ,filter         = Filter
            ,join_day       = JoinDay
            ,module_state   = ModuleState
            ,init_zones     = InitZones
            ,zone_sys       = ZoneSys
        } ->
            % ?INFO("check init ret:~w", [{ZoneType, NumPerZone, DelayTime, LaterJoin, ModuleState}]),
            Pid = self(),
            %% 检查是否已经存在分区数据
            {MaxZoneId, SvrZoneMap, IsNotInit} = set_init_zones(InitZones),
            ProcState = case IsNotInit of
                true  -> ?STATE_WAITING;
                false -> ?STATE_RUNNING
            end,
            DailyTimer  =
            case ZoneType =/= czone andalso LaterJoin of
                true ->     % 每天0点需要检测有无新的服符合加入条件（开服天数分区类型）
                    case lists:member(ZoneType, [open_time]) of
                        true  ->
                            After = calc_next_midnight_diff(),
                            erlang:send_after(After, Pid, 'DAILY_CHECK');
                        false -> undefined
                    end;
                _ ->
                    undefined
            end,
            State = #zone_proc_state{
                callback_mod    = CallbackMod
                ,zone_type      = ZoneType
                ,num_per_zone   = NumPerZone
                ,zone_sys       = ZoneSys
                ,module_state   = ModuleState
                ,later_join     = LaterJoin
                ,proc_name      = ProcName
                ,proc_state     = ProcState
                ,max_zone_id    = MaxZoneId
                ,svr_zone_map   = SvrZoneMap
                ,filter         = Filter
                ,join_day       = JoinDay
                ,daily_timer    = DailyTimer
            },
            case IsNotInit andalso is_integer(DelayTime) of
                true  ->
                    TimerRef = erlang:send_after(DelayTime, Pid, 'CALC_ZONE'),
                    erlang:put(?CALC_ZONE_TIMER, TimerRef);
                false ->    % 不主动进行分区计算
                    ignore
            end,
            cross_zone_api:insert_worker(Pid, ProcName, LaterJoin, ProcState),
            {ok, State};
        {error, Reason} ->
            {stop, {error, Reason}};
        _ ->
            {stop, "error init callback!"}
    end.

handle_call(Request, From, State) ->
    try
        check_handle_event({call, From}, Request, 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
        check_handle_event(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
        check_handle_event(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) ->
    cross_zone_api:delete_worker(self()),
    case State of
        #zone_proc_state{
            callback_mod = CallbackMod
            ,module_state = ModuleState
        } -> cb_check_terminate(CallbackMod, Reason, ModuleState);
        _ -> ignore
    end,
    ok.

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

%%%===================================================================
%%% gen_server callback Internal functions
%%%===================================================================

%% -------------------------------------------------------------------
%% czone方式分区忽略'CALC_ZONE'消息，由另外的规则维护分区资料
check_handle_event(info, {'CALC_ZONE', _}, State = #zone_proc_state{zone_type = czone}) ->
    {noreply, State};
check_handle_event(info, 'CALC_ZONE', State = #zone_proc_state{zone_type = czone}) ->
    {noreply, State};
%% -------------------------------------------------------------------

%% 指定了新的分区游戏服数量参数，进行重新分区
check_handle_event(info, {'CALC_ZONE', Params}, State) ->
    ParamList =
    case is_map(Params) of
        true  -> maps:to_list(Params);
        false -> Params
    end,
    TmpState = reset_params(State, ParamList),
    check_handle_event(info, 'CALC_ZONE', TmpState);

%% 重新分区
check_handle_event(info, 'CALC_ZONE', State = #zone_proc_state{
        zone_type       = ZoneType
        ,num_per_zone   = NumPerZone
        ,callback_mod   = CallbackMod
        ,max_zone_id    = _MaxZoneId
        ,proc_name      = ProcName
        ,later_join     = LaterJoin
        ,filter         = Filter
        ,join_day       = JoinDay
    }) ->
    %% 取消掉自动分区定时器
    %% 防止错误设置策略时（自动延迟分区定时器尚未触发又手工设置了分区），自动延迟分区消息会破坏原分区数据
    OldTimer = erase(?CALC_ZONE_TIMER),
    util:cancel_timer(OldTimer),
    %% before_calc需要放在erase_zones之前，回调模块有机会对数据读取做处理
    BeforeZoneState = to_zone_state(State),
    ModuleStateClean = cb_check_reset_module_state(before_calc, CallbackMod, BeforeZoneState),
    % ?INFO("do calc_zone"),
    erase_zones(),
    %% 更新进程状态，以供游戏服节点连接到跨服中心时判断是否需要通知本进程
    NewProcState = ?STATE_RUNNING,
    cross_zone_api:insert_worker(self(), ProcName, LaterJoin, NewProcState),
    {MaxZoneId, SvrZoneMap, ZoneList} = cb_check_calc_zone(CallbackMod, NumPerZone, ZoneType, Filter, JoinDay),
    proc_put_zones(ZoneList),
    %% after_calc需要放在proc_put_zones之后，回调模块可以读取新数据做处理
    ZoneState = to_zone_state(State#zone_proc_state{module_state = ModuleStateClean}),
    NewModuleState = cb_check_reset_module_state(after_calc, CallbackMod, ZoneState),
    NewState = State#zone_proc_state{
        max_zone_id     = MaxZoneId
        ,svr_zone_map   = SvrZoneMap
        ,module_state   = NewModuleState
        ,proc_state     = NewProcState
    },
    {noreply, NewState};

%% 已经分好区后，新连接的游戏服节点加入分区
check_handle_event(info, {'ADD_NODE', GameNode = #game_node{server_id = ServerId}, Type}, State = #zone_proc_state{
        callback_mod    = CallbackMod
        ,module_state   = ModuleState
        ,max_zone_id    = MaxZoneId
        ,svr_zone_map   = SvrZoneMap
        ,num_per_zone   = NumPerZone
        ,proc_state     = ProcState
        ,zone_type      = ZoneType
        ,later_join     = LaterJoin
        ,filter         = Filter
        ,join_day       = JoinDay
    }) ->
    % ?INFO("add node:~p", [GameNode]),
    IsInZones = maps:is_key(ServerId, SvrZoneMap),
    FilterRet =
    cross_zone_api:check_node_filter(Filter, GameNode) andalso
    cross_zone_api:filter_open_day(GameNode, JoinDay),
    if
        ProcState =/= ?STATE_RUNNING ->
            {noreply, State};
        IsInZones ->
            %% 包含两种情形: 1.初次连接 2.再次连接
            %% 初次连接时在分区中，属于启动有加载历史分区的情况，此时需要更新节点的全部信息
            %% 再次连接时在分区中，属于断开后重新连上（或合服重启之类）
            NewConnState = true,
            #game_node{node = Node, server_num = ServerNum} = GameNode,
            OtherUpdateList = [{server_num, ServerNum}],
            case change_node_info(ServerId, SvrZoneMap, Node, NewConnState, OtherUpdateList) of
                {NewZoneSvr, NewZoneInfo} ->
                    NewModuleState = cb_check_add_node(CallbackMod, false, NewZoneSvr, NewZoneInfo, ModuleState),
                    NewState = State#zone_proc_state{module_state = NewModuleState},
                    {noreply, NewState};
                _ ->
                    {noreply, State}
            end;
        ZoneType =:= czone ->
            %% czone类型分区，只关注在分区中的服，
            %% 非分区中的服，等待每天的数据检查（收集并排序）再加入分区
            {noreply, State};
        not LaterJoin andalso Type =:= normal ->
            %% 正常情况受限于LaterJoin，如果Type为gm，则忽略LaterJoin
            {noreply, State};
        not FilterRet ->
            {noreply, State};
        true ->
            ZoneSvr = cross_zone_api:conv_zone_svr(GameNode),
            CurrZone = proc_get_zone(MaxZoneId),
            NewState =
            case CurrZone of
                #zone_info{
                    servers = ServerList
                } ->
                    Size = length(ServerList),
                    case Size >= NumPerZone of
                        true  ->
                            NewMaxZoneId = MaxZoneId + 1,
                            {NewSvrZoneMap, ZoneInfo} = new_zone(NewMaxZoneId, [ZoneSvr], SvrZoneMap),
                            proc_put_zone(ZoneInfo),
                            IsNewZoneId = true,
                            State#zone_proc_state{
                                svr_zone_map = NewSvrZoneMap
                                ,max_zone_id = NewMaxZoneId
                            };
                        false ->
                            NewSvrZoneMap = maps:put(ServerId, MaxZoneId, SvrZoneMap),
                            ZoneInfo = CurrZone#zone_info{
                                servers = [ZoneSvr | ServerList]
                            },
                            proc_put_zone_only(ZoneInfo),
                            IsNewZoneId = false,
                            State#zone_proc_state{
                                svr_zone_map = NewSvrZoneMap
                            }
                    end;
                _False ->
                    {NewSvrZoneMap, ZoneInfo} = new_zone(MaxZoneId, [ZoneSvr], SvrZoneMap),
                    proc_put_zone(ZoneInfo),
                    IsNewZoneId = true,
                    State#zone_proc_state{
                        svr_zone_map = NewSvrZoneMap
                    }
            end,
            NewModuleState = cb_check_add_node(CallbackMod, IsNewZoneId, ZoneSvr, ZoneInfo, ModuleState),
            {noreply, NewState#zone_proc_state{module_state = NewModuleState}}
    end;

%% 节点下线
check_handle_event(info, {'NODE_OFF', ServerId, Node}, State = #zone_proc_state{
        svr_zone_map    = SvrZoneMap
        ,proc_state     = ProcState
        ,module_state   = ModuleState
        ,callback_mod   = CallbackMod
    }) ->
    if
        ProcState =/= ?STATE_RUNNING ->
            {noreply, State};
        true ->
            NewConnState = false,
            OtherUpdateList = [],
            case change_node_info(ServerId, SvrZoneMap, Node, NewConnState, OtherUpdateList) of
                {NewZoneSvr, NewZoneInfo} ->
                    NewModuleState = cb_check_node_off(CallbackMod, NewZoneSvr, NewZoneInfo, ModuleState),
                    NewState = State#zone_proc_state{
                        module_state = NewModuleState
                    },
                    {noreply, NewState};
                _ ->
                    {noreply, State}
            end
    end;

%% 每日定时器
check_handle_event(info, 'DAILY_CHECK', State = #zone_proc_state{zone_type = czone}) ->
    {noreply, State};
check_handle_event(info, 'DAILY_CHECK', State = #zone_proc_state{
        daily_timer = OldTimer
    }) ->
    util:cancel_timer(OldTimer),    % 防止外部消息引起多余定时消息
    After    = calc_next_midnight_diff(),
    NewTimer = erlang:send_after(After, self(), 'DAILY_CHECK'),
    NewState = daily_check(State),
    {noreply, NewState#zone_proc_state{daily_timer = NewTimer}};

%% 获取所有分区数据
check_handle_event({call, From}, 'GET_ALL_ZONES', State) ->
    Zones = proc_get_zones(),
    spawn(
        fun() -> gen_server:reply(From, Zones) end
    ),
    {noreply, State};
%% 获取指定分区数据
check_handle_event({call, _From}, {'GET_THE_ZONE', ServerId}, State = #zone_proc_state{
        svr_zone_map = SvrZoneMap
    }) ->
    ZoneId = cross_zone_api:find_zone_id(ServerId, SvrZoneMap),
    ZoneInfo = proc_get_zone(ZoneId),
    {reply, ZoneInfo, State};

%% 获取指定分区服务器
check_handle_event({call, _From}, {'GET_ZONE_SERVERS', ZoneId}, State) ->
    case proc_get_zone(ZoneId) of
        #zone_info{servers = Servers} ->
            SrvList =
                [
                    Server#zone_svr.server_id || Server <- Servers
                ],
            {reply, SrvList, State};
        _ ->
            {reply, [], State}
    end;

check_handle_event(cast, {'APPLY_TO_ZONE', ZoneId, MFA}, State) ->
    proc_apply_to_zone(ZoneId, MFA),
    {noreply, State};

%% 设置管理进程状态
check_handle_event(cast, 'SET_WAITING', State = #zone_proc_state{zone_type = czone}) ->
    {noreply, State};
check_handle_event(cast, 'SET_WAITING', State) ->
    NewState = do_set_waiting(State),
    {noreply, NewState};

%% 关闭进程
check_handle_event(cast, 'STOP', State) ->
    {stop, normal, State};

%% 执行指定MFA操作
check_handle_event(EventType, {'DO_MFA', WithData, MFA}, State) ->
    Ret =
    case {WithData, MFA} of
        {true,   {Mod, Fun, Args}} ->
            ZoneState = to_zone_state(State),
            erlang:apply(Mod, Fun, [ZoneState | Args]);
        {true,   {Fun, Args}} when is_function(Fun) ->
            ZoneState = to_zone_state(State),
            erlang:apply(Fun, [ZoneState | Args]);
        {_False, {Mod, Fun, Args}} ->
            erlang:apply(Mod, Fun, Args);
        {_False, {Fun, Args}} when is_function(Fun) ->
            erlang:apply(Fun, Args);
        _ ->
            {reply, error}
    end,
    conv_ret(EventType, Ret, State);

%% 其他所有操作通用处理
check_handle_event(EventType, EventContent, State = #zone_proc_state{
        callback_mod    = CallbackMod
    }) ->
    % ?INFO("handle other event:~w", [{EventType, EventContent}]),
    ZoneState = to_zone_state(State),
    Ret = cb_check_handle_event(CallbackMod, EventType, EventContent, ZoneState),
    conv_ret(EventType, Ret, State).

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

%% 通用的处理结果转换接口
conv_ret(EventType, {reply, Reply}, State) ->
    case EventType of
        {call, _} -> {reply, Reply, State};
        _         -> {noreply, State}
    end;

%% 通常情况下，返回的Data数据请直接返回ModuleState，
%% 在不得不更新SvrZoneMap、MaxZoneId数据时，才返回#zone_state{}，
%% 避免改坏分区管理数据（主要用于czone独立方式管理分区）
conv_ret(EventType, {reply, Reply, ReturnData}, State) ->
    NewState =
    case ReturnData of
        #zone_state{} ->
            update_proc_state(State, ReturnData);
        _ ->
            State#zone_proc_state{module_state = ReturnData}
    end,
    case EventType of
        {call, _} -> {reply, Reply, NewState};
        _         -> {noreply, NewState}
    end;

conv_ret(_EventType, {ok, ReturnData}, State) ->
    NewState =
    case ReturnData of
        #zone_state{} ->
            update_proc_state(State, ReturnData);
        _ ->
            State#zone_proc_state{module_state = ReturnData}
    end,
    {noreply, NewState};

conv_ret(_EventType, {stop, Reason}, State) ->
    {stop, Reason, State};

conv_ret(_EventType, ok, State) ->
    {noreply, State};

conv_ret(_EventType, _Ret, State) ->
    {noreply, State}.

%% 分配一个新的#zone_info{}，注意，ServerList的length应小于NumPerZone，这里不检验
new_zone(MaxZoneId, ServerList = [_|_], SvrZoneMap) ->
    ZoneInfo = #zone_info{
        zone_id = MaxZoneId,
        servers = ServerList
    },
    AddSvrZoneMap = maps:from_list([{ServerId, MaxZoneId} || #zone_svr{server_id = ServerId} <- ServerList]),
    NewSvrZoneMap = maps:merge(SvrZoneMap, AddSvrZoneMap),
    {NewSvrZoneMap, ZoneInfo}.

%% 设置等待状态，清空分区数据
do_set_waiting(State = #zone_proc_state{
        proc_name   = ProcName
        ,later_join = LaterJoin
    }) ->
    erase_zones(),
    erase(zone_ids),
    %% 更新进程状态，便于游戏服节点接入跨服中心时不需要通知到本进程
    NewProcState = ?STATE_WAITING,
    cross_zone_api:insert_worker(self(), ProcName, LaterJoin, NewProcState),
    %% module_state这里直接置空，如有需要，可以选择在重新计算分区'CALC_ZONE'时处理数据
    State#zone_proc_state{
        module_state    = undefined
        ,proc_state     = NewProcState
        ,max_zone_id    = 1
        ,svr_zone_map   = #{}
    }.

%% 无回调模块时，默认的handle_event
%% -> Ret :: {reply, Reply} | {reply, Reply, NewModuleState} |
%%      {ok, NewModuleState} | {stop, Reason} | ok
%% reply只针对call有效，cast/info会自动忽略
handle_event(_Event, _EventContent, _ZoneState) ->
    {reply, error}.

%% ---------------------------------------------
%% 检测进行回调
%% ---------------------------------------------

%% 无回调模块，按公共初始化接口进行初始
cb_check_init(undefined, InitArgs) ->
    ZoneInitRet = base_init(InitArgs),
    fix_init_ret(ZoneInitRet);
%% 尝试调用回调模块的init/1接口
cb_check_init(CallbackMod, InitArgs) when is_atom(CallbackMod) ->
    ZoneInitRet =
    case erlang:function_exported(CallbackMod, init, 1) of
        true  ->
            CallbackMod:init(InitArgs);
        false ->
            base_init(InitArgs)
    end,
    fix_init_ret(ZoneInitRet);
cb_check_init(_CallbackMod, _InitArgs) ->
    {error, badarg}.

%% 检测回调计算分区
cb_check_calc_zone(_CallbackMod, NumPerZone, open_time, Filter, JoinDay) ->
    %% 公共的按时间分区
    cross_zone_api:calc_zones_by_time(NumPerZone, Filter, JoinDay);
cb_check_calc_zone(undefined, NumPerZone, _, Filter, JoinDay) ->
    %% 未定义回调接口，其他类型分区都按时间分区
    cross_zone_api:calc_zones_by_time(NumPerZone, Filter, JoinDay);
cb_check_calc_zone(CallbackMod, NumPerZone, ZoneType, Filter, JoinDay) ->
    %% 定义了回调接口
    case erlang:function_exported(CallbackMod, calc_zone, 4) of
        true  ->
            CallbackMod:calc_zone(NumPerZone, ZoneType, Filter, JoinDay);
        false ->
            cross_zone_api:calc_zones_by_time(NumPerZone, Filter, JoinDay)
    end.

%% 重新计算分区时会调用
cb_check_reset_module_state(_, undefined, #zone_state{module_state = ModuleState}) ->
    ModuleState;
cb_check_reset_module_state(FunName, CallbackMod, ZoneState = #zone_state{module_state = ModuleState}) ->
    case erlang:function_exported(CallbackMod, FunName, 1) of
        true  ->
            CallbackMod:FunName(ZoneState);
        false ->
            ModuleState
    end.

%% 添加节点后回调计算
cb_check_add_node(undefined, _IsNewZoneId, _ZoneSvr, _ZoneInfo, OldModuleState) ->
    OldModuleState;
cb_check_add_node(CallbackMod, IsNewZoneId, ZoneSvr, ZoneInfo, OldModuleState) ->
    case erlang:function_exported(CallbackMod, add_node, 4) of
        true  ->
            CallbackMod:add_node(IsNewZoneId, ZoneSvr, ZoneInfo, OldModuleState);
        false ->
            OldModuleState
    end.

%% 节点离线后回调计算
cb_check_node_off(undefined, _ZoneSvr, _ZoneInfo, OldModuleState) ->
    OldModuleState;
cb_check_node_off(CallbackMod, NewZoneSvr, NewZoneInfo, OldModuleState) ->
    case erlang:function_exported(CallbackMod, node_off, 3) of
        true  ->
            CallbackMod:node_off(NewZoneSvr, NewZoneInfo, OldModuleState);
        false ->
            OldModuleState
    end.

%% 进程终止回调
cb_check_terminate(undefined, _Reason, _ModuleState) ->
    ignore;
cb_check_terminate(CallbackMod, Reason, ModuleState) ->
    case erlang:function_exported(CallbackMod, terminate, 2) of
        true  ->
            CallbackMod:terminate(Reason, ModuleState);
        false ->
            ignore
    end.

%% 事件处理回调
cb_check_handle_event(undefined, Event, EventContent, ZoneState) ->
    handle_event(Event, EventContent, ZoneState);
cb_check_handle_event(CallbackMod, Event, EventContent, ZoneState) ->
    CallbackMod:handle_event(Event, EventContent, ZoneState).

%% ---------------------------------------------
%% 每日检测应新加入分区的节点
%% 由于很多玩法限制了开服天数，导致节点连接的时候并未加入分区，
%% 到了相应开服天数，如果不主动触发加入分区，可能引起一些问题
%%
%% 1. 先看Filter有无值，进程状态是否?STATE_RUNNING，是否LaterJoin
%% 2. 通知节点管理器，取出游戏服节点数据，排除已加入节点后，
%%    用Filter筛选剩下符合条件的节点，再将结果异步到跨服小分区进程
%% 3. 由跨服小分区进程决定这一批未加入的服以何种顺序加入到分区
%% ---------------------------------------------

daily_check(State = #zone_proc_state{
        proc_state      = ?STATE_RUNNING
        ,later_join     = true
        ,filter         = Filter
        ,join_day       = JoinDay
        ,svr_zone_map   = SvrZoneMap
    }) ->
    case JoinDay > 1 orelse Filter =/= undefined of
        true  ->
            NowServerIds = maps:keys(SvrZoneMap),
            MFA = {?MODULE, get_add_nodes, [NowServerIds, JoinDay, Filter, self(), normal]},
            clusters_center:proc_apply(MFA);
        false ->
            ignore
    end,
    State;
daily_check(State) ->
    State.

%% ---------------------------------------------
%% 其他内部接口
%% ---------------------------------------------

%% 内部接口，由proc_put_zones调用
proc_put_zones_2([Zone | Zones], IsUpdate, ZoneIds) ->
    case proc_put_zone_only(Zone) of
        error ->
            proc_put_zones_2(Zones, IsUpdate, ZoneIds);
        ZoneId ->
            case lists:member(ZoneId, ZoneIds) of
                true  ->
                    proc_put_zones_2(Zones, IsUpdate, ZoneIds);
                false ->
                    proc_put_zones_2(Zones, true, [ZoneId | ZoneIds])
            end
    end;
proc_put_zones_2([], IsUpdate, ZoneIds) ->
    [IsUpdate, ZoneIds].

%% 进程字典分区数据清除
erase_zones() ->
    ZoneIds = proc_get_zone_ids(),
    erase_zones(ZoneIds).

erase_zones([ZoneId | ZoneIds]) ->
    erase(?ZONE(ZoneId)),
    erase_zones(ZoneIds);
erase_zones(_) ->
    ok.

%% 内部使用，异步到各分区或各游戏服执行MFA
do_apply_to_zone(false, _) -> ignore;
do_apply_to_zone(ZoneId, MFA) when is_integer(ZoneId) ->
    ZoneInfo = proc_get_zone(ZoneId),
    do_apply_to_zone(ZoneInfo, MFA);
do_apply_to_zone([ZoneInfo | ZoneInfos], MFA) ->
    do_apply_to_zone(ZoneInfo, MFA),
    do_apply_to_zone(ZoneInfos, MFA);
do_apply_to_zone(#zone_info{servers = ServerList}, MFA) ->
    do_apply_to_zone(ServerList, MFA);
do_apply_to_zone(#zone_svr{node = Node, conn = true}, MFA) ->
    rpc:cast(Node, util, do_mfa, [MFA]);
do_apply_to_zone(_, _MFA) -> ok.

%% 转换为通用的回调接口传入参数#zone_state{}
to_zone_state(#zone_proc_state{
        max_zone_id     = MaxZoneId
        ,svr_zone_map   = SvrZoneMap
        ,zone_sys       = ZoneSys
        ,module_state   = ModuleState
    }) ->
    #zone_state{
        max_zone_id     = MaxZoneId
        ,svr_zone_map   = SvrZoneMap
        ,zone_sys       = ZoneSys
        ,module_state   = ModuleState
    }.

%% 特殊的通过ZoneState更新ZoneProcState接口
update_proc_state(State, ZoneState) ->
    #zone_state{
        svr_zone_map = SvrZoneMap
        ,max_zone_id = MaxZoneId
        ,zone_sys = ZoneSys
        ,module_state = ModuleState
    } = ZoneState,
    State#zone_proc_state{
        svr_zone_map = SvrZoneMap
        ,max_zone_id = MaxZoneId
        ,zone_sys = ZoneSys
        ,module_state = ModuleState
    }.

%% 改变节点的连接状态
%% 如果是游戏节点下线，只有信息相符才会改变
%% 如果是改为连接状态，节点值不符则覆盖
change_node_info(ServerId, SvrZoneMap, Node, NewConnState, OtherUpdateList) ->
    case proc_get_svr_zone(ServerId, SvrZoneMap) of
        OldZoneInfo = #zone_info{servers = ServerList} ->
            case cross_zone_api:find_server(ServerId, ServerList) of
                OldZoneSvr = #zone_svr{node = Node} ->
                    %% 节点信息相符，直接更新状态
                    NewZoneSvr  = check_update_zone_svr(OldZoneSvr, [{conn, NewConnState} | OtherUpdateList]),
                    NewZoneInfo = cross_zone_api:store_to_zone_info(NewZoneSvr, OldZoneInfo),
                    proc_put_zone_only(NewZoneInfo),
                    {NewZoneSvr, NewZoneInfo};
                OldZoneSvr = #zone_svr{} when NewConnState =:= true ->
                    %% 节点信息不符，并且当前是建立连接，以当前Node信息为准
                    NewZoneSvr  = check_update_zone_svr(OldZoneSvr, [{node, Node}, {conn, NewConnState} | OtherUpdateList]),
                    NewZoneInfo = cross_zone_api:store_to_zone_info(NewZoneSvr, OldZoneInfo),
                    proc_put_zone_only(NewZoneInfo),
                    {NewZoneSvr, NewZoneInfo};
                _ ->
                    ignore
            end;
        _ ->
            ignore
    end.

check_update_zone_svr(ZoneSvr = #zone_svr{}, [{UpdateKey, UpdateVal} | Others]) ->
    NewZoneSvr =
    case UpdateKey of
        node        -> ZoneSvr#zone_svr{node = UpdateVal};
        conn        -> ZoneSvr#zone_svr{conn = UpdateVal};
        server_num  -> ZoneSvr#zone_svr{server_num = UpdateVal};
        _           -> ZoneSvr
    end,
    check_update_zone_svr(NewZoneSvr, Others);
check_update_zone_svr(ZoneSvr, []) ->
    ZoneSvr.

%% 检测修正小分区中游戏服数量参数
fix_num_per_zone(NumPerZone) ->
    case is_integer(NumPerZone) of
        true  ->
            min(max(NumPerZone, 1), 20);
        false ->
            4
    end.

fix_delay_time(false) -> false;
fix_delay_time(DelayTime) when is_integer(DelayTime) ->
    case DelayTime >= 0 andalso DelayTime < ?MAX_TIMEOUT of
        true  ->
            DelayTime;
        false ->
            ?DELAY_DEFAULT
    end;
fix_delay_time(_) ->
    ?DELAY_DEFAULT.

fix_join_day(JoinDay) when is_integer(JoinDay) ->
    max(JoinDay, 0);
fix_join_day(_) ->
    0.

check_filter(Filter = {M, F, A}) when is_atom(M) andalso is_atom(F) andalso is_list(A) ->
    Filter;
check_filter(_) ->
    undefined.

%% 初始化参数
init_params(InitRet, [{Key, Value} | RemainList]) ->
    RecordFields = record_info(fields, zone_init_ret),
    RecordSize   = record_info(size, zone_init_ret),
    FieldIndexs  = lists:zip(RecordFields, lists:seq(2, RecordSize)),
    NewRet =
    case lists:keyfind(Key, 1, FieldIndexs) of
        {_Field, Idx} ->
            setelement(Idx, InitRet, Value);
        _ -> InitRet
    end,
    init_params(NewRet, RemainList);
init_params(InitRet, [_|RemainList]) ->
    init_params(InitRet, RemainList);
init_params(Ret, _) ->
    Ret.

%% 重设部分参数
reset_params(State, [{Key, Value} | RemainList]) ->
    NewState =
    case Key of
        num_per_zone    -> State#zone_proc_state{num_per_zone = fix_num_per_zone(Value)};
        filter          -> State#zone_proc_state{filter       = check_filter(Value)};
        join_day        -> State#zone_proc_state{join_day     = fix_join_day(Value)};
        _               -> State
    end,
    reset_params(NewState, RemainList);
reset_params(State, [_|RemainList]) ->
    reset_params(State, RemainList);
reset_params(State, _) ->
    State.

fix_init_ret(ZoneInitRet = #zone_init_ret{
        num_per_zone = NumPerZone
        ,delay_time  = DelayTime
        ,filter      = Filter
        ,later_join  = LaterJoin
        ,join_day    = JoinDay
    }) ->
    ZoneInitRet#zone_init_ret{
        num_per_zone = fix_num_per_zone(NumPerZone)
        ,delay_time  = fix_delay_time(DelayTime)
        ,filter      = check_filter(Filter)
        ,later_join  = LaterJoin =:= true
        ,join_day    = fix_join_day(JoinDay)
    };
fix_init_ret(Unknown) ->
    Unknown.

%% 启动时，已经存在分区数据的情况，设置分区数据
%% -> set_init_zones(InitZones) -> {MaxZoneId, SvrZoneMap, IsNotInit}.
%% 不存在初始分区数据时，InitZones = undefined
%% 当存在初始分区数据时，InitZones支持两种数据：
%%  1. [{ServerId, ZoneId}, ...]
%%  2. [#zone_info{}, ...]
%% 通常不应混用两种格式，应当只使用一种数据格式（为更灵活设定初始数据才使用两种格式）
set_init_zones([_SvrZone | _SvrZoneList] = InitZones) ->
    {MaxZoneId, SvrZoneMap, ZoneInfoList} = do_set_init_zones(InitZones, 1, #{}, []),
    proc_put_zones(ZoneInfoList),
    {MaxZoneId, SvrZoneMap, false};
set_init_zones([]) ->
    SvrZoneMap = #{},
    MaxZoneId  = 1,
    {MaxZoneId, SvrZoneMap, false};
set_init_zones(_Undefined) ->
    SvrZoneMap = #{},
    MaxZoneId  = 1,
    {MaxZoneId, SvrZoneMap, true}.

%% 以[{ServerId, ZoneId}]生成初始信息
do_set_init_zones([{ServerId, ZoneId} | SvrZoneList], MaxZoneId, SvrZoneMap, AccZoneInfoList) ->
    NewSvrZoneMap   = SvrZoneMap#{ServerId => ZoneId},
    NewMaxZoneId    = max(MaxZoneId, ZoneId),
    ServerInfo      = #zone_svr{server_id = ServerId, conn = false},
    NewAccZoneList  =
    case lists:keyfind(ZoneId, #zone_info.zone_id, AccZoneInfoList) of
        #zone_info{
            servers = ServerList
        } = ZoneInfo ->
            NewZoneInfo = ZoneInfo#zone_info{
                servers = [ServerInfo | ServerList]
            },
            [NewZoneInfo | lists:keydelete(ZoneId, #zone_info.zone_id, AccZoneInfoList)];
        _ ->
            NewZoneInfo = #zone_info{zone_id = ZoneId, servers = [ServerInfo]},
            [NewZoneInfo | AccZoneInfoList]
    end,
    do_set_init_zones(SvrZoneList, NewMaxZoneId, NewSvrZoneMap, NewAccZoneList);

%% 直接以[#zone_info{}]设置分区数据（分区各服状态置为未连接）
do_set_init_zones([ZoneInfo | ZoneInfos], MaxZoneId, SvrZoneMap, AccZoneInfoList) ->
    case valid_zone(ZoneInfo) of
        true  ->
            #zone_info{zone_id = ZoneId, servers = ServerList} = ZoneInfo,
            NewMaxZoneId        = max(MaxZoneId, ZoneId),
            AddSvrZoneMap       = maps:from_list([{ServerId, ZoneId} || #zone_svr{server_id = ServerId} <- ServerList]),
            NewSvrZoneMap       = maps:merge(SvrZoneMap, AddSvrZoneMap),
            NewServerList       = [ZoneSvr#zone_svr{conn = false} || ZoneSvr = #zone_svr{} <- ServerList],
            NewZoneInfo         = ZoneInfo#zone_info{servers = NewServerList},
            NewAccZoneInfoList = [NewZoneInfo | AccZoneInfoList],
            do_set_init_zones(ZoneInfos, NewMaxZoneId, NewSvrZoneMap, NewAccZoneInfoList);
        false ->    %% 不合法数据，稳定版忽略错误执行此分支（开发版会抛出异常，便于及早发现问题）
            do_set_init_zones(ZoneInfos, MaxZoneId, SvrZoneMap, AccZoneInfoList)
    end;

do_set_init_zones([], MaxZoneId, SvrZoneMap, AccZoneInfoList) ->
    {MaxZoneId, SvrZoneMap, AccZoneInfoList}.

-ifdef(DEV_SERVER).
valid_zone(#zone_info{zone_id = ZoneId}) when ZoneId > 0 ->
    true;
valid_zone(ZoneInfo) ->
    erlang:throw({error, ZoneInfo}).
-else.
valid_zone(#zone_info{zone_id = ZoneId}) when ZoneId > 0 ->
    true;
valid_zone(_ZoneInfo) ->
    false.
-endif.

calc_next_midnight_diff() ->
    NowTime  = utime:unixtime(ms),
    NextMidnight = utime:tomorrow(NowTime div 1000),
    erlang:max(NextMidnight * 1000 - NowTime, 0).
