using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using DodoSSH.Client.Shell.ViewModels;

namespace DodoSSH.Client.App.Views;

/// <summary>
/// The two-pane file browser and the transfer queue.
/// </summary>
/// <remarks>
/// <para>
/// Its data context is the <c>TransfersViewModel</c>, which the shell owns for the life of the process — a
/// transfer in flight has to survive a lock, the same policy that keeps shells running. See
/// <c>MainWindowViewModel.LockAsync</c>.
/// </para>
/// <para>
/// <b>Everything about drag and drop is in this file and nothing about it is policy.</b> The handlers pull
/// paths or rows out of a drop and hand them to <c>QueueUploads</c>/<c>QueueDownloads</c>; what may be
/// queued, what is skipped and what is said about it all live in the view model, where they can be tested
/// without a window. Nothing headless can synthesise a real platform drag, so the wiring below is verified
/// by hand — see <c>docs/manual-checks.md</c>.
/// </para>
/// </remarks>
internal sealed partial class TransfersScreen : UserControl
{
    /// <summary>
    /// How remote rows travel while being dragged.
    /// </summary>
    /// <remarks>
    /// An in-process format, so the rows themselves cross rather than a list of path strings that would
    /// have to be looked up again on the other side. It also cannot be confused with a drop from the
    /// operating system: a file dragged out of the file manager arrives as <c>DataFormat.File</c> and never
    /// as this, so "did this come from our own remote pane" needs no guessing.
    /// </remarks>
    private static readonly DataFormat<RemoteDragPayload> RemoteEntries =
        DataFormat.CreateInProcessFormat<RemoteDragPayload>("dodossh/remote-entries");

    /// <summary>How far the pointer moves before a press becomes a drag.</summary>
    /// <remarks>
    /// Without a threshold every click on a row starts a drag, which makes selecting one impossible.
    /// </remarks>
    private const double DragThreshold = 4;

    private PointerPressedEventArgs? pressed;
    private Point pressedAt;

    public TransfersScreen()
    {
        InitializeComponent();

        // Wired here rather than in the markup because it is a gesture rather than a binding, and because
        // opening a directory has to be reachable without the mouse as well: both lists are ListBoxes, so
        // Enter on a keyboard-navigated row goes through the same commands from the buttons above them.
        LocalList.DoubleTapped += OnLocalActivated;
        RemoteList.DoubleTapped += OnRemoteActivated;

        // On the pane rather than on the list. A directory with nothing in it lays its ListBox out at zero
        // height behind the empty-state sentence, and a drop handler on the list would have nothing to hit.
        LocalPane.AddHandler(DragDrop.DragOverEvent, OnLocalDragOver);
        LocalPane.AddHandler(DragDrop.DragLeaveEvent, OnLocalDragLeave);
        LocalPane.AddHandler(DragDrop.DropEvent, OnLocalDrop);

        RemotePane.AddHandler(DragDrop.DragOverEvent, OnRemoteDragOver);
        RemotePane.AddHandler(DragDrop.DragLeaveEvent, OnRemoteDragLeave);
        RemotePane.AddHandler(DragDrop.DropEvent, OnRemoteDrop);

        // Tunnelling, so noting where a press started does not take the press away from the ListBox — a row
        // still selects, and the drag only begins once the pointer has moved far enough.
        foreach (var list in new Control[] { LocalList, RemoteList })
        {
            list.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
            list.AddHandler(PointerMovedEvent, OnPointerMoved, RoutingStrategies.Tunnel);
            list.AddHandler(PointerReleasedEvent, OnPointerReleased, RoutingStrategies.Tunnel);
        }
    }

    private TransfersViewModel? Transfers => DataContext as TransfersViewModel;

    private void OnLocalActivated(object? sender, TappedEventArgs e)
    {
        if (Transfers is { } transfers)
        {
            transfers.OpenLocalCommand.Execute(null);
        }
    }

    /// <remarks>
    /// Fire-and-forget, which is what a double-click on a directory can be: the command reports its own
    /// failures onto the screen's status line, and awaiting it here would mean an event handler that returns
    /// a task nothing observes — the same thing with a warning suppressed.
    /// </remarks>
    private void OnRemoteActivated(object? sender, TappedEventArgs e)
    {
        if (Transfers is { } transfers)
        {
            _ = transfers.OpenRemoteCommand.ExecuteAsync(null);
        }
    }

    // ---- Starting a drag ----

    private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
    {
        if (e.GetCurrentPoint(this).Properties.PointerUpdateKind is PointerUpdateKind.LeftButtonPressed)
        {
            pressed = e;
            pressedAt = e.GetPosition(this);
        }
    }

