#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <windowsx.h>
#include <stdio.h>
#include <stdlib.h>
#include <vector>
#include <string>
#include <algorithm>
#include <map>
#include <commdlg.h>
#include <shlobj.h>
#include <time.h>
#include "../include/hwscapture.h"
#pragma comment(lib, "Comdlg32.lib")
#pragma comment(lib, "Shell32.lib")
#pragma comment(lib, "Ole32.lib")

#define IDM_ADD_VIDEO_CAPTURE_DEVICE  1001
#define IDM_ADD_AUDIO_INPUT_DEVICE     1004
#define IDM_STOP_PREVIEW              1002
#define IDM_EXIT_APP                  1003
#define IDM_VIEW_1                    1101
#define IDM_VIEW_4                    1104
#define IDM_VIEW_8                    1108
#define IDM_VIEW_16                   1116
#define IDM_OSD_TEXT                   1201
#define IDM_OSD_IMAGE                  1202
#define IDM_OSD_CLEAR                  1203
#define IDM_OSD_REMOVE_LAST            1204
#define IDM_OSD_DATETIME               1205
#define TIMER_OSD_DATETIME             5001
#define IDM_REC_START                  1301
#define IDM_REC_STOP                   1302
#define IDM_REC_VIDEO_CB               1303
#define IDM_REC_AUDIO_CB               1304
#define IDM_REC_FPS_FOLLOW             1305
#define IDM_REC_FPS_CUSTOM             1306
#define IDM_SNAP_SETTINGS              1401
#define IDM_SNAP_CAPTURE               1402
#define IDM_CROP_ADD                   1501
#define IDM_CROP_REMOVE                1502
#define IDM_CTX_SELECT_VIDEO           1601
#define IDM_CTX_SELECT_AUDIO           1602
#define IDM_CTX_AUDIO_MONITOR_ON       1603
#define IDM_CTX_AUDIO_MONITOR_OFF      1604
#define IDM_CTX_REMOVE_AVIDEO          1605
#define IDM_DEMO_H264_DIRECT_RECORD     1701
#define IDM_DEMO_H264_DIRECT_RECORD_STOP 1702

#define IDC_DEVICE_COMBO              2001
#define IDC_RES_COMBO                 2002
#define IDC_FPS_COMBO                 2003
#define IDC_FMT_COMBO                 2004
#define IDC_CFG_VIDEO                 2005
#define IDC_CFG_CROSSBAR              2006
#define IDC_INFO_TEXT                 2007
#define IDC_OK_BUTTON                 2008
#define IDC_CANCEL_BUTTON             2009
#define IDC_OSD_TEXT_EDIT              3001
#define IDC_OSD_X_EDIT                 3002
#define IDC_OSD_Y_EDIT                 3003
#define IDC_OSD_W_EDIT                 3004
#define IDC_OSD_H_EDIT                 3005
#define IDC_OSD_OPACITY_EDIT           3006
#define IDC_OSD_FONT_EDIT              3007
#define IDC_OSD_TEXT_COLOR             3008
#define IDC_OSD_BG_COLOR               3009
#define IDC_OSD_TRANSPARENT            3010
#define IDC_OSD_FILE_EDIT              3011
#define IDC_OSD_BROWSE_FILE            3012
#define IDC_OSD_FONT_NAME              3013
#define IDC_SNAP_DIR_EDIT              4001
#define IDC_SNAP_BROWSE                4002
#define IDC_SNAP_FORMAT_COMBO          4003
#define IDC_CROP_X_EDIT                5001
#define IDC_CROP_Y_EDIT                5002
#define IDC_CROP_W_EDIT                5003
#define IDC_CROP_H_EDIT                5004
#define IDC_H264_DEVICE_COMBO          6001
#define IDC_H264_CAP_COMBO             6002
#define IDC_H264_OUTPUT_EDIT           6003
#define IDC_H264_BROWSE                6004
#define IDC_H264_SECONDS_EDIT          6005
#define IDC_H264_AUDIO_COMBO           6006
#define IDC_REC_FPS_EDIT               7001
#define WM_HWS_AUTOSTART              (WM_APP + 1)
#define WM_HWS_H264_RECORD_DONE       (WM_APP + 2)

struct CapItem { int width; int height; int fps; HWS_PIXEL_FORMAT fmt; };
struct DialogContext { HWS_VIDEO_INPUT_CONFIG config; bool accepted; bool forceDeviceDefault; std::vector<CapItem> caps; };
struct OsdDialogContext { HWS_OSD_ITEM item; bool accepted; bool imageMode; };
struct AudioDialogContext { int deviceIndex; bool accepted; };
struct SnapshotDialogContext { wchar_t directory[MAX_PATH]; HWS_IMAGE_FORMAT imageFormat; bool accepted; };
struct CropDialogContext { HWS_VIDEO_CROP_RECT rect; bool accepted; };
struct RecorderFpsDialogContext { int fps; bool accepted; };
struct H264DirectRecordDialogContext
{
    HWS_VIDEO_INPUT_CONFIG config;
    wchar_t outputPath[MAX_PATH];
    int seconds;
    int audioIndex;
    bool accepted;
    std::vector<CapItem> caps;
};

struct H264DirectRecordTask
{
    HWND notifyHwnd;
    HWS_VIDEO_INPUT_CONFIG config;
    wchar_t outputPath[MAX_PATH];
    int seconds;
    int audioIndex;
};

struct PreviewSlot
{
    int index;
    HWND hwnd;
    HWS_PORT videoIn;
    HWS_PORT render;
    HWS_PORT osd;
    HWS_PORT crop;
    HWS_PORT audioIn;
    HWS_PORT audioRender;
    bool audioMonitorEnabled;
    bool useOsd;
    bool useCrop;
    HWS_VIDEO_CROP_RECT cropRect;
    std::vector<HWS_OSD_ITEM> osdItems;
    HWS_VIDEO_INPUT_CONFIG config;
    char deviceName[256];
    char audioDeviceName[256];
    long long callbackCount;
    long long lastFrameIndex;
    unsigned long long fpsWindowTick;
    long long fpsWindowCount;
    double measuredInputFps;
    unsigned long long renderFpsWindowTick;
    long long renderFpsWindowCount;
    double measuredRenderFps;
    bool active;
    HWS_PORT recorder;
    bool recording;
    wchar_t recordPath[MAX_PATH];
    int dateTimeOsdItemId;
};

static HWND g_mainHwnd = NULL;
static int g_viewCount = 1;
static std::vector<PreviewSlot> g_slots;
static HWS_VIDEO_INPUT_CONFIG g_defaultConfig = {0, 1280, 720, 30, HWS_PIXFMT_RGB32, HWS_COLOR_SPACE_AUTO, HWS_COLOR_RANGE_AUTO, 4};
static bool g_autoStartFromCommandLine = false;
static int g_selectedSlot = 0;
static HWS_RENDER_INTERPOLATION_MODE g_renderInterpolationMode = HWS_RENDER_INTERPOLATION_AUTO;
static int g_fullscreenSlot = -1;
static RECT g_slotRects[16] = {};
static wchar_t g_snapshotDirectory[MAX_PATH] = L".";
static HWS_IMAGE_FORMAT g_snapshotFormat = HWS_IMAGE_FORMAT_BMP;
static bool g_enableRecorderVideoCallback = false;
static bool g_enableRecorderAudioCallback = false;
static bool g_recorderFpsFollowInput = true;
static int g_recorderCustomFps = 60;
static int g_nextDemoOsdItemId = 1000;
static HWS_PORT g_audioMonitorMux = NULL;
static HWS_PORT g_audioMonitorRender = NULL;
static int g_audioMonitorCount = 0;
static LONG g_h264DirectRecordRunning = 0;
static LONG g_h264DirectRecordStopRequested = 0;
static LONG g_h264DirectRecordStoppedByUser = 0;
static int g_h264DirectRecordResult = 0;
static std::wstring g_h264DirectRecordOutput;
// hwscapture_test keeps one real DirectShow source per video device.
// Later preview slots of the same device are SDK shared clients.
// When the owner slot is removed, all remaining clients for that device must be
// rebuilt so one of them becomes the new real source; otherwise they stay active
// but no longer receive frames after the old source graph is destroyed.
static std::map<int, int> g_videoSourceOwnerSlotByDevice;

static int AllocateDemoOsdItemId()
{
    return g_nextDemoOsdItemId++;
}

static int GetTargetSlotForAdd();
static bool ShowCropDialog(HWND parent, HWS_VIDEO_CROP_RECT* rect);
static bool ShowAudioInputDeviceDialog(HWND parent, int* deviceIndex);
static void AddVideoCropForSelectedSlot(HWND hwnd);
static void RemoveVideoCropForSelectedSlot();
static void RemoveVideoCropForSlot(size_t index);
static bool ShowVideoCaptureDeviceDialog(HWND parent, HWS_VIDEO_INPUT_CONFIG* config);
static bool StartPreviewSlot(size_t index, const HWS_VIDEO_INPUT_CONFIG& cfg);
static void StopPreviewSlotAndRepairPeers(size_t index);
static void AddVideoSourceToSlot(HWND hwnd, int slotIndex);
static void AddAudioInputToSelectedSlot(HWND hwnd);
static void EnableAudioMonitorForSelectedSlot(HWND hwnd);
static void DisableAudioMonitorForSelectedSlot();
static void ClearOsdForSlot(int slotIndex);
static bool ShowH264DirectRecordDialog(HWND parent, H264DirectRecordDialogContext* ctx);
static bool ShowRecorderFpsDialog(HWND parent, int* fps);
static void StartH264DirectRecordDemo(HWND hwnd);
static void StopH264DirectRecordDemo(HWND hwnd);

static const char* PixelFormatName(HWS_PIXEL_FORMAT fmt)
{
    switch (fmt) {
    case HWS_PIXFMT_YUY2:  return "YUY2";
    case HWS_PIXFMT_NV12:  return "NV12";
    case HWS_PIXFMT_RGB24: return "RGB24";
    case HWS_PIXFMT_RGB32: return "RGB32";
    case HWS_PIXFMT_MJPG:  return "MJPG";
    case HWS_PIXFMT_H264:  return "H264";
    default:              return "UNKNOWN";
    }
}


static bool IsDemoRenderablePixelFormat(HWS_PIXEL_FORMAT fmt)
{
    switch (fmt) {
    case HWS_PIXFMT_YUY2:
    case HWS_PIXFMT_NV12:
    case HWS_PIXFMT_RGB24:
    case HWS_PIXFMT_RGB32:
    case HWS_PIXFMT_MJPG:
        return true;
    default:
        return false;
    }
}

static void LayoutPreviewSlots(HWND hwnd)
{
    RECT rc;
    GetClientRect(hwnd, &rc);
    int width = rc.right - rc.left;
    int height = rc.bottom - rc.top;
    int gap = 4;
    int cols = 1;
    int rows = 1;

    for (int i = 0; i < 16; ++i) SetRectEmpty(&g_slotRects[i]);

    if (g_fullscreenSlot >= 0 && g_fullscreenSlot < (int)g_slots.size()) {
        for (size_t i = 0; i < g_slots.size(); ++i) {
            if ((int)i == g_fullscreenSlot) {
                RECT cell = { gap, gap, width - gap, height - gap };
                g_slotRects[i] = cell;
                ShowWindow(g_slots[i].hwnd, SW_SHOW);
                SetWindowPos(g_slots[i].hwnd, NULL, cell.left, cell.top, cell.right - cell.left, cell.bottom - cell.top, SWP_NOZORDER);
                if (!g_slots[i].active) InvalidateRect(g_slots[i].hwnd, NULL, FALSE);
            } else {
                ShowWindow(g_slots[i].hwnd, SW_HIDE);
            }
        }
        return;
    }

    if (g_viewCount == 1) { cols = 1; rows = 1; }
    else if (g_viewCount == 4) { cols = 2; rows = 2; }
    else if (g_viewCount == 8) { cols = 4; rows = 2; }
    else { cols = 4; rows = 4; }

    int cellW = (width - gap * (cols + 1)) / cols;
    int cellH = (height - gap * (rows + 1)) / rows;
    if (cellW < 1) cellW = 1;
    if (cellH < 1) cellH = 1;


    for (size_t i = 0; i < g_slots.size(); ++i) {
        if ((int)i < g_viewCount) {
            int r = (int)i / cols;
            int c = (int)i % cols;
            int x = gap + c * (cellW + gap);
            int y = gap + r * (cellH + gap);
            RECT cell = { x, y, x + cellW, y + cellH };
            g_slotRects[i] = cell;

            int inset = ((int)i == g_selectedSlot) ? 3 : 0;
            int childX = x + inset;
            int childY = y + inset;
            int childW = cellW - inset * 2;
            int childH = cellH - inset * 2;
            if (childW < 1) childW = 1;
            if (childH < 1) childH = 1;

            ShowWindow(g_slots[i].hwnd, SW_SHOW);
            SetWindowPos(g_slots[i].hwnd, NULL, childX, childY, childW, childH, SWP_NOZORDER);
            if (!g_slots[i].active) InvalidateRect(g_slots[i].hwnd, NULL, FALSE);
        } else {
            ShowWindow(g_slots[i].hwnd, SW_HIDE);
        }
    }
}

static void PrintVideoDevices()
{
    int n = HWS_GetVideoDeviceCount();
    printf("Video devices: %d\n", n);
    for (int i = 0; i < n; ++i) {
        HWS_DEVICE_INFO info = {};
        if (HWS_GetVideoDeviceInfo(i, &info) == HWS_OK) printf("[%d] %s\n", i, info.name);
    }
}

static void UpdateMainWindowTitle()
{
    int activeCount = 0;
    int recordingCount = 0;
    double inputFps = 0.0;
    double renderFps = 0.0;
    if (g_selectedSlot >= 0 && g_selectedSlot < (int)g_slots.size()) {
        inputFps = g_slots[g_selectedSlot].measuredInputFps;
        renderFps = g_slots[g_selectedSlot].measuredRenderFps;
    }
    for (size_t i = 0; i < g_slots.size(); ++i) {
        if (g_slots[i].active) ++activeCount;
        if (g_slots[i].recording) ++recordingCount;
    }
    char title[512];
    if (g_fullscreenSlot >= 0)
        sprintf_s(title, "hwscapture_test - view=%d active=%d recording=%d selected=%d capture=%.1ffps display=%.1ffps fullscreen=%d", g_viewCount, activeCount, recordingCount, g_selectedSlot + 1, inputFps, renderFps, g_fullscreenSlot + 1);
    else
        sprintf_s(title, "hwscapture_test - view=%d active=%d recording=%d selected=%d capture=%.1ffps display=%.1ffps", g_viewCount, activeCount, recordingCount, g_selectedSlot + 1, inputFps, renderFps);
    if (g_mainHwnd) SetWindowTextA(g_mainHwnd, title);
}


static int GetSlotIndexFromHwnd(HWND hwnd)
{
    for (size_t i = 0; i < g_slots.size(); ++i) {
        if (g_slots[i].hwnd == hwnd) return (int)i;
    }
    return -1;
}

static void SelectPreviewSlot(int index)
{
    if (index < 0 || index >= (int)g_slots.size()) return;
    if (g_fullscreenSlot < 0 && index >= g_viewCount) return;
    g_selectedSlot = index;
    if (g_mainHwnd) {
        LayoutPreviewSlots(g_mainHwnd);
        InvalidateRect(g_mainHwnd, NULL, TRUE);
        UpdateMainWindowTitle();
    }
}

static void ToggleFullscreenSlot(int index)
{
    if (index < 0 || index >= (int)g_slots.size()) return;
    if (g_fullscreenSlot == index) g_fullscreenSlot = -1;
    else g_fullscreenSlot = index;
    g_selectedSlot = index;
    if (g_mainHwnd) {
        LayoutPreviewSlots(g_mainHwnd);
        InvalidateRect(g_mainHwnd, NULL, TRUE);
        UpdateMainWindowTitle();
    }
}

static void __stdcall OnVideoFrame(const HWS_VIDEO_FRAME* frame, void* userData)
{
    PreviewSlot* slot = (PreviewSlot*)userData;
    if (!frame || !slot) return;
    slot->lastFrameIndex = frame->frameIndex;
    ++slot->callbackCount;

    unsigned long long now = GetTickCount64();
    if (slot->fpsWindowTick == 0) {
        slot->fpsWindowTick = now;
        slot->fpsWindowCount = 0;
    }
    ++slot->fpsWindowCount;
    unsigned long long elapsed = now - slot->fpsWindowTick;
    if (elapsed >= 1000) {
        slot->measuredInputFps = (double)slot->fpsWindowCount * 1000.0 / (double)elapsed;
        slot->fpsWindowTick = now;
        slot->fpsWindowCount = 0;
        UpdateMainWindowTitle();
    }
}

static void __stdcall OnRenderVideoFrame(const HWS_VIDEO_FRAME* frame, void* userData)
{
    PreviewSlot* slot = (PreviewSlot*)userData;
    if (!frame || !slot) return;

    unsigned long long now = GetTickCount64();
    if (slot->renderFpsWindowTick == 0) {
        slot->renderFpsWindowTick = now;
        slot->renderFpsWindowCount = 0;
    }
    ++slot->renderFpsWindowCount;
    unsigned long long elapsed = now - slot->renderFpsWindowTick;
    if (elapsed >= 1000) {
        slot->measuredRenderFps = (double)slot->renderFpsWindowCount * 1000.0 / (double)elapsed;
        // Only print measured FPS here.
        // Do not print frameIndex as the main value, because frameIndex is only
        // an accumulated frame counter and can be confused with real FPS.
        printf("FPS slot=%d capture_fps=%.1f display_fps=%.1f size=%dx%d fmt=%s stride=%d\n",
            slot->index,
            slot->measuredInputFps,
            slot->measuredRenderFps,
            frame->width,
            frame->height,
            PixelFormatName(frame->pixelFormat),
            frame->stride[0]);
        slot->renderFpsWindowTick = now;
        slot->renderFpsWindowCount = 0;
        UpdateMainWindowTitle();
    }
}


static bool SelectRecordFile(HWND parent, wchar_t* outPath, DWORD outChars)
{
    if (!outPath || outChars == 0) return false;
    outPath[0] = 0;
    OPENFILENAMEW ofn = {};
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = parent;
    ofn.lpstrFilter = L"MP4 Files (*.mp4)\0*.mp4\0MOV Files (*.mov)\0*.mov\0All Files (*.*)\0*.*\0";
    ofn.lpstrFile = outPath;
    ofn.nMaxFile = outChars;
    ofn.lpstrDefExt = L"mp4";
    ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
    return GetSaveFileNameW(&ofn) ? true : false;
}


