using global::Android.Views;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;

using DodoSSH.Client.Android.Platform;
using DodoSSH.Client.Shell.ViewModels;

namespace DodoSSH.Client.Android.Views;

/// <summary>The phone's single view. The desktop head's MainWindow, without the window.</summary>
internal sealed partial class PhoneShell : UserControl
{
    private MainWindowViewModel? shell;

    /// <summary>
    /// The open vault, while there is one, so that this control hears about the hosts screen's own state.
    /// </summary>
    /// <remarks>
    /// ◆ <b>A second subscription, and it is the price of the header being swappable.</b> Two of the flags
    /// below are questions about the vault rather than about the shell — whether hosts are ticked, and
    /// whether the host editor is filling the screen — and the shell does not forward the vault's
    /// notifications. Kept in step from <see cref="OnShellChanged"/>, because <c>Vault</c> is replaced on
    /// every unlock and nulled on every lock; a handler left on a disposed vault would keep it alive.
    /// </remarks>
    private VaultViewModel? vault;

    /// <summary>
    /// Everything the phone draws, which is the element the software keyboard is kept off.
    /// </summary>
    /// <remarks>
    /// Looked up rather than read off the field the name generator declares for <c>x:Name</c>, and
    /// <c>TerminalScreen</c> does the same for the same reason: that field is assigned by the generated
    /// <c>InitializeComponent</c>, and no view on this head calls it — they load their XAML directly. Using
    /// it compiles and is null at run time, which on this control means a crash before the first frame.
    /// </remarks>
    private readonly Panel body;

    /// <summary>The software keyboard, while this control is attached. Null on a platform without one.</summary>
    private IInputPane? keyboard;

    /// <summary>
    /// Whether the lock screen currently showing is the one the application launched into.
    /// </summary>
    /// <remarks>
    /// Set once, when the shell arrives, because this control is built once for the process. Cleared the
    /// moment the state leaves <see cref="ShellState.Locked"/>, and never set again — which is what stops a
    /// deliberate lock from being answered with an immediate request to unlock. See
    /// <see cref="TryOfferDeviceUnlock"/>.
    /// </remarks>
    private bool thisLockIsTheLaunch;

    private bool offeredDeviceUnlock;

    /// <summary>
    /// The width above which this surface is laid out like the desktop.
    /// </summary>
    /// <remarks>
    /// <para>
    /// 600, which is Android's own boundary between a compact window and a medium one, and is where a
    /// tablet, an unfolded foldable and a landscape phone land on the far side. It is measured in the units
    /// Avalonia lays out in, which are density-independent — so this is 600dp and not 600 physical pixels,
    /// and a 1080-pixel phone at 3× density is correctly on the narrow side of it.
    /// </para>
    /// <para>
    /// One number rather than a set of them. Android names three window classes and this head has two
    /// layouts, so a second breakpoint would be a third arrangement nothing has been designed for.
    /// </para>
    /// </remarks>
    private const double WideAt = 600;

    public PhoneShell()
    {
        AvaloniaXamlLoader.Load(this);

        body = this.FindControl<Panel>("Body")!;

        // Subscribed once, for the life of the control, rather than in OnAttachedToVisualTree: the panel is
        // this control's own child and cannot outlive it, and re-subscribing on every attach is how a
        // handler ends up registered twice.
        body.SizeChanged += OnBodyResized;

        DataContextChanged += (_, _) =>
        {
            if (shell is not null)
            {
                shell.PropertyChanged -= OnShellChanged;
            }

            shell = DataContext as MainWindowViewModel;

            if (shell is not null)
            {
                shell.PropertyChanged += OnShellChanged;
                ApplyScreenshotPolicy(shell.State);

                thisLockIsTheLaunch = true;
                TryOfferDeviceUnlock();
            }

            FollowTheVault();
            RefreshChrome();
        };
    }

    /// <summary>Moves this control's second subscription onto whichever vault is open now.</summary>
    /// <remarks>
    /// Compared before being swapped, so that the ordinary case — a shell notification about something else
    /// entirely — costs one reference comparison rather than an unsubscribe and a resubscribe per property
    /// change on the shell.
    /// </remarks>
    private void FollowTheVault()
    {
        if (ReferenceEquals(vault, shell?.Vault))
        {
            return;
        }

        if (vault is not null)
        {
            vault.PropertyChanged -= OnVaultChanged;
        }

        vault = shell?.Vault;

        if (vault is not null)
        {
            vault.PropertyChanged += OnVaultChanged;
        }
    }

