using System;
using NUnit.Framework.Internal;
using UnityEngine;
using ParadoxNotion.Design;
using Sirenix.OdinInspector;
using UnityEngine.Serialization;
using Quaternion = UnityEngine.Quaternion;

public enum PlayerFacingDirection{
    TowardsTarget = 0,
    MatchForward,
    MatchCamera,
    Static,
    Momentum,
    SpecifiedDirection
}

namespace Reset.Units{
    // public interface IBuffSource{ - Unused good idea?
    //     public Object sourceObject();
    //     public 
    // }

    [Serializable]
    public class UnitMovementData : ICloneable{
        // Movement Direction
        public float accelerationSmoothing = 5f;
        public float deaccelerationSmoothing = 5f;
        // public AnimationCurve deaccelerationCurve; // Currently unused, may return

        [SliderField(0,1)]
        public float airDirectionDecay;
        
        // Move Speed
        public float moveSpeed = 15f;
        public float moveSpeedSoothing = 10f;
        
        // Jumping
        [ShowInInspector, ReadOnly] public float jumpPower;
        public float jumpPowerDecay = 3f;
    
        // Gravity
        [ShowInInspector, ReadOnly] public float gravityPower;
        public float gravityMax = 8f;
        public float gravityAcceleration = 1f;
        public float gravityScale = 1f;
        public float settingsChangeSmoothing = 6f;
        
        // Rotation
        [ShowInInspector, SerializeReference]
        public Enum rotateFacing;
        [FormerlySerializedAs("rotationSpeedTarget")] public float rotationSpeed = 5f;
        public float rotationSmoothing = 1f;
        public float rotationInputBlending = .3f;

        public object Clone(){
            return MemberwiseClone();
        }
    }
    
    public class UnitMovementHandler : MonoBehaviour{
        // class MovementFloatModifier{
        //     // IBuffSource source 
        //     public float value;
        // }
        
