#include "pch.h"
#include "framework.h"
#include "CaptureManager.h"
#include <shlwapi.h>
#include <commdlg.h>
#include <stdio.h>
#include <time.h>
#pragma comment(lib, "Shlwapi.lib")

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

const wchar_t* HwsPixelFormatText(HWS_PIXEL_FORMAT fmt)
{
    switch (fmt) {
    case HWS_PIXFMT_YUY2: return L"YUY2";
    case HWS_PIXFMT_NV12: return L"NV12";
    case HWS_PIXFMT_RGB24: return L"RGB24";
    case HWS_PIXFMT_RGB32: return L"RGB32";
    case HWS_PIXFMT_MJPG: return L"MJPG";
    default: return L"UNKNOWN";
    }
}


static void __stdcall MfcVideoInputCb(const HWS_VIDEO_FRAME* frame, void* user)
{
    (void)frame;
    (void)user;
}

static void __stdcall MfcRenderVideoCb(const HWS_VIDEO_FRAME* frame, void* user)
{
    (void)frame;
    (void)user;
}

static void __stdcall RecorderVideoCb(const HWS_ENCODED_VIDEO_PACKET* pkt, void* user)
{
    if (!pkt) return;
    if ((pkt->frameIndex % 60) == 0 || pkt->keyFrame) {
        wchar_t buf[256];
        swprintf_s(buf, L"Recorder video callback slot=%d frame=%lld size=%d key=%d\n",
            (int)(intptr_t)user, pkt->frameIndex, pkt->size, pkt->keyFrame);
        OutputDebugStringW(buf);
    }
}

static void __stdcall RecorderAudioCb(const HWS_ENCODED_AUDIO_PACKET* pkt, void* user)
{
    if (!pkt) return;
    wchar_t buf[256];
    swprintf_s(buf, L"Recorder audio callback slot=%d size=%d pts=%lld\n", (int)(intptr_t)user, pkt->size, pkt->pts100ns);
    OutputDebugStringW(buf);
}

bool CaptureManager::Initialize()
{
    if (m_initialized) return true;
    if (HWS_Init() != HWS_OK) return false;
    m_initialized = true;
    return true;
}

void CaptureManager::Shutdown()
{
    StopAll();
    if (m_audioMonitorRender) { HWS_Stop(m_audioMonitorRender); HWS_DestroyPort(m_audioMonitorRender); m_audioMonitorRender = NULL; }
    if (m_audioMonitorMux) { HWS_DestroyPort(m_audioMonitorMux); m_audioMonitorMux = NULL; }
    m_slots.clear();
    m_slots.shrink_to_fit();
    if (m_initialized) { HWS_Shutdown(); }
    m_initialized = false;
}

void CaptureManager::SetSlotWindows(const std::vector<HWND>& hwnds)
{
    // This function defines the SDK slot-to-window mapping. It should be called
    // only during initial dialog setup, not when switching 1/4/8/16 views.
    StopAll();
    m_slots.clear();
    m_slots.reserve(hwnds.size());
    for (size_t i = 0; i < hwnds.size(); ++i) {
        Slot s;
        s.index = (int)i;
        s.hwnd = hwnds[i];
        GetCurrentDirectoryW(MAX_PATH, s.snapshotDir);
        s.snapshotFormat = HWS_IMAGE_FORMAT_BMP;
        m_slots.push_back(s);
    }
    m_selectedSlot = m_slots.empty() ? -1 : 0;
}

int CaptureManager::SlotCount() const { return (int)m_slots.size(); }
CaptureManager::Slot* CaptureManager::GetSlot(int index)
{
    if (index < 0 || index >= (int)m_slots.size()) return NULL;
    return &m_slots[(size_t)index];
}
void CaptureManager::SelectSlot(int index)
{
    if (index >= 0 && index < (int)m_slots.size()) m_selectedSlot = index;
}


