%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 跨服分区相关系统功能触发管理
%%% @end
%%% -------------------------------------------------------
-module(czone_fire).
%% 跨服中心节点回调
-export([
        init/2                      % 进程启动后初始回调，处理数据初始化
        ,rezone/5                   % 重新分区时回调
        ,add_svrs/2                 % 新增注册服时回调（初阶分区，各服独自一个子分区）
        ,conn/3                     % 节点连接时回调
        ,notice_rezone/2            % 通知不久的将来即将进行分区
    ]).
%% 游戏服节点回调
-export([
        conn_do_game_node/1         % 节点连接后，由跨服中心回调到游戏服节点执行
        ,rezone_do_game_node/1      % 跨服分区变化后，由跨服中心回调到游戏服节点执行
        ,add_svrs_do_game_node/1    % 游戏服初始加入跨服分区后，由跨服中心回调到游戏服节点执行
        ,notice_rezone_game_node/1  % 回调到游戏服节点执行，通知不久的将来即将进行分区
        ,disconnected/0             % 断开与跨服中心的连接之后回调
    ]).

-include("common.hrl").
-include("cross_zone.hrl").
-include("czone.hrl").
-include("def_event.hrl").

%%%===================================================================
%%% 回调统一管理接口
%%%===================================================================

%% @doc
%% 初始化加载跨服分区各功能业务数据
%%
%% Callbacks中，每个Callback为{Module, Func, Args}
%% 回调接口的实际参数为： [CBMap | Args]
%% 回调接口要求返回值为： #cz_state{} | ok
%% CBMap :: #{cz_state => CZState, svr_zone_map => SvrZoneMap}.
%% 注： CBMap 参见 init 接口
%% @end
init_callbacks() ->
    %% {Module, Func, Args}
    [
    ].

%% @doc
%% 添加注册服列表回调 - 初次分区
%%
%% Callbacks中，每个Callback为{Module, Func, Args}
%% 回调接口的实际参数为： [CBMap | Args]
%% 回调接口要求返回值为： #cz_state{} | ok
%% CBMap :: #{cz_state => CZState, add_servers => AddServers}.
%% 注： CBMap 参见 add_svrs 接口
%% @end
add_svrs_callbacks() ->
    %% {Module, Func, Args}
    [
        {cross_guild_war_mgr,add_svrs,[]}
        ,{race_rank_mgr,add_svrs,[]}
        ,{god_dun_rank_mgr, add_svrs, []}
        ,{mod_chat_zone, add_svrs, []}
        ,{deserted_boss_sup, add_svrs, []}
    ].

%% @doc
%% 跨服服务器开启状态下，动态重新分区回调
%%
%% Callbacks中，每个Callback为{Module, Func, Args}
%% 回调接口的实际参数为： [CBMap | Args]
%% 回调接口要求返回值为： #cz_state{} | ok
%% CBMap :: #{
%%     cz_state => CZState, svr_zone_map => SvrZoneMap,
%%     zone_map => ZoneMap, faction_map => FactionMap,
%%     stage => NewStage
%% }
%% 注： CBMap 参见 rezone 接口
%% @end
rezone_callbacks() ->
    % {Module, Func, Args}
    [
        {race_rank_mgr,recalc_zone,[]}
        ,{camp_war_mgr, rezone_callback, []}
        ,{god_dun_rank_mgr, recalc_zone, []}
        ,{beauty_contest, recalc_zone, []}
        ,{mod_chat_zone, recalc_zone, []}
        ,{cross_guild_war_mgr,recalc_zone,[]}
        ,{seckill_center,recalc_zone,[]}
        ,{cross_rank_mgr, recalc_zone, []}
        ,{mod_vie_rank_center,recalc_zone, []}
        ,{deserted_boss_sup, recalc_zone, []}
        ,{mod_military_rank_mgr, recalc_zone, []}
        ,{cross_point_fight_center, recalc_zone, []}
        ,{mod_legion_rank_mgr, recalc_zone, []}
        ,{mod_pet_support_center, recalc_zone, []}
    ].

%% @doc
%% 跨服分区中的游戏服连接到跨服节点时回调
%%
%% Callbacks中，每个Callback为{Module, Func, Args}
%% 回调接口的实际参数为： [CBMap | Args]
%% 回调接口要求返回值为： #cz_state{} | ok
%% CBMap :: #{cz_state => CZState, zone_info => #zone_info{}, zone_svr => #zone_svr{}}
%% 注： CBMap 参见 conn 接口
%% @end
conn_callbacks() ->
    % {module, Func, Args}
    [
    ].


