using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;

    public class PlayerController : MonoBehaviour
    {
        [Header("Components")]
        [SerializeField] Rigidbody2D RB;
        [SerializeField] CapsuleCollider2D Collider;

        [Space]
        [Header("Movement Settings")]
        [SerializeField] float MoveSpeed;
        [SerializeField] bool FacingRight = true;

        [Space]
        [Header("Jump Settings")]
        [SerializeField] float JumpForce;
        [SerializeField] float JumpCoyoteTime;
        [SerializeField] float JumpBufferTime;
        bool IsJumping = false;

        float TimeSinceLastJumpRequest = 0f;
        float TimeSinceLastGrounded = 0f;

        [Space]
        [Header("Physics Settings")]
        [SerializeField] LayerMask GroundLayer;
        [SerializeField] float WallCheckDistance;
        [SerializeField] float GroundCheckDistance;
        [SerializeField] Vector2 Gravity = Vector2.right;
        [SerializeField] bool IsGrounded = false;
        [SerializeField] float MaxSlopeAngle;
        List<Vector2> LocalDirections = new List<Vector2>();
        Vector2 GroundNormal;

        void Awake()
        {
            RB = GetComponent<Rigidbody2D>();
            Collider = GetComponent<CapsuleCollider2D>();

            LocalDirections = RetrieveLocalDirections();
        }

        void Update()
        {
            LocalDirections = RetrieveLocalDirections();

            Timer();
        }

        void FixedUpdate()
        {
            Vector2 DesiredHorizontalVelocity = Move();
            TryJump(JumpForce, LocalDirections[0], DesiredHorizontalVelocity);

            RB.AddForce(Gravity * RB.mass * -Physics2D.gravity.y, ForceMode2D.Force);

            float WallNormalThreshold = Mathf.Cos(MaxSlopeAngle * Mathf.Deg2Rad);

            if (TrySweep(LocalDirections[2], GroundCheckDistance, out RaycastHit2D Hit, WorldNormal => Vector2.Angle(WorldNormal, LocalDirections[0]) < MaxSlopeAngle))
            {
                TimeSinceLastGrounded = JumpCoyoteTime;
                IsJumping = false;
                GroundNormal = Hit.normal;
                IsGrounded = true;
            }
            else
            {
                IsGrounded = false;
            }
            
            Vector2 slopeTangent    = Vector2.Perpendicular(GroundNormal).normalized;
            Vector2 slopeVel        = Vector2.Dot(RB.linearVelocity, slopeTangent) * slopeTangent;
            Vector2 nonSlopeVel     = RB.linearVelocity - slopeVel;

            if (IsGrounded)
            {
                Debug.Log(nonSlopeVel);
            }
        }

        bool TrySweep(Vector2 Direction, float Distance, out RaycastHit2D Hit, System.Func<Vector2, bool> NormalTest)
        { // System.Func<Vector2, bool> NormalTest lets us create custom normal tests for different scenarios
            var Hits = new RaycastHit2D[5];
            int HitCount = Collider.Cast(Direction, new ContactFilter2D { layerMask = GroundLayer, useLayerMask = true }, Hits, Distance);

            for (int i = 0; i < HitCount; i++)
            {
                var H = Hits[i];

                if(H.collider == null) continue;
                
                if (NormalTest(H.normal))
                {
                    Hit = H;
                    return true;
                }
            }

            Hit = default;
            return false;
        }

        List<Vector2> RetrieveLocalDirections()
        {
            float Angle = Vector2.SignedAngle(Vector2.down, Gravity.normalized);
            return new List<Vector2>
            {
                Vector2.up.Rotate(Angle).normalized,
                Vector2.right.Rotate(Angle).normalized,
                Vector2.down.Rotate(Angle).normalized,
                Vector2.left.Rotate(Angle).normalized
            };
        }

        Vector2 Move()
        {
            float Angle = Vector2.SignedAngle(Vector2.down, Gravity.normalized);
            RB.rotation = Angle;
            transform.rotation = Quaternion.Euler(0f, 0f, Angle);

            Vector2 GravityAxis = LocalDirections[0];

            Vector2 VelocityAlongGravity = Vector2.Dot(RB.linearVelocity, GravityAxis) * GravityAxis;


            Vector2 DesiredHorizontalVelocity = LocalDirections[1] * MoveSpeed * (FacingRight ? 1f : -1f);


            Vector2 Direction = FacingRight ? LocalDirections[1] : LocalDirections[3];

            float WallNormalThreshold = Mathf.Cos(MaxSlopeAngle * Mathf.Deg2Rad);

            if (TrySweep(Direction, WallCheckDistance, out RaycastHit2D Hit, WorldNormal => Mathf.Abs(WorldNormal.x) > (1f - WallNormalThreshold) && Mathf.Abs(WorldNormal.y) < WallNormalThreshold))
            {
                FacingRight = !FacingRight;
                DesiredHorizontalVelocity = Vector2.zero;
            }

            RB.linearVelocity = VelocityAlongGravity + DesiredHorizontalVelocity;
                
            return DesiredHorizontalVelocity;
        }

        public void TryJump(float JumpForce, Vector2 JumpDirection, Vector2 DesiredHorizontalVelocity)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                TimeSinceLastJumpRequest = JumpBufferTime;
            }

            if (TimeSinceLastGrounded > 0 && TimeSinceLastJumpRequest > 0 && !IsJumping)
            {
                IsJumping = true;
                TimeSinceLastJumpRequest = 0f;
                TimeSinceLastGrounded = 0f;

                RB.AddForce(JumpDirection.normalized * JumpForce, ForceMode2D.Impulse);
            }
        }

        void Timer()
        {
            if (TimeSinceLastJumpRequest > 0)
            {
                TimeSinceLastJumpRequest -= Time.deltaTime;
            }

            if (TimeSinceLastGrounded > 0)
            {
                TimeSinceLastGrounded -= Time.deltaTime;
            }
        }
    }

    public static class Vector2Extensions
    {
        public static Vector2 Rotate(this Vector2 Vector, float Degrees)
        {
            float X = Vector.x * Mathf.Cos(Degrees * Mathf.Deg2Rad) - Vector.y * Mathf.Sin(Degrees * Mathf.Deg2Rad); // x cos phi - y sin phi
            float Y = Vector.x * Mathf.Sin(Degrees * Mathf.Deg2Rad) + Vector.y * Mathf.Cos(Degrees * Mathf.Deg2Rad); // x sin phi + y cos phi

            Vector = new Vector2(X, Y);
            return Vector;
        }
    }