int CaptureManager::FindVideoSourceOwnerSlot(int deviceIndex) const
{
    std::map<int, int>::const_iterator it = m_videoSourceOwnerSlotByDevice.find(deviceIndex);
    if (it == m_videoSourceOwnerSlotByDevice.end()) return -1;
    int slot = it->second;
    if (slot < 0 || slot >= (int)m_slots.size()) return -1;
    const Slot& s = m_slots[(size_t)slot];
    if (!s.active || !s.videoIn || s.config.deviceIndex != deviceIndex) return -1;
    return slot;
}

void CaptureManager::RebuildVideoSourceOwnerMap()
{
    m_videoSourceOwnerSlotByDevice.clear();
    for (size_t i = 0; i < m_slots.size(); ++i) {
        const Slot& s = m_slots[i];
        if (!s.active || !s.videoIn || s.config.deviceIndex < 0) continue;
        if (m_videoSourceOwnerSlotByDevice.find(s.config.deviceIndex) == m_videoSourceOwnerSlotByDevice.end()) {
            m_videoSourceOwnerSlotByDevice[s.config.deviceIndex] = (int)i;
        }
    }
}

std::vector<CaptureManager::PeerRestartInfo> CaptureManager::CollectSameDevicePeers(int sourceSlotIndex, int deviceIndex) const
{
    std::vector<PeerRestartInfo> peers;
    if (deviceIndex < 0) return peers;
    for (size_t i = 0; i < m_slots.size(); ++i) {
        if ((int)i == sourceSlotIndex) continue;
        const Slot& s = m_slots[i];
        if (!s.active || !s.videoIn) continue;
        if (s.config.deviceIndex != deviceIndex) continue;
        PeerRestartInfo info;
        info.index = (int)i;
        info.config = s.config;
        peers.push_back(info);
    }
    return peers;
}

void CaptureManager::DestroyVideoChain(Slot& s)
{
    StopRecorder(s.index);

    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) {
        HWS_DestroyPort(s.osd);
        s.osd = NULL;
    }

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

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

    s.active = false;
    s.useOsd = false;
    s.useCrop = false;
    s.recording = false;
    s.dateTimeOsd = false;
    s.osdIds.clear();
    s.osdItems.clear();
    ZeroMemory(&s.cropRect, sizeof(s.cropRect));
    if (s.hwnd) InvalidateRect(s.hwnd, NULL, TRUE);
}

void CaptureManager::StopSlot(int slotIndex)
{
    Slot* ps = GetSlot(slotIndex); if (!ps) return;
    DisableAudioMonitor(slotIndex);
    if (ps->audioIn) { HWS_Stop(ps->audioIn); HWS_DestroyPort(ps->audioIn); ps->audioIn = NULL; }
    DestroyVideoChain(*ps);
    RebuildVideoSourceOwnerMap();
}

void CaptureManager::StopSlotAndRepairPeers(int slotIndex)
{
    Slot* ps = GetSlot(slotIndex); if (!ps) return;
    const bool wasActive = ps->active && ps->videoIn;
    const int oldDeviceIndex = wasActive ? ps->config.deviceIndex : -1;
    const int oldOwnerSlot = (oldDeviceIndex >= 0) ? FindVideoSourceOwnerSlot(oldDeviceIndex) : -1;
    const bool wasOwner = wasActive && (oldOwnerSlot == slotIndex);
    std::vector<PeerRestartInfo> peers = wasOwner ? CollectSameDevicePeers(slotIndex, oldDeviceIndex) : std::vector<PeerRestartInfo>();

    // Stop all shared clients first, then the source owner. This prevents a
    // remaining window from binding to a stale DirectShow source during repair.
    if (wasOwner) {
        for (size_t i = 0; i < peers.size(); ++i) {
            StopSlot(peers[i].index);
        }
    }

    StopSlot(slotIndex);

    if (wasOwner) {
        for (size_t i = 0; i < peers.size(); ++i) {
            RebuildVideoChainInternal(m_slots[(size_t)peers[i].index].hwnd, peers[i].index, false);
        }
    }
    RebuildVideoSourceOwnerMap();
}

void CaptureManager::StopAll()
{
    for (int i = (int)m_slots.size() - 1; i >= 0; --i) StopSlot(i);
    m_videoSourceOwnerSlotByDevice.clear();
}

