using System;
using System.Collections.Generic;
using Intrepid.Core.Entities.Messages;
using Intrepid.Core.Grounding;
using Intrepid.Diagnostics;
using Intrepid.GameManagers;
using Intrepid.Utilities;
using Sirenix.OdinInspector;
using UnityEngine;

namespace Intrepid.Core.Entities
{
    public sealed class EntityMotorTopDown : MonoBehaviour
    {
        private const float FlatGroundNormalThreshold = 0.999f;

        [GroupInjections, SerializeField] private Entity entity;
        [GroupConfigurations, SerializeField] private LayerMask groundMask;

        [GroupConfigurations, SerializeField] private bool inheritGroundMotionY;
        [GroupConfigurations, SerializeField] private bool preserveGroundVelocityWhenUngrounded = true;
        [GroupConfigurations, SerializeField] private float inheritedGroundVelocityDuration = 0.18f;
        [GroupConfigurations, SerializeField] private float inheritedGroundVelocityDecay = 30f;
        [GroupConfigurations, SerializeField] private float groundCheckHeight = 1.0f;
        [GroupConfigurations, SerializeField] private float groundCheckDistance = 3.0f;
        [GroupConfigurations, SerializeField] private float groundOffset;
        [GroupConfigurations, SerializeField] private float maxSlopeAngle = 45f;
        [GroupConfigurations, SerializeField] private float groundProbeRadiusGrounded = 0.5f;
        [GroupConfigurations, SerializeField] private float groundProbeRadiusFalling = 0.2f;
        [GroupConfigurations, SerializeField] private float fallingGroundCheckDistance = 150f;
        [GroupConfigurations, SerializeField] private float minFallHeight = 0.25f;
        [GroupConfigurations, SerializeField] private float fallAcceleration = 35f;
        [GroupConfigurations, SerializeField] private float maxFallSpeed = 18f;

        private readonly List<RaycastHit> _groundHits = new(16);
        private RaycastHit _groundHit;
        private Vector3 Origin { get; set; }
        private Vector3 End { get; set; }
        private bool HasHit { get; set; }
        private Vector3 _inheritedGroundVelocity;
        private float _fallVerticalSpeed;

        private float GroundProbeRadius => IsFalling || IsFallingToAbyss
            ? groundProbeRadiusFalling
            : groundProbeRadiusGrounded;

        public GroundProfiles CurrentGroundProfiles { get; private set; } = GroundProfiles.None;
        public float MaxSlopeAngle => maxSlopeAngle;

        [GroupDebug, ShowInInspector, ReadOnly]
        public Vector3 LastSafeGroundPosition { get; private set; }

        [GroupDebug, ShowInInspector, ReadOnly]
        public bool HasStableGroundNormal { get; private set; }

        [GroupDebug, ShowInInspector, ReadOnly]
        public Vector3 StableGroundNormal { get; private set; }

        [GroupDebug, ShowInInspector, ReadOnly]
        public bool IsFalling { get; private set; }

        [GroupDebug, ShowInInspector, ReadOnly]
        public bool IsFallingToAbyss { get; private set; }

        [GroupDebug, ShowInInspector, ReadOnly]
        public bool IsGrounded { get; private set; }

        [GroupDebug, ShowInInspector, ReadOnly]
        public Vector3 InheritedGroundVelocity => _inheritedGroundVelocity;

        [GroupDebug, ShowInInspector, ReadOnly]
        public float InheritedGroundVelocityTimer { get; private set; }

        public void ResetFallingToAbyss()
        {
            IsFalling = false;
            IsFallingToAbyss = false;
            CurrentGroundProfiles = GroundProfiles.None;
            ClearInheritedGroundVelocity();
        }

