%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 移动行为检验
%%% @end
%%% -------------------------------------------------------
-module(motion_check).
-export([
        check_motion/6
        ,to_client_motion_type/1
        ,check_prohibed_jump/2
    ]).

-include("common.hrl").
-include("server.hrl").
-include("scene.hrl").
-include("motion.hrl").

%% 跳跃状态
-define(JUMP_STAGE_OPENED, 0).
-define(JUMP_STAGE_CLOSED, 1).

%% 移动数据信息 进程字典中值的结构
-define(MOTION_INFO_VAL(JumpStage, StartX, StartY, StartTime, StageState),
    <<JumpStage:8, StartX:32, StartY:32, StartTime:48, StageState:8>>).

%% 检查禁跳状态，修正移动类型为走路
check_prohibed_jump(BattleAttr, ReqtMotionType) ->
    case ReqtMotionType =:= ?MOVE_ROLE_JUMP orelse
        ReqtMotionType =:= ?MOVE_ROLE_FLASH orelse
        ReqtMotionType =:= ?MOVE_ROLE_JUMP_COST_ENG of
        true  ->    % 跳跃/闪现操作，需要检查禁跳状态
            case lib_skill_buff:is_prohibed_jump(BattleAttr) of
                true  -> ?MOVE_ROLE_WALK;
                false -> ReqtMotionType
            end;
        false -> ReqtMotionType
    end.

%% check_motion ->
%%  {MoveErrCode, X, Y, FixMotionType, NewRoleMotion}
%% 当MoveErrCode不是?MOTION_ERR_OK时，外部不通知场景进程处理移动，只通知错误给玩家
check_motion(Player, ClientSceneId, ReqtMotionType, TargetX, TargetY, Param1) ->
    #player_status{
        scene = SceneId,
        x = OldX, y = OldY,
        role_motion = RoleMotion,
        battle_attr = BattleAttr,
        fly_state = _FlyState
    } = Player,
    MotionType = check_prohibed_jump(BattleAttr, ReqtMotionType),
    Result =
    if
        %% 前端走路场景异常，不允许移动
        ClientSceneId =/= SceneId ->
            {?MOTION_ERR_SCENE, OldX, OldY, ?MOVE_ROLE_WALK};

        %% 场景内瞬移（CD限制 + TODO 战斗状态限制）
        MotionType =:= ?MOVE_ROLE_BLINK ->
            fix_blink(MotionType, TargetX, TargetY, Player);

        %% 角色闪现
        MotionType =:= ?MOVE_ROLE_FLASH ->
            fix_flash(MotionType, TargetX, TargetY, Player);

        %% 主动跳跃第三跳可以非常远，前端打印最远大约为2100像素
        MotionType =:= ?MOVE_ROLE_JUMP ->
            fix_jump(MotionType, TargetX, TargetY, Player, Param1);

        %% 任务、起跳点被动式跳跃，策划前端配置1000像素，放宽限制到1500
        %% 任务跳需要小飞鞋（由于前端任务流程配合较难，飞鞋价值不高，所以去掉服务端校验）
        MotionType =:= ?MOVE_ROLE_TASK_JUMP ->
            % case FlyState =:= 1 of
            %     true  ->
            case abs(OldX-TargetX) > 15 * ?LOGIC_PT_WIDTH orelse
                abs(OldY-TargetY) > 15 * ?LOGIC_PT_HEIGHT of
                true  -> {?MOTION_ERR_DIS, OldX, OldY, ?MOVE_ROLE_WALK};
                false -> {?MOTION_ERR_OK,  TargetX, TargetY, MotionType}
            end;
            % false ->
            %     {false, OldX, OldY, ?MOVE_ROLE_WALK}
            % end;

        %% 其余走路情况不能一次性超过场景九网格的一个格子大小
        %% 是否考虑按速度计算移动距离来进行限制？暂时是综合各类行为以宽䡆值简单限制
        abs(OldX-TargetX) > ?GRID_WIDTH orelse abs(OldY-TargetY) > ?GRID_HEIGHT ->
            {?MOTION_ERR_DIS, OldX, OldY, ?MOVE_ROLE_WALK};

        %% 其余合法
        true ->
            {?MOTION_ERR_OK, TargetX, TargetY, MotionType}
    end,
    case Result of
        {_, _RetX, _RetY, _, _RetRoleMotion} ->
            Result;
        {MoveErrCode, X, Y, FixMotionType} ->
            {MoveErrCode, X, Y, FixMotionType, RoleMotion}
    end.

