using System.Runtime.InteropServices;

// WASAPI shared-mode capture from a real hardware INPUT device (a microphone / line-in / aux
// device), plus enumeration of capture endpoints for the aux-stream picker.
//
// This is the input-device analogue of ProcessLoopbackCapture (which captures *render* loopback
// via the process-loopback activation hack). Here the source is an ordinary capture endpoint, so
// we use the standard IMMDevice.Activate(IAudioClient) path with RCW interfaces — no vtable
// gymnastics needed (a normal device's COM objects honour QueryInterface).
//
// Why client-side capture at all? The core already owns ONE capture device (the mic). It can't
// open a second arbitrary input device, so for the aux stream the client captures the device and
// feeds 48 kHz / 20 ms int16 frames into the core via vc_stream_feed_pcm — the same external-feed
// pipeline screen-audio sharing uses. The device ids here are WASAPI endpoint ids and are NOT the
// core's miniaudio ids, so the aux picker is populated independently of vc_list_devices.
namespace VoiceCat.App.Audio;

/// <summary>An audio input (capture) endpoint for the aux-stream device picker. <see cref="Id"/>
/// is a WASAPI endpoint id (round-trip only; never construct by hand) — pass null to capture the
/// system default. <see cref="ToString"/> returns the friendly name for ComboBox display.</summary>
public sealed record InputDeviceInfo(string Id, string Name, bool IsDefault)
{
    public override string ToString() => Name;
}

/// <summary>Enumerates WASAPI capture endpoints. Separate from the core's vc_list_devices because
/// the aux device is opened client-side and needs a WASAPI id, not a miniaudio one.</summary>
public static class InputDeviceEnumerator
{
    public static IReadOnlyList<InputDeviceInfo> List()
    {
        var result = new List<InputDeviceInfo>();
        InputDeviceCapture.IMMDeviceEnumerator? enumerator = null;
        IntPtr collectionPtr = IntPtr.Zero;
        string? defaultId = null;

        try
        {
            enumerator = (InputDeviceCapture.IMMDeviceEnumerator)Activator.CreateInstance(
                Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!;

            // Resolve the default capture endpoint id so the picker can flag it.
            if (enumerator.GetDefaultAudioEndpoint(1 /*eCapture*/, 0 /*eConsole*/,
                    out var defDev) == 0 && defDev != null)
            {
                try { if (defDev.GetId(out string id) == 0) defaultId = id; }
                finally { Marshal.ReleaseComObject(defDev); }
            }

            // DEVICE_STATE_ACTIVE = 0x1 — only currently-usable endpoints.
            if (enumerator.EnumAudioEndpoints(1 /*eCapture*/, 0x1, out collectionPtr) != 0
                || collectionPtr == IntPtr.Zero)
                return result;

            var collection = (InputDeviceCapture.IMMDeviceCollection)
                Marshal.GetObjectForIUnknown(collectionPtr);
            collection.GetCount(out int count);

            for (int i = 0; i < count; i++)
            {
                if (collection.Item(i, out var dev) != 0 || dev == null) continue;
                try
                {
                    if (dev.GetId(out string id) != 0) continue;
                    string name = ReadFriendlyName(dev) ?? "Unknown input device";
                    result.Add(new InputDeviceInfo(id, name, id == defaultId));
                }
                finally { Marshal.ReleaseComObject(dev); }
            }
        }
        catch { /* no audio subsystem / WASAPI unavailable — return what we have */ }
        finally
        {
            if (collectionPtr != IntPtr.Zero) Marshal.Release(collectionPtr);
            if (enumerator != null) Marshal.ReleaseComObject(enumerator);
        }

        return result.OrderBy(d => d.Name, StringComparer.OrdinalIgnoreCase).ToList();
    }

    private static string? ReadFriendlyName(InputDeviceCapture.IMMDevice dev)
    {
        if (dev.OpenPropertyStore(0 /*STGM_READ*/, out IntPtr storePtr) != 0
            || storePtr == IntPtr.Zero)
            return null;
        try
        {
            var store = (InputDeviceCapture.IPropertyStore)Marshal.GetObjectForIUnknown(storePtr);
            // PKEY_Device_FriendlyName = {a45c254e-df1c-4efd-8020-67d146a850e0}, pid 14.
            var key = new InputDeviceCapture.PropertyKey
            {
                fmtid = new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"),
                pid = 14,
            };
            if (store.GetValue(ref key, out var pv) != 0) return null;
            try
            {
                // VT_LPWSTR = 31.
                return pv.vt == 31 ? Marshal.PtrToStringUni(pv.pointerValue) : null;
            }
            finally { InputDeviceCapture.PropVariantClear(ref pv); }
        }
        finally { Marshal.Release(storePtr); }
    }
}

/// <summary>Captures a single hardware input device in WASAPI shared mode and raises a 20 ms
/// (960 samples/channel @ 48 kHz, interleaved s16) frame event. The caller feeds these to the
/// core's external-feed stream. Threading mirrors ProcessLoopbackCapture: all WASAPI work runs on
/// a dedicated background (MTA) thread; the event fires on that thread.</summary>
public sealed class InputDeviceCapture : IDisposable
{
    /// <summary>Fired on the capture thread every 20 ms: (interleaved s16 PCM, samplesPerChannel
    /// = 960, channels).</summary>
    public event Action<short[], int /*samplesPerChannel*/, int /*channels*/>? PcmFrameReady;