        public void Move(
            Rigidbody body,
            Vector3 velocity,
            float deltaTime,
            EntityMotorMoveOptions options)
        {
            var current = body.position;

            if (IsFallingToAbyss)
            {
                return;
            }

            if (IsFalling)
            {
                var nextFalling = ResolveFallingPosition(current, deltaTime);
                ApplyMove(body, nextFalling);
                return;
            }

            var movementVelocity = ResolveMovementVelocity(velocity);
            var groundMotionDelta = ResolveGroundMotionDelta(deltaTime);

            var next = current
                       + groundMotionDelta
                       + movementVelocity * deltaTime;

            if (TryGetGroundSurfaceAt(next, out _groundHit, out var nextNormal, out var groundProfiles))
            {
                IsGrounded = true;
                IsFallingToAbyss = false;

                CurrentGroundProfiles = groundProfiles;

                RegisterStableGroundNormal(nextNormal);
                RefreshInheritedGroundVelocityFrom(groundProfiles);

                next.y = _groundHit.point.y + groundOffset;

                if (CurrentGroundProfiles.IsSafe)
                {
                    LastSafeGroundPosition = new Vector3(
                        next.x,
                        next.y,
                        next.z
                    );
                }
            }
            else if (options.KeepHeightWhenNoGround)
            {
                IsGrounded = false;
                IsFalling = false;
                IsFallingToAbyss = false;

                // Important:
                // We are no longer grounded, so the current ground profile must be cleared.
                // But inherited ground velocity is intentionally preserved for game feel.
                CurrentGroundProfiles = GroundProfiles.None;
            }
            else if (TryAnyGroundSurfaceBelowAt(next, out var fallHit, out _, out _)
                     && IsGroundMeaningfullyBelow(current, fallHit))
            {
                BeginFall();
                next = current;
            }
            else
            {
                BeginFallToAbyss();
                next = current;
            }

            ApplyMove(body, next);
        }

        private Vector3 ResolveGroundMotionDelta(float deltaTime)
        {
            if (IsGrounded && CurrentGroundProfiles.TryGetGroundProfileMotion(out var motion))
            {
                var delta = motion.DeltaPosition;

                CaptureInheritedGroundVelocity(motion.Velocity);

                if (inheritGroundMotionY)
                {
                    return delta;
                }

                return new Vector3(delta.x, 0f, delta.z);
            }

            return ResolveInheritedGroundMotionDelta(deltaTime);
        }

        private Vector3 ResolveInheritedGroundMotionDelta(float deltaTime)
        {
            if (!preserveGroundVelocityWhenUngrounded)
            {
                return Vector3.zero;
            }

            if (InheritedGroundVelocityTimer <= 0f
                || _inheritedGroundVelocity.sqrMagnitude <= GonConstants.CommonZeroSqrt)
            {
                ClearInheritedGroundVelocity();
                return Vector3.zero;
            }

            var delta = _inheritedGroundVelocity * deltaTime;

            InheritedGroundVelocityTimer = Mathf.Max(
                0f,
                InheritedGroundVelocityTimer - deltaTime
            );

            _inheritedGroundVelocity = Vector3.MoveTowards(
                _inheritedGroundVelocity,
                Vector3.zero,
                inheritedGroundVelocityDecay * deltaTime
            );

            if (inheritGroundMotionY)
            {
                return delta;
            }

            return new Vector3(delta.x, 0f, delta.z);
        }

        private void CaptureInheritedGroundVelocity(Vector3 velocity)
        {
            if (!preserveGroundVelocityWhenUngrounded)
            {
                return;
            }

            if (!inheritGroundMotionY)
            {
                velocity.y = 0f;
            }

            _inheritedGroundVelocity = velocity;
            InheritedGroundVelocityTimer = inheritedGroundVelocityDuration;
        }

        private void RefreshInheritedGroundVelocityFrom(GroundProfiles groundProfiles)
        {
            if (groundProfiles.TryGetGroundProfileMotion(out var motion))
            {
                CaptureInheritedGroundVelocity(motion.Velocity);
                return;
            }

            ClearInheritedGroundVelocity();
        }

