using TeamsISO.Engine.Domain;

namespace TeamsISO.Engine.Pipeline;

/// <summary>
/// Pure-managed BGRA frame scaler with nearest-neighbor sampling and aspect-mode handling.
/// Suitable for v1.0 ship; libyuv is a v1.5 perf optimization (per spec §6 deferred items).
/// </summary>
public sealed class ManagedNearestNeighborFrameScaler : IFrameScaler
{
    public ProcessedFrame Scale(RawFrame source, int targetWidth, int targetHeight, AspectMode aspect, long timestampTicks)
    {
        if (source.Format != PixelFormat.Bgra)
            throw new NotSupportedException(
                $"ManagedNearestNeighborFrameScaler only supports BGRA input (got {source.Format}). " +
                "v1.0 receivers request BGRA from NDI; UYVY support comes in v1.5.");

        var output = new byte[targetWidth * targetHeight * 4];

        // Compute the destination rectangle within the target according to aspect mode.
        var (drawX, drawY, drawW, drawH) = ComputeFitRect(
            source.Width, source.Height, targetWidth, targetHeight, aspect);

        // Pre-fill with black if there will be unfilled bars (Pillarbox/Letterbox).
        if (drawX > 0 || drawY > 0 || drawW < targetWidth || drawH < targetHeight)
        {
            for (var i = 3; i < output.Length; i += 4) output[i] = 0xFF; // alpha
            // BGR remain 0x00 from default initialization.
        }

        var srcSpan = source.Pixels.Span;
        var dstSpan = output.AsSpan();

        // Nearest-neighbor scale into the (drawX, drawY, drawW, drawH) region.
        for (var dy = 0; dy < drawH; dy++)
        {
            // Map dest-row inside drawn rect → source row
            var sy = (int)((long)dy * source.Height / drawH);
            if (sy >= source.Height) sy = source.Height - 1;
            var srcRowStart = sy * source.Width * 4;
            var dstRowStart = ((drawY + dy) * targetWidth + drawX) * 4;

            for (var dx = 0; dx < drawW; dx++)
            {
                var sx = (int)((long)dx * source.Width / drawW);
                if (sx >= source.Width) sx = source.Width - 1;
                var srcOffset = srcRowStart + sx * 4;
                var dstOffset = dstRowStart + dx * 4;
                dstSpan[dstOffset + 0] = srcSpan[srcOffset + 0];
                dstSpan[dstOffset + 1] = srcSpan[srcOffset + 1];
                dstSpan[dstOffset + 2] = srcSpan[srcOffset + 2];
                dstSpan[dstOffset + 3] = srcSpan[srcOffset + 3];
            }
        }

        return new ProcessedFrame(targetWidth, targetHeight, timestampTicks, output, PixelFormat.Bgra);
    }

    private static (int X, int Y, int W, int H) ComputeFitRect(int srcW, int srcH, int dstW, int dstH, AspectMode aspect)
    {
        switch (aspect)
        {
            case AspectMode.Stretch:
                return (0, 0, dstW, dstH);

            case AspectMode.Pillarbox:
            case AspectMode.Letterbox:
            {
                // Both modes preserve aspect ratio and fit within the destination — the difference is
                // descriptive (which dimension gets bars), but the math is the same: scale to fit.
                var srcAspect = (double)srcW / srcH;
                var dstAspect = (double)dstW / dstH;
                int w, h;
                if (srcAspect > dstAspect)
                {
                    // Source is wider → letterbox (fill width, bars top/bottom).
                    w = dstW;
                    h = (int)Math.Round(dstW / srcAspect);
                }
                else
                {
                    // Source is taller/narrower → pillarbox (fill height, bars left/right).
                    h = dstH;
                    w = (int)Math.Round(dstH * srcAspect);
                }
                var x = (dstW - w) / 2;
                var y = (dstH - h) / 2;
                return (x, y, w, h);
            }

            default:
                throw new InvalidOperationException($"Unknown aspect mode: {aspect}");
        }
    }
}