static void AddVideoSourceToSlot(HWND hwnd, int slotIndex)
{
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) return;
    if (g_fullscreenSlot < 0 && slotIndex >= g_viewCount) return;
    SelectPreviewSlot(slotIndex);
    HWS_VIDEO_INPUT_CONFIG cfg = g_defaultConfig;
    if (g_slots[slotIndex].active) {
        cfg = g_slots[slotIndex].config;
    } else {
        // For a new source, let the dialog select this camera's first/default
        // capability instead of forcing the demo's cached 1280x720/YUY2 value.
        cfg.width = 0;
        cfg.height = 0;
        cfg.fps = 0;
        cfg.pixelFormat = (HWS_PIXEL_FORMAT)0;
    }
    if (ShowVideoCaptureDeviceDialog(hwnd, &cfg)) {
        g_defaultConfig = cfg;
        StartPreviewSlot((size_t)slotIndex, cfg);
    }
}

static void AddAudioInputToSelectedSlot(HWND hwnd)
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[g_selectedSlot];
    int dev = 0;
    if (!ShowAudioInputDeviceDialog(hwnd, &dev)) return;

    if (s.audioIn) {
        HWS_Stop(s.audioIn);
        HWS_DestroyPort(s.audioIn);
        s.audioIn = NULL;
        s.audioDeviceName[0] = 0;
    }

    s.audioIn = HWS_CreateAudioInput();
    if (!s.audioIn) {
        MessageBoxA(hwnd, "Failed to create audio input port.", "hwscapture", MB_ICONERROR | MB_OK);
        return;
    }
    HWS_SetAudioInputDevice(s.audioIn, dev);
    HWS_SetAudioInputPCM(s.audioIn, 48000, 2, 16);
    HWS_DEVICE_INFO info = {};
    if (HWS_GetAudioInputDeviceInfo(dev, &info) == HWS_OK) strncpy_s(s.audioDeviceName, info.name, _TRUNCATE);
    if (HWS_Start(s.audioIn) != HWS_OK) {
        HWS_DestroyPort(s.audioIn);
        s.audioIn = NULL;
        MessageBoxA(hwnd, "Failed to start audio input.", "hwscapture", MB_ICONERROR | MB_OK);
        return;
    }
    printf("Audio input added slot=%d device=%d %s\n", s.index, dev, s.audioDeviceName);
    UpdateMainWindowTitle();
}

static bool EnsureGlobalAudioMonitor()
{
    if (!g_audioMonitorMux) {
        g_audioMonitorMux = HWS_CreateAudioMux();
    }
    if (!g_audioMonitorRender) {
        g_audioMonitorRender = HWS_CreateAudioRender();
        if (g_audioMonitorRender) {
            HWS_SetAudioRenderDefaultDevice(g_audioMonitorRender);
            HWS_Start(g_audioMonitorRender);
        }
    }
    if (g_audioMonitorMux && g_audioMonitorRender) {
        HWS_Connect(g_audioMonitorMux, g_audioMonitorRender);
        return true;
    }
    return false;
}

static void ReleaseGlobalAudioMonitorIfUnused()
{
    if (g_audioMonitorCount > 0) return;
    if (g_audioMonitorMux && g_audioMonitorRender) {
        HWS_Disconnect(g_audioMonitorMux, g_audioMonitorRender);
    }
    if (g_audioMonitorRender) {
        HWS_Stop(g_audioMonitorRender);
        HWS_DestroyPort(g_audioMonitorRender);
        g_audioMonitorRender = NULL;
    }
    if (g_audioMonitorMux) {
        HWS_DestroyPort(g_audioMonitorMux);
        g_audioMonitorMux = NULL;
    }
}

static void EnableAudioMonitorForSelectedSlot(HWND hwnd)
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[g_selectedSlot];
    if (!s.audioIn) {
        AddAudioInputToSelectedSlot(hwnd);
    }
    if (!s.audioIn) {
        MessageBoxA(hwnd, "Please select an audio input device first.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    if (s.audioMonitorEnabled) return;
    if (!EnsureGlobalAudioMonitor()) {
        MessageBoxA(hwnd, "Failed to create audio monitor renderer.", "hwscapture", MB_ICONERROR | MB_OK);
        return;
    }
    HWS_Connect(s.audioIn, g_audioMonitorMux);
    s.audioMonitorEnabled = true;
    ++g_audioMonitorCount;
    printf("Audio monitor enabled slot=%d source=%s\n", s.index, s.audioDeviceName);
    UpdateMainWindowTitle();
}

static void DisableAudioMonitorForSlot(size_t index)
{
    if (index >= g_slots.size()) return;
    PreviewSlot& s = g_slots[index];
    if (!s.audioMonitorEnabled) return;
    if (s.audioIn && g_audioMonitorMux) {
        HWS_Disconnect(s.audioIn, g_audioMonitorMux);
    }
    s.audioMonitorEnabled = false;
    if (g_audioMonitorCount > 0) --g_audioMonitorCount;
    printf("Audio monitor disabled slot=%d\n", s.index);
    ReleaseGlobalAudioMonitorIfUnused();
    UpdateMainWindowTitle();
}

static void DisableAudioMonitorForSelectedSlot()
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    DisableAudioMonitorForSlot((size_t)g_selectedSlot);
}


static void __stdcall RecorderVideoEncodedCallback(const HWS_ENCODED_VIDEO_PACKET* pkt, void* userData)
{
    int slot = (int)(intptr_t)userData;
    if (!pkt) return;
    if ((pkt->frameIndex % 60) == 0 || pkt->keyFrame) {
        printf("Recorder video callback slot=%d frame=%lld size=%d key=%d pts=%lld %dx%d\n",
            slot, pkt->frameIndex, pkt->size, pkt->keyFrame, pkt->pts100ns, pkt->width, pkt->height);
    }
}

static void __stdcall RecorderAudioEncodedCallback(const HWS_ENCODED_AUDIO_PACKET* pkt, void* userData)
{
    int slot = (int)(intptr_t)userData;
    if (!pkt) return;
    static long long count = 0;
    ++count;
    if ((count % 100) == 0) {
        printf("Recorder audio callback slot=%d count=%lld size=%d pts=%lld rate=%d ch=%d\n",
            slot, count, pkt->size, pkt->pts100ns, pkt->sampleRate, pkt->channels);
    }
}

static void StopRecorderForSlot(size_t index)
{
    if (index >= g_slots.size()) return;
    PreviewSlot& s = g_slots[index];
    if (s.recorder) {
        // Disconnect first so no new frames are delivered to the recorder while
        // Media Foundation is finalizing the file. The OSD port waits for any
        // in-flight OSD dispatch before returning from HWS_Disconnect.
        if (s.osd) HWS_Disconnect(s.osd, s.recorder);
        if (s.crop) HWS_Disconnect(s.crop, s.recorder);
        if (s.videoIn) HWS_Disconnect(s.videoIn, s.recorder);
        if (s.audioIn) HWS_Disconnect(s.audioIn, s.recorder);
        Sleep(30);
        HWS_Stop(s.recorder);
        HWS_DestroyPort(s.recorder);
        s.recorder = NULL;
    }
    s.recording = false;
    s.recordPath[0] = 0;
    UpdateMainWindowTitle();
}

static void StartRecorderForSelectedSlot(HWND hwnd)
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[g_selectedSlot];
    if (!s.active || !s.videoIn || !s.render) {
        MessageBoxA(hwnd, "Please add a video capture device to the selected slot first.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    if (s.recording) return;

    wchar_t filePath[MAX_PATH] = {};
    if (!SelectRecordFile(hwnd, filePath, MAX_PATH)) return;

    s.recorder = HWS_CreateRecorder(NULL);
    if (!s.recorder) {
        MessageBoxA(hwnd, "Failed to create recorder port.", "hwscapture", MB_ICONERROR | MB_OK);
        return;
    }

    HWS_RECORDER_CONFIG cfg = {};
    wcscpy_s(cfg.filePath, filePath);
    cfg.container = HWS_CONTAINER_MP4;
    cfg.videoCodec = HWS_VIDEO_CODEC_H264;
    cfg.width = (s.useCrop && s.cropRect.width > 0) ? s.cropRect.width : s.config.width;
    cfg.height = (s.useCrop && s.cropRect.height > 0) ? s.cropRect.height : s.config.height;
    cfg.fps = s.config.fps > 0 ? s.config.fps : 30;
    cfg.recordFps = g_recorderFpsFollowInput ? 0 : (g_recorderCustomFps > 0 ? g_recorderCustomFps : 60);
    cfg.videoBitrate = 8000000;
    cfg.enableAudio = s.audioIn ? 1 : 0;

    // 30һļ
    cfg.splitMode = HWS_RECORDER_SPLIT_BY_TIME;
    cfg.segmentSeconds = 30 * 60;  //30 * 60

    //  1GB һļ
    // cfg.splitMode = HWS_RECORDER_SPLIT_BY_SIZE;
    // cfg.maxSegmentBytes = 1024ULL * 1024ULL * 1024ULL;

    HWS_SetRecorderConfig(s.recorder, &cfg);
    if (g_enableRecorderVideoCallback) {
        HWS_SetRecorderVideoCallback(s.recorder, RecorderVideoEncodedCallback, (void*)(intptr_t)s.index);
    }
    if (g_enableRecorderAudioCallback) {
        HWS_SetRecorderAudioCallback(s.recorder, RecorderAudioEncodedCallback, (void*)(intptr_t)s.index);
    }

    if (s.useOsd && s.osd) {
        HWS_ConnectVideoOsdOutput(s.osd, HWS_OSD_OUTPUT_OSD, s.recorder);
    } else if (s.useCrop && s.crop) {
        HWS_Connect(s.crop, s.recorder);
    } else {
        HWS_Connect(s.videoIn, s.recorder);
    }
    if (s.audioIn) HWS_Connect(s.audioIn, s.recorder);

    HWS_RESULT r = HWS_Start(s.recorder);
    if (r != HWS_OK) {
        if (s.osd) HWS_Disconnect(s.osd, s.recorder);
        if (s.crop) HWS_Disconnect(s.crop, s.recorder);
        if (s.videoIn) HWS_Disconnect(s.videoIn, s.recorder);
        if (s.audioIn) HWS_Disconnect(s.audioIn, s.recorder);
        HWS_DestroyPort(s.recorder);
        s.recorder = NULL;
        MessageBoxA(hwnd, "Failed to start recorder.", "hwscapture", MB_ICONERROR | MB_OK);
        return;
    }

    s.recording = true;
    wcscpy_s(s.recordPath, filePath);
    printf("Recorder start slot=%d file=%S fps_mode=%s input_fps=%d record_fps=%d\n",
        s.index, filePath, g_recorderFpsFollowInput ? "follow-input" : "custom",
        cfg.fps, cfg.recordFps > 0 ? cfg.recordFps : cfg.fps);
    UpdateMainWindowTitle();
}

static void StopRecorderForSelectedSlot()
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    StopRecorderForSlot((size_t)g_selectedSlot);
}


static int FindVideoSourceOwnerSlot(int deviceIndex)
{
    auto it = g_videoSourceOwnerSlotByDevice.find(deviceIndex);
    if (it == g_videoSourceOwnerSlotByDevice.end()) return -1;
    const int slotIndex = it->second;
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) return -1;
    const PreviewSlot& s = g_slots[(size_t)slotIndex];
    if (!s.active || s.config.deviceIndex != deviceIndex) return -1;
    return slotIndex;
}

static void RebuildVideoSourceOwnerMap()
{
    g_videoSourceOwnerSlotByDevice.clear();
    for (size_t i = 0; i < g_slots.size(); ++i) {
        const PreviewSlot& s = g_slots[i];
        if (!s.active) continue;
        if (g_videoSourceOwnerSlotByDevice.find(s.config.deviceIndex) == g_videoSourceOwnerSlotByDevice.end()) {
            g_videoSourceOwnerSlotByDevice[s.config.deviceIndex] = (int)i;
        }
    }
}

struct PeerRestartInfo {
    size_t index;
    HWS_VIDEO_INPUT_CONFIG config;
};

static std::vector<PeerRestartInfo> CollectSharedPeerRestartInfo(size_t sourceIndex)
{
    std::vector<PeerRestartInfo> peers;
    if (sourceIndex >= g_slots.size()) return peers;

    const PreviewSlot& source = g_slots[sourceIndex];
    if (!source.active) return peers;

    const int deviceIndex = source.config.deviceIndex;
    const int ownerSlot = FindVideoSourceOwnerSlot(deviceIndex);
    if (ownerSlot != (int)sourceIndex) return peers;

    for (size_t i = 0; i < g_slots.size(); ++i) {
        if (i == sourceIndex) continue;
        const PreviewSlot& peer = g_slots[i];
        if (!peer.active) continue;
        if (peer.config.deviceIndex != deviceIndex) continue;

        PeerRestartInfo info = {};
        info.index = i;
        info.config = peer.config;
        peers.push_back(info);
    }
    return peers;
}

static void StopPreviewSlot(size_t index, bool /*rebuildSharedPeers*/)
{
    if (index >= g_slots.size()) return;
    PreviewSlot& s = g_slots[index];

    const bool wasActive = s.active;
    const int oldDeviceIndex = wasActive ? s.config.deviceIndex : -1;
    const int oldOwnerSlot = (oldDeviceIndex >= 0) ? FindVideoSourceOwnerSlot(oldDeviceIndex) : -1;

    StopRecorderForSlot(index);
    DisableAudioMonitorForSlot(index);

    if (s.audioIn) {
        HWS_SetAudioFrameCallback(s.audioIn, NULL, NULL);
        HWS_Stop(s.audioIn);
        HWS_DestroyPort(s.audioIn);
        s.audioIn = NULL;
        s.audioDeviceName[0] = 0;
    }

    // Stop and destroy the video input before destroying the renderer.
    // The capture callback may still dispatch frames during graph shutdown,
    // so the downstream render port must remain valid until the input port is gone.
    if (s.videoIn) {
        HWS_SetVideoFrameCallback(s.videoIn, NULL, NULL);
        if (s.render) HWS_SetVideoFrameCallback(s.render, NULL, NULL);
        HWS_Stop(s.videoIn);
        HWS_DestroyPort(s.videoIn);
        s.videoIn = NULL;
    }

    if (s.osd) {
        ClearOsdForSlot(index);
        HWS_DestroyPort(s.osd);
        s.osd = NULL;
    }

    if (s.crop) {
        RemoveVideoCropForSlot(index);
        HWS_DestroyPort(s.crop);
        s.crop = NULL;
    }

    if (s.render) {
        HWS_DestroyPort(s.render);
        s.render = NULL;
    }

    s.callbackCount = 0;
    s.lastFrameIndex = 0;
    s.fpsWindowTick = 0;
    s.fpsWindowCount = 0;
    s.measuredInputFps = 0.0;
    s.renderFpsWindowTick = 0;
    s.renderFpsWindowCount = 0;
    s.measuredRenderFps = 0.0;
    s.active = false;
    s.recording = false;
    s.recordPath[0] = 0;
    s.deviceName[0] = 0;

    if (oldDeviceIndex >= 0 && oldOwnerSlot == (int)index) {
        g_videoSourceOwnerSlotByDevice.erase(oldDeviceIndex);
    }

    if (s.hwnd) InvalidateRect(s.hwnd, NULL, TRUE);
    RebuildVideoSourceOwnerMap();
    UpdateMainWindowTitle();
}

