%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 战斗击退计算（通过碰撞检测，计算击退位置）
%%%
%%% 按攻方到守方的直线方向击退守方，
%%% 如果中途遇到障碍区，则守方会停留在障碍区前一个逻辑
%%% 坐标位置的中间位置（一个逻辑坐标为屏幕上一个小方块），
%%% 如果未碰到障碍区，则直接按击退距离计算得到结果。
%%% @end
%%% -------------------------------------------------------
-module(lib_battle_repel).
-export([
        calc_repel/3
        ,calc_repel_pos/6
    ]).

-export([
        test/1
        ,test_sincos/1
        ,calc_sin_cos/4
        ,calc_sin_cos2/4
    ]).
-include("scene.hrl").
-include("battle.hrl").
-include("skill.hrl").

%% 检测斜线障碍区的相关参数及返回结果数据结构
%% 注意，
%% x_last与y_last通常不在直线上，而是
%% 分别记录上一个遍历点的x、y逻辑坐标对应起始像素的坐标
-record(skew_chk_args, {
        scene_id = 0
        ,x_last = 0
        ,y_last = 0
        ,x_src = 0
        ,y_src = 0
        ,x_dst = 0
        ,y_dst = 0
        ,slope_k = 1
        ,recip_k = 0
        ,is_near_horz = true
        ,x_step_incre = 0
        ,y_step_incre = 0
        ,cross_points = []
    }).

%% 击退计算
%% 目前击退只针对普通怪物生效，对人和BOSS、塔之类不生效
calc_repel(_Aer = #battle_status{x = AerX, y = AerY, scene = SceneId},
    Der = #battle_status{x = DerX, y = DerY, sign = ?BATTLE_SIGN_MON, kind = 0, boss = BossType},
    #skill{repel = Distance}) when Distance > 0 ->
    if
        BossType >= 0 andalso BossType =< 2 ->
            {NewDerX, NewDerY} = calc_repel_pos(AerX, AerY, DerX, DerY, SceneId, Distance),
            NewDer = Der#battle_status{x = NewDerX, y = NewDerY},
            {1, NewDer};
        true ->
            {0, Der}
    end;
calc_repel(_Aer, Der, _) ->
    {0, Der}.

%% -----------------------------------------------
%% 击退位置计算
%% 1. 计算击退方向（角度）
%% 2. 计算击退目的位置
%% 3. 判断是否途经障碍区，修正击退目的位置
%% -----------------------------------------------
calc_repel_pos(_, _, DerX, DerY, _SceneId, Distance) when Distance =< 0 ->
    {DerX, DerY};
%% 同一横轴上（包括同一个点情况，此时会水平往右击退）
calc_repel_pos(AerX, Y, DerX, Y, SceneId, Distance) ->
    NewDerX = case AerX > DerX of
        true  -> DerX - Distance;
        false -> DerX + Distance
    end,
    check_block_fix(DerX, Y, NewDerX, Y, SceneId);
%% 同一纵轴上
calc_repel_pos(X, AerY, X, DerY, SceneId, Distance) ->
    NewDerY = case AerY > DerY of
        true  -> DerY - Distance;
        false -> DerY + Distance
    end,
    check_block_fix(X, DerY, X, NewDerY, SceneId);
%% 斜率存在且不为0（通俗说法即在斜线上）
calc_repel_pos(AerX, AerY, DerX, DerY, SceneId, Distance) ->
    {Sin, Cos} = calc_sin_cos(AerX, AerY, DerX, DerY),
    NewDerY = round(DerY + Sin * Distance),
    NewDerX = round(DerX + Cos * Distance),
    check_block_fix(DerX, DerY, NewDerX, NewDerY, SceneId).

%% 同一个点
check_block_fix(X, Y, X, Y, _SceneId) ->
    {X, Y};
%% 同一横轴上
check_block_fix(X, Y, XDst, Y, SceneId) ->
    {LogicX, LogicY} = lib_scene_calc:pixel_to_logic_coordinate(X, Y),
    LogicXDst = lib_scene_calc:x_pixel_to_logic(XDst),
    LogicXDiff = calc_horizonal_diff(LogicX, LogicY, LogicXDst, SceneId),
    %% 防止超出范围，如果逻辑坐标差值超出预定范围则直接为目标坐标
    NewX =
    case LogicXDiff >= (LogicXDst - LogicX) of
        true  -> XDst;
        false -> X + lib_scene_calc:x_logic_to_pixel(LogicXDiff)
    end,
    {NewX, Y};
%% 同一纵轴上
check_block_fix(X, Y, X, YDst, SceneId) ->
    {LogicX, LogicY} = lib_scene_calc:pixel_to_logic_coordinate(X, Y),
    LogicYDst = lib_scene_calc:x_pixel_to_logic(YDst),
    LogicYDiff = calc_vertical_diff(LogicX, LogicY, LogicYDst, SceneId),
    %% 防止超出范围，如果逻辑坐标差值超出预定范围则直接为目标坐标
    NewY =
    case LogicYDiff >= (LogicYDst - LogicY) of
        true  -> YDst;
        false -> Y + lib_scene_calc:y_logic_to_pixel(LogicYDiff)
    end,
    {X, NewY};
%% 斜率存在且不为0（通俗说法即在斜线上）
check_block_fix(X, Y, XDst, YDst, SceneId) ->
    %% 只在一个方向找交叉点有可能会遗漏经过的网格（逻辑坐标），
    %% 所以实际上x轴、y轴两个方向（或反方向）都需要进行检查
    SlopeK = calc_slope_k(X, Y, XDst, YDst),
    RecipSlopeK = 1 / SlopeK,
    {LogicX, LogicY} = lib_scene_calc:pixel_to_logic_coordinate(X, Y),
    %% 将原始点作为路径初始值，同时可以保证结果一定有值
    AccList = [{X, Y}],
    {XFix, YFix} = lib_scene_calc:logic_to_pixel_coordinate(LogicX, LogicY),
    %% 斜线是否更偏向水平方向（偏水平方向优先x步进，偏重垂线方向优先y步进）
    IsNearHorz = abs(SlopeK) =< 1,
    XStepIncre = case XDst < X of
        true  -> -?LOGIC_PT_WIDTH;
        false ->  ?LOGIC_PT_WIDTH
    end,
    YStepIncre = case YDst < Y of
        true  -> -?LOGIC_PT_HEIGHT;
        false ->  ?LOGIC_PT_HEIGHT
    end,
    ChkArgs = #skew_chk_args{
        scene_id = SceneId
        ,x_last = XFix
        ,y_last = YFix
        ,x_src = X
        ,y_src = Y
        ,x_dst = XDst
        ,y_dst = YDst
        ,x_step_incre = XStepIncre
        ,y_step_incre = YStepIncre
        ,slope_k = SlopeK
        ,recip_k = RecipSlopeK
        ,is_near_horz = IsNearHorz
        ,cross_points = AccList
    },
    check_block_skew_line(ChkArgs).

