using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using TeamsISO.Engine.Interop;

namespace TeamsISO.Engine.Pipeline;

/// <summary>
/// Wraps an <see cref="INdiInterop"/> receiver handle and pushes captured frames into a
/// <see cref="ChannelWriter{T}"/>. The channel is expected to be configured drop-oldest
/// by the caller (the <see cref="IsoPipeline"/>); this receiver does not enforce backpressure itself.
/// </summary>
public sealed class NdiReceiver : IDisposable
{
    private readonly INdiInterop _interop;
    private readonly string _sourceName;
    private readonly ChannelWriter<RawFrame> _output;
    private readonly ILogger<NdiReceiver> _logger;
    private readonly NdiReceiverHandle _handle;
    private long _framesCaptured;

    // Audio peak high-water mark since the last read, in [0, 1].
    //
    // Why a high-water mark and not a "latest peak":
    //   - Audio frames arrive at ~50Hz (1024-sample blocks @ 48kHz).
    //   - UI stats polling reads at 1Hz.
    // If we only published the most recent buffer's peak, the UI would see
    // exactly one of every ~50 audio frames per second — loud transients
    // between reads would be invisible. By keeping the running max and
    // consuming it on read, the UI sees the true peak across the entire
    // polling interval, which is the actual behavior of a peak VU meter.
    //
    // Stored as the IEEE 754 long bits of a double so we can atomically
    // CompareExchange-update without a struct lock. Volatile reads suffice
    // for the consume side because we follow with an Exchange to 0.
    private long _audioPeakBits;

    public NdiReceiver(
        INdiInterop interop,
        string sourceName,
        ChannelWriter<RawFrame> output,
        ILogger<NdiReceiver> logger)
    {
        _interop = interop;
        _sourceName = sourceName;
        _output = output;
        _logger = logger;
        _handle = interop.CreateReceiver(sourceName);
    }

    public long FramesCaptured => Interlocked.Read(ref _framesCaptured);

    /// <summary>
    /// Reads the peak audio amplitude (in [0.0, 1.0]) seen since the last
    /// call and resets the high-water mark to zero. The reset semantics are
    /// what makes this a true peak meter — between two consecutive reads
    /// the caller sees the loudest sample that crossed the receiver, not
    /// just whatever the latest buffer happened to contain.
    ///
    /// Returns 0 if no audio has been received since the last read (silent
    /// source, video-only sender, audio loop hasn't started, or the source
    /// genuinely went quiet). Safe to call from any thread; reset is atomic.
    /// </summary>
    public double ConsumeAudioPeak()
    {
        var bits = Interlocked.Exchange(ref _audioPeakBits, 0L);
        return BitConverter.Int64BitsToDouble(bits);
    }

    /// <summary>
    /// Non-consuming peek at the current peak high-water mark. Useful for
    /// observability paths that need to read the value without affecting the
    /// max-since-last-read behavior — for example, an external diagnostics
    /// dashboard that polls more often than the UI.
    /// </summary>
    public double PeekAudioPeak()
    {
        var bits = Volatile.Read(ref _audioPeakBits);
        return BitConverter.Int64BitsToDouble(bits);
    }

    /// <summary>
    /// Captures one frame (or returns on timeout). Test seam.
    /// </summary>
    public void CaptureOnce(int timeoutMs)
    {
        var frame = _interop.CaptureFrame(_handle, timeoutMs);
        if (frame is null) return;
        Interlocked.Increment(ref _framesCaptured);
        _output.TryWrite(frame);
    }

    /// <summary>
    /// Captures one audio frame (or returns on timeout) and atomically
    /// updates the high-water peak. Test seam mirroring <see cref="CaptureOnce"/>.
    /// The CAS loop is needed because two writers (this method, called from
    /// the audio capture thread) and one reader (<see cref="ConsumeAudioPeak"/>,
    /// called from the UI poll thread) compete for the same field — a plain
    /// Volatile.Write would lose updates if Consume fires between our read
    /// and write.
    /// </summary>
    public void CaptureAudioOnce(int timeoutMs)
    {
        var peak = _interop.CaptureAudioPeak(_handle, timeoutMs);
        if (peak is null) return;
        var newBits = BitConverter.DoubleToInt64Bits(peak.Value);
        long current;
        do
        {
            current = Volatile.Read(ref _audioPeakBits);
            // If our peak is no louder than what's already there, leave it —
            // somebody else (the audio thread itself, on a previous frame)
            // already published a higher max.
            if (peak.Value <= BitConverter.Int64BitsToDouble(current)) return;
        } while (Interlocked.CompareExchange(ref _audioPeakBits, newBits, current) != current);
    }

    /// <summary>
    /// Long-running capture loop. Run on a dedicated thread (<c>TaskCreationOptions.LongRunning</c>).
    /// Spins up a sibling audio loop on the same lifetime so the UI VU bar
    /// can read peaks without polluting the video read path.
    /// </summary>
    public Task RunAsync(CancellationToken cancellationToken)
    {
        var videoTask = Task.Factory.StartNew(() =>
        {
            try
            {
                while (!cancellationToken.IsCancellationRequested)
                {
                    CaptureOnce(timeoutMs: 100);
                }
            }
            catch (OperationCanceledException) { }
            catch (Exception ex)
            {
                _logger.LogError(ex, "NdiReceiver video loop crashed for source {Source}.", _sourceName);
                throw;
            }
        }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);

        var audioTask = Task.Factory.StartNew(() =>
        {
            try
            {
                while (!cancellationToken.IsCancellationRequested)
                {
                    // Audio frames arrive at the source's frame rate (typically
                    // 48kHz delivered in 1024-sample chunks ~= 50Hz). A 50ms
                    // poll matches that cadence — we won't miss frames and we
                    // won't busy-spin if the source is video-only.
                    //
                    // Audio loop failures are logged but never re-thrown — a
                    // misbehaving audio path must NEVER tear down the live
                    // video pipeline. The UI VU bar will simply freeze at its
                    // last value.
                    try { CaptureAudioOnce(timeoutMs: 50); }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "NdiReceiver audio loop hiccup for source {Source}.", _sourceName);
                    }
                }
            }
            catch (OperationCanceledException) { }
        }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);

        return Task.WhenAll(videoTask, audioTask);
    }

    public void Dispose() => _handle.Dispose();
}