static bool StartPreviewSlotInternal(size_t index, const HWS_VIDEO_INPUT_CONFIG& cfg, bool repairPeers)
{
    if (index >= g_slots.size()) return false;

    std::vector<PeerRestartInfo> peersToRestart;
    if (repairPeers) {
        peersToRestart = CollectSharedPeerRestartInfo(index);
    }

    PreviewSlot& s = g_slots[index];
    bool keepUseOsd = s.useOsd;
    bool keepUseCrop = s.useCrop;
    HWS_VIDEO_CROP_RECT keepCropRect = s.cropRect;
    std::vector<HWS_OSD_ITEM> keepOsdItems = s.osdItems;

    // If this slot owns the real capture graph, stop every shared client of the
    // same device before stopping the owner. Starting a new owner while old
    // shared clients are still attached can bind a client to the stale source
    // just before the SDK unregisters it, leaving that window frozen.
    for (size_t i = 0; i < peersToRestart.size(); ++i) {
        StopPreviewSlot(peersToRestart[i].index, false);
    }

    StopPreviewSlot(index, false);
    s.useOsd = keepUseOsd;
    s.useCrop = keepUseCrop;
    s.cropRect = keepCropRect;
    s.osdItems = keepOsdItems;

    HWS_DEVICE_INFO info = {};
    if (HWS_GetVideoDeviceInfo(cfg.deviceIndex, &info) != HWS_OK) {
        MessageBoxA(g_mainHwnd, "Invalid video capture device index.", "hwscapture", MB_ICONERROR | MB_OK);
        for (size_t i = 0; i < peersToRestart.size(); ++i) {
            StartPreviewSlotInternal(peersToRestart[i].index, peersToRestart[i].config, false);
        }
        return false;
    }

    s.config = cfg;
    strcpy_s(s.deviceName, info.name);
    printf("Start slot=%d device=%d name=%s %dx%d fps=%d fmt=%s\n", (int)index, cfg.deviceIndex, s.deviceName, cfg.width, cfg.height, cfg.fps, PixelFormatName(cfg.pixelFormat));

    s.videoIn = HWS_CreateVideoInput();
    s.render = HWS_CreateVideoRender(s.hwnd);
    if (s.useCrop) s.crop = HWS_CreateVideoCrop();

    // Always keep an OSD port in the live preview chain.
    // This allows adding/removing OSD items without rebuilding the VideoInput graph.
    // It is important for multi-preview of the same capture device: rebuilding the
    // primary source graph can stop other shared preview clients.
    s.osd = HWS_CreateVideoOsd();

    if (!s.videoIn || !s.render || (s.useCrop && !s.crop) || !s.osd) {
        StopPreviewSlot(index, false);
        for (size_t i = 0; i < peersToRestart.size(); ++i) {
            StartPreviewSlotInternal(peersToRestart[i].index, peersToRestart[i].config, false);
        }
        MessageBoxA(g_mainHwnd, "Failed to create video input, OSD or video renderer.", "hwscapture", MB_ICONERROR | MB_OK);
        return false;
    }

    HWS_SetVideoInputConfig(s.videoIn, &s.config);
    HWS_VIDEO_RENDER_CONFIG renderCfg = {};
    renderCfg.width = (s.useCrop && s.cropRect.width > 0) ? s.cropRect.width : s.config.width;
    renderCfg.height = (s.useCrop && s.cropRect.height > 0) ? s.cropRect.height : s.config.height;
    renderCfg.pixelFormat = s.config.pixelFormat;
    HWS_SetVideoRenderConfig(s.render, &renderCfg);
    HWS_SetVideoRenderInterpolationMode(s.render, g_renderInterpolationMode);
    HWS_SetVideoFrameCallback(s.videoIn, OnVideoFrame, &s);
    HWS_SetVideoFrameCallback(s.render, OnRenderVideoFrame, &s);
    HWS_PORT upstream = s.videoIn;
    if (s.useCrop && s.crop) {
        HWS_SetVideoCropRect(s.crop, &s.cropRect);
        HWS_EnableVideoCrop(s.crop, 1);
        HWS_Connect(upstream, s.crop);
        upstream = s.crop;
    }

    // The preview path always passes through OSD. When there are no OSD items,
    // the OSD port works as a transparent pass-through. Later Add OSD only calls
    // HWS_AddVideoOsdText/Image and does not restart the capture graph.
    if (s.osd) {
        for (size_t osdIndex = 0; osdIndex < s.osdItems.size(); ++osdIndex) {
            HWS_OSD_ITEM& osdItem = s.osdItems[osdIndex];
            if (osdItem.type == HWS_OSD_ITEM_IMAGE || osdItem.filePath[0]) HWS_AddVideoOsdImage(s.osd, &osdItem);
            else HWS_AddVideoOsdText(s.osd, &osdItem);
        }
        HWS_Connect(upstream, s.osd);
        HWS_ConnectVideoOsdOutput(s.osd, HWS_OSD_OUTPUT_OSD, s.render);
    } else {
        HWS_Connect(upstream, s.render);
    }

    HWS_RESULT r = HWS_Start(s.videoIn);
    printf("HWS_Start slot=%d result=%d\n", (int)index, (int)r);
    if (r != HWS_OK) {
        StopPreviewSlot(index, false);
        for (size_t i = 0; i < peersToRestart.size(); ++i) {
            StartPreviewSlotInternal(peersToRestart[i].index, peersToRestart[i].config, false);
        }
        MessageBoxA(g_mainHwnd, "Failed to start video preview.", "hwscapture", MB_ICONERROR | MB_OK);
        return false;
    }

    s.active = true;
    if (FindVideoSourceOwnerSlot(s.config.deviceIndex) < 0) {
        g_videoSourceOwnerSlotByDevice[s.config.deviceIndex] = (int)index;
    }

    // Restart peers only after the new owner is fully active. The first live
    // port for the device is now stable, so all peers attach to the new source.
    for (size_t i = 0; i < peersToRestart.size(); ++i) {
        StartPreviewSlotInternal(peersToRestart[i].index, peersToRestart[i].config, false);
    }

    RebuildVideoSourceOwnerMap();
    UpdateMainWindowTitle();
    return true;
}

static bool StartPreviewSlot(size_t index, const HWS_VIDEO_INPUT_CONFIG& cfg)
{
    return StartPreviewSlotInternal(index, cfg, true);
}

static void StopPreviewSlotAndRepairPeers(size_t index)
{
    std::vector<PeerRestartInfo> peersToRestart = CollectSharedPeerRestartInfo(index);

    // Stop all shared clients before stopping the owner. This guarantees that no
    // live window can attach to the old source while it is being unregistered.
    for (size_t i = 0; i < peersToRestart.size(); ++i) {
        StopPreviewSlot(peersToRestart[i].index, false);
    }

    StopPreviewSlot(index, false);

    // The first restarted peer becomes the new independent source. Remaining
    // peers attach to that new source, so no window is left bound to a stale graph.
    for (size_t i = 0; i < peersToRestart.size(); ++i) {
        StartPreviewSlotInternal(peersToRestart[i].index, peersToRestart[i].config, false);
    }

    RebuildVideoSourceOwnerMap();
    UpdateMainWindowTitle();
}

static void StopAllPreviewSlots()
{
    // Stop in reverse order. In common multi-preview testing, the first slot is
    // the real capture source and later slots are shared clients. Stopping
    // clients first prevents stale downstream pointers during source shutdown.
    for (size_t i = g_slots.size(); i > 0; --i) {
        StopPreviewSlot(i - 1, false);
    }
    g_videoSourceOwnerSlotByDevice.clear();
    UpdateMainWindowTitle();
}


static std::wstring Utf8ToWide(const char* text)
{
    if (!text || !text[0]) return std::wstring();
    int len = MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0);
    if (len <= 0) return std::wstring();
    std::wstring out((size_t)len, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, text, -1, &out[0], len);
    if (!out.empty() && out.back() == L'\0') out.pop_back();
    return out;
}

static void CenterWindowToParent(HWND hwnd, HWND parent)
{
    RECT rc = {}, prc = {};
    GetWindowRect(hwnd, &rc);
    if (parent) GetWindowRect(parent, &prc); else SystemParametersInfoA(SPI_GETWORKAREA, 0, &prc, 0);
    int w = rc.right - rc.left, h = rc.bottom - rc.top;
    int x = prc.left + ((prc.right - prc.left) - w) / 2;
    int y = prc.top + ((prc.bottom - prc.top) - h) / 2;
    SetWindowPos(hwnd, NULL, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}

static void AddComboItem(HWND combo, const char* text, LPARAM data)
{
    int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)text);
    SendMessageA(combo, CB_SETITEMDATA, pos, data);
}

static int GetComboData(HWND hwnd, int id)
{
    HWND combo = GetDlgItem(hwnd, id);
    int sel = (int)SendMessageA(combo, CB_GETCURSEL, 0, 0);
    if (sel == CB_ERR) return -1;
    return (int)SendMessageA(combo, CB_GETITEMDATA, sel, 0);
}


static int H264CapFps(const HWS_VIDEO_CAP& cap)
{
    if (cap.fpsNum <= 0) return 30;
    if (cap.fpsDen > 1) {
        int fps = (cap.fpsNum + cap.fpsDen / 2) / cap.fpsDen;
        return fps > 0 ? fps : 30;
    }
    return cap.fpsNum;
}

static void LoadH264CapsForDevice(std::vector<CapItem>& caps, int deviceIndex)
{
    caps.clear();
    int count = HWS_GetVideoCapabilityCount(deviceIndex);
    for (int i = 0; i < count; ++i) {
        HWS_VIDEO_CAP cap = {};
        if (HWS_GetVideoCapability(deviceIndex, i, &cap) != HWS_OK) continue;
        if (cap.pixelFormat != HWS_PIXFMT_H264) continue;

        CapItem item = { cap.width, cap.height, H264CapFps(cap), HWS_PIXFMT_H264 };
        bool exists = false;
        for (size_t k = 0; k < caps.size(); ++k) {
            if (caps[k].width == item.width && caps[k].height == item.height && caps[k].fps == item.fps && caps[k].fmt == item.fmt) {
                exists = true;
                break;
            }
        }
        if (!exists) caps.push_back(item);
    }
}

static bool DeviceHasH264Caps(int deviceIndex)
{
    std::vector<CapItem> caps;
    LoadH264CapsForDevice(caps, deviceIndex);
    return !caps.empty();
}

static void BuildDefaultH264OutputPath(wchar_t* path, DWORD chars)
{
    if (!path || chars == 0) return;
    wchar_t cwd[MAX_PATH] = {};
    GetCurrentDirectoryW(MAX_PATH, cwd);
    SYSTEMTIME st = {};
    GetLocalTime(&st);
    swprintf_s(path, chars, L"%s\\h264_direct_%04d%02d%02d_%02d%02d%02d.mp4",
        cwd[0] ? cwd : L".",
        st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}

static void FillH264DeviceCombo(HWND hwnd, H264DirectRecordDialogContext* ctx)
{
    HWND combo = GetDlgItem(hwnd, IDC_H264_DEVICE_COMBO);
    SendMessageA(combo, CB_RESETCONTENT, 0, 0);

    HWS_RefreshDeviceList();
    int count = HWS_GetVideoDeviceCount();
    int selectPos = -1;
    for (int i = 0; i < count; ++i) {
        if (!DeviceHasH264Caps(i)) continue;
        HWS_DEVICE_INFO info = {};
        if (HWS_GetVideoDeviceInfo(i, &info) != HWS_OK) continue;
        char line[384];
        sprintf_s(line, "%d: %s", i, info.name);
        int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)line);
        SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)i);
        if (ctx && ctx->config.deviceIndex == i) selectPos = pos;
    }
    if (SendMessageA(combo, CB_GETCOUNT, 0, 0) > 0) {
        if (selectPos < 0) selectPos = 0;
        SendMessageA(combo, CB_SETCURSEL, selectPos, 0);
    }
}

static void FillH264CapCombo(HWND hwnd, H264DirectRecordDialogContext* ctx)
{
    HWND combo = GetDlgItem(hwnd, IDC_H264_CAP_COMBO);
    SendMessageA(combo, CB_RESETCONTENT, 0, 0);
    if (!ctx) return;

    int dev = GetComboData(hwnd, IDC_H264_DEVICE_COMBO);
    if (dev < 0) return;
    ctx->config.deviceIndex = dev;
    LoadH264CapsForDevice(ctx->caps, dev);

    int selectPos = 0;
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        const CapItem& c = ctx->caps[i];
        char line[128];
        sprintf_s(line, "%d x %d @ %d fps  H264", c.width, c.height, c.fps);
        int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)line);
        SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)i);
        if (ctx->config.width == c.width && ctx->config.height == c.height && ctx->config.fps == c.fps) {
            selectPos = pos;
        }
    }
    if (!ctx->caps.empty()) {
        SendMessageA(combo, CB_SETCURSEL, selectPos, 0);
    }
}

static void FillH264AudioCombo(HWND hwnd, int selectedAudio)
{
    HWND combo = GetDlgItem(hwnd, IDC_H264_AUDIO_COMBO);
    SendMessageW(combo, CB_RESETCONTENT, 0, 0);

    int pos = (int)SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"No audio");
    SendMessageW(combo, CB_SETITEMDATA, pos, (LPARAM)-1);
    int selectPos = 0;

    int count = HWS_GetAudioInputDeviceCount();
    for (int i = 0; i < count; ++i) {
        HWS_DEVICE_INFO info = {};
        if (HWS_GetAudioInputDeviceInfo(i, &info) != HWS_OK) continue;
        std::wstring name = Utf8ToWide(info.name);
        wchar_t line[512] = {};
        swprintf_s(line, L"%d: %s", i, name.empty() ? L"Unknown Audio Device" : name.c_str());
        pos = (int)SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)line);
        SendMessageW(combo, CB_SETITEMDATA, pos, (LPARAM)i);
        if (i == selectedAudio) selectPos = pos;
    }
    SendMessageW(combo, CB_SETCURSEL, selectPos, 0);
}

static void UpdateH264DialogInfo(HWND hwnd, H264DirectRecordDialogContext* ctx)
{
    if (!ctx) return;
    int capIndex = GetComboData(hwnd, IDC_H264_CAP_COMBO);
    if (capIndex >= 0 && capIndex < (int)ctx->caps.size()) {
        const CapItem& c = ctx->caps[(size_t)capIndex];
        ctx->config.width = c.width;
        ctx->config.height = c.height;
        ctx->config.fps = c.fps;
        ctx->config.pixelFormat = HWS_PIXFMT_H264;
    }
}