calc_horizonal_diff(LogicX, LogicY, LogicXDst, SceneId) ->
    case LogicXDst >= LogicX of
        true  -> sub_calc_horizonal_diff(LogicX, LogicY,  1, LogicXDst, SceneId, 0);
        false -> sub_calc_horizonal_diff(LogicX, LogicY, -1, LogicXDst, SceneId, 0)
    end.

%% 此接口依赖于上一层参数的正确性，否则可能死循环，不允许外部调用
sub_calc_horizonal_diff(LogicX, LogicY, Incre, LogicXDst, SceneId, Diff) ->
    case scene_landform:is_block_logic(SceneId, LogicX, LogicY) of
        true  -> Diff;
        false ->
            case LogicX =:= LogicXDst of
                true  -> Diff;
                false ->
                    NewLogicX = LogicX + Incre,
                    NewDiff = Diff + Incre,
                    sub_calc_horizonal_diff(NewLogicX, LogicY, Incre, LogicXDst, SceneId, NewDiff)
            end
    end.

calc_vertical_diff(LogicX, LogicY, LogicYDst, SceneId) ->
    case LogicYDst >= LogicY of
        true  -> sub_calc_vertical_diff(LogicX, LogicY,  1, LogicYDst, SceneId, 0);
        false -> sub_calc_vertical_diff(LogicX, LogicY, -1, LogicYDst, SceneId, 0)
    end.

%% 此接口依赖于上一层参数的正确性，否则可能死循环，不允许外部调用
sub_calc_vertical_diff(LogicX, LogicY, Incre, LogicYDst, SceneId, Diff) ->
    case scene_landform:is_block_logic(SceneId, LogicX, LogicY) of
        true  -> Diff;
        false ->
            case LogicY =:= LogicYDst of
                true  -> Diff;
                false ->
                    NewLogicY = LogicY + Incre,
                    NewDiff = Diff + Incre,
                    sub_calc_vertical_diff(LogicX, NewLogicY, Incre, LogicYDst, SceneId, NewDiff)
            end
    end.