    private void OnVaultChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
        => RefreshChrome();

    /// <summary>Whether this surface is wide enough to be laid out like the desktop.</summary>
    /// <remarks>
    /// A property of the control rather than of the view model, because it is a fact about the surface and
    /// the view models are shared with a head where it is always true. The markup reads it through
    /// <c>$parent[views:PhoneShell]</c>.
    /// </remarks>
    public static readonly StyledProperty<bool> IsWideProperty =
        AvaloniaProperty.Register<PhoneShell, bool>(nameof(IsWide));

    /// <summary>Whether the rail down the left edge is drawn.</summary>
    /// <remarks>
    /// The wide surface's answer to <see cref="ShowsBottomBarProperty"/> and asks the same question about
    /// which screen is up, so the two move together — including over Connections with nothing running. See
    /// <see cref="RefreshChrome"/>.
    /// </remarks>
    public static readonly StyledProperty<bool> ShowsRailProperty =
        AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsRail));

    /// <summary>Whether the three-entry bar across the bottom is drawn.</summary>
    /// <remarks>
    /// Not simply the pages: it also stays up on Connections with nothing running, which is the terminal
    /// surface drawing a page. See <see cref="RefreshChrome"/>.
    /// </remarks>
    public static readonly StyledProperty<bool> ShowsBottomBarProperty =
        AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsBottomBar));

    /// <summary>Whether the header carrying the vault's name, the sync light and LOCK is drawn.</summary>
    public static readonly StyledProperty<bool> ShowsVaultHeaderProperty =
        AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsVaultHeader));

    /// <summary>Whether the bar about the chosen hosts is drawn in the header's place.</summary>
    public static readonly StyledProperty<bool> ShowsHostSelectionBarProperty =
        AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsHostSelectionBar));

    /// <summary>Whether the strip of open shells above the bottom bar is drawn.</summary>
    public static readonly StyledProperty<bool> ShowsShellStripProperty =
        AvaloniaProperty.Register<PhoneShell, bool>(nameof(ShowsShellStrip));

    /// <inheritdoc cref="IsWideProperty" />
    public bool IsWide
    {
        get => GetValue(IsWideProperty);
        private set => SetValue(IsWideProperty, value);
    }

    /// <inheritdoc cref="ShowsRailProperty" />
    public bool ShowsRail
    {
        get => GetValue(ShowsRailProperty);
        private set => SetValue(ShowsRailProperty, value);
    }

    /// <inheritdoc cref="ShowsBottomBarProperty" />
    public bool ShowsBottomBar
    {
        get => GetValue(ShowsBottomBarProperty);
        private set => SetValue(ShowsBottomBarProperty, value);
    }

    /// <inheritdoc cref="ShowsVaultHeaderProperty" />
    public bool ShowsVaultHeader
    {
        get => GetValue(ShowsVaultHeaderProperty);
        private set => SetValue(ShowsVaultHeaderProperty, value);
    }

    /// <inheritdoc cref="ShowsHostSelectionBarProperty" />
    public bool ShowsHostSelectionBar
    {
        get => GetValue(ShowsHostSelectionBarProperty);
        private set => SetValue(ShowsHostSelectionBarProperty, value);
    }

    /// <inheritdoc cref="ShowsShellStripProperty" />
    public bool ShowsShellStrip
    {
        get => GetValue(ShowsShellStripProperty);
        private set => SetValue(ShowsShellStripProperty, value);
    }

    /// <summary>
    /// Works out which chrome this surface should be wearing.
    /// </summary>
    /// <remarks>
    /// <para>
    /// ◆ <b>Five flags computed here rather than five conditions in the markup, because Avalonia's
    /// bindings have no "and" and none of these is a single question any more.</b> Everywhere else on this
    /// head that costs a wrapper element; here it would cost two nested ones per row and the header's would
    /// have to be an "or", which a wrapper cannot express at all.
    /// </para>
    /// <para>
    /// ◆ <b>The nav stands down for a shell rather than for the terminal surface, and those parted company
    /// when that surface gained a page.</b> Connections with nothing running is a box, a CONNECT button and
    /// the machines connected to before — see TerminalScreen.axaml — and none of that is worth the screen a
    /// shell is worth it for. It is also the one screen somebody can arrive at by closing their last tab,
    /// which made the collapsed bar a way to end up with no route to Hosts or Settings but the system back
    /// gesture. The header is deliberately not part of this: the surface draws its own bar with the back
    /// arrow and the +, and the vault header above that is the second row this head exists to avoid.
    /// </para>
    /// <para>
    /// <b>The header is the one worth reading twice.</b> Narrow, it stands down behind SETTINGS, because the
    /// screens under that hub draw their own header with a back arrow and two rows of chrome is what this
    /// surface exists to avoid. Wide, there is no hub to be behind and no back arrow to duplicate — the rail
    /// is how you leave — so the vault's name, the sync light and LOCK stay where they are on every screen.
    /// Losing them on the keychain would be losing the only LOCK button on the surface.
    /// </para>
    /// <para>
    /// ◆ <b>The header is now a swap rather than a switch, and the editor takes the whole screen.</b> Two
    /// more flags and two more inputs, both of them the vault's rather than the shell's — see
    /// <see cref="vault"/>. While hosts are ticked the header stands down and
    /// <see cref="ShowsHostSelectionBar"/> puts the action bar in its place, which is what makes that bar
    /// unambiguous: the screen is about the ticked hosts and nothing else. While the host editor is open it
    /// is a page rather than a card, so all four rows of chrome stand down and the form has the display —
    /// which is what "opens in a separate page" means on a 360dp screen.
    /// </para>
    /// <para>
    /// Recomputed on every notification from either object rather than on a named list of them. Five boolean
    /// comparisons and no allocation is cheaper than being wrong: the properties this reads are computed
    /// ones, and which of them raise a change is a fact about a file in another project that nothing here
    /// would notice going stale.
    /// </para>
    /// </remarks>
    private void RefreshChrome()
    {
        var wide = body.Bounds.Width >= WideAt;
        var pages = shell?.IsShowingPages == true;

        // ◆ Connections with nothing running, which is the terminal surface drawing a page: a box, a CONNECT
        // button and the machines connected to before. The nav stands down for a shell — the whole of the
        // arrangement below — and there is no shell here to stand down for, so it stays. Taking it away on
        // this one screen was worst where it was least affordable: somebody who has just closed their last
        // tab, or who pressed Connections to see what was open and found nothing, was left on a screen whose
        // only way to Hosts or Settings was the system back gesture.
        var connectPage = shell is { IsTerminalSurface: true, HasTabs: false };

        // The editor is a page of its own now, so nothing else is drawn around it — not the vault header,
        // not the shells strip, and not the way off the screen. Its own header carries the back arrow, which
        // is the one control it needs and the one the system gesture already maps to.
        var editing = vault?.IsEditing == true;

        // Only on the hosts screen. The ticks survive a trip to the keychain — the set is not cleared by
        // navigating — and a bar counting hosts over the transfers screen would be chrome about a list that
        // is not on the display.
        var choosing = vault?.IsChoosingHosts == true && shell?.IsHostsShowing == true;

        // Before the flags, because it changes what one of them reads. Nothing else on this head navigates
        // in response to a resize, and this is not navigation for its own sake: the hub is a list of the
        // destinations the rail now carries, so an unfolded device would otherwise sit on a menu of things
        // it can already see. Only from the hub itself — a screen reached through it stays put, because the
        // user asked for that screen rather than for the menu.
        if (wide && !IsWide && shell is { Screen: ShellScreen.More })
        {
            shell.ShowScreenCommand.Execute(ShellScreen.Hosts);
        }

        IsWide = wide;
        ShowsRail = wide && (pages || connectPage) && !editing;
        ShowsBottomBar = !wide && (pages || connectPage) && !editing;
        ShowsShellStrip = pages && !editing;
        ShowsHostSelectionBar = pages && choosing && !editing;
        ShowsVaultHeader = pages && !editing && !choosing && (wide || shell?.IsMoreSurface != true);
    }

    private void OnShellChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (shell is null)
        {
            return;
        }

        FollowTheVault();
        RefreshChrome();

        if (e.PropertyName is nameof(MainWindowViewModel.State))
        {
            ApplyScreenshotPolicy(shell.State);

            if (shell.State is not ShellState.Locked)
            {
                thisLockIsTheLaunch = false;
                offeredDeviceUnlock = false;
            }
        }

        // Three properties rather than one, because the condition is assembled out of order. Startup sets
        // State to Locked and only then awaits the keystore to decide CanUnlockWithDevice, and it does all
        // of it inside the busy wrapper — which refuses a second command outright. So whichever of the
        // three settles last is the one that has to ask again.
        if (e.PropertyName is nameof(MainWindowViewModel.State)
            or nameof(MainWindowViewModel.CanUnlockWithDevice)
            or nameof(MainWindowViewModel.IsBusy))
        {
            TryOfferDeviceUnlock();
        }
    }

    /// <summary>
    /// Raises the fingerprint prompt on arriving at the lock screen, rather than waiting to be asked.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The button is still there and still says what it does; this only spends the tap for you. On a phone
    /// that is the difference between opening the application in one gesture and in two, and the second of
    /// the two was a button whose entire content was "yes, do the thing you already know I want".
    /// </para>
    /// <para>
    /// <b>Only at launch.</b> A lock the user asked for is not answered with a request to unlock — that
    /// turns LOCK into a control that appears to do nothing, and worse, trains the reflex of authenticating
    /// at a prompt that appeared without being asked for. So the offer belongs to the locked screen the
    /// process started on and to no other.
    /// </para>
    /// <para>
    /// <b>Once.</b> A declined gesture leaves the passphrase box exactly where it was, which is the whole
    /// fallback — and a prompt that reappeared after being dismissed would be a modal the user cannot get
    /// out of to type into it.
    /// </para>
    /// <para>
    /// Nothing here needs a failure path. <c>UnlockWithDeviceAsync</c> turns every refusal into a status
    /// line, and a phone with no enrolled fingerprint never gets here at all, because
    /// <c>CanUnlockWithDevice</c> already asked the keystore.
    /// </para>
    /// </remarks>
    private void TryOfferDeviceUnlock()
    {
        if (!thisLockIsTheLaunch || offeredDeviceUnlock)
        {
            return;
        }

        if (shell is not { State: ShellState.Locked, CanUnlockWithDevice: true, IsBusy: false } current)
        {
            return;
        }

        offeredDeviceUnlock = true;
        current.UnlockWithDeviceCommand.Execute(null);
    }

    /// <inheritdoc />
    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
    {
        base.OnAttachedToVisualTree(e);

        if (TopLevel.GetTopLevel(this) is { } top)
        {
            top.BackRequested += OnBackRequested;

            keyboard = top.InputPane;

            if (keyboard is not null)
            {
                keyboard.StateChanged += OnKeyboardChanged;
                ApplyKeyboardInset(keyboard);
            }
        }
    }

    /// <inheritdoc />
    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
    {
        if (TopLevel.GetTopLevel(this) is { } top)
        {
            top.BackRequested -= OnBackRequested;
        }

        if (keyboard is not null)
        {
            keyboard.StateChanged -= OnKeyboardChanged;
            keyboard = null;
        }

        base.OnDetachedFromVisualTree(e);
    }

    private void OnKeyboardChanged(object? sender, InputPaneStateEventArgs e)
        => ApplyKeyboardInset(e.NewState is InputPaneState.Open ? e.EndRect.Height : 0);

    private void ApplyKeyboardInset(IInputPane pane)
        => ApplyKeyboardInset(pane.State is InputPaneState.Open ? pane.OccludedRect.Height : 0);

    /// <summary>
    /// Holds the phone's whole interface clear of the software keyboard.
    /// </summary>
    /// <remarks>
    /// <para>
    /// <b>Here rather than on each screen, because the keyboard is not a screen's business.</b> Five of them
    /// have a box that can be typed into and every one of them would need the same handler; a sixth added
    /// later would silently not have it. Everything the phone draws is inside <c>Body</c>, so one bottom
    /// margin shortens all of them at once — which is the same thing the window resizing would have done,
    /// and is why the two paths below never both apply.
    /// </para>
    /// <para>
    /// <b>Two paths, one of which is dead on any given device.</b> Before Android 15, the activity's
    /// <c>AdjustResize</c> makes the platform shorten the window itself and the keyboard inset reaches
    /// Avalonia already consumed — this measures zero and the margin stays where it is. From Android 15 the
    /// window is no longer resized for the keyboard at all, edge-to-edge being enforced, and the inset is
    /// reported instead: that is the number applied here. Adding a margin on top of a window that had
    /// already shrunk would strand the interface an entire keyboard above the keyboard, which is why the
    /// value is taken from the inset alone and never from both.
    /// </para>
    /// <para>
    /// Scrolling the box back into view is deliberately not done here. <c>ScrollViewer</c> already brings a
    /// newly focused child into view, and every screen with a box on it is inside one; what it cannot know
    /// is that the visible region shrank *after* the focus. So the trigger is the resize this margin causes
    /// — see <see cref="OnBodyResized"/> — and not this method, which would run a layout pass too early to
    /// have anything to scroll to.
    /// </para>
    /// </remarks>
    private void ApplyKeyboardInset(double occluded)
    {
        var inset = double.IsFinite(occluded) ? Math.Max(occluded, 0) : 0;

        if (Math.Abs(body.Margin.Bottom - inset) > 0.5)
        {
            body.Margin = new Thickness(0, 0, 0, inset);
        }
    }

    /// <summary>
    /// Answers a resize: which chrome this surface wears, and where the keyboard left the focused box.
    /// </summary>
    /// <remarks>
    /// <para>
    /// ◆ <b>Two jobs, and the chrome's is unconditional while the keyboard's is not.</b> A rotation, an
    /// unfold and Android's freeform window all arrive here and all of them can cross the width at which
    /// this surface stops being a phone — so <see cref="RefreshChrome"/> runs first and runs always. What
    /// follows it is the older job, and the early return below belongs to that one alone: it used to be the
    /// first thing in this method, which would have meant a foldable opening to a bottom bar until somebody
    /// typed something.
    /// </para>
    /// <para>
    /// The one moment the scroll is needed is the one no other handler sees: the box was focused while the
    /// whole screen was available, and the space it sits in shrank afterwards. Both ways of losing that
    /// space end here — the margin applied above, and the platform shortening the window on Android 14 and
    /// earlier — which is why the resize is the trigger rather than either of the two things that cause it.
    /// </para>
    /// <para>
    /// Posted rather than called, and at <c>Loaded</c> priority, because the size change is raised during
    /// the layout pass that caused it: asking a <c>ScrollViewer</c> to scroll to a child whose new bounds
    /// have not been written yet scrolls to where the child used to be.
    /// </para>
    /// <para>
    /// Only while the keyboard is up. Every rotation and every screen change resizes this control too, and
    /// a shell that scrolled to the focused control on each of them would be a shell that moves under you.
    /// </para>
    /// </remarks>
    private void OnBodyResized(object? sender, SizeChangedEventArgs e)
    {
        RefreshChrome();

        if (keyboard is not { State: InputPaneState.Open })
        {
            return;
        }

        Dispatcher.UIThread.Post(
            () =>
            {
                // Whatever holds focus, not the passphrase box by name: this runs for eleven screens and
                // the one the keyboard is up for is the only one that can answer which box that is.
                if (TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() is Control focused)
                {
                    focused.BringIntoView();
                }
            },
            DispatcherPriority.Loaded);
    }

    /// <summary>
    /// Takes the system back gesture up the hierarchy rather than out of the application.
    /// </summary>
    /// <remarks>
    /// <para>
    /// v2 is the first arrangement here with a second level: six destinations sit behind MORE, each with
    /// its own back arrow. Android's back is the same gesture as that arrow and users reach for it first,
    /// and left unhandled it does not go up — it finishes the activity. Ending the application from a log
    /// screen is not a plausible reading of "back".
    /// </para>
    /// <para>
    /// Handled in the order the interface is stacked, not by screen alone: the terminal is a surface over a
    /// page, so it is dismissed before the page under it is considered. Setting <c>Handled</c> is what stops
    /// the event being re-dispatched to the activity's own default.
    /// </para>
    /// <para>
    /// <b>Two things it deliberately does not do.</b> It does not answer a host-key prompt — those are
    /// decisions with two named buttons, and a gesture that dismissed one would be the swipe-to-dismiss this
    /// head refused when it made the changed-key refusal a full-screen panel rather than a sheet. And from
    /// the host list it does nothing at all, so back still leaves the application from the screen the
    /// application opens on, which is what every other Android app does.
    /// </para>
    /// <para>
    /// <b>v3 adds one guard above the switch rather than another case inside it.</b> The switch's first case
    /// is the membership test of <see cref="MainWindowViewModel.IsMoreSurface"/> minus <c>More</c> itself,
    /// and has to stay in step with it — a screen added to the hub and not to that case would trap the user
    /// on it. An editor is not a screen and has no entry there. It is also strictly nearer: the add sheet
    /// sits over the host editor's own screen, so back has to lower whatever is topmost before it considers
    /// moving between screens at all. Closing an editor is not the same refusal as leaving a host-key
    /// decision alone — an editor is abandonable by design, and the CANCEL button beside it says so.
    /// </para>
    /// <para>
    /// <b>The connect menu is a second such guard, and it matters more than the first.</b> A terminal now
    /// fills the screen — no header, no bottom bar — so while that menu is up this gesture is the only way
    /// off it other than the scrim and CANCEL. It is checked before the terminal is dismissed for the
    /// reason it is drawn over it: back takes the topmost thing, and dismissing the surface underneath a
    /// menu would take two, neither of them the one being looked at.
    /// </para>
    /// </remarks>
    private void OnBackRequested(object? sender, RoutedEventArgs e)
    {
        if (shell is not { State: ShellState.Unlocked } current)
        {
            return;
        }

        // A decision is on screen. Leave it alone — see the remark.
        if (current.Vault is { HasPendingHostKey: true } or { HasHostKeyMismatch: true }
            || current.Transfers is { HasPendingHostKey: true } or { HasHostKeyMismatch: true })
        {
            return;
        }

        // The connect menu, which is raised from the terminal's own bar and is the topmost thing the phone
        // draws while it is up. Ahead of the editors below because it is nearer, and ahead of leaving the
        // terminal because a gesture that dismissed the surface underneath a menu would close two things at
        // once — and the one the user was looking at would not be either of them.
        if (current.IsConnectSheetOpen)
        {
            current.CloseConnectSheetCommand.Execute(null);
            e.Handled = true;
            return;
        }

        if (TryCloseAnOpenEditor(current))
        {
            e.Handled = true;
            return;
        }

        if (!current.IsShowingPages)
        {
            current.ShowScreenCommand.Execute(current.Screen);
            e.Handled = true;
            return;
        }

        switch (current.Screen)
        {
            case ShellScreen.Snippets or ShellScreen.Logs or ShellScreen.Transfers
                or ShellScreen.Buckets or ShellScreen.Preferences or ShellScreen.Vaults
                or ShellScreen.Keychain:

                // ◆ The hub, or Hosts where there is no hub. A wide surface draws the rail instead and
                // never draws SETTINGS, so backing out to it would land on a screen with no way off it but
                // a second back — and the arrows in these screens' own headers are hidden there for the
                // same reason. Home is where back goes when the thing you came from is not on the surface.
                current.ShowScreenCommand.Execute(IsWide ? ShellScreen.Hosts : ShellScreen.More);
                e.Handled = true;
                break;

            case ShellScreen.More:
                current.ShowScreenCommand.Execute(ShellScreen.Hosts);
                e.Handled = true;
                break;

            default:
                // Hosts, and anything the phone does not draw. Left unhandled, so back leaves the app.
                break;
        }
    }

    /// <summary>
    /// Lowers whatever the hosts screen has raised over its list, topmost first.
    /// </summary>
    /// <returns>Whether anything was closed, and so whether back has been spent.</returns>
    /// <remarks>
    /// <para>
    /// Order is the whole of it. The sheets sit over the list, the panels sit above it and the editors sit
    /// in place of it, so a sheet has to go first — closing an editor while a sheet was open would leave the
    /// sheet floating over a list nobody asked to see, and the second back would then close the sheet rather
    /// than the editor the user was looking at.
    /// </para>
    /// <para>
    /// The editors are cancelled rather than merely hidden. Cancelling is what clears the boxes, and the
    /// host editor's boxes are the ones worth clearing: leaving a half-typed hostname behind would have the
    /// next NEW HOST open on somebody else's abandoned draft.
    /// </para>
    /// <para>
    /// ◆ <b>Selection mode is last and is still a thing back has to spend itself on.</b> It is a mode rather
    /// than a surface — the list underneath is fully drawn and the only sign of it is the bar across the top
    /// — and a gesture that left the application from it would take somebody out of the app because they had
    /// held a row down. Its panels go before it, in the order they are stacked: the picker or the question is
    /// what the user is looking at, and the ticks underneath are what it is about.
    /// </para>
    /// </remarks>
    private static bool TryCloseAnOpenEditor(MainWindowViewModel current)
    {
        if (current.Vault is not { } vault)
        {
            return false;
        }

        return TryLowerASheet(vault) || TryCloseSomethingBehindTheSheets(vault);
    }

    /// <summary>Lowers the nearest of the four sheets, which are what sits over everything else.</summary>
    /// <remarks>
    /// The four cannot be open at once — each is raised from a control the others hide — so their order
    /// between themselves decides nothing. What matters is that all of them come before the panels and the
    /// editors underneath: closing an editor while a sheet was open would leave the sheet floating over a
    /// list nobody asked to see.
    /// </remarks>
    private static bool TryLowerASheet(VaultViewModel vault)
    {
        // ◆ The action bar's own menu, first of the four because it is raised from chrome that is already
        // over everything else.
        if (vault.IsHostActionSheetOpen)
        {
            vault.CloseHostActionSheetCommand.Execute(null);
            return true;
        }

        // The password sheet, which is what a tap on a machine that wants one raises. Cancelled rather than
        // hidden, because cancelling is what empties the box — see VaultViewModel.CancelConnectPassword.
        if (vault.IsAskingForConnectPassword)
        {
            vault.CancelConnectPasswordCommand.Execute(null);
            return true;
        }

        if (vault.IsAddSheetOpen)
        {
            vault.CloseAddSheetCommand.Execute(null);
            return true;
        }

        if (vault.GroupSheet is not null)
        {
            vault.CloseGroupSheetCommand.Execute(null);
            return true;
        }

        return false;
    }

    /// <summary>Closes the nearest of the panels, the editors and selection mode itself.</summary>
    /// <inheritdoc cref="TryCloseAnOpenEditor" path="/remarks" />
    private static bool TryCloseSomethingBehindTheSheets(VaultViewModel vault)
    {
        if (vault.IsSendingChosenHostsToAVault)
        {
            vault.CancelSendChosenHostsToAVaultCommand.Execute(null);
            return true;
        }

        if (vault.IsRegroupingChosenHosts)
        {
            vault.CancelRegroupChosenHostsCommand.Execute(null);
            return true;
        }

        if (vault.IsConfirmingChosenHostDeletion)
        {
            vault.CancelDeleteCommand.Execute(null);
            return true;
        }

        if (vault.IsEditing)
        {
            vault.CancelEditCommand.Execute(null);
            return true;
        }

        if (vault.IsEditingGroup)
        {
            vault.CancelGroupEditCommand.Execute(null);
            return true;
        }

        if (vault.IsChoosingHosts)
        {
            vault.ClearHostChoiceCommand.Execute(null);
            return true;
        }

        return false;
    }

    /// <summary>
    /// Blocks screenshots and screen recording while the recovery code is on screen.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The recovery screen says screenshots are blocked, and this is what makes that true rather than a
    /// claim. <c>FLAG_SECURE</c> is a window flag — no control can set it — so it lives here, on the one
    /// object that has the activity.
    /// </para>
    /// <para>
    /// <b>It is worth being clear about what this buys.</b> It stops the obvious accident — a screenshot
    /// of the only copy of an unrecoverable code landing in a cloud photo library — and it excludes the
    /// screen from the recent-apps thumbnail, which is the part users never think about. It stops nothing
    /// determined: a second phone photographs the screen perfectly well. The code is meant to be written
    /// down, and this only pushes people away from the one place it must not be written down to.
    /// </para>
    /// <para>
    /// Lowered again afterwards rather than left on. Leaving it set would make the terminal unscreenshotable
    /// too, and a screenshot of a shell is a thing people legitimately want.
    /// </para>
    /// </remarks>
    private static void ApplyScreenshotPolicy(ShellState state)
    {
        if (PhoneEnvironment.CurrentActivity?.Window is not { } window)
        {
            return;
        }

        if (state is ShellState.ShowingRecoveryCode)
        {
            window.SetFlags(WindowManagerFlags.Secure, WindowManagerFlags.Secure);
        }
        else
        {
            window.ClearFlags(WindowManagerFlags.Secure);
        }
    }
}
