using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

namespace CS_TEST
{
    public partial class Form1 : Form, IMessageFilter
    {
        private const int WM_DPICHANGED = 0x02E0;
        private const int WS_CLIPCHILDREN = 0x02000000;
        private const int WS_CLIPSIBLINGS = 0x04000000;

        protected override CreateParams CreateParams
        {
            get
            {
                var cp = base.CreateParams;
                cp.Style |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
                return cp;
            }
        }

        private sealed class PreviewPanel : Panel
        {
            private const int WS_CLIPSIBLINGS_LOCAL = 0x04000000;

            public PreviewPanel()
            {
                // The panel itself is the native D3D render target HWND. Avoid WinForms
                // repaint/double-buffer scaling around this child window.
                SetStyle(ControlStyles.AllPaintingInWmPaint |
                         ControlStyles.UserPaint |
                         ControlStyles.OptimizedDoubleBuffer |
                         ControlStyles.ResizeRedraw, true);
            }

            protected override CreateParams CreateParams
            {
                get
                {
                    var cp = base.CreateParams;
                    cp.Style |= WS_CLIPSIBLINGS_LOCAL;
                    return cp;
                }
            }

            public int SlotIndex { get; set; }
            public bool Selected { get; set; }
            public bool Active { get; set; }

            protected override void OnPaint(PaintEventArgs e)
            {
                base.OnPaint(e);
                if (!Active)
                {
                    using var back = new SolidBrush(Color.FromArgb(22, 22, 22));
                    e.Graphics.FillRectangle(back, ClientRectangle);
                    TextRenderer.DrawText(e.Graphics, "No Video", Font, ClientRectangle, Color.LightGray,
                        TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
                }
                using var pen = new Pen(Selected ? Color.DodgerBlue : Color.DimGray, Selected ? 3 : 1);
                e.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
            }
        }

        private sealed class Slot
        {
            public int Index;
            public PreviewPanel Panel = null!;
            public bool Active;
            public bool UseOsd;
            public bool UseCrop;
            public bool DateTimeOsd;
            public bool Recording;
            public bool AudioMonitor;
            public int DateTimeOsdId;
            public HwsVideoInputConfig Config;
            public HwsCropRect CropRect;
            public HwsImageFormat SnapshotFormat = HwsImageFormat.BMP;
            public string SnapshotDir = Environment.CurrentDirectory;
            public readonly List<HwsOsdItem> OsdItems = new();
            public readonly List<int> OsdIds = new();
            public IntPtr VideoIn;
            public IntPtr Render;
            public IntPtr Crop;
            public IntPtr Osd;
            public IntPtr AudioIn;
            public IntPtr Recorder;
        }

        private sealed class PendingVideoRestart
        {
            public Slot Slot = null!;
            public HwsVideoInputConfig Config;
        }

        private readonly List<Slot> _slots = new();
        private readonly System.Windows.Forms.Timer _dateTimeTimer = new System.Windows.Forms.Timer();
        private int _viewCount = 1;
        private int _selectedSlot = 0;
        private int _fullScreenSlot = -1;
        private bool _sdkReady;
        private bool _recVideoCallback;
        private bool _recAudioCallback;
        private IntPtr _audioMonitorMux;
        private IntPtr _audioMonitorRender;
        private int _audioMonitorCount;
        private ContextMenuStrip? _slotContextMenu;
        private int _contextSlotIndex = -1;
        private bool _videoGraphChanging;
        private readonly Dictionary<int, int> _videoSourceOwnerSlotByDevice = new();

        private static readonly HwsVideoCallback VideoInputCallback = OnVideoFrame;
        private static readonly HwsVideoCallback RenderVideoCallback = OnVideoFrame;
        private static readonly HwsEncodedVideoCallback EncodedVideoCallback = OnEncodedVideo;
        private static readonly HwsEncodedAudioCallback EncodedAudioCallback = OnEncodedAudio;

        public Form1()
        {
            InitializeComponent();
            AutoScaleMode = AutoScaleMode.None;
            Text = "hwscapture_test";
            MinimumSize = new Size(960, 600);
            StartPosition = FormStartPosition.CenterScreen;
            CreateMainMenu();
            Load += Form1_Load;
            FormClosing += Form1_FormClosing;
            Resize += (_, _) => RequestLayoutPreviewPanes();
            Disposed += (_, _) => Application.RemoveMessageFilter(this);
        }