%% 计算斜率k（存在的时候才有效，当X1 == X2时无效）
calc_slope_k(_X1, Y, _X2, Y) ->
    0;
calc_slope_k(X, _Y1, X, _Y2) ->
    error;
calc_slope_k(X1, Y1, X2, Y2) ->
    (Y2 - Y1) / (X2 - X1).

%% 已知直线上X坐标求Y坐标
calc_line_point_y(SlopeK, X, XDst, YDst) ->
    % SlopeK * (X - XSrc) + YSrc.
    round(YDst - SlopeK * (XDst - X)).

%% 已知直线上Y坐标求X坐标（使用斜率K的倒数计算，运算多的话用乘法代替除法好）
%% RecipSlopeK = 1 / SlopeK
calc_line_point_x(RecipSlopeK, Y, XDst, YDst) ->
    % (Y - YSrc) * RecipSlopeK + XSrc.
    round(XDst - (YDst - Y) * RecipSlopeK).

%% 斜线击退区域判断障碍修正目标位置
%% 斜线偏向水平方向时，优先以X轴方向/反方向做步进操作
check_block_skew_line(ChkArgs = #skew_chk_args{is_near_horz = true}) ->
    #skew_chk_args{
        scene_id = SceneId,
        x_src = XSrc, y_src = YSrc,
        x_last = XLast, x_dst = XDst,
        y_last = YLast, y_dst = YDst,
        x_step_incre = XStepIncre, y_step_incre = YStepIncre,
        slope_k = SlopeK, recip_k = RecipSlopeK,
        cross_points = CrossPoints
    } = ChkArgs,
    XNext = XLast + XStepIncre,
    %% 实际上，XNext/YNext只要检测到在中间，则说明有交点，
    %% 按公式计算出来，不需要再检测对应坐标Y/X值范围
    case check_middle(XNext, XLast, XDst) of
        false ->    % 已超出线外，但是要回退检查是否还存在重垂线方向交点
            YNext = YLast + YStepIncre,
            case check_middle(YNext, YLast, YDst) of
                false ->    % 已超出线外，不需要检查，最终添加dst点做检查
                    {_, [Point | _]} = check_block_add_pos(SceneId, CrossPoints, {XDst, YDst}),
                    fix_point(Point, {XSrc, YSrc}, {XDst, YDst});
                true  ->    % 重垂线方向有交点，添加YNext到y_last（X方向回退了，未到XNext）
                    XCross = calc_line_point_x(RecipSlopeK, YNext, XDst, YDst),
                    case check_block_add_pos(SceneId, CrossPoints, {XCross, YNext}) of
                        {true, NewCrossPoints} ->
                            NewChkArgs = ChkArgs#skew_chk_args{
                                y_last = YNext,
                                cross_points = NewCrossPoints
                            },
                            check_block_skew_line(NewChkArgs);
                        {false, _} ->   % 最终添加dst点做检查
                            {_, [Point | _]} = check_block_add_pos(SceneId, CrossPoints, {XDst, YDst}),
                            fix_point(Point, {XSrc, YSrc}, {XDst, YDst})
                    end
            end;
        true  ->    % 在线内
            YCross = calc_line_point_y(SlopeK, XNext, XDst, YDst),
            YNext = YLast + YStepIncre,
            case check_middle(YCross, YNext, YDst) of
                true  ->    % 出现此情况说明漏过了一个交叉点，需要增加交叉点
                    XCross = calc_line_point_x(RecipSlopeK, YNext, XDst, YDst),
                    %% 先经过的点要放在前面
                    case check_block_add_pos(SceneId, CrossPoints, [{XCross, YNext}, {XNext, YCross}]) of
                        {true, NewCrossPoints} ->
                            NewChkArgs = ChkArgs#skew_chk_args{
                                x_last = XNext, y_last = YNext,
                                cross_points = NewCrossPoints
                            },
                            check_block_skew_line(NewChkArgs);
                        {false, [Point | _]} ->
                            fix_point(Point, {XSrc, YSrc}, {XDst, YDst})
                    end;
                false ->    % 此情况说明未漏过交叉点，YNext暂时不加入检查
                    case check_block_add_pos(SceneId, CrossPoints, {XNext, YCross}) of
                        {true, NewCrossPoints} ->
                            NewChkArgs = ChkArgs#skew_chk_args{
                                x_last = XNext,
                                cross_points = NewCrossPoints
                            },
                            check_block_skew_line(NewChkArgs);
                        {false, [Point | _]} ->
                            fix_point(Point, {XSrc, YSrc}, {XDst, YDst})
                    end
            end
    end;
