using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

namespace Unity.BossRoom.Navigation
{
    public sealed class DynamicNavPath : IDisposable
    {
        /// <summary>
        /// The tolerance to decide whether the path needs to be recalculated when the position of a target transform changed.
        /// </summary>
        const float k_RepathToleranceSqr = 9f;

        NavMeshAgent m_Agent;

        NavigationSystem m_NavigationSystem;

        /// <summary>
        /// The target position value which was used to calculate the current path.
        /// This get stored to make sure the path gets recalculated if the target
        /// </summary>
        Vector3 m_CurrentPathOriginalTarget;

        /// <summary>
        /// This field caches a NavMesh Path so that we don't have to allocate a new one each time.
        /// </summary>
        NavMeshPath m_NavMeshPath;

        /// <summary>
        /// The remaining path points to follow to reach the target position.
        /// </summary>
        List<Vector3> m_Path;

        /// <summary>
        /// The target position of this path.
        /// </summary>
        Vector3 m_PositionTarget;

        /// <summary>
        /// A moving transform target, the path will readjust when the target moves. If this is non-null, it takes precedence over m_PositionTarget.
        /// </summary>
        Transform m_TransformTarget;

        /// <summary>
        /// Creates a new instance of the <see cref="DynamicNavPath"/>.
        /// </summary>
        /// <param name="agent">The NavMeshAgent of the object which uses this path.</param>
        /// <param name="navigationSystem">The navigation system which updates this path.</param>
        public DynamicNavPath(NavMeshAgent agent, NavigationSystem navigationSystem)
        {
            m_Agent = agent;
            m_Path = new List<Vector3>();
            m_NavMeshPath = new NavMeshPath();
            m_NavigationSystem = navigationSystem;

            navigationSystem.OnNavigationMeshChanged += OnNavMeshChanged;
        }

        Vector3 TargetPosition => m_TransformTarget != null ? m_TransformTarget.position : m_PositionTarget;

        /// <summary>
        /// Set the target of this path to follow a moving transform.
        /// </summary>
        /// <param name="target">The transform to follow.</param>
        public void FollowTransform(Transform target)
        {
            m_TransformTarget = target;
        }

        /// <summary>
        /// Set the target of this path to a static position target.
        /// </summary>
        /// <param name="target">The target position.</param>
        public void SetTargetPosition(Vector3 target)
        {
            // If there is an nav mesh area close to the target use a point inside the nav mesh instead.
            if (NavMesh.SamplePosition(target, out NavMeshHit hit, 2f, NavMesh.AllAreas))
            {
                target = hit.position;
            }

            m_PositionTarget = target;
            m_TransformTarget = null;
            RecalculatePath();
        }

        /// <summary>
        /// Call this to recalculate the path when the navigation mesh or dynamic obstacles changed.
        /// </summary>
        void OnNavMeshChanged()
        {
            RecalculatePath();
        }

        /// <summary>
        /// Clears the path.
        /// </summary>
        public void Clear()
        {
            m_Path.Clear();
        }

        /// <summary>
        /// Gets the movement vector for moving this object while following the path. This function changes the state of the path and should only be called once per tick.
        /// </summary>
        /// <param name="distance">The distance to move.</param>
        /// <returns>Returns the movement vector.</returns>
        public Vector3 MoveAlongPath(float distance)
        {
            if (m_TransformTarget != null)
            {
                OnTargetPositionChanged(TargetPosition);
            }

            if (m_Path.Count == 0)
            {
                return Vector3.zero;
            }

            var currentPredictedPosition = m_Agent.transform.position;
            var remainingDistance = distance;

            while (remainingDistance > 0)
            {
                var toNextPathPoint = m_Path[0] - currentPredictedPosition;

                // If end point is closer then distance to move
                if (toNextPathPoint.sqrMagnitude < remainingDistance * remainingDistance)
                {
                    currentPredictedPosition = m_Path[0];
                    m_Path.RemoveAt(0);
                    remainingDistance -= toNextPathPoint.magnitude;
                }

                // Move towards point
                currentPredictedPosition += toNextPathPoint.normalized * remainingDistance;

                // There is definitely no remaining distance to cover here.
                break;
            }

            return currentPredictedPosition - m_Agent.transform.position;
        }

        void OnTargetPositionChanged(Vector3 newTarget)
        {
            if (m_Path.Count == 0)
            {
                RecalculatePath();
            }

            if ((newTarget - m_CurrentPathOriginalTarget).sqrMagnitude > k_RepathToleranceSqr)
            {
                RecalculatePath();
            }
        }

        /// <summary>
        /// Recalculates the cached navigationPath
        /// </summary>
        void RecalculatePath()
        {
            m_CurrentPathOriginalTarget = TargetPosition;
            m_Agent.CalculatePath(TargetPosition, m_NavMeshPath);

            m_Path.Clear();

            var corners = m_NavMeshPath.corners;

            for (int i = 1; i < corners.Length; i++) // Skip the first corner because it is the starting point.
            {
                m_Path.Add(corners[i]);
            }
        }

        public void Dispose()
        {
            m_NavigationSystem.OnNavigationMeshChanged -= OnNavMeshChanged;
        }
    }
}