// The Android SDK's own namespaces are reached through `global::` throughout this project, and it is not
// a style choice. This assembly's root namespace ends in `Android`, so inside it a bare `Android.App`
// binds to `DodoSSH.Client.Android.App` — this head's own Avalonia application type — rather than to the
// platform. The desktop head hit the same class of collision and answered it by renaming its type; here
// the collision is in the namespace itself, so the qualification is the honest fix.
using Avalonia.Android;
using DodoSSH.Client.Android.Platform;
using global::Android.App;
using global::Android.Content;
using global::Android.Content.PM;
using global::Android.Views;

namespace DodoSSH.Client.Android;

/// <summary>
/// The launcher activity.
/// </summary>
/// <remarks>
/// <para>
/// Deliberately empty. Avalonia is configured on the application object — see
/// <see cref="DodoSshAndroidApplication"/> — and the desktop head's <c>Program.Main</c> has no counterpart
/// at all here: Android constructs the activity, and its <c>[STAThread]</c> is a WebView2 requirement that
/// means nothing on this platform.
/// </para>
/// <para>
/// <b>The ConfigurationChanges list is load-bearing.</b> Without it Android destroys and recreates the
/// activity on every rotation and every time the software keyboard appears — and this head holds live SSH
/// sessions and a terminal data plane behind a process-wide composition root. Letting the activity restart
/// would tear the Avalonia application down under them. Declaring the changes handled is what keeps a
/// shell alive across turning the phone sideways, which is the same promise the foreground service makes
/// about backgrounding, arrived at from a different direction.
/// </para>
/// <para>
/// <c>SingleTask</c> for the sign-in redirect: the authorization response comes back as an intent, and any
/// other launch mode answers it with a second copy of this activity on top of the first — which on this
/// head would mean a second Avalonia application over a live one.
/// </para>
/// <para>
/// <b><c>AdjustResize</c> is declared rather than left unspecified</b>, and it is half of how this head
/// keeps the software keyboard off the box being typed into; <c>PhoneShell</c> is the other half. Left
/// unspecified, Android chooses, and what it chooses for a window whose entire content is one native view
/// — which is what an Avalonia surface is — is to pan: it slides the window up by however much it thinks
/// the focused *native* view needs, and since that view is the whole surface, the passphrase box goes on
/// sitting under the keyboard. Resizing instead makes the window shorter, which the layout inside it can
/// answer, and a screen built around a <c>ScrollViewer</c> then scrolls the focused box into view by
/// itself. On Android 15 and later this attribute is ignored — edge-to-edge is enforced there and the
/// window is no longer resized for the keyboard — which is precisely the case PhoneShell handles from the
/// reported inset. The two are complementary and never both in effect: where the window resizes, the
/// keyboard inset arrives already consumed and measures zero.
/// </para>
/// </remarks>
[Activity(
    // The launcher reads this rather than the application's, so a channel that renamed only the
    // application element would still put two identical entries on the home screen. A resource because
    // this is a compile-time string and the two channels need different answers from one binary's
    // sources. See Resources/values/strings.xml.
    Label = "@string/app_name",
    Theme = "@style/DodoTheme",
    MainLauncher = true,
    LaunchMode = LaunchMode.SingleTask,
    WindowSoftInputMode = SoftInput.AdjustResize,
    ConfigurationChanges = ConfigChanges.Orientation
        | ConfigChanges.ScreenSize
        | ConfigChanges.ScreenLayout
        | ConfigChanges.SmallestScreenSize
        | ConfigChanges.KeyboardHidden
        | ConfigChanges.UiMode)]
// The intent filter is what makes the sign-in redirect reach this application rather than a loopback
// socket. The scheme is the reversed package name — RFC 8252 §7.1 — and it must match the constant on
// AndroidRedirectCallback exactly; they are two declarations of one fact, so the constant is referenced
// here rather than retyped and a rename cannot break only one of them.
[IntentFilter(
    [Intent.ActionView],
    Categories = [Intent.CategoryDefault, Intent.CategoryBrowsable],
    DataScheme = AndroidRedirectCallback.Scheme)]
public sealed class MainActivity : AvaloniaMainActivity
{
    /// <inheritdoc />
    /// <remarks>
    /// Resuming is also how a cancelled sign-in is noticed. There is no event for a user pressing back out
    /// of the login page — the browser simply goes away and this application is foreground again with
    /// nothing delivered — so being resumed while a sign-in is still waiting for its redirect is the
    /// signal, and the only one there is. See <see cref="AndroidRedirectCallback.Abandon"/>, which is a
    /// no-op unless a browser was opened and has not answered.
    /// </remarks>
    protected override void OnResume()
    {
        base.OnResume();

        PhoneEnvironment.CurrentActivity = this;

        AndroidRedirectCallback.Abandon();
    }

    /// <inheritdoc />
    /// <remarks>
    /// Cleared only if it is still this activity. Android may resume the next one before pausing this one,
    /// and clearing unconditionally would drop a reference the newcomer had just set.
    /// </remarks>
    protected override void OnPause()
    {
        base.OnPause();

        if (ReferenceEquals(PhoneEnvironment.CurrentActivity, this))
        {
            PhoneEnvironment.CurrentActivity = null;
        }
    }

    /// <inheritdoc />
    /// <remarks>
    /// <c>OnNewIntent</c> rather than <c>OnCreate</c>, and that is what <c>SingleTask</c> above buys: the
    /// activity is already running with a sign-in waiting inside it, so the redirect has to be delivered
    /// into that instance. Any other launch mode would start a second copy of the activity — and with it a
    /// second Avalonia application over a live one — leaving the original waiting for a response that had
    /// already been consumed.
    /// </remarks>
    protected override void OnNewIntent(Intent? intent)
    {
        base.OnNewIntent(intent);

        if (intent?.Data is { } data
            && string.Equals(data.Scheme, AndroidRedirectCallback.Scheme, StringComparison.Ordinal)
            && Uri.TryCreate(data.ToString(), UriKind.Absolute, out var redirect))
        {
            AndroidRedirectCallback.Complete(redirect);
        }
    }
}
