%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 模拟客户端角色连接管理进程
%%% @end
%%% -------------------------------------------------------
-module(cli_simu_conn).

-behaviour(gen_server).

%% API
-export([
        start_link/2
        , conn/2
    ]).

%% gen_server callbacks
-export([init/1,
        handle_call/3,
        handle_cast/2,
        handle_info/2,
        terminate/2,
        code_change/3
    ]).

-include("cli_simu.hrl").

-define(SERVER, ?MODULE).

%% TCP连接相关设置
-define(TCP_OPTS, [
        binary
        , {packet, 0}
        , {active, false}
        , {keepalive, true}
        , {nodelay, false}
        , {delay_send, true}
        , {exit_on_close, false}
    ]).

-define(SVR_PKG_HEADER_LEN, 4).
-define(RECV_TIMEOUT, 10000).

%% 心跳间隔
-define(HEART_INTERVAL, 8000).

%% 角色数据
-record(conn_data, {
        id = undefined
        ,role_pid = undefined
        ,socket = undefined
    }).

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

conn(Host, Port) ->
    gen_tcp:connect(Host, Port, ?TCP_OPTS, 5000).

start_link(RolePid, Socket) ->
    gen_server:start_link(?MODULE, [RolePid, Socket], []).

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

init([RolePid, Socket]) ->
    {ok, #conn_data{id = undefined, role_pid = RolePid, socket = Socket}}.

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

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

handle_info(get_package, ConnData = #conn_data{role_pid = RolePid}) ->
    %% 角色进程在处理完recv_packet消息时，一定要发回消息给这里，否则就停住了
    Ret =
    case get_package(ConnData) of
        {recv_packet, _Cmd, _Data} = Package ->
            %% 将会由主进程处理完后通知get_package
            RolePid ! Package,
            ok;
        ignore ->
            ignore;
        {error, closed} ->
            throw({error, socket_closed});
        {error, _Error} ->
            ignore
    end,
    %% 本进程除了正确解包情况由角色进程通知下一次读取数据之外，
    %% 应当进行循环读取数据包处理
    case Ret of
        ok -> ignore;
        _  -> self() ! get_package
    end,
    {noreply, ConnData};

%% 这个消息在碰到进程正在进行gen_tcp:recv/2时，会阻塞住，
%% 不过本身不重要，只是为了日志显示，所以这个无所谓，没必要修改为专有的消息接收
handle_info({set_id, RoleId, FromPid}, ConnData = #conn_data{role_pid = RolePid}) ->
    case FromPid =:= RolePid of
        true  ->
            NewConnData = ConnData#conn_data{id = RoleId},
            {noreply, NewConnData};
        false ->
            {noreply, ConnData}
    end;

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

terminate(_Reason, _State) ->
    ok.

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

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

get_package(#conn_data{id = _RoleId, socket = Socket}) ->
    case gen_tcp:recv(Socket, ?SVR_PKG_HEADER_LEN, infinity) of
        {ok, <<Len:(?SVR_PKG_HEADER_LEN * 8)>>} ->
            case Len > ?SVR_PKG_HEADER_LEN of
                true  ->
                    case gen_tcp:recv(Socket, Len - ?SVR_PKG_HEADER_LEN, ?RECV_TIMEOUT) of
                        {ok, <<Cmd:16, IsZip:8, Bin/binary>>} ->
                            try
                                case cli_simu_proto:unpack(Cmd, IsZip, Bin) of
                                    {ok, Data} ->
                                        % ?INFO_TTY("[~w] recv cmd :~w, unpack ok", [_RoleId, Cmd]),
                                        {recv_packet, Cmd, Data};
                                    ignore ->
                                        %% 一些过于频繁的协议，不打印信息
                                        % case lists:member(Cmd,  [10006, 12001]) of
                                        %     true  -> ignore;
                                        %     false -> ?INFO_TTY("[~w] recv cmd [~w], ignore", [_RoleId, Cmd])
                                        % end,
                                        ignore;
                                    {error, _Error} ->
                                        % ?INFO_TTY("[~w] recv cmd [~w], unpack error", [_RoleId, Cmd]),
                                        ?ERR("recv error package, Cmd:~w,~w, error:~w", [Cmd, Bin, _Error]),
                                        {error, _Error}
                                end
                            catch
                                ?EXCEPTION(_ErrClass, _ErrReason, _Stacktrace) ->
                                    ?ERR("unpack error: ~w", [{Cmd, _ErrClass, _ErrReason, ?GET_STACK(_Stacktrace)}]),
                                    throw({error, {Cmd, _ErrClass, _ErrReason}})
                            end;
                        {ok, Bin} ->
                            ?ERR("recv error package:~w", [Bin]),
                            {error, unknown_package};
                        {error, _Error} ->
                            ?ERR("error:~w", [_Error]),
                            {error, _Error}
                    end;
                false ->
                    ?ERR("error: header_too_short"),
                    {error, header_too_short}
            end;
        {error, _Error} ->
            ?ERR("error:~w", [_Error]),
            {error, _Error}
    end.