static LRESULT CALLBACK H264DirectRecordDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    H264DirectRecordDialogContext* ctx = (H264DirectRecordDialogContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
    switch (msg) {
    case WM_CREATE:
    {
        CREATESTRUCTW* cs = (CREATESTRUCTW*)lp;
        ctx = (H264DirectRecordDialogContext*)cs->lpCreateParams;
        SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);

        CreateWindowW(L"STATIC", L"Video Device", WS_CHILD | WS_VISIBLE, 18, 22, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 170, 18, 480, 200, hwnd, (HMENU)IDC_H264_DEVICE_COMBO, NULL, NULL);

        CreateWindowW(L"STATIC", L"H264 Format", WS_CHILD | WS_VISIBLE, 18, 64, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 170, 60, 260, 200, hwnd, (HMENU)IDC_H264_CAP_COMBO, NULL, NULL);

        CreateWindowW(L"STATIC", L"Audio Input", WS_CHILD | WS_VISIBLE, 18, 106, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 170, 102, 480, 200, hwnd, (HMENU)IDC_H264_AUDIO_COMBO, NULL, NULL);

        CreateWindowW(L"STATIC", L"Output File", WS_CHILD | WS_VISIBLE, 18, 148, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, 170, 144, 390, 24, hwnd, (HMENU)IDC_H264_OUTPUT_EDIT, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Browse...", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 570, 142, 80, 28, hwnd, (HMENU)IDC_H264_BROWSE, NULL, NULL);

        CreateWindowW(L"STATIC", L"This demo selects a camera H264 compressed format and writes it directly into MP4. Video is not decoded and not re-encoded. If audio is selected, PCM audio is encoded to AAC and muxed with the original H264 video. Recording continues until Test Demo -> Stop H264 Direct Record is clicked.",
            WS_CHILD | WS_VISIBLE | SS_LEFT, 18, 190, 632, 62, hwnd, NULL, NULL, NULL);

        CreateWindowW(L"BUTTON", L"Start", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 460, 270, 90, 30, hwnd, (HMENU)IDC_OK_BUTTON, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 560, 270, 90, 30, hwnd, (HMENU)IDC_CANCEL_BUTTON, NULL, NULL);

        FillH264DeviceCombo(hwnd, ctx);
        FillH264CapCombo(hwnd, ctx);
        FillH264AudioCombo(hwnd, ctx ? ctx->audioIndex : -1);
        if (ctx && !ctx->outputPath[0]) BuildDefaultH264OutputPath(ctx->outputPath, MAX_PATH);
        if (ctx) {
            ctx->seconds = 0;
            SetWindowTextW(GetDlgItem(hwnd, IDC_H264_OUTPUT_EDIT), ctx->outputPath);
        }

        CenterWindowToParent(hwnd, GetParent(hwnd));
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDC_H264_DEVICE_COMBO:
            if (HIWORD(wp) == CBN_SELCHANGE && ctx) {
                FillH264CapCombo(hwnd, ctx);
            }
            return 0;
        case IDC_H264_CAP_COMBO:
            if (HIWORD(wp) == CBN_SELCHANGE && ctx) {
                UpdateH264DialogInfo(hwnd, ctx);
            }
            return 0;
        case IDC_H264_BROWSE:
        {
            wchar_t path[MAX_PATH] = {};
            GetWindowTextW(GetDlgItem(hwnd, IDC_H264_OUTPUT_EDIT), path, MAX_PATH);
            if (!path[0] && ctx) wcscpy_s(path, ctx->outputPath);
            if (SelectRecordFile(hwnd, path, MAX_PATH)) {
                SetWindowTextW(GetDlgItem(hwnd, IDC_H264_OUTPUT_EDIT), path);
            }
            return 0;
        }
        case IDC_OK_BUTTON:
        case IDOK:
            if (ctx) {
                int dev = GetComboData(hwnd, IDC_H264_DEVICE_COMBO);
                int capIndex = GetComboData(hwnd, IDC_H264_CAP_COMBO);
                int audio = GetComboData(hwnd, IDC_H264_AUDIO_COMBO);
                wchar_t path[MAX_PATH] = {};
                GetWindowTextW(GetDlgItem(hwnd, IDC_H264_OUTPUT_EDIT), path, MAX_PATH);
                int seconds = 0;

                if (dev < 0 || capIndex < 0 || capIndex >= (int)ctx->caps.size()) {
                    MessageBoxW(hwnd, L"No H264 capability is selected.", L"hwscapture", MB_ICONWARNING | MB_OK);
                    return 0;
                }
                if (!path[0]) {
                    MessageBoxW(hwnd, L"Please select an output MP4 file.", L"hwscapture", MB_ICONWARNING | MB_OK);
                    return 0;
                }
                const CapItem& c = ctx->caps[(size_t)capIndex];
                ctx->config.deviceIndex = dev;
                ctx->config.width = c.width;
                ctx->config.height = c.height;
                ctx->config.fps = c.fps;
                ctx->config.pixelFormat = HWS_PIXFMT_H264;
                ctx->config.colorSpace = HWS_COLOR_SPACE_AUTO;
                ctx->config.colorRange = HWS_COLOR_RANGE_AUTO;
                ctx->config.bufferCount = 4;
                ctx->seconds = seconds;
                ctx->audioIndex = audio;
                wcscpy_s(ctx->outputPath, path);
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDC_CANCEL_BUTTON:
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

static bool ShowH264DirectRecordDialog(HWND parent, H264DirectRecordDialogContext* ctx)
{
    if (!ctx) return false;
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleW(NULL);
    WNDCLASSW wc = {};
    wc.lpfnWndProc = H264DirectRecordDialogProc;
    wc.hInstance = hInst;
    wc.lpszClassName = L"HwsH264DirectRecordDialogV514";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassW(&wc);

    HWND dlg = CreateWindowExW(WS_EX_DLGMODALFRAME, wc.lpszClassName, L"Test Demo: H264 Direct Record",
        WS_CAPTION | WS_SYSMENU | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 690, 360,
        parent, NULL, hInst, ctx);
    if (!dlg) return false;

    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageW(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageW(dlg, &msg)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    EnableWindow(parent, TRUE);
    SetForegroundWindow(parent);
    return ctx->accepted;
}

static void __stdcall H264DemoVideoEncodedCallback(const HWS_ENCODED_VIDEO_PACKET* packet, void* userData)
{
    UNREFERENCED_PARAMETER(userData);
    if (!packet) return;
    if ((packet->frameIndex % 120) == 0 || packet->keyFrame) {
        printf("H264 demo video packet frame=%lld size=%d fmt=%d key=%d pts=%lld %dx%d\n",
            packet->frameIndex, packet->size, (int)packet->format, packet->keyFrame,
            packet->pts100ns, packet->width, packet->height);
    }
}

static void __stdcall H264DemoAudioEncodedCallback(const HWS_ENCODED_AUDIO_PACKET* packet, void* userData)
{
    UNREFERENCED_PARAMETER(userData);
    if (!packet) return;
    static long long count = 0;
    ++count;
    if ((count % 100) == 0) {
        printf("H264 demo audio packet count=%lld size=%d pts=%lld rate=%d ch=%d\n",
            count, packet->size, packet->pts100ns, packet->sampleRate, packet->channels);
    }
}

static DWORD WINAPI H264DirectRecordThreadProc(LPVOID param)
{
    H264DirectRecordTask* task = (H264DirectRecordTask*)param;
    if (!task) return 0;

    int result = -1;
    HWS_PORT video = NULL;
    HWS_PORT audio = NULL;
    HWS_PORT recorder = NULL;
    HWS_RECORDER_CONFIG rcfg = {};

    printf("H264 direct record demo start device=%d %dx%d@%d audio=%d file=%S\n",
        task->config.deviceIndex, task->config.width, task->config.height, task->config.fps,
        task->audioIndex, task->outputPath);

    video = HWS_CreateVideoInput();
    recorder = HWS_CreateRecorder(NULL);
    if (!video || !recorder) {
        printf("H264 demo failed: create video/recorder port failed.\n");
        goto cleanup;
    }

    if (HWS_SetVideoInputConfig(video, &task->config) != HWS_OK) {
        printf("H264 demo failed: HWS_SetVideoInputConfig(H264) failed.\n");
        goto cleanup;
    }

    wcscpy_s(rcfg.filePath, task->outputPath);
    rcfg.container = HWS_CONTAINER_MP4;
    rcfg.videoCodec = HWS_VIDEO_CODEC_H264;
    rcfg.width = task->config.width;
    rcfg.height = task->config.height;
    rcfg.fps = task->config.fps > 0 ? task->config.fps : 30;
    rcfg.recordFps = 0; // follow selected input FPS for direct-record demo
    rcfg.videoBitrate = 0;
    rcfg.enableAudio = task->audioIndex >= 0 ? 1 : 0;
    rcfg.inputMode = HWS_RECORDER_INPUT_DIRECT_H264;
    rcfg.splitMode = HWS_RECORDER_SPLIT_NONE;

    if (HWS_SetRecorderConfig(recorder, &rcfg) != HWS_OK) {
        printf("H264 demo failed: HWS_SetRecorderConfig failed.\n");
        goto cleanup;
    }

    HWS_SetRecorderVideoCallback(recorder, H264DemoVideoEncodedCallback, NULL);
    HWS_SetRecorderAudioCallback(recorder, H264DemoAudioEncodedCallback, NULL);

    if (HWS_Connect(video, recorder) != HWS_OK) {
        printf("H264 demo failed: connect video -> recorder failed.\n");
        goto cleanup;
    }

    if (task->audioIndex >= 0) {
        audio = HWS_CreateAudioInput();
        if (!audio) {
            printf("H264 demo failed: create audio input failed.\n");
            goto cleanup;
        }
        if (HWS_SetAudioInputDevice(audio, task->audioIndex) != HWS_OK ||
            HWS_SetAudioInputPCM(audio, 48000, 2, 16) != HWS_OK) {
            printf("H264 demo failed: configure audio input failed.\n");
            goto cleanup;
        }
        if (HWS_Connect(audio, recorder) != HWS_OK) {
            printf("H264 demo failed: connect audio -> recorder failed.\n");
            goto cleanup;
        }
    }

    if (HWS_Start(recorder) != HWS_OK) {
        printf("H264 demo failed: start recorder failed.\n");
        goto cleanup;
    }
    if (audio && HWS_Start(audio) != HWS_OK) {
        printf("H264 demo failed: start audio failed.\n");
        goto cleanup;
    }
    if (HWS_Start(video) != HWS_OK) {
        printf("H264 demo failed: start video failed.\n");
        goto cleanup;
    }

    // The H264 direct record demo is controlled by the test menu.
    // The Start menu creates this worker thread. The Stop menu only sets
    // g_h264DirectRecordStopRequested. The worker thread owns the ports and
    // performs HWS_Stop/HWS_DestroyPort here so MP4 finalization never blocks
    // the UI thread and no cross-thread Media Foundation shutdown race occurs.
    DWORD startTick = GetTickCount();
    DWORD lastStatsTick = startTick;
    for (;;) {
        if (InterlockedCompareExchange(&g_h264DirectRecordStopRequested, 0, 0) != 0) {
            InterlockedExchange(&g_h264DirectRecordStoppedByUser, 1);
            printf("H264 demo stop requested by menu. Finalizing MP4...\n");
            result = 0;
            break;
        }

        DWORD now = GetTickCount();
        int elapsedSec = (int)((now - startTick) / 1000);
        if (now - lastStatsTick >= 1000) {
            lastStatsTick = now;
            HWS_RECORDER_STATS stats = {};
            if (HWS_GetRecorderStats(recorder, &stats) == HWS_OK) {
                printf("H264 demo recording %d sec input=%lld encoded=%lld dropped=%lld\n",
                    elapsedSec + 1, stats.inputFrames, stats.encodedFrames, stats.droppedFrames);
                if (elapsedSec >= 3 && stats.inputFrames > 30 && stats.encodedFrames == 0 && stats.droppedFrames > 30) {
                    printf("H264 demo detected recorder writer failure: input frames arrived but no H264 sample was written. Check Raw H264 SetInputMediaType / sequence-header logs.\n");
                    result = -2;
                    break;
                }
            }
        }
        Sleep(50);
    }

cleanup:
    if (video) HWS_Stop(video);
    if (audio) HWS_Stop(audio);
    if (recorder) HWS_Stop(recorder);

    if (video && recorder) HWS_Disconnect(video, recorder);
    if (audio && recorder) HWS_Disconnect(audio, recorder);

    if (video) HWS_DestroyPort(video);
    if (audio) HWS_DestroyPort(audio);
    if (recorder) HWS_DestroyPort(recorder);

    g_h264DirectRecordResult = result;
    g_h264DirectRecordOutput = task->outputPath;
    InterlockedExchange(&g_h264DirectRecordStopRequested, 0);
    InterlockedExchange(&g_h264DirectRecordRunning, 0);
    if (task->notifyHwnd) PostMessageW(task->notifyHwnd, WM_HWS_H264_RECORD_DONE, (WPARAM)result, 0);
    printf("H264 direct record demo done result=%d file=%S\n", result, task->outputPath);
    delete task;
    return 0;
}

static void StartH264DirectRecordDemo(HWND hwnd)
{
    if (InterlockedCompareExchange(&g_h264DirectRecordRunning, 1, 0) != 0) {
        MessageBoxW(hwnd, L"H264 direct record demo is already running.", L"hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    InterlockedExchange(&g_h264DirectRecordStopRequested, 0);
    InterlockedExchange(&g_h264DirectRecordStoppedByUser, 0);

    H264DirectRecordDialogContext ctx = {};
    ctx.config.deviceIndex = 0;
    ctx.config.width = 1920;
    ctx.config.height = 1080;
    ctx.config.fps = 30;
    ctx.config.pixelFormat = HWS_PIXFMT_H264;
    ctx.config.colorSpace = HWS_COLOR_SPACE_AUTO;
    ctx.config.colorRange = HWS_COLOR_RANGE_AUTO;
    ctx.config.bufferCount = 4;
    ctx.seconds = 0;
    ctx.audioIndex = -1;
    BuildDefaultH264OutputPath(ctx.outputPath, MAX_PATH);

    if (!ShowH264DirectRecordDialog(hwnd, &ctx)) {
        InterlockedExchange(&g_h264DirectRecordRunning, 0);
        return;
    }

    H264DirectRecordTask* task = new H264DirectRecordTask();
    ZeroMemory(task, sizeof(*task));
    task->notifyHwnd = hwnd;
    task->config = ctx.config;
    wcscpy_s(task->outputPath, ctx.outputPath);
    task->seconds = 0;
    task->audioIndex = ctx.audioIndex;

    HANDLE thread = CreateThread(NULL, 0, H264DirectRecordThreadProc, task, 0, NULL);
    if (!thread) {
        delete task;
        InterlockedExchange(&g_h264DirectRecordRunning, 0);
        MessageBoxW(hwnd, L"Failed to create H264 direct record worker thread.", L"hwscapture", MB_ICONERROR | MB_OK);
        return;
    }
    CloseHandle(thread);
    MessageBoxW(hwnd, L"H264 direct record demo started. Use Test Demo -> Stop H264 Direct Record to stop and finalize the MP4 file.", L"hwscapture", MB_ICONINFORMATION | MB_OK);
}

static void StopH264DirectRecordDemo(HWND hwnd)
{
    if (InterlockedCompareExchange(&g_h264DirectRecordRunning, 1, 1) == 0) {
        MessageBoxW(hwnd, L"H264 direct record demo is not running.", L"hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }

    if (InterlockedExchange(&g_h264DirectRecordStopRequested, 1) == 0) {
        printf("H264 direct record stop requested from menu. Waiting worker thread to finalize MP4.\n");
        MessageBoxW(hwnd, L"Stop requested. The worker thread is finalizing the MP4 file. Please wait for the completion message.", L"hwscapture", MB_ICONINFORMATION | MB_OK);
    }
}

static void LoadCaps(DialogContext* ctx, int deviceIndex)
{
    ctx->caps.clear();
    int count = HWS_GetVideoCapabilityCount(deviceIndex);
    for (int i = 0; i < count; ++i) {
        HWS_VIDEO_CAP cap = {};
        if (HWS_GetVideoCapability(deviceIndex, i, &cap) == HWS_OK) {
            if (!IsDemoRenderablePixelFormat(cap.pixelFormat)) {
                continue;
            }
            CapItem item = {cap.width, cap.height, cap.fpsNum > 0 ? cap.fpsNum : 30, cap.pixelFormat};
            bool exists = false;
            for (size_t k = 0; k < ctx->caps.size(); ++k) {
                if (ctx->caps[k].width == item.width && ctx->caps[k].height == item.height && ctx->caps[k].fps == item.fps && ctx->caps[k].fmt == item.fmt) { exists = true; break; }
            }
            if (!exists) ctx->caps.push_back(item);
        }
    }
    if (ctx->caps.empty()) {
        CapItem item = {1280, 720, 30, HWS_PIXFMT_RGB32};
        ctx->caps.push_back(item);
    }
}

static void RefreshVideoDevicesForDialog()
{
    // v4.0.1: device selection dialog must see hot-plugged devices.
    // This clears SDK enumeration/capability cache and re-enumerates video/audio devices.
    HWS_RefreshDeviceList();
}

static void FillDeviceCombo(HWND hwnd, int currentIndex)
{
    HWND combo = GetDlgItem(hwnd, IDC_DEVICE_COMBO);
    SendMessageA(combo, CB_RESETCONTENT, 0, 0);

    int count = HWS_GetVideoDeviceCount();
    int selectPos = -1;

    for (int i = 0; i < count; ++i) {
        HWS_DEVICE_INFO info = {};
        if (HWS_GetVideoDeviceInfo(i, &info) == HWS_OK) {
            char line[320]; sprintf_s(line, "%d: %s", i, info.name);
            int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)line);
            SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)i);
            if (i == currentIndex) selectPos = pos;
        }
    }

    if (count > 0) {
        if (selectPos < 0) selectPos = 0;
        SendMessageA(combo, CB_SETCURSEL, selectPos, 0);
    }
}

static int ResolutionKey(int w, int h) { return (w << 16) ^ h; }
static int FmtKey(HWS_PIXEL_FORMAT fmt) { return (int)fmt; }

static bool FindExactCap(const DialogContext* ctx, int w, int h, int fps, HWS_PIXEL_FORMAT fmt)
{
    if (!ctx) return false;
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        const CapItem& c = ctx->caps[i];
        if (c.width == w && c.height == h && c.fps == fps && c.fmt == fmt) return true;
    }
    return false;
}

static void ApplyFirstCapToConfig(DialogContext* ctx)
{
    if (!ctx || ctx->caps.empty()) return;

    const CapItem& c = ctx->caps[0];
    ctx->config.width = c.width;
    ctx->config.height = c.height;
    ctx->config.fps = c.fps;
    ctx->config.pixelFormat = c.fmt;
}

static void NormalizeVideoConfigToCaps(DialogContext* ctx)
{
    if (!ctx || ctx->caps.empty()) return;

    // First opening of the dialog should follow the device capability default,
    // not the demo fallback value such as 1280x720/YUY2.
    if (ctx->forceDeviceDefault ||
        ctx->config.width <= 0 ||
        ctx->config.height <= 0 ||
        ctx->config.fps <= 0 ||
        ctx->config.pixelFormat == (HWS_PIXEL_FORMAT)0) {
        ApplyFirstCapToConfig(ctx);
        ctx->forceDeviceDefault = false;
        return;
    }

    if (FindExactCap(ctx, ctx->config.width, ctx->config.height, ctx->config.fps, ctx->config.pixelFormat)) {
        return;
    }

    // Keep resolution and FPS first. If the previous pixel format is invalid for this
    // mode, switch to the first real format supported by this resolution+FPS.
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        const CapItem& c = ctx->caps[i];
        if (c.width == ctx->config.width && c.height == ctx->config.height && c.fps == ctx->config.fps) {
            ctx->config.pixelFormat = c.fmt;
            return;
        }
    }

    // Keep resolution if possible, then choose the first FPS/format for that resolution.
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        const CapItem& c = ctx->caps[i];
        if (c.width == ctx->config.width && c.height == ctx->config.height) {
            ctx->config.fps = c.fps;
            ctx->config.pixelFormat = c.fmt;
            return;
        }
    }

    // Last fallback: use the first filtered/renderable capability.
    ctx->config.width = ctx->caps[0].width;
    ctx->config.height = ctx->caps[0].height;
    ctx->config.fps = ctx->caps[0].fps;
    ctx->config.pixelFormat = ctx->caps[0].fmt;
}

static void SyncSelectedVideoConfigFromControls(HWND hwnd, DialogContext* ctx)
{
    if (!ctx) return;

    int dev = GetComboData(hwnd, IDC_DEVICE_COMBO);
    int resKey = GetComboData(hwnd, IDC_RES_COMBO);
    int fps = GetComboData(hwnd, IDC_FPS_COMBO);
    int fmt = GetComboData(hwnd, IDC_FMT_COMBO);

    if (dev >= 0) ctx->config.deviceIndex = dev;
    if (resKey >= 0) {
        ctx->config.width = (resKey >> 16) & 0xffff;
        ctx->config.height = resKey & 0xffff;
    }
    if (fps > 0) ctx->config.fps = fps;
    if (fmt > 0) ctx->config.pixelFormat = (HWS_PIXEL_FORMAT)fmt;
}

static void FillResolutionCombo(HWND hwnd, DialogContext* ctx)
{
    HWND combo = GetDlgItem(hwnd, IDC_RES_COMBO);
    SendMessageA(combo, CB_RESETCONTENT, 0, 0);
    std::vector<int> keys;
    int selectPos = 0;
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        int key = ResolutionKey(ctx->caps[i].width, ctx->caps[i].height);
        if (std::find(keys.begin(), keys.end(), key) != keys.end()) continue;
        keys.push_back(key);
        char line[64]; sprintf_s(line, "%d x %d", ctx->caps[i].width, ctx->caps[i].height);
        int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)line);
        SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)key);
        if (ctx->caps[i].width == ctx->config.width && ctx->caps[i].height == ctx->config.height) selectPos = pos;
    }
    if (!keys.empty()) SendMessageA(combo, CB_SETCURSEL, selectPos, 0);
    SyncSelectedVideoConfigFromControls(hwnd, ctx);
}

static void FillFpsCombo(HWND hwnd, DialogContext* ctx)
{
    HWND combo = GetDlgItem(hwnd, IDC_FPS_COMBO);
    SendMessageA(combo, CB_RESETCONTENT, 0, 0);

    int resKey = GetComboData(hwnd, IDC_RES_COMBO);
    int selectedFmt = GetComboData(hwnd, IDC_FMT_COMBO);
    std::vector<int> fpsList;
    int selectPos = 0;

    // v4.0.1: FPS list follows Resolution + PixelFormat when a pixel format is selected.
    // This avoids showing FPS values that are not valid for the selected format.
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        if (ResolutionKey(ctx->caps[i].width, ctx->caps[i].height) != resKey) continue;
        if (selectedFmt > 0 && FmtKey(ctx->caps[i].fmt) != selectedFmt) continue;
        if (std::find(fpsList.begin(), fpsList.end(), ctx->caps[i].fps) != fpsList.end()) continue;
        fpsList.push_back(ctx->caps[i].fps);
        char line[32]; sprintf_s(line, "%d", ctx->caps[i].fps);
        int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)line);
        SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)ctx->caps[i].fps);
        if (ctx->caps[i].fps == ctx->config.fps) selectPos = pos;
    }

    // If the selected format has no FPS for the selected resolution, fall back to all FPS for that resolution.
    if (fpsList.empty() && selectedFmt > 0) {
        for (size_t i = 0; i < ctx->caps.size(); ++i) {
            if (ResolutionKey(ctx->caps[i].width, ctx->caps[i].height) != resKey) continue;
            if (std::find(fpsList.begin(), fpsList.end(), ctx->caps[i].fps) != fpsList.end()) continue;
            fpsList.push_back(ctx->caps[i].fps);
            char line[32]; sprintf_s(line, "%d", ctx->caps[i].fps);
            int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)line);
            SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)ctx->caps[i].fps);
            if (ctx->caps[i].fps == ctx->config.fps) selectPos = pos;
        }
    }

    if (fpsList.empty()) AddComboItem(combo, "30", 30);
    SendMessageA(combo, CB_SETCURSEL, selectPos, 0);
    SyncSelectedVideoConfigFromControls(hwnd, ctx);
}

static void FillFormatCombo(HWND hwnd, DialogContext* ctx)
{
    HWND combo = GetDlgItem(hwnd, IDC_FMT_COMBO);
    SendMessageA(combo, CB_RESETCONTENT, 0, 0);

    int resKey = GetComboData(hwnd, IDC_RES_COMBO);
    std::vector<int> fmtList;
    int selectPos = 0;

    // v4.0.1: PixelFormat list follows the selected resolution only.
    // Some devices expose YUY2/NV12/MJPG at different FPS sets. If the
    // format combo is also filtered by FPS, valid formats such as NV12 can be
    // hidden when current FPS happens to be supported only by YUY2.
    for (size_t i = 0; i < ctx->caps.size(); ++i) {
        if (ResolutionKey(ctx->caps[i].width, ctx->caps[i].height) != resKey) continue;
        int fmt = FmtKey(ctx->caps[i].fmt);
        if (std::find(fmtList.begin(), fmtList.end(), fmt) != fmtList.end()) continue;
        fmtList.push_back(fmt);
        int pos = (int)SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)PixelFormatName(ctx->caps[i].fmt));
        SendMessageA(combo, CB_SETITEMDATA, pos, (LPARAM)fmt);
        if (ctx->caps[i].fmt == ctx->config.pixelFormat) selectPos = pos;
    }

    if (fmtList.empty()) AddComboItem(combo, "RGB32", HWS_PIXFMT_RGB32);
    SendMessageA(combo, CB_SETCURSEL, selectPos, 0);
    SyncSelectedVideoConfigFromControls(hwnd, ctx);
}