%% 移动时检验移动行为是否有效，修正移动位置
fix_blink(MotionType = ?MOVE_ROLE_BLINK, TargetX, TargetY, Player) ->
    #player_status{x = OldX, y = OldY} = Player,
    NowTime = utime:unixtime(ms),
    %% N.B.
    %% 1. 正常瞬移评估时间在1秒以上，为防止战斗挂，一定要限制CD时间，
    %%    但是鉴于客户端将瞬移同时应用于剧情、任务飞天落地，可能出现短时间打断触发第二次操作，
    %%    所以为兼顾战斗及剧情、任务，要检测连续的多次时间限制，
    %%    尽可能在满足任务前提下，最大程度限制战斗挂
    %% 3. 【暂未处理】未来考虑是否需要增加战斗状态限制不能触发（借助于出手与受击时间判断）
    IsValid =
    case get(?MOTION_TIME(MotionType)) of
        %% 避免前端剧情、任务等触发打断上一个操作【前端可能在500毫秒内】
        %% 放宽限制，距离上一次操作至少要200ms。后续多次的判断则增加间隔以逼近正常限制
        [Last, Last2, Last3] ->
            NewMotionTimes = [NowTime, Last, Last2],
            NowTime - Last >= 200 andalso NowTime - Last3 >= 2000;
        [Last, Last2] ->
            NewMotionTimes = [NowTime, Last, Last2],
            NowTime - Last >= 200 andalso NowTime - Last2 >= 1000;
        [Last] ->
            NewMotionTimes = [NowTime, Last],
            NowTime - Last >= 200;
        _Undefined ->
            NewMotionTimes = [NowTime],
            true
    end,
    case IsValid of
        true  ->
            put(?MOTION_TIME(MotionType), NewMotionTimes),
            {?MOTION_ERR_OK, TargetX, TargetY, MotionType};
        false ->
            {?MOTION_ERR_CD, OldX, OldY, ?MOVE_ROLE_WALK}
    end.

fix_flash(MotionType = ?MOVE_ROLE_FLASH, TargetX, TargetY, Player) ->
    #player_status{x = OldX, y = OldY, role_motion = RoleMotion} = Player,
    case check_distance_common(OldX, OldY, TargetX, TargetY) of
        true  ->
            {NewRoleMotion, FixMotionType} =
            motion_energy:check_cost_energy(RoleMotion, MotionType),
            {?MOTION_ERR_OK, TargetX, TargetY, FixMotionType, NewRoleMotion};
        false ->
            {?MOTION_ERR_DIS, OldX, OldY, ?MOVE_ROLE_WALK}
    end.

