using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using DragonISO.Engine.Controller;
using DragonISO.Engine.Pipeline;

namespace DragonISO.App;

/// <summary>
/// Non-modal floating preview window for a single participant. Shows the
/// engine's most recent <see cref="ProcessedFrame"/> at a higher refresh
/// rate (~20Hz) than the participants DataGrid thumbnails (1Hz). Multi-
/// monitor friendly: operator drags it to a second display, leaves the
/// main Dragon-ISO window on the primary.
///
/// Uses <see cref="WriteableBitmap"/> with <see cref="WriteableBitmap.WritePixels(Int32Rect, IntPtr, int, int)"/>
/// â€” the engine produces full-resolution BGRA frames so we can write them
/// straight into the bitmap without scaling. WPF's Image control with
/// Stretch=Uniform handles aspect-correct fit to the window size.
/// </summary>
public partial class PreviewWindow : Window
{
    private readonly IIsoController _controller;
    private readonly Guid _participantId;
    private readonly DispatcherTimer _refreshTimer;
    private WriteableBitmap? _bitmap;
    private int _lastWidth;
    private int _lastHeight;

    public PreviewWindow(IIsoController controller, Guid participantId, string displayName)
    {
        InitializeComponent();
        _controller = controller;
        _participantId = participantId;
        TitleText.Text = displayName;

        _refreshTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
        {
            // 50ms = 20Hz. High enough for a smooth-feeling preview without
            // hogging the dispatcher; still cheap because each refresh is just
            // a memcpy from the engine's last frame into our pinned BackBuffer.
            Interval = TimeSpan.FromMilliseconds(50),
        };
        _refreshTimer.Tick += OnTick;
        Loaded += (_, _) => _refreshTimer.Start();
        Closed += (_, _) =>
        {
            _refreshTimer.Stop();
            _refreshTimer.Tick -= OnTick;
        };
    }

    private void OnClose(object sender, RoutedEventArgs e) => Close();

    private void OnTick(object? sender, EventArgs e)
    {
        ProcessedFrame? frame;
        try { frame = _controller.GetLatestProcessedFrame(_participantId); }
        catch { return; }
        if (frame is null || frame.Pixels.IsEmpty || frame.Width <= 0 || frame.Height <= 0) return;
        if (frame.Pixels.Length < frame.Width * frame.Height * 4) return;

        // (Re)allocate the WriteableBitmap when the source resolution changes.
        // FrameProcessor normalizes to a configured target so this happens at
        // most once per session, but we still defend against switches.
        if (_bitmap is null || frame.Width != _lastWidth || frame.Height != _lastHeight)
        {
            _bitmap = new WriteableBitmap(
                frame.Width, frame.Height, 96, 96, PixelFormats.Bgra32, null);
            PreviewImage.Source = _bitmap;
            _lastWidth = frame.Width;
            _lastHeight = frame.Height;
            ResolutionText.Text = $"{frame.Width}Ã—{frame.Height}";
        }

        // WritePixels takes a buffer + stride + rect. Stride = width * 4 for
        // BGRA. We slice the ProcessedFrame's ReadOnlyMemory<byte> via .Span
        // and use the IntPtr overload via MemoryMarshal â€” but the
        // byte-array overload is simpler and the compiler picks the right
        // ToArray-free path because the engine already allocates a fresh
        // array per frame.
        unsafe
        {
            fixed (byte* p = frame.Pixels.Span)
            {
                _bitmap.WritePixels(
                    new Int32Rect(0, 0, frame.Width, frame.Height),
                    (IntPtr)p,
                    frame.Pixels.Length,
                    frame.Width * 4);
            }
        }
    }
}
