using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace Theriapolis.Game.CodexUI.Core;

/// <summary>
/// Per-frame input snapshot used by every CodexUI widget. <see cref="Tick"/>
/// is called once at the top of <see cref="CodexScreen.Update"/>; widgets
/// read the resulting edge-detected event flags during their own Update pass.
///
/// Text input piggybacks on MonoGame's <see cref="GameWindow.TextInput"/>
/// event — the screen subscribes during Initialize and pushes characters
/// into <see cref="TextEnteredThisFrame"/>. Backspace, Enter and arrow keys
/// route through <see cref="KeyJustPressed"/> instead.
///
/// <para>
/// Mouse clipping: a parent widget can set <see cref="SetMouseClip"/> before
/// updating a region of the tree, which causes <see cref="MousePosition"/>
/// to report off-screen for any cursor outside the clip rect. Children that
/// rely on <c>Bounds.Contains(input.MousePosition)</c> to detect hover/click
/// then ignore events that visually land in some other layer (e.g. a
/// stepper bar painted over a scrolled card). Without this, both the
/// clipped widget and the chrome above it would handle the same click.
/// </para>
/// </summary>
public sealed class CodexInput
{
    public Point MousePosition { get; private set; }
    public Point PreviousMousePosition { get; private set; }
    public Point MouseDelta { get; private set; }
    public bool LeftButtonDown { get; private set; }
    public bool LeftJustPressed { get; private set; }
    public bool LeftJustReleased { get; private set; }
    public bool RightJustPressed { get; private set; }
    public int  ScrollDelta { get; private set; }

    private MouseState _prev;
    private KeyboardState _prevKb;
    private KeyboardState _curKb;

    private Rectangle? _mouseClip;
    private Point _rawMouse;

    /// <summary>Characters typed this frame (collected from the window's TextInput event).</summary>
    public string TextEnteredThisFrame { get; private set; } = "";

    private System.Text.StringBuilder _textBuf = new();

    public void OnTextInput(char c) => _textBuf.Append(c);

    public void Tick()
    {
        var m = Mouse.GetState();
        var kb = Keyboard.GetState();

        PreviousMousePosition = _rawMouse;
        _rawMouse = new Point(m.X, m.Y);
        MouseDelta = _rawMouse - PreviousMousePosition;
        ScrollDelta = m.ScrollWheelValue - _prev.ScrollWheelValue;

        bool prevLeft = _prev.LeftButton == ButtonState.Pressed;
        bool curLeft  = m.LeftButton    == ButtonState.Pressed;
        LeftButtonDown   = curLeft;
        LeftJustPressed  = !prevLeft && curLeft;
        LeftJustReleased = prevLeft  && !curLeft;

        bool prevRight = _prev.RightButton == ButtonState.Pressed;
        bool curRight  = m.RightButton    == ButtonState.Pressed;
        RightJustPressed = !prevRight && curRight;

        _prev = m;
        _prevKb = _curKb;
        _curKb = kb;

        TextEnteredThisFrame = _textBuf.ToString();
        _textBuf.Clear();

        ApplyClip();
    }

    /// <summary>
    /// Restrict <see cref="MousePosition"/> reads to <paramref name="clipRect"/>.
    /// Outside the rect the position is reported far off-screen so widgets
    /// that hit-test by <c>Bounds.Contains</c> stop registering hover/click.
    /// Pair with <see cref="ClearMouseClip"/> after the clipped subtree's
    /// Update completes.
    /// </summary>
    public void SetMouseClip(Rectangle clipRect)
    {
        _mouseClip = clipRect;
        ApplyClip();
    }

    public void ClearMouseClip()
    {
        _mouseClip = null;
        ApplyClip();
    }

    /// <summary>Read the current clip rectangle, if any. Used by widgets that
    /// nest a tighter clip and need to restore the outer one after their
    /// child subtree has finished updating.</summary>
    public Rectangle? GetMouseClip() => _mouseClip;

    private void ApplyClip()
    {
        if (_mouseClip is Rectangle r && !r.Contains(_rawMouse))
            MousePosition = new Point(-99999, -99999);
        else
            MousePosition = _rawMouse;
    }

    public bool KeyDown(Keys k) => _curKb.IsKeyDown(k);
    public bool KeyJustPressed(Keys k) => _curKb.IsKeyDown(k) && !_prevKb.IsKeyDown(k);
    public bool KeyJustReleased(Keys k) => !_curKb.IsKeyDown(k) && _prevKb.IsKeyDown(k);
}
