using System.Text;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;

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

// Avalonia's Button, not Android.Widget's. .NET for Android puts Android.Widget in this project's
// implicit usings, so the bare name is ambiguous — the same collision the namespace itself causes
// for Android.App. See the note at the top of MainActivity.
using Application = Avalonia.Application;
using Button = Avalonia.Controls.Button;

namespace DodoSSH.Client.Android.Views;

/// <summary>Design 03 — the terminal, and the accessory key row under it.</summary>
internal sealed partial class TerminalScreen : UserControl
{
    /// <summary>
    /// The keys a software keyboard does not have.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Chosen by what a shell session actually needs rather than by what a keyboard has: Esc leaves vi's
    /// insert mode, Tab completes a path, Ctrl makes <c>^C</c> reachable, the arrows reach history, and the
    /// pipe and hyphen are two characters that are three taps deep on every Android keyboard and appear in
    /// almost every command worth typing on a phone.
    /// </para>
    /// <para>
    /// The order matches the design's row. It is scrollable rather than compressed, because shrinking ten
    /// keys to fit 360dp produces ten targets too small to hit.
    /// </para>
    /// </remarks>
    private static readonly (string Label, byte[] Bytes, bool Latches)[] Keys =
    [
        ("esc", [0x1B], false),
        ("tab", [0x09], false),
        ("ctrl", [], true),
        ("|", [(byte)'|'], false),
        ("-", [(byte)'-'], false),
        ("~", [(byte)'~'], false),
        ("/", [(byte)'/'], false),

        // The cursor keys, as the ANSI sequences a PTY expects. Written out rather than composed, because
        // the difference between CSI A and SS3 A is the difference between working in bash and not.
        ("↑", [0x1B, (byte)'[', (byte)'A'], false),
        ("↓", [0x1B, (byte)'[', (byte)'B'], false),
        ("←", [0x1B, (byte)'[', (byte)'D'], false),
        ("→", [0x1B, (byte)'[', (byte)'C'], false),
    ];

    /// <summary>
    /// Whether the next ordinary key should be sent as a control character.
    /// </summary>
    /// <remarks>
    /// Latching rather than held. Holding a modifier while typing needs two hands and a keyboard that
    /// reports chords, and this row has neither — so Ctrl is pressed, then C, and releases itself. Every
    /// Android SSH client does this and users expect it.
    /// </remarks>
    private bool controlLatched;

    private Button? controlKey;

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

        BuildAccessoryRow();

        var renderer = this.FindControl<NativeWebView>("Renderer")!;

        renderer.EnvironmentRequested += OnRendererEnvironmentRequested;