static void UpdateInfoText(HWND hwnd, DialogContext* ctx)
{
    SyncSelectedVideoConfigFromControls(hwnd, ctx);
    int resKey = GetComboData(hwnd, IDC_RES_COMBO);
    int fps = GetComboData(hwnd, IDC_FPS_COMBO);
    int fmt = GetComboData(hwnd, IDC_FMT_COMBO);
    char text[512];
    sprintf_s(text, "Current Resolution : %d x %d\r\nCurrent FPS        : %d\r\nCurrent Format     : %s\r\nCapabilities       : %d",
              (resKey >> 16) & 0xffff, resKey & 0xffff, fps, PixelFormatName((HWS_PIXEL_FORMAT)fmt), (int)ctx->caps.size());
    SetWindowTextA(GetDlgItem(hwnd, IDC_INFO_TEXT), text);
}

static void ReloadFormatControls(HWND hwnd, DialogContext* ctx)
{
    NormalizeVideoConfigToCaps(ctx);
    FillResolutionCombo(hwnd, ctx);
    // v4.0.1: build PixelFormat before FPS, because FPS is filtered by Resolution+PixelFormat.
    FillFormatCombo(hwnd, ctx);
    FillFpsCombo(hwnd, ctx);
    UpdateInfoText(hwnd, ctx);
}