%% @doc
%% 跨服分区中的游戏服连接后，回调到游戏服节点
%% 回调接口的实际参数为： [ZoneId | Args]
%% 回调接口的返回值将被忽略
%% @end
conn_game_node_callbacks() ->
    % {module, Func, Args}
    [
        {deserted_boss_mgr, conn_game_node, []}
    ].

%%%===================================================================
%%% 跨服中心回调API
%%%===================================================================

-spec init(CZState, SvrZoneMap) -> NewCZState when
    SvrZoneMap :: #{ServerId => ZoneId},
    ServerId   :: non_neg_integer(), ZoneId :: non_neg_integer(),
    CZState    :: #cz_state{},
    NewCZState :: #cz_state{}.
%% @doc
%% 初始化加载跨服分区各功能业务数据
%%
%% 跨服分区进程启动时调用，注意性能影响
%% 传入参数 #cz_state{} 为系统数据，包含分区基本信息数据
%% 具体的分区详细数据保存在进程字典中，可通过cross_zone相关proc_XXX接口获取
%% 函数返回值必须是 #cz_state{}
%%
%% 注意：
%% 如果功能系统是独立进程维护，数据并不存储于czone分区进程中，
%% 则此处回调通常是无意义的，因为czone启动后，相关功能进程不确定是否已经启动
%% @end
init(CZState, SvrZoneMap) ->
    Callbacks = init_callbacks(),
    CBMap = #{
        cz_state => CZState
        ,svr_zone_map => SvrZoneMap
    },
    #{cz_state := NewCZState} = lists:foldl(
        fun do_init_cb/2,
        CBMap,
        Callbacks),
    NewCZState.

-spec add_svrs(CZState, AddServers) -> NewCZState when
    CZState    :: #cz_state{},
    NewCZState :: #cz_state{},
    AddServers :: [{ServerId, ZoneId, Faction}],
    ServerId :: non_neg_integer(), ZoneId :: non_neg_integer(),
    Faction :: non_neg_integer().
%% @doc
%% 添加注册服列表回调
%%
%% 有新的游戏服正式注册时，将产生新的独立子分区（初阶分区为1区1服）
%% AddServers :: [{ServerId, ZoneId, Faction}, ...]
%% @end
add_svrs(CZState, AddServers) ->
    %% 通知回调到游戏服节点
    add_svrs_notice_game_node(AddServers),
    %% 执行其他回调
    Callbacks = add_svrs_callbacks(),
    CBMap = #{
        cz_state => CZState
        ,add_servers => AddServers
    },
    #{cz_state := NewCZState} = lists:foldl(
        fun do_add_svrs_cb/2,
        CBMap,
        Callbacks),
    NewCZState.

add_svrs_notice_game_node(AddServers) ->
    lists:foreach(fun({_ServerId, ZoneId, Faction}) ->
                Stage = 0,
                CZoneSvrLocal = czone:to_czone_svr_local(ZoneId, Stage, Faction),
                MFA = {?MODULE, add_svrs_do_game_node, [CZoneSvrLocal]},
                cross_zone:proc_apply_to_zone(ZoneId, MFA)
        end, AddServers).

%% 预告将进行跨服分区
notice_rezone(ZoneId, ScheduledTime) ->
    MFA = {?MODULE, notice_rezone_game_node, [ScheduledTime]},
    cross_zone:proc_apply_to_zone(ZoneId, MFA),
    ok.

-spec rezone(CZState, SvrZoneMap, ZoneMap, FactionMaps, NewZoneInfo) -> NewCZState when
    CZState     :: #cz_state{},
    NewCZState  :: #cz_state{},
    SvrZoneMap  :: #{ServerId => NewZoneId},
    ZoneMap     :: #{OldZoneId => NewZoneId},
    FactionMaps :: #{OldZoneId => FactionMap :: #{OldFaction => NewFaction}},
    NewZoneInfo :: #zone_info{},
    ServerId    :: non_neg_integer(),
    OldZoneId   :: non_neg_integer(), NewZoneId  :: non_neg_integer(),
    OldFaction  :: non_neg_integer(), NewFaction :: non_neg_integer().