        // 
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] private float outputSpeed;
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] private float additionalSpeed;
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] public Vector3 outputMoveDirection;
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] public Vector3 additionalMoveDirection;
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] private Quaternion outputRotation;
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] private Quaternion specifiedRotation;
        [FoldoutGroup("Final Values"), ShowInInspector, ReadOnly] private float outputRotationSpeed;

        // Used by graph to gradually shift into new values rather than dump them. Even if they're smoothed values they need to be eased into
        [FoldoutGroup("Smoothed Values"), ShowInInspector, ReadOnly] private float smoothedJumpDecay;
        [FoldoutGroup("Smoothed Values"), ShowInInspector, ReadOnly] private float smoothedGravityAccel;
        [FoldoutGroup("Smoothed Values"), ShowInInspector, ReadOnly] private float smoothedGravityScale;
        [FoldoutGroup("Smoothed Values"), ShowInInspector, ReadOnly] private float settingsChangeRotationSpeed;

        private float directionChangeDot;
        private bool moveCallDisabledNextFrame;

        private bool movedThisFrame;
        
        private CharacterController controller;
        private PlayerControls controls;
        private LockOnManager lockOnManager;

        private Vector3 moveSmooth;
        public float gravitySmooth;

        private bool relativeToCamera;
        
        [ShowInInspector, PropertyOrder(2)]
        public UnitMovementData data = new();

        [HideInInspector]
        public UnitMovementData defaultData;
        
        void Awake(){
            controller = GetComponent<CharacterController>();
            controls = GetComponent<PlayerControls>();
            lockOnManager = GetComponent<LockOnManager>();
        }

        private void Start(){
            defaultData = (UnitMovementData)data.Clone();
        }

        void Update(){
            UpdateCurrentDirection();
            UpdateCurrentGravity();
            UpdateCurrentSpeed();
            
            UpdateCurrentRotation();
            
            DoMovement();
        }

        // Add directly to the direction
        public void AddToCurrentDirection(Vector3 inputDirection, float power){
            additionalMoveDirection += inputDirection.normalized;
            additionalSpeed = power;
        }
        
        public void SmoothToSpeed(float desiredSpeed, float smoothing){
            additionalSpeed = Mathf.Lerp(additionalSpeed, desiredSpeed, smoothing * Time.deltaTime);
        }
        
        public void SetNewDirection(Vector3 inputDirection){ // NOTE: If smoothing desired add a default bool for smoothing maybe?
            additionalMoveDirection = inputDirection.Flatten(null, 0f, null);
        }

        public void SetNewGravity(float value){
            additionalMoveDirection.y = value;
        }

        // Hold a new rotation to be moved to during PlayerFacingDirection.SpecifiedRotation
        public void SetSpecifiedRotation(Quaternion inputRotation){
            specifiedRotation = inputRotation;
        }

        // Blend between the current direction and an input direction, based on the current controller input
        public void OverwriteDirectionFromInput(Vector2 value, float priority, float speed = Mathf.Infinity){
            // Create a new direction that is the current controller input * the amount of power they should have
            Vector3 dirToAdd = new Vector3(controls.rawMoveInput.x * value.x, 0f, controls.rawMoveInput.y * value.y);
            
            // Multiply it by the current magnitude (why? i forgor)
            dirToAdd *= controls.rawMoveInput.magnitude;
            
            // Blend the existing direction into it
            dirToAdd = Vector3.Lerp(outputMoveDirection, dirToAdd, priority * controls.rawMoveInput.magnitude);
            
            // Set the new direction
            outputMoveDirection = new Vector3(dirToAdd.x, outputMoveDirection.y, dirToAdd.z);

            // Everthing under here is for the speed now. If it's not set do...nothing. Otherwise set it to the max value between move speed and the new speed
            if (float.IsPositiveInfinity(speed)) {
                return;
            }
            
            outputSpeed = Mathf.Lerp(outputSpeed, Mathf.Max(data.moveSpeed, speed), priority);
        }
        
        // Update the direction, called every frame
        private void UpdateCurrentDirection(){
            // Get input value
            Vector3 inputMovement = new Vector3(controls.rawMoveInput.x, 0f, controls.rawMoveInput.y);
            
            // Construct move direction
            Vector3 targetDirection = inputMovement;
            
            // Deadzone
            if (inputMovement.magnitude < .05f) {
                targetDirection = Vector3.zero;
            }

            // Remove Y from variables
            Vector3 targetNoY = new Vector3(targetDirection.x, 0f, targetDirection.z);
            Vector3 currentNoY = new Vector3(outputMoveDirection.x, 0f, outputMoveDirection.z);
			
            // Also need to find the dot value of the current input versus the current move direction
            Vector3 slerpedValue;
            Vector3 lerpedValue;
            
            float switchedDirection = Vector3.Dot(inputMovement, outputMoveDirection);
            float switchedDirectionRemapped = Mathf.Lerp(0, 1, switchedDirection);

            directionChangeDot = Mathf.Lerp(switchedDirection, switchedDirectionRemapped, 5f * Time.deltaTime) ; // turn that .5f into a variable
            
            // Smooth movement. Use deaccel smoothing if the input magnitude is lower, and accel smoothing if it's higher
            // Also checks when grounded to only use Slerp on the ground            
            if (targetNoY.magnitude > currentNoY.magnitude) {
                if (controller.isGrounded){
                    slerpedValue = Vector3.Slerp(currentNoY, targetNoY, data.accelerationSmoothing * Time.deltaTime);
                    lerpedValue = Vector3.Lerp(currentNoY, targetNoY, data.accelerationSmoothing * Time.deltaTime);

                    currentNoY = Vector3.Lerp(lerpedValue, slerpedValue, directionChangeDot);
                } else {
                    currentNoY = Vector3.Lerp(currentNoY, targetNoY, data.accelerationSmoothing * Time.deltaTime);
                }
            } else {
                if (controller.isGrounded){
                    slerpedValue = Vector3.Slerp(currentNoY, targetNoY, data.deaccelerationSmoothing * Time.deltaTime);
                    lerpedValue = Vector3.Lerp(currentNoY, targetNoY, data.deaccelerationSmoothing * Time.deltaTime);
                    
                    currentNoY = Vector3.Lerp(lerpedValue, slerpedValue, directionChangeDot);
                } else {
                    currentNoY = Vector3.Lerp(currentNoY, targetNoY, data.deaccelerationSmoothing * data.airDirectionDecay * Time.deltaTime);
                }
            }
            
            // Commit move direction
            outputMoveDirection = Vector3.SmoothDamp(outputMoveDirection, new Vector3(currentNoY.x, outputMoveDirection.y, currentNoY.z),ref moveSmooth , .5f *Time.deltaTime);
        }

        // Update the speed, called every frame
        private void UpdateCurrentSpeed(){
            Debug.Log(data.moveSpeed);
            outputSpeed = Mathf.Lerp(outputSpeed, data.moveSpeed, data.moveSpeedSoothing * Time.deltaTime);
        }

        // Update the gravity, called every frame
        private void UpdateCurrentGravity(){
            // Accelerate gravity
            data.gravityPower += smoothedGravityAccel * Time.deltaTime;
            data.gravityPower = Mathf.Clamp(data.gravityPower, Mathf.NegativeInfinity, data.gravityMax);
            
            // Apply a constant gravity if the player is grounded
            if (controller.isGrounded) {
                data.gravityPower = .1f;
            }
            
            // Create the final gravity value
            float gravityMoveDirection = data.jumpPower + (Physics.gravity.y * data.gravityPower);
            
            // Commit gravity to move direction, ignoring XZ since those are done in UpdateMovementDirection
            outputMoveDirection.y = Mathf.SmoothDamp(outputMoveDirection.y, gravityMoveDirection, ref gravitySmooth, .1f * Time.deltaTime);
        }

        // Update the rotation, called every frame
        private void UpdateCurrentRotation(){
            // Get input value
            Vector3 inputMovement = new Vector3(controls.rawMoveInput.x, 0f, controls.rawMoveInput.y);
			
            // Switch the desired rotation based on current movement setting
            switch (data.rotateFacing) {
                // Just look at target
                case PlayerFacingDirection.TowardsTarget:
                    // Look directly at the target
                    outputRotation = Quaternion.LookRotation(transform.position.DirectionTo(lockOnManager.mainTarget.gameObject.transform.position));
                    break;
                case PlayerFacingDirection.Momentum:
                    // Look towards the current direction the agent is moving 
                    if (inputMovement.magnitude > .05f){
                        outputRotation = Camera.main.transform.rotation * Quaternion.LookRotation(outputMoveDirection);
                    }
                    break;
                case PlayerFacingDirection.MatchForward:
                    // Look towards the input direction....why??? I guess cause move direction is PlayerFacingDirection.Momentum.
                    if (controls.rawMoveInput.magnitude < 0.05f) { break; }
                    
                    outputRotation = Camera.main.transform.rotation * Quaternion.LookRotation(inputMovement);
                    break;
                case PlayerFacingDirection.MatchCamera:
                    // Look the same direction as the camera
                    outputRotation = Quaternion.Euler(Camera.main.transform.rotation.eulerAngles.Flatten(0, null, 0));
                    break;
                case PlayerFacingDirection.Static:
                    // Don't change
                    outputRotation  = transform.rotation;
                    break;
                case PlayerFacingDirection.SpecifiedDirection:
                    // Look at an inputed rotation
                    outputRotation = specifiedRotation;
                    break;
            }
            
            // Add the current input into the created rotation
            if (inputMovement.magnitude > .05){
                outputRotation = Quaternion.Lerp(outputRotation, Camera.main.transform.rotation * Quaternion.LookRotation(inputMovement), data.rotationInputBlending);
            }
           
            // Calculate rotation speed, as in how fast the fella rotates. This value has it's own smoothing to allow for gradual increasing/decreasing of the speed till it reaches the target
            outputRotationSpeed = Mathf.Lerp(outputRotationSpeed, data.rotationSpeed, settingsChangeRotationSpeed * Time.deltaTime);
			
            // Set final rotation
            transform.rotation = Quaternion.Slerp(transform.rotation, outputRotation, outputRotationSpeed * Time.deltaTime).Flatten(0, null, 0);
        }

        // Move with default settings
        public void DoMovement(){
            DoMovement(outputMoveDirection, outputSpeed, data.gravityScale);
        }

        public void DisableNextMoveCall(){
            moveCallDisabledNextFrame = true;
        }
        
        // Custom move from input
        public void DoMovement(Vector3 moveDir, float speed, float gravityScale){
            // Debug.Log($"moveDir: {moveDir}, agent velocity: {transform.InverseTransformDirection(controller.velocity.normalized)}");

            // Seperate the different move directions. Additonal becomes it's own because it needs to be added independently of the other two
            Vector3 moveXZDir = new Vector3(moveDir.x, 0f, moveDir.z);
            Vector3 moveYDir = new Vector3(0f, moveDir.y, 0f);
            Vector3 addDir = additionalMoveDirection;

            // Add their related speeds
            moveXZDir *= speed * Time.deltaTime;
            moveYDir *= smoothedGravityScale * Time.deltaTime;
            addDir *= additionalSpeed * Time.deltaTime;

            // Construct the direction and move
            Vector3 finalDir = moveXZDir + moveYDir;
            controller.Move((Camera.main.transform.rotation.Flatten(0, null, 0) * finalDir) + addDir);    
        }

        void LateUpdate(){
            UpdateGravityLate();
            DecayAdditionalDirection();
            SmoothingSettingsChanges();
        }

        private void SmoothingSettingsChanges(){
            smoothedJumpDecay = Mathf.Lerp(smoothedJumpDecay, data.jumpPowerDecay, data.settingsChangeSmoothing * Time.deltaTime);
            smoothedGravityAccel = Mathf.Lerp(smoothedGravityAccel, data.gravityAcceleration, data.settingsChangeSmoothing * Time.deltaTime);
            smoothedGravityScale = Mathf.Lerp(smoothedGravityScale, data.gravityScale, data.settingsChangeSmoothing * Time.deltaTime);
            settingsChangeRotationSpeed = Mathf.Lerp(settingsChangeRotationSpeed, data.rotationSpeed, data.settingsChangeSmoothing * Time.deltaTime);
        }

        void DecayAdditionalDirection(){
            // Get input value
            Vector3 inputMovement = new Vector3(controls.rawMoveInput.x, 0f, controls.rawMoveInput.y);
            
            // Ignore values under deadzone
            if (inputMovement.magnitude < .1f) {
                inputMovement = Vector3.zero;
            }
            
            // Remove Y from variables
            Vector3 currentNoY = new Vector3(additionalMoveDirection.x, 0f, additionalMoveDirection.z);
            
            // Decay the direction
            if (inputMovement.magnitude < currentNoY.magnitude) {
                additionalMoveDirection = Vector3.Slerp(additionalMoveDirection, Vector3.zero,data.accelerationSmoothing * Time.deltaTime);	
            } else {
                // float deaccelValue = data.deaccelerationCurve.Evaluate(inputMovement.magnitude);
                additionalMoveDirection = Vector3.Lerp(additionalMoveDirection, Vector3.zero, data.deaccelerationSmoothing * Time.deltaTime);	
            }
            
            // Decay the gravity
            additionalMoveDirection.y -= data.gravityPower;
            additionalMoveDirection.y = Mathf.Max(0f, additionalMoveDirection.y);
        }
        
        private void UpdateGravityLate(){
            // Decay jump power
            data.jumpPower -= smoothedJumpDecay * Time.deltaTime;
            data.jumpPower = Mathf.Max(0f, data.jumpPower);
        }
    }
}

