using System;
using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    #region Variables: Movement

    private Vector2 _input;
    private CharacterController _characterController;
    private Animator _animator;
    private Vector3 _direction;

    [SerializeField] private float speed;
    [SerializeField] private float directionDampTime;

    [SerializeField] private Movement movement;

    #endregion

    #region Variables: Rotation

    [SerializeField] private float rotationSpeed = 500;
    private Camera _mainCamera;

    #endregion

    #region Variables: Gravity

    private float _gravity = -9.81f;
    [SerializeField] private float gravityMultiplier = 3;
    private float _velocity;

    #endregion

    #region Variables: Jumping

    [SerializeField] private float jumpPower;
    [SerializeField] private float jumpCooldown = 0.5f;
    [SerializeField] private float jumpBufferTime = 0.2f;
    [SerializeField] private float coyoteTime = 0.2f;

    private float lastJumpTime = -Mathf.Infinity;
    private float lastJumpPressedTime = -Mathf.Infinity;
    private float lastGroundedTime = -Mathf.Infinity;

    #endregion

    private void Start()
    {
        if (_animator.layerCount >= 2)
        {
            _animator.SetLayerWeight(1, 1);
        }
    }

    private void Awake()
    {
        _characterController = GetComponent<CharacterController>();
        _animator = GetComponent<Animator>();
        _mainCamera = Camera.main;
    }

    private void Update()
    {
        ApplyRotation();
        ApplyGravity();
        ApplyMovement();
        HandleBufferedJump();
        UpdateLastGroundedTime();

        enableFeetIk = IsGrounded();

        float inputMagnitude = new Vector2(_input.x, _input.y).magnitude;
        float targetSpeed = inputMagnitude * (movement.isSprinting ? 2f : 1f);
        speed = Mathf.Lerp(speed, targetSpeed, Time.deltaTime * 10f);
        float direction = 0f;

        if (Mathf.Abs(_input.y) > 0.1f)
        {
            direction = _input.x * Mathf.Sign(_input.y);
        }
        else
        {
            direction = 0f;
        }

        _animator.SetFloat("Speed", speed);
        _animator.SetFloat("Direction", direction, directionDampTime, Time.deltaTime);
    }

    private void ApplyGravity()
    {
        if (IsGrounded() && _velocity < 0)
        {
            _velocity = -1;

            _animator.SetBool("Jump", false);
        }
        else
        {
            _velocity += _gravity * gravityMultiplier * Time.deltaTime;
        }

        _direction.y = _velocity;
    }

    private void ApplyRotation()
    {
        if (_input.sqrMagnitude == 0) return;

        _direction = Quaternion.Euler(0, _mainCamera.transform.eulerAngles.y, 0) * new Vector3(_input.x, 0, _input.y);
        var targetRotation = Quaternion.LookRotation(_direction, Vector3.up);

        transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
    }

    private void ApplyMovement()
    {
        var targetSpeed = movement.isSprinting ? movement.speed * movement.multiplier : movement.speed;
        movement.currentSpeed = Mathf.MoveTowards(movement.currentSpeed, targetSpeed, movement.acceleration * Time.deltaTime);

        Vector3 move = new Vector3(_direction.x, 0, _direction.z).normalized * movement.currentSpeed;
        move.y = _velocity;
        _characterController.Move(move * Time.deltaTime);
    }

    public void Move(InputAction.CallbackContext context)
    {
        _input = context.ReadValue<Vector2>();
        _direction = new Vector3(_input.x, 0, _input.y);
    }

    public void Jump(InputAction.CallbackContext context)
    {
        if (!context.started) return;

        lastJumpPressedTime = Time.time;
    }

    private void UpdateLastGroundedTime()
    {
        if (IsGrounded())
        {
            lastGroundedTime = Time.time;
        }
    }

    private void HandleBufferedJump()
    {
        if (Time.time - lastJumpPressedTime <= jumpBufferTime &&
            Time.time >= lastJumpTime + jumpCooldown &&
            (IsGrounded() || Time.time - lastGroundedTime <= coyoteTime))
        {
            PerformJump();
            lastJumpPressedTime = -Mathf.Infinity;
        }
    }

    private void PerformJump()
    {
        _velocity += jumpPower;
        _animator.SetBool("Jump", true);
        lastJumpTime = Time.time;
    }

    public void Sprint(InputAction.CallbackContext context)
    {
        movement.isSprinting = context.started || context.performed;
    }

    private bool IsGrounded() => _characterController.isGrounded;

    private Vector3 rightFootPosition, leftFootPosition, leftFootIkPostion, rightFootIkPosition;
    private Quaternion leftFootIkRotation, rightFootIkRotation;
    private float lastPelvisPositionY, lastRightFootPostionY, lastLeftFootPostionY;

    [Header("Feet IK")]
    public bool enableFeetIk = true;
    [Range(0, 2f)][SerializeField] private float heightFromGroundRayCast = 1.14f;
    [Range(0, 2f)][SerializeField] private float raycastDownDistance = 1.5f;
    [SerializeField] private LayerMask environmentLayer;
    [SerializeField] private float pelvisOffset;
    [Range(0, 1f)][SerializeField] private float pelvisUpAndDownSpeed = 0.28f;
    [Range(0, 1f)][SerializeField] private float feetToIkPositionSpeed = 0.5f;

    public string leftFootAnimVariableName = "RightFootCurve";
    public string rightFootAnimVariableName = "LeftFootCurve";

    public bool useProIkFeature = false;
    public bool showSolverDebug = true;

    /// <summary>
    /// We are updating the AdjustFeetTarget method and also find the position of each foot inside our Solver Position.
    /// </summary>
    private void FixedUpdate()
    {
        if (enableFeetIk == false) { return; }
        if (_animator == null) { return; }

        AdjustFeetTarget(ref rightFootPosition, HumanBodyBones.RightFoot);
        AdjustFeetTarget(ref leftFootPosition, HumanBodyBones.LeftFoot);

        //find an raycast to the ground to find positions
        FeetPositionSolver(rightFootPosition, ref rightFootIkPosition, ref rightFootIkRotation); // handle the solver for right foot
        FeetPositionSolver(leftFootPosition, ref leftFootIkPostion, ref leftFootIkRotation); // handle the solver for left foot

    }

    private void OnAnimatorIK(int layerIndex)
    {
        if (enableFeetIk == false) { return; }
        if (_animator == null) { return; }

        MovePelvisHeight();

        //right foot ik position and rotation -- utilise the pro featuresin here
        _animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1);

        if (useProIkFeature)
        {
            _animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, _animator.GetFloat(rightFootAnimVariableName));
        }

        MoveFeetToIkPoint(AvatarIKGoal.RightFoot, rightFootIkPosition, rightFootIkRotation, ref lastRightFootPostionY);

        //left foot ik position and rotation -- utilise the pro featuresin here
        _animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1);

        if (useProIkFeature)
        {
            _animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, _animator.GetFloat(leftFootAnimVariableName));
        }

        MoveFeetToIkPoint(AvatarIKGoal.LeftFoot, leftFootIkPostion, leftFootIkRotation, ref lastLeftFootPostionY);
    }

    void MoveFeetToIkPoint(AvatarIKGoal foot, Vector3 positionIkHollder, Quaternion rotationIkHolder, ref float lastFootPostionY)
    {
        Vector3 targetIkPosition = _animator.GetIKPosition(foot);

        if (positionIkHollder != Vector3.zero)
        {
            targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
            positionIkHollder = transform.InverseTransformPoint(positionIkHollder);

            float yVariable = Mathf.Lerp(lastFootPostionY, positionIkHollder.y, feetToIkPositionSpeed);
            targetIkPosition.y += yVariable;

            lastFootPostionY = yVariable;

            targetIkPosition = transform.TransformPoint(targetIkPosition);

            _animator.SetIKRotation(foot, rotationIkHolder);
        }

        _animator.SetIKPosition(foot, targetIkPosition);
    }

    private void MovePelvisHeight()
    {
        if (rightFootIkPosition == Vector3.zero || leftFootIkPostion == Vector3.zero || lastPelvisPositionY == 0)
        {
            lastPelvisPositionY = _animator.bodyPosition.y;
            return;
        }

        float lOffestPosition = leftFootIkPostion.y - transform.position.y;
        float rOffsetPosition = rightFootIkPosition.y - transform.position.y;

        float totalOffset = (lOffestPosition < rOffsetPosition) ? lOffestPosition : rOffsetPosition;

        Vector3 newPelvisPosition = _animator.bodyPosition + Vector3.up * totalOffset;

        newPelvisPosition.y = Mathf.Lerp(lastPelvisPositionY, newPelvisPosition.y, pelvisUpAndDownSpeed);

        _animator.bodyPosition = newPelvisPosition;

        lastPelvisPositionY = _animator.bodyPosition.y;
    }
    /// <summary>
    /// We are locating the feet position via a Raycast and then Solving
    /// </summary>
    /// <param name="fromSkyPosition"></param>
    /// <param name="feetIkPostions"></param>
    /// <param name="feetIkRotations"></param>
    private void FeetPositionSolver(Vector3 fromSkyPosition, ref Vector3 feetIkPostions, ref Quaternion feetIkRotations)
    {
        // raycast handling section
        RaycastHit feetOutHit;

        if (showSolverDebug)
        {
            Debug.DrawLine(fromSkyPosition, fromSkyPosition + Vector3.down * (raycastDownDistance + heightFromGroundRayCast), Color.yellow);
        }
        if (Physics.Raycast(fromSkyPosition, Vector3.down, out feetOutHit, raycastDownDistance + heightFromGroundRayCast, environmentLayer))
        {
            // finding our feet ik positions from the sky position
            feetIkPostions = fromSkyPosition;
            feetIkPostions.y = feetOutHit.point.y + pelvisOffset;
            feetIkRotations = Quaternion.FromToRotation(Vector3.up, feetOutHit.normal) * transform.rotation;

            return;
        }
        feetIkPostions = Vector3.zero; // it didn't work
    }
    /// <summary>
    /// Adjusts the feet target.
    /// </summary>
    /// <param name="feetPositions"></param>
    /// <param name="foot"></param>
    private void AdjustFeetTarget(ref Vector3 feetPositions, HumanBodyBones foot)
    {
        feetPositions = _animator.GetBoneTransform(foot).position;
        feetPositions.y = transform.position.y + heightFromGroundRayCast;
    }
}

[Serializable]
public struct Movement
{
    public float speed;
    public float multiplier;
    public float acceleration;

    [HideInInspector] public bool isSprinting;
    [HideInInspector] public float currentSpeed;
}