%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 涉及业务逻辑的专用线上调试工具
%%%
%%% 原则上本工具中的接口仅用于调试目的
%%% 不允许在游戏业务逻辑中调用
%%% 2018-10-26
%%% @end
%%% -------------------------------------------------------
-module(game_tool).
-author("huangyongxing@yeah.net").
%% API: 场景、怪物相关部分
-export([
        get_user_num/1                  % 获取场景各分线房间角色数量
        ,get_user_num/2                 % 获取指定场景分线的各房间角色数量
        ,get_user_num/3                 % 获取场景房间玩家人数（数据不一致时查询真实场景玩家数）
        ,get_user_num/5                 % 获取场景区域玩家人数

        ,get_mon_num/3                  % 获取场景怪物数量
        ,get_mon_num/5                  % 获取场景区域怪物数量

        ,diff_mon_info/3

        ,count_scene_mem/0              % 场景数量、内存统计排序
        ,count_mon_mem/0                % 怪物数量、内存统计排序

        ,valid_position/1               % 场景玩家信息位置检查

        ,reload_mons/3
        ,find_mon_pids/0
    ]).
%% API: 玩家数据部分
-export([
        player_info/3
        ,get_scene_user_info/2

        ,count_role_mem/0                % 角色进程内存统计排序
        ,count_conn_mem/0                % 角色连接进程内存统计排序

        ,role_proc_dict/2               % 获取角色进程进程字典
        ,role_diff_value/2              % 比较两个角色的字段是否相同
    ]).
%% API: for get all scene mem info from cross center
-export([
        cross_get_scene_mem_info/1
        ,node_get_scene_mem_info/3
    ]).
%% 特殊操作接口
-export([
        restart_scene/2                 % 重启指定场景分线（用于特殊情形，需确保无玩家在该场景中）
        ,restart_scene_on_node/2
    ]).

%% Internal export
-export([
        player_info_internal/4
        ,role_get_scene_user/3
    ]).
%% Internal: only for internal debug call (call by scene process)
-export([
        scene_get_mon_num/1
        ,scene_get_mon_num/3
        ,scene_get_user_num/1
        ,scene_get_user_num/3
    ]).

-include("common.hrl").
-include("scene.hrl").
-include("server.hrl").
-include("battle.hrl").
-include("goods.hrl").

