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

/// <summary>
/// GAME 1303 Tier 5 — Movement in discrete steps, snapped to a grid. Weenie Maze (the
/// snake head), Last Delivery if Chris goes lane-based like Crossy Road.
///
/// Two very different feels, one script:
///   Mode = Stepped     – you press a direction, the character slides one cell and stops.
///                        Crossy Road, Pokémon, sokoban puzzles.
///   Mode = Continuous  – the character keeps moving in the last direction forever; input
///                        only CHANGES direction. This is Snake. Use with SnakeBody.
///
/// SETUP
///  1. Character: Sprite + Rigidbody2D (Gravity 0, Kinematic is fine) + Collider2D +
///     this script + PlayerInput (Send Messages).
///  2. Cell Size must match your Tilemap's cell size (usually 1).
///  3. Blocking Layers: walls. In Stepped mode a blocked step is refused (you don't move);
///     in Continuous mode hitting a wall fires OnBlocked — wire it to game over, or to
///     stop the snake.
///  4. Snap On Start puts the character exactly on a cell centre so everything lines up.
///  5. Step Time: seconds per cell. In Continuous mode this is the snake's speed —
///     0.15 is brisk, 0.25 is gentle. Speed Up Per Step lets it accelerate as you grow.
///
/// The character always finishes a step before starting the next, so movement stays on
/// the grid even if the player mashes keys. Queued Input remembers one press during a
/// step, which makes tight turns feel fair.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class GridMover : MonoBehaviour
{
    public enum Mode { Stepped, Continuous }

    public Mode mode = Mode.Stepped;
    public float cellSize = 1f;
    public float stepTime = 0.15f;
    [Tooltip("Continuous mode: each step gets this much faster (seconds). 0 = constant speed.")]
    public float speedUpPerStep = 0f;
    public float minStepTime = 0.06f;
    public LayerMask blockingLayers;
    public bool snapOnStart = true;
    [Tooltip("Continuous mode: can't reverse straight into yourself.")]
    public bool preventReversal = true;

    [Header("Facing")]
    public bool rotateToDirection = false;
    public float rotationOffset = 0f;
    public bool flipSprite = true;

    [Header("Events")]
    public UnityEvent<Vector2Int> OnStepStarted;     // direction of the step
    public UnityEvent<Vector3> OnStepCompleted;      // the cell we just arrived at
    public UnityEvent OnBlocked;                     // ran into a wall
    public UnityEvent<Vector2Int> OnDirectionChanged;

    public Vector2Int Direction { get; private set; } = Vector2Int.right;
    public bool IsMoving { get; private set; }
    public bool InputEnabled { get; set; } = true;
    /// <summary>Position at the START of the current step — SnakeBody follows this.</summary>
    public Vector3 PreviousCell { get; private set; }
    public int StepCount { get; private set; }

    private Rigidbody2D rb;
    private Vector3 stepFrom, stepTo;
    private float stepProgress;
    private Vector2Int queuedDirection;
    private bool hasQueued;
    private float currentStepTime;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        rb.gravityScale = 0f;
        currentStepTime = stepTime;
    }

    void Start()
    {
        if (snapOnStart) transform.position = Snap(transform.position);
        PreviousCell = transform.position;
        stepFrom = stepTo = transform.position;
        if (mode == Mode.Continuous) TryStartStep(Direction);
    }

    void OnMove(InputValue value)
    {
        if (!InputEnabled) return;
        Vector2 v = value.Get<Vector2>();
        if (v.sqrMagnitude < 0.25f) return;

        // Snap to the dominant axis — no diagonals on a grid
        Vector2Int dir = Mathf.Abs(v.x) > Mathf.Abs(v.y)
            ? new Vector2Int((int)Mathf.Sign(v.x), 0)
            : new Vector2Int(0, (int)Mathf.Sign(v.y));

        if (mode == Mode.Continuous && preventReversal && dir == -Direction) return;

        if (IsMoving) { queuedDirection = dir; hasQueued = true; }
        else SetDirection(dir);
    }

    void SetDirection(Vector2Int dir)
    {
        if (dir != Direction)
        {
            Direction = dir;
            ApplyFacing();
            OnDirectionChanged?.Invoke(dir);
        }
        if (mode == Mode.Stepped) TryStartStep(dir);
    }

    void Update()
    {
        if (IsMoving)
        {
            stepProgress += Time.deltaTime / Mathf.Max(0.01f, currentStepTime);
            if (stepProgress >= 1f)
            {
                transform.position = stepTo;
                IsMoving = false;
                StepCount++;
                OnStepCompleted?.Invoke(stepTo);

                // Apply a queued turn, then continue if we're in Continuous mode
                if (hasQueued)
                {
                    hasQueued = false;
                    if (!(mode == Mode.Continuous && preventReversal && queuedDirection == -Direction))
                    {
                        Direction = queuedDirection;
                        ApplyFacing();
                        OnDirectionChanged?.Invoke(Direction);
                    }
                }
                if (mode == Mode.Continuous)
                {
                    if (speedUpPerStep > 0f)
                        currentStepTime = Mathf.Max(minStepTime, currentStepTime - speedUpPerStep);
                    TryStartStep(Direction);
                }
            }
            else
            {
                transform.position = Vector3.Lerp(stepFrom, stepTo, stepProgress);
            }
        }
    }

    bool TryStartStep(Vector2Int dir)
    {
        if (IsMoving) return false;

        Vector3 from = Snap(transform.position);
        Vector3 to = from + new Vector3(dir.x, dir.y, 0f) * cellSize;

        // Wall check: a box just smaller than a cell at the destination
        Collider2D hit = Physics2D.OverlapBox(to, Vector2.one * cellSize * 0.8f, 0f, blockingLayers);
        if (hit != null)
        {
            OnBlocked?.Invoke();
            return false;
        }

        PreviousCell = from;
        stepFrom = from;
        stepTo = to;
        stepProgress = 0f;
        IsMoving = true;
        OnStepStarted?.Invoke(dir);
        return true;
    }

    /// <summary>Force a direction from code (AI, a redirect tile).</summary>
    public void ForceDirection(Vector2Int dir) => SetDirection(dir);

    /// <summary>Stop dead (game over, cutscene).</summary>
    public void Halt()
    {
        IsMoving = false;
        hasQueued = false;
        transform.position = Snap(transform.position);
    }

    public void ResetSpeed() => currentStepTime = stepTime;

    Vector3 Snap(Vector3 p) => new Vector3(
        Mathf.Round(p.x / cellSize) * cellSize,
        Mathf.Round(p.y / cellSize) * cellSize,
        p.z);

    void ApplyFacing()
    {
        if (rotateToDirection)
        {
            float angle = Mathf.Atan2(Direction.y, Direction.x) * Mathf.Rad2Deg + rotationOffset;
            transform.rotation = Quaternion.Euler(0f, 0f, angle);
        }
        else if (flipSprite && Direction.x != 0)
        {
            Vector3 s = transform.localScale;
            s.x = Mathf.Abs(s.x) * (Direction.x > 0 ? 1f : -1f);
            transform.localScale = s;
        }
    }
}