%% @doc
%% 跨服服务器开启状态下，动态重新分区回调
%%
%% 注意：1. 处理业务逻辑时，需要留意，有可能相关游戏服当时并未连接到跨服，
%%       例如正在迁移、合服或其他意外情况
%%       2. 此接口会被反复调用，每次只执行单个分区合并任务
%% CZState       分区各功能模块状态数据
%% NewZoneInfo   #zone_info{}
%% ZoneMap       #{OldZoneId  => NewZoneId}
%% FactionMaps   #{OldZoneId  => FactionMap :: #{OldFaction => NewFaction}}
%%
%% ** ZoneMap、FactionMaps记录的是副分区变更情况，主分区由于没有变化，所以不记录在这里。
%%    这两个数据并不是全量数据，仅是全量的变化数据。
%%
%% 实际意义上（未来如有变更，再另行说明），
%% ZoneMap中只有一个key-value，    即 "副分区ZoneId" => "新分区ZoneId（原主分区ZoneId）"
%% FactionMaps也只有一个key-value，即 "副分区ZoneId" => "阵营变更情况FactionMap"
%% -------------------------
%% FactionMap（阵营变更情况）中，有两类情况:
%% OldFaction == NewFaction ----> 该阵营并入了主分区的相同id的阵营
%% OldFaction /= NewFaction ----> 该阵营并入了主分区另一个id的阵营
%% @end
rezone(CZState, NewSvrZoneMap, ZoneMap, FactionMaps, NewZoneInfo) ->
    #zone_info{
        zone_ext = #czone_info{stage = Stage}
    } = NewZoneInfo,
    %% 更新各服的ZoneId数据
    rezone_notice_game_node(ZoneMap),
    %% 执行其他回调
    Callbacks = rezone_callbacks(),
    CBMap = #{
        cz_state => CZState
        ,svr_zone_map => NewSvrZoneMap
        ,zone_map => ZoneMap
        ,faction_maps => FactionMaps
        ,stage => Stage
    },
    #{cz_state := NewCZState} = lists:foldl(
        fun do_rezone_cb/2,
        CBMap,
        Callbacks
    ),
    NewCZState.

%% @doc
%% 重新分区通知到游戏服节点
%% 目前是该新分区中全部服都通知（分区id无变化的服也会通知到）
%% @end
rezone_notice_game_node(ZoneMap) ->
    ZoneChangeList = maps:to_list(ZoneMap),
    lists:foreach(fun({OldZoneId, NewZoneId}) when OldZoneId =/= NewZoneId ->
                ZoneInfo = cross_zone:proc_get_zone(NewZoneId),
                CZoneSvrLocalList = czone:to_czone_svr_local_list(ZoneInfo),
                lists:foreach(fun({ServerId, CZoneSvrLocal}) ->
                            MFA = {?MODULE, rezone_do_game_node, [CZoneSvrLocal]},
                            clusters_center:apply_cast(ServerId, MFA)
                    end, CZoneSvrLocalList);
            (_) ->
                ignore
        end, ZoneChangeList).

%% @doc
%% 跨服分区中的游戏服连接到跨服节点时回调
%% @end
conn(CZState, ZoneInfo, ZoneSvr) ->
    %% 先触发通知游戏服统一回调
    %% 如果是在游戏服回调执行的，通常在这个子接口中进行回调管理
    conn_notice_game_node(ZoneInfo, ZoneSvr),
    %% 执行其他回调
    Callbacks = conn_callbacks(),
    CBMap = #{
        cz_state => CZState
        ,zone_info => ZoneInfo
        ,zone_svr => ZoneSvr
    },
    #{cz_state := NewCZState} = lists:foldl(
        fun do_conn_cb/2,
        CBMap,
        Callbacks
    ),
    NewCZState.

%% 跨服分区中游戏服连接后，通知进行游戏服回调
conn_notice_game_node(ZoneInfo, ZoneSvr) ->
    #zone_info{zone_id = ZoneId, zone_ext = #czone_info{stage = Stage}} = ZoneInfo,
    CZoneSvrLocal = czone:to_czone_svr_local(ZoneId, Stage, ZoneSvr),
    %% 由于基于cross_zone的add_node，无法直接取得ScheduledTime，
    %% 这里异步产生一个进程，执行通知此服计划分区时间的任务
    %% （跨服中心节点在notice后、rezone前启动，或游戏服在此期间连接过来的，
    %% 由于错过了notice时机，所以需要重新通知此数据到游戏服）
    #zone_svr{server_id = ServerId, node = Node} = ZoneSvr,
    proc_lib:spawn(fun() ->
                case czone:get_scheduled_time(ServerId) of
                    {_NoticeState, _ZoneId, ScheduledTime, _NextStage} ->
                        MFA = {czone_api, env_set_schedule, [ScheduledTime]},
                        clusters_center:apply_cast(Node, MFA);
                    _Error ->
                        ignore
                end
        end),
    MFA = {?MODULE, conn_do_game_node, [CZoneSvrLocal]},
    cross_zone:proc_apply_to_zone(ZoneSvr, MFA).