        private void ClearInheritedGroundVelocity()
        {
            _inheritedGroundVelocity = Vector3.zero;
            InheritedGroundVelocityTimer = 0f;
        }

        private Vector3 ResolveFallingPosition(Vector3 current, float deltaTime)
        {
            IsGrounded = false;
            IsFallingToAbyss = false;

            var inheritedMotionDelta = ResolveInheritedGroundMotionDelta(deltaTime);

            _fallVerticalSpeed = Mathf.Min(
                _fallVerticalSpeed + fallAcceleration * deltaTime,
                maxFallSpeed
            );

            var next = current + inheritedMotionDelta;
            next.y -= _fallVerticalSpeed * deltaTime;

            if (!TryAnyGroundSurfaceBelowAt(
                    next,
                    out var fallHit,
                    out var fallNormal,
                    out var fallProfiles))
            {
                BeginFallToAbyss();
                return current;
            }

            var targetY = fallHit.point.y + groundOffset;

            if (next.y > targetY)
            {
                return next;
            }

            next.y = targetY;

            RegisterStableGroundNormal(fallNormal);

            IsGrounded = true;
            IsFalling = false;
            IsFallingToAbyss = false;
            _fallVerticalSpeed = 0f;

            CurrentGroundProfiles = fallProfiles;
            RefreshInheritedGroundVelocityFrom(fallProfiles);

            if (CurrentGroundProfiles.IsSafe)
            {
                LastSafeGroundPosition = new Vector3(
                    next.x,
                    next.y,
                    next.z
                );
            }

            Proxy.Instance.EventBus.Raise(new EntityRecoverStableGroundEvent
            {
                EntityId = entity.Id,
            });

            return next;
        }

        private void BeginFall()
        {
            if (IsFalling)
            {
                return;
            }

            IsGrounded = false;
            IsFalling = true;
            IsFallingToAbyss = false;
            _fallVerticalSpeed = 0f;

            // We clear the contact, but not the inherited velocity.
            // This gives a short carry from the moving platform after leaving it.
            CurrentGroundProfiles = GroundProfiles.None;

            Proxy.Instance.EventBus.Raise(new EntityLeaveStableGroundEvent
            {
                EntityId = entity.Id,
            });
        }

        private void BeginFallToAbyss()
        {
            if (IsFallingToAbyss)
            {
                return;
            }

            DLogger.LogDebug("Real abyss fall");

            IsGrounded = false;
            IsFalling = false;
            IsFallingToAbyss = true;
            _fallVerticalSpeed = 0f;

            CurrentGroundProfiles = GroundProfiles.None;
            ClearInheritedGroundVelocity();

            Proxy.Instance.EventBus.Raise(new EntityFaillingToAbyssEvent
            {
                EntityId = entity.Id,
            });
        }

        private static void ApplyMove(Rigidbody body, Vector3 next)
        {
            body.linearVelocity = Vector3.zero;
            body.angularVelocity = Vector3.zero;
            body.MovePosition(next);
        }

        private bool IsGroundMeaningfullyBelow(Vector3 current, RaycastHit hit)
        {
            return hit.point.y + groundOffset < current.y - minFallHeight;
        }

        public void RefreshStableGroundNormalAt(Vector3 position)
        {
            if (!TryGetGroundSurfaceAt(position, out _, out var normal, out var groundProfiles))
            {
                return;
            }

            if (groundProfiles.IsSafe)
            {
                RegisterStableGroundNormal(normal);
            }
        }
        
        private Vector3 ResolveMovementVelocity(Vector3 velocity)
        {
            if (velocity.sqrMagnitude <= GonConstants.CommonZeroSqrt)
            {
                return Vector3.zero;
            }

            if (!HasStableGroundNormal)
            {
                return velocity;
            }

            var projectedVelocity = ProjectVelocityOnPlane(
                velocity,
                StableGroundNormal
            );

            return ApplyGroundMovementProfile(
                projectedVelocity,
                StableGroundNormal
            );
        }
        
