using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using DodoSSH.Client.Shell.ViewModels;

namespace DodoSSH.Client.App.Views;

/// <summary>The Ctrl+K host search, overlaid on the window.</summary>
/// <remarks>
/// <para>
/// Everything a palette does once it is open belongs here rather than in <see cref="MainWindow"/>: the four
/// keys it answers to, the press outside it that dismisses it, and taking the keyboard the moment it appears.
/// Opening it is the window's business, because Ctrl+K has to work when this control is not showing.
/// </para>
/// <para>
/// The split used to fall the other way, and the cost was that none of it could be tested. Showing
/// <see cref="MainWindow"/> initialises WebView2, which refuses the headless dispatcher's thread — see
/// <c>LayoutHarnessTests.WhyTheWindowItselfIsNeverShown</c> — so behaviour that lived on the window could only
/// be checked by hand. A <see cref="UserControl"/> hosts in a bare window and takes real key and pointer
/// input.
/// </para>
/// </remarks>
internal sealed partial class QuickConnect : UserControl
{
    public QuickConnect()
    {
        InitializeComponent();

        // Tunnelled, and deliberately: the query box below is on the route these keys take, and a text box
        // that grows a use for Enter or the arrows — a multi-line box, a completion list — would take them
        // before a bubbling handler here ever ran. The palette owns them while it is open, so it says so at
        // the point on the route where nothing else has had a chance yet.
        AddHandler(KeyDownEvent, OnPaletteKey, RoutingStrategies.Tunnel);

        // A click on a result connects to it, which is the pointer's version of what Enter already does.
        // Tapped rather than PointerPressed: a press has not chosen anything yet — it is also the start of a
        // drag across the list — and the ListBox has moved its own selection by the time a tap completes,
        // which is what makes the handler below a matter of reading the selection rather than hit-testing.
        Results.Tapped += OnResultTapped;
    }

    /// <summary>The box, so the caret can be put in it the moment the palette opens.</summary>
    internal TextBox QueryBox => Query;

    private MainWindowViewModel? Shell => DataContext as MainWindowViewModel;

    /// <summary>
    /// Answers one of the palette's keys, wherever in the window it was pressed.
    /// </summary>
    /// <remarks>
    /// Internal because <see cref="MainWindow"/> calls it too, for the case this control's own tunnelled
    /// handler cannot see: a key routes through here only while the focus is inside the palette, and the
    /// window is what catches Escape when it is not.
    /// </remarks>
    internal void HandleKey(KeyEventArgs e)
    {
        ArgumentNullException.ThrowIfNull(e);

        if (Shell is not { IsSearching: true } shell)
        {
            return;
        }

        switch (e.Key)
        {
            case Key.Escape:
                shell.CloseSearchCommand.Execute(null);
                e.Handled = true;
                break;

            case Key.Enter:
                shell.ConnectToSearchResultCommand.Execute(null);
                e.Handled = true;
                break;

            case Key.Down:
                Move(shell, 1);
                e.Handled = true;
                break;

            case Key.Up:
                Move(shell, -1);
                e.Handled = true;
                break;

            default:
                break;
        }
    }

    /// <summary>
    /// Takes the keyboard, so the palette can be typed into the instant it appears.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Both halves, for the reason <see cref="NativeKeyboardFocus"/> gives: the terminal's WebView is a native
    /// child window that keeps Win32 focus even after it is collapsed, so focusing an Avalonia control without
    /// the Win32 call produces a box with a caret in it that silently receives nothing.
    /// </para>
    /// <para>
    /// Done here rather than by the window, and that is the fix rather than a tidying. <c>Focus()</c> on a
    /// collapsed control is measurably a no-op that is not replayed when the control is revealed, and the
    /// window's own attempt ran from the view model's <c>PropertyChanged</c> — ahead of the binding that makes
    /// this control visible, so it focused a control that was still collapsed and the keyboard stayed wherever
    /// it was.
    /// </para>
    /// <para>
    /// Becoming visible is still too early on its own, which is why this is posted rather than called. A
    /// control that has never been laid out has no visual children — measured: at the instant
    /// <c>IsVisible</c> turns true the query box reports <c>IsAttachedToVisualTree() == false</c>, and focus is
    /// refused to anything not in the tree. Layout runs at a higher priority than this callback, so by the
    /// time it is picked up the box exists.
    /// </para>
    /// </remarks>
    private void TakeKeyboard()
    {
        // The palette can have been dismissed between the post and the callback — a press on the wash, or a
        // second Ctrl+K — and stealing the keyboard back into a control nobody can see would be worse than
        // arriving late.
        if (!IsVisible)
        {
            return;
        }

        if (TopLevel.GetTopLevel(this) is Window window)
        {
            NativeKeyboardFocus.ReturnTo(window);
        }

        Query.Focus();
    }

    /// <remarks>Clamped rather than wrapped: a list that jumps from the last row to the first loses people.</remarks>
    private static void Move(MainWindowViewModel shell, int delta)
    {
        if (shell.SearchResults.Count == 0)
        {
            return;
        }

        var current = shell.SelectedSearchResult is { } selected
            ? shell.SearchResults.IndexOf(selected)
            : -1;

        shell.SelectedSearchResult =
            shell.SearchResults[Math.Clamp(current + delta, 0, shell.SearchResults.Count - 1)];
    }

    /// <inheritdoc />
    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        // Base first, so the visibility this control has just been given has already reached its descendants:
        // focus is refused to anything not effectively visible, and the box below is a descendant.
        base.OnPropertyChanged(change);

        if (change.Property == IsVisibleProperty && change.GetNewValue<bool>())
        {
            Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded);
        }
    }

    private void OnPaletteKey(object? sender, KeyEventArgs e) => HandleKey(e);

    /// <summary>
    /// Connects to the result that was clicked.
    /// </summary>
    /// <remarks>
    /// Goes through the shell's own command rather than reading the row out of the event, so the pointer and
    /// the keyboard take exactly the same path: the palette closes, the hosts screen is put back in case the
    /// connection has a question to ask, and the vault's connect command makes every refusal it already
    /// makes. Fire-and-forget, as the sidebar's double-click is — the command reports its own failures onto
    /// the status line and into the tab it opens.
    /// </remarks>
    private void OnResultTapped(object? sender, TappedEventArgs e)
    {
        if (Shell is not { IsSearching: true, SelectedSearchResult: not null } shell)
        {
            return;
        }

        e.Handled = true;
        _ = shell.ConnectToSearchResultCommand.ExecuteAsync(null);
    }

    /// <remarks>
    /// Only a press on the wash itself. Presses on the card bubble through here as well, and closing on those
    /// would make the palette impossible to click into.
    /// </remarks>
    private void OnBackdropPressed(object? sender, PointerPressedEventArgs e)
    {
        if (!ReferenceEquals(e.Source, Backdrop) || Shell is not { IsSearching: true } shell)
        {
            return;
        }

        shell.CloseSearchCommand.Execute(null);
        e.Handled = true;
    }
}