%% TODO
%% 合服 做分区数据更新回调、小分区合并回调
%%


%%%===================================================================
%%% 游戏服回调API
%%%===================================================================

%% 跨服分区中游戏服连接后，回调到游戏服本地执行
conn_do_game_node(CZoneSvrLocal = #czone_svr_local{
        zone_id = ZoneId
    }) ->
    %% 先设置分区信息
    czone_api:env_set_zone(CZoneSvrLocal),
    %% 跨服主城状态通知
    czone_api:notice_city_state(),
    %% 执行其他回调
    Callbacks = conn_game_node_callbacks(),
    lists:foreach(fun({Mod, Func, Args}) ->
                erlang:apply(Mod, Func, [ZoneId | Args])
        end, Callbacks),
    %% 分区连接事件（因为时机通常会发生在节点连接事件之后，所以有必要区分）
    lib_player_event:brocast_dispatch(?EVENT_CZONE_CONN),
    ok.

%% 游戏服初始加入分区时，由跨服中心回调到游戏服节点执行
add_svrs_do_game_node(CZoneSvrLocal) ->
    %% 设置分区信息
    czone_api:env_set_zone(CZoneSvrLocal),
    %% 跨服主城状态通知
    czone_api:notice_city_state(),
    %% 执行其他回调
    lib_player_event:brocast_dispatch(?EVENT_CZONE_INIT),
    ok.

%% 跨服分区即将调整前，回调到游戏服本地执行
notice_rezone_game_node(ScheduledTime) ->
    %% 设置状态
    czone_api:env_set_schedule(ScheduledTime),
    %% 通知客户端
    czone_api:notice_rezone_to_client(ScheduledTime),
    %% 其他回调
    lib_player_event:brocast_dispatch(?EVENT_CZONE_NOTICE_REZONE),
    ok.

%% 跨服分区变化时，回调到游戏服本地执行
rezone_do_game_node(CZoneSvrLocal = #czone_svr_local{zone_id = ZoneId,stage=Stage}) ->
    %% 清除分区倒计时状态
    czone_api:env_del_schedule(),
    %% 获取旧分区资料
    _OldZoneId = czone_api:env_get_zone_id(),
    %% 重新设置分区信息
    czone_api:env_set_zone(CZoneSvrLocal),
    %% 跨服主城状态通知
    czone_api:notice_city_state(),
    %% 执行其他回调
    lib_player_event:brocast_dispatch(?EVENT_CZONE_REZONE_FINSIH,[ZoneId]),

    Stage>=2 andalso mod_cgwar_ctrl:sync_cur_state(node()),

    ok.

%% 与跨服连接断开
disconnected() ->
    %% 清除分区倒计时状态
    czone_api:env_del_schedule(),
    %% 清除分区资料
    czone_api:env_del_zone(),
    %% 跨服主城状态通知
    czone_api:notice_city_state(),
    %% 执行其他回调

    %% 暂时没有必要做角色事件回调，相关逻辑可以在与跨服中心断开连接事件中处理
    ok.

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

do_init_cb({Mod, Func, Args}, CBMap) ->
    case erlang:apply(Mod, Func, [CBMap | Args]) of
        NewCZState = #cz_state{} ->
            CBMap#{cz_state := NewCZState};
        _Ok ->
            CBMap
    end.

do_add_svrs_cb({Mod, Func, Args}, CBMap) ->
    case erlang:apply(Mod, Func, [CBMap | Args]) of
        NewCZState = #cz_state{} ->
            CBMap#{cz_state := NewCZState};
        _Ok ->
            CBMap
    end.

do_rezone_cb({Mod, Func, Args}, CBMap) ->
    case erlang:apply(Mod, Func, [CBMap | Args]) of
        NewCZState = #cz_state{} ->
            CBMap#{
                cz_state := NewCZState
            };
        _Ok ->
            CBMap
    end.

do_conn_cb({Mod, Func, Args}, CBMap) ->
    case erlang:apply(Mod, Func, [CBMap | Args]) of
        NewCZState = #cz_state{} ->
            CBMap#{cz_state := NewCZState};
        _Ok ->
            CBMap
    end.

