using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;

/// <summary>
/// GAME 1303 Tier 1 — Side-scrolling player movement.   (v1.1 — Tier 3 hooks added)
///
/// 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: coyote time, jump buffering, variable jump height, sprite flipping,
/// OnJumped / OnLanded events.
///
/// v1.1 adds hooks that Tier 3 modules (Dash, WallSlideJump, Ladder, Glide, Sprint,
/// DoubleJump, GrapplingHook, RailRide, MomentumLauncher) use:
///   MovementLocked  – while true this script leaves velocity alone (a module owns it)
///   SpeedMultiplier – Sprint sets 1.5, etc.
///   CanCoyoteJump   – true when a normal ground/coyote jump is available
///   ForceJump()     – jump now, regardless of ground (double jump, wall jump)
///   ClearJumpBuffer(), JumpHeld, MoveVector, Facing
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController2D : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed = 8f;
    public float acceleration = 60f;
    public float deceleration = 80f;

    [Header("Jump")]
    public float jumpForce = 14f;
    public float coyoteTime = 0.1f;
    public float jumpBufferTime = 0.1f;
    [Range(0f, 1f)] public float jumpCutMultiplier = 0.5f;
    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 UnityEvent OnJumped;
    public UnityEvent OnLanded;

    // --- Read-only state ---
    public bool IsGrounded { get; private set; }
    public bool FacingRight { get; private set; } = true;
    public Vector2 Facing => FacingRight ? Vector2.right : Vector2.left;
    public Vector2 Velocity => rb.linearVelocity;
    public float MoveInput => moveInput.x;
    public Vector2 MoveVector => moveInput;
    public bool JumpHeld => jumpHeld;
    public bool CanCoyoteJump => coyoteCounter > 0f;
    public float BaseGravity => baseGravity;

    /// <summary>Set false to freeze input (cutscenes, death, dialogue).</summary>
    public bool InputEnabled { get; set; } = true;
    /// <summary>A Tier 3 module sets this while it controls velocity (dash, wall slide, ladder, grapple, rail).</summary>
    public bool MovementLocked { get; set; }
    /// <summary>Sprint and slow effects multiply moveSpeed by this.</summary>
    public float SpeedMultiplier { get; set; } = 1f;

    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 ----------
    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;
        else if (!MovementLocked && rb.linearVelocity.y > 0f)
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
    }

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

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

        if (!MovementLocked && jumpBufferCounter > 0f && coyoteCounter > 0f)
        {
            ForceJump(jumpForce);
        }

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

    void FixedUpdate()
    {
        if (MovementLocked) return;

        float target = moveInput.x * moveSpeed * SpeedMultiplier;
        float rate = Mathf.Abs(target) > 0.01f ? acceleration : deceleration;
        float newX = Mathf.MoveTowards(rb.linearVelocity.x, target, rate * Time.fixedDeltaTime);

        rb.gravityScale = rb.linearVelocity.y < 0f ? baseGravity * fallGravityMultiplier : baseGravity;
        float newY = Mathf.Max(rb.linearVelocity.y, -maxFallSpeed);

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

    // ---------- Public helpers for modules ----------
    public void ForceJump(float force)
    {
        rb.linearVelocity = new Vector2(rb.linearVelocity.x, force);
        jumpBufferCounter = 0f;
        coyoteCounter = 0f;
        OnJumped?.Invoke();
    }

    public void ClearJumpBuffer() => jumpBufferCounter = 0f;

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

    public void FaceDirection(float x)
    {
        if (x > 0.01f && !FacingRight) Flip();
        else if (x < -0.01f && FacingRight) Flip();
    }

    public void ResetMotion()
    {
        rb.linearVelocity = Vector2.zero;
        moveInput = Vector2.zero;
        jumpBufferCounter = 0f;
        MovementLocked = false;
        SpeedMultiplier = 1f;
        rb.gravityScale = baseGravity;
    }

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