%% 斜线偏向重垂线方向时，优先以Y轴方向/反方向做步进操作
check_block_skew_line(ChkArgs) ->
    #skew_chk_args{
        scene_id = SceneId,
        x_src = XSrc, y_src = YSrc,
        x_last = XLast, x_dst = XDst,
        y_last = YLast, y_dst = YDst,
        x_step_incre = XStepIncre, y_step_incre = YStepIncre,
        slope_k = SlopeK, recip_k = RecipSlopeK,
        cross_points = CrossPoints
    } = ChkArgs,
    YNext = YLast + YStepIncre,
    %% 实际上，XNext/YNext只要检测到在中间，则说明有交点，
    %% 按公式计算出来，不需要再检测对应坐标Y/X值范围
    case check_middle(YNext, YLast, YDst) of
        false ->    % 已超出线外，但是要回退检查是否还存在水平方向交点
            XNext = XLast + XStepIncre,
            case check_middle(XNext, XLast, XDst) of
                false ->    % 已超出线外，不需要检查，最终添加dst点做检查
                    {_, [Point | _]} = check_block_add_pos(SceneId, CrossPoints, {XDst, YDst}),
                    fix_point(Point, {XSrc, YSrc}, {XDst, YDst});
                true  ->    % 水平方向有交点，添加XNext到x_last（Y方向回退了，未到YNext）
                    YCross = calc_line_point_y(SlopeK, XNext, XDst, YDst),
                    case check_block_add_pos(SceneId, CrossPoints, {XNext, YCross}) of
                        {true, NewCrossPoints} ->
                            NewChkArgs = ChkArgs#skew_chk_args{
                                x_last = XNext,
                                cross_points = NewCrossPoints
                            },
                            check_block_skew_line(NewChkArgs);
                        {false, _} ->   % 最终添加dst点做检查
                            {_, [Point | _]} = check_block_add_pos(SceneId, CrossPoints, {XDst, YDst}),
                            fix_point(Point, {XSrc, YSrc}, {XDst, YDst})
                    end
            end;
        true  ->    % 在线内
            XCross = calc_line_point_x(RecipSlopeK, YNext, XDst, YDst),
            XNext = XLast + XStepIncre,
            case check_middle(XCross, XNext, XDst) of
                true  ->    % 出现此情况说明漏过了一个交叉点，需要增加交叉点
                    YCross = calc_line_point_y(SlopeK, XNext, XDst, YDst),
                    %% 先经过的点要放在前面
                    case check_block_add_pos(SceneId, CrossPoints, [{XNext, YCross}, {XCross, YNext}]) of
                        {true, NewCrossPoints} ->
                            NewChkArgs = ChkArgs#skew_chk_args{
                                x_last = XNext, y_last = YNext,
                                cross_points = NewCrossPoints
                            },
                            check_block_skew_line(NewChkArgs);
                        {false, [Point | _]} ->
                            fix_point(Point, {XSrc, YSrc}, {XDst, YDst})
                    end;
                false ->    % 此情况说明未漏过交叉点，XNext暂时不加入检查
                    case check_block_add_pos(SceneId, CrossPoints, {XCross, YNext}) of
                        {true, NewCrossPoints} ->
                            NewChkArgs = ChkArgs#skew_chk_args{
                                y_last = YNext,
                                cross_points = NewCrossPoints
                            },
                            check_block_skew_line(NewChkArgs);
                        {false, [Point | _]} ->
                            fix_point(Point, {XSrc, YSrc}, {XDst, YDst})
                    end
            end
    end.

%% 对坐标进行修正，如果本身是原始坐标或目标坐标则不修正，
%% 否则要转换为该逻辑坐标的中间点位置
fix_point(PointSrc, PointSrc, _PointDst) ->
    PointSrc;
fix_point(PointDst, _PointSrc, PointDst) ->
    PointDst;
