%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 跨服本地服节点管理
%%% @end
%%% @since 2017-03-03
%%% -------------------------------------------------------
-module(clusters_node).
-behaviour(gen_server).
-author("huangyongxing@yeah.net").
%% API
-export([
        start_link/0
        ,apply_call/1
        ,apply_call/3
        ,apply_cast/1
        ,apply_cast/3
        ,get_node/0
        ,connected/2
        ,notice_invalid/2
        ,get_process_pid/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("clusters.hrl").

-define(SERVER, ?MODULE).

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

start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%% 跨服节点同步调用跨服中心函数
%% 为防止跨服堵塞，实际在管理进程中是异步实现，只是调用者被阻塞
apply_call(Module, Func, Args) ->
    call_server({'apply_call', {Module, Func, Args}}).

apply_call(MFA) ->
    call_server({'apply_call', MFA}).

%% 跨服节点异步调用跨服中心函数
apply_cast(Module, Func, Args) ->
    cast_server({'apply_cast', {Module, Func, Args}}).

apply_cast(MFA) ->
    cast_server({'apply_cast', MFA}).

%% 获取负责连接跨服中心的本地服节点名
get_node() ->
    case get(clusters_node) of
        undefined ->
            case call_server(get_node) of
                none -> none;
                Node ->
                    case is_atom(Node) of
                        true  ->
                            put(clusters_node, Node),
                            Node;
                        false -> none
                    end
            end;
        Node ->
            Node
    end.

connected(CenterNode, Pid) ->
    cast_server({connected, CenterNode, Pid}),
    clusters_api:node_connected(CenterNode).

notice_invalid(CenterNode, GameNode) ->
    cast_server({invalid, CenterNode, GameNode}).

%% 获取管理进程pid（如果后续扩展为可伸缩多节点需要调整）
get_process_pid() ->
    whereis(?SERVER).

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

init([]) ->
    process_flag(trap_exit, true),
    ClustersNode = #clusters_node{
        center_node = CenterNode = config:get_cls_node()
        ,cookie = Cookie = config:get_cls_cookie()
        ,platform = config:get_platform()
        ,server_id = config:get_server_id()
        ,wait_conn = false
    },
    erlang:set_cookie(CenterNode, Cookie),
    MyPid = self(),
    ConnWorker = proc_lib:spawn_link(fun() -> conn_node_server(MyPid) end),
    ConnTimer = try_conn(ClustersNode#clusters_node{conn_worker = ConnWorker}),
    NewClustersNode = ClustersNode#clusters_node{
        conn_timer = ConnTimer, conn_times = 1, conn_worker = ConnWorker, wait_conn = true
    },
    {ok, NewClustersNode}.

%% 已经建立连接，可以进行跨服操作
handle_cast({connected, CenterNode, Pid}, State = #clusters_node{
        center_node = CenterNode, pid = undefined, conn_timer = Timer, conn_times = _Times
    }) ->
    Monitor = erlang:monitor(process, Pid),
    util:cancel_timer(Timer),
    NewState = State#clusters_node{pid = Pid, monitor = Monitor, conn_timer = undefined, wait_conn = false},
    ?INFO("Success to build connection to [~w], try times[~w]", [CenterNode, _Times]),
    {noreply, NewState};

handle_cast({invalid, CenterNode, _GameNode}, State = #clusters_node{
        center_node = CenterNode, pid = undefined, conn_timer = Timer
    }) ->
    util:cancel_timer(Timer),
    {ok, [[ConfigFile | _] | _]} = init:get_argument(config),
    ?ERR("Invalid cluster config! Please check server_id in ~ts, it may be repetitive!", [ConfigFile]),
    NewState = State#clusters_node{conn_timer = undefined, conn_times = -1, wait_conn = false},
    {noreply, NewState};