bool CaptureManager::RebuildVideoChain(HWND owner, int slotIndex)
{
    return RebuildVideoChainInternal(owner, slotIndex, true);
}

bool CaptureManager::RebuildVideoChainInternal(HWND owner, int slotIndex, bool repairPeers)
{
    Slot* ps = GetSlot(slotIndex); if (!ps) return false;

    const bool wasActive = ps->active && ps->videoIn;
    const int oldDeviceIndex = wasActive ? ps->config.deviceIndex : -1;
    const int oldOwnerSlot = (oldDeviceIndex >= 0) ? FindVideoSourceOwnerSlot(oldDeviceIndex) : -1;
    const bool oldWasOwner = wasActive && oldOwnerSlot == slotIndex;
    std::vector<PeerRestartInfo> peers = (repairPeers && oldWasOwner) ? CollectSameDevicePeers(slotIndex, oldDeviceIndex) : std::vector<PeerRestartInfo>();
    Slot keep = *ps;

    if (oldWasOwner && repairPeers) {
        for (size_t i = 0; i < peers.size(); ++i) {
            StopSlot(peers[i].index);
        }
    }

    DestroyVideoChain(*ps);
    ps->config = keep.config;
    ps->useOsd = keep.useOsd;
    ps->useCrop = keep.useCrop;
    ps->cropRect = keep.cropRect;
    ps->osdIds = keep.osdIds;
    ps->osdItems = keep.osdItems;
    ps->dateTimeOsd = keep.dateTimeOsd;
    ps->dateTimeOsdId = keep.dateTimeOsdId;
    
    ps->videoIn = HWS_CreateVideoInput();
    ps->render = HWS_CreateVideoRender(ps->hwnd);
    if (ps->useCrop) ps->crop = HWS_CreateVideoCrop();
    ps->osd = HWS_CreateVideoOsd();

    if (!ps->videoIn || !ps->render || !ps->osd || (ps->useCrop && !ps->crop)) {
        DestroyVideoChain(*ps);
        for (size_t i = 0; i < peers.size(); ++i) {
            RebuildVideoChainInternal(m_slots[(size_t)peers[i].index].hwnd, peers[i].index, false);
        }
        MessageBoxW(owner, L"Failed to create video ports.", L"hwscapture", MB_ICONERROR);
        return false;
    }

    HWS_SetVideoInputConfig(ps->videoIn, &ps->config);
    HWS_VIDEO_RENDER_CONFIG rcfg = {};
    rcfg.width = (ps->useCrop && ps->cropRect.width > 0) ? ps->cropRect.width : ps->config.width;
    rcfg.height = (ps->useCrop && ps->cropRect.height > 0) ? ps->cropRect.height : ps->config.height;
    rcfg.pixelFormat = ps->config.pixelFormat;
    HWS_SetVideoRenderConfig(ps->render, &rcfg);

    HWS_SetVideoFrameCallback(ps->videoIn, MfcVideoInputCb, ps);
    HWS_SetVideoFrameCallback(ps->render, MfcRenderVideoCb, ps);
    
    HWS_PORT upstream = ps->videoIn;
    if (ps->useCrop && ps->crop) {
        HWS_SetVideoCropRect(ps->crop, &ps->cropRect);
        HWS_EnableVideoCrop(ps->crop, 1);
        HWS_Connect(upstream, ps->crop);
        upstream = ps->crop;
    }

    for (size_t i = 0; i < ps->osdItems.size(); ++i) {
        HWS_OSD_ITEM& item = ps->osdItems[i];
        if (item.type == HWS_OSD_ITEM_IMAGE || item.filePath[0]) HWS_AddVideoOsdImage(ps->osd, &item);
        else HWS_AddVideoOsdText(ps->osd, &item);
    }

    HWS_Connect(upstream, ps->osd);
    HWS_ConnectVideoOsdOutput(ps->osd, HWS_OSD_OUTPUT_OSD, ps->render);

    HWS_RESULT r = HWS_Start(ps->videoIn);
    if (r != HWS_OK) {
        DestroyVideoChain(*ps);
        for (size_t i = 0; i < peers.size(); ++i) {
            RebuildVideoChainInternal(m_slots[(size_t)peers[i].index].hwnd, peers[i].index, false);
        }
        MessageBoxW(owner, L"Failed to start video preview.", L"hwscapture", MB_ICONERROR);
        return false;
    }

    ps->active = true;
    if (ps->hwnd) InvalidateRect(ps->hwnd, NULL, TRUE);

    if (FindVideoSourceOwnerSlot(ps->config.deviceIndex) < 0) {
        m_videoSourceOwnerSlotByDevice[ps->config.deviceIndex] = slotIndex;
    }

    for (size_t i = 0; i < peers.size(); ++i) {
        RebuildVideoChainInternal(m_slots[(size_t)peers[i].index].hwnd, peers[i].index, false);
    }
    RebuildVideoSourceOwnerMap();
    return true;
}