    private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) => pressed = null;

    /// <remarks>
    /// The drag starts here rather than on the press, because a press is also how a row is selected.
    /// <c>DoDragDropAsync</c> wants the original <c>PointerPressedEventArgs</c>, so it is held from the
    /// press until either the pointer moves far enough or the button comes back up.
    /// </remarks>
    private void OnPointerMoved(object? sender, PointerEventArgs e)
    {
        if (pressed is not { } origin || Transfers is not { } transfers)
        {
            return;
        }

        if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
        {
            pressed = null;
            return;
        }

        var moved = e.GetPosition(this) - pressedAt;

        if (Math.Abs(moved.X) < DragThreshold && Math.Abs(moved.Y) < DragThreshold)
        {
            return;
        }

        pressed = null;

        if (ReferenceEquals(sender, RemoteList))
        {
            StartRemoteDrag(origin, transfers);
        }
        else
        {
            _ = StartLocalDragAsync(origin, transfers);
        }
    }

    private static void StartRemoteDrag(PointerPressedEventArgs origin, TransfersViewModel transfers)
    {
        if (transfers.SelectedRemoteEntry is not { } row)
        {
            return;
        }

        using var transfer = new DataTransfer();
        transfer.Add(DataTransferItem.Create(RemoteEntries, new RemoteDragPayload([row])));

        _ = DragDrop.DoDragDropAsync(origin, transfer, DragDropEffects.Copy);
    }

    /// <remarks>
    /// Local files travel as the platform's own file format rather than as an in-process one, which is what
    /// makes a single drag work both onto the remote pane and out into the file manager. It needs a real
    /// <see cref="IStorageItem"/>, hence the asynchronous lookup — and hence a fire-and-forget call, because
    /// nothing on a pointer-moved path can await.
    /// </remarks>
    private async Task StartLocalDragAsync(PointerPressedEventArgs origin, TransfersViewModel transfers)
    {
        if (transfers.SelectedLocalEntry is not { IsFile: true } row
            || TopLevel.GetTopLevel(this) is not { } top)
        {
            return;
        }

        var file = await top.StorageProvider.TryGetFileFromPathAsync(row.FullPath).ConfigureAwait(true);

        if (file is null)
        {
            return;
        }

        using var transfer = new DataTransfer();
        transfer.Add(DataTransferItem.CreateFile(file));

        await DragDrop.DoDragDropAsync(origin, transfer, DragDropEffects.Copy).ConfigureAwait(true);
    }

    // ---- Accepting a drop ----

    /// <remarks>
    /// The local pane takes remote rows and nothing else. A file dragged from the file manager onto it
    /// would be a copy from this machine to this machine, which is not what this screen is for.
    /// </remarks>
    private void OnLocalDragOver(object? sender, DragEventArgs e)
    {
        var accepted = e.DataTransfer.Contains(RemoteEntries);

        e.DragEffects = accepted ? DragDropEffects.Copy : DragDropEffects.None;

        if (Transfers is { } transfers)
        {
            transfers.IsLocalDropTarget = accepted;
        }

        e.Handled = true;
    }

    private void OnLocalDragLeave(object? sender, DragEventArgs e)
    {
        if (Transfers is { } transfers)
        {
            transfers.IsLocalDropTarget = false;
        }
    }

    private void OnLocalDrop(object? sender, DragEventArgs e)
    {
        if (Transfers is not { } transfers)
        {
            return;
        }

        transfers.IsLocalDropTarget = false;
        e.Handled = true;

        if (e.DataTransfer.TryGetValue(RemoteEntries) is { } payload)
        {
            transfers.QueueDownloads(payload.Rows);
        }
    }

    /// <remarks>
    /// The remote pane takes files: from the file manager, and from the local pane, which offers the same
    /// platform format. A drop while disconnected is refused visibly rather than accepted and then
    /// explained, because a red pane under the pointer is the answer arriving before the drop rather than
    /// after it.
    /// </remarks>
    private void OnRemoteDragOver(object? sender, DragEventArgs e)
    {
        var files = e.DataTransfer.Contains(DataFormat.File);
        var connected = Transfers is { IsConnected: true };

        e.DragEffects = files && connected ? DragDropEffects.Copy : DragDropEffects.None;

        if (Transfers is { } transfers)
        {
            transfers.IsRemoteDropTarget = files && connected;
            transfers.IsRemoteDropRefused = files && !connected;
        }

        e.Handled = true;
    }

    private void OnRemoteDragLeave(object? sender, DragEventArgs e) => ClearRemoteHighlight();

    private void OnRemoteDrop(object? sender, DragEventArgs e)
    {
        if (Transfers is not { } transfers)
        {
            return;
        }

        ClearRemoteHighlight();
        e.Handled = true;

        if (e.DataTransfer.TryGetFiles() is not { } files)
        {
            return;
        }

        // TryGetLocalPath, because the queue reads bytes off a real path. A storage item that is not a
        // local file — one from a cloud provider's virtual folder — has none, and dropping it is a thing
        // this screen declines rather than a thing it half does.
        var paths = files
            .Select(file => file.TryGetLocalPath())
            .OfType<string>()
            .ToList();

        transfers.QueueUploads(paths);
    }

    private void ClearRemoteHighlight()
    {
        if (Transfers is { } transfers)
        {
            transfers.IsRemoteDropTarget = false;
            transfers.IsRemoteDropRefused = false;
        }
    }
}

/// <summary>
/// The remote rows carried by one drag.
/// </summary>
/// <remarks>
/// A record wrapping the list rather than the list itself, because <c>DataFormat.CreateInProcessFormat</c>
/// keys on the type and a bare <c>IReadOnlyList&lt;T&gt;</c> is too general a key to be sure of.
/// </remarks>
internal sealed record RemoteDragPayload(IReadOnlyList<RemoteEntryRowViewModel> Rows);
