%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 游戏服节点管理
%%%
%%% 补充说明：
%%% 运维系统、注册系统通过此模块写入的node表数据，来获取到游戏服节点的域名和端口信息。
%%% 客户端需要连接的节点所监听的ip、host，必须写入到node表，且id必须要>=10，才能被获取到。
%%% 【不要问为什么，问就是历史包袱，非游戏代码内部系统，很难要求其他部门配合修改】，
%%% 所以，需要硬编码id为10。
%%% @end
%%% @since 2017-03-08
%%% -------------------------------------------------------
-module(node_mgr).
-behaviour(gen_server).
-author("huangyongxing@yeah.net").
%% API
-export([
        start_link/1
        ,node_id/0                          % 节点Id
        ,node_list/0                        % 集群中所有节点
        ,node_list_others/0                 % 集群中除本节点外的其他节点
        ,apply_cast_other_nodes/3           % 异步在集群其他节点执行函数
        ,update_element/2                   % 更新节点信息
        ,is_master_node/0                   % 是否主节点
    ]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-include("common.hrl").
-include("node.hrl").
-include("server.hrl").

-define(MASTER_NODE_ID, 10).                % 游戏服主节点（兼容旧项目的设定，后台需要，设定节点id为10）

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

start_link({Host, Port}) ->
    NodeId = ?MASTER_NODE_ID,       % 如果后续需要扩展，可传递参数决定节点Id
    gen_server:start_link({local, ?MODULE}, ?MODULE, [NodeId, Host, Port], []).

%% 更新节点信息（通常只更新ETS表中的部分数据，并且节点管理器中不需要更新这些数据）
-spec update_element(Id :: integer(), UpdateList :: [{Index, Value}]) -> boolean() when
    Index :: integer(),
    Value :: term().
update_element(Id, UpdateList) ->
    NewUpdateList = [
        Update || Update = {Index, _} <- UpdateList,
        lists:member(Index, [#node.num, #node.state])
    ],
    ets:update_element(?ETS_NODE, Id, NewUpdateList).

%% 查询当前节点ID号
node_id() ->
    gen_server:call(?MODULE, node_id).

%% 获取所有游戏节点的列表（兼容旧调用，后续再扩展）
node_list() ->
    try ets:tab2list(?ETS_NODE) of
        NodeList when is_list(NodeList) ->
            NodeList
    catch _A:_B -> []
    end.

%% 获取除本身之外的所有节点信息
node_list_others() ->
    AllNodeList = node_list(),
    SelfNode = node(),
    [Node || Node = #node{node = NodeName} <- AllNodeList, NodeName =/= SelfNode].

%% 异步在集群所有其他节点执行函数
apply_cast_other_nodes(Module, Func, Args) ->
    NodeListOther = node_list_others(),
    [rpc:cast(Node, Module, Func, Args) || #node{node = Node} <- NodeListOther].

%% 检查是否主节点
is_master_node() ->
    node_id() =:= ?MASTER_NODE_ID.

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

init([Id, Host, Port]) ->
    process_flag(trap_exit, true),
    net_kernel:monitor_nodes(true, [{node_type, all}]),
    ets:new(?ETS_NODE, [{keypos, #node.id}, named_table, public, set]),
    Node = #node{
        id = Id
        ,host = Host
        ,port = Port
        ,node = node()
        ,cookie = erlang:get_cookie()
    },
    add_node(Node),
    add_node_ets(Node),
    {ok, #node_mgr{self_node = Node, others = []}}.

handle_cast(_Request, State) ->
    {noreply, State}.

handle_call(node_id, _From, State = #node_mgr{self_node = #node{id = NodeId}}) ->
    {reply, NodeId, State};

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

handle_info({join, Node = #node{node = NodeName}}, State = #node_mgr{
        self_node = #node{node = SelfNodeName}, others = Others}) ->
    ClsNode = config:get_cls_node(),
    NewOthers =
    if
        NodeName =:= SelfNodeName -> Others;
        NodeName =:= ClsNode      -> Others;
        true ->
            ?INFO("node [~ts] get msg node [~ts] join", [SelfNodeName, NodeName]),
            case lists:keymember(NodeName, #node.node, Others) of
                true  -> Others;
                false ->
                    add_node_ets(Node),
                    [Node | Others]
            end
    end,
    {noreply, State#node_mgr{others = NewOthers}};

handle_info({nodeup, NodeName, _}, State = #node_mgr{self_node = SelfNode, others = Others}) ->
    ClsNode = config:get_cls_node(),
    AllNodes = [SelfNode | Others],
    if
        NodeName =:= ClsNode -> ignore;
        true ->
            case lists:keymember(NodeName, #node.node, AllNodes) of
                true  ->    %% 节点信息已经存在，忽略
                    ignore;
                false ->
                    case atom_to_binary(NodeName, utf8) of
                        <<"peb_client_", _/binary>> ->
                            %% php的peb库通过erlang Interface与erlang节点通讯
                            %% 如果向该通讯节点发送消息，会被认为是返回的执行结果
                            %% 所以过滤掉节点名前缀为peb_client_的节点
                            ignore;
                        _ ->
                            erlang:send({?MODULE, NodeName}, {join, SelfNode})
                    end
            end
    end,
    {noreply, State};

handle_info({nodedown, NodeName, _}, State = #node_mgr{others = Others}) ->
    NewOthers =
    case lists:keyfind(NodeName, #node.node, Others) of
        #node{id = NodeId} ->
            ets:delete(?ETS_NODE, NodeId),
            try del_node(NodeId)
            catch _:_ -> ignore
            end,
            lists:keydelete(NodeName, #node.node, Others);
        _ ->
            Others
    end,
    {noreply, State#node_mgr{others = NewOthers}};

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, State = #node_mgr{self_node = #node{id = NodeId}}) ->
    del_node(NodeId),
    {ok, State}.

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

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

add_node_ets(Node) when is_record(Node, node) ->
    ets:insert(?ETS_NODE, Node);
add_node_ets(_) -> ignore.

%% 添加节点信息到数据库
add_node(#node{id = Id, host = Host, port = Port, node = Node, cookie = Cookie}) ->
    db:execute(
        io_lib:format(
            "REPLACE INTO `node` (`id`,`ip`,`port`,`node`,`cookie`) VALUES(~w,'~ts',~w,'~ts','~ts')",
            [Id, Host, Port, Node, Cookie]
        )
    ).

%% 从数据库删除节点信息
del_node(Id) ->
    db:execute(io_lib:format(<<"DELETE FROM `node` WHERE id=~w">>,[Id])).