handle_cast({'apply_cast', MFA}, State = #clusters_node{center_node = CenterNode, pid = Pid}) ->
    case is_pid(Pid) of
        true  -> clusters_api:rpc_cast_do_mfa(CenterNode, MFA);
        false -> ignore
    end,
    {noreply, State};

handle_cast(_Request, State) ->
    ?ERR("handle_cast recv unexpected request:~p", [_Request]),
    {noreply, State}.

handle_call({'apply_call', MFA}, From, State = #clusters_node{center_node = CenterNode, pid = Pid}) ->
    case is_pid(Pid) of
        true  ->
            case util:valid_mfa(MFA) of
                true  -> clusters_api:async_apply_call(CenterNode, From, MFA);
                false -> gen_server:reply(From, none)
            end;
        false -> gen_server:reply(From, none)
    end,
    {noreply, State};

handle_call(get_node, _From, State) ->
    Node = node(),
    {reply, Node, State};

handle_call(_Request, _From, State) ->
    ?ERR("[~w] handle_call recv unexpected request:~p", [?MODULE, _Request]),
    {noreply, State}.

handle_info({timeout, Timer, conn}, State = #clusters_node{
        conn_timer = Timer, conn_times = ConnTimes,
        pid = undefined         % 表明当前未连接跨服
    }) ->
    ConnTimer = try_conn(State),
    {noreply, State#clusters_node{conn_timer = ConnTimer, conn_times = ConnTimes + 1, wait_conn = true}};

handle_info({'DOWN', Monitor, _Type, _Obj, _Reason}, State = #clusters_node{
        monitor = Monitor, center_node = CenterNode
    }) ->
    ?INFO("The connection to [~w] was interrupted!", [CenterNode]),
    case is_reference(Monitor) of
        true  -> erlang:demonitor(Monitor);
        false -> ignore
    end,
    clusters_api:node_disconnected(CenterNode),
    ConnTimer = try_conn(State),
    NewState = State#clusters_node{conn_timer = ConnTimer, conn_times = 1, wait_conn = true,
        pid = undefined, monitor = undefined},
    {noreply, NewState};

handle_info({'EXIT', ConnWorker, _Why}, State = #clusters_node{conn_worker = ConnWorker}) ->
    erlang:send_after(5000, self(), {restart, conn_worker}),
    {noreply, State};

handle_info({restart, conn_worker}, State = #clusters_node{conn_worker = ConnWorker}) ->
    case is_process_alive(ConnWorker) of
        true  ->
            {noreply, State};
        false ->
            MyPid = self(),
            Pid = proc_lib:spawn_link(fun() -> conn_node_server(MyPid) end),
            ConnTimer = try_conn(State#clusters_node{wait_conn = false}),
            {noreply, State#clusters_node{conn_worker = Pid, wait_conn = true, conn_timer = ConnTimer}}
    end;

handle_info({check_conn_done, ConnWorker}, State = #clusters_node{conn_worker = ConnWorker}) ->
    {noreply, State#clusters_node{wait_conn = false}};

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

terminate(_Reason, _State = #clusters_node{center_node = CenterNode, conn_worker = Pid}) ->
    case CenterNode of
        none -> ok; %% 一直没有连接成功
        _ -> clusters_api:node_disconnected(CenterNode)
    end,
    case is_pid(Pid) andalso is_process_alive(Pid) of
        true  -> exit(Pid, kill);
        false -> ignore
    end,
    ?INFO("cluster node terminate!, reason:~w", [_Reason]),
    ok.

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

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

call_server(Msg) ->
    try
        case get_process_pid() of
            undefined -> none;
            Pid -> gen_server:call(Pid, Msg, 3000)
        end
    catch
        exit:{noproc, _} -> none;
        ?EXCEPTION(_ExType, _ExPattern, _Stacktrace) ->
            none
    end.

cast_server(Msg) ->
    try
        case get_process_pid() of
            undefined -> none;
            Pid -> gen_server:cast(Pid, Msg)
        end
    catch
        exit:{noproc, _} -> none;
        ?EXCEPTION(_ExType, _ExPattern, _Stacktrace) -> none
    end.

try_conn(#clusters_node{center_node = CenterNode, server_id = ServerId,
        conn_worker = ConnWorker, wait_conn = WaitConn}) ->
    ConnTimer = erlang:start_timer(?RECONNECT_TIME, self(), conn),
    case WaitConn of
        true  -> ignore;
        false -> ConnWorker ! {conn, self(), CenterNode, ServerId}
    end,
    ConnTimer.

conn_node_server(ParentPid) ->
    Ret =
    receive
        {conn, ParentPid, CenterNode, ServerId} ->
            case net_kernel:connect_node(CenterNode) of
                true  ->
                    do_conn(CenterNode, ServerId, ParentPid);
                _ ->    % false | ignored
                    ignore
            end,
            ParentPid ! {check_conn_done, self()},
            ok;
        over ->
            over;
        _ ->
            ignore
    end,
    case Ret =:= over of
        true  -> ok;
        false -> conn_node_server(ParentPid)
    end.

do_conn(CenterNode, ServerId, MyPid) ->
    GameNode = #game_node{
        node = node()
        ,server_id = ServerId
        ,platform = config:get_platform()
        ,server_num = config:get_server_num()
        ,server_name = config:get_server_name()
        ,pid = MyPid
        ,open_time = util:get_real_open_time()
        ,merge_server_ids = config:get_merge_server_ids()
    },
    rpc:cast(CenterNode, clusters_center, add_node, [GameNode]).

