using Microsoft.Extensions.Logging;

namespace DragonISO.App.Services;

// GET /participants/{id}/thumbnail.bmp â€” small BMP of the latest
// processed frame. Used by the embedded HTML control panel for live
// preview tiles with a cache-busting query param at ~1Hz.
//
// BMP (not JPEG) because the System.Windows.Media.Imaging path NREs on
// non-UI threads and marshaling 1Hz JPEG encodes through the WPF
// dispatcher hurts responsiveness. ~40KB at 192-wide compresses fine
// over LAN gzip.
public sealed partial class ControlSurfaceServer
{
    /// <summary>
    /// Encode the engine's most recent processed frame for the given
    /// participant as a BMP. Returns null when no pipeline is running for
    /// this participant or the frame can't be encoded.
    /// </summary>
    private byte[]? TryEncodeThumbnailJpeg(Guid participantId)
    {
        try
        {
            var frame = _controller.GetLatestProcessedFrame(participantId);
            if (frame is null)
            {
                _logger?.LogDebug("Thumbnail: no frame for {Id}", participantId);
                return null;
            }
            if (frame.Pixels.Length == 0)
            {
                _logger?.LogDebug("Thumbnail: empty pixel buffer for {Id} ({W}x{H})", participantId, frame.Width, frame.Height);
                return null;
            }

            // Nearest-neighbor downscale to ~192 wide. Source is BGRA32.
            const int targetWidth = 192;
            var ratio = (double)frame.Height / frame.Width;
            var targetHeight = Math.Max(1, (int)(targetWidth * ratio));
            return EncodeBmpDownscaled(frame.Pixels.Span, frame.Width, frame.Height, targetWidth, targetHeight);
        }
        catch (Exception ex)
        {
            _logger?.LogWarning(ex, "Thumbnail encode failed for {Id}", participantId);
            return null;
        }
    }

    /// <summary>
    /// Nearest-neighbor downscale of a BGRA32 source then write a 32-bpp
    /// top-down BMP. Returns the full BMP file bytes including the 14-byte
    /// BMP header and 40-byte BITMAPINFOHEADER. Browsers handle it directly
    /// (no JPEG / PNG codec needed in-process).
    /// </summary>
    private static byte[] EncodeBmpDownscaled(ReadOnlySpan<byte> srcBgra, int srcW, int srcH, int dstW, int dstH)
    {
        var pixelBytes = dstW * dstH * 4;
        var bmp = new byte[54 + pixelBytes];

        // BMP file header (14 bytes): 'BM', file size, reserved, pixel offset.
        bmp[0] = (byte)'B'; bmp[1] = (byte)'M';
        WriteUInt32LE(bmp, 2, (uint)bmp.Length);
        WriteUInt32LE(bmp, 6, 0);
        WriteUInt32LE(bmp, 10, 54);

        // DIB header (BITMAPINFOHEADER, 40 bytes): negative height = top-down.
        WriteUInt32LE(bmp, 14, 40);
        WriteInt32LE(bmp, 18, dstW);
        WriteInt32LE(bmp, 22, -dstH);
        WriteUInt16LE(bmp, 26, 1);
        WriteUInt16LE(bmp, 28, 32);
        WriteUInt32LE(bmp, 30, 0);
        WriteUInt32LE(bmp, 34, (uint)pixelBytes);
        WriteUInt32LE(bmp, 38, 2835);
        WriteUInt32LE(bmp, 42, 2835);
        WriteUInt32LE(bmp, 46, 0);
        WriteUInt32LE(bmp, 50, 0);

        // Nearest-neighbor downscale, top-down (matches negative-height header).
        var srcStride = srcW * 4;
        var dstOffset = 54;
        for (var dy = 0; dy < dstH; dy++)
        {
            var sy = (int)((long)dy * srcH / dstH);
            for (var dx = 0; dx < dstW; dx++)
            {
                var sx = (int)((long)dx * srcW / dstW);
                var si = sy * srcStride + sx * 4;
                bmp[dstOffset++] = srcBgra[si];
                bmp[dstOffset++] = srcBgra[si + 1];
                bmp[dstOffset++] = srcBgra[si + 2];
                bmp[dstOffset++] = srcBgra[si + 3];
            }
        }
        return bmp;
    }

    private static void WriteUInt16LE(byte[] buf, int offset, ushort value)
    {
        buf[offset] = (byte)(value & 0xFF);
        buf[offset + 1] = (byte)((value >> 8) & 0xFF);
    }

    private static void WriteInt32LE(byte[] buf, int offset, int value) => WriteUInt32LE(buf, offset, (uint)value);

    private static void WriteUInt32LE(byte[] buf, int offset, uint value)
    {
        buf[offset] = (byte)(value & 0xFF);
        buf[offset + 1] = (byte)((value >> 8) & 0xFF);
        buf[offset + 2] = (byte)((value >> 16) & 0xFF);
        buf[offset + 3] = (byte)((value >> 24) & 0xFF);
    }
}