bool CaptureManager::AddVideoSource(HWND owner, int slotIndex, const HWS_VIDEO_INPUT_CONFIG& cfg)
{
    Slot* ps = GetSlot(slotIndex); if (!ps) return false;

    if (ps->active && ps->videoIn &&
        ps->config.deviceIndex == cfg.deviceIndex &&
        ps->config.width == cfg.width &&
        ps->config.height == cfg.height &&
        ps->config.fps == cfg.fps &&
        ps->config.pixelFormat == cfg.pixelFormat &&
        ps->config.colorSpace == cfg.colorSpace &&
        ps->config.colorRange == cfg.colorRange &&
        ps->config.bufferCount == cfg.bufferCount) {
        if (ps->hwnd) InvalidateRect(ps->hwnd, NULL, TRUE);
        return true;
    }

    ps->config = cfg;
    return RebuildVideoChain(owner, slotIndex);
}

bool CaptureManager::AddDefaultVideoSource(HWND owner, int slotIndex)
{
    int count = HWS_GetVideoDeviceCount();
    if (count <= 0) { MessageBoxW(owner, L"No video capture device found.", L"hwscapture", MB_ICONINFORMATION); return false; }
    int caps = HWS_GetVideoCapabilityCount(0);
    HWS_VIDEO_CAP cap = {};
    bool found = false;
    for (int i = 0; i < caps; ++i) {
        if (HWS_GetVideoCapability(0, i, &cap) == HWS_OK && cap.width > 0 && cap.height > 0) { found = true; break; }
    }
    HWS_VIDEO_INPUT_CONFIG cfg = {};
    cfg.deviceIndex = 0;
    cfg.width = found ? cap.width : 1280;
    cfg.height = found ? cap.height : 720;
    cfg.fps = found && cap.fpsDen ? cap.fpsNum / cap.fpsDen : 30;
    cfg.pixelFormat = found ? cap.pixelFormat : HWS_PIXFMT_YUY2;
    cfg.colorSpace = HWS_COLOR_SPACE_AUTO;
    cfg.colorRange = HWS_COLOR_RANGE_AUTO;
    cfg.bufferCount = 4;
    return AddVideoSource(owner, slotIndex, cfg);
}

bool CaptureManager::AddAudioSource(HWND owner, int slotIndex, int deviceIndex)
{
    Slot* ps = GetSlot(slotIndex); if (!ps) return false;
    if (ps->audioIn) { DisableAudioMonitor(slotIndex); HWS_Stop(ps->audioIn); HWS_DestroyPort(ps->audioIn); ps->audioIn = NULL; }
    ps->audioIn = HWS_CreateAudioInput();
    if (!ps->audioIn) return false;
    HWS_SetAudioInputDevice(ps->audioIn, deviceIndex);
    HWS_SetAudioInputPCM(ps->audioIn, 48000, 2, 16);
    if (HWS_Start(ps->audioIn) != HWS_OK) { HWS_DestroyPort(ps->audioIn); ps->audioIn = NULL; MessageBoxW(owner, L"Failed to start audio input.", L"hwscapture", MB_ICONERROR); return false; }
    return true;
}

