using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.Utilities;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.AI;
using VOID.Generic.Dict;
using VOID.Generic.Navigation;
using VOID.Generic.Objects.Abstract;
using VOID.Generic.Objects.Unit.Animations;
using VOID.Generic.Objects.Unit.Events;
using VOID.Generic.Settings;
using VOID.Generic.Util;
using VOID.ScriptableObjects.Animations;

namespace VOID.Generic.Objects.Unit
{
    [Serializable]
    public class UnitObjectNavAgent : ObjectComponent<UnitObject>
    {
        #region Initialize

        protected override void SelfInitialize()
        {
            _navMeshAgent = parent.GetComponent<NavMeshAgent>();
            _rigidbody = parent.GetComponent<Rigidbody>();

            _lastPosition = parent.transform.position;
        }

        #endregion

        // Config
        [field: SerializeField] public float moveSpeed { get; private set; } = 2;

        // Getters
        public Vector3 velocity => _navMeshAgent.velocity;
        public Vector3 localVelocity => _navMeshAgent.transform.InverseTransformDirection(velocity);

        // Current path and movement info
        private MoveEvent _currentMoveEvent;
        private NavMeshPath _path;
        private PathCorrected _pathCorrected;
        private Vector3 _lastPosition;

        // Components
        private NavMeshAgent _navMeshAgent;
        private Rigidbody _rigidbody;

        private static readonly List<Vector3> Directions = new()
        {
            Vector3.forward,
            Vector3.Normalize(Vector3.forward + Vector3.right),
            Vector3.right,
            Vector3.Normalize(Vector3.back + Vector3.right),
            Vector3.back,
            Vector3.Normalize(Vector3.back + Vector3.left),
            Vector3.left,
            Vector3.Normalize(Vector3.forward + Vector3.left),
        };

        /// <summary>
        /// Checks if given point is on navmesh for current <see cref="UnitObject"/>. 
        /// </summary>
        public bool IsValidGround(Vector3 position)
        {
            const float maxDistance = 0.1f;
            NavMesh.SamplePosition(position, out var hit, maxDistance, _navMeshAgent.areaMask);
            if (!hit.hit) return false;
            
            // Make sure that hit point is not too far horizontally
            return Vector3.Distance(
                Vector3.Scale(position, new Vector3(1, 0, 1)),
                Vector3.Scale(hit.position, new Vector3(1, 0, 1))
            ) < maxDistance / 2;
        }

        /// <summary>
        /// To find destination checks all 8 directions around target <see cref="AbstractObject"/> and select point with shortest path.
        /// </summary>
        /// <param name="targetObject">Object to which we want move</param>
        /// <param name="untilInRange">If not <b>zero</b> then path can be ended ealier when within provided range.<br/>
        /// ALSO selecting between those 8 directions prioritize those points within this range</param>
        public void CalculatePathTo(AbstractObject targetObject, float? untilInRange = null)
        {
            var unitPos = targetObject.transform.position;
            var positions = Directions
                .Select(direction => unitPos + targetObject.transform.TransformDirection(direction) * targetObject.basicData.radius)
                .Where(position => position != default)
                .ToList();
            
            var isInRange = new bool[positions.Count];
            var paths = new NavMeshPath[positions.Count];
            var lengths = new float[positions.Count];
            var correctedPaths = new PathCorrected[positions.Count];

            // Get points in all directions which will be used to decided to which one will we move
            paths.ToList().ForEach((_, i) =>
            {
                CalculatePathTo(positions[i], untilInRange.GetValueOrDefault(), unitPos);
                paths[i] = GetPath();
                correctedPaths[i] = _pathCorrected;
                positions[i] = paths[i].corners.Last();
                isInRange[i] = !untilInRange.HasValue || Vector3.Distance(positions[i], unitPos) < untilInRange;
                lengths[i] = NavMeshPathUtil.GetPathLength(paths[i]);
            });

            // Find closest point that also are within given range
            var selectedKey = paths
                .Select((_, i) => i)
                .Where(i => isInRange[i])
                .OrderBy(i => lengths[i])
                .Aggregate(-1, (a, i) => a == -1 || lengths[a] > lengths[i] ? i : a);

            // Point found! And it is without needed range!
            if (selectedKey != -1)
            {
                _path = paths[selectedKey];
                _pathCorrected = correctedPaths[selectedKey];
                return;
            }
            
            // Find any closest point
            selectedKey = paths
                .Select((_, i) => i)
                .OrderBy(i => lengths[i])
                .Aggregate(-1, (a, i) => a == -1 || lengths[a] > lengths[i] ? i : a);

            // Point found! But it is NOT within needed range! :(
            if (selectedKey != -1)
            {
                _path = paths[selectedKey];
                _pathCorrected = correctedPaths[selectedKey];
                return;
            }
            
            // Somehow all paths are invalid!
            _path = default;
            _pathCorrected = null;
        }