    private const int SampleRate   = 48000;
    private const int FrameSamples = 960;  // 20 ms

    private readonly string? _deviceId;  // null = system default capture endpoint

    private IAudioClient?        _audioClient;
    private IAudioCaptureClient? _captureClient;

    private AutoResetEvent? _bufferEvent;
    private Thread?         _captureThread;
    private volatile bool   _running;
    private int             _channels;

    // Accumulator: assembles driver-callback-sized fragments into FrameSamples chunks.
    private short[] _accumBuf = [];
    private int     _accumCount;

    // Init-done signal: Set() by the capture thread after activation completes.
    private readonly ManualResetEventSlim _initDone = new(false);
    private bool _initOk;

    public InputDeviceCapture(string? deviceId) => _deviceId = deviceId;

    /// <summary>Starts capture. Blocks until WASAPI activation completes (typically &lt;100 ms).
    /// Returns false if the device cannot be opened.</summary>
    public bool Start()
    {
        if (_running) return false;
        _running       = true;
        _captureThread = new Thread(CaptureThreadProc)
        {
            IsBackground = true,
            Name         = "AuxInputCapture",
        };
        _captureThread.Start();

        bool ok = _initDone.Wait(5000) && _initOk;
        if (!ok) _running = false;
        return ok;
    }

    public void Stop()
    {
        _running = false;
        _bufferEvent?.Set();
        _captureThread?.Join(500);
        try { _audioClient?.Stop(); } catch { /* device already gone */ }
    }

    public void Dispose()
    {
        Stop();
        if (_captureClient != null) { Marshal.ReleaseComObject(_captureClient); _captureClient = null; }
        if (_audioClient   != null) { Marshal.ReleaseComObject(_audioClient);   _audioClient   = null; }
        _bufferEvent?.Dispose();
        _initDone.Dispose();
    }

    // ── Capture thread (MTA) ──────────────────────────────────────────────────

    private void CaptureThreadProc()
    {
        _initOk = ActivateAndStart();
        _initDone.Set();
        if (!_initOk) return;
        CaptureLoop();
    }

    private bool ActivateAndStart()
    {
        if (!ActivateClient()) return false;

        _bufferEvent = new AutoResetEvent(false);
        if (_audioClient!.SetEventHandle(_bufferEvent.SafeWaitHandle.DangerousGetHandle()) < 0)
            return false;

        return _audioClient.Start() >= 0;
    }