bool CaptureManager::StartRecorder(HWND owner, int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->active || !s->videoIn) { MessageBoxW(owner, L"Please add video source first.", L"hwscapture", MB_ICONINFORMATION); return false; }
    if (s->recording) return true;
    wchar_t path[MAX_PATH] = L"record.mp4";
    OPENFILENAMEW ofn = {};
    ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = owner; ofn.lpstrFile = path; ofn.nMaxFile = MAX_PATH;
    ofn.lpstrFilter = L"MP4 Files (*.mp4)\0*.mp4\0All Files (*.*)\0*.*\0"; ofn.lpstrDefExt = L"mp4"; ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST;
    if (!GetSaveFileNameW(&ofn)) return false;
    s->recorder = HWS_CreateRecorder(NULL);
    if (!s->recorder) return false;
    HWS_RECORDER_CONFIG cfg = {};
    wcscpy_s(cfg.filePath, path);
    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 = 60; cfg.videoBitrate = 8000000; cfg.enableAudio = s->audioIn ? 1 : 0;
    HWS_SetRecorderConfig(s->recorder, &cfg);
    if (m_recVideoCb) HWS_SetRecorderVideoCallback(s->recorder, RecorderVideoCb, (void*)(intptr_t)slotIndex);
    if (m_recAudioCb) HWS_SetRecorderAudioCallback(s->recorder, RecorderAudioCb, (void*)(intptr_t)slotIndex);
    if (s->osd) HWS_ConnectVideoOsdOutput(s->osd, HWS_OSD_OUTPUT_OSD, s->recorder);
    else if (s->crop) HWS_Connect(s->crop, s->recorder);
    else HWS_Connect(s->videoIn, s->recorder);
    if (s->audioIn) HWS_Connect(s->audioIn, s->recorder);
    if (HWS_Start(s->recorder) != HWS_OK) { HWS_DestroyPort(s->recorder); s->recorder = NULL; MessageBoxW(owner, L"Failed to start recorder.", L"hwscapture", MB_ICONERROR); return false; }
    s->recording = true; wcscpy_s(s->recordPath, path); return true;
}

void CaptureManager::StopRecorder(int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->recorder) return;
    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;
}

std::wstring CaptureManager::MakeSnapshotFileName(int slotIndex, HWS_IMAGE_FORMAT fmt) const
{
    const Slot& s = m_slots[(size_t)slotIndex];
    SYSTEMTIME st; GetLocalTime(&st);
    wchar_t file[MAX_PATH];
    swprintf_s(file, L"%s\\ch%d_%04d%02d%02d_%02d%02d%02d.%s", s.snapshotDir, slotIndex,
        st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, fmt == HWS_IMAGE_FORMAT_JPG ? L"jpg" : L"bmp");
    return file;
}

bool CaptureManager::CaptureSnapshot(HWND owner, int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->render) return false;
    std::wstring file = MakeSnapshotFileName(slotIndex, s->snapshotFormat);
    HWS_RESULT r = HWS_SaveVideoRenderSnapshot(s->render, file.c_str(), s->snapshotFormat);
    if (r != HWS_OK) { MessageBoxW(owner, L"Failed to capture snapshot.", L"hwscapture", MB_ICONERROR); return false; }
    return true;
}

void CaptureManager::ConfigureSnapshot(HWND owner, int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s) return;
    wchar_t path[MAX_PATH] = L"snapshot.bmp";
    OPENFILENAMEW ofn = {}; ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = owner; ofn.lpstrFile = path; ofn.nMaxFile = MAX_PATH;
    ofn.lpstrFilter = L"Bitmap (*.bmp)\0*.bmp\0JPEG (*.jpg)\0*.jpg\0"; ofn.lpstrDefExt = L"bmp"; ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST;
    if (GetSaveFileNameW(&ofn)) {
        wcscpy_s(s->snapshotDir, path); PathRemoveFileSpecW(s->snapshotDir);
        const wchar_t* ext = PathFindExtensionW(path);
        s->snapshotFormat = (_wcsicmp(ext, L".jpg") == 0 || _wcsicmp(ext, L".jpeg") == 0) ? HWS_IMAGE_FORMAT_JPG : HWS_IMAGE_FORMAT_BMP;
    }
}

