using System.Runtime.InteropServices;

namespace TeamsISO.Engine.Pipeline;

/// <summary>
/// Computes a single peak amplitude (in [0.0, 1.0]) from one NDI audio frame.
///
/// NDI 6's preferred audio format is <c>NDIlib_FourCC_audio_type_FLTP</c> —
/// 32-bit IEEE float, planar (one contiguous chunk per channel). Values are
/// nominally normalized to [-1, 1]; brief excursions past 1 during transient
/// clipping are clamped here. We compute a max-absolute peak across every
/// sample of every channel rather than RMS so the UI VU bar reads
/// "loudest part of the buffer" — the same convention OBS / Resolve / Studio
/// Monitor use for their meters.
///
/// Pulled out of <see cref="NdiReceiver"/> so the math is unit-testable
/// without an NDI runtime; the heavy work (FLTP decode) runs entirely on
/// managed memory the caller has already copied across the P/Invoke
/// boundary, so tests exercise the same code path that production does.
/// </summary>
public static class AudioPeakComputer
{
    /// <summary>FourCC for FLTP — 32-bit float, planar layout. <c>'F','L','T','p'</c>.</summary>
    public const uint FourCC_FLTP = 0x70544c46;

    /// <summary>FourCC for FLT — 32-bit float, interleaved. <c>'F','L','T',' '</c>. Rarely seen but cheap to handle.</summary>
    public const uint FourCC_FLT = 0x20544c46;

    /// <summary>FourCC for PCM 16-bit signed integer, interleaved. Some legacy senders use this. <c>'P','C','M','s'</c>.</summary>
    public const uint FourCC_PCMs16 = 0x73334d50;

    /// <summary>
    /// Returns the largest absolute sample value found in the buffer,
    /// normalized to [0.0, 1.0] and clamped to 1.0 for any input that exceeds it.
    /// Returns 0.0 for an empty / zero-length buffer.
    /// </summary>
    /// <param name="data">The raw audio sample bytes for the entire frame.</param>
    /// <param name="fourCC">The NDI audio FourCC (see the constants on this class).</param>
    /// <param name="totalSamples">
    /// Total sample count across all channels (e.g. <c>no_samples * no_channels</c>
    /// for FLTP — channels are concatenated planes, but every sample contributes).
    /// </param>
    public static double ComputePeak(ReadOnlySpan<byte> data, uint fourCC, int totalSamples)
    {
        if (data.IsEmpty || totalSamples <= 0) return 0.0;

        return fourCC switch
        {
            FourCC_FLTP or FourCC_FLT => ComputePeakFloat32(data, totalSamples),
            FourCC_PCMs16 => ComputePeakInt16(data, totalSamples),
            _ => 0.0, // unknown format — surface silence rather than throw
        };
    }

    private static double ComputePeakFloat32(ReadOnlySpan<byte> data, int totalSamples)
    {
        // 4 bytes per sample. Cap by what's actually in the buffer in case
        // the caller's totalSamples disagrees with the byte length (defensive
        // — a misreporting source shouldn't take down the receiver loop).
        var available = Math.Min(totalSamples, data.Length / 4);
        if (available <= 0) return 0.0;

        var floats = MemoryMarshal.Cast<byte, float>(data.Slice(0, available * 4));
        float peak = 0;
        // Tight scalar loop. .NET's auto-vectorizer turns this into SIMD on
        // modern x64 builds; benchmarking showed a manual Vector<float> loop
        // wasn't faster for the typical 1024-sample @ 48kHz buffer.
        for (var i = 0; i < floats.Length; i++)
        {
            var v = floats[i];
            var abs = v < 0 ? -v : v;
            if (abs > peak) peak = abs;
        }
        return peak > 1.0f ? 1.0 : peak;
    }

    private static double ComputePeakInt16(ReadOnlySpan<byte> data, int totalSamples)
    {
        var available = Math.Min(totalSamples, data.Length / 2);
        if (available <= 0) return 0.0;

        var samples = MemoryMarshal.Cast<byte, short>(data.Slice(0, available * 2));
        int peak = 0;
        for (var i = 0; i < samples.Length; i++)
        {
            // short.MinValue (-32768) negated overflows back to itself in two's
            // complement. Clamp the result up to short.MaxValue (32767) so we
            // can divide by a stable max in the normalize step below.
            var s = samples[i];
            var abs = s == short.MinValue ? short.MaxValue : (s < 0 ? -s : s);
            if (abs > peak) peak = abs;
        }
        var normalized = peak / (double)short.MaxValue;
        return normalized > 1.0 ? 1.0 : normalized;
    }
}
