using UnityEngine;

namespace Game.Plugins.JNGame.Sync.Frame.AstarPath.RVO
{
    
    /// <summary>
    /// RVO角色控制器 用于处理基于RVO算法的局部避让，允许开发者在复杂的动态环境中控制角色的移动，同时避免与其他角色发生碰撞。。  
    /// 与Unity的CharacterController类似。它处理移动计算并考虑其他代理。  
    /// 它本身不处理移动，但允许调用脚本获取计算出的速度，  
    /// 并使用适合的方法移动对象（例如使用CharacterController，使用Point = transform.position + transform.forward * 100;  
    ///  
    ///     // 使用期望的速度10和最大速度12设置要移动到的目标点  
    ///     controller.SetTarget(targetPoint, 10, 12);  
    ///  
    ///     // 计算这一帧中要移动的距离  
    ///     // 此信息基于之前帧的移动命令  
    ///     // 因为局部避让是由RVOSimulator组件定期全局计算的  
    ///     var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime);  
    ///     transform.position = transform.position + delta;  
    /// }  
    ///  
    /// 关于这个类中许多变量的文档：请参阅Pathfinding.RVO.IAgent接口。  
    ///  
    /// 注意：场景中需要一个RVOSimulator组件  
    ///  
    /// 请参阅：Pathfinding.RVO.IAgent  
    /// 请参阅：RVOSimulator  
    /// 请参阅：local-avoidance（在在线文档中查看工作链接）  
    /// </summary>
    public class JNRVOController
    {
        
        // 内部浮点型字段，用于存储半径值，初始化为0.5  
        internal float radiusBackingField = 0.5f;
        // 浮点型字段，用于存储高度值，初始化为2  
        float heightBackingField = 2;
        // 浮点型字段，用于存储中心点值，初始化为1
        float centerBackingField = 1;
        
        
        /// <summary>  
        /// 代理在世界单位中的半径。  
        /// 注意：如果同一个GameObject上附加了移动脚本（AIPath/RichAI/AILerp，任何实现IAstarAI接口的内容），则该值将由该脚本驱动。  
        /// </summary>  
        // 定义一个公共的浮点型属性radius，用于表示代理在世界单位中的半径。  
        // 当一个GameObject同时附加了移动脚本（实现了IAstarAI接口的脚本）时，radius的值将由该脚本决定。  
        public float radius {  
            get => radiusBackingField;
            set => radiusBackingField = value;
        }
        
        /// <summary>  
        /// 代理在世界单位中的高度。  
        /// </summary>  
        public float height
        {
            get => heightBackingField; 
            set => heightBackingField = value;
        }
        
        /// <summary>  
        /// 代理相对于此游戏对象枢转点的中心位置。  
        /// </summary>  
        public float center
        {
            get => centerBackingField;
            // 如果有 AI 脚本附加，该值将始终被驱动为 height/2，因为移动脚本期望对象的位置在其脚下  
            // 如果没有 AI 脚本附加，则返回 centerBackingField 的值  
            set => centerBackingField = value;
        }
        
        /// <summary>内部代理的引用</summary>  
        public JNIAgent rvoAgent { get; private set; }
        
        
        /// <summary>缓存的对象</summary>
        protected GameObject obj;

        ///避障核心类
        private JNRVOSimulator Simulator;
        
        /// <summary>  
        /// 在未来多少秒内寻找与其他代理的碰撞（以秒为单位）。这个变量表示在预测未来的移动路径时，应该考虑多少秒内的碰撞检测。  
        /// </summary>  
        public float agentTimeHorizon = 2;
        
        /// <summary>  
        /// 在未来多少秒内寻找与障碍物的碰撞（以秒为单位）。  这个变量表示在预测未来的移动路径时，应该考虑多少秒内的与障碍物的碰撞检测。  
        /// </summary>  
        public float obstacleTimeHorizon = 2;
        
        /// <summary>  
        /// 一个被锁定的单位不能移动。其他单位仍然会避开它，但避开的质量不是最好的。  
        /// </summary>  
        public bool locked = false;
        
        /// <summary>  
        /// 考虑在内的其他代理的最大数量。  
        /// 较小的值可以减少CPU负载，而较高的值可以提高局部避让的质量。  
        /// 如果将maxNeighbours设置得较小，可以减少代理需要计算的碰撞避免逻辑，从而减轻CPU的负担。  
        /// 如果将maxNeighbours设置得较高，代理将考虑更多的其他代理，这可能会导致更精确的避让行为，但也会增加CPU的负载。  
        /// </summary>  
        public int maxNeighbours = 10;

