using UnityEngine;
using UnityEngine.InputSystem;

/// <summary>
/// GAME 1303 Tier 1 — Side-scrolling player movement.
///
/// SETUP
///  1. Add to your player GameObject along with: Rigidbody2D, a Collider2D, and a
///     PlayerInput component (Behavior = "Send Messages", Default Map = "Player").
///  2. Create an empty child called "GroundCheck" at the player's feet and drag it
///     into the Ground Check slot. Set Ground Layer to whatever layer your floors use.
///  3. In Rigidbody2D: Freeze Rotation Z, Collision Detection = Continuous,
///     Gravity Scale ≈ 3 for a snappy feel.
///  4. The default Input Actions asset already has "Move" (Vector2) and "Jump" (Button)
///     in the "Player" map — this script listens for those names.
///
/// FEATURES students get for free: coyote time, jump buffering, variable jump height,
/// sprite flipping, and an OnJumped event other scripts (audio, animation) can hook.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController2D : MonoBehaviour
{
    [Header("Movement")]
    [Tooltip("Horizontal speed in units per second.")]
    public float moveSpeed = 8f;
    [Tooltip("How quickly the player reaches full speed. Higher = tighter.")]
    public float acceleration = 60f;
    [Tooltip("How quickly the player stops. Higher = less slide.")]
    public float deceleration = 80f;

    [Header("Jump")]
    public float jumpForce = 14f;
    [Tooltip("Seconds after leaving a ledge during which you can still jump.")]
    public float coyoteTime = 0.1f;
    [Tooltip("Seconds before landing during which a jump press is remembered.")]
    public float jumpBufferTime = 0.1f;
    [Tooltip("Releasing Jump early multiplies upward velocity by this (0-1). 1 = fixed-height jumps.")]
    [Range(0f, 1f)] public float jumpCutMultiplier = 0.5f;
    [Tooltip("Extra gravity while falling makes jumps feel less floaty.")]
    public float fallGravityMultiplier = 1.8f;
    public float maxFallSpeed = 25f;

    [Header("Ground Check")]
    public Transform groundCheck;
    public Vector2 groundCheckSize = new Vector2(0.8f, 0.1f);
    public LayerMask groundLayer;

    [Header("Events")]
    public UnityEngine.Events.UnityEvent OnJumped;
    public UnityEngine.Events.UnityEvent OnLanded;

    // --- Read-only state other scripts can look at (Animator bridge, Dash, etc.) ---
    public bool IsGrounded { get; private set; }
    public bool FacingRight { get; private set; } = true;
    public Vector2 Velocity => rb.linearVelocity;
    public float MoveInput => moveInput.x;

    /// <summary>Set false to freeze input (cutscenes, death, dialogue). Physics still runs.</summary>
    public bool InputEnabled { get; set; } = true;

    private Rigidbody2D rb;
    private Vector2 moveInput;
    private float coyoteCounter;
    private float jumpBufferCounter;
    private bool jumpHeld;
    private bool wasGrounded;
    private float baseGravity;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        baseGravity = rb.gravityScale;
    }

    // ---------- Input System messages (PlayerInput -> Send Messages) ----------
    void OnMove(InputValue value)
    {
        moveInput = InputEnabled ? value.Get<Vector2>() : Vector2.zero;
    }

    void OnJump(InputValue value)
    {
        if (!InputEnabled) return;
        jumpHeld = value.isPressed;
        if (jumpHeld) jumpBufferCounter = jumpBufferTime;   // remember the press
        else if (rb.linearVelocity.y > 0f)                    // released early -> cut the jump
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
    }

    void Update()
    {
        // Ground check
        IsGrounded = Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer);
        if (IsGrounded && !wasGrounded) OnLanded?.Invoke();
        wasGrounded = IsGrounded;

        // Timers
        coyoteCounter = IsGrounded ? coyoteTime : coyoteCounter - Time.deltaTime;
        jumpBufferCounter -= Time.deltaTime;

        // Jump when a buffered press meets coyote-eligible ground
        if (jumpBufferCounter > 0f && coyoteCounter > 0f)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
            jumpBufferCounter = 0f;
            coyoteCounter = 0f;
            OnJumped?.Invoke();
        }

        // Flip sprite
        if (moveInput.x > 0.01f && !FacingRight) Flip();
        else if (moveInput.x < -0.01f && FacingRight) Flip();
    }

    void FixedUpdate()
    {
        // Horizontal: accelerate toward target speed
        float target = moveInput.x * moveSpeed;
        float rate = Mathf.Abs(target) > 0.01f ? acceleration : deceleration;
        float newX = Mathf.MoveTowards(rb.linearVelocity.x, target, rate * Time.fixedDeltaTime);

        // Vertical: heavier gravity when falling, clamp fall speed
        rb.gravityScale = rb.linearVelocity.y < 0f ? baseGravity * fallGravityMultiplier : baseGravity;
        float newY = Mathf.Max(rb.linearVelocity.y, -maxFallSpeed);

        rb.linearVelocity = new Vector2(newX, newY);
    }

    void Flip()
    {
        FacingRight = !FacingRight;
        Vector3 s = transform.localScale;
        s.x *= -1f;
        transform.localScale = s;
    }

    /// <summary>Instantly stop all motion (used by RespawnManager).</summary>
    public void ResetMotion()
    {
        rb.linearVelocity = Vector2.zero;
        moveInput = Vector2.zero;
        jumpBufferCounter = 0f;
    }

    void OnDrawGizmosSelected()
    {
        if (groundCheck == null) return;
        Gizmos.color = Color.green;
        Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
    }
}