@namespace Lumeo
@using Lumeo.Services
@implements IAsyncDisposable

@if (Shell is not null)
{
    @* OverlayProvider already rendered the SheetContent shell around our parent
       component — rendering another shell here would stack a second backdrop +
       sliding panel on top. Pass the children straight through to the host
       shell instead. Side / Size / PreventClose / Class on this nested element
       are intentionally ignored; the provider controls those via OverlayOptions. *@
    @ChildContent
}
else if (Context.IsOpen)
{
    <div class="fixed inset-0 z-50">
        <div class="fixed inset-0 bg-black/80 animate-fade-in" @onclick="HandleBackdropClick"></div>
        <div id="@_contentId" class="@CssClass" role="dialog" aria-modal="true" aria-labelledby="@Context.TitleId" aria-describedby="@Context.DescriptionId" @onkeydown="HandleKeyDown" tabindex="-1" @attributes="AdditionalAttributes">
            @ChildContent
            @if (!PreventClose)
            {
                <button class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none" @onclick="Close">
                    <Blazicon Svg="Lucide.X" class="h-4 w-4" />
                    <span class="sr-only">@L["Dialog.Close"]</span>
                </button>
            }
        </div>
    </div>
}

@inject ComponentInteropService Interop
@inject Lumeo.Services.Localization.ILumeoLocalizer L