        private void Form1_Load(object? sender, EventArgs e)
        {
            Application.AddMessageFilter(this);
            if (Native.HWS_Init() != 0)
            {
                MessageBox.Show(this, "HWS_Init failed.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Error);
                Close();
                return;
            }
            // v6.0: default SDK path is CPU ColorEngine / CPU OSD.
            // Applications that require renderer-side GPU LUT/OSD can switch explicitly.
            Native.HWS_SetDefaultPipelineMode(HwsSdkPipelineMode.CPU);
            _sdkReady = true;
            CreatePreviewPanes();
            SelectSlot(0);
            SetViewCount(1);
            _dateTimeTimer.Interval = 1000;
            _dateTimeTimer.Tick += (_, _) => UpdateDateTimeOsd();
            _dateTimeTimer.Start();
        }

        private void Form1_FormClosing(object? sender, FormClosingEventArgs e)
        {
            _dateTimeTimer.Stop();
            StopAllSlots();
            DestroyGlobalAudioMonitor();
            if (_sdkReady)
            {
                Native.HWS_Shutdown();
                _sdkReady = false;
            }
        }

        private static HwsVideoInputConfig DefaultVideoConfig()
        {
            return new HwsVideoInputConfig
            {
                deviceIndex = 0,
                width = 1280,
                height = 720,
                fps = 30,
                pixelFormat = HwsPixelFormat.YUY2,
                colorSpace = HwsColorSpace.Auto,
                colorRange = HwsColorRange.Auto,
                bufferCount = 4
            };
        }

        private void CreateMainMenu()
        {
            var menu = new MenuStrip();

            var source = new ToolStripMenuItem("Source");
            source.DropDownItems.Add("Add Video Capture Device...", null, (_, _) => OnSourceVideo());
            source.DropDownItems.Add("Add Audio Input Device...", null, (_, _) => OnSourceAudio());
            source.DropDownItems.Add("Stop All Preview", null, (_, _) => StopAllSlots());

            var view = new ToolStripMenuItem("View");
            view.DropDownItems.Add("1 View", null, (_, _) => SetViewCount(1));
            view.DropDownItems.Add("4 Views", null, (_, _) => SetViewCount(4));
            view.DropDownItems.Add("8 Views", null, (_, _) => SetViewCount(8));
            view.DropDownItems.Add("16 Views", null, (_, _) => SetViewCount(16));

            var osd = new ToolStripMenuItem("OSD");
            osd.DropDownItems.Add("Add Text OSD...", null, (_, _) => OnOsdText());
            osd.DropDownItems.Add("Add Image OSD...", null, (_, _) => OnOsdImage());
            osd.DropDownItems.Add("Add Date/Time OSD", null, (_, _) => OnOsdDateTime());
            osd.DropDownItems.Add(new ToolStripSeparator());
            osd.DropDownItems.Add("Remove Last OSD", null, (_, _) => OnOsdRemoveLast());
            osd.DropDownItems.Add("Clear All OSD", null, (_, _) => OnOsdClear());

            var recorder = new ToolStripMenuItem("Recorder");
            recorder.DropDownItems.Add("Start Recording...", null, (_, _) => OnRecorderStart());
            recorder.DropDownItems.Add("Stop Recording", null, (_, _) => OnRecorderStop());
            recorder.DropDownItems.Add(new ToolStripSeparator());
            recorder.DropDownItems.Add("Enable Video Encoded Callback", null, (_, _) => { _recVideoCallback = !_recVideoCallback; UpdateTitleText(); });
            recorder.DropDownItems.Add("Enable Audio Encoded Callback", null, (_, _) => { _recAudioCallback = !_recAudioCallback; UpdateTitleText(); });

            var snapshot = new ToolStripMenuItem("Snapshot");
            snapshot.DropDownItems.Add("Snapshot Settings...", null, (_, _) => OnSnapshotSettings());
            snapshot.DropDownItems.Add("Capture Snapshot", null, (_, _) => OnSnapshotCapture());

            var crop = new ToolStripMenuItem("Video Crop");
            crop.DropDownItems.Add("Add Video Crop...", null, (_, _) => OnCropAdd());
            crop.DropDownItems.Add("Remove Video Crop", null, (_, _) => OnCropRemove());

            var file = new ToolStripMenuItem("File");
            file.DropDownItems.Add("Exit", null, (_, _) => Close());

            menu.Items.AddRange(new ToolStripItem[] { source, view, osd, recorder, snapshot, crop, file });
            MainMenuStrip = menu;
            Controls.Add(menu);
        }

        private void CreatePreviewPanes()
        {
            for (int i = 0; i < 16; ++i)
            {
                var p = new PreviewPanel
                {
                    SlotIndex = i,
                    BackColor = Color.Black,
                    Margin = Padding.Empty,
                    BorderStyle = BorderStyle.None
                };
                p.MouseDown += Preview_MouseDown;
                p.MouseUp += Preview_MouseUp;
                p.ContextMenuStrip = EnsureSlotContextMenu();
                p.DoubleClick += (_, _) => ToggleFullScreen(p.SlotIndex);
                Controls.Add(p);
                _slots.Add(new Slot
                {
                    Index = i,
                    Panel = p,
                    Config = DefaultVideoConfig(),
                    CropRect = new HwsCropRect { x = 0, y = 0, width = 640, height = 480 },
                    DateTimeOsdId = 10000 + i
                });
            }
        }

        private void Preview_MouseDown(object? sender, MouseEventArgs e)
        {
            if (sender is not PreviewPanel p) return;
            SelectSlot(p.SlotIndex);
            if (e.Button == MouseButtons.Right) ShowSlotContextMenu(p, e.Location);
        }

        private void Preview_MouseUp(object? sender, MouseEventArgs e)
        {
            if (sender is not PreviewPanel p) return;
            if (e.Button != MouseButtons.Right) return;
            SelectSlot(p.SlotIndex);
            ShowSlotContextMenu(p, e.Location);
        }

        public bool PreFilterMessage(ref Message m)
        {
            const int WM_RBUTTONUP = 0x0205;
            const int WM_CONTEXTMENU = 0x007B;
            if (m.Msg != WM_RBUTTONUP && m.Msg != WM_CONTEXTMENU) return false;
            if (!Visible || WindowState == FormWindowState.Minimized) return false;

            Point screen;
            if (m.Msg == WM_CONTEXTMENU)
            {
                int lp = m.LParam.ToInt32();
                int x = (short)(lp & 0xffff);
                int y = (short)((lp >> 16) & 0xffff);
                if (x == -1 && y == -1) return false;
                screen = new Point(x, y);
            }
            else
            {
                screen = Cursor.Position;
            }

            for (int i = 0; i < _viewCount && i < _slots.Count; ++i)
            {
                var panel = _slots[i].Panel;
                if (!panel.Visible) continue;
                var rc = new Rectangle(panel.PointToScreen(Point.Empty), panel.Size);
                if (!rc.Contains(screen)) continue;
                SelectSlot(i);
                ShowSlotContextMenu(panel, panel.PointToClient(screen));
                return true;
            }
            return false;
        }

        private ContextMenuStrip EnsureSlotContextMenu()
        {
            if (_slotContextMenu != null) return _slotContextMenu;
            var ctx = new ContextMenuStrip();
            ctx.Opening += (_, e) =>
            {
                var p = ctx.SourceControl as PreviewPanel;
                if (p == null) { e.Cancel = true; return; }
                SelectSlot(p.SlotIndex);
                _contextSlotIndex = p.SlotIndex;
                RefreshSlotContextMenu(ctx);
            };
            _slotContextMenu = ctx;
            RefreshSlotContextMenu(ctx);
            return ctx;
        }

        private void RefreshSlotContextMenu(ContextMenuStrip ctx)
        {
            ctx.Items.Clear();
            var slot = SelectedSlot;
            bool hasVideo = slot != null && slot.VideoIn != IntPtr.Zero;
            bool hasAudio = slot != null && slot.AudioIn != IntPtr.Zero;
            bool monitor = slot != null && slot.AudioMonitor;

            ctx.Items.Add("Select Video Source...", null, (_, _) => OnSourceVideo());
            ctx.Items.Add("Remvo Video/Audio Source", null, (_, _) => OnRemoveVideoAudioSource());
            ctx.Items.Add("Select Audio Source...", null, (_, _) => OnSourceAudio());
            
            ctx.Items.Add("Enable Audio Monitor", null, (_, _) => OnAudioMonitorOn()).Enabled = hasAudio && !monitor;
            ctx.Items.Add("Disable Audio Monitor", null, (_, _) => OnAudioMonitorOff()).Enabled = hasAudio && monitor;
            ctx.Items.Add(new ToolStripSeparator());
            ctx.Items.Add("Add Text OSD...", null, (_, _) => OnOsdText()).Enabled = hasVideo;
            ctx.Items.Add("Add Image OSD...", null, (_, _) => OnOsdImage()).Enabled = hasVideo;
            ctx.Items.Add("Add Date/Time OSD", null, (_, _) => OnOsdDateTime()).Enabled = hasVideo;
            ctx.Items.Add("Remove Last OSD", null, (_, _) => OnOsdRemoveLast()).Enabled = hasVideo && slot!.OsdIds.Count > 0;
            ctx.Items.Add("Clear All OSD", null, (_, _) => OnOsdClear()).Enabled = hasVideo;
            ctx.Items.Add(new ToolStripSeparator());
            ctx.Items.Add("Add Video Crop...", null, (_, _) => OnCropAdd()).Enabled = hasVideo;
            ctx.Items.Add("Remove Video Crop", null, (_, _) => OnCropRemove()).Enabled = hasVideo && slot!.UseCrop;
            ctx.Items.Add(new ToolStripSeparator());
            ctx.Items.Add("Start Recording...", null, (_, _) => OnRecorderStart()).Enabled = hasVideo && !slot!.Recording;
            ctx.Items.Add("Stop Recording", null, (_, _) => OnRecorderStop()).Enabled = hasVideo && slot!.Recording;
            ctx.Items.Add("Capture Snapshot", null, (_, _) => OnSnapshotCapture()).Enabled = hasVideo;
            ctx.Items.Add("Snapshot Settings...", null, (_, _) => OnSnapshotSettings());
            ctx.Items.Add(new ToolStripSeparator());
            ctx.Items.Add("Video Device Properties...", null, (_, _) => OnVideoDeviceProperty()).Enabled = hasVideo;
            ctx.Items.Add("Video Crossbar Properties...", null, (_, _) => OnVideoCrossbarProperty()).Enabled = hasVideo;
            ctx.Items.Add(new ToolStripSeparator());
            ctx.Items.Add("1 View", null, (_, _) => SetViewCount(1));
            ctx.Items.Add("4 Views", null, (_, _) => SetViewCount(4));
            ctx.Items.Add("8 Views", null, (_, _) => SetViewCount(8));
            ctx.Items.Add("16 Views", null, (_, _) => SetViewCount(16));
            ctx.Items.Add("Full Screen", null, (_, _) => { var s = SelectedSlot; if (s != null) ToggleFullScreen(s.Index); });
        }

        private void ShowSlotContextMenu(Control owner, Point pt)
        {
            if (owner is PreviewPanel p) SelectSlot(p.SlotIndex);
            var ctx = EnsureSlotContextMenu();
            RefreshSlotContextMenu(ctx);
            ctx.Show(owner, pt);
        }

        private void SelectSlot(int index)
        {
            if (index < 0 || index >= _viewCount) return;
            _selectedSlot = index;
            for (int i = 0; i < _slots.Count; ++i)
            {
                _slots[i].Panel.Selected = i == _selectedSlot;
                _slots[i].Panel.Invalidate();
            }
            UpdateTitleText();
        }

        private void SetViewCount(int count)
        {
            _viewCount = count;
            _fullScreenSlot = -1;
            if (_selectedSlot >= count) _selectedSlot = 0;
            LayoutPreviewPanes();
            SelectSlot(_selectedSlot);
        }

        private void ToggleFullScreen(int slot)
        {
            if (slot < 0 || slot >= _viewCount) return;
            _fullScreenSlot = _fullScreenSlot == slot ? -1 : slot;
            LayoutPreviewPanes();
        }

        private void RequestLayoutPreviewPanes()
        {
            if (!IsHandleCreated || IsDisposed) return;
            if (WindowState == FormWindowState.Minimized) return;
            BeginInvoke((Action)LayoutPreviewPanes);
        }

        protected override void WndProc(ref Message m)
        {
            base.WndProc(ref m);
            if (m.Msg == WM_DPICHANGED)
            {
                // Re-layout after WinForms applies the DPI suggested bounds. This keeps
                // preview child HWND/backbuffer size aligned with native pixels, matching
                // the C++ hwscapture_test behavior.
                RequestLayoutPreviewPanes();
            }
        }

        private void LayoutPreviewPanes()
        {
            if (MainMenuStrip == null || _slots.Count == 0 || WindowState == FormWindowState.Minimized) return;
            SuspendLayout();
            try
            {
            int top = MainMenuStrip.Height;
            var area = new Rectangle(0, top, ClientSize.Width, Math.Max(1, ClientSize.Height - top));
            int gap = 4;

            if (_fullScreenSlot >= 0)
            {
                for (int i = 0; i < _slots.Count; ++i)
                {
                    _slots[i].Panel.Visible = i == _fullScreenSlot;
                    if (i == _fullScreenSlot)
                        _slots[i].Panel.Bounds = new Rectangle(gap, top + gap, area.Width - gap * 2, area.Height - gap * 2);
                }
                return;
            }

            int cols = _viewCount == 1 ? 1 : (_viewCount == 4 ? 2 : 4);
            int rows = _viewCount == 1 ? 1 : (_viewCount == 4 ? 2 : (_viewCount == 8 ? 2 : 4));
            int w = Math.Max(1, (area.Width - gap * (cols + 1)) / cols);
            int h = Math.Max(1, (area.Height - gap * (rows + 1)) / rows);
            for (int i = 0; i < _slots.Count; ++i)
            {
                if (i < _viewCount)
                {
                    int r = i / cols;
                    int c = i % cols;
                    _slots[i].Panel.Visible = true;
                    _slots[i].Panel.Bounds = new Rectangle(gap + c * (w + gap), top + gap + r * (h + gap), w, h);
                }
                else
                {
                    _slots[i].Panel.Visible = false;
                }
            }
            }
            finally
            {
                ResumeLayout(false);
            }
        }

        private Slot? SelectedSlot => _selectedSlot >= 0 && _selectedSlot < _slots.Count ? _slots[_selectedSlot] : null;

        private void OnSourceVideo()
        {
            var slot = SelectedSlot;
            if (slot == null) return;

            var init = slot.Config;
            if (!slot.Active)
            {
                // For a new source, let the dialog select the selected camera's
                // first/default capability instead of forcing demo fallback
                // 1280x720/30/YUY2 on the first open.
                init.width = 0;
                init.height = 0;
                init.fps = 0;
                init.pixelFormat = HwsPixelFormat.Unknown;
            }

            using var dlg = new VideoSourceDialog(init);
            if (dlg.ShowDialog(this) == DialogResult.OK)
            {
                AddVideoSource(slot, dlg.Config);
            }
        }

        private void AddVideoSource(Slot slot, HwsVideoInputConfig cfg)
        {
            if (slot.Active && SameConfig(slot.Config, cfg))
            {
                slot.Panel.Invalidate();
                return;
            }
            ReplaceVideoSourceWithPeerRepair(slot, cfg);
        }

        private static bool SameConfig(HwsVideoInputConfig a, HwsVideoInputConfig b)
        {
            return a.deviceIndex == b.deviceIndex && a.width == b.width && a.height == b.height && a.fps == b.fps &&
                   a.pixelFormat == b.pixelFormat && a.colorSpace == b.colorSpace && a.colorRange == b.colorRange && a.bufferCount == b.bufferCount;
        }

        private void RebuildVideoSourceOwnerMap()
        {
            _videoSourceOwnerSlotByDevice.Clear();
            for (int i = 0; i < _slots.Count; ++i)
            {
                var s = _slots[i];
                if (!s.Active || s.VideoIn == IntPtr.Zero) continue;
                int deviceIndex = s.Config.deviceIndex;
                if (!_videoSourceOwnerSlotByDevice.ContainsKey(deviceIndex))
                    _videoSourceOwnerSlotByDevice[deviceIndex] = i;
            }
        }

        private int FindVideoSourceOwnerSlot(int deviceIndex)
        {
            RebuildVideoSourceOwnerMap();
            return _videoSourceOwnerSlotByDevice.TryGetValue(deviceIndex, out int slot) ? slot : -1;
        }

        private List<PendingVideoRestart> CollectSharedPeersForSourceOwner(Slot sourceSlot)
        {
            var peers = new List<PendingVideoRestart>();
            if (!sourceSlot.Active || sourceSlot.VideoIn == IntPtr.Zero) return peers;

            int deviceIndex = sourceSlot.Config.deviceIndex;
            if (FindVideoSourceOwnerSlot(deviceIndex) != sourceSlot.Index) return peers;

            for (int i = 0; i < _slots.Count; ++i)
            {
                var s = _slots[i];
                if (s.Index == sourceSlot.Index) continue;
                if (!s.Active || s.VideoIn == IntPtr.Zero) continue;
                if (s.Config.deviceIndex != deviceIndex) continue;
                peers.Add(new PendingVideoRestart { Slot = s, Config = s.Config });
            }
            return peers;
        }

        private void ReplaceVideoSourceWithPeerRepair(Slot slot, HwsVideoInputConfig cfg)
        {
            if (_videoGraphChanging) return;
            _videoGraphChanging = true;
            try
            {
                var peers = CollectSharedPeersForSourceOwner(slot);
                foreach (var peer in peers) DestroyVideoChain(peer.Slot);

                slot.Config = cfg;
                RebuildVideoChain(slot);

                foreach (var peer in peers)
                {
                    peer.Slot.Config = peer.Config;
                    RebuildVideoChain(peer.Slot);
                }
                RebuildVideoSourceOwnerMap();
                UpdateTitleText();
            }
            finally
            {
                _videoGraphChanging = false;
            }
        }

        private void RebuildActiveVideoSlotWithPeerRepair(Slot slot)
        {
            if (!slot.Active)
            {
                RebuildVideoChain(slot);
                return;
            }
            ReplaceVideoSourceWithPeerRepair(slot, slot.Config);
        }

        private void RemoveVideoSourceWithPeerRepair(Slot slot)
        {
            if (_videoGraphChanging) return;
            _videoGraphChanging = true;
            try
            {
                var peers = CollectSharedPeersForSourceOwner(slot);
                foreach (var peer in peers) DestroyVideoChain(peer.Slot);

                DestroyVideoChain(slot);

                foreach (var peer in peers)
                {
                    peer.Slot.Config = peer.Config;
                    RebuildVideoChain(peer.Slot);
                }
                RebuildVideoSourceOwnerMap();
                UpdateTitleText();
            }
            finally
            {
                _videoGraphChanging = false;
            }
        }

        private bool RebuildVideoChain(Slot slot)
        {
            var osdItems = new List<HwsOsdItem>(slot.OsdItems);
            bool useOsd = slot.UseOsd;
            bool useCrop = slot.UseCrop;
            bool dateTime = slot.DateTimeOsd;
            var cropRect = slot.CropRect;
            DestroyVideoChain(slot);
            slot.UseOsd = useOsd;
            slot.UseCrop = useCrop;
            slot.DateTimeOsd = dateTime;
            slot.CropRect = cropRect;
            slot.OsdItems.Clear();
            slot.OsdItems.AddRange(osdItems);
            slot.OsdIds.Clear();
            foreach (var item in osdItems) if (item.id != 0) slot.OsdIds.Add(item.id);

            slot.VideoIn = Native.HWS_CreateVideoInput();
            slot.Render = Native.HWS_CreateVideoRender(slot.Panel.Handle);
            if (slot.UseCrop) slot.Crop = Native.HWS_CreateVideoCrop();

            // v4.0.6: keep VideoOsd permanently in the preview chain.
            // When there is no OSD item, the OSD port works as transparent pass-through.
            // This matches hwscapture_test/MFC behavior and avoids rebuilding VideoInput
            // when adding the first OSD item, which would stop other shared previews
            // using the same capture device.
            slot.Osd = Native.HWS_CreateVideoOsd();

            if (slot.VideoIn == IntPtr.Zero || slot.Render == IntPtr.Zero || (slot.UseCrop && slot.Crop == IntPtr.Zero) || slot.Osd == IntPtr.Zero)
            {
                DestroyVideoChain(slot);
                MessageBox.Show(this, "Failed to create video ports.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            Native.HWS_SetVideoInputConfig(slot.VideoIn, ref slot.Config);
            var renderConfig = new HwsVideoRenderConfig
            {
                width = slot.UseCrop && slot.CropRect.width > 0 ? slot.CropRect.width : slot.Config.width,
                height = slot.UseCrop && slot.CropRect.height > 0 ? slot.CropRect.height : slot.Config.height,
                pixelFormat = slot.Config.pixelFormat
            };
            Native.HWS_SetVideoRenderConfig(slot.Render, ref renderConfig);
            Native.HWS_SetVideoRenderInterpolationMode(slot.Render, HwsRenderInterpolationMode.Auto);
            Native.HWS_SetVideoFrameCallback(slot.VideoIn, VideoInputCallback, IntPtr.Zero);
            Native.HWS_SetVideoFrameCallback(slot.Render, RenderVideoCallback, IntPtr.Zero);

            IntPtr upstream = slot.VideoIn;
            if (slot.UseCrop && slot.Crop != IntPtr.Zero)
            {
                Native.HWS_SetVideoCropRect(slot.Crop, ref slot.CropRect);
                Native.HWS_EnableVideoCrop(slot.Crop, 1);
                Native.HWS_Connect(upstream, slot.Crop);
                upstream = slot.Crop;
            }

            // v4.0.6: OSD is always present in the preview path.
            // No OSD item means pass-through; adding/removing OSD later must not
            // rebuild the DirectShow input graph.
            Native.HWS_Connect(upstream, slot.Osd);
            foreach (var item0 in slot.OsdItems)
            {
                var item = item0;
                if (item.type == HwsOsdItemType.Image) Native.HWS_AddVideoOsdImage(slot.Osd, ref item);
                else Native.HWS_AddVideoOsdText(slot.Osd, ref item);
            }
            if (slot.DateTimeOsd && !slot.OsdIds.Contains(slot.DateTimeOsdId))
            {
                var item = MakeDateTimeOsd(slot.DateTimeOsdId);
                Native.HWS_AddVideoOsdText(slot.Osd, ref item);
                slot.OsdIds.Add(slot.DateTimeOsdId);
            }
            Native.HWS_ConnectVideoOsdOutput(slot.Osd, HwsOsdOutput.Osd, slot.Render);

            if (Native.HWS_Start(slot.VideoIn) != 0)
            {
                DestroyVideoChain(slot);
                MessageBox.Show(this, "Failed to start video preview.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            slot.Active = true;
            slot.Panel.Active = true;
            slot.Panel.Invalidate();
            RebuildVideoSourceOwnerMap();
            UpdateTitleText();
            return true;
        }

        private void DestroyVideoChain(Slot slot)
        {
            StopRecorder(slot);
            if (slot.VideoIn != IntPtr.Zero)
            {
                Native.HWS_SetVideoFrameCallback(slot.VideoIn, null, IntPtr.Zero);
                if (slot.Render != IntPtr.Zero) Native.HWS_SetVideoFrameCallback(slot.Render, null, IntPtr.Zero);
                Native.HWS_Stop(slot.VideoIn);
                Native.HWS_DestroyPort(slot.VideoIn);
                slot.VideoIn = IntPtr.Zero;
            }
            DestroyPort(ref slot.Osd);
            DestroyPort(ref slot.Crop);
            DestroyPort(ref slot.Render);
            slot.Active = false;
            slot.Panel.Active = false;
            slot.Panel.Invalidate();
            RebuildVideoSourceOwnerMap();
            UpdateTitleText();
        }

        private static void DestroyPort(ref IntPtr port)
        {
            if (port != IntPtr.Zero)
            {
                Native.HWS_DestroyPort(port);
                port = IntPtr.Zero;
            }
        }

        private void OnSourceAudio()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            using var dlg = new AudioSourceDialog();
            if (dlg.ShowDialog(this) == DialogResult.OK)
                AddAudioSource(slot, dlg.DeviceIndex);
        }

        private void OnRemoveVideo()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            RemoveVideoSourceWithPeerRepair(slot);
            slot.Panel.Invalidate();
        }

        private void OnRemoveVideoAudioSource()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            RemoveVideoSourceWithPeerRepair(slot);
            OnRemoveAudio();
        }

        private void OnRemoveAudio()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            DisableAudioMonitor(slot);
            if (slot.AudioIn != IntPtr.Zero)
            {
                Native.HWS_Stop(slot.AudioIn);
                Native.HWS_DestroyPort(slot.AudioIn);
                slot.AudioIn = IntPtr.Zero;
            }
            UpdateTitleText();
        }

        private void AddAudioSource(Slot slot, int deviceIndex)
        {
            DisableAudioMonitor(slot);
            if (slot.AudioIn != IntPtr.Zero)
            {
                Native.HWS_Stop(slot.AudioIn);
                Native.HWS_DestroyPort(slot.AudioIn);
                slot.AudioIn = IntPtr.Zero;
            }
            slot.AudioIn = Native.HWS_CreateAudioInput();
            if (slot.AudioIn == IntPtr.Zero) return;
            Native.HWS_SetAudioInputDevice(slot.AudioIn, deviceIndex);
            Native.HWS_SetAudioInputPCM(slot.AudioIn, 48000, 2, 16);
            if (Native.HWS_Start(slot.AudioIn) != 0)
            {
                DestroyPort(ref slot.AudioIn);
                MessageBox.Show(this, "Failed to start audio input.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void OnAudioMonitorOn()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            if (slot.AudioIn == IntPtr.Zero)
            {
                MessageBox.Show(this, "Please select audio source first.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            EnableAudioMonitor(slot);
        }

        private void OnAudioMonitorOff()
        {
            var slot = SelectedSlot;
            if (slot != null) DisableAudioMonitor(slot);
        }

        private bool EnsureGlobalAudioMonitor()
        {
            if (_audioMonitorMux != IntPtr.Zero && _audioMonitorRender != IntPtr.Zero) return true;
            _audioMonitorMux = Native.HWS_CreateAudioMux();
            _audioMonitorRender = Native.HWS_CreateAudioRender();
            if (_audioMonitorMux == IntPtr.Zero || _audioMonitorRender == IntPtr.Zero)
            {
                DestroyGlobalAudioMonitor();
                return false;
            }
            Native.HWS_SetAudioRenderDefaultDevice(_audioMonitorRender);
            Native.HWS_Connect(_audioMonitorMux, _audioMonitorRender);
            Native.HWS_Start(_audioMonitorRender);
            return true;
        }

        private void EnableAudioMonitor(Slot slot)
        {
            if (slot.AudioMonitor) return;
            if (!EnsureGlobalAudioMonitor()) return;
            Native.HWS_Connect(slot.AudioIn, _audioMonitorMux);
            slot.AudioMonitor = true;
            _audioMonitorCount++;
        }

        private void DisableAudioMonitor(Slot slot)
        {
            if (!slot.AudioMonitor) return;
            if (slot.AudioIn != IntPtr.Zero && _audioMonitorMux != IntPtr.Zero)
                Native.HWS_Disconnect(slot.AudioIn, _audioMonitorMux);
            slot.AudioMonitor = false;
            if (_audioMonitorCount > 0) _audioMonitorCount--;
            if (_audioMonitorCount == 0) DestroyGlobalAudioMonitor();
        }

        private void DestroyGlobalAudioMonitor()
        {
            if (_audioMonitorRender != IntPtr.Zero)
            {
                Native.HWS_Stop(_audioMonitorRender);
                Native.HWS_DestroyPort(_audioMonitorRender);
                _audioMonitorRender = IntPtr.Zero;
            }
            DestroyPort(ref _audioMonitorMux);
            _audioMonitorCount = 0;
            foreach (var s in _slots) s.AudioMonitor = false;
        }

        private void OnOsdText()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            using var dlg = new TextOsdDialog();
            if (dlg.ShowDialog(this) == DialogResult.OK) AddOsdItem(slot, dlg.Item);
        }

        private void OnOsdImage()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            using var dlg = new ImageOsdDialog();
            if (dlg.ShowDialog(this) == DialogResult.OK) AddOsdItem(slot, dlg.Item);
        }

        private void AddOsdItem(Slot slot, HwsOsdItem item)
        {
            if (item.id == 0) item.id = 20000 + slot.Index * 1000 + slot.OsdItems.Count + 1;
            slot.UseOsd = true;
            slot.OsdItems.Add(item);
            slot.OsdIds.Add(item.id);
            if (slot.Active)
            {
                if (slot.Osd == IntPtr.Zero) RebuildVideoChain(slot);
                else
                {
                    if (item.type == HwsOsdItemType.Image) Native.HWS_AddVideoOsdImage(slot.Osd, ref item);
                    else Native.HWS_AddVideoOsdText(slot.Osd, ref item);
                }
            }
        }

        private void OnOsdDateTime()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            slot.DateTimeOsd = true;
            slot.UseOsd = true;
            if (slot.Active)
            {
                if (slot.Osd == IntPtr.Zero) RebuildVideoChain(slot);
                else
                {
                    var item = MakeDateTimeOsd(slot.DateTimeOsdId);
                    Native.HWS_AddVideoOsdText(slot.Osd, ref item);
                    if (!slot.OsdIds.Contains(slot.DateTimeOsdId)) slot.OsdIds.Add(slot.DateTimeOsdId);
                }
            }
        }

        private static HwsOsdItem MakeDateTimeOsd(int id)
        {
            var item = MakeTextOsd(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
            item.id = id;
            item.x = 20;
            item.y = 20;
            item.width = 520;
            item.height = 56;
            item.zOrder = 10000;
            return item;
        }

        private static HwsOsdItem MakeTextOsd(string text)
        {
            return new HwsOsdItem
            {
                id = 0,
                enable = 1,
                type = HwsOsdItemType.Text,
                zOrder = 100,
                x = 24,
                y = 24,
                width = 500,
                height = 60,
                opacity = 0.85f,
                color = 0xffffffff,
                backgroundColor = 0x80000000,
                transparentBackground = 0,
                fontSize = 28,
                text = text,
                fontName = "Microsoft YaHei",
                filePath = string.Empty
            };
        }

        private void UpdateDateTimeOsd()
        {
            foreach (var slot in _slots)
                if (slot.DateTimeOsd && slot.Osd != IntPtr.Zero)
                    Native.HWS_UpdateVideoOsdText(slot.Osd, slot.DateTimeOsdId, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
        }

        private void OnOsdRemoveLast()
        {
            var slot = SelectedSlot;
            if (slot == null || slot.OsdIds.Count == 0) return;
            int id = slot.OsdIds[^1];
            slot.OsdIds.RemoveAt(slot.OsdIds.Count - 1);
            slot.OsdItems.RemoveAll(x => x.id == id);
            if (id == slot.DateTimeOsdId) slot.DateTimeOsd = false;
            if (slot.Osd != IntPtr.Zero) Native.HWS_RemoveVideoOsdItem(slot.Osd, id);
        }

        private void OnOsdClear()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            slot.OsdItems.Clear();
            slot.OsdIds.Clear();
            slot.DateTimeOsd = false;
            if (slot.Osd != IntPtr.Zero) Native.HWS_ClearVideoOsd(slot.Osd);
        }

        private void OnCropAdd()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            using var dlg = new CropDialog(slot.CropRect.width > 0 ? slot.CropRect : new HwsCropRect { x = 0, y = 0, width = 640, height = 480 });
            if (dlg.ShowDialog(this) == DialogResult.OK)
            {
                slot.CropRect = dlg.Rect;
                slot.UseCrop = true;
                if (slot.Active) RebuildActiveVideoSlotWithPeerRepair(slot);
            }
        }

        private void OnCropRemove()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            slot.UseCrop = false;
            if (slot.Active) RebuildActiveVideoSlotWithPeerRepair(slot);
        }

        private void OnRecorderStart()
        {
            var slot = SelectedSlot;
            if (slot == null || slot.VideoIn == IntPtr.Zero)
            {
                MessageBox.Show(this, "Please add video source first.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Information);
                return;
            }
            if (slot.Recording) return;
            using var dlg = new SaveFileDialog { Filter = "MP4 Files (*.mp4)|*.mp4|MOV Files (*.mov)|*.mov|All Files (*.*)|*.*", DefaultExt = "mp4", OverwritePrompt = true };
            if (dlg.ShowDialog(this) != DialogResult.OK) return;

            slot.Recorder = Native.HWS_CreateRecorder(null);
            if (slot.Recorder == IntPtr.Zero) return;
            var cfg = new HwsRecorderConfig
            {
                filePath = dlg.FileName,
                container = Path.GetExtension(dlg.FileName).Equals(".mov", StringComparison.OrdinalIgnoreCase) ? HwsContainerFormat.MOV : HwsContainerFormat.MP4,
                videoCodec = HwsVideoCodec.H264,
                width = slot.UseCrop && slot.CropRect.width > 0 ? slot.CropRect.width : slot.Config.width,
                height = slot.UseCrop && slot.CropRect.height > 0 ? slot.CropRect.height : slot.Config.height,
                fps = slot.Config.fps > 0 ? slot.Config.fps : 30,
                recordFps = 60,
                videoBitrate = 8000000,
                enableAudio = slot.AudioIn != IntPtr.Zero ? 1 : 0,
                inputMode = HwsRecorderInputMode.Auto
            };
            Native.HWS_SetRecorderConfig(slot.Recorder, ref cfg);
            if (_recVideoCallback) Native.HWS_SetRecorderVideoCallback(slot.Recorder, EncodedVideoCallback, new IntPtr(slot.Index));
            if (_recAudioCallback) Native.HWS_SetRecorderAudioCallback(slot.Recorder, EncodedAudioCallback, new IntPtr(slot.Index));

            if (slot.Osd != IntPtr.Zero) Native.HWS_ConnectVideoOsdOutput(slot.Osd, HwsOsdOutput.Osd, slot.Recorder);
            else if (slot.Crop != IntPtr.Zero) Native.HWS_Connect(slot.Crop, slot.Recorder);
            else Native.HWS_Connect(slot.VideoIn, slot.Recorder);
            if (slot.AudioIn != IntPtr.Zero) Native.HWS_Connect(slot.AudioIn, slot.Recorder);
            if (Native.HWS_Start(slot.Recorder) != 0)
            {
                DestroyPort(ref slot.Recorder);
                MessageBox.Show(this, "Failed to start recorder.", "hwscapture", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            slot.Recording = true;
            UpdateTitleText();
        }

        private void OnRecorderStop()
        {
            var slot = SelectedSlot;
            if (slot != null) StopRecorder(slot);
        }

        private void StopRecorder(Slot slot)
        {
            if (slot.Recorder == IntPtr.Zero) return;
            if (slot.Osd != IntPtr.Zero) Native.HWS_Disconnect(slot.Osd, slot.Recorder);
            if (slot.Crop != IntPtr.Zero) Native.HWS_Disconnect(slot.Crop, slot.Recorder);
            if (slot.VideoIn != IntPtr.Zero) Native.HWS_Disconnect(slot.VideoIn, slot.Recorder);
            if (slot.AudioIn != IntPtr.Zero) Native.HWS_Disconnect(slot.AudioIn, slot.Recorder);
            Native.HWS_Stop(slot.Recorder);
            Native.HWS_DestroyPort(slot.Recorder);
            slot.Recorder = IntPtr.Zero;
            slot.Recording = false;
            UpdateTitleText();
        }

        private void OnSnapshotSettings()
        {
            var slot = SelectedSlot;
            if (slot == null) return;
            using var dlg = new SnapshotSettingsDialog(slot.SnapshotDir, slot.SnapshotFormat);
            if (dlg.ShowDialog(this) == DialogResult.OK)
            {
                slot.SnapshotDir = dlg.Directory;
                slot.SnapshotFormat = dlg.Format;
            }
        }

        private void OnSnapshotCapture()
        {
            var slot = SelectedSlot;
            if (slot == null || slot.Render == IntPtr.Zero) return;
            Directory.CreateDirectory(slot.SnapshotDir);
            string ext = slot.SnapshotFormat == HwsImageFormat.JPG ? "jpg" : "bmp";
            string file = Path.Combine(slot.SnapshotDir, $"ch{slot.Index}_{DateTime.Now:yyyyMMdd_HHmmss}.{ext}");
            if (Native.HWS_SaveVideoRenderSnapshot(slot.Render, file, slot.SnapshotFormat) == 0)
                MessageBox.Show(this, file, "Snapshot saved", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        private void OnVideoDeviceProperty()
        {
            var slot = SelectedSlot;
            if (slot != null) Native.HWS_ShowVideoDevicePropertyPage(Handle, slot.Config.deviceIndex);
        }

        private void OnVideoCrossbarProperty()
        {
            var slot = SelectedSlot;
            if (slot != null) Native.HWS_ShowVideoCrossbarPropertyPage(Handle, slot.Config.deviceIndex);
        }

        private void StopAllSlots()
        {
            for (int i = _slots.Count - 1; i >= 0; --i)
            {
                DisableAudioMonitor(_slots[i]);
                if (_slots[i].AudioIn != IntPtr.Zero)
                {
                    Native.HWS_Stop(_slots[i].AudioIn);
                    Native.HWS_DestroyPort(_slots[i].AudioIn);
                    _slots[i].AudioIn = IntPtr.Zero;
                }
                DestroyVideoChain(_slots[i]);
            }
            _videoSourceOwnerSlotByDevice.Clear();
            UpdateTitleText();
        }

        private void UpdateTitleText()
        {
            int active = 0;
            int recording = 0;
            foreach (var s in _slots)
            {
                if (s.Active) active++;
                if (s.Recording) recording++;
            }
            Text = $"hwscapture_test - view={_viewCount} active={active} recording={recording} selected={_selectedSlot + 1}";
        }

        private static void OnVideoFrame(IntPtr frame, IntPtr userData) { }
        private static void OnEncodedVideo(IntPtr packet, IntPtr userData)
        {
            try
            {
                var p = System.Runtime.InteropServices.Marshal.PtrToStructure<HwsEncodedVideoPacket>(packet);
                if (p.keyFrame != 0 || (p.frameIndex % 60) == 0) Debug.WriteLine($"Recorder video cb slot={userData.ToInt32()} frame={p.frameIndex} size={p.size} key={p.keyFrame}");
            }
            catch { }
        }
        private static void OnEncodedAudio(IntPtr packet, IntPtr userData)
        {
            try
            {
                var p = System.Runtime.InteropServices.Marshal.PtrToStructure<HwsEncodedAudioPacket>(packet);
                Debug.WriteLine($"Recorder audio cb slot={userData.ToInt32()} size={p.size} pts={p.pts100ns}");
            }
            catch { }
        }
    }


    internal sealed class VideoSourceDialog : Form
    {
        private sealed class CapItem
        {
            public HwsVideoFormat Cap;
            public int Fps => Cap.fpsDen != 0 ? Cap.fpsNum / Cap.fpsDen : 0;
            public string Resolution => $"{Cap.width} x {Cap.height}";
            public string Format => Cap.CapFormatName();
        }

        private static bool IsDemoRenderablePixelFormat(HwsPixelFormat fmt)
        {
            return fmt == HwsPixelFormat.YUY2 ||
                   fmt == HwsPixelFormat.NV12 ||
                   fmt == HwsPixelFormat.RGB24 ||
                   fmt == HwsPixelFormat.RGB32 ||
                   fmt == HwsPixelFormat.MJPG;
        }

        private readonly ComboBox _device = new() { DropDownStyle = ComboBoxStyle.DropDownList };
        private readonly ComboBox _resolution = new() { DropDownStyle = ComboBoxStyle.DropDownList };
        private readonly ComboBox _fps = new() { DropDownStyle = ComboBoxStyle.DropDownList };
        private readonly ComboBox _format = new() { DropDownStyle = ComboBoxStyle.DropDownList };
        private readonly Label _info = new() { BorderStyle = BorderStyle.FixedSingle, AutoEllipsis = true };
        private readonly List<CapItem> _caps = new();
        private bool _forceDeviceDefault;
        private bool _updatingCombos;
        public HwsVideoInputConfig Config { get; private set; }

        public VideoSourceDialog(HwsVideoInputConfig init)
        {
            Config = init;
            _forceDeviceDefault = Config.width <= 0 ||
                                  Config.height <= 0 ||
                                  Config.fps <= 0 ||
                                  Config.pixelFormat == HwsPixelFormat.Unknown;
            Text = "Properties: Video Capture";
            FormBorderStyle = FormBorderStyle.FixedDialog;
            MaximizeBox = false;
            MinimizeBox = false;
            ClientSize = new Size(650, 360);
            StartPosition = FormStartPosition.CenterParent;

            AddLabel("Video Capture Device", 20, 22, 155);
            _device.SetBounds(185, 18, 430, 25);
            Controls.Add(_device);

            var cfgVideo = new Button { Text = "Configure Video", Left = 185, Top = 55, Width = 145, Height = 28 };
            var cfgCrossbar = new Button { Text = "Configure Crossbar", Left = 345, Top = 55, Width = 160, Height = 28 };
            cfgVideo.Click += (_, _) => { if (_device.SelectedIndex >= 0) Native.HWS_ShowVideoDevicePropertyPage(Handle, _device.SelectedIndex); };
            cfgCrossbar.Click += (_, _) => { if (_device.SelectedIndex >= 0) Native.HWS_ShowVideoCrossbarPropertyPage(Handle, _device.SelectedIndex); };
            Controls.AddRange(new Control[] { cfgVideo, cfgCrossbar });

            AddLabel("Resolution", 20, 106, 155);
            _resolution.SetBounds(185, 102, 190, 25);
            Controls.Add(_resolution);

            AddLabel("Frame Rate", 390, 106, 80);
            _fps.SetBounds(470, 102, 145, 25);
            Controls.Add(_fps);

            AddLabel("Pixel Format", 20, 146, 155);
            _format.SetBounds(185, 142, 190, 25);
            Controls.Add(_format);

            AddLabel("Selected Format", 20, 192, 155);
            _info.SetBounds(185, 188, 430, 95);
            Controls.Add(_info);

            var ok = new Button { Text = "OK", Left = 415, Top = 310, Width = 90, Height = 30, DialogResult = DialogResult.OK };
            var cancel = new Button { Text = "Cancel", Left = 525, Top = 310, Width = 90, Height = 30, DialogResult = DialogResult.Cancel };
            Controls.AddRange(new Control[] { ok, cancel });
            AcceptButton = ok;
            CancelButton = cancel;

            Load += (_, _) => LoadDevices();
            _device.SelectedIndexChanged += (_, _) =>
            {
                if (_updatingCombos) return;
                _forceDeviceDefault = true;
                LoadCapsForSelectedDevice();
            };
            _resolution.SelectedIndexChanged += (_, _) =>
            {
                if (_updatingCombos) return;
                RefillFpsAndFormat();
            };
            _fps.SelectedIndexChanged += (_, _) =>
            {
                if (_updatingCombos) return;
                UpdateInfo();
            };
            _format.SelectedIndexChanged += (_, _) =>
            {
                if (_updatingCombos) return;
                RefillFpsOnly();
                UpdateInfo();
            };
            ok.Click += (_, _) => Apply();
        }

        private void AddLabel(string text, int left, int top, int width)
        {
            Controls.Add(new Label { Text = text, Left = left, Top = top, Width = width, Height = 22 });
        }

        private void LoadDevices()
        {
            Native.HWS_RefreshDeviceList();
            _device.Items.Clear();
            int count = Native.HWS_GetVideoDeviceCount();
            for (int i = 0; i < count; ++i)
            {
                Native.HWS_GetVideoDeviceInfo(i, out var info);
                _device.Items.Add($"{i}: {info.Name}");
            }
            if (_device.Items.Count > 0)
                _device.SelectedIndex = Math.Min(Math.Max(Config.deviceIndex, 0), _device.Items.Count - 1);
        }

        private void LoadCapsForSelectedDevice()
        {
            _caps.Clear();
            int dev = Math.Max(0, _device.SelectedIndex);
            int count = Native.HWS_GetVideoCapabilityCount(dev);
            for (int i = 0; i < count; ++i)
            {
                if (Native.HWS_GetVideoCapability(dev, i, out var cap) == 0 &&
                    cap.width > 0 &&
                    cap.height > 0 &&
                    cap.pixelFormat != HwsPixelFormat.Unknown &&
                    IsDemoRenderablePixelFormat(cap.pixelFormat))
                {
                    _caps.Add(new CapItem { Cap = cap });
                }
            }

            if (_forceDeviceDefault)
            {
                ApplyFirstCapToConfig();
                _forceDeviceDefault = false;
            }

            FillResolutionCombo();
        }

        private void ApplyFirstCapToConfig()
        {
            if (_caps.Count == 0) return;
            var cap = _caps[0];
            Config = new HwsVideoInputConfig
            {
                deviceIndex = Math.Max(0, _device.SelectedIndex),
                width = cap.Cap.width,
                height = cap.Cap.height,
                fps = cap.Fps > 0 ? cap.Fps : 30,
                pixelFormat = cap.Cap.pixelFormat,
                colorSpace = HwsColorSpace.Auto,
                colorRange = HwsColorRange.Auto,
                bufferCount = 4
            };
        }

        private void FillResolutionCombo()
        {
            string old = Config.width > 0 ? $"{Config.width} x {Config.height}" : string.Empty;
            _updatingCombos = true;
            try
            {
                _resolution.Items.Clear();
                var seen = new HashSet<string>();
                foreach (var c in _caps)
                    if (seen.Add(c.Resolution)) _resolution.Items.Add(c.Resolution);
                if (_resolution.Items.Count == 0) return;
                int idx = _resolution.Items.IndexOf(old);
                _resolution.SelectedIndex = idx >= 0 ? idx : 0;
            }
            finally
            {
                _updatingCombos = false;
            }
            RefillFpsAndFormat();
        }

        private void RefillFpsAndFormat()
        {
            RefillFormatOnly();
            RefillFpsOnly();
            UpdateInfo();
        }

        private void RefillFormatOnly()
        {
            string res = _resolution.SelectedItem as string ?? string.Empty;
            string old = Config.pixelFormat.FormatName();

            _updatingCombos = true;
            try
            {
                _format.Items.Clear();
                var seen = new HashSet<string>();
                foreach (var c in _caps)
                    if (c.Resolution == res && seen.Add(c.Format)) _format.Items.Add(c.Format);
                if (_format.Items.Count == 0) return;
                int idx = _format.Items.IndexOf(old);
                _format.SelectedIndex = idx >= 0 ? idx : 0;
            }
            finally
            {
                _updatingCombos = false;
            }
        }

        private void RefillFpsOnly()
        {
            string res = _resolution.SelectedItem as string ?? string.Empty;
            string fmt = _format.SelectedItem as string ?? string.Empty;
            int oldFps = Config.fps;

            _updatingCombos = true;
            try
            {
                _fps.Items.Clear();
                var seen = new HashSet<int>();
                foreach (var c in _caps)
                    if (c.Resolution == res && c.Format == fmt && seen.Add(c.Fps)) _fps.Items.Add(c.Fps);
                if (_fps.Items.Count == 0)
                {
                    foreach (var c in _caps)
                        if (c.Resolution == res && seen.Add(c.Fps)) _fps.Items.Add(c.Fps);
                }
                if (_fps.Items.Count == 0) return;
                int idx = _fps.Items.IndexOf(oldFps);
                _fps.SelectedIndex = idx >= 0 ? idx : 0;
            }
            finally
            {
                _updatingCombos = false;
            }
        }

        private CapItem? SelectedCap()
        {
            string res = _resolution.SelectedItem as string ?? string.Empty;
            int fps = _fps.SelectedItem is int v ? v : 0;
            string fmt = _format.SelectedItem as string ?? string.Empty;
            foreach (var c in _caps)
                if (c.Resolution == res && c.Fps == fps && c.Format == fmt) return c;
            return null;
        }

        private void UpdateInfo()
        {
            var c = SelectedCap();
            if (c == null)
            {
                _info.Text = string.Empty;
                return;
            }
            _info.Text = $"Device: {_device.SelectedIndex}\r\nResolution: {c.Cap.width} x {c.Cap.height}\r\nFrame Rate: {c.Fps} fps\r\nPixel Format: {c.Format}";
        }

        private void Apply()
        {
            var cap = SelectedCap();
            if (cap == null) return;
            Config = new HwsVideoInputConfig
            {
                deviceIndex = Math.Max(0, _device.SelectedIndex),
                width = cap.Cap.width,
                height = cap.Cap.height,
                fps = cap.Fps > 0 ? cap.Fps : 30,
                pixelFormat = cap.Cap.pixelFormat,
                colorSpace = HwsColorSpace.Auto,
                colorRange = HwsColorRange.Auto,
                bufferCount = 4
            };
        }
    }

    internal sealed class AudioSourceDialog : Form
    {
        private readonly ComboBox _device = new() { DropDownStyle = ComboBoxStyle.DropDownList };
        public int DeviceIndex => _device.SelectedIndex;
        public AudioSourceDialog()
        {
            Text = "Properties: Audio Input Capture";
            FormBorderStyle = FormBorderStyle.FixedDialog;
            MaximizeBox = false;
            MinimizeBox = false;
            ClientSize = new Size(640, 150);
            StartPosition = FormStartPosition.CenterParent;
            Controls.Add(new Label { Text = "Audio Input Device", Left = 20, Top = 24, Width = 160 });
            Controls.Add(_device); _device.SetBounds(190, 20, 410, 25);
            var ok = new Button { Text = "OK", Left = 410, Top = 85, Width = 90, DialogResult = DialogResult.OK };
            var cancel = new Button { Text = "Cancel", Left = 510, Top = 85, Width = 90, DialogResult = DialogResult.Cancel };
            Controls.AddRange(new Control[] { ok, cancel }); AcceptButton = ok; CancelButton = cancel;
            Load += (_, _) =>
            {
                Native.HWS_RefreshDeviceList();
                for (int i = 0; i < Native.HWS_GetAudioInputDeviceCount(); ++i)
                {
                    Native.HWS_GetAudioInputDeviceInfo(i, out var info);
                    _device.Items.Add($"{i}: {info.Name}");
                }
                if (_device.Items.Count > 0) _device.SelectedIndex = 0;
            };
        }
    }

    internal sealed class TextOsdDialog : Form
    {
        private readonly TextBox _text = new() { Text = "hwscapture OSD" };
        private readonly NumericUpDown _x = Num(0, 10000, 20), _y = Num(0, 10000, 20), _w = Num(1, 10000, 520), _h = Num(1, 10000, 60);
        private readonly NumericUpDown _fontSize = Num(8, 200, 28), _opacity = Num(0, 100, 90);
        private readonly ComboBox _fontName = new() { DropDownStyle = ComboBoxStyle.DropDown };
        private readonly CheckBox _transparent = new() { Text = "Transparent Background", AutoSize = true };
        private Color _textColor = Color.White;
        private Color _bgColor = Color.FromArgb(128, 0, 0, 0);
        public HwsOsdItem Item { get; private set; }

        public TextOsdDialog()
        {
            Text = "Add Text OSD";
            FormBorderStyle = FormBorderStyle.FixedDialog;
            MaximizeBox = false;
            MinimizeBox = false;
            ClientSize = new Size(620, 330);
            StartPosition = FormStartPosition.CenterParent;

            AddLabel("Text", 20, 22, 95);
            _text.SetBounds(125, 18, 360, 24); Controls.Add(_text);
            AddLabel("X", 20, 70, 30); _x.SetBounds(55, 66, 80, 24); Controls.Add(_x);
            AddLabel("Y", 150, 70, 30); _y.SetBounds(185, 66, 80, 24); Controls.Add(_y);
            AddLabel("Width", 20, 110, 55); _w.SetBounds(80, 106, 80, 24); Controls.Add(_w);
            AddLabel("Height", 180, 110, 60); _h.SetBounds(245, 106, 80, 24); Controls.Add(_h);
            AddLabel("Opacity", 20, 145, 120); _opacity.SetBounds(145, 141, 80, 24); Controls.Add(_opacity);

            AddLabel("Font", 20, 180, 95);
            _fontName.SetBounds(125, 176, 180, 28);
            _fontName.Items.AddRange(new object[] { "Microsoft YaHei", "Arial", "Tahoma", "Consolas", "SimSun" });
            _fontName.Text = "Microsoft YaHei";
            Controls.Add(_fontName);
            AddLabel("Size", 320, 180, 35); _fontSize.SetBounds(360, 176, 55, 24); Controls.Add(_fontSize);
            var textColor = new Button { Text = "Text Color", Left = 430, Top = 176, Width = 105, Height = 28 };
            textColor.Click += (_, _) => PickColor(ref _textColor);
            Controls.Add(textColor);
            var bgColor = new Button { Text = "Background Color", Left = 125, Top = 212, Width = 135, Height = 28 };
            bgColor.Click += (_, _) => PickColor(ref _bgColor);
            Controls.Add(bgColor);
            _transparent.SetBounds(280, 216, 200, 24); Controls.Add(_transparent);

            var ok = new Button { Text = "OK", Left = 395, Top = 280, Width = 90, Height = 30, DialogResult = DialogResult.OK };
            var cancel = new Button { Text = "Cancel", Left = 500, Top = 280, Width = 90, Height = 30, DialogResult = DialogResult.Cancel };
            Controls.AddRange(new Control[] { ok, cancel });
            AcceptButton = ok; CancelButton = cancel;
            ok.Click += (_, _) => Apply();
        }

        private void AddLabel(string text, int left, int top, int width) => Controls.Add(new Label { Text = text, Left = left, Top = top, Width = width, Height = 22 });
        private static NumericUpDown Num(int min, int max, int val) => new() { Minimum = min, Maximum = max, Value = val };
        private void PickColor(ref Color color) { using var dlg = new ColorDialog { Color = color, FullOpen = true }; if (dlg.ShowDialog(this) == DialogResult.OK) color = dlg.Color; }
        private static uint ToArgb(Color c) => ((uint)c.A << 24) | ((uint)c.R << 16) | ((uint)c.G << 8) | c.B;

        private void Apply()
        {
            Item = new HwsOsdItem
            {
                id = 0,
                enable = 1,
                type = HwsOsdItemType.Text,
                zOrder = 100,
                x = (int)_x.Value,
                y = (int)_y.Value,
                width = (int)_w.Value,
                height = (int)_h.Value,
                opacity = (float)_opacity.Value / 100.0f,
                color = ToArgb(_textColor),
                backgroundColor = ToArgb(_bgColor),
                transparentBackground = _transparent.Checked ? 1 : 0,
                fontSize = (int)_fontSize.Value,
                text = _text.Text,
                fontName = string.IsNullOrWhiteSpace(_fontName.Text) ? "Microsoft YaHei" : _fontName.Text,
                filePath = string.Empty
            };
        }
    }

    internal sealed class ImageOsdDialog : Form
    {
        private readonly TextBox _path = new();
        private readonly NumericUpDown _x = Num(20), _y = Num(20), _w = Num(320), _h = Num(180), _opacity = Num(90, 0, 100);
        private readonly CheckBox _transparent = new() { Text = "Transparent Background Color", AutoSize = true, Checked = true };
        private Color _keyColor = Color.Black;
        public HwsOsdItem Item { get; private set; }
        public ImageOsdDialog()
        {
            Text = "Add Image OSD";
            FormBorderStyle = FormBorderStyle.FixedDialog;
            MaximizeBox = false;
            MinimizeBox = false;
            ClientSize = new Size(620, 330);
            StartPosition = FormStartPosition.CenterParent;

            Controls.Add(new Label { Text = "Image File", Left = 20, Top = 22, Width = 95 });
            _path.SetBounds(125, 18, 360, 24); Controls.Add(_path);
            var browse = new Button { Text = "Browse...", Left = 495, Top = 17, Width = 90, Height = 26 }; Controls.Add(browse); browse.Click += (_, _) => Browse();
            AddRow("X", _x, 66, 55); AddRow("Y", _y, 66, 185);
            AddRow("Width", _w, 106, 80); AddRow("Height", _h, 106, 245);
            Controls.Add(new Label { Text = "Opacity", Left = 20, Top = 145, Width = 120 }); _opacity.SetBounds(145, 141, 80, 24); Controls.Add(_opacity);
            _transparent.SetBounds(125, 176, 230, 24); Controls.Add(_transparent);
            var key = new Button { Text = "Choose Key Color", Left = 370, Top = 172, Width = 135, Height = 28 };
            key.Click += (_, _) => { using var dlg = new ColorDialog { Color = _keyColor, FullOpen = true }; if (dlg.ShowDialog(this) == DialogResult.OK) _keyColor = dlg.Color; };
            Controls.Add(key);
            var ok = new Button { Text = "OK", Left = 395, Top = 250, Width = 90, Height = 30, DialogResult = DialogResult.OK };
            var cancel = new Button { Text = "Cancel", Left = 500, Top = 250, Width = 90, Height = 30, DialogResult = DialogResult.Cancel };
            Controls.AddRange(new Control[] { ok, cancel }); AcceptButton = ok; CancelButton = cancel; ok.Click += (_, _) => Apply();
        }
        private void Browse() { using var dlg = new OpenFileDialog { Filter = "Image Files|*.bmp;*.png;*.jpg;*.jpeg|All Files|*.*" }; if (dlg.ShowDialog(this) == DialogResult.OK) _path.Text = dlg.FileName; }
        private void AddRow(string label, Control c, int top, int left) { Controls.Add(new Label { Text = label, Left = left - 60, Top = top + 4, Width = 55 }); c.SetBounds(left, top, 80, 24); Controls.Add(c); }
        private static NumericUpDown Num(int val, int min = 0, int max = 10000) => new() { Minimum = min, Maximum = max, Value = val };
        private static uint ToArgb(Color c) => ((uint)c.A << 24) | ((uint)c.R << 16) | ((uint)c.G << 8) | c.B;
        private void Apply()
        {
            Item = new HwsOsdItem
            {
                id = 0,
                enable = 1,
                type = HwsOsdItemType.Image,
                zOrder = 200,
                x = (int)_x.Value,
                y = (int)_y.Value,
                width = (int)_w.Value,
                height = (int)_h.Value,
                opacity = (float)_opacity.Value / 100.0f,
                color = 0xffffffff,
                backgroundColor = ToArgb(_keyColor),
                transparentBackground = _transparent.Checked ? 1 : 0,
                fontSize = 0,
                text = string.Empty,
                fontName = string.Empty,
                filePath = _path.Text
            };
        }
    }

    internal sealed class CropDialog : Form
    {
        private readonly NumericUpDown _x = Num(0), _y = Num(0), _w = Num(640), _h = Num(480);
        public HwsCropRect Rect { get; private set; }
        public CropDialog(HwsCropRect init)
        {
            Rect = init; Text = "Add Video Crop"; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; ClientSize = new Size(320, 260); StartPosition = FormStartPosition.CenterParent;
            _x.Value = init.x; _y.Value = init.y; _w.Value = init.width > 0 ? init.width : 640; _h.Value = init.height > 0 ? init.height : 480;
            AddRow("Left", _x, 22); AddRow("Top", _y, 57); AddRow("Width", _w, 92); AddRow("Height", _h, 127);
            var ok = new Button { Text = "OK", Left = 95, Top = 178, Width = 80, Height = 28, DialogResult = DialogResult.OK };
            var cancel = new Button { Text = "Cancel", Left = 190, Top = 178, Width = 80, Height = 28, DialogResult = DialogResult.Cancel };
            Controls.AddRange(new Control[] { ok, cancel }); AcceptButton = ok; CancelButton = cancel; ok.Click += (_, _) => Rect = new HwsCropRect { x = (int)_x.Value, y = (int)_y.Value, width = (int)_w.Value, height = (int)_h.Value };
        }
        private void AddRow(string label, Control c, int top) { Controls.Add(new Label { Text = label, Left = 25, Top = top + 4, Width = 90 }); c.SetBounds(120, top, 100, 24); Controls.Add(c); }
        private static NumericUpDown Num(int val) => new() { Minimum = 0, Maximum = 10000, Value = val };
    }

    internal sealed class SnapshotSettingsDialog : Form
    {
        private readonly TextBox _dir = new();
        private readonly ComboBox _fmt = new() { DropDownStyle = ComboBoxStyle.DropDownList };
        public string Directory => _dir.Text;
        public HwsImageFormat Format => _fmt.SelectedIndex == 1 ? HwsImageFormat.JPG : HwsImageFormat.BMP;
        public SnapshotSettingsDialog(string dir, HwsImageFormat fmt)
        {
            Text = "Snapshot Settings"; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; ClientSize = new Size(600, 160); StartPosition = FormStartPosition.CenterParent;
            Controls.Add(new Label { Text = "Directory", Left = 20, Top = 24, Width = 100 }); _dir.SetBounds(130, 20, 350, 24); _dir.Text = dir; Controls.Add(_dir);
            var browse = new Button { Text = "Browse...", Left = 490, Top = 19, Width = 80 }; Controls.Add(browse); browse.Click += (_, _) => { using var f = new FolderBrowserDialog { SelectedPath = _dir.Text }; if (f.ShowDialog(this) == DialogResult.OK) _dir.Text = f.SelectedPath; };
            Controls.Add(new Label { Text = "Format", Left = 20, Top = 64, Width = 100 }); _fmt.SetBounds(130, 60, 160, 24); _fmt.Items.AddRange(new object[] { "BMP", "JPG" }); _fmt.SelectedIndex = fmt == HwsImageFormat.JPG ? 1 : 0; Controls.Add(_fmt);
            var ok = new Button { Text = "OK", Left = 390, Top = 110, Width = 80, DialogResult = DialogResult.OK };
            var cancel = new Button { Text = "Cancel", Left = 490, Top = 110, Width = 80, DialogResult = DialogResult.Cancel };
            Controls.AddRange(new Control[] { ok, cancel }); AcceptButton = ok; CancelButton = cancel;
        }
    }

    internal static class FormatNameExtensions
    {
        public static string FormatName(this HwsPixelFormat fmt)
        {
            return fmt switch
            {
                HwsPixelFormat.YUY2 => "YUY2",
                HwsPixelFormat.NV12 => "NV12",
                HwsPixelFormat.RGB24 => "RGB24",
                HwsPixelFormat.RGB32 => "RGB32",
                HwsPixelFormat.MJPG => "MJPG",
                HwsPixelFormat.H264 => "H264",
                _ => fmt.ToString()
            };
        }

        public static string CapFormatName(this HwsVideoFormat cap) => cap.pixelFormat.FormatName();
    }

}
