using System;
using UniRx;
using UnityEngine;

namespace SpringConstrainedPickup
{
    /// <summary>
    /// 
    /// The SpringUtil class is attached to the anchor of the spring, i.e. the fix part of the spring.
    /// Here we define
    /// - the callback action for the scenario of the spring breaking.
    /// - the LineRenderer to display the spring (+ visually show when it will break)
    /// 
    /// </summary>
    [RequireComponent(typeof(SpringJoint2D))]
    [RequireComponent(typeof(LineRenderer))]
    public class SpringUtil : MonoBehaviour
    {
        [SerializeField] private SpringJoint2D springJoint2D;
        [SerializeField] private float dampingValueOnBreak;
        [SerializeField] private Rigidbody2D rigidbody2D;
        public Rigidbody2D connectedRigidBody2D;
        private Pickup pickup;
        
        // LineRenderer
        [SerializeField] private LineRenderer lineRenderer;
        [SerializeField] private Color defaultColor;
        [SerializeField] private Color breakColor;

        // these are the values for the line end, giving a stretch effect on the line.
        [SerializeField] private float lineMinWidth;
        [SerializeField] private float lineMaxWidth;
        
        // particle system, for when the spring breaks
        [SerializeField] private ParticleSystem springBrokePS; 
        
        // We need a reference to grandma to know when she resolves something that can be moved
        [SerializeField] private GrandmaAI grandmaAI;
        
        private void Awake()
        {
            springJoint2D = GetComponent<SpringJoint2D>();
            lineRenderer = GetComponent<LineRenderer>();
            Vector3 defaultVec = new Vector3(0, 0, 0);
            Vector3[] points = {defaultVec, defaultVec};
            lineRenderer.SetPositions(points);
            AnimationCurve curve = new AnimationCurve();
            curve.AddKey(0.0f, lineMaxWidth);  // Start of line
            curve.AddKey(1.0f, lineMaxWidth);  // End of line

            // Handling the error, where grandma picks up a banana, which is dragged by the player:
            grandmaAI = FindFirstObjectByType<GrandmaAI>(); // there should be only one GrandmaAI in the game
            grandmaAI.resolveBanana.Subscribe(resolved =>
            {
                if (resolved)
                {
                    DisableLineRenderer();
                    grandmaAI.resolveBanana.Value = false;
                } 
            });

        }
        
        private void Start()
        {
            connectedRigidBody2D = springJoint2D.connectedBody;
            pickup = connectedRigidBody2D.GetComponent<Pickup>();
        }

        private Color currentSpringColor;
        
        private void Update()
        {
            if (!pickup.springBroke && springJoint2D.connectedBody != null && connectedRigidBody2D != null)
            {
                Debug.Log("Doing the color interpolation");
                float totalForce = ComputeSpringForce() * 0.25f;
                ColorInterpolation(totalForce);
                LineWidthInterpolation(totalForce);
            }
            else
            {
                Debug.Log($"springJoint2D.connectedBody != null: {springJoint2D.connectedBody != null}");
                Debug.Log($"connectedRigidBody2D != null: {connectedRigidBody2D != null}");
                Debug.Log("Cannot do the color interpolation");
            }
        }

        public void SetConnectedRigidBody(Rigidbody2D rigidbody2DConnect)
        {
            connectedRigidBody2D = rigidbody2DConnect;
        }

        public void SetPickup(Pickup p)
        {
            pickup = p;
        }
        
