using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;

using DodoSSH.Client.Shell.ViewModels;

namespace DodoSSH.Client.Android.Views;

/// <summary>Design 02 — the host list, and the two gestures that are the whole of this file.</summary>
/// <remarks>
/// <para>
/// ◆ <b>A tap connects, a long press chooses, and once anything is chosen a tap ticks instead.</b> The
/// branch in the middle of that is why the tap is handled here rather than bound in the markup: which of
/// the two a tap means depends on whether the screen is in selection mode, and Avalonia's bindings cannot
/// ask.
/// </para>
/// <para>
/// The handlers are on the <c>ListBox</c> rather than on the row, and the row stays a plain
/// <c>Border</c>. A button as the item template swallows the press before the list sees it —
/// <c>FilesScreen</c> writes that out at length — so neither handler would ever run.
/// </para>
/// </remarks>
internal sealed partial class HostsScreen : UserControl
{
    /// <summary>
    /// Whether the press that is about to end was long enough to have meant something else.
    /// </summary>
    /// <remarks>
    /// Avalonia raises <c>Tapped</c> on release whatever the press lasted, so without this a long press
    /// would tick the row and then the tap it becomes would untick it again — one touch doing a thing and
    /// undoing it, which is the one outcome that would make both gestures untrustworthy. Set when the hold
    /// starts and cleared by the tap it suppresses, so it never survives the gesture that set it.
    /// </remarks>
    private bool held;

    /// <remarks>
    /// <para>
    /// Both gestures are attached here rather than in the markup so that they sit beside the property that
    /// makes one of them fire at all. <see cref="InputElement.IsHoldingEnabledProperty"/> is set rather than
    /// assumed: it is the whole of the long press, and a default that changed would take it away silently —
    /// every tap would go on working and nothing would ever choose a host again.
    /// </para>
    /// <para>
    /// ◆ <b><c>FindControl</c> rather than the field the name generator declares for <c>x:Name</c></b>,
    /// which is the same rule <c>PhoneShell</c> and <c>TerminalScreen</c> already carry, and which this
    /// file learned the hard way. That field is
    /// assigned by the generated <c>InitializeComponent</c>, and no view on this head calls it — they load
    /// their XAML directly, as above. So the field compiles, is null at run time, and the first thing
    /// touching it throws. Here that was a <see cref="NullReferenceException"/> in this constructor, which
    /// is a crash before the first frame: the phone shell builds this control on the way up, so the
    /// application died on launch rather than on the hosts screen.
    /// </para>
    /// </remarks>
    public HostsScreen()
    {
        AvaloniaXamlLoader.Load(this);

        var rows = this.FindControl<ListBox>("Rows")!;

        InputElement.SetIsHoldingEnabled(rows, true);
        rows.Holding += OnRowHeld;
    }

    /// <summary>
    /// Connects to the row that was tapped, or ticks it where hosts are already ticked.
    /// </summary>
    /// <remarks>
    /// <para>
    /// ◆ <b>The branch is the selection, not the row.</b> Once anything is chosen the screen is in selection
    /// mode — the bar across the top says so — and in that mode a tap adds and removes rather than
    /// connecting. That is what every Android list does, and the alternative is worse than a rule to
    /// remember: a tap that connected while five machines sat ticked would open a terminal on top of a
    /// selection somebody was halfway through building.
    /// </para>
    /// <para>
    /// Fire-and-forget where it connects, as the desktop grid's activation is: the command reports its own
    /// failures onto the status line — an unknown host key, a refused password — and awaiting it here would
    /// be an event handler returning a task nothing observes.
    /// </para>
    /// </remarks>
    private void OnRowTapped(object? sender, TappedEventArgs e)
    {
        if (held)
        {
            held = false;
            return;
        }

        if (DataContext is not VaultViewModel vault || RowUnder(e) is not { } row)
        {
            return;
        }

        if (vault.IsChoosingHosts)
        {
            vault.ToggleHostChoiceCommand.Execute(row);
            return;
        }

        _ = vault.ConnectToRowCommand.ExecuteAsync(row);
    }

    /// <summary>Puts a tick against the row that was held, entering selection mode with it.</summary>
    /// <remarks>
    /// On <see cref="HoldingState.Started"/> rather than on completion, so the bar is up while the finger is
    /// still down. A long press that showed nothing until release would be a gesture with no way to tell it
    /// had been recognised, and the only feedback available on this list is the thing it does.
    /// </remarks>
    private void OnRowHeld(object? sender, HoldingRoutedEventArgs e)
    {
        if (e.HoldingState != HoldingState.Started)
        {
            return;
        }

        held = true;

        if (DataContext is VaultViewModel vault && RowUnder(e) is { } row)
        {
            vault.ChooseHostCommand.Execute(row);
        }
    }

    /// <summary>
    /// The host the gesture landed on, or null where it landed on something that is not one.
    /// </summary>
    /// <remarks>
    /// <para>
    /// ◆ <b>Read off the element under the finger rather than off the list's selection, which is what both
    /// handlers used to do.</b> The selection was defensible while a tap only ever connected: a tap on a
    /// group heading moves the selection and the view model bounces it straight back — see
    /// <c>VaultViewModel.SelectedSidebarRow</c> — so reading it answered "a host, or nothing" for free.
    /// It stops being defensible the moment a tap can tick one: a heading would then bounce the selection to
    /// whichever host was last chosen and tick <em>that</em>, which is a machine the user was not pointing
    /// at going into a set they are about to delete.
    /// </para>
    /// <para>
    /// The ancestor rather than <c>e.Source</c> itself, because the source is whatever leaf the finger
    /// landed on — a tag chip's <c>TextBlock</c> has a string for a data context, and the row's own
    /// <c>Ellipse</c> has the row. The <see cref="ListBoxItem"/> is the one element in that chain whose data
    /// context is always the list's item, whatever kind it is.
    /// </para>
    /// </remarks>
    private static HostRowViewModel? RowUnder(RoutedEventArgs e) =>
        (e.Source as Visual)?.FindAncestorOfType<ListBoxItem>(includeSelf: true)?.DataContext
            as HostRowViewModel;
}
