using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using TeamsISO.Engine.Interop;
using TeamsISO.Engine.Pipeline;

namespace TeamsISO.Engine.NdiInterop;

/// <summary>
/// Production <see cref="INdiInterop"/> implementation backed by the NDI 6 SDK.
/// Initializes the NDI runtime on construction and tears it down on dispose.
/// All frame buffers crossing the managed boundary are copied so the engine never holds
/// a reference to NDI-owned memory after the call returns.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class NdiInteropPInvoke : INdiInterop, IDisposable
{
    private readonly ILogger<NdiInteropPInvoke> _logger;
    private bool _initialized;
    private bool _disposed;

    public NdiInteropPInvoke(ILogger<NdiInteropPInvoke> logger)
    {
        _logger = logger;
        if (!NdiNative.Initialize())
            throw new InvalidOperationException(
                "NDI runtime failed to initialize. Ensure the NDI Runtime is installed and " +
                "Processing.NDI.Lib.x64.dll is on the application's DLL search path.");
        _initialized = true;
    }

    public string GetRuntimeVersion()
    {
        var ptr = NdiNative.Version();
        return Marshal.PtrToStringAnsi(ptr) ?? string.Empty;
    }

    // ---- Discovery ----

    public NdiFindHandle CreateFinder(string? groups = null)
    {
        // Empty/whitespace -> default (Public). Otherwise allocate a UTF-8 buffer
        // for the comma-separated group list and pin a settings struct around it.
        //
        // Memory ownership: the NDI SDK's NDIlib_find_create_v2 (and _send_create,
        // _recv_create_v3) copy the strings out of the settings struct synchronously
        // before returning — they don't retain pointers into our buffers. This is the
        // same lifetime contract CreateReceiver / CreateSender below have relied on
        // since Phase B-2; if it ever turns out to be wrong, those will fail too. The
        // loopback discovery integration test would catch a regression here.
        var trimmed = string.IsNullOrWhiteSpace(groups) ? null : groups!.Trim();
        if (trimmed is null)
        {
            var nativeDefault = NdiNative.FindCreateV2(IntPtr.Zero);
            if (nativeDefault == IntPtr.Zero)
                throw new InvalidOperationException("NDIlib_find_create_v2 returned null.");
            return new NdiPInvokeFindHandle(nativeDefault);
        }

        var groupsUtf8 = Marshal.StringToHGlobalAnsi(trimmed);
        try
        {
            var settings = new NdiNative.FindCreateSettings
            {
                show_local_sources = true,
                p_groups = groupsUtf8,
                p_extra_ips = IntPtr.Zero,
            };
            var settingsPtr = Marshal.AllocHGlobal(Marshal.SizeOf<NdiNative.FindCreateSettings>());
            try
            {
                Marshal.StructureToPtr(settings, settingsPtr, false);
                var native = NdiNative.FindCreateV2(settingsPtr);
                if (native == IntPtr.Zero)
                    throw new InvalidOperationException(
                        $"NDIlib_find_create_v2 returned null for groups='{trimmed}'.");
                _logger.LogInformation("NDI finder created with groups: {Groups}", trimmed);
                return new NdiPInvokeFindHandle(native);
            }
            finally
            {
                Marshal.FreeHGlobal(settingsPtr);
            }
        }
        finally
        {
            Marshal.FreeHGlobal(groupsUtf8);
        }
    }

    public IReadOnlyList<string> GetCurrentSources(NdiFindHandle finder)
    {
        var pInvokeFinder = (NdiPInvokeFindHandle)finder;
        var ptr = NdiNative.FindGetCurrentSources(pInvokeFinder.Native, out var count);
        if (ptr == IntPtr.Zero || count == 0) return Array.Empty<string>();

        // The native call returns a pointer to an array of NDIlib_source_t.
        // Each entry is sizeof(IntPtr)*2 bytes (p_ndi_name + p_url_address).
        var sourceSize = Marshal.SizeOf<NdiNative.Source>();
        var result = new string[count];
        for (var i = 0; i < count; i++)
        {
            var sourcePtr = IntPtr.Add(ptr, i * sourceSize);
            var src = Marshal.PtrToStructure<NdiNative.Source>(sourcePtr);
            result[i] = Marshal.PtrToStringAnsi(src.p_ndi_name) ?? string.Empty;
        }
        return result;
    }

    // ---- Receive ----

    public NdiReceiverHandle CreateReceiver(string sourceFullName)
    {
        var nameUtf8 = Marshal.StringToHGlobalAnsi(sourceFullName);
        var recvNameUtf8 = Marshal.StringToHGlobalAnsi("TeamsISO");
        try
        {
            var settings = new NdiNative.RecvCreateV3Settings
            {
                source_to_connect_to = new NdiNative.Source
                {
                    p_ndi_name = nameUtf8,
                    p_url_address = IntPtr.Zero
                },
                color_format = NdiNative.RecvColorFormat.BgrxBgra,
                bandwidth = NdiNative.RecvBandwidth.Highest,
                allow_video_fields = false,
                p_ndi_recv_name = recvNameUtf8
            };
            var native = NdiNative.RecvCreateV3(ref settings);
            if (native == IntPtr.Zero)
                throw new InvalidOperationException($"NDIlib_recv_create_v3 returned null for source '{sourceFullName}'.");
            return new NdiPInvokeReceiverHandle(native, sourceFullName);
        }
        finally
        {
            Marshal.FreeHGlobal(nameUtf8);
            Marshal.FreeHGlobal(recvNameUtf8);
        }
    }

    /// <summary>
    /// Pulls one audio frame and returns its peak amplitude in [0,1], or null
    /// if the timeout elapsed without an audio frame arriving. Uses the same
    /// underlying NDIlib_recv_capture_v3 the video path does, but binds the
    /// audio output slot only — the receiver's internal queue serves video
    /// and audio independently, so this can be polled from a separate thread
    /// without contending with the video capture loop.
    /// </summary>
    public double? CaptureAudioPeak(NdiReceiverHandle receiver, int timeoutMs)
    {
        var pInvokeReceiver = (NdiPInvokeReceiverHandle)receiver;
        var frameType = NdiNative.RecvCaptureV3Audio(
            pInvokeReceiver.Native,
            IntPtr.Zero,
            out var nativeAudio,
            IntPtr.Zero,
            (uint)Math.Max(0, timeoutMs));

        if (frameType != NdiNative.FrameType.Audio || nativeAudio.p_data == IntPtr.Zero)
        {
            // Free defensively on the off-chance an audio struct was partially
            // populated despite the wrong frame-type return — the SDK's free
            // is a no-op on a zero pointer.
            if (nativeAudio.p_data != IntPtr.Zero)
                NdiNative.RecvFreeAudioV3(pInvokeReceiver.Native, ref nativeAudio);
            return null;
        }

        try
        {
            // Total bytes for the entire frame's audio buffer. For FLTP that's
            // no_channels * channel_stride_in_bytes. The struct's union slot
            // exposed as channel_stride_in_bytes is the per-channel stride
            // when FourCC=FLTp; total samples across all channels is
            // no_channels * no_samples and we walk every sample for the peak.
            var totalBytes = nativeAudio.no_channels * nativeAudio.channel_stride_in_bytes;
            if (totalBytes <= 0 || nativeAudio.no_samples <= 0)
                return 0.0;

            var managed = new byte[totalBytes];
            Marshal.Copy(nativeAudio.p_data, managed, 0, totalBytes);

            var totalSamples = nativeAudio.no_channels * nativeAudio.no_samples;
            return TeamsISO.Engine.Pipeline.AudioPeakComputer.ComputePeak(
                managed, nativeAudio.FourCC, totalSamples);
        }
        finally
        {
            NdiNative.RecvFreeAudioV3(pInvokeReceiver.Native, ref nativeAudio);
        }
    }

    public RawFrame? CaptureFrame(NdiReceiverHandle receiver, int timeoutMs)
    {
        var pInvokeReceiver = (NdiPInvokeReceiverHandle)receiver;
        var frameType = NdiNative.RecvCaptureV3(
            pInvokeReceiver.Native,
            out var nativeFrame,
            IntPtr.Zero,
            IntPtr.Zero,
            (uint)Math.Max(0, timeoutMs));

        if (frameType != NdiNative.FrameType.Video || nativeFrame.p_data == IntPtr.Zero)
        {
            // Even non-video frame types may need to be released; recv_free_video_v2 is safe on zeroed structs.
            if (nativeFrame.p_data != IntPtr.Zero)
                NdiNative.RecvFreeVideoV2(pInvokeReceiver.Native, ref nativeFrame);
            return null;
        }

        try
        {
            var pixelFormat = nativeFrame.FourCC switch
            {
                NdiNative.FourCC.BGRA or NdiNative.FourCC.BGRX => PixelFormat.Bgra,
                NdiNative.FourCC.UYVY => PixelFormat.Uyvy,
                NdiNative.FourCC.RGBA => PixelFormat.Rgba,
                _ => PixelFormat.Bgra
            };

            // Compute total byte length. line_stride_in_bytes is the union slot for video frames.
            var byteLength = nativeFrame.line_stride_in_bytes * nativeFrame.yres;
            if (byteLength <= 0)
            {
                _logger.LogWarning("NDI video frame had non-positive byte length ({Length}); skipping.", byteLength);
                return null;
            }

            var managed = new byte[byteLength];
            Marshal.Copy(nativeFrame.p_data, managed, 0, byteLength);

            return new RawFrame(
                Width: nativeFrame.xres,
                Height: nativeFrame.yres,
                TimestampTicks: nativeFrame.timestamp,
                Pixels: managed,
                Format: pixelFormat);
        }
        finally
        {
            NdiNative.RecvFreeVideoV2(pInvokeReceiver.Native, ref nativeFrame);
        }
    }

    // ---- Send ----

    public NdiSenderHandle CreateSender(string outputName, string? groups = null)
    {
        var trimmedGroups = string.IsNullOrWhiteSpace(groups) ? null : groups!.Trim();
        var nameUtf8 = Marshal.StringToHGlobalAnsi(outputName);
        var groupsUtf8 = trimmedGroups is null
            ? IntPtr.Zero
            : Marshal.StringToHGlobalAnsi(trimmedGroups);
        try
        {
            var settings = new NdiNative.SendCreateSettings
            {
                p_ndi_name = nameUtf8,
                p_groups = groupsUtf8,
                clock_video = true,
                clock_audio = false
            };
            var native = NdiNative.SendCreate(ref settings);
            if (native == IntPtr.Zero)
                throw new InvalidOperationException(
                    $"NDIlib_send_create returned null for output '{outputName}' on groups '{trimmedGroups ?? "<default>"}'.");
            if (trimmedGroups is not null)
                _logger.LogInformation("NDI sender '{Output}' created on groups: {Groups}", outputName, trimmedGroups);
            return new NdiPInvokeSenderHandle(native, outputName);
        }
        finally
        {
            Marshal.FreeHGlobal(nameUtf8);
            if (groupsUtf8 != IntPtr.Zero) Marshal.FreeHGlobal(groupsUtf8);
        }
    }

    public void SendFrame(NdiSenderHandle sender, ProcessedFrame frame)
    {
        var pInvokeSender = (NdiPInvokeSenderHandle)sender;

        var fourcc = frame.Format switch
        {
            PixelFormat.Bgra => NdiNative.FourCC.BGRA,
            PixelFormat.Rgba => NdiNative.FourCC.RGBA,
            PixelFormat.Uyvy => NdiNative.FourCC.UYVY,
            _ => NdiNative.FourCC.BGRA
        };

        // Pin the managed buffer so the native call can read it directly.
        var pixels = frame.Pixels.ToArray();
        var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        try
        {
            var nativeFrame = new NdiNative.VideoFrameV2
            {
                xres = frame.Width,
                yres = frame.Height,
                FourCC = fourcc,
                frame_rate_N = 60000,
                frame_rate_D = 1001, // 59.94 default; controller-supplied later
                picture_aspect_ratio = 0.0f, // 0 = derive from xres/yres
                frame_format_type = NdiNative.FrameFormatType.Progressive,
                timecode = NdiTimecodeSynthesize, // synthesize
                p_data = handle.AddrOfPinnedObject(),
                line_stride_in_bytes = frame.Width * BytesPerPixel(frame.Format),
                p_metadata = IntPtr.Zero,
                timestamp = frame.TimestampTicks
            };
            NdiNative.SendSendVideoV2(pInvokeSender.Native, ref nativeFrame);
        }
        finally
        {
            handle.Free();
        }
    }

    private const long NdiTimecodeSynthesize = unchecked((long)0x8000000000000000UL); // NDIlib_send_timecode_synthesize

    private static int BytesPerPixel(PixelFormat fmt) => fmt switch
    {
        PixelFormat.Bgra or PixelFormat.Rgba => 4,
        PixelFormat.Uyvy => 2,
        _ => 4
    };

    public void Dispose()
    {
        if (_disposed) return;
        if (_initialized)
        {
            NdiNative.Destroy();
            _initialized = false;
        }
        _disposed = true;
    }
}