        /// <summary>
        /// Calculates path from agent position to destination, when range provided whole path will be shortened.
        /// After that you can use <see cref="GetPath"/> and <see cref="GetCorrectedPath"/>.
        /// </summary>
        /// <param name="destination">Position to which we want move</param>
        /// <param name="untilInRange">Possible ending path earlier when gets in range</param>
        /// <param name="untilInRangeOfPosition">If given then <b><see cref="untilInRange"/> will be checked to this position</b></param>
        public void CalculatePathTo(Vector3 destination, float untilInRange = default, Vector3 untilInRangeOfPosition = default)
        {
            if (_path?.corners.IsNullOrEmpty() == false && parent.transform.position == _path.corners.First() && destination == _path.corners.Last()) return;

            _path = NavMeshPathUtil.GetPathTo(_navMeshAgent, destination);
            var pathCorrected = _path.corners.ToList();

            // Early path ending when in range to given position or destination
            if (untilInRange > 0f)
            {
                untilInRangeOfPosition = untilInRangeOfPosition == default ? destination : untilInRangeOfPosition;
                pathCorrected = NavMeshPathUtil.GetPathShortenedByRange(_path, untilInRangeOfPosition, untilInRange);
            }

            // Fixing height on path to be sure it wont move through things like ramps, stairs, ladders, etc.
            _pathCorrected = NavMeshPathUtil.GetCorrectedPath(pathCorrected, parent);
        }

        /// <summary>
        /// NavMeshPath's array of Vector3. This path can have invalid height and should not be used by anything else than Unity's NavMesh Components.
        /// </summary>
        public NavMeshPath GetPath()
        {
            return _path;
        }

        /// <summary>
        /// List of Vector3 with corrected height and safe to use for further calculations and drawings.
        /// </summary>
        public List<Vector3> GetCorrectedPath()
        {
            if (_path.status is not NavMeshPathStatus.PathInvalid) return _pathCorrected.path;
            
            Debug.LogWarning("(Unity Internal) Cant calculate path. This can be false-positive.");
            return new List<Vector3>();
        }

        /// <summary>
        /// Move current Unit to target object (until in range to that object).
        /// </summary>
        /// <param name="targetObject">Movement target</param>
        /// <param name="untilInRange">Range which path will be shortened</param>
        /// <returns>TRUE when move started successfully</returns>
        public bool Move(AbstractObject targetObject, float untilInRange = 0f)
        {
            CalculatePathTo(targetObject, untilInRange);
            return Move();
        }

        /// <summary>
        /// Move current Unit to target destination (shortened by given range).
        /// </summary>
        /// <param name="targetPosition">Movement target</param>
        /// <param name="untilInRange">Range which path will be shortened</param>
        /// <returns>TRUE when move started successfully</returns>
        public bool Move(Vector3 targetPosition, float untilInRange = 0f)
        {
            CalculatePathTo(targetPosition, untilInRange);
            return Move();
        }

        private bool Move()
        {
            _navMeshAgent.speed = moveSpeed;
            _navMeshAgent.enabled = true;

            // Initialize whole movement by agent, but not moving yet...
            _navMeshAgent.SetPath(_path);
            _navMeshAgent.isStopped = true;

            // ...we can be already at our destination so there is no need to move
            if (CanFinish())
            {
                MoveFinished();
                return true;
            }
            
            _currentMoveEvent = new MoveEvent
            {
                unit = parent,
                pathCorrectedTarget = _path.corners.Last(),
                path = _path,
                pathCorrected = _pathCorrected
            };
            parent.unitEvents.onMoveStart.Invoke(_currentMoveEvent);

            // Manage movement on every fixed update
            parent.basicEvents.onFixedUpdate.AddListener(OnMove);

            _navMeshAgent.isStopped = false;
            parent.unitState.isMoving = true;

            return true;
        }

        /// <summary>
        /// Rotate current Unit to look at given point. Only yaw, which mean he cant look up or down.
        /// </summary>
        /// <param name="lootAt">Point at which unit will look</param>
        public void Rotate(Vector3 lootAt)
        {
            var startPos = parent.transform.position;
            startPos.y = 0f;
            lootAt.y = 0f;
            parent.transform.rotation = Quaternion.LookRotation(lootAt - startPos, Vector3.up);
        }

        /// <summary>
        /// Stops Unit's movement.
        /// </summary>
        public void Stop()
        {
            if (_navMeshAgent.isStopped) return;

            MoveFinished();
        }