        private Vector3 ApplyGroundMovementProfile(
            Vector3 velocity,
            Vector3 groundNormal)
        {
            if (!IsGrounded || !CurrentGroundProfiles.TryGetGroundProfileMovement(out var movement))
            {
                return velocity;
            }

            if (velocity.sqrMagnitude <= GonConstants.CommonZeroSqrt)
            {
                return Vector3.zero;
            }

            var downhill = Vector3.ProjectOnPlane(
                Vector3.down,
                groundNormal.normalized
            );

            if (downhill.sqrMagnitude <= GonConstants.CommonZeroSqrt)
            {
                return velocity * movement.SpeedMultiplier;
            }

            downhill.Normalize();

            var moveDirection = velocity.normalized;

            var downhillFactor = Vector3.Dot(moveDirection, downhill);

            float directionalMultiplier;

            if (downhillFactor > 0f)
            {
                directionalMultiplier = Mathf.Lerp(
                    movement.SidewaysSpeedMultiplier,
                    movement.DownhillSpeedMultiplier,
                    downhillFactor
                );
            }
            else
            {
                directionalMultiplier = Mathf.Lerp(
                    movement.SidewaysSpeedMultiplier,
                    movement.UphillSpeedMultiplier,
                    -downhillFactor
                );
            }

            return velocity * (movement.SpeedMultiplier * directionalMultiplier);
        }

        private void RegisterStableGroundNormal(Vector3 normal)
        {
            if (normal.sqrMagnitude <= GonConstants.CommonZeroSqrt)
            {
                return;
            }

            StableGroundNormal = normal.normalized;
            HasStableGroundNormal = true;
        }

        public bool HasGroundSurfaceAt(Vector3 position)
        {
            return TryGetGroundSurfaceAt(position, out _, out _, out _);
        }

        private bool TryGetBestGroundSurfaceHit(
            Vector3 position,
            float checkDistance,
            out RaycastHit hit,
            out Vector3 normal,
            out GroundProfiles groundProfiles)
        {
            hit = default;
            normal = default;
            groundProfiles = GroundProfiles.None;

            Origin = position + Vector3.up * groundCheckHeight;
            End = Origin + Vector3.down * checkDistance;

            _groundHits.Clear();

            var hitCount = PhysicUtils.SphereCast3DAll(
                Origin,
                GroundProbeRadius,
                Vector3.down,
                _groundHits,
                tags: null,
                maxDistance: checkDistance,
                layerMask: groundMask,
                query: QueryTriggerInteraction.Ignore
            );

            HasHit = hitCount > 0;

            if (hitCount == 0)
            {
                return false;
            }

            var found = false;

            var bestHit = default(RaycastHit);
            var bestNormal = Vector3.zero;
            var bestProfiles = GroundProfiles.None;

            var bestIsSlope = false;
            var bestDistance = float.PositiveInfinity;

            foreach (var candidateHit in _groundHits)
            {
                if (candidateHit.collider.IsNull())
                {
                    continue;
                }

                var surface = candidateHit.collider.GetComponentInParent<GroundSurface>();

                if (surface.IsNull())
                {
                    continue;
                }

                var candidateNormal = surface.Normal;

                if (!IsWalkable(candidateNormal))
                {
                    continue;
                }

                var candidateIsSlope = IsSlope(candidateNormal);
                var candidateDistance = candidateHit.distance;

                var shouldReplace =
                    !found ||

                    // any walkable slope wins a plane
                    candidateIsSlope && !bestIsSlope ||

                    // if both are slopes or planes take the closest one
                    candidateIsSlope == bestIsSlope &&
                    candidateDistance < bestDistance;

                if (!shouldReplace)
                {
                    continue;
                }

                found = true;

                bestHit = candidateHit;
                bestNormal = candidateNormal;
                bestProfiles = surface.GroundProfiles;

                bestIsSlope = candidateIsSlope;
                bestDistance = candidateDistance;
            }

            if (!found)
            {
                return false;
            }

            hit = bestHit;
            normal = bestNormal;
            groundProfiles = bestProfiles;

            return true;
        }
        