        /// <summary>DEBUG 绘制</summary>
        public bool debug;
        
        /// <summary>  
        /// 指定此代理的避让层。  
        /// 其他代理的<see cref="collidesWith"/>掩码将确定它们是否会避开此代理。  
        /// </summary>  
        public JNRVOLayer layer = JNRVOLayer.DefaultAgent;
        
        /// <summary>  
        /// 一个层掩码，指定此代理将避免哪些层。  
        /// 你可以这样设置它：CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ...  
        /// 在具有多个团队的游戏中，这非常有用。例如，你通常希望一个团队中的代理避免彼此，但你不希望他们避免敌人。  
        /// 此字段仅影响此代理将避免哪些其他代理，它不影响其他代理如何对此代理做出反应。  
        /// 参见：位掩码（在线文档中查看相关链接）  
        /// 参见：http://en.wikipedia.org/wiki/Mask_(computing)  
        /// </summary>  
        public JNRVOLayer collidesWith = (JNRVOLayer)(-1);
        
        /// <summary>  
        /// 使用Tooltip特性为属性提供工具提示，说明其他代理将如何强烈地避免此代理。
        /// 使用UnityEngine.Range特性限制priority属性的值范围在0到1之间。
        /// 这个变量表示其他代理在避让此代理时的优先级或强度。值越高，其他代理将更强烈地避免与此代理碰撞。
        /// </summary>  
        public float priority = 0.5f;
        
        /// <summary>  
        /// 当期望的速度接近于零时，自动将 <see cref="locked"/> 设置为 true。  
        /// 这可以防止其他单位在它们应该执行例如阻塞瓶颈等操作时将它们推开。  
        ///  
        /// 当此属性为 true 时，每次调用 <see cref="SetTarget"/> 或 <see cref="Move"/> 方法时，  
        /// 如果期望的速度不为零，则将 <see cref="locked"/> 字段设置为 true，否则设置为 false。  
        /// </summary>  
        public bool lockWhenNotMoving = false;
        
        /// <summary>  
        /// 将3D向量转换为运动平面上的2D向量。  
        /// 如果运动平面是XZ，则将其投影到XZ平面上，  
        /// 否则将其投影到XY平面上。  
        /// </summary>  
        /// <param name="p">要转换的3D向量</param>  
        /// <returns>转换后的2D向量</returns>  
        public Vector2 To2D(Vector3 p) {  
            float dummy; // 定义一个临时变量dummy，用于接收不需要的输出值  
  
            // 调用重载的To2D方法，将3D向量p和dummy作为参数传递，并返回转换后的2D向量  
            return To2D(p, out dummy);  
        }
        
        /// <summary>  
        /// 将运动平面上的二维向量及其高度转换为三维坐标。  
        /// 参见：To2D 方法（用于将三维坐标转换为二维向量和高度）  
        /// 参见：movementPlane 属性（定义代理的运动平面）  
        /// </summary>  
        /// <param name="p">二维向量，表示运动平面上的坐标。</param>  
        /// <param name="elevationCoordinate">高度坐标，根据运动平面的不同，它可能表示不同的轴。</param>  
        /// <returns>返回转换后的三维坐标。</returns>  
        public Vector3 To3D(Vector2 p, float elevationCoordinate)  
        {  
            // 根据 movementPlane 的值，将二维向量和高度转换为三维坐标  
            if (movementPlane == MovementPlane.XY)  
            {  
                // 如果运动平面是 XY 平面，则 Z 轴表示高度（但此处使用负值，可能是根据具体应用场景而定）  
                return new Vector3(p.x, p.y, -elevationCoordinate);  
            }  
            else  
            {  
                // 如果运动平面不是 XY 平面（可能是 XZ 平面），则 Y 轴表示高度  
                return new Vector3(p.x, elevationCoordinate, p.y);  
            }  
        }
        
