using VoiceCat.Interop;

// Owns N ProcessLoopbackCapture instances, mixes their PCM every 20 ms, and feeds
// the result to the core via vc_stream_feed_pcm. Used for per-app audio sharing.
namespace VoiceCat.App.Audio;

public sealed class ProcessAudioMixer : IDisposable
{
    private const int SampleRate   = 48000;
    private const int FrameSamples = 960;
    private const int Channels     = 2; // stereo; captures fall back to mono if needed

    private readonly List<ProcessLoopbackCapture> _captures = [];

    // Per-capture latest frame, protected by _frameLock.
    private readonly object _frameLock = new();
    private List<short[]>   _latestFrames = [];
    private int             _activeChannels = Channels;

    private Thread?          _mixThread;
    private volatile bool    _running;
    private VoiceCatClient?  _client;
    private uint             _streamId;

    public void Start(AppAudioScope scope, VoiceCatClient client, uint streamId)
    {
        if (_running) return;
        _client   = client;
        _streamId = streamId;

        var specs = ResolveCaptures(scope);
        if (specs.Count == 0)
        {
            // nothing to capture — scope resolved to empty set
            return;
        }

        lock (_frameLock)
        {
            _latestFrames   = new List<short[]>(new short[specs.Count][]);
            _activeChannels = Channels;
        }

        for (int i = 0; i < specs.Count; i++)
        {
            int captureIndex = i;
            var (pid, mode) = specs[i];
            var cap = new ProcessLoopbackCapture(pid, mode);
            cap.PcmFrameReady += (pcm, spc, ch) => OnCaptureFrame(captureIndex, pcm, ch);
            _captures.Add(cap);
        }

        foreach (var c in _captures) c.Start();

        _running   = true;
        _mixThread = new Thread(MixLoop) { IsBackground = true, Name = "ProcessAudioMixer" };
        _mixThread.Start();
    }

    public void Stop()
    {
        _running = false;
        _mixThread?.Join(500);
        foreach (var c in _captures) { c.Stop(); c.Dispose(); }
        _captures.Clear();
    }

    public void Dispose() => Stop();

    // ── Capture callback ──────────────────────────────────────────────────────

    private void OnCaptureFrame(int index, short[] pcm, int channels)
    {
        lock (_frameLock)
        {
            // Upmix mono → stereo interleave if the capture fell back to mono.
            if (channels == 1 && _activeChannels == 2)
                pcm = MonoToStereo(pcm);
            if (index < _latestFrames.Count)
                _latestFrames[index] = pcm;
        }
    }

    // ── Mix loop (20 ms timer) ────────────────────────────────────────────────

    private void MixLoop()
    {
        // Use a target period close to 20 ms; small under-shoot avoids accumulating drift.
        const int periodMs = 19;
        while (_running)
        {
            Thread.Sleep(periodMs);
            if (!_running) break;

            short[] mix;
            lock (_frameLock)
            {
                int len = FrameSamples * _activeChannels;
                mix = new short[len];

                foreach (var frame in _latestFrames)
                {
                    if (frame == null) continue;
                    int frameLen = Math.Min(frame.Length, len);
                    for (int i = 0; i < frameLen; i++)
                    {
                        int sum = mix[i] + frame[i];
                        // Saturating clamp
                        mix[i] = (short)Math.Clamp(sum, short.MinValue, short.MaxValue);
                    }
                }
            }

            _client?.StreamFeedPcm(_streamId, mix, FrameSamples, (uint)_activeChannels);
        }
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    // Resolve the scope to the WASAPI captures to open:
    //  OnlyApps      → one INCLUDE capture per selected process tree.
    //  AllExceptApps → one EXCLUDE capture of the single selected process tree, which
    //                  natively captures the whole system render mix minus that tree
    //                  (dynamic — apps launched later are included automatically).
    private static List<(int pid, ProcessLoopbackCapture.Mode mode)> ResolveCaptures(AppAudioScope scope)
    {
        return scope switch
        {
            OnlyApps      o => o.Pids.Select(p => (p, ProcessLoopbackCapture.Mode.Include)).ToList(),
            AllExceptApps a when a.Pids.Count > 0 =>
                [(a.Pids[0], ProcessLoopbackCapture.Mode.Exclude)],
            // Entire desktop minus VoiceCat itself: EXCLUDE our own process tree.
            EntireDesktop { ExcludeSelf: true } =>
                [(Environment.ProcessId, ProcessLoopbackCapture.Mode.Exclude)],
            _ => [],
        };
    }

    private static short[] MonoToStereo(short[] mono)
    {
        var stereo = new short[mono.Length * 2];
        for (int i = 0; i < mono.Length; i++)
        {
            stereo[i * 2]     = mono[i];
            stereo[i * 2 + 1] = mono[i];
        }
        return stereo;
    }
}