    private bool ActivateClient()
    {
        IMMDeviceEnumerator? enumerator = null;
        IMMDevice? device = null;
        try
        {
            enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(
                Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!;

            int hr = _deviceId is null
                ? enumerator.GetDefaultAudioEndpoint(1 /*eCapture*/, 0 /*eConsole*/, out device)
                : enumerator.GetDevice(_deviceId, out device);
            if (hr != 0 || device == null) return false;

            var iidAudioClient = new Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2");
            if (device.Activate(ref iidAudioClient, 0x17 /*CLSCTX_ALL*/, IntPtr.Zero,
                    out object acObj) != 0 || acObj is not IAudioClient ac)
                return false;
            _audioClient = ac;

            return InitializeStream();
        }
        catch { return false; }
        finally
        {
            if (device     != null) Marshal.ReleaseComObject(device);
            if (enumerator != null) Marshal.ReleaseComObject(enumerator);
        }
    }

    private bool InitializeStream()
    {
        // Try s16 stereo first; fall back to s16 mono. AUTOCONVERTPCM lets the audio engine
        // resample/convert the device's native format to our requested 48 kHz s16; EVENTCALLBACK
        // drives the buffer-ready event. Shared mode (0), no LOOPBACK (this is a capture device).
        //   AUDCLNT_STREAMFLAGS_EVENTCALLBACK    = 0x00040000
        //   AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM   = 0x80000000
        //   AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY = 0x08000000
        const uint streamFlags = 0x00040000u | 0x80000000u | 0x08000000u;

        foreach (int ch in new[] { 2, 1 })
        {
            var fmt = new WaveFormatEx
            {
                wFormatTag      = 1,           // WAVE_FORMAT_PCM
                nChannels       = (ushort)ch,
                nSamplesPerSec  = SampleRate,
                wBitsPerSample  = 16,
                nBlockAlign     = (ushort)(ch * 2),
                nAvgBytesPerSec = (uint)(SampleRate * ch * 2),
                cbSize          = 0,
            };

            IntPtr pFmt = Marshal.AllocHGlobal(Marshal.SizeOf<WaveFormatEx>());
            try
            {
                Marshal.StructureToPtr(fmt, pFmt, false);
                int hr = _audioClient!.Initialize(0 /*AUDCLNT_SHAREMODE_SHARED*/, streamFlags,
                    2_000_000 /*200 ms hns*/, 0, pFmt, IntPtr.Zero);
                if (hr >= 0)
                {
                    _channels   = ch;
                    _accumBuf   = new short[FrameSamples * ch];
                    _accumCount = 0;

                    var iidCapture = new Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317");
                    if (_audioClient.GetService(ref iidCapture, out object ccObj) != 0
                        || ccObj is not IAudioCaptureClient cc)
                        return false;
                    _captureClient = cc;
                    return true;
                }
                if (ch == 1) return false;
            }
            finally { Marshal.FreeHGlobal(pFmt); }
        }
        return false;
    }

    // ── Capture loop ─────────────────────────────────────────────────────────

    private void CaptureLoop()
    {
        while (_running)
        {
            _bufferEvent!.WaitOne(100);
            if (!_running) break;

            while (_running)
            {
                if (_captureClient!.GetNextPacketSize(out uint packetSize) < 0 || packetSize == 0)
                    break;

                if (_captureClient.GetBuffer(out IntPtr dataPtr, out uint framesAvailable,
                        out uint flags, out _, out _) < 0)
                    break;

                bool silent = (flags & 2) != 0; // AUDCLNT_BUFFERFLAGS_SILENT
                if (framesAvailable > 0)
                {
                    if (silent) AccumulateSilence((int)framesAvailable);
                    else        AccumulatePcm(dataPtr, (int)framesAvailable);
                }

                _captureClient.ReleaseBuffer(framesAvailable);
            }
        }
    }

    private unsafe void AccumulatePcm(IntPtr data, int frames)
    {
        var src   = (short*)data.ToPointer();
        int total = frames * _channels;
        int idx   = 0;
        while (idx < total)
        {
            int space = _accumBuf.Length - _accumCount;
            int copy  = Math.Min(total - idx, space);
            fixed (short* dst = _accumBuf)
                Buffer.MemoryCopy(src + idx, dst + _accumCount, copy * 2L, copy * 2L);
            _accumCount += copy;
            idx         += copy;
            if (_accumCount == _accumBuf.Length)
                FlushFrame();
        }
    }

    private void AccumulateSilence(int frames)
    {
        int total = frames * _channels;
        int idx   = 0;
        while (idx < total)
        {
            int space = _accumBuf.Length - _accumCount;
            int fill  = Math.Min(total - idx, space);
            Array.Clear(_accumBuf, _accumCount, fill);
            _accumCount += fill;
            idx         += fill;
            if (_accumCount == _accumBuf.Length)
                FlushFrame();
        }
    }

    private void FlushFrame()
    {
        var copy = new short[_accumBuf.Length];
        _accumBuf.AsSpan().CopyTo(copy);
        PcmFrameReady?.Invoke(copy, FrameSamples, _channels);
        _accumCount = 0;
    }

    // ── COM declarations ───────────────────────────────────────────────────────
    //
    // Declared internal here so InputDeviceEnumerator can share them. These are standard MMDevice
    // / WASAPI interfaces; a normal capture endpoint honours QueryInterface, so RCW marshalling is
    // safe (unlike ProcessLoopbackCapture's process-loopback objects, which need raw vtable calls).

    [ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDeviceEnumerator
    {
        [PreserveSig] int EnumAudioEndpoints(int dataFlow, int stateMask, out IntPtr devices);
        [PreserveSig] int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
        [PreserveSig] int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string id, out IMMDevice device);
        [PreserveSig] int RegisterEndpointNotificationCallback(IntPtr client);
        [PreserveSig] int UnregisterEndpointNotificationCallback(IntPtr client);
    }

    [ComImport, Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDeviceCollection
    {
        [PreserveSig] int GetCount(out int count);
        [PreserveSig] int Item(int index, out IMMDevice device);
    }

    [ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDevice
    {
        [PreserveSig] int Activate(ref Guid iid, int clsCtx, IntPtr activationParams,
            [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
        [PreserveSig] int OpenPropertyStore(int stgmAccess, out IntPtr propStore);
        [PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
        [PreserveSig] int GetState(out int state);
    }

    [ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IPropertyStore
    {
        [PreserveSig] int GetCount(out int count);
        [PreserveSig] int GetAt(int index, out PropertyKey key);
        [PreserveSig] int GetValue(ref PropertyKey key, out PropVariant value);
        [PreserveSig] int SetValue(ref PropertyKey key, ref PropVariant value);
        [PreserveSig] int Commit();
    }

    [ComImport, Guid("1CB9AD4C-DBFA-4C32-B178-C2F568A703B2"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioClient
    {
        [PreserveSig] int Initialize(int shareMode, uint streamFlags, long hnsBufferDuration,
            long hnsPeriodicity, IntPtr pFormat, IntPtr audioSessionGuid);
        [PreserveSig] int GetBufferSize(out uint numBufferFrames);
        [PreserveSig] int GetStreamLatency(out long latency);
        [PreserveSig] int GetCurrentPadding(out uint numPaddingFrames);
        [PreserveSig] int IsFormatSupported(int shareMode, IntPtr pFormat, out IntPtr closestMatch);
        [PreserveSig] int GetMixFormat(out IntPtr deviceFormat);
        [PreserveSig] int GetDevicePeriod(out long defaultDevicePeriod, out long minimumDevicePeriod);
        [PreserveSig] int Start();
        [PreserveSig] int Stop();
        [PreserveSig] int Reset();
        [PreserveSig] int SetEventHandle(IntPtr eventHandle);
        [PreserveSig] int GetService(ref Guid riid,
            [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
    }

    [ComImport, Guid("C8ADBD64-E71E-48A0-A4DE-185C395CD317"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioCaptureClient
    {
        [PreserveSig] int GetBuffer(out IntPtr data, out uint numFramesToRead, out uint flags,
            out ulong devicePosition, out ulong qpcPosition);
        [PreserveSig] int ReleaseBuffer(uint numFramesRead);
        [PreserveSig] int GetNextPacketSize(out uint numFramesInNextPacket);
    }

    [StructLayout(LayoutKind.Sequential, Pack = 2)]
    private struct WaveFormatEx
    {
        public ushort wFormatTag;
        public ushort nChannels;
        public uint   nSamplesPerSec;
        public uint   nAvgBytesPerSec;
        public ushort nBlockAlign;
        public ushort wBitsPerSample;
        public ushort cbSize;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct PropertyKey
    {
        public Guid fmtid;
        public int  pid;
    }

    // Minimal PROPVARIANT: we only ever read VT_LPWSTR (friendly name). x64 layout — the value
    // union starts at offset 8 after vt(2)+reserved(6).
    [StructLayout(LayoutKind.Explicit)]
    internal struct PropVariant
    {
        [FieldOffset(0)] public ushort vt;
        [FieldOffset(8)] public IntPtr pointerValue;
    }

    [DllImport("ole32.dll")]
    internal static extern int PropVariantClear(ref PropVariant pvar);
}
