@* Reusable right-click / context menu.

   Consumers get two (independently optional) triggers over one item list:
     • ChildContent — wrapped as a right-clickable surface (browser menu suppressed).
     • ShowEllipsis — a "⋯" button, the keyboard/touch fallback for the right-click.

   Mouse/touch open the menu anchored at the pointer (fixed positioning). Keyboard
   activation of the "⋯" button carries no pointer coordinates, so the menu instead
   anchors just below the button (absolute positioning inside the trigger wrap).
   Outside-click (or a right-click elsewhere) closes via a transparent full-viewport
   backdrop — no JS alert/confirm. Keyboard: Arrow up/down move selection, Enter/Space
   activate, Esc closes; focus returns to the "⋯" trigger on close. *@

@if (ChildContent is not null)
{
    <span class="ctx-surface" @oncontextmenu="OpenAt" @oncontextmenu:preventDefault="true">
        @ChildContent
    </span>
}

@if (_open)
{
    @* Transparent catch-all: any click — or a right-click — that misses the menu closes it. *@
    <div class="ctx-backdrop" @onclick="Close" @oncontextmenu="Close" @oncontextmenu:preventDefault="true"></div>
}

@if (ShowEllipsis)
{
    <span class="ctx-ellipsis-wrap">
        <button type="button" class="ctx-ellipsis" title="More actions"
                aria-haspopup="true" aria-expanded="@_open" @ref="_ellipsisRef" @onclick="OpenAt">
            &#x22EF;@* ⋯ MIDLINE HORIZONTAL ELLIPSIS *@
        </button>
        @if (_open && _anchorToEllipsis)
        {
            @RenderMenu("ctx-menu ctx-menu--anchored", null)
        }
    </span>
}

@if (_open && !_anchorToEllipsis)
{
    @RenderMenu("ctx-menu", $"top:{_y.ToString(Culture)}px; left:{_x.ToString(Culture)}px")
}

@code {
    private static readonly System.Globalization.CultureInfo Culture =
        System.Globalization.CultureInfo.InvariantCulture;

    /// <summary>The rows to show. Separators are permitted.</summary>
    [Parameter] public IReadOnlyList<ContextMenuItem> Items { get; set; } = Array.Empty<ContextMenuItem>();

    /// <summary>Wrapped as the right-clickable surface. Optional.</summary>
    [Parameter] public RenderFragment? ChildContent { get; set; }

    /// <summary>Render the "⋯" trigger button (keyboard/touch fallback).</summary>
    [Parameter] public bool ShowEllipsis { get; set; } = true;

    private bool _open;
    private double _x;
    private double _y;
    private bool _anchorToEllipsis;
    private int _activeIndex = -1;
    private ElementReference _menuRef;
    private ElementReference _ellipsisRef;
    private bool _focusMenuPending;
    private bool _focusEllipsisPending;

    // Mouse right-click / touch-tap carry real pointer coordinates → anchor the menu there
    // (fixed). Keyboard activation of the "⋯" button dispatches a click with clientX/Y == 0
    // (no pointer) → anchor the menu below the button instead of at viewport origin (0,0).
    private void OpenAt(MouseEventArgs e)
    {
        _anchorToEllipsis = e.ClientX == 0 && e.ClientY == 0;
        _x = e.ClientX;
        _y = e.ClientY;
        _activeIndex = FirstEnabledIndex();
        _open = true;
        _focusMenuPending = true;
    }

    private void Close()
    {
        if (!_open) return;
        var returnFocus = _anchorToEllipsis; // keyboard-opened via the ⋯ button — restore its focus
        _open = false;
        _activeIndex = -1;
        _anchorToEllipsis = false;
        if (returnFocus && ShowEllipsis) _focusEllipsisPending = true;
    }

    private async Task Activate(ContextMenuItem item)
    {
        if (item.Disabled || item.IsSeparator) return;
        Close();
        if (item.OnClick is not null) await item.OnClick.Invoke();
    }

    private async Task OnKeyDown(KeyboardEventArgs e)
    {
        switch (e.Key)
        {
            case "Escape":
                Close();
                break;
            case "ArrowDown":
                Move(1);
                break;
            case "ArrowUp":
                Move(-1);
                break;
            case "Enter":
            case " ":
                if (_activeIndex >= 0 && _activeIndex < Items.Count)
                {
                    await Activate(Items[_activeIndex]);
                }
                break;
        }
    }

    // Advance the selection in the given direction, skipping separators and
    // disabled rows, wrapping around the ends.
    private void Move(int dir)
    {
        if (Items.Count == 0) return;
        var i = _activeIndex;
        for (var step = 0; step < Items.Count; step++)
        {
            i = (i + dir + Items.Count) % Items.Count;
            var item = Items[i];
            if (!item.IsSeparator && !item.Disabled)
            {
                _activeIndex = i;
                return;
            }
        }
    }

    private int FirstEnabledIndex()
    {
        for (var i = 0; i < Items.Count; i++)
        {
            if (!Items[i].IsSeparator && !Items[i].Disabled) return i;
        }
        return -1;
    }

    // The menu body, shared between the pointer-anchored (fixed) and ellipsis-anchored
    // (absolute) placements so the item loop lives in exactly one place.
    private RenderFragment RenderMenu(string cssClass, string? style) => __builder =>
    {
        <div class="@cssClass" style="@style" role="menu" tabindex="0" @ref="_menuRef" @onkeydown="OnKeyDown">
            @for (var i = 0; i < Items.Count; i++)
            {
                var item = Items[i];
                if (item.IsSeparator)
                {
                    <div class="ctx-separator" role="separator"></div>
                }
                else
                {
                    var index = i;
                    <button type="button" role="menuitem"
                            class="ctx-item @(index == _activeIndex ? "active" : "")"
                            disabled="@item.Disabled"
                            @onclick="() => Activate(item)">
                        @if (!string.IsNullOrEmpty(item.Icon))
                        {
                            <span class="ctx-icon">@item.Icon</span>
                        }
                        <span class="ctx-label">@item.Label</span>
                    </button>
                }
            }
        </div>
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (_focusMenuPending)
        {
            _focusMenuPending = false;
            try { await _menuRef.FocusAsync(); }
            catch { /* the menu may have closed before the focus round-trip completed */ }
        }
        if (_focusEllipsisPending)
        {
            _focusEllipsisPending = false;
            try { await _ellipsisRef.FocusAsync(); }
            catch { /* the trigger may have unmounted */ }
        }
    }
}