%% -------------------------------------------------------
%% 跨服中心获取各服内存开销前N名场景信息，组装再截取
%% -------------------------------------------------------
cross_get_scene_mem_info(MinBytes) ->
    GetHeadLen = 2,
    Timeout = 5000,
    From = self(),
    CenterNode = node(),
    #{mem_list := CenterMemList} = game_tool:count_scene_mem(),
    CenterHeadList = lists:sublist([M#{node => CenterNode} || M = #{mem := Mem} <- CenterMemList, Mem >= MinBytes], GetHeadLen),
    clusters_center:apply_to_all_node(?MODULE, node_get_scene_mem_info, [From, GetHeadLen, MinBytes]),
    StartTime = utime:unixtime(ms),
    ServerList = clusters_center:get_nodes_open_time(),
    Len = length(ServerList),
    {IsTimeout, RestLen, NodeMemList} = loop_recv_result(StartTime + Timeout, Len, []),
    SortList = lists:sort(fun(#{mem := MemA}, #{mem := MemB}) ->
                MemA >= MemB;
            (_, _) -> true
        end, NodeMemList),
    SubList = lists:sublist(SortList, 20),
    #{is_timeout => IsTimeout, len_all => Len, recv_len => Len - RestLen,
        center => CenterHeadList, node => SubList
    }.

node_get_scene_mem_info(From, GetHeadLen, MinBytes) ->
    #{mem_list := MemList} = game_tool:count_scene_mem(),
    HeadList = lists:sublist([M || M = #{mem := Mem} <- MemList, Mem >= MinBytes], GetHeadLen),
    clusters_node:apply_cast(erlang, send, [From, {mem_list, node(), HeadList}]).

loop_recv_result(_, 0, Acc) ->
    {false, 0, Acc};
loop_recv_result(EndTime, RestLen, Acc) ->
    NowTime = utime:unixtime(ms),
    Timeout = max(EndTime - NowTime, 0),
    {IsTimeout, NewRestLen, ResultList} =
    receive
        {mem_list, Node, HeadList} ->
            NodeHeadList = [M#{node => Node} || M <- HeadList],
            {false, RestLen - 1, NodeHeadList ++ Acc};
        _ ->
            {false, RestLen, Acc}
    after Timeout ->
            {true, RestLen, Acc}
    end,
    case IsTimeout orelse NewRestLen =:= 0 of
        true  ->
            {IsTimeout, NewRestLen, ResultList};
        false ->
            loop_recv_result(EndTime, NewRestLen, ResultList)
    end.

%% -------------------------------------------------------
%% 场景人数
%% -------------------------------------------------------

%% 获取场景玩家数信息（包含各分线、房间人数信息）
get_user_num(SceneId) ->
    PoolIds = scene_mgr:get_scene_pool_ids(SceneId),
    lists:foldl(fun(PoolId, Acc) ->
                case ets:lookup(?SCENE_ROLE_COUNT, {SceneId, PoolId}) of
                    [RoleCountInfo] -> [RoleCountInfo | Acc];
                    _ -> Acc
                end
        end, [], PoolIds).

%% 获取场景玩家数（直接读取ets表）
get_user_num(SceneId, ScenePoolId) ->
    ets:lookup(?SCENE_ROLE_COUNT, {SceneId, ScenePoolId}).

%% 获取场景的玩家人数（直接基于存储在场景中的用户数据量）
%% （数据不一致时查询真实场景玩家数）
get_user_num(SceneId, ScenePoolId, CopyId)->
    mod_scene_agent:apply_call(SceneId, ScenePoolId, ?MODULE, scene_get_user_num, [CopyId]).

get_user_num(SceneId, ScenePoolId, CopyId, X, Y) ->
    mod_scene_agent:apply_call(SceneId, ScenePoolId, ?MODULE, scene_get_user_num, [CopyId, X, Y]).

%% PS：此接口不允许调用，专用于调试目的，为获取准确可靠数据提供，以验证记录的数据是否正确
%%     一般外部进程通过lib_scene:get_role_count/2获取（查ets表）
%%     场景内部进程使用进程字典获取记录好的角色数量数据
scene_get_user_num(CopyId) ->
    RoomUserKeys = lib_scene_agent:get_room_user_keys(CopyId),
    AllNum = lists:foldl(fun
            ({user, _}, Acc) -> Acc + 1;
            (_, Acc) -> Acc
        end, 0, get_keys()),
    #{role_count => AllNum, room_role_count => length(RoomUserKeys)}.

%% 此接口不允许调用，专用于调试目的
%% 获取场景区域角色数量
scene_get_user_num(CopyId, X, Y) ->
    AreaIdList = lib_scene_calc:get_the_area(X, Y),
    UpdateFunInit = fun(OldVal) -> OldVal end,
    UpdateFunAdd = fun(OldVal) -> OldVal + 1 end,
    OriginAreaId = lib_scene_calc:get_xy(X, Y),
    AccInit = #{
        online => #{},
        roles => #{},
        total => #{online => 0, roles => 0},
        area_id => OriginAreaId
    },
    lists:foldl(fun(AreaId, AccIn) ->
                IdList = lib_scene_agent:get_area(AreaId, CopyId),
                lists:foldl(fun({user, Id}, SubAccIn = #{online := Onlines, roles := Roles, total := TotalMaps}) ->
                            case lib_scene_agent:get_user(Id) of
                                #ets_scene_user{online = ?ONLINE_ON} ->
                                    #{
                                        online => maps:update_with({area_id, AreaId}, UpdateFunAdd, 1, Onlines),
                                        roles  => maps:update_with({area_id, AreaId}, UpdateFunAdd, 1, Roles),
                                        total  => maps:update_with(roles, UpdateFunAdd,
                                            maps:update_with(online, UpdateFunAdd, TotalMaps)
                                        )
                                    };
                                #ets_scene_user{online = ?ONLINE_OFF_HOSTING} ->
                                    #{
                                        online => maps:update_with({area_id, AreaId}, UpdateFunInit, 0, Onlines),
                                        roles  => maps:update_with({area_id, AreaId}, UpdateFunAdd,  1, Roles),
                                        total  => maps:update_with(roles, UpdateFunAdd, TotalMaps)
                                    };
                                _Other ->
                                    SubAccIn
                            end;
                        (_, SubAccIn) ->
                            SubAccIn
                    end, AccIn, IdList)
        end, AccInit, AreaIdList).

%% -------------------------------------------------------
%% 场景怪数
%% -------------------------------------------------------

%% 获取场景怪物数量
get_mon_num(SceneId, PoolId, CopyId) ->
    mod_scene_agent:apply_call(SceneId, PoolId, ?MODULE, scene_get_mon_num, [CopyId]).

%% 获取场景区域怪物数量
get_mon_num(SceneId, PoolId, CopyId, X, Y) ->
    mod_scene_agent:apply_call(SceneId, PoolId, ?MODULE, scene_get_mon_num, [CopyId, X, Y]).

%% 获取指定房间中怪物数量（此接口不允许调用，专用于调试目的）
scene_get_mon_num(CopyId) ->
    Objects = lib_scene_object_agent:get_scene_object(CopyId),
    Mons = [Object || Object = #scene_object{sign = ?BATTLE_SIGN_MON} <- Objects],
    #{mon => length(Mons), object => length(Objects)}.

%% 获取指定房间区域中怪物数量（此接口不允许调用，专用于调试目的）
scene_get_mon_num(CopyId, X, Y) ->
    AreaIdList = lib_scene_calc:get_the_area(X, Y),
    UpdateFunInit = fun(OldVal) -> OldVal end,
    UpdateFunAdd = fun(OldVal) -> OldVal + 1 end,
    OriginAreaId = lib_scene_calc:get_xy(X, Y),
    AccInit = #{
        mon => #{},
        object => #{},
        total => #{mon => 0, object => 0},
        area_id => OriginAreaId
    },
    lists:foldl(fun(AreaId, AccIn) ->
                IdList = lib_scene_object_agent:get_area(AreaId, CopyId),
                lists:foldl(fun(Id, SubAccIn = #{mon := Mons, object := Objs, total := TotalMaps}) ->
                            case lib_scene_object_agent:get_object(Id) of
                                #scene_object{sign = ?BATTLE_SIGN_MON} ->
                                    #{
                                        mon    => maps:update_with({area_id, AreaId}, UpdateFunAdd, 1, Mons),
                                        object => maps:update_with({area_id, AreaId}, UpdateFunAdd, 1, Objs),
                                        total  => maps:update_with(object, UpdateFunAdd,
                                            maps:update_with(mon, UpdateFunAdd, TotalMaps)
                                        )
                                    };
                                #scene_object{} ->
                                    #{
                                        mon    => maps:update_with({area_id, AreaId}, UpdateFunInit, 0, Mons),
                                        object => maps:update_with({area_id, AreaId}, UpdateFunAdd,  1, Objs),
                                        total  => maps:update_with(object, UpdateFunAdd, TotalMaps)
                                    };
                                _ ->
                                    SubAccIn
                            end
                    end, AccIn, IdList)
        end, AccInit, AreaIdList).

diff_mon_info(SceneId, PoolId, CopyId) ->
    SceneInfo = get_mon_num(SceneId, PoolId, CopyId),
    MgrInfo = mon_mgr:gm_get_mon_info(SceneId, PoolId, CopyId, num),
    #{
        scene_info => SceneInfo,
        mgr_info => MgrInfo
    }.
%% -------------------------------------------------------
%% 场景相关内存状态
%% -------------------------------------------------------

%% 场景按占用的内存排序
count_scene_mem() ->
    AllScenePools = ets:select(?SCENE_ROLE_COUNT, [{#scene_role_count{key = '$1', _ = '_'}, [], ['$1']}]),
    {MemList, Count, MemTotal} =
    lists:foldl(fun(Key = {SceneId, PoolId}, Acc = {MemAcc, CountAcc, MemTotalAcc}) ->
                erlang:erase({get_scene_pid, SceneId, PoolId}),
                Pid = mod_scene_agent:get_scene_pid(SceneId, PoolId),
                case is_pid(Pid) andalso is_process_alive(Pid) of
                    true  ->
                        case erlang:process_info(Pid, memory) of
                            {memory, MemSize} ->
                                MemInfo = #{
                                    key => Key
                                    ,pid => Pid
                                    ,mem => MemSize
                                },
                                {[MemInfo | MemAcc], CountAcc + 1, MemTotalAcc + MemSize};
                            _ ->
                                Acc
                        end;
                    false ->
                        Acc
                end
        end, {[], 0, 0}, AllScenePools),
    NewMemList = lists:sort(fun sort_scene_by_mem/2, MemList),
    Avg = case Count > 0 of
        true  -> list_to_float(io_lib:format("~.5f", [MemTotal / Count]));
        false -> 0.0
    end,
    case NewMemList of
        [#{mem := Max} | _] ->
            #{mem := Min} = lists:last(NewMemList);
        _ ->
            Min = 0,
            Max = 0
    end,
    #{
        mem_list => NewMemList
        ,mem_total => MemTotal
        ,count_total => Count
        ,avg => Avg
        ,min => Min
        ,max => Max
    }.

sort_scene_by_mem(#{mem := MemA}, #{mem := MemB}) ->
    MemA >= MemB;
sort_scene_by_mem(_, _) ->
    false.

%% 统计怪物内存
count_mon_mem() ->
    LenMax = 50,
    dev_tool:count_proc_mem({mon_mgr, init, 1}, LenMax).

%% 角色进程内存统计排序
count_role_mem() ->
    dev_tool:count_proc_mem({mod_server, init, 1}, 100).

%% 角色连接进程内存统计排序
count_conn_mem() ->
    dev_tool:count_proc_mem({game_conn, init, 1}, 100).

%% ---------------------------------------------------------------------------

%% 检查角色的位置信息【角色状态及场景数据】
valid_position(Id) ->
    ReqFields = [
        #player_status.pid,
        #player_status.scene,
        #player_status.scene_pool_id,
        #player_status.copy_id,
        #player_status.x,
        #player_status.y,
        #player_status.fin_change_scene,
        #player_status.online
    ],
    case lib_player:get_player_info(Id, ReqFields) of
        [Pid, SceneId, PoolId, CopyId, X, Y, FinChangeScene, Online] ->
            case get_scene_pos(SceneId, PoolId, Id) of
                [SceneId2, PoolId2, CopyId2, X2, Y2, Online2] ->
                    IsSceneMatch = {SceneId, PoolId, CopyId} =:= {SceneId2, PoolId2, CopyId2},
                    IsPosMatch   = IsSceneMatch andalso {X, Y} =:= {X2, Y2},
                    ?INFO_TTY("~nid:~w, pid:~w fin_change_scene:~w"
                        "~n role process [scene:~w,~w CopyId:~w xy:~w,~w online:~w]"
                        "~nscene process [scene:~w,~w CopyId:~w xy:~w,~w online:~w]"
                        "~nscene match:~w, pos match:~w",
                        [
                            Id, Pid, FinChangeScene,
                            SceneId, PoolId, CopyId, X, Y, Online,
                            SceneId2, PoolId2, CopyId2, X2, Y2, Online2,
                            IsSceneMatch, IsPosMatch
                        ]
                    );
                _Error ->
                    ?INFO_TTY("id:~w, pid:~w fin_change_scene:~w"
                        "~n role process [scene:~w,~w CopyId:~w xy:~w,~w]"
                        "~nscene process error:~w",
                        [
                            Id, Pid, FinChangeScene,
                            SceneId, PoolId, CopyId, X, Y,
                            _Error
                        ]
                    )
            end;
        _Error ->
            ?INFO_TTY("[~w]get online info failed", [Id]),
            _Error
    end.

get_scene_pos(SceneId, PoolId, Id) ->
    Func = fun() ->
            case lib_scene_agent:get_user(Id) of
                #ets_scene_user{
                    scene = SceneId, scene_pool_id = PoolId,
                    copy_id = CopyId, x = X, y = Y, online = Online
                } ->
                    [SceneId, PoolId, CopyId, X, Y, Online];
                _Error ->
                    _Error
            end
    end,
    gm_util:gm_scene_call(SceneId, PoolId, Func).

%% 重新加载场景怪物及其他场景SceneObject
%% PS: 会先清理怪物，再执行重新加载
reload_mons(SceneId, ScenePoolId, CopyId)->
    mod_scene_agent:gm_reload_scene_object(SceneId, ScenePoolId, CopyId).

%% ---------------------------------------------------------------------------
%% @doc 查找所有怪物pids
-spec find_mon_pids() -> Pids when
    Pids      :: [pid()].
%% ---------------------------------------------------------------------------
find_mon_pids() ->
    AllProcs = erlang:processes(),
    find_mon_pids(AllProcs, []).

find_mon_pids([], Pids) ->
    Pids;
find_mon_pids([Pid | T], Pids) ->
    Pids2 = case dev_tool:proc_init_call(Pid) of
        % {mod_mon_active, init, _} -> [Pid | Pids];
        {mon_mgr, init, _} -> [Pid | Pids];
        _ -> Pids
    end,
    find_mon_pids(T, Pids2).

%% ---------------------------------------------------------------------------
%% @doc 获得内存中玩家数据
%% ---------------------------------------------------------------------------

%% 获取角色进程中的特定数据
%% -> player_info(RoleId, InfoTag, OtherExtraArgs).
%% InfoTag分为两类：
%%   1. position in struct #player_status{}.
%%      e.g: #player_status.battle_attr.
%%   2. special atom
%%      活动 custom_act
%%      道具 goods
%% OtherExtraArgs为额外参数，依据InfoTag而定，
%%   在InfoTag表示#player_status{}中的位置时，此值将忽略，一般传空列表即可
%%   在InfoTag为其他特定的atom时，则为对应的参数，具体参见player_info_internal接口
player_info(RoleId, InfoTag, Arg) ->
    Pid = misc:get_player_process(RoleId),
    case misc:is_process_alive(Pid) of
        true  ->
            gen_server:call(Pid, {fast_call, {?MODULE, player_info_internal, [InfoTag, Arg]}});
        false ->
            offline
    end.

%% 角色进程内部回调的信息接口
player_info_internal(_Player, From, custom_act, [Type, SubType]) ->
    Res = lib_custom_act_data:custom_act_data(Type, SubType),
    gen_server:reply(From, Res),
    ok;
player_info_internal(Player, From, goods, [LocOfGoods]) ->
    GS = lib_goods_do:get_goods_status(),
    GoodsList = lib_goods_util:get_goods_list(Player#player_status.id, LocOfGoods, GS#goods_status.dict),
    gen_server:reply(From, GoodsList),
    ok;
player_info_internal(Player, From, PosInPS, _Arg) ->
    Res = try
              erlang:element(PosInPS, Player)
          catch _:_ -> error
          end,
    gen_server:reply(From, Res),
    ok.

%% 获取场景中角色信息
%% PosInSceneUser :: scene_user | non_neg_integer().
%%   当为scene_user时，表示直接取回完整的#ets_scene_user{}结构
%%   当为数值索引值时，表示取#ets_scene_user{}结构中对应索引编号的值
get_scene_user_info(RoleId, PosInSceneUser) ->
    Pid = misc:get_player_process(RoleId),
    case misc:is_process_alive(Pid) of
        true  ->
            gen_server:call(Pid, {fast_call, {?MODULE, role_get_scene_user, [PosInSceneUser]}});
        false ->
            offline
    end.

%% 角色进程内部回调接口
role_get_scene_user(Player, From, PosInSceneUser) ->
    #player_status{id = RoleId, scene = SceneId, scene_pool_id = PoolId} = Player,
    Func = fun() ->
            case lib_scene_agent:get_user(RoleId) of
                #ets_scene_user{} = User ->
                    try
                        case PosInSceneUser of
                            scene_user -> User;
                            _ -> erlang:element(PosInSceneUser, User)
                        end
                    catch
                        _:_ ->
                            error
                    end;
                _ ->
                    not_found
            end
    end,
    Result = gm_util:gm_scene_call(SceneId, PoolId, Func),
    gen_server:reply(From, Result).

%% ---------------------------------------------------------------------------
%% @doc 获取角色进程进程字典.
-spec role_proc_dict(RoleId | Pid, Key) -> term() when
    RoleId :: non_neg_integer(), Pid :: pid(),
    Key    :: term().
%% ---------------------------------------------------------------------------
role_proc_dict(RoleId, Key) when is_integer(RoleId) ->
    Pid = misc:get_player_process(RoleId),
    role_proc_dict(Pid, Key);
role_proc_dict(Pid, Key) when is_pid(Pid) ->
    {_, DictList} = erlang:process_info(Pid, dictionary),
    lists:keyfind(Key, 1, DictList).

%% ---------------------------------------------------------------------------
%% @doc 获取两个角色状态数据的差异字段数据
-spec role_diff_value(Role1, Role2) -> ok when
    Role1 :: #player_status{},
    Role2 :: #player_status{}.
%% ---------------------------------------------------------------------------
role_diff_value(Role1, Role2) ->
    Fields = record_info(fields, player_status),
    % _Size = record_info(size, player_status),
    diff_value_do(Fields, 2, Role1, Role2).

diff_value_do([], _Index, _Role1, _Role2) ->
    ok;
diff_value_do([Field | Fields], Index, Role1, Role2) ->
    case element(Index, Role1) =:= element(Index, Role2) of
        true ->
            diff_value_do(Fields, Index + 1, Role1, Role2);
        false ->
            io:format(
                "diff field [~p] of #player_status{}:~n"
                " [~p] in Role1: ~p,~n"
                " [~p] in Role2: ~p~n",
                [
                    Field,
                    Field, element(Index, Role1),
                    Field, element(Index, Role2)
                ]),
            diff_value_do(Fields, Index + 1, Role1, Role2)
    end.

%% 重启指定场景分线（用于特殊情形，需确保无玩家在该场景中）
%% 通常用于大世界场景某些特别更新，需要在不停服情况下特殊处理的
restart_scene(SceneId, PoolId) ->
    NodeType = config:get_cls_type(),
    case data_scene:get(SceneId) of
        #ets_scene{cls_type = 0} when NodeType =:= 1 ->
            %% 在跨服中心节点，重启游戏服场景需要通知到各服
            clusters_center:apply_to_all_node(
                ?MODULE, restart_scene_on_node, [SceneId, PoolId]
            );
        #ets_scene{} ->
            %% 在跨服中心重启跨服中的场景、在游戏服重启游戏服或跨服场景，都直接操作
            proc_lib:spawn(fun() ->
                        restart_scene_on_node(SceneId, PoolId)
                end);
        _ ->
            ignore
    end.

restart_scene_on_node(SceneId, PoolId) ->
    mod_scene_agent:close_scene(SceneId, PoolId),
    timer:sleep(1000),
    mod_scene_agent:get_scene_pid(SceneId, PoolId),
    mod_scene_agent:erase_scene_pid(SceneId, PoolId).

