using System.Collections.Generic;
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;
    bool CanMove = true;

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

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

    [Space]
    [Header("Physics Settings")]
    [SerializeField] LayerMask GroundLayer;
    [SerializeField] float WallCheckDistance;
    [SerializeField] float GroundCheckDistance;
    [SerializeField] Vector2 Gravity = Vector2.right;
    [SerializeField] float GravityRotationSpeed;
    [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();
        TryJump(JumpForce, LocalDirections[0]);

        Timer();
    }

    void FixedUpdate()
    {
        if (CanMove)
        {
            Move();
        }

        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, GroundLayer, WorldNormal => Vector2.Angle(WorldNormal, LocalDirections[0]) < MaxSlopeAngle) && TimeSinceLastJump <= 0)
        {
            TimeSinceLastGrounded = JumpCoyoteTime;
            TimeSinceLastJump = 0f;

            CanMove = true;
            IsJumping = false;
            GroundNormal = Hit.normal;
        }

    }

    Vector2 Move()
    {
        float Angle = Vector2.SignedAngle(Vector2.down, Gravity.normalized);

        float SmoothZ = Mathf.LerpAngle(transform.eulerAngles.z, Angle, GravityRotationSpeed * Time.deltaTime);
        transform.eulerAngles = new Vector3(0, 0, SmoothZ);

        Vector2 VelocityAlongGravity = Vector2.Dot(RB.linearVelocity, LocalDirections[0]) * LocalDirections[0];
        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, GroundLayer, WorldNormal => Mathf.Abs(WorldNormal.x) > (1f - WallNormalThreshold) && Mathf.Abs(WorldNormal.y) < WallNormalThreshold))
        {
            FacingRight = !FacingRight;
            DesiredHorizontalVelocity = Vector2.zero;
        }

        RB.linearVelocity = VelocityAlongGravity + DesiredHorizontalVelocity;

        return DesiredHorizontalVelocity;
    }

    bool TrySweep(Vector2 Direction, float Distance, out RaycastHit2D Hit, LayerMask Layer, 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 = Layer, 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;
    }

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

        if (TimeSinceLastGrounded > 0 && TimeSinceLastJumpRequest > 0 && !IsJumping && TimeSinceLastJump <= 0)
        {
            IsJumping = true;
            TimeSinceLastJumpRequest = 0f;
            TimeSinceLastGrounded = 0f;
            TimeSinceLastJump = JumpResetTime; // Reset the jump timer to prevent multiple jumps in quick succession

            Vector2 LocalGravityVelocity = (Vector2)Vector3.Project(RB.linearVelocity, LocalDirections[2]);
            Vector2 LateralVelocity = RB.linearVelocity - LocalGravityVelocity;

            RB.linearVelocity = (JumpDirection * JumpForce) + LateralVelocity;
        }
    }

    public void AddForce(Vector2 Force, ForceMode2D ForceMode = ForceMode2D.Impulse, bool AllowMovement = true)
    {
        TimeSinceLastJumpRequest = 0f;
        TimeSinceLastGrounded = 0f;
        TimeSinceLastJump = 0f;

        CanMove = AllowMovement;

        IsJumping = false;
        
        float VelocityAlong = Vector2.Dot(RB.linearVelocity, Force.normalized);
        RB.linearVelocity -= Force.normalized * VelocityAlong;
        RB.AddForce(Force, ForceMode);
    }

    public void ChangeGravity(Vector2 NewGravity)
    {
        LocalDirections = RetrieveLocalDirections();
        Gravity = NewGravity.normalized;
    }

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

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

        if (TimeSinceLastJump > 0)
        {
            TimeSinceLastJump -= Time.deltaTime;
        }
    }
    
    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
        };
    }
}

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;
    }
}