static LRESULT CALLBACK VideoDeviceDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    DialogContext* ctx = (DialogContext*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
    switch (msg) {

    case WM_CREATE:
    {
        CREATESTRUCTA* cs = (CREATESTRUCTA*)lp;
        ctx = (DialogContext*)cs->lpCreateParams;
        SetWindowLongPtrA(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);

        CreateWindowA("STATIC", "Video Capture Device", WS_CHILD | WS_VISIBLE, 18, 20, 170, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("COMBOBOX", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 185, 18, 430, 180, hwnd, (HMENU)IDC_DEVICE_COMBO, NULL, NULL);
        CreateWindowA("BUTTON", "Configure Video", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 185, 55, 145, 28, hwnd, (HMENU)IDC_CFG_VIDEO, NULL, NULL);
        CreateWindowA("BUTTON", "Configure Crossbar", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 345, 55, 160, 28, hwnd, (HMENU)IDC_CFG_CROSSBAR, NULL, NULL);

        CreateWindowA("STATIC", "Resolution", WS_CHILD | WS_VISIBLE, 18, 105, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("COMBOBOX", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 185, 102, 190, 180, hwnd, (HMENU)IDC_RES_COMBO, NULL, NULL);
        CreateWindowA("STATIC", "FPS", WS_CHILD | WS_VISIBLE, 400, 105, 50, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("COMBOBOX", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 450, 102, 165, 180, hwnd, (HMENU)IDC_FPS_COMBO, NULL, NULL);

        CreateWindowA("STATIC", "Pixel Format", WS_CHILD | WS_VISIBLE, 18, 145, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("COMBOBOX", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 185, 142, 190, 180, hwnd, (HMENU)IDC_FMT_COMBO, NULL, NULL);

        CreateWindowA("STATIC", "Information", WS_CHILD | WS_VISIBLE, 18, 190, 150, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("STATIC", "", WS_CHILD | WS_VISIBLE | SS_LEFT, 185, 188, 430, 95, hwnd, (HMENU)IDC_INFO_TEXT, NULL, NULL);

        CreateWindowA("BUTTON", "OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 415, 310, 90, 30, hwnd, (HMENU)IDC_OK_BUTTON, NULL, NULL);
        CreateWindowA("BUTTON", "Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 525, 310, 90, 30, hwnd, (HMENU)IDC_CANCEL_BUTTON, NULL, NULL);

        RefreshVideoDevicesForDialog();
        FillDeviceCombo(hwnd, ctx->config.deviceIndex);
        ctx->config.deviceIndex = GetComboData(hwnd, IDC_DEVICE_COMBO);
        LoadCaps(ctx, ctx->config.deviceIndex);
        ReloadFormatControls(hwnd, ctx);
        CenterWindowToParent(hwnd, GetParent(hwnd));
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDC_DEVICE_COMBO:
            if (HIWORD(wp) == CBN_SELCHANGE && ctx) {
                int dev = GetComboData(hwnd, IDC_DEVICE_COMBO);
                if (dev < 0) return 0;
                ctx->config.deviceIndex = dev;
                ctx->forceDeviceDefault = true;
                LoadCaps(ctx, dev);
                ReloadFormatControls(hwnd, ctx);
            }
            return 0;
        case IDC_RES_COMBO:
            if (HIWORD(wp) == CBN_SELCHANGE && ctx) {
                SyncSelectedVideoConfigFromControls(hwnd, ctx);
                FillFormatCombo(hwnd, ctx);
                FillFpsCombo(hwnd, ctx);
                UpdateInfoText(hwnd, ctx);
            }
            return 0;
        case IDC_FPS_COMBO:
            if (HIWORD(wp) == CBN_SELCHANGE && ctx) {
                SyncSelectedVideoConfigFromControls(hwnd, ctx);
                FillFormatCombo(hwnd, ctx);
                UpdateInfoText(hwnd, ctx);
            }
            return 0;
        case IDC_FMT_COMBO:
            if (HIWORD(wp) == CBN_SELCHANGE && ctx) {
                SyncSelectedVideoConfigFromControls(hwnd, ctx);
                FillFpsCombo(hwnd, ctx);
                UpdateInfoText(hwnd, ctx);
            }
            return 0;
        case IDC_CFG_VIDEO:
            if (ctx) HWS_ShowVideoDevicePropertyPage(hwnd, GetComboData(hwnd, IDC_DEVICE_COMBO));
            return 0;
        case IDC_CFG_CROSSBAR:
            if (ctx) {
                HWS_RESULT r = HWS_ShowVideoCrossbarPropertyPage(hwnd, GetComboData(hwnd, IDC_DEVICE_COMBO));
                if (r != HWS_OK) MessageBoxA(hwnd, "This device does not expose a DirectShow crossbar property page.", "hwscapture", MB_ICONINFORMATION | MB_OK);
            }
            return 0;
        case IDC_OK_BUTTON:
        case IDOK:
            if (ctx) {
                int dev = GetComboData(hwnd, IDC_DEVICE_COMBO);
                int resKey = GetComboData(hwnd, IDC_RES_COMBO);
                int fps = GetComboData(hwnd, IDC_FPS_COMBO);
                int fmt = GetComboData(hwnd, IDC_FMT_COMBO);
                if (dev < 0 || resKey < 0 || fps <= 0 || fmt <= 0) {
                    MessageBoxA(hwnd, "Invalid video capture configuration.", "hwscapture", MB_ICONWARNING | MB_OK);
                    return 0;
                }
                ctx->config.deviceIndex = dev;
                ctx->config.width = (resKey >> 16) & 0xffff;
                ctx->config.height = resKey & 0xffff;
                ctx->config.fps = fps;
                ctx->config.pixelFormat = (HWS_PIXEL_FORMAT)fmt;
                ctx->config.colorSpace = HWS_COLOR_SPACE_AUTO;
                ctx->config.colorRange = HWS_COLOR_RANGE_AUTO;
                ctx->config.bufferCount = 4;
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDC_CANCEL_BUTTON:
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wp, lp);
}

static bool ShowVideoCaptureDeviceDialog(HWND parent, HWS_VIDEO_INPUT_CONFIG* config)
{
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleA(NULL);
    WNDCLASSA wc = {};
    wc.lpfnWndProc = VideoDeviceDialogProc;
    wc.hInstance = hInst;
    wc.lpszClassName = "HwsVideoDeviceDialogV050";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassA(&wc);

    DialogContext ctx = {};
    ctx.config = config ? *config : g_defaultConfig;
    ctx.forceDeviceDefault =
        (ctx.config.width <= 0 ||
         ctx.config.height <= 0 ||
         ctx.config.fps <= 0 ||
         ctx.config.pixelFormat == (HWS_PIXEL_FORMAT)0);

    HWND dlg = CreateWindowExA(WS_EX_DLGMODALFRAME, wc.lpszClassName, "Properties: Video Capture Device",
                               WS_CAPTION | WS_SYSMENU | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 650, 390,
                               parent, NULL, hInst, &ctx);
    if (!dlg) return false;
    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageA(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageA(dlg, &msg)) { TranslateMessage(&msg); DispatchMessageA(&msg); }
    }
    EnableWindow(parent, TRUE);
    SetForegroundWindow(parent);
    if (ctx.accepted && config) { *config = ctx.config; return true; }
    return false;
}

static LRESULT CALLBACK AudioDeviceDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    AudioDialogContext* ctx = (AudioDialogContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
    switch (msg) {
    case WM_CREATE:
    {
        CREATESTRUCTW* cs = (CREATESTRUCTW*)lp;
        ctx = (AudioDialogContext*)cs->lpCreateParams;
        SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);

        CreateWindowW(L"STATIC", L"Audio Input Device", WS_CHILD | WS_VISIBLE, 18, 24, 160, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, 185, 20, 430, 200, hwnd, (HMENU)IDC_DEVICE_COMBO, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Use device timestamps", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, 185, 55, 250, 24, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"STATIC", L"This first audio version captures PCM from the selected WASAPI input device.\r\nThe selected audio port is connected to Recorder when recording starts.", WS_CHILD | WS_VISIBLE | SS_LEFT, 18, 105, 600, 70, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 415, 220, 90, 30, hwnd, (HMENU)IDC_OK_BUTTON, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 525, 220, 90, 30, hwnd, (HMENU)IDC_CANCEL_BUTTON, NULL, NULL);

        HWND combo = GetDlgItem(hwnd, IDC_DEVICE_COMBO);
        int count = HWS_GetAudioInputDeviceCount();
        for (int i = 0; i < count; ++i) {
            HWS_DEVICE_INFO info = {};
            if (HWS_GetAudioInputDeviceInfo(i, &info) == HWS_OK) {
                wchar_t text[512] = {};
                std::wstring name = Utf8ToWide(info.name);
                swprintf_s(text, L"%d: %s", i, name.empty() ? L"Unknown Audio Device" : name.c_str());
                int pos = (int)SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)text);
                SendMessageW(combo, CB_SETITEMDATA, pos, (LPARAM)i);
            }
        }
        SendMessageW(combo, CB_SETCURSEL, ctx && ctx->deviceIndex >= 0 ? ctx->deviceIndex : 0, 0);
        CenterWindowToParent(hwnd, GetParent(hwnd));
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDC_OK_BUTTON:
        case IDOK:
            if (ctx) {
                int idx = GetComboData(hwnd, IDC_DEVICE_COMBO);
                if (idx < 0) {
                    MessageBoxW(hwnd, L"Invalid audio input device.", L"hwscapture", MB_ICONWARNING | MB_OK);
                    return 0;
                }
                ctx->deviceIndex = idx;
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDC_CANCEL_BUTTON:
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

static bool ShowAudioInputDeviceDialog(HWND parent, int* deviceIndex)
{
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleW(NULL);
    WNDCLASSW wc = {};
    wc.lpfnWndProc = AudioDeviceDialogProc;
    wc.hInstance = hInst;
    wc.lpszClassName = L"HwsAudioDeviceDialogV122";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassW(&wc);

    AudioDialogContext ctx = {};
    ctx.deviceIndex = deviceIndex ? *deviceIndex : 0;

    HWND dlg = CreateWindowExW(WS_EX_DLGMODALFRAME, wc.lpszClassName, L"Properties: Audio Input Capture",
        WS_CAPTION | WS_SYSMENU | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 650, 310,
        parent, NULL, hInst, &ctx);
    if (!dlg) return false;
    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageW(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageW(dlg, &msg)) { TranslateMessage(&msg); DispatchMessageW(&msg); }
    }
    EnableWindow(parent, TRUE);
    SetForegroundWindow(parent);
    if (ctx.accepted && deviceIndex) { *deviceIndex = ctx.deviceIndex; return true; }
    return false;
}


static bool BrowseSnapshotDirectory(HWND owner, wchar_t* outDir, size_t outChars, HWS_IMAGE_FORMAT imageFormat)
{
    if (!outDir || outChars == 0) return false;

    // Keep the snapshot path picker consistent with the recorder path picker.
    // Use the classic Win32 save-file dialog, then keep only the selected folder.
    // The final snapshot file name is still generated by the SDK demo as:
    // ch<index>_yyyymmdd_hhmmss.bmp/jpg.
    wchar_t filePath[MAX_PATH] = {};
    const wchar_t* ext = (imageFormat == HWS_IMAGE_FORMAT_JPG) ? L"jpg" : L"bmp";
    swprintf_s(filePath, L"snapshot.%s", ext);

    wchar_t initialDir[MAX_PATH] = {};
    if (outDir[0]) {
        wcsncpy_s(initialDir, outDir, _TRUNCATE);
    }

    OPENFILENAMEW ofn = {};
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = owner;
    ofn.lpstrFile = filePath;
    ofn.nMaxFile = MAX_PATH;
    ofn.lpstrInitialDir = initialDir[0] ? initialDir : NULL;
    ofn.lpstrFilter = (imageFormat == HWS_IMAGE_FORMAT_JPG) ?
        L"JPEG Image (*.jpg)\0*.jpg\0All Files (*.*)\0*.*\0" :
        L"Bitmap Image (*.bmp)\0*.bmp\0All Files (*.*)\0*.*\0";
    ofn.lpstrDefExt = ext;
    ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
    ofn.lpstrTitle = L"Select snapshot folder";

    if (!GetSaveFileNameW(&ofn)) return false;

    wchar_t* slash1 = wcsrchr(filePath, L'\\');
    wchar_t* slash2 = wcsrchr(filePath, L'/');
    wchar_t* slash = slash1 > slash2 ? slash1 : slash2;
    if (!slash) return false;
    *slash = 0;
    wcsncpy_s(outDir, outChars, filePath, _TRUNCATE);
    return true;
}

static LRESULT CALLBACK SnapshotSettingsDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    SnapshotDialogContext* ctx = (SnapshotDialogContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
    switch (msg) {
    case WM_CREATE:
    {
        CREATESTRUCTW* cs = (CREATESTRUCTW*)lp;
        ctx = (SnapshotDialogContext*)cs->lpCreateParams;
        SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);

        CreateWindowW(L"STATIC", L"Snapshot Directory", WS_CHILD | WS_VISIBLE, 18, 28, 160, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"EDIT", ctx ? ctx->directory : L".", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, 180, 24, 360, 24, hwnd, (HMENU)IDC_SNAP_DIR_EDIT, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Browse...", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 550, 23, 85, 27, hwnd, (HMENU)IDC_SNAP_BROWSE, NULL, NULL);

        CreateWindowW(L"STATIC", L"Image Format", WS_CHILD | WS_VISIBLE, 18, 72, 160, 22, hwnd, NULL, NULL, NULL);
        HWND combo = CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST, 180, 68, 160, 100, hwnd, (HMENU)IDC_SNAP_FORMAT_COMBO, NULL, NULL);
        int bmp = (int)SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"BMP");
        SendMessageW(combo, CB_SETITEMDATA, bmp, HWS_IMAGE_FORMAT_BMP);
        int jpg = (int)SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)L"JPG");
        SendMessageW(combo, CB_SETITEMDATA, jpg, HWS_IMAGE_FORMAT_JPG);
        SendMessageW(combo, CB_SETCURSEL, (ctx && ctx->imageFormat == HWS_IMAGE_FORMAT_JPG) ? jpg : bmp, 0);

        CreateWindowW(L"STATIC", L"The snapshot file name is generated as ch<index>_<yyyymmdd>_<hhmmss>.<ext>.", WS_CHILD | WS_VISIBLE | SS_LEFT, 18, 115, 610, 40, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 440, 170, 90, 30, hwnd, (HMENU)IDC_OK_BUTTON, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 545, 170, 90, 30, hwnd, (HMENU)IDC_CANCEL_BUTTON, NULL, NULL);
        CenterWindowToParent(hwnd, GetParent(hwnd));
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDC_SNAP_BROWSE:
        {
            wchar_t dir[MAX_PATH] = {};
            GetWindowTextW(GetDlgItem(hwnd, IDC_SNAP_DIR_EDIT), dir, MAX_PATH);
            int selFmt = (int)SendMessageW(GetDlgItem(hwnd, IDC_SNAP_FORMAT_COMBO), CB_GETCURSEL, 0, 0);
            HWS_IMAGE_FORMAT browseFmt = (HWS_IMAGE_FORMAT)SendMessageW(GetDlgItem(hwnd, IDC_SNAP_FORMAT_COMBO), CB_GETITEMDATA, selFmt, 0);
            if (BrowseSnapshotDirectory(hwnd, dir, MAX_PATH, browseFmt)) {
                SetWindowTextW(GetDlgItem(hwnd, IDC_SNAP_DIR_EDIT), dir);
            }
            return 0;
        }
        case IDC_OK_BUTTON:
        case IDOK:
            if (ctx) {
                GetWindowTextW(GetDlgItem(hwnd, IDC_SNAP_DIR_EDIT), ctx->directory, MAX_PATH);
                if (!ctx->directory[0]) {
                    MessageBoxW(hwnd, L"Please select a snapshot directory.", L"hwscapture", MB_ICONWARNING | MB_OK);
                    return 0;
                }
                int sel = (int)SendMessageW(GetDlgItem(hwnd, IDC_SNAP_FORMAT_COMBO), CB_GETCURSEL, 0, 0);
                ctx->imageFormat = (HWS_IMAGE_FORMAT)SendMessageW(GetDlgItem(hwnd, IDC_SNAP_FORMAT_COMBO), CB_GETITEMDATA, sel, 0);
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDC_CANCEL_BUTTON:
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

static bool ShowSnapshotSettingsDialog(HWND parent)
{
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleW(NULL);
    WNDCLASSW wc = {};
    wc.lpfnWndProc = SnapshotSettingsDialogProc;
    wc.hInstance = hInst;
    wc.lpszClassName = L"HwsSnapshotSettingsDialogV130";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassW(&wc);

    SnapshotDialogContext ctx = {};
    wcscpy_s(ctx.directory, g_snapshotDirectory);
    ctx.imageFormat = g_snapshotFormat;

    HWND dlg = CreateWindowExW(WS_EX_DLGMODALFRAME, wc.lpszClassName, L"Snapshot Settings",
        WS_CAPTION | WS_SYSMENU | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 670, 260,
        parent, NULL, hInst, &ctx);
    if (!dlg) return false;
    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageW(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageW(dlg, &msg)) { TranslateMessage(&msg); DispatchMessageW(&msg); }
    }
    EnableWindow(parent, TRUE);
    SetForegroundWindow(parent);
    if (ctx.accepted) {
        wcscpy_s(g_snapshotDirectory, ctx.directory);
        g_snapshotFormat = ctx.imageFormat;
        return true;
    }
    return false;
}

static void EnsureDirectoryPathNoTrailingSlash(wchar_t* path)
{
    if (!path) return;
    size_t n = wcslen(path);
    while (n > 0 && (path[n - 1] == L'\\' || path[n - 1] == L'/')) {
        if (n == 3 && path[1] == L':') break;
        path[n - 1] = 0;
        --n;
    }
}

static void CaptureSnapshotForSelectedSlot(HWND hwnd)
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[g_selectedSlot];
    if (!s.active || !s.render) {
        MessageBoxW(hwnd, L"The selected channel has no active video render.", L"hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }

    wchar_t dir[MAX_PATH] = {};
    wcscpy_s(dir, g_snapshotDirectory);
    EnsureDirectoryPathNoTrailingSlash(dir);

    SYSTEMTIME st = {};
    GetLocalTime(&st);
    const wchar_t* ext = (g_snapshotFormat == HWS_IMAGE_FORMAT_JPG) ? L"jpg" : L"bmp";
    wchar_t path[MAX_PATH] = {};
    swprintf_s(path, L"%s\\ch%d_%04d%02d%02d_%02d%02d%02d.%s",
        dir,
        s.index + 1,
        st.wYear, st.wMonth, st.wDay,
        st.wHour, st.wMinute, st.wSecond,
        ext);

    HWS_RESULT r = HWS_SaveVideoRenderSnapshot(s.render, path, g_snapshotFormat);
    if (r != HWS_OK) {
        MessageBoxW(hwnd, L"Failed to capture snapshot. Please make sure the selected channel is rendering video.", L"hwscapture", MB_ICONERROR | MB_OK);
        return;
    }
    wchar_t msg[512] = {};
    swprintf_s(msg, L"Snapshot saved:\r\n%s", path);
    MessageBoxW(hwnd, msg, L"hwscapture", MB_ICONINFORMATION | MB_OK);
}

static int GetDlgIntValue(HWND hwnd, int id, int defValue)
{
    char buf[64] = {};
    GetWindowTextA(GetDlgItem(hwnd, id), buf, sizeof(buf));
    if (!buf[0]) return defValue;
    return atoi(buf);
}

static float GetDlgFloatValue(HWND hwnd, int id, float defValue)
{
    char buf[64] = {};
    GetWindowTextA(GetDlgItem(hwnd, id), buf, sizeof(buf));
    if (!buf[0]) return defValue;
    return (float)atof(buf);
}

static void SetDlgIntValue(HWND hwnd, int id, int value)
{
    char buf[64];
    sprintf_s(buf, "%d", value);
    SetWindowTextA(GetDlgItem(hwnd, id), buf);
}

static void SetDlgFloatValue(HWND hwnd, int id, float value)
{
    char buf[64];
    sprintf_s(buf, "%.2f", value);
    SetWindowTextA(GetDlgItem(hwnd, id), buf);
}


static LRESULT CALLBACK RecorderFpsDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    RecorderFpsDialogContext* ctx = (RecorderFpsDialogContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
    switch (msg) {
    case WM_CREATE:
    {
        CREATESTRUCTW* cs = (CREATESTRUCTW*)lp;
        ctx = (RecorderFpsDialogContext*)cs->lpCreateParams;
        SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);
        CreateWindowW(L"STATIC", L"Custom recording FPS:", WS_CHILD | WS_VISIBLE, 18, 22, 180, 22, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"EDIT", L"60", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP,
            190, 18, 80, 24, hwnd, (HMENU)IDC_REC_FPS_EDIT, NULL, NULL);
        CreateWindowW(L"STATIC", L"Recorder writes a constant-frame-rate MP4. If source FPS is lower, frames are duplicated; if source FPS is higher, frames are dropped.",
            WS_CHILD | WS_VISIBLE | SS_LEFT, 18, 55, 420, 48, hwnd, NULL, NULL, NULL);
        CreateWindowW(L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
            230, 115, 88, 28, hwnd, (HMENU)IDOK, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
            330, 115, 88, 28, hwnd, (HMENU)IDCANCEL, NULL, NULL);
        SetDlgIntValue(hwnd, IDC_REC_FPS_EDIT, ctx ? ctx->fps : 60);
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDOK:
            if (ctx) {
                int fps = GetDlgIntValue(hwnd, IDC_REC_FPS_EDIT, 60);
                if (fps < 1) fps = 1;
                if (fps > 240) fps = 240;
                ctx->fps = fps;
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

static bool ShowRecorderFpsDialog(HWND parent, int* fps)
{
    RecorderFpsDialogContext ctx = {};
    ctx.fps = (fps && *fps > 0) ? *fps : 60;
    ctx.accepted = false;

    WNDCLASSW wc = {};
    wc.lpfnWndProc = RecorderFpsDialogProc;
    wc.hInstance = GetModuleHandleW(NULL);
    wc.lpszClassName = L"HwsRecorderFpsDialogV515";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    RegisterClassW(&wc);

    HWND dlg = CreateWindowExW(WS_EX_DLGMODALFRAME, wc.lpszClassName, L"Custom Recording FPS",
        WS_POPUP | WS_CAPTION | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT, 470, 190,
        parent, NULL, wc.hInstance, &ctx);
    if (!dlg) return false;
    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageW(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageW(dlg, &msg)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    EnableWindow(parent, TRUE);
    SetActiveWindow(parent);
    if (ctx.accepted && fps) *fps = ctx.fps;
    return ctx.accepted;
}

static bool ChooseRgbColor(HWND hwnd, unsigned int* argbColor)
{
    static COLORREF customColors[16] = {};
    CHOOSECOLORA cc = {};
    cc.lStructSize = sizeof(cc);
    cc.hwndOwner = hwnd;
    cc.lpCustColors = customColors;
    cc.Flags = CC_FULLOPEN | CC_RGBINIT;
    unsigned int c = argbColor ? *argbColor : 0xFFFFFFFF;
    cc.rgbResult = RGB((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
    if (!ChooseColorA(&cc)) return false;
    if (argbColor) {
        *argbColor = 0xFF000000 | ((unsigned int)GetRValue(cc.rgbResult) << 16) | ((unsigned int)GetGValue(cc.rgbResult) << 8) | (unsigned int)GetBValue(cc.rgbResult);
    }
    return true;
}

static bool ChooseBmpFile(HWND hwnd, wchar_t* path, size_t pathChars)
{
    if (!path || pathChars == 0) return false;
    OPENFILENAMEW ofn = {};
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = hwnd;
    ofn.lpstrFilter = L"Bitmap Files (*.bmp)\0*.bmp\0All Files (*.*)\0*.*\0";
    ofn.lpstrFile = path;
    ofn.nMaxFile = (DWORD)pathChars;
    ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
    ofn.lpstrDefExt = L"bmp";
    return GetOpenFileNameW(&ofn) ? true : false;
}

static LRESULT CALLBACK OsdDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    OsdDialogContext* ctx = (OsdDialogContext*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
    switch (msg) {

    case WM_CREATE:
    {
        CREATESTRUCTA* cs = (CREATESTRUCTA*)lp;
        ctx = (OsdDialogContext*)cs->lpCreateParams;
        SetWindowLongPtrA(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);

        if (!ctx->imageMode) {
            CreateWindowW(L"STATIC", L"Text", WS_CHILD | WS_VISIBLE, 18, 22, 110, 22, hwnd, NULL, NULL, NULL);
            HWND textEdit = CreateWindowW(L"EDIT", ctx->item.text[0] ? ctx->item.text : L"hwscapture OSD", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, 125, 18, 360, 24, hwnd, (HMENU)IDC_OSD_TEXT_EDIT, NULL, NULL);
            (void)textEdit;
            CreateWindowW(L"STATIC", L"Font", WS_CHILD | WS_VISIBLE, 18, 180, 90, 22, hwnd, NULL, NULL, NULL);
            HWND fontCombo = CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL, 125, 176, 180, 220, hwnd, (HMENU)IDC_OSD_FONT_NAME, NULL, NULL);
            SendMessageW(fontCombo, CB_ADDSTRING, 0, (LPARAM)L"Microsoft YaHei UI");
            SendMessageW(fontCombo, CB_ADDSTRING, 0, (LPARAM)L"Microsoft YaHei");
            SendMessageW(fontCombo, CB_ADDSTRING, 0, (LPARAM)L"SimSun");
            SendMessageW(fontCombo, CB_ADDSTRING, 0, (LPARAM)L"Segoe UI");
            SendMessageW(fontCombo, CB_ADDSTRING, 0, (LPARAM)L"Arial");
            SetWindowTextW(fontCombo, ctx->item.fontName[0] ? ctx->item.fontName : L"Microsoft YaHei UI");
            CreateWindowW(L"STATIC", L"Size", WS_CHILD | WS_VISIBLE, 318, 180, 42, 22, hwnd, NULL, NULL, NULL);
            CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 360, 176, 55, 24, hwnd, (HMENU)IDC_OSD_FONT_EDIT, NULL, NULL);
            SetDlgIntValue(hwnd, IDC_OSD_FONT_EDIT, ctx->item.fontSize > 0 ? ctx->item.fontSize : 36);
            CreateWindowA("BUTTON", "Text Color", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 430, 176, 105, 28, hwnd, (HMENU)IDC_OSD_TEXT_COLOR, NULL, NULL);
            CreateWindowA("BUTTON", "Background Color", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 125, 212, 135, 28, hwnd, (HMENU)IDC_OSD_BG_COLOR, NULL, NULL);
            CreateWindowA("BUTTON", "Transparent Background", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, 280, 212, 200, 24, hwnd, (HMENU)IDC_OSD_TRANSPARENT, NULL, NULL);
            SendMessageA(GetDlgItem(hwnd, IDC_OSD_TRANSPARENT), BM_SETCHECK, ctx->item.transparentBackground ? BST_CHECKED : BST_UNCHECKED, 0);
        } else {
            CreateWindowA("STATIC", "BMP File", WS_CHILD | WS_VISIBLE, 18, 22, 110, 22, hwnd, NULL, NULL, NULL);
            CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL, 125, 18, 360, 24, hwnd, (HMENU)IDC_OSD_FILE_EDIT, NULL, NULL);
            CreateWindowA("BUTTON", "Browse...", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 495, 17, 90, 26, hwnd, (HMENU)IDC_OSD_BROWSE_FILE, NULL, NULL);
            if (ctx->item.filePath[0]) SetWindowTextW(GetDlgItem(hwnd, IDC_OSD_FILE_EDIT), ctx->item.filePath);
            CreateWindowA("BUTTON", "Transparent Background Color", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX, 125, 176, 230, 24, hwnd, (HMENU)IDC_OSD_TRANSPARENT, NULL, NULL);
            SendMessageA(GetDlgItem(hwnd, IDC_OSD_TRANSPARENT), BM_SETCHECK, ctx->item.transparentBackground ? BST_CHECKED : BST_UNCHECKED, 0);
            CreateWindowA("BUTTON", "Choose Key Color", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 370, 172, 135, 28, hwnd, (HMENU)IDC_OSD_BG_COLOR, NULL, NULL);
        }

        CreateWindowA("STATIC", "X", WS_CHILD | WS_VISIBLE, 18, 70, 30, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 55, 66, 80, 24, hwnd, (HMENU)IDC_OSD_X_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Y", WS_CHILD | WS_VISIBLE, 150, 70, 30, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 185, 66, 80, 24, hwnd, (HMENU)IDC_OSD_Y_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Width", WS_CHILD | WS_VISIBLE, 18, 110, 50, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 80, 106, 80, 24, hwnd, (HMENU)IDC_OSD_W_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Height", WS_CHILD | WS_VISIBLE, 180, 110, 55, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 245, 106, 80, 24, hwnd, (HMENU)IDC_OSD_H_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Opacity 0.0-1.0", WS_CHILD | WS_VISIBLE, 18, 145, 110, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER, 145, 141, 80, 24, hwnd, (HMENU)IDC_OSD_OPACITY_EDIT, NULL, NULL);

        SetDlgIntValue(hwnd, IDC_OSD_X_EDIT, ctx->item.x);
        SetDlgIntValue(hwnd, IDC_OSD_Y_EDIT, ctx->item.y);
        SetDlgIntValue(hwnd, IDC_OSD_W_EDIT, ctx->item.width > 0 ? ctx->item.width : (ctx->imageMode ? 160 : 400));
        SetDlgIntValue(hwnd, IDC_OSD_H_EDIT, ctx->item.height > 0 ? ctx->item.height : (ctx->imageMode ? 120 : 80));
        SetDlgFloatValue(hwnd, IDC_OSD_OPACITY_EDIT, ctx->item.opacity > 0.0f ? ctx->item.opacity : 1.0f);

        CreateWindowA("BUTTON", "OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 395, 250, 90, 30, hwnd, (HMENU)IDC_OK_BUTTON, NULL, NULL);
        CreateWindowA("BUTTON", "Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 500, 250, 90, 30, hwnd, (HMENU)IDC_CANCEL_BUTTON, NULL, NULL);
        CenterWindowToParent(hwnd, GetParent(hwnd));
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDC_OSD_TEXT_COLOR:
            if (ctx) ChooseRgbColor(hwnd, &ctx->item.color);
            return 0;
        case IDC_OSD_BG_COLOR:
            if (ctx) ChooseRgbColor(hwnd, &ctx->item.backgroundColor);
            return 0;
        case IDC_OSD_BROWSE_FILE:
            if (ctx && ChooseBmpFile(hwnd, ctx->item.filePath, MAX_PATH)) {
                SetWindowTextW(GetDlgItem(hwnd, IDC_OSD_FILE_EDIT), ctx->item.filePath);
            }
            return 0;
        case IDC_OK_BUTTON:
        case IDOK:
            if (ctx) {
                ctx->item.enable = 1;
                ctx->item.x = GetDlgIntValue(hwnd, IDC_OSD_X_EDIT, 0);
                ctx->item.y = GetDlgIntValue(hwnd, IDC_OSD_Y_EDIT, 0);
                ctx->item.width = GetDlgIntValue(hwnd, IDC_OSD_W_EDIT, 0);
                ctx->item.height = GetDlgIntValue(hwnd, IDC_OSD_H_EDIT, 0);
                ctx->item.opacity = GetDlgFloatValue(hwnd, IDC_OSD_OPACITY_EDIT, 1.0f);
                ctx->item.transparentBackground = (SendMessageA(GetDlgItem(hwnd, IDC_OSD_TRANSPARENT), BM_GETCHECK, 0, 0) == BST_CHECKED) ? 1 : 0;
                if (!ctx->imageMode) {
                    GetWindowTextW(GetDlgItem(hwnd, IDC_OSD_TEXT_EDIT), ctx->item.text, 512);
                    GetWindowTextW(GetDlgItem(hwnd, IDC_OSD_FONT_NAME), ctx->item.fontName, 64);
                    if (!ctx->item.fontName[0]) wcscpy_s(ctx->item.fontName, L"Microsoft YaHei UI");
                    ctx->item.fontSize = GetDlgIntValue(hwnd, IDC_OSD_FONT_EDIT, 36);
                    ctx->item.filePath[0] = 0;
                } else {
                    GetWindowTextW(GetDlgItem(hwnd, IDC_OSD_FILE_EDIT), ctx->item.filePath, MAX_PATH);
                    ctx->item.text[0] = 0;
                    ctx->item.fontSize = 0;
                    if (!ctx->item.filePath[0]) {
                        MessageBoxA(hwnd, "Please select a BMP file.", "hwscapture", MB_ICONWARNING | MB_OK);
                        return 0;
                    }
                }
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDC_CANCEL_BUTTON:
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wp, lp);
}

static bool ShowOsdDialog(HWND parent, bool imageMode, HWS_OSD_ITEM* item)
{
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleA(NULL);
    WNDCLASSA wc = {};
    wc.lpfnWndProc = OsdDialogProc;
    wc.hInstance = hInst;
    wc.lpszClassName = imageMode ? "HwsImageOsdDialogV071" : "HwsTextOsdDialogV071";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassA(&wc);

    OsdDialogContext ctx = {};
    ctx.imageMode = imageMode;
    if (item) ctx.item = *item;
    if (ctx.item.opacity <= 0.0f) ctx.item.opacity = 1.0f;
    if (ctx.item.color == 0) ctx.item.color = 0xFFFFFFFF;
    if (ctx.item.backgroundColor == 0) ctx.item.backgroundColor = 0xFF000000;
    if (!imageMode && ctx.item.fontSize <= 0) ctx.item.fontSize = 36;

    HWND dlg = CreateWindowExA(WS_EX_DLGMODALFRAME, wc.lpszClassName, imageMode ? "Image OSD" : "Text OSD",
        WS_CAPTION | WS_SYSMENU | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, imageMode ? 620 : 520, 330,
        parent, NULL, hInst, &ctx);
    if (!dlg) return false;
    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageA(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageA(dlg, &msg)) { TranslateMessage(&msg); DispatchMessageA(&msg); }
    }
    EnableWindow(parent, TRUE);
    SetForegroundWindow(parent);
    if (ctx.accepted && item) { *item = ctx.item; return true; }
    return false;
}

static void ApplyOsdToSelectedSlot(bool imageMode)
{
    int slotIndex = GetTargetSlotForAdd();
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) {
        MessageBoxA(g_mainHwnd, "No visible preview slot is available.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    SelectPreviewSlot(slotIndex);
    PreviewSlot& s = g_slots[slotIndex];
    HWS_OSD_ITEM item = {};
    item.id = 0;
    item.enable = 1;
    item.type = imageMode ? HWS_OSD_ITEM_IMAGE : HWS_OSD_ITEM_TEXT;
    item.zOrder = (int)s.osdItems.size();
    item.opacity = 1.0f;
    item.color = 0xFFFFFFFF;
    item.backgroundColor = 0x80000000;
    item.transparentBackground = imageMode ? 1 : 0;
    item.fontSize = 36;
    wcscpy_s(item.fontName, L"Microsoft YaHei UI");
    item.width = imageMode ? 160 : 400;
    item.height = imageMode ? 120 : 80;
    if (ShowOsdDialog(g_mainHwnd, imageMode, &item)) {
        if (item.id <= 0) item.id = AllocateDemoOsdItemId();
        s.useOsd = true;
        s.osdItems.push_back(item);
        if (s.active) {
            if (s.osd) {
                if (item.type == HWS_OSD_ITEM_IMAGE || item.filePath[0]) HWS_AddVideoOsdImage(s.osd, &item);
                else HWS_AddVideoOsdText(s.osd, &item);
            } else {
                // Compatibility fallback for old running graphs. New graphs always have OSD.
                HWS_VIDEO_INPUT_CONFIG cfg = s.config;
                StartPreviewSlot((size_t)slotIndex, cfg);
            }
        } else {
            MessageBoxA(g_mainHwnd, "OSD is configured for this slot. Add a video capture device to preview it.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        }
    }
}


static void FormatDateTimeText(wchar_t* text, size_t count)
{
    SYSTEMTIME st = {};
    GetLocalTime(&st);
    swprintf_s(text, count, L"%04d-%02d-%02d %02d:%02d:%02d",
        st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}

static void UpdateDateTimeOsdItems()
{
    wchar_t text[128] = {};
    FormatDateTimeText(text, _countof(text));

    for (size_t i = 0; i < g_slots.size(); ++i) {
        PreviewSlot& s = g_slots[i];
        if (!s.osd || s.dateTimeOsdItemId <= 0) continue;
        HWS_UpdateVideoOsdText(s.osd, s.dateTimeOsdItemId, text);
        for (size_t j = 0; j < s.osdItems.size(); ++j) {
            if (s.osdItems[j].id == s.dateTimeOsdItemId) {
                wcscpy_s(s.osdItems[j].text, text);
                break;
            }
        }
    }
}

static void AddDateTimeOsdForSelectedSlot()
{
    int slotIndex = GetTargetSlotForAdd();
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) {
        MessageBoxA(g_mainHwnd, "No visible preview slot is available.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    SelectPreviewSlot(slotIndex);
    PreviewSlot& s = g_slots[slotIndex];

    HWS_OSD_ITEM item = {};
    item.id = AllocateDemoOsdItemId();
    item.enable = 1;
    item.type = HWS_OSD_ITEM_TEXT;
    item.zOrder = (int)s.osdItems.size();
    item.x = 20;
    item.y = 20;
    item.width = 520;
    item.height = 70;
    item.opacity = 1.0f;
    item.color = 0xFFFFFFFF;
    item.backgroundColor = 0x80000000;
    item.transparentBackground = 0;
    item.fontSize = 36;
    wcscpy_s(item.fontName, L"Microsoft YaHei UI");
    FormatDateTimeText(item.text, _countof(item.text));

    s.useOsd = true;
    s.dateTimeOsdItemId = item.id;
    s.osdItems.push_back(item);

    if (s.active) {
        if (s.osd) {
            HWS_AddVideoOsdText(s.osd, &item);
        } else {
            HWS_VIDEO_INPUT_CONFIG cfg = s.config;
            StartPreviewSlot((size_t)slotIndex, cfg);
        }
    } else {
        MessageBoxA(g_mainHwnd, "Date/time OSD is configured for this slot. Add a video capture device to preview it.", "hwscapture", MB_ICONINFORMATION | MB_OK);
    }

    SetTimer(g_mainHwnd, TIMER_OSD_DATETIME, 1000, NULL);
}

static void RemoveLastOsdForSelectedSlot()
{
    int slotIndex = GetTargetSlotForAdd();
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) {
        MessageBoxA(g_mainHwnd, "No visible preview slot is available.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    SelectPreviewSlot(slotIndex);
    PreviewSlot& s = g_slots[slotIndex];
    if (s.osdItems.empty()) {
        MessageBoxA(g_mainHwnd, "No OSD item exists in the selected slot.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    int itemId = s.osdItems.back().id;
    s.osdItems.pop_back();
    if (itemId == s.dateTimeOsdItemId) s.dateTimeOsdItemId = 0;
    if (s.osd && itemId > 0) {
        HWS_RemoveVideoOsdItem(s.osd, itemId);
    } else if (s.osd) {
        HWS_ClearVideoOsd(s.osd);
        for (size_t i = 0; i < s.osdItems.size(); ++i) {
            if (s.osdItems[i].type == HWS_OSD_ITEM_IMAGE || s.osdItems[i].filePath[0]) HWS_AddVideoOsdImage(s.osd, &s.osdItems[i]);
            else HWS_AddVideoOsdText(s.osd, &s.osdItems[i]);
        }
    }
    if (s.osdItems.empty()) s.useOsd = false;
}

static void ClearOsdForSelectedSlot()
{
    int slotIndex = GetTargetSlotForAdd();
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) {
        MessageBoxA(g_mainHwnd, "No visible preview slot is available.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    SelectPreviewSlot(slotIndex);
    PreviewSlot& s = g_slots[slotIndex];
    s.osdItems.clear();
    s.dateTimeOsdItemId = 0;
    if (s.osd) {
        HWS_ClearVideoOsd(s.osd);
    }
    s.useOsd = false;
    if (!s.active) {
        return;
    }
    // Keep the current graph running when possible. Clearing the live OSD port
    // removes all overlays immediately and avoids a disruptive preview restart.
    if (!s.osd) {
        HWS_VIDEO_INPUT_CONFIG cfg = s.config;
        StartPreviewSlot((size_t)slotIndex, cfg);
    }
}
static void ClearOsdForSlot(int slotIndex)
{
    if (slotIndex < 0 || slotIndex >= (int)g_slots.size()) {
        MessageBoxA(g_mainHwnd, "No visible preview slot is available.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }
    //SelectPreviewSlot(slotIndex);
    PreviewSlot& s = g_slots[slotIndex];
    s.osdItems.clear();
    s.dateTimeOsdItemId = 0;
    if (s.osd) {
        HWS_ClearVideoOsd(s.osd);
    }
    s.useOsd = false;
    if (!s.active) {
        return;
    }
}

static void PaintSlotPlaceholder(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);
    RECT rc;
    GetClientRect(hwnd, &rc);
    HBRUSH brush = CreateSolidBrush(RGB(20, 20, 20));
    FillRect(hdc, &rc, brush);
    DeleteObject(brush);
    SetBkMode(hdc, TRANSPARENT);
    SetTextColor(hdc, RGB(180, 180, 180));
    const char* text = "No Video Source";
    DrawTextA(hdc, text, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    EndPaint(hwnd, &ps);
}

static LRESULT CALLBACK PreviewSlotProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg) {
    case WM_LBUTTONDOWN:
        SelectPreviewSlot(GetSlotIndexFromHwnd(hwnd));
        SetFocus(hwnd);
        return 0;
    case WM_LBUTTONDBLCLK:
        ToggleFullscreenSlot(GetSlotIndexFromHwnd(hwnd));
        return 0;
    case WM_RBUTTONUP:
    {
        int idx = GetSlotIndexFromHwnd(hwnd);
        if (idx >= 0) SelectPreviewSlot(idx);
        POINT pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) };
        ClientToScreen(hwnd, &pt);
        HWND parent = GetParent(hwnd);
        if (parent) {
            POINT cpt = pt;
            ScreenToClient(parent, &cpt);
            PostMessageA(parent, WM_RBUTTONUP, 0, MAKELPARAM(cpt.x, cpt.y));
        }
        return 0;
    }
    case WM_PAINT:
    {
        int idx = GetSlotIndexFromHwnd(hwnd);
        if (idx >= 0 && idx < (int)g_slots.size() && g_slots[idx].active) {
            PAINTSTRUCT ps;
            BeginPaint(hwnd, &ps);
            EndPaint(hwnd, &ps);
            return 0;
        }
        PaintSlotPlaceholder(hwnd);
        return 0;
    }
    case WM_ERASEBKGND:
        return 1;
    }
    return DefWindowProcA(hwnd, msg, wp, lp);
}

static void RegisterPreviewSlotClass(HINSTANCE hInst)
{
    WNDCLASSA wc = {};
    wc.lpfnWndProc = PreviewSlotProc;
    wc.style = CS_DBLCLKS;
    wc.hInstance = hInst;
    wc.lpszClassName = "HwsPreviewSlotWindow";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = NULL;
    RegisterClassA(&wc);
}

static void CreatePreviewSlots(HWND hwnd)
{
    if (!g_slots.empty()) return;
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleA(NULL);
    RegisterPreviewSlotClass(hInst);
    g_slots.resize(16);
    for (int i = 0; i < 16; ++i) {
        PreviewSlot& s = g_slots[i];
        s.index = i;
        s.hwnd = CreateWindowExA(0, "HwsPreviewSlotWindow", "", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
                                 0, 0, 100, 100, hwnd, NULL, hInst, NULL);
        s.videoIn = NULL;
        s.render = NULL;
        s.osd = NULL;
        s.crop = NULL;
        s.audioIn = NULL;
        s.audioRender = NULL;
        s.audioMonitorEnabled = false;
        s.useOsd = false;
        s.useCrop = false;
        s.cropRect = {0, 0, 0, 0};
        s.osdItems.clear();
        s.config = g_defaultConfig;
        s.deviceName[0] = 0;
        s.audioDeviceName[0] = 0;
        s.callbackCount = 0;
        s.lastFrameIndex = 0;
        s.fpsWindowTick = 0;
        s.fpsWindowCount = 0;
        s.measuredInputFps = 0.0;
        s.renderFpsWindowTick = 0;
        s.renderFpsWindowCount = 0;
        s.measuredRenderFps = 0.0;
        s.active = false;
        s.recorder = NULL;
        s.recording = false;
        s.recordPath[0] = 0;
        s.dateTimeOsdItemId = 0;
    }
    g_selectedSlot = 0;
    LayoutPreviewSlots(hwnd);
    InvalidateRect(hwnd, NULL, TRUE);
}

static int FindNextFreeVisibleSlot()
{
    for (int i = 0; i < g_viewCount && i < (int)g_slots.size(); ++i) {
        if (!g_slots[i].active) return i;
    }
    return -1;
}


static int GetTargetSlotForAdd()
{
    if (g_fullscreenSlot >= 0) return g_fullscreenSlot;
    if (g_selectedSlot >= 0 && g_selectedSlot < g_viewCount && g_selectedSlot < (int)g_slots.size()) return g_selectedSlot;
    return FindNextFreeVisibleSlot();
}

static void SetViewLayout(HWND hwnd, int count)
{
    if (count != 1 && count != 4 && count != 8 && count != 16) return;
#if 0
    if (count < g_viewCount) {
        for (int i = count; i < g_viewCount && i < (int)g_slots.size(); ++i) StopPreviewSlot((size_t)i, false);
    }
#endif 
    g_viewCount = count;
    g_fullscreenSlot = -1;
    if (g_selectedSlot >= g_viewCount) g_selectedSlot = 0;
    LayoutPreviewSlots(hwnd);
    InvalidateRect(hwnd, NULL, TRUE);
    UpdateMainWindowTitle();
}


static LRESULT CALLBACK CropDialogProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    CropDialogContext* ctx = (CropDialogContext*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
    switch (msg) {
    case WM_CREATE:
    {
        CREATESTRUCTA* cs = (CREATESTRUCTA*)lp;
        ctx = (CropDialogContext*)cs->lpCreateParams;
        SetWindowLongPtrA(hwnd, GWLP_USERDATA, (LONG_PTR)ctx);
        CreateWindowA("STATIC", "Left", WS_CHILD | WS_VISIBLE, 18, 25, 70, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 120, 22, 100, 24, hwnd, (HMENU)IDC_CROP_X_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Top", WS_CHILD | WS_VISIBLE, 18, 60, 70, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 120, 57, 100, 24, hwnd, (HMENU)IDC_CROP_Y_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Width", WS_CHILD | WS_VISIBLE, 18, 95, 70, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 120, 92, 100, 24, hwnd, (HMENU)IDC_CROP_W_EDIT, NULL, NULL);
        CreateWindowA("STATIC", "Height", WS_CHILD | WS_VISIBLE, 18, 130, 70, 22, hwnd, NULL, NULL, NULL);
        CreateWindowA("EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_NUMBER, 120, 127, 100, 24, hwnd, (HMENU)IDC_CROP_H_EDIT, NULL, NULL);
        CreateWindowA("BUTTON", "OK", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 95, 178, 80, 28, hwnd, (HMENU)IDC_OK_BUTTON, NULL, NULL);
        CreateWindowA("BUTTON", "Cancel", WS_CHILD | WS_VISIBLE | WS_TABSTOP, 190, 178, 80, 28, hwnd, (HMENU)IDC_CANCEL_BUTTON, NULL, NULL);
        SetDlgIntValue(hwnd, IDC_CROP_X_EDIT, ctx->rect.x);
        SetDlgIntValue(hwnd, IDC_CROP_Y_EDIT, ctx->rect.y);
        SetDlgIntValue(hwnd, IDC_CROP_W_EDIT, ctx->rect.width);
        SetDlgIntValue(hwnd, IDC_CROP_H_EDIT, ctx->rect.height);
        CenterWindowToParent(hwnd, GetParent(hwnd));
        return 0;
    }
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDC_OK_BUTTON:
        case IDOK:
            if (ctx) {
                ctx->rect.x = GetDlgIntValue(hwnd, IDC_CROP_X_EDIT, 0);
                ctx->rect.y = GetDlgIntValue(hwnd, IDC_CROP_Y_EDIT, 0);
                ctx->rect.width = GetDlgIntValue(hwnd, IDC_CROP_W_EDIT, 0);
                ctx->rect.height = GetDlgIntValue(hwnd, IDC_CROP_H_EDIT, 0);
                if (ctx->rect.width <= 0 || ctx->rect.height <= 0) {
                    MessageBoxA(hwnd, "Crop width and height must be greater than zero.", "hwscapture", MB_ICONWARNING | MB_OK);
                    return 0;
                }
                ctx->accepted = true;
            }
            DestroyWindow(hwnd);
            return 0;
        case IDC_CANCEL_BUTTON:
        case IDCANCEL:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wp, lp);
}

static bool ShowCropDialog(HWND parent, HWS_VIDEO_CROP_RECT* rect)
{
    if (!rect) return false;
    HINSTANCE hInst = (HINSTANCE)GetModuleHandleA(NULL);
    WNDCLASSA wc = {};
    wc.lpfnWndProc = CropDialogProc;
    wc.hInstance = hInst;
    wc.lpszClassName = "HwsVideoCropDialogV180";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    RegisterClassA(&wc);

    CropDialogContext ctx = {};
    ctx.rect = *rect;
    HWND dlg = CreateWindowExA(WS_EX_DLGMODALFRAME, wc.lpszClassName, "Video Crop",
        WS_CAPTION | WS_SYSMENU | WS_POPUP, CW_USEDEFAULT, CW_USEDEFAULT, 320, 260,
        parent, NULL, hInst, &ctx);
    if (!dlg) return false;
    EnableWindow(parent, FALSE);
    ShowWindow(dlg, SW_SHOW);
    UpdateWindow(dlg);
    MSG msg;
    while (IsWindow(dlg) && GetMessageA(&msg, NULL, 0, 0)) {
        if (!IsDialogMessageA(dlg, &msg)) { TranslateMessage(&msg); DispatchMessageA(&msg); }
    }
    EnableWindow(parent, TRUE);
    SetActiveWindow(parent);
    if (ctx.accepted) *rect = ctx.rect;
    return ctx.accepted;
}

static void AddVideoCropForSelectedSlot(HWND hwnd)
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[g_selectedSlot];
    if (!s.active) {
        MessageBoxA(hwnd, "Please add a video capture device to the selected slot first.", "hwscapture", MB_ICONINFORMATION | MB_OK);
        return;
    }

    HWS_VIDEO_CROP_RECT rect = s.useCrop ? s.cropRect : HWS_VIDEO_CROP_RECT{ 0, 0, s.config.width, s.config.height };
    if (rect.width <= 0) rect.width = s.config.width;
    if (rect.height <= 0) rect.height = s.config.height;
    if (!ShowCropDialog(hwnd, &rect)) return;

    s.useCrop = true;
    s.cropRect = rect;
    printf("Add video crop slot=%d x=%d y=%d w=%d h=%d\n", s.index, rect.x, rect.y, rect.width, rect.height);
    StartPreviewSlot((size_t)g_selectedSlot, s.config);
}

static void RemoveVideoCropForSelectedSlot()
{
    if (g_selectedSlot < 0 || g_selectedSlot >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[g_selectedSlot];
    if (!s.useCrop) return;
    HWS_VIDEO_INPUT_CONFIG cfg = s.config;
    s.useCrop = false;
    s.cropRect = { 0, 0, 0, 0 };
    printf("Remove video crop slot=%d\n", s.index);
    if (s.active) StartPreviewSlot((size_t)g_selectedSlot, cfg);
}
static void RemoveVideoCropForSlot(size_t index)
{
    if (index < 0 || index >= (int)g_slots.size()) return;
    PreviewSlot& s = g_slots[index];
    if (!s.useCrop) return;
    HWS_VIDEO_INPUT_CONFIG cfg = s.config;
    s.useCrop = false;
    s.cropRect = { 0, 0, 0, 0 };
    printf("Remove video crop slot=%d\n", s.index);
    //if (s.active) StartPreviewSlot((size_t)index, cfg);
}

static HMENU CreateMainMenu()
{
    HMENU mainMenu = CreateMenu();
    HMENU sourceMenu = CreatePopupMenu();
    HMENU viewMenu = CreatePopupMenu();
    HMENU osdMenu = CreatePopupMenu();
    HMENU recorderMenu = CreatePopupMenu();
    HMENU testDemoMenu = CreatePopupMenu();
    HMENU snapshotMenu = CreatePopupMenu();
    HMENU cropMenu = CreatePopupMenu();
    HMENU fileMenu = CreatePopupMenu();
    AppendMenuA(sourceMenu, MF_STRING, IDM_ADD_VIDEO_CAPTURE_DEVICE, "Add Video Capture Device...");
    AppendMenuA(sourceMenu, MF_STRING, IDM_ADD_AUDIO_INPUT_DEVICE, "Add Audio Input Device...");
    AppendMenuA(sourceMenu, MF_STRING, IDM_STOP_PREVIEW, "Stop All Preview");
    AppendMenuA(viewMenu, MF_STRING, IDM_VIEW_1, "1 View");
    AppendMenuA(viewMenu, MF_STRING, IDM_VIEW_4, "4 Views");
    AppendMenuA(viewMenu, MF_STRING, IDM_VIEW_8, "8 Views");
    AppendMenuA(viewMenu, MF_STRING, IDM_VIEW_16, "16 Views");
    AppendMenuA(viewMenu, MF_SEPARATOR, 0, NULL);
    AppendMenuA(osdMenu, MF_STRING, IDM_OSD_TEXT, "Add Text OSD...");
    AppendMenuA(osdMenu, MF_STRING, IDM_OSD_IMAGE, "Add Image OSD...");
    AppendMenuA(osdMenu, MF_STRING, IDM_OSD_DATETIME, "Add Date/Time OSD");
    AppendMenuA(osdMenu, MF_SEPARATOR, 0, NULL);
    AppendMenuA(osdMenu, MF_STRING, IDM_OSD_REMOVE_LAST, "Remove Last OSD");
    AppendMenuA(osdMenu, MF_STRING, IDM_OSD_CLEAR, "Clear All OSD");
    AppendMenuA(recorderMenu, MF_STRING, IDM_REC_START, "Start Recording...");
    AppendMenuA(recorderMenu, MF_STRING, IDM_REC_STOP, "Stop Recording");
    AppendMenuA(recorderMenu, MF_SEPARATOR, 0, NULL);
    AppendMenuA(recorderMenu, MF_STRING, IDM_REC_FPS_FOLLOW, "Recording FPS: Follow Input FPS");
    AppendMenuA(recorderMenu, MF_STRING, IDM_REC_FPS_CUSTOM, "Recording FPS: Custom FPS...");
    AppendMenuA(recorderMenu, MF_SEPARATOR, 0, NULL);
    AppendMenuA(recorderMenu, MF_STRING, IDM_REC_VIDEO_CB, "Enable Video Encoded Callback");
    AppendMenuA(recorderMenu, MF_STRING, IDM_REC_AUDIO_CB, "Enable Audio Encoded Callback");
    AppendMenuA(testDemoMenu, MF_STRING, IDM_DEMO_H264_DIRECT_RECORD, "Start H264 Direct Record Demo...");
    AppendMenuA(testDemoMenu, MF_STRING, IDM_DEMO_H264_DIRECT_RECORD_STOP, "Stop H264 Direct Record");
    AppendMenuA(snapshotMenu, MF_STRING, IDM_SNAP_SETTINGS, "Snapshot Settings...");
    AppendMenuA(snapshotMenu, MF_STRING, IDM_SNAP_CAPTURE, "Capture Snapshot");
    AppendMenuA(cropMenu, MF_STRING, IDM_CROP_ADD, "Add Video Crop...");
    AppendMenuA(cropMenu, MF_STRING, IDM_CROP_REMOVE, "Remove Video Crop");
    AppendMenuA(fileMenu, MF_STRING, IDM_EXIT_APP, "Exit");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)sourceMenu, "Source");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)viewMenu, "View");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)osdMenu, "OSD");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)recorderMenu, "Recorder");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)testDemoMenu, "H264 Hardware Demo");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)snapshotMenu, "Snapshot");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)cropMenu, "Video Crop");
    AppendMenuA(mainMenu, MF_POPUP, (UINT_PTR)fileMenu, "File");
    return mainMenu;
}


static void PaintMainBackground(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hwnd, &ps);
    RECT rc;
    GetClientRect(hwnd, &rc);
    HBRUSH black = CreateSolidBrush(RGB(0, 0, 0));
    FillRect(hdc, &rc, black);
    DeleteObject(black);

    HPEN whitePen = CreatePen(PS_SOLID, 1, RGB(230, 230, 230));
    HPEN redPen = CreatePen(PS_SOLID, 3, RGB(255, 0, 0));
    HBRUSH oldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(HOLLOW_BRUSH));
    HPEN oldPen = (HPEN)SelectObject(hdc, whitePen);

    int visibleCount = (g_fullscreenSlot >= 0) ? 16 : g_viewCount;
    for (int i = 0; i < visibleCount && i < 16; ++i) {
        if (IsRectEmpty(&g_slotRects[i])) continue;
        SelectObject(hdc, (i == g_selectedSlot) ? redPen : whitePen);
        Rectangle(hdc, g_slotRects[i].left, g_slotRects[i].top, g_slotRects[i].right, g_slotRects[i].bottom);
    }

    SelectObject(hdc, oldPen);
    SelectObject(hdc, oldBrush);
    DeleteObject(whitePen);
    DeleteObject(redPen);
    EndPaint(hwnd, &ps);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg) {
    case WM_INITMENUPOPUP:
    {
        HMENU menu = GetMenu(hwnd);
        bool active = false;
        bool recording = false;
        if (g_selectedSlot >= 0 && g_selectedSlot < (int)g_slots.size()) {
            active = g_slots[g_selectedSlot].active;
            recording = g_slots[g_selectedSlot].recording;
        }
        EnableMenuItem(menu, IDM_REC_START, MF_BYCOMMAND | ((active && !recording) ? MF_ENABLED : MF_GRAYED));
        EnableMenuItem(menu, IDM_REC_STOP, MF_BYCOMMAND | (recording ? MF_ENABLED : MF_GRAYED));
        CheckMenuItem(menu, IDM_REC_FPS_FOLLOW, MF_BYCOMMAND | (g_recorderFpsFollowInput ? MF_CHECKED : MF_UNCHECKED));
        CheckMenuItem(menu, IDM_REC_FPS_CUSTOM, MF_BYCOMMAND | (!g_recorderFpsFollowInput ? MF_CHECKED : MF_UNCHECKED));
        CheckMenuItem(menu, IDM_REC_VIDEO_CB, MF_BYCOMMAND | (g_enableRecorderVideoCallback ? MF_CHECKED : MF_UNCHECKED));
        CheckMenuItem(menu, IDM_REC_AUDIO_CB, MF_BYCOMMAND | (g_enableRecorderAudioCallback ? MF_CHECKED : MF_UNCHECKED));
        EnableMenuItem(menu, IDM_DEMO_H264_DIRECT_RECORD, MF_BYCOMMAND | (g_h264DirectRecordRunning ? MF_GRAYED : MF_ENABLED));
        EnableMenuItem(menu, IDM_DEMO_H264_DIRECT_RECORD_STOP, MF_BYCOMMAND | (g_h264DirectRecordRunning ? MF_ENABLED : MF_GRAYED));
        EnableMenuItem(menu, IDM_SNAP_CAPTURE, MF_BYCOMMAND | (active ? MF_ENABLED : MF_GRAYED));
        EnableMenuItem(menu, IDM_CROP_ADD, MF_BYCOMMAND | (active ? MF_ENABLED : MF_GRAYED));
        EnableMenuItem(menu, IDM_CROP_REMOVE, MF_BYCOMMAND | ((active && g_slots[g_selectedSlot].useCrop) ? MF_ENABLED : MF_GRAYED));
        DrawMenuBar(hwnd);
        return 0;
    }

    case WM_HWS_H264_RECORD_DONE:
    {
        wchar_t msgText[MAX_PATH + 128] = {};
        if ((int)wp == 0) {
            if (InterlockedCompareExchange(&g_h264DirectRecordStoppedByUser, 0, 0) != 0) {
                swprintf_s(msgText, L"H264 direct record demo stopped and MP4 finalized.\r\n%s", g_h264DirectRecordOutput.c_str());
            } else {
                swprintf_s(msgText, L"H264 direct record demo completed.\r\n%s", g_h264DirectRecordOutput.c_str());
            }
            MessageBoxW(hwnd, msgText, L"hwscapture", MB_ICONINFORMATION | MB_OK);
        } else {
            swprintf_s(msgText, L"H264 direct record demo failed. Please check the console log.\r\n%s", g_h264DirectRecordOutput.c_str());
            MessageBoxW(hwnd, msgText, L"hwscapture", MB_ICONERROR | MB_OK);
        }
        InterlockedExchange(&g_h264DirectRecordStoppedByUser, 0);
        DrawMenuBar(hwnd);
        return 0;
    }

    case WM_PAINT:
        PaintMainBackground(hwnd);
        return 0;
    case WM_ERASEBKGND:
        return 1;
    case WM_LBUTTONDOWN:
    {
        POINT pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) };
        for (int i = 0; i < 16; ++i) {
            if (!IsRectEmpty(&g_slotRects[i]) && PtInRect(&g_slotRects[i], pt)) { SelectPreviewSlot(i); break; }
        }
        return 0;
    }
    case WM_LBUTTONDBLCLK:
    {
        POINT pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) };
        for (int i = 0; i < 16; ++i) {
            if (!IsRectEmpty(&g_slotRects[i]) && PtInRect(&g_slotRects[i], pt)) { ToggleFullscreenSlot(i); break; }
        }
        return 0;
    }
    case WM_RBUTTONUP:
    {
        POINT pt = { GET_X_LPARAM(lp), GET_Y_LPARAM(lp) };
        int hit = -1;
        for (int i = 0; i < 16; ++i) {
            if (!IsRectEmpty(&g_slotRects[i]) && PtInRect(&g_slotRects[i], pt)) { hit = i; break; }
        }
        if (hit >= 0) {
            SelectPreviewSlot(hit);
            HMENU menu = CreatePopupMenu();
            AppendMenuA(menu, MF_STRING, IDM_CTX_SELECT_VIDEO, "Select Video Source...");
            AppendMenuA(menu, MF_STRING, IDM_CTX_SELECT_AUDIO, "Select Audio Source...");
            AppendMenuA(menu, MF_SEPARATOR, 0, NULL);
            AppendMenuA(menu, MF_STRING, IDM_CTX_REMOVE_AVIDEO, "Remvo Video/Audio Source");
            AppendMenuA(menu, MF_SEPARATOR, 0, NULL);
            bool mon = g_slots[hit].audioMonitorEnabled;
            AppendMenuA(menu, mon ? MF_GRAYED : MF_STRING, IDM_CTX_AUDIO_MONITOR_ON, "Enable Audio Monitor");
            AppendMenuA(menu, mon ? MF_STRING : MF_GRAYED, IDM_CTX_AUDIO_MONITOR_OFF, "Disable Audio Monitor");
            ClientToScreen(hwnd, &pt);
            TrackPopupMenu(menu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, NULL);
            DestroyMenu(menu);
        }
        return 0;
    }

    case WM_CREATE:
        HWS_SDK_PIPELINE_MODE m_mode;
        g_mainHwnd = hwnd;
        AllocConsole();
        freopen("CONOUT$", "w", stdout);
        HWS_Init();
        HWS_EnableLog(1);//Enable Log 
        m_mode = HWS_SDK_PIPELINE_MODE_CPU;// HWS_SDK_PIPELINE_MODE_GPU;
        //HWS_SetDefaultPipelineMode(m_mode);
        PrintVideoDevices();
        SetMenu(hwnd, CreateMainMenu());
        CreatePreviewSlots(hwnd);
        UpdateMainWindowTitle();
        if (g_autoStartFromCommandLine) PostMessageA(hwnd, WM_HWS_AUTOSTART, 0, 0);
        return 0;
    case WM_COMMAND:
        switch (LOWORD(wp)) {
        case IDM_ADD_VIDEO_CAPTURE_DEVICE:
        {
            int slotIndex = GetTargetSlotForAdd();
            if (slotIndex < 0) {
                MessageBoxA(hwnd, "No visible preview slot is available.", "hwscapture", MB_ICONINFORMATION | MB_OK);
                return 0;
            }
            AddVideoSourceToSlot(hwnd, slotIndex);
            return 0;
        }
        case IDM_ADD_AUDIO_INPUT_DEVICE:
        case IDM_CTX_SELECT_AUDIO:
            AddAudioInputToSelectedSlot(hwnd);
            return 0;
        case IDM_CTX_SELECT_VIDEO:
            AddVideoSourceToSlot(hwnd, g_selectedSlot);
            return 0;
        case IDM_CTX_REMOVE_AVIDEO:
            StopPreviewSlotAndRepairPeers((size_t)g_selectedSlot);
            return 0;
        case IDM_CTX_AUDIO_MONITOR_ON:
            EnableAudioMonitorForSelectedSlot(hwnd);
            return 0;
        case IDM_CTX_AUDIO_MONITOR_OFF:
            DisableAudioMonitorForSelectedSlot();
            return 0;
        case IDM_STOP_PREVIEW:
            StopAllPreviewSlots();
            return 0;
        case IDM_VIEW_1:
            SetViewLayout(hwnd, 1);
            return 0;
        case IDM_VIEW_4:
            SetViewLayout(hwnd, 4);
            return 0;
        case IDM_VIEW_8:
            SetViewLayout(hwnd, 8);
            return 0;
        case IDM_VIEW_16:
            SetViewLayout(hwnd, 16);
            return 0;
        case IDM_OSD_TEXT:
            ApplyOsdToSelectedSlot(false);
            return 0;
        case IDM_OSD_IMAGE:
            ApplyOsdToSelectedSlot(true);
            return 0;
        case IDM_OSD_DATETIME:
            AddDateTimeOsdForSelectedSlot();
            return 0;
        case IDM_OSD_REMOVE_LAST:
            RemoveLastOsdForSelectedSlot();
            return 0;
        case IDM_OSD_CLEAR:
            ClearOsdForSelectedSlot();
            return 0;
        case IDM_REC_START:
            StartRecorderForSelectedSlot(hwnd);
            return 0;
        case IDM_REC_STOP:
            StopRecorderForSelectedSlot();
            return 0;
        case IDM_REC_FPS_FOLLOW:
            g_recorderFpsFollowInput = true;
            printf("Recorder FPS mode: follow input FPS\n");
            DrawMenuBar(hwnd);
            return 0;
        case IDM_REC_FPS_CUSTOM:
        {
            int fps = g_recorderCustomFps > 0 ? g_recorderCustomFps : 60;
            if (ShowRecorderFpsDialog(hwnd, &fps)) {
                g_recorderCustomFps = fps;
                g_recorderFpsFollowInput = false;
                printf("Recorder FPS mode: custom %d fps\n", g_recorderCustomFps);
                DrawMenuBar(hwnd);
            }
            return 0;
        }
        case IDM_REC_VIDEO_CB:
            g_enableRecorderVideoCallback = !g_enableRecorderVideoCallback;
            printf("Recorder video encoded callback %s\n", g_enableRecorderVideoCallback ? "enabled" : "disabled");
            return 0;
        case IDM_REC_AUDIO_CB:
            g_enableRecorderAudioCallback = !g_enableRecorderAudioCallback;
            printf("Recorder audio encoded callback %s\n", g_enableRecorderAudioCallback ? "enabled" : "disabled");
            return 0;
        case IDM_DEMO_H264_DIRECT_RECORD:
            StartH264DirectRecordDemo(hwnd);
            return 0;
        case IDM_DEMO_H264_DIRECT_RECORD_STOP:
            StopH264DirectRecordDemo(hwnd);
            return 0;
        case IDM_SNAP_SETTINGS:
            ShowSnapshotSettingsDialog(hwnd);
            return 0;
        case IDM_SNAP_CAPTURE:
            CaptureSnapshotForSelectedSlot(hwnd);
            return 0;
        case IDM_CROP_ADD:
            AddVideoCropForSelectedSlot(hwnd);
            return 0;
        case IDM_CROP_REMOVE:
            RemoveVideoCropForSelectedSlot();
            return 0;
        case IDM_EXIT_APP:
            DestroyWindow(hwnd);
            return 0;
        }
        break;
    case WM_HWS_AUTOSTART:
        StartPreviewSlot(0, g_defaultConfig);
        return 0;
    case WM_SIZE:
        LayoutPreviewSlots(hwnd);
        InvalidateRect(hwnd, NULL, TRUE);
        return 0;
    case WM_TIMER:
        if (wp == TIMER_OSD_DATETIME) {
            UpdateDateTimeOsdItems();
            return 0;
        }
        break;
    case WM_DESTROY:
        KillTimer(hwnd, TIMER_OSD_DATETIME);
        StopAllPreviewSlots();
        g_audioMonitorCount = 0;
        ReleaseGlobalAudioMonitorIfUnused();
        HWS_Shutdown();
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcA(hwnd, msg, wp, lp);
}


static void EnableBestProcessDpiAwareness()
{
    HMODULE user32 = GetModuleHandleA("user32.dll");
    if (user32) {
        typedef BOOL(WINAPI* SetProcessDpiAwarenessContextFn)(HANDLE);
        SetProcessDpiAwarenessContextFn setCtx =
            (SetProcessDpiAwarenessContextFn)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
        if (setCtx) {
            if (setCtx((HANDLE)-4)) {
                printf("[DPI] SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) ok\n");
                return;
            }
            printf("[DPI] SetProcessDpiAwarenessContext failed err=%lu\n", GetLastError());
        }
        typedef BOOL(WINAPI* SetProcessDPIAwareFn)();
        SetProcessDPIAwareFn setAware =
            (SetProcessDPIAwareFn)GetProcAddress(user32, "SetProcessDPIAware");
        if (setAware) {
            BOOL ok = setAware();
            printf("[DPI] SetProcessDPIAware result=%d err=%lu\n", ok ? 1 : 0, GetLastError());
        }
    }
}

int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE, LPSTR cmdLine, int nCmdShow)
{
    EnableBestProcessDpiAwareness();

    if (cmdLine && cmdLine[0]) {
        int idx = atoi(cmdLine);
        if (idx >= 0) { g_defaultConfig.deviceIndex = idx; g_autoStartFromCommandLine = true; }
    }

    WNDCLASSA wc = {};
    wc.lpfnWndProc = WndProc;
    wc.style = CS_DBLCLKS;
    wc.hInstance = hInst;
    wc.lpszClassName = "HwsCaptureTestWindow";
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = NULL;
    RegisterClassA(&wc);

    HWND hwnd = CreateWindowA(wc.lpszClassName, "hwscapture_test", WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                              CW_USEDEFAULT, CW_USEDEFAULT, 1280, 720, NULL, NULL, hInst, NULL);
    if (!hwnd) return 0;
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg;
    while (GetMessageA(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }
    return (int)msg.wParam;
}