void CaptureManager::EnsureOsd(Slot& s)
{
    // v4.0.2: VideoOsd is created when video preview starts and remains in the
    // preview chain as a transparent pass-through module. Enabling OSD must not
    // rebuild/reopen VideoInput, otherwise another preview slot sharing the
    // same physical device may be stopped.
    s.useOsd = true;
}

bool CaptureManager::AddTextOsd(int slotIndex, const wchar_t* text)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->active) return false;
    EnsureOsd(*s); if (!s->osd) return false;
    int id = 1000 + (int)s->osdIds.size();
    HWS_OSD_ITEM item = {}; item.id = id; item.enable = 1; item.type = HWS_OSD_ITEM_TEXT; item.x = 20; item.y = 20 + (int)s->osdIds.size() * 50; item.width = 600; item.height = 45; item.opacity = 0.9f; item.fontSize = 28; item.color = 0xFFFFFFFF; item.backgroundColor = 0x80000000; wcscpy_s(item.fontName, L"Microsoft YaHei"); wcscpy_s(item.text, text ? text : L"OSD Text");
    if (HWS_AddVideoOsdText(s->osd, &item) == HWS_OK) { s->osdIds.push_back(id); s->osdItems.push_back(item); return true; }
    return false;
}

bool CaptureManager::AddOsdItem(int slotIndex, HWS_OSD_ITEM item)
{
    Slot* s = GetSlot(slotIndex);
    if (!s || !s->active) return false;
    EnsureOsd(*s);
    if (!s->osd) return false;
    if (item.id <= 0) item.id = 3000 + (int)s->osdIds.size();
    item.enable = 1;
    HWS_RESULT r = HWS_OK;
    if (item.type == HWS_OSD_ITEM_IMAGE || item.filePath[0]) {
        item.type = HWS_OSD_ITEM_IMAGE;
        r = HWS_AddVideoOsdImage(s->osd, &item);
    } else {
        item.type = HWS_OSD_ITEM_TEXT;
        r = HWS_AddVideoOsdText(s->osd, &item);
    }
    if (r == HWS_OK) {
        s->osdIds.push_back(item.id);
        s->osdItems.push_back(item);
        return true;
    }
    return false;
}

bool CaptureManager::AddDateTimeOsd(int slotIndex)
{
    Slot* s = GetSlot(slotIndex);
    if (!s || !s->active) return false;

    EnsureOsd(*s);
    if (!s->osd) return false;

    if (!s->dateTimeOsd) {
        s->dateTimeOsd = true;
        s->dateTimeOsdId = 9000 + slotIndex;

        HWS_OSD_ITEM item = {};
        item.id = s->dateTimeOsdId;
        item.enable = 1;
        item.type = HWS_OSD_ITEM_TEXT;
        item.x = 20;
        item.y = 20;
        item.width = 520;
        item.height = 60;
        item.opacity = 0.9f;
        item.fontSize = 28;
        item.color = 0xFFFFFFFF;
        item.backgroundColor = 0x80000000;
        wcscpy_s(item.fontName, L"Microsoft YaHei");
        SYSTEMTIME st;
        GetLocalTime(&st);

        swprintf_s(
            item.text,
            _countof(item.text),
            L"%04d-%02d-%02d %02d:%02d:%02d",
            st.wYear,
            st.wMonth,
            st.wDay,
            st.wHour,
            st.wMinute,
            st.wSecond);

        if (HWS_AddVideoOsdText(s->osd, &item) != HWS_OK) {
            s->dateTimeOsd = false;
            return false;
        }

        bool exists = false;
        for (size_t i = 0; i < s->osdIds.size(); ++i) {
            if (s->osdIds[i] == item.id) { exists = true; break; }
        }
        if (!exists) { s->osdIds.push_back(item.id); s->osdItems.push_back(item); }
    }

    UpdateDateTimeOsd();
    return true;
}