        // The URL is only known once the loopback listener has bound a port, so it cannot be set in XAML.
        DataContextChanged += (_, _) =>
        {
            if (DataContext is MainWindowViewModel shell)
            {
                renderer.Source = shell.TerminalPageUrl;
            }
        };
    }

    /// <summary>
    /// Puts the keyboard away on the way to the connect menu.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A handler beside the button's command rather than something the command does, because the command is
    /// on the shared view model and this is a platform call: the desktop head has no software keyboard to
    /// put away, and a shell view model that knew about Android's input-method manager would be the first
    /// thing in that layer that did.
    /// </para>
    /// <para>
    /// <b>The keyboard is up in the ordinary case.</b> This button sits above a terminal somebody is typing
    /// into, so the sheet it raises would otherwise arrive underneath a keyboard covering half the display —
    /// and the three rows on it are the bottom half. Worse, the keyboard's inset shortens everything the
    /// phone draws (see <c>PhoneShell.ApplyKeyboardInset</c>), so the sheet would be laid out into the
    /// remaining strip rather than merely hidden behind it.
    /// </para>
    /// <para>
    /// Click and Command both fire for one press and their order is not something to rely on. Neither reads
    /// what the other writes: this one talks to the platform and the command sets a flag, so they commute.
    /// </para>
    /// </remarks>
    private void OnConnectMenuRequested(object? sender, RoutedEventArgs e) => SoftKeyboard.Hide();

    /// <summary>
    /// Takes the browser gestures back off a surface that is not a web page.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A terminal is a fixed grid that the fit addon sizes to the window. Pinch-zoom breaks that in both
    /// directions at once: the visible width stops matching the column count the remote was told about, so
    /// wrapping goes wrong, and the part of the grid under the thumb is no longer the part that gets the
    /// tap. It is the WebView's own zoom rather than anything the page asked for.
    /// </para>
    /// <para>
    /// This is the knob that works. <c>user-scalable=no</c> in the page's viewport tag does not: Blink has
    /// ignored it since Chrome 48 for accessibility reasons and WebView follows Blink. The page still
    /// carries the viewport tag, for the layout width rather than the zoom — see WebAssets/terminal.html.
    /// </para>
    /// </remarks>
    private static void OnRendererEnvironmentRequested(object? sender, EventArgs e)
    {
        if (e is AndroidWebViewEnvironmentRequestedEventArgs android)
        {
            android.BuiltInZoomControls = false;
        }
    }

    private void BuildAccessoryRow()
    {
        var row = this.FindControl<StackPanel>("AccessoryKeys")!;

        // Both halves of a press steal Android's own focus — the platform requests it for Avalonia's view
        // after dispatching every handled touch, DOWN and UP alike; TerminalFocus carries the decompiled
        // citation. Countered at the row rather than inside each key's Click, and for two reasons: Click
        // only exists for the UP half, so a keyboard detached at DOWN would stay detached for the whole
        // length of the press; and the Return is posted past the current dispatch, so its ordering against
        // the key's own handler does not matter — which is what lets one pair of handlers cover ten keys.
        row.AddHandler(PointerPressedEvent, (_, _) => TerminalFocus.Return(), RoutingStrategies.Tunnel);
        row.AddHandler(PointerReleasedEvent, (_, _) => TerminalFocus.Return(), RoutingStrategies.Tunnel);

        foreach (var (label, bytes, latches) in Keys)
        {
            var key = CreateKey(label);

            if (latches)
            {
                controlKey = key;
                key.Click += (_, _) => ToggleControl();
            }
            else
            {
                key.Click += (_, _) => SendAsync(bytes);
            }

            row.Children.Add(key);
        }
    }

    /// <summary>One key of the accessory row, before its click is wired.</summary>
    /// <remarks>Split from <see cref="BuildAccessoryRow"/> for length rather than for reuse.</remarks>
    private static Button CreateKey(string label) =>
        new()
        {
            Content = new TextBlock
            {
                Text = label,
                FontFamily = (FontFamily)Application.Current!.FindResource("MonoFont")!,
                FontSize = 11,
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center,
            },

            // 44 wide, and that is the number that matters: the design draws these flexed across the
            // width, which at 360dp with ten keys is 32 pixels each — under every thumb-target
            // guideline there is. The height came down with the row it sits in, from 38 to 30, and it
            // costs nothing a width does: the keys are a single row with the terminal above and the
            // system's gesture bar below, so there is no neighbour a short press can land on instead.
            MinWidth = 44,
            Height = 30,
            Padding = new Thickness(10, 0),
            CornerRadius = new CornerRadius(9),
            Background = Palette("Panel"),
            BorderBrush = Palette("BorderMid"),
            BorderThickness = new Thickness(1),
            Foreground = Palette("TextDim"),
            HorizontalContentAlignment = HorizontalAlignment.Center,

            // ◆ NOT FOCUSABLE, AND THAT IS THE WHOLE CONTROL RATHER THAN A DETAIL.
            //
            // These keys are an extension of the keyboard, not a place the keyboard should go. As
            // ordinary buttons they took Avalonia's focus on tap, which takes it off the NativeWebView
            // — and the package's own OnLostFocus then calls the adapter's ResignFocus(). So pressing
            // Tab or an arrow handed the terminal one byte and took the keyboard away from it: the next
            // thing typed on a hardware keyboard went nowhere, and the row went on working because its
            // buttons are pressed rather than typed into, which is what makes it look like the terminal
            // had died instead.
            //
            // Focusable=false is what a toolbar button is, and it means the focused element never
            // changes: the WebView is still it, so nothing resigns and nothing has to be handed back.
            //
            // At the Avalonia layer, that is. Android keeps a focus of its own, and the touch that
            // presses one of these keys hands it to Avalonia's input view regardless of what Avalonia
            // decides about its element — taking the keyboard's input connection off the terminal and
            // swapping its layout mid-typing. The row's own pointer handlers in BuildAccessoryRow hand
            // that half back; see TerminalFocus for the whole story, including why the handing back has
            // to be posted rather than done inline.
            Focusable = false,
        };

    private void ToggleControl()
    {
        controlLatched = !controlLatched;

        if (controlKey is null)
        {
            return;
        }

        // Latched state has to be visible. A modifier that is on and does not look on is how somebody sends
        // ^L to a database prompt believing they typed an l.
        controlKey.Background = Palette(controlLatched ? "Active" : "Panel");
        controlKey.Foreground = Palette(controlLatched ? "AccentText" : "TextDim");
    }

    /// <summary>One brush from <c>Theme/Palette.axaml</c>, by key.</summary>
    /// <remarks>
    /// The accessory row is built in code because its keys come from a table, so its colours cannot be set in
    /// XAML with the rest of the screen's. Resolving them by name is the next best thing: a row that named
    /// its own blues is how the palette ends up with a fifth surface nobody meant to add.
    /// </remarks>
    private static IBrush? Palette(string key) => Application.Current?.FindResource(key) as IBrush;

    /// <remarks>
    /// The control translation is the ASCII one and nothing cleverer: Ctrl-A through Ctrl-Z are 0x01 to
    /// 0x1A, which is letter &amp; 0x1F. Applied only to letters, because Ctrl with an arrow key is a
    /// different sequence entirely and silently mangling one into a control byte would be worse than
    /// ignoring the latch.
    /// </remarks>
    private void SendAsync(byte[] bytes)
    {
        if (DataContext is not MainWindowViewModel { SelectedTab: { } tab } shell)
        {
            return;
        }

        var payload = bytes;

        if (controlLatched && bytes.Length == 1)
        {
            var c = bytes[0];

            if (c is >= (byte)'a' and <= (byte)'z' or >= (byte)'A' and <= (byte)'Z')
            {
                payload = [(byte)(c & 0x1F)];
            }

            ToggleControl();
        }

        // Discarded rather than awaited: this is a keystroke, the workspace ignores a session that has
        // gone, and a button handler that awaited would serialise the row behind a slow link.
        _ = shell.SendTerminalInputAsync(tab.SessionId, payload).AsTask();
    }

    /// <summary>Sends a literal string, for the keys that carry text rather than a control code.</summary>
    /// <remarks>Kept because the snippet feature will type into a terminal exactly this way.</remarks>
    internal void Send(string text) => SendAsync(Encoding.UTF8.GetBytes(text));
}