        /// <summary>
        /// Destination reached, clear everything.
        /// </summary>
        private void MoveFinished()
        {
            parent.basicEvents.onFixedUpdate.RemoveListener(OnMove);
            
            // Clear unit's path and stop its movement
            _navMeshAgent.ResetPath();
            _navMeshAgent.isStopped = true;
            _navMeshAgent.enabled = false;

            parent.unitState.isMoving = false;

            if (_currentMoveEvent != null) parent.unitEvents.onMoveEnd.Invoke(_currentMoveEvent);
            _currentMoveEvent = null;

            // Clear
            _path = null;
            _pathCorrected = null;

            _lastPosition = parent.transform.position;
        }

        /// <summary>
        /// For every FixedUpdate check for:
        /// - NavMeshLink enter and movement
        /// - Turn movement resource consuming
        /// - Ending movement when reaching destination
        /// </summary>
        private void OnMove()
        {
            _navMeshAgent.velocity = _navMeshAgent.desiredVelocity;
            
            if (_navMeshAgent.isOnOffMeshLink)
            {
                LinkStart();
                return;
            }

            if (parent.basicState.turnManager)
            {
                var neededMovePoints = Vector3.Distance(_lastPosition, parent.transform.position);
                var canMove = parent.unitData.UseResource(UnitResourceType.MovePoints, neededMovePoints);
                if (!canMove)
                {
                    MoveFinished();
                    return;
                }
            }
            
            _lastPosition = parent.transform.position;

            if (CanFinish())
            {
                // When unit is REALLY close to destination - make sure it stand at destination point
                _rigidbody.MovePosition(_pathCorrected.path.Last());
                
                MoveFinished();
            }
        }

        private bool CanFinish()
        {
            return _navMeshAgent.remainingDistance <= 0.1f;
        }

        #region LINK TRAVERSE
        
        // Current NavMeshLink handler
        private Vector3 _linkPathStart;
        private List<Vector3> _linkPath;
        private PathCorrectedLinkData _linkData;
        private Vector3 _linkExitPosition;
        private Coroutine _linkCoroutine;
        private ActionLoopAnimationList _linkTraverseAnimation;
        
        /// <summary>
        /// When path finding moves on NavMeshLink prepare for custom moving through it.
        /// </summary>
        private void LinkStart()
        {
            if (parent.unitState.isTraversing) return;

            // When somehow NavMeshLink is not valid - end traversing instantly
            var offMeshLink = _navMeshAgent.currentOffMeshLinkData.owner as NavMeshLink;
            if (!offMeshLink)
            {
                Debug.LogWarning(
                    $"{parent.name} tried to use {nameof(NavMeshLink)}, but link is not found!"
                    + "Instantly ending link movement!");
                LinkEnd();
                return;
            }

            // Get info about next NavMeshLink
            _linkData = GetLinkData();
            if (_linkData == null)
            {
                Debug.LogWarning(
                    $"{parent.name} tried to use {nameof(NavMeshLink)}, but {nameof(PathCorrectedLinkData)} is missing!"
                    + "Instantly ending link movement!");
                LinkEnd();
                return;
            }

            // Get exit point for this traversable 
            _linkExitPosition = _linkData.isReverse
                ? _linkData.traversable.traversableData.linkStartPosition
                : _linkData.traversable.traversableData.linkEndPosition;
            if (_linkExitPosition == default)
            {
                Debug.LogWarning(
                    $"{parent.name} tried to use {nameof(NavMeshLink)}, but start and/or end position is missing!"
                    + "Instantly ending link movement!");
                LinkEnd();
                return;
            }
            
            // Get animation for this traversal
            _linkTraverseAnimation = _linkData.isReverse
                ? SettingsManager.humanoidAnimation.defaultLadderDownAnimation
                : SettingsManager.humanoidAnimation.defaultLadderUpAnimation;
            if (_linkExitPosition == default)
            {
                Debug.LogWarning(
                    $"{parent.name} tried to use {nameof(NavMeshLink)}, but traverse animation loop is missing!"
                    + "Instantly ending link movement!");
                LinkEnd();
                return;
            }
            
            parent.unitAnimator.SetLoopAnimation(AnimationType.Traverse, _linkTraverseAnimation);
            _navMeshAgent.updateRotation = false;
            parent.unitState.isTraversing = true;
            parent.unitAnimator.applyRootMotion = true;

            // Finds:
            // 1. Starting point for start animation
            // 2. Starting and ending point for loop animation
            FindLinkPath();

            _linkCoroutine = parent.StartCoroutine(LinkTraverse());
        }

        private IEnumerator LinkTraverse()
        {
            yield return LinkTraverse_Start();
            yield return LinkTraverse_LoopStart();
            yield return LinkTraverse_Loop();
            yield return LinkTraverse_LoopEnd();
            yield return LinkTraverse_End();
            
            LinkEnd();
        }