%% 注意：
%%   ?MOVE_ROLE_JUMP_COST_ENG 仅为服务端内部值，由MOVE_ROLE_JUMP转换而来，
%%   目的是可以让场景进程不需要再次计算即知道是否需要消耗能量。这是以精确度
%%   为代价节省场景进程计算逻辑（角色进程计算其实不一定与场景数据一致）
%%   此跳跃类型仅在角色进程与场景进程通信中使用，
%%   存取数据还是使用?MOVE_ROLE_JUMP，发送回前端也仍使用MOVE_ROLE_JUMP。
fix_jump(MotionType = ?MOVE_ROLE_JUMP, TargetX, TargetY, Player, Param1) ->
    #player_status{x = OldX, y = OldY, role_motion = RoleMotion} = Player,
    % _RoleName = element(3, Player#player_status.figure),
    % ?INFO(
    %     "------------------------------------------------------~n"
    %     "[~ts]接收到跳跃请求，Param1:~w, TargetX:~w, TargetY:~w",
    %     [_RoleName, Param1, TargetX, TargetY]
    % ),
    {JumpStage, StageState} = calc_jump_stage(Param1),
    NowTime = utime:unixtime(ms),
    {IsValidStage, IsNewStage, StartX, StartY, StartTime} =
    check_jump_stage(MotionType, JumpStage, OldX, OldY, NowTime),
    IsValidDistance =
    case IsValidStage of
        true  ->
            check_distance_jump(JumpStage, StartX, StartY, TargetX, TargetY);
        false ->
            %% 需要修正为走路，按走路距离检测
            check_distance_common(OldX, OldY, TargetX, TargetY)
    end,
    % ?INFO("[~ts] stage:~w, IsValidStage:~w, IsValidDistance:~w, IsNewStage:~w",
    %     [_RoleName, JumpStage, IsValidStage, IsValidDistance, IsNewStage]
    % ),
    if
        not IsValidDistance ->
            %% 无效的移动（超越范围）
            % ?INFO("[~ts]无效的移动，超越范围", [_RoleName]),
            {?MOTION_ERR_DIS, OldX, OldY, ?MOVE_ROLE_WALK};
        IsValidStage andalso IsNewStage  ->
            %% 新跳跃阶段，需要扣取能量
            {NewRoleMotion, FixMotionType} =
            motion_energy:check_cost_energy(RoleMotion, ?MOVE_ROLE_JUMP_COST_ENG),
            % ?INFO("[~ts]新跳跃阶段，需要扣取能量，FixMotionType:~w, NewEnergy:~w",
            %     [_RoleName, FixMotionType, NewRoleMotion#role_motion.energy]
            % ),
            case FixMotionType of
                ?MOVE_ROLE_JUMP_COST_ENG -> % 成功起跳，记录扣除能量及状态
                    put(?MOTION_INFO(MotionType),
                        ?MOTION_INFO_VAL(JumpStage, OldX, OldY, NowTime, StageState)
                    );
                _ -> ignore
            end,
            {?MOTION_ERR_OK, TargetX, TargetY, FixMotionType, NewRoleMotion};
        IsValidStage ->
            % ?INFO("[~ts]接续前一个跳跃阶段，不扣取能量", [_RoleName]),
            %% 接续前一个跳跃阶段，不扣取能量
            case StageState =:= ?JUMP_STAGE_CLOSED of
                true  ->
                    %% 状态结束需要更新数据
                    put(?MOTION_INFO(MotionType),
                        ?MOTION_INFO_VAL(JumpStage, StartX, StartY, StartTime, StageState)
                    );
                false ->
                    ignore
            end,
            {?MOTION_ERR_OK, TargetX, TargetY, MotionType};
        true ->
            % ?INFO("[~ts]有效的移动，但是无效的跳跃阶段，需要修正为走路", [_RoleName]),
            %% 有效的移动，但是无效的跳跃阶段，需要修正为走路
            {?MOTION_ERR_OK, TargetX, TargetY, ?MOVE_ROLE_WALK}
    end.

%% 计算三段跳的跳跃阶段 -> {Stage, StageState}
%% 客户端跳跃阶段发送的Param1，
%% 三段跳跃的开始标记分别为100、200、300，结束标记分别为199、299、399,
%% 为防止外挂发包，服务端自行模拟维护跳跃阶段，不关心阶段开始，只关心阶段结束
calc_jump_stage(Param1) ->
    {Stage, IsStageClose} =
    if
        Param1 >= 400 -> {1, false};
        Param1 >= 300 -> {3, Param1 =:= 399};
        Param1 >= 200 -> {2, Param1 =:= 299};
        Param1 >= 100 -> {1, Param1 =:= 199};
        true          -> {1, false}
    end,
    StageState =
    case IsStageClose of
        true  -> ?JUMP_STAGE_CLOSED;
        false -> ?JUMP_STAGE_OPENED
    end,
    {Stage, StageState}.

%% 检测跳跃阶段变化、是否有效的跳跃移动（阶段衔接）
%% 返回的StartX与StartY，需要做修正
check_jump_stage(MotionType, JumpStage, OldX, OldY, NowTime) ->
    case get(?MOTION_INFO(MotionType)) of
        ?MOTION_INFO_VAL(JumpStage, StartX, StartY, StartTime, StageState) ->
            IsNewStage = case StageState =:= ?JUMP_STAGE_CLOSED of
                true  -> true;
                false ->
                    %% 与上一次是相同的跳跃阶段，检查有无超过时间限制
                    JumpStageTimeMax = 3000,
                    NowTime - StartTime >= JumpStageTimeMax
            end,
            IsValidStage =
            case IsNewStage of
                true  ->    %% 超过时间，是新的一次跳跃了，必须从第一跳开始
                    JumpStage == 1;
                false ->
                    true
            end,
            % ?INFO("旧数据中为相同阶段，经时间等判定是否新阶段:[~w], 是否有效的阶段:[~w], StartXY:~w",
            %     [IsNewStage, IsValidStage, {StartX,StartY}]
            % ),
            [FixStartX, FixStartY] = fix_start_pos(StartX, StartY, OldX, OldY, IsValidStage, IsNewStage),
            {IsValidStage, IsNewStage, FixStartX, FixStartY, StartTime};
        ?MOTION_INFO_VAL(OldStage, StartX, StartY, StartTime, _StageState) ->
            %% 不同的跳跃阶段
            IsNewStage = true,
            NextJumpTimeout = 3000,
            IsValidStage = (
                JumpStage == 1 orelse
                (OldStage+1 == JumpStage andalso NowTime - StartTime < NextJumpTimeout)
            ),
            % ?INFO("旧数据中为不同阶段，最终确定是否新阶段:[~w], 是否有效的阶段:[~w], StartXY:~w",
            %     [IsNewStage, IsValidStage, {StartX,StartY}]
            % ),
            [FixStartX, FixStartY] = fix_start_pos(StartX, StartY, OldX, OldY, IsValidStage, IsNewStage),
            {IsValidStage, IsNewStage, FixStartX, FixStartY, StartTime};
        _ ->
            %% 无数据，必须是第一次跳跃
            IsValidStage = JumpStage == 1,
            IsNewStage = true,
            % ?INFO("无旧数据，是否新阶段:[~w], 是否有效的阶段:[~w], OldXY:~w",
            %     [IsNewStage, IsValidStage, {OldX,OldY}]
            % ),
            {IsValidStage, IsNewStage, OldX, OldY, 0}
    end.

fix_start_pos(_OldStartX, _OldStartY, NowX, NowY, _IsValidStage, _IsNewStage = true) ->
    [NowX, NowY];
fix_start_pos(_OldStartX, _OldStartY, NowX, NowY, _IsValidStage = false, _IsNewStage) ->
    [NowX, NowY];
fix_start_pos(OldStartX, OldStartY, _NowX, _NowY, _IsValidStage, _IsNewStage) ->
    [OldStartX, OldStartY].

%% 检查移动距离合法性
-compile({inline, [check_distance_common/4]}).
check_distance_common(OldX, OldY, NewX, NewY) ->
    abs(NewX - OldX) =< ?GRID_WIDTH andalso
    abs(NewY - OldY) =< ?GRID_HEIGHT.

%% 检查跳跃距离合法性
%% -------------------------------------------------------
%% 主动跳跃第三跳可以非常远，
%% 前端打印从较高处跳跃，最远大约为2100像素，放宽限制到2500
%% 结合项目实际，即为25个逻辑坐标格
%% 第一跳和第二跳最远大约为1100像素，放宽限制到1500
check_distance_jump(_JumpStage = 3, OldX, OldY, NewX, NewY) ->
    abs(OldX-NewX) =< 25 * ?LOGIC_PT_WIDTH andalso
    abs(OldY-NewY) =< 25 * ?LOGIC_PT_HEIGHT;
check_distance_jump(_JumpStage, OldX, OldY, NewX, NewY) ->
    abs(OldX-NewX) =< 15 * ?LOGIC_PT_WIDTH andalso
    abs(OldY-NewY) =< 15 * ?LOGIC_PT_HEIGHT.

%% 转换回客户端使用的MotionType
to_client_motion_type(?MOVE_ROLE_JUMP_COST_ENG) -> ?MOVE_ROLE_JUMP;
to_client_motion_type(MotionType) -> MotionType.