        /// <summary>  
        /// 将3D向量转换为运动平面上的2D向量。  
        /// 如果运动平面是XZ，则将其投影到XZ平面上，并将高度坐标设置为Y坐标；  
        /// 否则，将其投影到XY平面上，并将高度设置为Z坐标。  
        /// </summary>  
        /// <param name="p">要转换的3D向量</param>  
        /// <param name="elevation">转换后的高度坐标</param>  
        /// <returns>转换后的2D向量</returns>
        public Vector2 To2D(Vector3 p, out float elevation) {  
            // 判断当前的运动平面  
            if (movementPlane == MovementPlane.XY) {  
                // 如果运动平面是XY，则将Y坐标作为高度，并返回X和Y组成的2D向量  
                elevation = -p.z; // 高度设置为-Z坐标（通常用于表示高度或海拔）  
                return new Vector2(p.x, p.y); // 返回X和Y组成的2D向量  
            } else {  
                // 如果运动平面不是XY（即XZ），则将Y坐标作为高度，并返回X和Z组成的2D向量  
                elevation = p.y; // 高度设置为Y坐标  
                return new Vector2(p.x, p.z); // 返回X和Z组成的2D向量  
            }  
        }
        
        /// <summary>  
        /// 确定是否使用XY（2D）或XZ（3D）平面进行移动  
        /// </summary>  
        public MovementPlane movementPlane {  
            get => Simulator.movementPlane;
        }
        

        public JNRVOController(JNRVOSimulator Simulator,GameObject obj)
        {
            this.obj = obj;
            this.Simulator = Simulator;
            
            // 如果是这样的话，我们可以简单地将它再次添加到模拟中  
            if (rvoAgent != null) {  
                // 我们将其再次添加到模拟器中  
                Simulator.AddAgent(rvoAgent);  
            } else {  
                // 如果 rvoAgent 为空，表示之前没有实例或者实例已被销毁  
                // 我们创建一个新的代理实例并添加到模拟器中  
                // 使用初始位置 Vector2.zero（通常代表坐标(0,0)）和初始方向 0（通常代表面向上）来创建新的代理  
                rvoAgent = Simulator.AddAgent(Vector2.zero, 0);  
      
                // 设置 rvoAgent 的 PreCalculationCallback 为 UpdateAgentProperties 方法  
                // 这意味着在每次模拟计算之前，都会调用 UpdateAgentProperties 方法来更新代理的属性  
                rvoAgent.PreCalculationCallback = UpdateAgentProperties;  
            }
            
        }
        
        /// <summary>
        /// 更新代理的属性
        /// </summary>
        protected void UpdateAgentProperties()  
        {  
            // 获取当前代理的本地缩放比例  
            var scale = obj.transform.localScale;  
  
            // 设置代理的半径，至少为0.001，通常根据半径与X轴缩放比例的乘积来确定  
            rvoAgent.Radius = Mathf.Max(0.001f, radius * scale.x);  
  
            // 设置代理的时间预测范围  
            rvoAgent.AgentTimeHorizon = agentTimeHorizon;  
  
            // 设置代理与障碍物的时间预测范围  
            rvoAgent.ObstacleTimeHorizon = obstacleTimeHorizon;  
  
            // 锁定代理，使其不参与模拟计算  
            rvoAgent.Locked = locked;  
  
            // 设置代理的最大邻居数量  
            rvoAgent.MaxNeighbours = maxNeighbours;  
  
            // 是否开启代理的调试绘制  
            rvoAgent.DebugDraw = debug;  
  
            // 设置代理所在的层级  
            rvoAgent.Layer = layer;  
  
            // 设置代理与哪些类型的代理发生碰撞  
            rvoAgent.CollidesWith = collidesWith;  
  
            // 设置代理的优先级
            rvoAgent.Priority = priority;  
  
            float elevation;  
  
            // 如果存在移动脚本，则使用移动脚本的位置作为代理的位置  
            // 因为移动脚本的位置可能与Transform组件的位置不同（特别是在IAstarAI.updatePosition为false时）  
            rvoAgent.Position = To2D(obj.transform.position, out elevation);  
  
            // 根据移动平面设置代理的高度和高度坐标  
            if (movementPlane == MovementPlane.XZ)  
            {  
                // 如果移动平面是XZ平面，则设置代理的高度为缩放后的高度值  
                rvoAgent.Height = height * scale.y;  
  
                // 设置代理的高度坐标，根据中心点和高度计算得出  
                rvoAgent.ElevationCoordinate = elevation + (center - 0.5f * height) * scale.y;  
            }  
            else  
            {  
                // 如果移动平面不是XZ平面，则将代理的高度设置为1，高度坐标设置为0  
                rvoAgent.Height = 1;  
                rvoAgent.ElevationCoordinate = 0;  
            }  
        }
        