        /// <summary>
        /// 
        /// Computing spring force manually, as GetReactionForce(float) is returning 0 all the time.
        /// (Maybe because the settings use very small numbers, as the set distance is supposed to be near to 0,
        /// for the drag effect.)
        ///
        /// Using F = -kx -bv, with
        ///
        /// k: stiffness coefficient
        /// x: vector of displacement
        /// b: damping coefficient
        /// v: relative velocity between anchor and spring end
        ///
        /// https://gafferongames.com/post/spring_physics/
        /// https://en.wikipedia.org/wiki/Damping
        /// https://en.wikipedia.org/wiki/Angular_frequency
        /// 
        /// </summary>
        /// <returns></returns>
        private float ComputeSpringForce()
        {
            // Get positions
            Vector2 posA = transform.position;
            Vector2 posB = connectedRigidBody2D.transform.position;
                
            // Calculate current distance and displacement
            float currentDistance = Vector2.Distance(posA, posB);
            float displacement = currentDistance - springJoint2D.distance;
                
            // Calculate spring direction (normalized)
            Vector2 springDirection = (posB - posA).normalized;
                
            // Get velocities
            Vector2 velocityA = rigidbody2D.linearVelocity;
            Vector2 velocityB = connectedRigidBody2D.linearVelocity;
                
            // Calculate relative velocity along spring direction
            Vector2 relativeVelocity = velocityB - velocityA;
            float relativeVelocityAlongSpring = Vector2.Dot(relativeVelocity, springDirection);
                
            // Calculate spring parameters
            float mass = connectedRigidBody2D.mass;
            float frequency = springJoint2D.frequency;
            float dampingRatio = springJoint2D.dampingRatio;
            float omega = 2f * Mathf.PI * frequency; // omega: angular frequency
                
            // Calculate spring constant k and damping coefficient β
            float k = mass * omega * omega;
            float beta = 2f * mass * dampingRatio * omega;
                
            // Calculate total force: F = kx + βv
            float springForce = k * Mathf.Abs(displacement);
            float dampingForce = beta * Mathf.Abs(relativeVelocityAlongSpring);
            float totalForce = springForce + dampingForce;

            return totalForce;
        }

        private void ColorInterpolation(float currentForce)
        {
            // Calculate ratio for color interpolation
            float ratio = Mathf.Clamp01(currentForce / springJoint2D.breakForce);
                
            // Update line renderer color
            lineRenderer.startColor = defaultColor;
            lineRenderer.endColor = Color.Lerp(defaultColor, breakColor, ratio);
        }

        private void LineWidthInterpolation(float currentForce)
        {
            // Calculate ratio for width interpolation
            float ratio = Mathf.Clamp01(currentForce / springJoint2D.breakForce);

            // Interpolate between min and max width
            float widthValue = Mathf.Lerp(lineMaxWidth,lineMinWidth, ratio);
    
            // Create a new AnimationCurve and assign it
            AnimationCurve curve = new AnimationCurve();
            curve.AddKey(0.0f, lineMaxWidth);  // Start of line
            curve.AddKey(1.0f, widthValue);  // End of line
    
            lineRenderer.widthCurve = curve;
        }
        
        private void FixedUpdate()
        {
            Debug.Log($"pickup != null: {pickup != null}");
            Debug.Log($"!pickup.springBroke: {!pickup.springBroke}");
            // Debug.Log($"springJoint2D.reactionForce.magnitude: = {springJoint2D.reactionForce.magnitude:F6}");
            if (pickup != null && !pickup.springBroke)
            {
                // this call might be not necessary here: 
                connectedRigidBody2D = springJoint2D.connectedBody;
                if (connectedRigidBody2D != null)
                {
                    pickup = connectedRigidBody2D.GetComponent<Pickup>();
                }
                else
                {
                    DisableLineRenderer();
                }

                Debug.Log("Set Start and End");
                
                Vector3 start = transform.position;
                Vector3 end = connectedRigidBody2D.transform.position;
                lineRenderer.SetPosition(0, start);
                lineRenderer.SetPosition(1, end);
                
            }
        }

        private void OnJointBreak2D(Joint2D brokenJoint)
        {
           
            AudioManager.Instance.PlaySpringSnap();
            
            pickup.springBroke = true;
            
            // spawn particle system towards the direction were the reaction force has been applied
            Vector2 breakDirection = brokenJoint.reactionForce * 200;
            springBrokePS.transform.position = connectedRigidBody2D.transform.position + new Vector3(breakDirection.x, breakDirection.y, 0);
            var vel = springBrokePS.velocityOverLifetime;
            vel.x = breakDirection.x * 15;
            vel.y = breakDirection.y * 15;
            springBrokePS.Play();
            
            // when the spring breaks, we increase the linear damping of the loose part, so it does not move too much
            springJoint2D.connectedBody.linearDamping = dampingValueOnBreak;
            
            springJoint2D.connectedBody = null;
            gameObject.SetActive(false);
        }

        public void OnJointRelease()
        {
            springJoint2D.connectedBody.linearDamping = dampingValueOnBreak;
            
            pickup.springBroke = true;
            
            springJoint2D.connectedBody = null;
            gameObject.SetActive(false);
        }
        
        public void EnableLineRenderer()
        {
            lineRenderer.enabled = true;
        }

        public void DisableLineRenderer()
        {
            lineRenderer.enabled = false;
            gameObject.SetActive(false);
        }

        
    }
}