        private IEnumerator LinkTraverse_Start()
        {
            var transitionFrames = Mathf.RoundToInt(0.1f / Time.fixedDeltaTime);
            
            var fromPosition = parent.transform.position;
            var fromRotation = parent.transform.rotation;

            var toPosition = _linkPathStart;
            var toRotation = _linkData.traversable.transform.rotation;
            
            for (var i = 1; i <= transitionFrames; i++)
            {
                yield return new WaitForFixedUpdate();
                parent.transform.position += (toPosition - fromPosition) / transitionFrames;
                parent.transform.rotation = Quaternion.Slerp(fromRotation, toRotation, (float)i / transitionFrames);
            }
        }
        
        private IEnumerator LinkTraverse_LoopStart()
        {
            parent.unitAnimator.StartLoopAnimation(new AnimationGenericEvent
                { animationType = AnimationType.Traverse, unit = parent });

            yield return new WaitForSeconds(_linkTraverseAnimation.list.startClip.length);
        }

        private IEnumerator LinkTraverse_Loop()
        {
            var linkPathDistance = Vector3.Distance(_linkPath[0], _linkPath[^1]);
            var loopSpeedY = _linkTraverseAnimation.list.loopClip.averageSpeed.y;
            var loopLength = Mathf.Abs(linkPathDistance / loopSpeedY);
            yield return new WaitForSeconds(loopLength);
        }

        private IEnumerator LinkTraverse_LoopEnd()
        {
            parent.unitAnimator.EndLoopAnimation();

            yield return new WaitForSeconds(_linkTraverseAnimation.list.endClip.length);
        }

        private IEnumerator LinkTraverse_End()
        {
            var transitionFrames = Mathf.RoundToInt(0.1f / Time.fixedDeltaTime);
            
            var fromPosition = parent.transform.position;
            var fromRotation = parent.transform.rotation;

            var toPosition = _linkExitPosition;
            var toRotation = _linkData.traversable.transform.rotation;
            
            for (var i = 1; i <= transitionFrames; i++)
            {
                yield return new WaitForFixedUpdate();
                parent.transform.position += (toPosition - fromPosition) / transitionFrames;
                parent.transform.rotation = Quaternion.Slerp(fromRotation, toRotation, (float)i / transitionFrames);
            }
        }

        /// <summary>
        /// Finish link movement. No matter if its valid or not.
        /// </summary>
        private void LinkEnd()
        {
            _navMeshAgent.updateRotation = true;
            _navMeshAgent.CompleteOffMeshLink();
            parent.unitAnimator.EndLoopAnimation();
            parent.unitState.isTraversing = false;
            parent.unitAnimator.applyRootMotion = false;
            if (_linkCoroutine != null)
            {
                parent.StopCoroutine(_linkCoroutine);
                _linkCoroutine = null;
            }
        }

        private void FindLinkPath()
        {
            // Unit's offset to traversable
            var ladderOffset = Vector3.Scale(parent.unitData.ladderPosition.localPosition, new Vector3(1, 1, - 1));
            var ladderOffsetVector = _linkData.traversable.transform.TransformDirection(ladderOffset);
            
            // Whole traversable path with offset added
            _linkPath = _linkData.traversable.traversableData.path
                .Select(transform => transform.position + ladderOffsetVector)
                .ToList();
            if (_linkData.isReverse) _linkPath.Reverse();

            var startOffset = _linkData.traversable.transform.TransformDirection(
                _linkTraverseAnimation.list.startClip.averageSpeed * _linkTraverseAnimation.list.startClip.length);
            var endOffset = _linkData.traversable.transform.TransformDirection(
                _linkTraverseAnimation.list.endClip.averageSpeed * _linkTraverseAnimation.list.endClip.length);

            // Path for loop animation
            _linkPath[0] += new Vector3(0, startOffset.y, 0);
            _linkPath[^1] -= new Vector3(0, endOffset.y, 0);
            
            // Starting point for animation
            _linkPathStart = _linkPath[0] - startOffset;
            
            Debug.DrawLine(_linkPathStart, _linkPath[0], Color.red, 5f);
            Debug.DrawLine(_linkPath[0], _linkPath[^1], Color.green, 5f);
            Debug.DrawLine(_linkPath[^1], _linkPath[^1] + endOffset, Color.blue, 5f);
        }

        /// <summary>
        /// Returns next link to use from generated path.
        /// </summary>
        private PathCorrectedLinkData GetLinkData()
        {
            var currentIndex = _pathCorrected.traversables.IndexOf(_linkData);
            return _pathCorrected.traversables.ElementAtOrDefault(currentIndex + 1);
        }

        #endregion
    }
}