bool CaptureManager::AddImageOsd(HWND owner, int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->active) return false;
    wchar_t path[MAX_PATH] = L""; OPENFILENAMEW ofn = {}; ofn.lStructSize = sizeof(ofn); ofn.hwndOwner = owner; ofn.lpstrFile = path; ofn.nMaxFile = MAX_PATH; ofn.lpstrFilter = L"Bitmap (*.bmp)\0*.bmp\0All Files (*.*)\0*.*\0"; ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
    if (!GetOpenFileNameW(&ofn)) return false;
    EnsureOsd(*s); if (!s->osd) return false;
    int id = 2000 + (int)s->osdIds.size();
    HWS_OSD_ITEM item = {}; item.id = id; item.enable = 1; item.type = HWS_OSD_ITEM_IMAGE; item.x = 20; item.y = 80; item.width = 160; item.height = 80; item.opacity = 0.8f; wcscpy_s(item.filePath, path);
    if (HWS_AddVideoOsdImage(s->osd, &item) == HWS_OK) { s->osdIds.push_back(id); s->osdItems.push_back(item); return true; }
    return false;
}

void CaptureManager::UpdateDateTimeOsd()
{
    SYSTEMTIME st; GetLocalTime(&st); wchar_t text[128];
    swprintf_s(text, L"%04d-%02d-%02d %02d:%02d:%02d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
    for (size_t i = 0; i < m_slots.size(); ++i) if (m_slots[i].dateTimeOsd && m_slots[i].osd) HWS_UpdateVideoOsdText(m_slots[i].osd, m_slots[i].dateTimeOsdId, text);
}

void CaptureManager::RemoveLastOsd(int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->osd || s->osdIds.empty()) return;
    int itemId = s->osdIds.back();
    HWS_RemoveVideoOsdItem(s->osd, itemId);
    s->osdIds.pop_back();
    for (std::vector<HWS_OSD_ITEM>::iterator it = s->osdItems.begin(); it != s->osdItems.end(); ++it) {
        if (it->id == itemId) { s->osdItems.erase(it); break; }
    }
    if (itemId == s->dateTimeOsdId) {
        s->dateTimeOsd = false;
    }
}
void CaptureManager::ClearOsd(int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->osd) return;
    HWS_ClearVideoOsd(s->osd); s->osdIds.clear(); s->osdItems.clear(); s->dateTimeOsd = false;
}

bool CaptureManager::SetCrop(int slotIndex, const HWS_VIDEO_CROP_RECT& rc)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->active) return false;
    s->cropRect = rc; s->useCrop = true; return RebuildVideoChain(s->hwnd, slotIndex);
}
void CaptureManager::RemoveCrop(int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->active) return; s->useCrop = false; RebuildVideoChain(s->hwnd, slotIndex);
}

bool CaptureManager::EnableAudioMonitor(int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->audioIn) return false;
    if (!m_audioMonitorMux) m_audioMonitorMux = HWS_CreateAudioMux();
    if (!m_audioMonitorRender) { m_audioMonitorRender = HWS_CreateAudioRender(); HWS_SetAudioRenderDefaultDevice(m_audioMonitorRender); HWS_Connect(m_audioMonitorMux, m_audioMonitorRender); HWS_Start(m_audioMonitorRender); }
    if (!s->audioMonitorEnabled) { HWS_Connect(s->audioIn, m_audioMonitorMux); s->audioMonitorEnabled = true; ++m_audioMonitorCount; }
    return true;
}
void CaptureManager::DisableAudioMonitor(int slotIndex)
{
    Slot* s = GetSlot(slotIndex); if (!s || !s->audioMonitorEnabled) return;
    if (s->audioIn && m_audioMonitorMux) HWS_Disconnect(s->audioIn, m_audioMonitorMux);
    s->audioMonitorEnabled = false; if (m_audioMonitorCount > 0) --m_audioMonitorCount;
    if (m_audioMonitorCount <= 0) {
        if (m_audioMonitorRender) { HWS_Stop(m_audioMonitorRender); HWS_DestroyPort(m_audioMonitorRender); m_audioMonitorRender = NULL; }
        if (m_audioMonitorMux) { HWS_DestroyPort(m_audioMonitorMux); m_audioMonitorMux = NULL; }
    }
}