@code {
    [CascadingParameter] public Sheet.SheetContext Context { get; set; } = default!;
    [CascadingParameter] public OverlayShellMarker? Shell { get; set; }
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public SheetSide Side { get; set; } = SheetSide.Right;
    [Parameter] public SheetSize Size { get; set; } = SheetSize.Default;
    /// <summary>How the sheet enters/leaves. <c>Slide</c> (default) animates
    /// from the configured <see cref="Side"/>. <c>Fade</c> uses a simple
    /// opacity tween — useful in dense forms where popovers and animations
    /// compete. <c>None</c> disables the animation entirely.</summary>
    [Parameter] public SheetAnimation Animation { get; set; } = SheetAnimation.Slide;
    [Parameter] public bool PreventClose { get; set; }
    /// <summary>When true, the sheet can be dismissed by swiping in the
    /// direction opposite to its Side (e.g. swipe-down on a Bottom sheet).
    /// Default false to avoid conflicts with scrollable content. Wire to
    /// true only when the sheet's content fits without scrolling.</summary>
    [Parameter] public bool SwipeToClose { get; set; } = false;
    [Parameter] public string? Class { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    private readonly string _contentId = $"sheet-{Guid.NewGuid():N}";
    private bool _wasOpen;

    // Slide-animation transform trap is fixed via JS interop:
    // Interop.AttachOverlaySlideEnd registers a native animationend listener
    // that filters strictly on slide-in-from-* AND on event.target === this
    // element (so descendant animations like Calendar's animate-zoom-in
    // don't trigger us), then sets inline `transform: none`. Inline style
    // beats the keyframe fill-mode forwarding — guarantees no containing
    // block trap for popovers (Select / DatePicker / Combobox).
    private string CssClass => $"fixed z-50 flex flex-col gap-4 bg-background p-6 shadow-lg {PositionClasses} {AnimationClass} {Class}".Trim();

    private string SizeClass => Side is SheetSide.Left or SheetSide.Right
        ? Size switch
        {
            SheetSize.Sm => "sm:max-w-xs",
            SheetSize.Lg => "sm:max-w-lg",
            SheetSize.Xl => "sm:max-w-xl",
            SheetSize.Full => "sm:max-w-full",
            _ => "sm:max-w-sm"
        }
        // Top/Bottom: width is always full (inset-x-0). Size controls
        // the height — Full takes the entire viewport (mobile-fullscreen
        // pattern), the other sizes cap at a fraction of the viewport
        // height. Default keeps the legacy content-driven height so
        // existing layouts are unchanged.
        : Size switch
        {
            SheetSize.Sm => "max-h-[40vh] h-auto",
            SheetSize.Lg => "max-h-[70vh] h-auto",
            SheetSize.Xl => "max-h-[85vh] h-auto",
            SheetSize.Full => "inset-y-0 h-full max-h-full",
            _ => ""
        };

    private string PositionClasses => Side switch
    {
        SheetSide.Top => $"inset-x-0 top-0 {SizeClass}",
        SheetSide.Bottom => $"inset-x-0 bottom-0 {SizeClass}",
        SheetSide.Left => $"inset-y-0 left-0 h-full w-3/4 {SizeClass}",
        SheetSide.Right => $"inset-y-0 right-0 h-full w-3/4 {SizeClass}",
        _ => $"inset-y-0 right-0 h-full w-3/4 {SizeClass}"
    };

    // Animation=Slide is paired with the JS-interop AttachOverlaySlideEnd
    // (see OnAfterRenderAsync) which clears the inline transform once the
    // slide animation completes — this defeats the `animation-fill-mode:
    // both` identity-matrix trap that would otherwise create a containing
    // block for fixed-positioned descendants. Fade / None don't use a
    // transform at all, so no JS cleanup is needed.
    private string AnimationClass => Animation switch
    {
        SheetAnimation.None => "",
        SheetAnimation.Fade => "animate-fade-in",
        SheetAnimation.Slide => Side switch
        {
            SheetSide.Top => "animate-slide-in-from-top",
            SheetSide.Bottom => "animate-slide-in-from-bottom",
            SheetSide.Left => "animate-slide-in-from-left",
            SheetSide.Right => "animate-slide-in-from-right",
            _ => "animate-slide-in-from-right"
        },
        _ => ""
    };

    // Kept for backward-compat with tests / consumers that read SideClasses.
    private string SideClasses => $"{PositionClasses} {AnimationClass}".Trim();

    // Swipe direction is the direction of the gesture that should dismiss the sheet:
    // a Right-anchored sheet flies out to the right when the user swipes right, etc.
    // Drawer uses the same convention — see DrawerContent.SwipeDirection.
    private string SwipeDirection => Side switch
    {
        SheetSide.Top => "up",
        SheetSide.Right => "right",
        SheetSide.Bottom => "down",
        SheetSide.Left => "left",
        _ => "right"
    };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // When passthrough mode is active the host SheetContent owns scroll lock
        // + focus trap; this nested instance is a no-op for lifecycle effects.
        if (Shell is not null) return;

        if (Context.IsOpen && !_wasOpen)
        {
            await Interop.LockScroll();
            await Interop.SetupFocusTrap(_contentId);
            // Native animationend listener sets inline transform:none after
            // the slide animation completes (filtered on slide-in-from-* so
            // descendant zoom-in / fade-in events don't trigger us). Inline
            // style beats the keyframe fill-mode forwarding — guarantees no
            // containing-block trap for popovers.
            if (Animation == SheetAnimation.Slide)
            {
                await Interop.AttachOverlaySlideEnd(_contentId);
            }
            // Touch swipe-to-dismiss (WCAG 2.5.1 / mobile UX parity with Drawer).
            // Reuses the Drawer JS helper as-is — it already handles all four
            // directions and matches the Sheet's Side parameter 1:1.
            // Gated on SwipeToClose (default false) because Sheets commonly
            // contain scrollable content, and a downward scroll on a Bottom
            // sheet would otherwise be misread as a dismiss gesture.
            if (!PreventClose && SwipeToClose)
            {
                await Interop.RegisterDrawerSwipe(_contentId, SwipeDirection, Close);
            }
            _wasOpen = true;
        }
        else if (!Context.IsOpen && _wasOpen)
        {
            await Cleanup();
            _wasOpen = false;
        }
    }


    private async Task Close() => await Context.SetOpen.InvokeAsync(false);

    private async Task HandleBackdropClick()
    {
        if (!PreventClose) await Close();
    }

    private async Task HandleKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Escape" && !PreventClose) await Close();
    }

    private async Task Cleanup()
    {
        try
        {
            await Interop.UnregisterDrawerSwipe(_contentId);
            await Interop.RemoveFocusTrap(_contentId);
            await Interop.UnlockScroll();
        }
        catch (JSDisconnectedException) { }
    }

    public async ValueTask DisposeAsync()
    {
        if (_wasOpen) await Cleanup();
    }

    public enum SheetSide { Top, Right, Bottom, Left }
    public enum SheetSize { Sm, Default, Lg, Xl, Full }
    public enum SheetAnimation { Slide, Fade, None }
}