        private bool TryGetGroundSurfaceAt(
            Vector3 position,
            out RaycastHit hit,
            out Vector3 normal,
            out GroundProfiles groundProfiles)
        {
            return TryGetBestGroundSurfaceHit(
                position,
                groundCheckDistance,
                out hit,
                out normal,
                out groundProfiles
            );
        }

        private bool TryGetGroundAt(Vector3 position, out RaycastHit hit)
        {
            return TryGetBestGroundSurfaceHit(
                position,
                groundCheckDistance,
                out hit,
                out _,
                out _
            );
        }

        private bool TryAnyGroundSurfaceBelowAt(
            Vector3 position,
            out RaycastHit hit,
            out Vector3 normal,
            out GroundProfiles groundProfiles)
        {
            return TryGetBestGroundSurfaceHit(
                position,
                fallingGroundCheckDistance,
                out hit,
                out normal,
                out groundProfiles
            );
        }

        private static bool IsSlope(Vector3 normal)
        {
            return normal.y < FlatGroundNormalThreshold;
        }

        private bool IsWalkable(Vector3 normal)
        {
            var angle = Vector3.Angle(normal, Vector3.up);
            return angle <= maxSlopeAngle;
        }

        private static Vector3 ProjectVelocityOnPlane(
            Vector3 velocity,
            Vector3 planeNormal)
        {
            var projected = Vector3.ProjectOnPlane(
                velocity,
                planeNormal.normalized
            );

            if (projected.sqrMagnitude <= GonConstants.CommonZeroSqrt)
            {
                return Vector3.zero;
            }

            return projected.normalized * velocity.magnitude;
        }

#if UNITY_EDITOR
        private void OnDrawGizmosSelected()
        {
            Gizmos.color = HasHit ? Color.green : Color.red;

            Gizmos.DrawWireSphere(Origin, GroundProbeRadius);
            Gizmos.DrawWireSphere(End, GroundProbeRadius);

            Gizmos.DrawLine(
                Origin + Vector3.right * GroundProbeRadius,
                End + Vector3.right * GroundProbeRadius
            );

            Gizmos.DrawLine(
                Origin - Vector3.right * GroundProbeRadius,
                End - Vector3.right * GroundProbeRadius
            );

            Gizmos.DrawLine(
                Origin + Vector3.forward * GroundProbeRadius,
                End + Vector3.forward * GroundProbeRadius
            );

            Gizmos.DrawLine(
                Origin - Vector3.forward * GroundProbeRadius,
                End - Vector3.forward * GroundProbeRadius
            );

            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(Origin, End);

            if (HasHit)
            {
                Gizmos.color = Color.cyan;
                Gizmos.DrawSphere(_groundHit.point, 0.06f);

                Gizmos.color = Color.magenta;
                Gizmos.DrawLine(
                    _groundHit.point,
                    _groundHit.point + _groundHit.normal * 0.75f
                );

                Gizmos.color = Color.blue;
                var sphereAtHit = Origin + Vector3.down * _groundHit.distance;
                Gizmos.DrawWireSphere(sphereAtHit, GroundProbeRadius);
            }

            if (HasStableGroundNormal)
            {
                Gizmos.color = Color.white;
                Gizmos.DrawLine(
                    transform.position,
                    transform.position + StableGroundNormal * 1.25f
                );
            }

            if (_inheritedGroundVelocity.sqrMagnitude > GonConstants.CommonZeroSqrt)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawLine(
                    transform.position,
                    transform.position + _inheritedGroundVelocity.normalized * 1.5f
                );
            }
        }
#endif
    }
}