        /// <summary>  
        /// 为代理设置移动的目标点。  
        /// 与 <see cref="Move"/> 方法类似，但更加灵活。  
        /// 在路径的末尾使用此方法更好，因为使用 Move 方法时，代理不知道应该在何处停止，因此可能会超过目标点。  
        /// 使用此方法时，代理不会超过目标点。  
        /// 代理将假设当它到达目标点时会停止，因此请确保如果你只是想朝特定方向移动，则不要将点设置得太靠近代理。  
        ///  
        /// 目标点将保持不变，直到请求其他点（而不是每帧重置）。  
        ///  
        /// 请参阅：也请查看 <see cref="IAgent.SetTarget"/> 的文档，其中有更多详细信息。  
        /// 请参阅：<see cref="Move"/>  
        /// </summary>  
        /// <param name="pos">要移动到的世界空间中的点。</param>  
        /// <param name="speed">每秒在世界单位中期望的速度。</param>  
        /// <param name="maxSpeed">每秒在世界单位中的最大速度。  
        /// 	如果必要，代理将使用此速度以避免与其他代理发生碰撞。  
        /// 	应该至少与 speed 一样高，但建议使用比 speed 略高的值（例如 speed*1.2）。</param>  
        public void SetTarget(Vector3 pos, float speed, float maxSpeed) {  
  
            // 将 3D 位置转换为 2D 位置，并设置给 rvoAgent  
            rvoAgent.SetTarget(To2D(pos), speed, maxSpeed);  
  
            // 如果 lockWhenNotMoving 为 true，则根据速度是否低于某个阈值来锁定代理  
            if (lockWhenNotMoving) {  
                locked = speed < 0.001f;  
            }  
            
        }
        
        /// <summary>  
        /// 设置代理的期望速度。  
        /// 请注意，这是一个速度（单位/秒），而不是移动增量（单位/帧）。  
        ///  
        /// 此速度将保持不变，直到请求其他值（而不是每帧重置）。  
        ///  
        /// 注意：在大多数情况下，使用 SetTarget 方法是更好的选择。  
        ///  实际上，此方法将调用 SetTarget 并传递（位置 + 速度）作为目标点。  
        ///  请参阅 IAgent.SetTarget 文档中的说明，了解这可能导致的潜在问题（特别是可能很难让代理在精确的点停止）。  
        /// </summary>  
        public void Move(Vector3 vel) {  
            
            // 将 3D 速度转换为 2D 速度  
            var velocity2D = To2D(vel);  
            // 获取速度的大小（即速度的模）  
            var speed = velocity2D.magnitude;  
  
            // 调用 rvoAgent 的 SetTarget 方法，设置目标点为当前位置（如果 ai 不为 null，则使用 ai.position，否则使用 tr.position）加上 2D 速度  
            // 最大速度和期望速度都设置为 speed  
            rvoAgent.SetTarget(To2D(obj.transform.position) + velocity2D, speed, speed);  
  
            // 如果 lockWhenNotMoving 为 true，则根据速度是否低于 0.001f 来设置 locked 标志  
            if (lockWhenNotMoving) {  
                locked = speed < 0.001f;  
            }
        }
        
        /// <summary>  
        /// 计算单个帧内为避免障碍物而移动的方向和距离。  
        ///  
        /// 代理的位置取自附加的移动脚本的位置（参见 <see cref="Pathfinding.IAstarAI.position"/>），如果没有附加，则取自 transform.position。  
        /// </summary>  
        /// <param name="deltaTime">移动的时间长度[秒]。  
        ///      通常设置为 Time.deltaTime。</param>  
        /// <returns>返回一个 Vector3 类型的值，表示代理在单个帧内为避免障碍物而移动的方向和距离。</returns>  
        public Vector3 CalculateMovementDelta(float deltaTime)  
        {  
            // 如果 rvoAgent 为空，则返回零向量，即代理不进行移动  
            if (rvoAgent == null) return Vector3.zero;  
  
            // 计算目标点和代理当前位置之间的二维向量差  
            Vector2 movementVector = rvoAgent.CalculatedTargetPoint - To2D(obj.transform.position);  
  
            // 限制向量的幅度（即长度），确保不超过代理在给定时间内可以移动的最大距离  
            Vector2 clampedVector = Vector2.ClampMagnitude(movementVector, rvoAgent.CalculatedSpeed * deltaTime);  
  
            // 将二维向量转换为三维向量，其中 Z 分量为 0  
            Vector3 movementDelta = To3D(clampedVector, 0);  
  
            // 返回计算出的移动方向和距离  
            return movementDelta;  
        }
        
    }
}