fix_point({X, Y}, _PointSrc, _PointDst) ->
    {LogicX, LogicY} = lib_scene_calc:pixel_to_logic_coordinate(X, Y),
    {XFix, YFix} = lib_scene_calc:logic_to_pixel_coordinate(LogicX + 0.5, LogicY + 0.5),
    {round(XFix), round(YFix)}.

%% AccPoints在上层决定了一定非空列表，至少包含初始点
%% 注意，由于可能处理了列表，所以false情况也有可能更新了原始的AccPoints
%% 如果传入参数是单独的Point，则false情况不会更新原始的AccPoints
check_block_add_pos(SceneId, AccPoints, Point = {X, Y}) ->
    case lists:member(Point, AccPoints) of
        true  -> {true, AccPoints};
        false ->
            case scene_landform:is_block(SceneId, X, Y) of
                true  -> {false, AccPoints};
                false -> {true,  [Point | AccPoints]}
            end
    end;
check_block_add_pos(SceneId, AccPoints, [Point = {_X, _Y} | RestPoints]) ->
    case check_block_add_pos(SceneId, AccPoints, Point) of
        {true, NewAccPoints} ->
            check_block_add_pos(SceneId, NewAccPoints, RestPoints);
        {false, NewAccPoints} ->
            {false, NewAccPoints}
    end;
check_block_add_pos(_SceneId, AccPoints, []) ->
    {true, AccPoints}.

%% 检测Value是否在ValueA与ValueB之间，是则返回true
check_middle(Value, ValueA, ValueB) when ValueA >= ValueB ->
    Value =< ValueA andalso Value >= ValueB;
check_middle(Value, ValueA, ValueB) ->
    Value >= ValueA andalso Value =< ValueB.

-compile({inline, [calc_sin_cos/4]}).
%% 排除计算精度影响，以下算法结果上是等效的
%% 经过千万次执行测试，效率近乎完全一致
calc_sin_cos(AerX, AerY, DerX, DerY) ->
    Angle = math:atan2(DerY - AerY, DerX - AerX),
    Sin = math:sin(Angle),
    Cos = math:cos(Angle),
    {Sin, Cos}.
calc_sin_cos2(AerX, AerY, DerX, DerY) ->
    DisX = DerX - AerX,
    DisY = DerY - AerY,
    DisAerDer = math:sqrt(DisX*DisX + DisY*DisY),
    Sin = DisY / DisAerDer,
    Cos = DisX / DisAerDer,
    {Sin, Cos}.

%% -------------------------------------------------------
%% 测试
%% -------------------------------------------------------

%% N为测试随机坐标点组数（限制在1-200之间）
test(N) ->
    test(10, 10, 15, 15, 1001, 100),
    test(550, 550, 555, 555, 1001, 100),
    test(1800, 20300, 1810, 20310, 1001, 200),
    test(1800, 20300, 1810, 20310, 1001, 250),

    test(1800, 20300, 1810, 20310, 1001, 300),
    test(1800, 20300, 1810, 20300, 1001, 300),
    test(1800, 20300, 1800, 20310, 1001, 300),
    test(1810, 20310, 1800, 20300, 1001, 300),
    test(1810, 20050, 1790, 19999, 1001, 300),
    test(1910, 20450, 1900, 20433, 1001, 300),

    Fun = fun(SceneId) ->
            AerX = urand:rand(2000, 20000),
            AerY = urand:rand(2000, 25000),
            DerX = urand:rand(-100, 100) + AerX,
            DerY = urand:rand(-100, 100) + AerY,
            Distance = urand:rand(100, 300),
            test(AerX, AerY, DerX, DerY, SceneId, Distance)
    end,
    [Fun(1001) || _ <- lists:seq(1, min(max(N, 1), 200))],
    ok.

test(AerX, AerY, DerX, DerY, SceneId, Distance) ->
    Result = calc_repel_pos(AerX, AerY, DerX, DerY, SceneId, Distance),
    io:format("{AerX, AerY, DerX, DerY, SceneId, Distance}:~w, Result:~w~n", [
            {AerX, AerY, DerX, DerY, SceneId, Distance}, Result
        ]).

test_sincos(N) ->
    [AerX, AerY, DerX, DerY] = [urand:rand(1, 10000) || _ <- lists:seq(1, 4)],
    ptester:run(N, [
            {calc_sin_cos1, fun(_) -> calc_sin_cos(AerX, AerY, DerX, DerY) end},
            {calc_sin_cos2, fun(_) -> calc_sin_cos2(AerX, AerY, DerX, DerY) end}
        ]),
    ok.

