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

/// <summary>
/// GAME 1303 Tier 3 — Fire a rope, swing from it, reel in. Hot Air's core mechanic;
/// also usable by Spark of Corruption's Servant.
///
/// SETUP
///  1. Add to any Rigidbody2D object (balloon or player). Add a LineRenderer (2 positions,
///     width 0.05) for the rope and drag it in. Add a DistanceJoint2D? NO — this script
///     adds one at runtime.
///  2. Input: add a "Grapple" Button action to the Player map (Right Mouse / gamepad
///     Left Trigger). Aim:
///        Mouse    – toward the mouse cursor (needs Camera.main; best for the balloon)
///        Facing   – forward and up at Aim Angle degrees (PlayerController2D games)
///        Stick    – Move vector direction (gamepad)
///  3. Grapple Layer: what the hook can attach to (rocks, ceiling, anchors). Max Distance
///     is how far the rope can reach.
///  4. Hold Move Up/Down (Move Y) to reel in/out while attached. Release the button to
///     let go. Jump also lets go if a PlayerController2D is present.
///  5. OnAttached / OnReleased for SFX and animation.
///
/// Physics tip: swings feel best with Rigidbody2D Linear Damping 0.2–0.5 and the joint's
/// Max Distance Only = true (rope goes slack instead of acting like a rod).
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class GrapplingHook : MonoBehaviour
{
    public enum Aim { Mouse, Facing, Stick }

    public Aim aim = Aim.Mouse;
    [Tooltip("Facing mode: degrees above horizontal.")]
    public float aimAngle = 45f;
    public float maxDistance = 10f;
    public LayerMask grappleLayer;

    [Header("Rope")]
    public LineRenderer rope;
    public float reelSpeed = 4f;
    public float minRopeLength = 1f;
    [Tooltip("Rope is a max length only (swing) vs. rigid rod.")]
    public bool ropeGoesSlack = true;
    [Tooltip("Small pull toward anchor on attach so the swing starts moving.")]
    public float attachImpulse = 2f;

    [Header("Events")]
    public UnityEvent OnAttached;
    public UnityEvent OnReleased;
    public UnityEvent OnMissed;

    public bool IsAttached => joint != null && joint.enabled;
    public Vector2 AnchorPoint { get; private set; }

    private Rigidbody2D rb;
    private DistanceJoint2D joint;
    private PlayerController2D controller;
    private Vector2 moveInput;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        controller = GetComponent<PlayerController2D>();
        joint = gameObject.AddComponent<DistanceJoint2D>();
        joint.enabled = false;
        joint.autoConfigureDistance = false;
        joint.enableCollision = true;
        if (rope != null) rope.enabled = false;
    }

    void OnMove(InputValue v) { moveInput = v.Get<Vector2>(); }

    void OnGrapple(InputValue v)
    {
        if (v.isPressed) TryAttach();
        else Release();
    }

    void OnJump(InputValue v)
    {
        if (v.isPressed && IsAttached && controller != null)
        {
            Release();
            controller.ForceJump(controller.jumpForce);
            controller.ClearJumpBuffer();
        }
    }

    public bool TryAttach()
    {
        if (IsAttached) return false;
        Vector2 dir = GetAimDirection();
        RaycastHit2D hit = Physics2D.Raycast(rb.position, dir, maxDistance, grappleLayer);
        if (hit.collider == null) { OnMissed?.Invoke(); return false; }

        AnchorPoint = hit.point;
        joint.connectedBody = hit.rigidbody;   // null = world anchor
        joint.connectedAnchor = hit.rigidbody != null ? hit.rigidbody.transform.InverseTransformPoint(hit.point) : hit.point;
        joint.anchor = Vector2.zero;
        joint.distance = Vector2.Distance(rb.position, hit.point);
        joint.maxDistanceOnly = ropeGoesSlack;
        joint.enabled = true;

        rb.AddForce(dir * attachImpulse, ForceMode2D.Impulse);
        if (controller != null) controller.MovementLocked = true;
        if (rope != null) rope.enabled = true;
        OnAttached?.Invoke();
        return true;
    }

    public void Release()
    {
        if (!IsAttached) return;
        joint.enabled = false;
        if (controller != null) controller.MovementLocked = false;
        if (rope != null) rope.enabled = false;
        OnReleased?.Invoke();
    }

    void FixedUpdate()
    {
        if (!IsAttached) return;

        // Reel in/out
        if (Mathf.Abs(moveInput.y) > 0.2f)
            joint.distance = Mathf.Clamp(joint.distance - moveInput.y * reelSpeed * Time.fixedDeltaTime, minRopeLength, maxDistance);

        // Gentle swing steering with left/right
        if (Mathf.Abs(moveInput.x) > 0.2f)
            rb.AddForce(Vector2.right * moveInput.x * 6f);

        // Keep gravity normal for a PlayerController2D (it skips FixedUpdate while locked)
        if (controller != null) rb.gravityScale = controller.BaseGravity;
    }

    void LateUpdate()
    {
        if (rope == null || !IsAttached) return;
        Vector3 anchorWorld = joint.connectedBody != null
            ? joint.connectedBody.transform.TransformPoint(joint.connectedAnchor)
            : (Vector3)joint.connectedAnchor;
        rope.positionCount = 2;
        rope.SetPosition(0, transform.position);
        rope.SetPosition(1, anchorWorld);
    }

    Vector2 GetAimDirection()
    {
        switch (aim)
        {
            case Aim.Mouse:
                if (Mouse.current != null && Camera.main != null)
                {
                    Vector3 m = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
                    Vector2 d = (Vector2)m - rb.position;
                    if (d.sqrMagnitude > 0.01f) return d.normalized;
                }
                goto case Aim.Facing;
            case Aim.Stick:
                if (moveInput.sqrMagnitude > 0.1f) return moveInput.normalized;
                goto case Aim.Facing;
            default:
                float facing = controller != null ? (controller.FacingRight ? 1f : -1f) : Mathf.Sign(transform.lossyScale.x);
                float rad = aimAngle * Mathf.Deg2Rad;
                return new Vector2(Mathf.Cos(rad) * facing, Mathf.Sin(rad)).normalized;
        }
    }
}