#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif

#include <windows.h>
#include <objbase.h>      // CoInitializeEx / CoUninitialize / COINIT_MULTITHREADED
#include <windowsx.h>
#include <commdlg.h>
#include <stdio.h>
#include <time.h>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <string>
#include <sstream>
#include <fstream>
#pragma warning(push)
#pragma warning(disable: 4996) // sphelper.h uses deprecated GetVersionExW in some Windows SDKs.
#include <sapi.h>
#include <sphelper.h>
#pragma warning(pop)
#include <thread>
#include <atomic>
#include "../include/hwscapture.h"
#include "AiInputPort.h"
#include "AiOutputPort.h"
#include "AiAudioInputPort.h"
#include "AiFrameConverter.h"

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "comdlg32.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "sapi.lib")

#define IDC_VIDEO_DEVICE       1001
#define IDC_VIDEO_FORMAT       1002
#define IDC_AUDIO_DEVICE       1003
#define IDC_AI_ENDPOINT        1004
#define IDC_AI_FPS             1005
#define IDC_START_PREVIEW      1101
#define IDC_STOP_PREVIEW       1102
#define IDC_START_AI           1103
#define IDC_STOP_AI            1104
#define IDC_SNAPSHOT           1105
#define IDC_REC_START          1106
#define IDC_REC_STOP           1107
#define IDC_INPUT_AUDIO_PLAY   1108
#define IDC_OUTPUT_AUDIO_PLAY  1111
#define IDC_DEMO_MODE          1112
#define IDC_VIEW_MODE          1109
#define IDC_CLEAR_LOG          1110
#define IDC_AI_TRANSPORT       1113
#define IDC_ORIGINAL_VIEW      1301
#define IDC_AI_VIEW            1302
#define IDC_LOG_EDIT           1201
#define WM_HWS_AI_RESULT       (WM_APP + 101)

struct FormatItem
{
    int width = 1280;
    int height = 720;
    int fps = 30;
    HWS_PIXEL_FORMAT fmt = HWS_PIXFMT_RGB32;
};

enum DemoViewMode
{
    DEMO_VIEW_SPLIT = 0,
    DEMO_VIEW_ORIGINAL_ONLY = 1,
    DEMO_VIEW_AI_ONLY = 2
};

enum DemoAiMode
{
    DEMO_AI_MODE_YOLO = 0,
    DEMO_AI_MODE_ASR_EN_ZH = 1
};

struct DemoApp
{
    HWND hwnd = NULL;
    HWND videoCombo = NULL;
    HWND formatCombo = NULL;
    HWND audioCombo = NULL;
    HWND demoModeCombo = NULL;
    HWND inputAudioPlayButton = NULL;
    HWND outputAudioPlayButton = NULL;
    HWND viewModeButton = NULL;
    HWND clearLogButton = NULL;
    HWND endpointEdit = NULL;
    HWND transportCombo = NULL;
    HWND aiFpsEdit = NULL;
    HWND originalWnd = NULL;
    HWND aiWnd = NULL;
    HWND logEdit = NULL;

    HWS_PORT videoIn = NULL;
    HWS_PORT renderOriginal = NULL;
    HWS_PORT renderAi = NULL;
    HWS_PORT osdAi = NULL;
    HWS_PORT audioIn = NULL;
    HWS_PORT audioRender = NULL;
    HWS_PORT recorder = NULL;

    bool previewRunning = false;
    bool aiRunning = false;
    bool recording = false;
    bool inputAudioPlayback = false;
    bool outputAudioPlayback = false;
    DemoViewMode viewMode = DEMO_VIEW_SPLIT;
    DemoAiMode demoMode = DEMO_AI_MODE_YOLO;
    std::wstring lastSpokenTranslation;
    HWS_VIDEO_INPUT_CONFIG currentVideoConfig = {};
    int currentAudioDevice = -1;
    wchar_t recordPath[MAX_PATH] = {};

    std::vector<std::string> videoNames;
    std::vector<std::string> audioNames;
    std::vector<FormatItem> formats;
    AiInputPort aiInput;
    AiAudioInputPort audioAiInput;
    AiOutputPort aiOutput;
};

static DemoApp g_app;
static std::atomic<unsigned long> g_speechGeneration(0);

static void LayoutControls(HWND hwnd);

static bool IsRenderableFormat(HWS_PIXEL_FORMAT fmt)
{
    return fmt == HWS_PIXFMT_YUY2 || fmt == HWS_PIXFMT_NV12 || fmt == HWS_PIXFMT_RGB24 ||
           fmt == HWS_PIXFMT_RGB32 || fmt == HWS_PIXFMT_MJPG;
}

static std::wstring FormatTimeFileName(const wchar_t* prefix, const wchar_t* ext)
{
    SYSTEMTIME st = {};
    GetLocalTime(&st);
    wchar_t buf[MAX_PATH] = {};
    swprintf_s(buf, L"%s_%04d%02d%02d_%02d%02d%02d.%s",
               prefix,
               st.wYear, st.wMonth, st.wDay,
               st.wHour, st.wMinute, st.wSecond,
               ext);
    return buf;
}

static void AppendLog(const wchar_t* text)
{
    if (!g_app.logEdit || !text) return;
    SYSTEMTIME st = {};
    GetLocalTime(&st);
    wchar_t line[2048] = {};
    swprintf_s(line, L"[%02d:%02d:%02d] %s\r\n", st.wHour, st.wMinute, st.wSecond, text);
    int len = GetWindowTextLengthW(g_app.logEdit);
    SendMessageW(g_app.logEdit, EM_SETSEL, (WPARAM)len, (LPARAM)len);
    SendMessageW(g_app.logEdit, EM_REPLACESEL, FALSE, (LPARAM)line);
}

static void AppendLogUtf8(const std::string& text)
{
    AppendLog(AiUtf8ToWide(text).c_str());
}

static void ClearLogWindow()
{
    if (!g_app.logEdit) return;
    SetWindowTextW(g_app.logEdit, L"");
}

static bool AiContainsCjkText(const std::wstring& text)
{
    for (wchar_t ch : text) {
        unsigned int c = (unsigned int)ch;
        if ((c >= 0x4E00 && c <= 0x9FFF) ||
            (c >= 0x3400 && c <= 0x4DBF) ||
            (c >= 0xF900 && c <= 0xFAFF)) {
            return true;
        }
    }
    return false;
}

static bool AiContainsWideSequence(const std::wstring& text, const wchar_t* seq)
{
    if (!seq || !seq[0] || text.empty()) return false;
    return text.find(seq) != std::wstring::npos;
}

static bool AiIsDiagnosticTranslationText(const std::wstring& text)
{
    if (text.empty()) return false;

    // These strings are generated by the demo/service only to explain a
    // translation setup problem. They must never be spoken as translated audio.
    // Keep this source file ASCII-only: do not place Chinese characters directly
    // in string literals, otherwise VS codepage settings may report C2001/C2070.
    const wchar_t* asciiMarkers[] = {
        L"translation is not Chinese",
        L"diagnostic/mock",
        L"translator",
        L"fallback",
        L"mock"
    };

    for (int i = 0; i < (int)(sizeof(asciiMarkers) / sizeof(asciiMarkers[0])); ++i) {
        if (AiContainsWideSequence(text, asciiMarkers[i])) {
            return true;
        }
    }

    // [mock Chinese translation]
    const wchar_t markerMockZh[] = {
        L'[', 0x6A21, 0x62DF, 0x4E2D, 0x6587, 0x7FFB, 0x8BD1, L']', 0
    };
    // [translation did not output Chinese...]
    const wchar_t markerNoZh[] = {
        L'[', 0x7FFB, 0x8BD1, 0x672A, 0x8F93, 0x51FA, 0x4E2D, 0x6587, 0
    };
    // [translation model...]
    const wchar_t markerModel[] = {
        L'[', 0x7FFB, 0x8BD1, 0x6A21, 0x578B, 0
    };

    if (AiContainsWideSequence(text, markerMockZh)) return true;
    if (AiContainsWideSequence(text, markerNoZh)) return true;
    if (AiContainsWideSequence(text, markerModel)) return true;

    return false;
}

static bool AiIsAsrEnToCnMode()
{
    return g_app.demoMode == DEMO_AI_MODE_ASR_EN_ZH;
}

static bool AiSelectChineseSapiVoice(ISpVoice* voice)
{
    if (!voice) return false;

    // SAPI language attribute uses Windows LANGID hex. Chinese Simplified is 804.
    // Try a few common Chinese LANGIDs before falling back to the system default.
    const wchar_t* candidates[] = {
        L"Language=804",  // zh-CN
        L"Language=1004", // zh-SG
        L"Language=404",  // zh-TW
        L"Language=C04",  // zh-HK
        L"Language=1404"  // zh-MO
    };

    for (int i = 0; i < (int)(sizeof(candidates) / sizeof(candidates[0])); ++i) {
        ISpObjectToken* token = NULL;
        HRESULT hr = SpFindBestToken(SPCAT_VOICES, candidates[i], NULL, &token);
        if (SUCCEEDED(hr) && token) {
            hr = voice->SetVoice(token);
            token->Release();
            if (SUCCEEDED(hr)) return true;
        }
    }
    return false;
}

static void SpeakTranslatedTextAsync(const std::wstring& text)
{
    if (text.empty()) return;
    const unsigned long myGeneration = ++g_speechGeneration;
    std::thread([text, myGeneration]() {
        HRESULT cohr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
        bool needCoUninit = SUCCEEDED(cohr);
        ISpVoice* voice = NULL;
        HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void**)&voice);
        if (SUCCEEDED(hr) && voice) {
            voice->SetVolume(100);
            voice->SetRate(0);
            if (AiContainsCjkText(text)) {
                AiSelectChineseSapiVoice(voice);
            }
            voice->Speak(text.c_str(), SPF_ASYNC | SPF_PURGEBEFORESPEAK, NULL);
            while (voice->WaitUntilDone(80) == S_FALSE) {
                if (g_speechGeneration.load() != myGeneration) {
                    voice->Speak(L"", SPF_PURGEBEFORESPEAK, NULL);
                    break;
                }
            }
            voice->Release();
        }
        if (needCoUninit) CoUninitialize();
    }).detach();
}

static const wchar_t* ViewModeName(DemoViewMode mode)
{
    switch (mode) {
    case DEMO_VIEW_ORIGINAL_ONLY: return L"Original";
    case DEMO_VIEW_AI_ONLY:       return L"AI Result";
    case DEMO_VIEW_SPLIT:
    default:                      return L"Split";
    }
}

static void UpdateViewModeButtonText()
{
    if (!g_app.viewModeButton) return;
    wchar_t text[64] = {};
    swprintf_s(text, L"View: %s", ViewModeName(g_app.viewMode));
    SetWindowTextW(g_app.viewModeButton, text);
}

static void SetViewMode(DemoViewMode mode, bool writeLog)
{
    g_app.viewMode = mode;
    UpdateViewModeButtonText();
    if (g_app.hwnd) {
        LayoutControls(g_app.hwnd);
    }
    if (writeLog) {
        wchar_t log[128] = {};
        swprintf_s(log, L"Video view mode changed: %s. Double-click a video window to enter/leave single-view mode.", ViewModeName(mode));
        AppendLog(log);
    }
}

static void CycleViewMode()
{
    if (g_app.viewMode == DEMO_VIEW_SPLIT) {
        SetViewMode(DEMO_VIEW_AI_ONLY, true);
    } else if (g_app.viewMode == DEMO_VIEW_AI_ONLY) {
        SetViewMode(DEMO_VIEW_ORIGINAL_ONLY, true);
    } else {
        SetViewMode(DEMO_VIEW_SPLIT, true);
    }
}

static void ToggleSingleViewFromPane(bool aiPane)
{
    if (g_app.viewMode == DEMO_VIEW_SPLIT) {
        SetViewMode(aiPane ? DEMO_VIEW_AI_ONLY : DEMO_VIEW_ORIGINAL_ONLY, true);
    } else {
        SetViewMode(DEMO_VIEW_SPLIT, true);
    }
}


static DemoAiMode GetSelectedDemoAiMode()
{
    if (!g_app.demoModeCombo) return g_app.demoMode;
    int sel = (int)SendMessageW(g_app.demoModeCombo, CB_GETCURSEL, 0, 0);
    return (sel == 1) ? DEMO_AI_MODE_ASR_EN_ZH : DEMO_AI_MODE_YOLO;
}

static const wchar_t* DemoAiModeName(DemoAiMode mode)
{
    return (mode == DEMO_AI_MODE_ASR_EN_ZH) ? L"ASR EN->CN" : L"YOLO Objects";
}

static const wchar_t* AiTransportName(AiTransportType transport)
{
    switch (transport) {
    case AI_TRANSPORT_HTTP:          return L"HTTP";
    case AI_TRANSPORT_WEBSOCKET:     return L"WS";
    case AI_TRANSPORT_SHARED_MEMORY: return L"Shared Memory";
    default:                         return L"Unknown";
    }
}

static std::wstring DefaultEndpointForTransport(AiTransportType transport)
{
    switch (transport) {
    case AI_TRANSPORT_HTTP:
        return L"http://127.0.0.1:8765/v1/analyze_frame";
    case AI_TRANSPORT_SHARED_MEMORY:
        return L"shm://hws_ai_demo";
    case AI_TRANSPORT_WEBSOCKET:
    default:
        return L"ws://127.0.0.1:8766/v1/ai/stream";
    }
}

static AiTransportType GetSelectedAiTransport()
{
    if (!g_app.transportCombo) return AI_TRANSPORT_WEBSOCKET;
    int sel = (int)SendMessageW(g_app.transportCombo, CB_GETCURSEL, 0, 0);
    if (sel == 1) return AI_TRANSPORT_HTTP;
    if (sel == 2) return AI_TRANSPORT_SHARED_MEMORY;
    return AI_TRANSPORT_WEBSOCKET;
}

static void ApplyAiTransport(AiTransportType transport, bool updateEndpoint, bool writeLog)
{
    if (g_app.transportCombo) {
        int sel = 0;
        if (transport == AI_TRANSPORT_HTTP) sel = 1;
        else if (transport == AI_TRANSPORT_SHARED_MEMORY) sel = 2;
        SendMessageW(g_app.transportCombo, CB_SETCURSEL, sel, 0);
    }

    if (updateEndpoint && g_app.endpointEdit) {
        std::wstring endpoint = DefaultEndpointForTransport(transport);
        SetWindowTextW(g_app.endpointEdit, endpoint.c_str());
    }

    if (writeLog) {
        std::wstring log = L"AI transport changed: ";
        log += AiTransportName(transport);
        if (transport == AI_TRANSPORT_SHARED_MEMORY) {
            log += L" reserved for later version.";
        }
        AppendLog(log.c_str());
    }
}

static void ApplyDemoAiMode(DemoAiMode mode, bool writeLog)
{
    g_app.demoMode = mode;
    if (mode == DEMO_AI_MODE_ASR_EN_ZH) {
        g_app.aiOutput.SetVideoOverlayEnabled(false);
        g_app.aiOutput.SetSubtitleText(L"ASR EN->CN: waiting for English speech...");
    } else {
        g_app.aiOutput.SetVideoOverlayEnabled(true);
        g_app.aiOutput.SetSubtitleText(L"");
    }
    if (writeLog) {
        std::wstring log = L"AI demo mode changed: ";
        log += DemoAiModeName(mode);
        if (mode == DEMO_AI_MODE_ASR_EN_ZH) {
            log += L". YOLO video OSD is disabled; subtitles remain enabled.";
        }
        AppendLog(log.c_str());
    }
}

static std::wstring GetWindowTextString(HWND hwnd)
{
    int len = GetWindowTextLengthW(hwnd);
    std::wstring out((size_t)len + 1, L'\0');
    GetWindowTextW(hwnd, &out[0], len + 1);
    if (!out.empty() && out.back() == L'\0') out.pop_back();
    return out;
}

static const char* FormatName(HWS_PIXEL_FORMAT fmt)
{
    return AiPixelFormatName(fmt);
}

static void AddComboString(HWND combo, const std::wstring& text)
{
    SendMessageW(combo, CB_ADDSTRING, 0, (LPARAM)text.c_str());
}

static void RefreshVideoFormats(int deviceIndex)
{
    g_app.formats.clear();
    SendMessageW(g_app.formatCombo, CB_RESETCONTENT, 0, 0);

    int count = HWS_GetVideoFormatCount(deviceIndex);
    for (int i = 0; i < count; ++i) {
        HWS_VIDEO_FORMAT fmt = {};
        if (HWS_GetVideoFormatInfo(deviceIndex, i, &fmt) != HWS_OK) continue;
        if (!IsRenderableFormat(fmt.pixelFormat)) continue;
        if (fmt.width <= 0 || fmt.height <= 0) continue;

        FormatItem item;
        item.width = fmt.width;
        item.height = fmt.height;
        item.fps = (fmt.fpsDen > 0) ? (fmt.fpsNum / fmt.fpsDen) : fmt.fpsNum;
        if (item.fps <= 0) item.fps = 30;
        item.fmt = fmt.pixelFormat;
        g_app.formats.push_back(item);

        wchar_t text[256] = {};
        swprintf_s(text, L"%dx%d %dfps %S", item.width, item.height, item.fps, FormatName(item.fmt));
        AddComboString(g_app.formatCombo, text);
    }

    if (g_app.formats.empty()) {
        FormatItem item;
        item.width = 1280;
        item.height = 720;
        item.fps = 30;
        item.fmt = HWS_PIXFMT_RGB32;
        g_app.formats.push_back(item);
        AddComboString(g_app.formatCombo, L"1280x720 30fps RGB32 (fallback)");
    }

    SendMessageW(g_app.formatCombo, CB_SETCURSEL, 0, 0);
}

static void RefreshDevices()
{
    SendMessageW(g_app.videoCombo, CB_RESETCONTENT, 0, 0);
    SendMessageW(g_app.audioCombo, CB_RESETCONTENT, 0, 0);
    g_app.videoNames.clear();
    g_app.audioNames.clear();

    HWS_RefreshDeviceList();

    int videoCount = HWS_GetVideoDeviceCount();
    for (int i = 0; i < videoCount; ++i) {
        HWS_DEVICE_INFO info = {};
        if (HWS_GetVideoDeviceInfo(i, &info) == HWS_OK) {
            g_app.videoNames.push_back(info.name);
            AddComboString(g_app.videoCombo, AiUtf8ToWide(info.name));
        }
    }
    if (videoCount > 0) SendMessageW(g_app.videoCombo, CB_SETCURSEL, 0, 0);

    g_app.audioNames.push_back("No audio");
    AddComboString(g_app.audioCombo, L"No audio");
    int audioCount = HWS_GetAudioInputDeviceCount();
    for (int i = 0; i < audioCount; ++i) {
        HWS_DEVICE_INFO info = {};
        if (HWS_GetAudioInputDeviceInfo(i, &info) == HWS_OK) {
            g_app.audioNames.push_back(info.name);
            AddComboString(g_app.audioCombo, AiUtf8ToWide(info.name));
        }
    }
    SendMessageW(g_app.audioCombo, CB_SETCURSEL, 0, 0);

    if (videoCount > 0) RefreshVideoFormats(0);
    else AppendLog(L"No video capture device found.");
}

static void __stdcall OnSdkVideoFrame(const HWS_VIDEO_FRAME* frame, void* userData)
{
    (void)userData;
    if (!frame) return;
    if (g_app.aiRunning && g_app.demoMode == DEMO_AI_MODE_YOLO) {
        g_app.aiInput.PushFrame(frame);
    }
}

static void __stdcall OnSdkAudioFrame(const HWS_AUDIO_FRAME* frame, void* userData)
{
    (void)userData;
    if (!frame) return;
    if (g_app.aiRunning && g_app.demoMode == DEMO_AI_MODE_ASR_EN_ZH) {
        g_app.audioAiInput.PushAudioFrame(frame);
    }
}

static bool CreateOrUpdateAiOsd()
{
    if (!g_app.osdAi) return false;
    g_app.aiOutput.SetOsdPort(g_app.osdAi);
    g_app.aiOutput.SetTextItemId(910001);
    g_app.aiOutput.SetVideoSize(g_app.currentVideoConfig.width > 0 ? g_app.currentVideoConfig.width : 1280,
                                g_app.currentVideoConfig.height > 0 ? g_app.currentVideoConfig.height : 720);
    g_app.aiOutput.EnsureDefaultTextItem();
    return true;
}

static void StopRecording()
{
    if (!g_app.recording) return;

    // The demo records the AI result video by default.
    // Therefore the recorder is connected to the OSD output of osdAi, not directly to videoIn.
    if (g_app.osdAi && g_app.recorder) HWS_Disconnect(g_app.osdAi, g_app.recorder);
    if (g_app.videoIn && g_app.recorder) HWS_Disconnect(g_app.videoIn, g_app.recorder); // Safe fallback cleanup.
    if (g_app.audioIn && g_app.recorder) HWS_Disconnect(g_app.audioIn, g_app.recorder);
    if (g_app.recorder) {
        HWS_Stop(g_app.recorder);
        HWS_DestroyPort(g_app.recorder);
        g_app.recorder = NULL;
    }
    g_app.recording = false;
    AppendLog(L"Recording stopped.");
}



static void StopInputAudioPlayback(bool writeLog);

static void StopOutputAudioPlayback(bool writeLog)
{
    ++g_speechGeneration;
    g_app.outputAudioPlayback = false;
    if (g_app.outputAudioPlayButton) SetWindowTextW(g_app.outputAudioPlayButton, L"Out Play");
    if (writeLog) AppendLog(L"Translated output voice playback disabled.");
}

static void StartOutputAudioPlayback()
{
    // Avoid mixing original input monitoring and translated output audio.
    if (g_app.inputAudioPlayback) {
        StopInputAudioPlayback(true);
    }
    g_app.outputAudioPlayback = true;
    g_app.lastSpokenTranslation.clear();
    if (g_app.outputAudioPlayButton) SetWindowTextW(g_app.outputAudioPlayButton, L"Out Mute");
    AppendLog(L"Translated output audio playback enabled. Input audio monitor is muted to avoid mixing.");
}

static void ToggleOutputAudioPlayback()
{
    if (g_app.outputAudioPlayback) {
        StopOutputAudioPlayback(true);
    } else {
        StartOutputAudioPlayback();
    }
}

static void PlayTranslatedOutputAudioIfEnabled(const AiDemoResult& result)
{
    if (!g_app.outputAudioPlayback) {
        return;
    }

    // Simplified customer demo path:
    // AI Service returns translated text only. The demo uses Windows SAPI as a
    // simple local system voice output path. No service-side speech/MP3/WAV payload
    // is requested or parsed.
    if (result.isFinal && !result.translationUtf8.empty()) {
        std::wstring text = AiUtf8ToWide(result.translationUtf8);

        if (AiIsAsrEnToCnMode()) {
            if (!AiContainsCjkText(text)) {
                AppendLog(L"Output voice skipped: translation is not Chinese. Check AI Service translator config.");
                return;
            }
            if (AiIsDiagnosticTranslationText(text)) {
                AppendLog(L"Output voice skipped: translation is diagnostic/mock text, not a real Chinese translation.");
                return;
            }
        }

        if (!text.empty() && text != g_app.lastSpokenTranslation) {
            g_app.lastSpokenTranslation = text;
            SpeakTranslatedTextAsync(text);
            AppendLog(AiIsAsrEnToCnMode() ?
                L"Output voice: speaking Chinese translation with Windows SAPI." :
                L"Output voice: speaking translated text with Windows SAPI.");
        }
        return;
    }

    if (AiIsAsrEnToCnMode() && result.isFinal && !result.textUtf8.empty()) {
        AppendLog(L"Output voice skipped: no Chinese translation field in AI result.");
    }
}

static void StopInputAudioPlayback(bool writeLog)
{
    if (!g_app.inputAudioPlayback && !g_app.audioRender) {
        if (g_app.inputAudioPlayButton) SetWindowTextW(g_app.inputAudioPlayButton, L"Input Play");
        return;
    }

    if (g_app.audioIn && g_app.audioRender) {
        HWS_Disconnect(g_app.audioIn, g_app.audioRender);
    }
    if (g_app.audioRender) {
        HWS_Stop(g_app.audioRender);
        HWS_DestroyPort(g_app.audioRender);
        g_app.audioRender = NULL;
    }
    g_app.inputAudioPlayback = false;
    if (g_app.inputAudioPlayButton) SetWindowTextW(g_app.inputAudioPlayButton, L"Input Play");
    if (writeLog) AppendLog(L"Input audio playback stopped.");
}

static bool StartInputAudioPlayback()
{
    if (g_app.inputAudioPlayback) return true;
    if (g_app.outputAudioPlayback) {
        StopOutputAudioPlayback(true);
    }

    if (!g_app.previewRunning) {
        AppendLog(L"Input audio playback requires preview/audio capture to be started first.");
        MessageBoxW(g_app.hwnd, L"Please start preview with an audio device selected first.", L"HWS AI Demo", MB_ICONWARNING);
        return false;
    }

    if (!g_app.audioIn) {
        AppendLog(L"Input audio playback failed: no audio input device selected.");
        MessageBoxW(g_app.hwnd, L"No audio input device is active. Please select an audio device and restart preview.", L"HWS AI Demo", MB_ICONWARNING);
        return false;
    }

    g_app.audioRender = HWS_CreateAudioRender();
    if (!g_app.audioRender) {
        AppendLog(L"Input audio playback failed: HWS_CreateAudioRender returned NULL.");
        return false;
    }

    HWS_RESULT r = HWS_SetAudioRenderDefaultDevice(g_app.audioRender);
    if (r != HWS_OK) {
        StopInputAudioPlayback(false);
        AppendLog(L"Input audio playback failed: cannot select default render device.");
        return false;
    }

    r = HWS_Connect(g_app.audioIn, g_app.audioRender);
    if (r != HWS_OK) {
        StopInputAudioPlayback(false);
        AppendLog(L"Input audio playback failed: cannot connect audio input to render.");
        return false;
    }

    r = HWS_Start(g_app.audioRender);
    if (r != HWS_OK) {
        StopInputAudioPlayback(false);
        AppendLog(L"Input audio playback failed: cannot start audio render.");
        return false;
    }

    g_app.inputAudioPlayback = true;
    if (g_app.inputAudioPlayButton) SetWindowTextW(g_app.inputAudioPlayButton, L"Input Mute");
    AppendLog(L"Input audio playback started. Click Input Mute to stop monitoring before translation audio playback.");
    return true;
}

static void ToggleInputAudioPlayback()
{
    if (g_app.inputAudioPlayback) {
        StopInputAudioPlayback(true);
    } else {
        StartInputAudioPlayback();
    }
}

static void StopPreview()
{
    StopRecording();

    if (g_app.aiRunning) {
        g_app.aiInput.Stop();
        g_app.audioAiInput.Stop();
        g_app.aiRunning = false;
    }

    StopInputAudioPlayback(false);
    StopOutputAudioPlayback(false);

    if (g_app.videoIn) {
        HWS_SetVideoFrameCallback(g_app.videoIn, NULL, NULL);
        HWS_Stop(g_app.videoIn);
        HWS_DestroyPort(g_app.videoIn);
        g_app.videoIn = NULL;
    }
    if (g_app.audioIn) {
        HWS_SetAudioFrameCallback(g_app.audioIn, NULL, NULL);
        HWS_Stop(g_app.audioIn);
        HWS_DestroyPort(g_app.audioIn);
        g_app.audioIn = NULL;
    }
    if (g_app.osdAi) {
        HWS_DestroyPort(g_app.osdAi);
        g_app.osdAi = NULL;
    }
    if (g_app.renderOriginal) {
        HWS_DestroyPort(g_app.renderOriginal);
        g_app.renderOriginal = NULL;
    }
    if (g_app.renderAi) {
        HWS_DestroyPort(g_app.renderAi);
        g_app.renderAi = NULL;
    }

    g_app.previewRunning = false;
    g_app.aiOutput.SetOsdPort(NULL);
    InvalidateRect(g_app.originalWnd, NULL, TRUE);
    InvalidateRect(g_app.aiWnd, NULL, TRUE);
    AppendLog(L"Preview stopped.");
}

static bool StartPreview()
{
    if (g_app.previewRunning) return true;

    int videoIndex = (int)SendMessageW(g_app.videoCombo, CB_GETCURSEL, 0, 0);
    int formatIndex = (int)SendMessageW(g_app.formatCombo, CB_GETCURSEL, 0, 0);
    int audioComboIndex = (int)SendMessageW(g_app.audioCombo, CB_GETCURSEL, 0, 0);
    if (videoIndex < 0 || videoIndex >= (int)g_app.videoNames.size()) {
        MessageBoxW(g_app.hwnd, L"Please select a video device.", L"HWS AI Demo", MB_ICONERROR);
        return false;
    }
    if (formatIndex < 0 || formatIndex >= (int)g_app.formats.size()) formatIndex = 0;

    const FormatItem& fmt = g_app.formats[(size_t)formatIndex];
    HWS_VIDEO_INPUT_CONFIG cfg = {};
    cfg.deviceIndex = videoIndex;
    cfg.width = fmt.width;
    cfg.height = fmt.height;
    cfg.fps = fmt.fps;
    cfg.pixelFormat = fmt.fmt;
    cfg.colorSpace = HWS_COLOR_SPACE_AUTO;
    cfg.colorRange = HWS_COLOR_RANGE_AUTO;
    cfg.bufferCount = 4;

    g_app.videoIn = HWS_CreateVideoInput();
    g_app.renderOriginal = HWS_CreateVideoRender(g_app.originalWnd);
    g_app.renderAi = HWS_CreateVideoRender(g_app.aiWnd);
    g_app.osdAi = HWS_CreateVideoOsd();
    if (!g_app.videoIn || !g_app.renderOriginal || !g_app.renderAi || !g_app.osdAi) {
        StopPreview();
        MessageBoxW(g_app.hwnd, L"Failed to create SDK video ports.", L"HWS AI Demo", MB_ICONERROR);
        return false;
    }

    HWS_SetVideoInputConfig(g_app.videoIn, &cfg);
    HWS_VIDEO_RENDER_CONFIG renderCfg = {};
    renderCfg.width = cfg.width;
    renderCfg.height = cfg.height;
    renderCfg.pixelFormat = cfg.pixelFormat;
    HWS_SetVideoRenderConfig(g_app.renderOriginal, &renderCfg);
    HWS_SetVideoRenderConfig(g_app.renderAi, &renderCfg);
    HWS_SetVideoRenderInterpolationMode(g_app.renderOriginal, HWS_RENDER_INTERPOLATION_AUTO);
    HWS_SetVideoRenderInterpolationMode(g_app.renderAi, HWS_RENDER_INTERPOLATION_AUTO);

    g_app.currentVideoConfig = cfg;

    HWS_SetVideoFrameCallback(g_app.videoIn, OnSdkVideoFrame, &g_app);

    // Left window: clean original preview. Right window: same video through OSD for AI result.
    HWS_Connect(g_app.videoIn, g_app.renderOriginal);
    HWS_Connect(g_app.videoIn, g_app.osdAi);
    HWS_ConnectVideoOsdOutput(g_app.osdAi, HWS_OSD_OUTPUT_OSD, g_app.renderAi);
    CreateOrUpdateAiOsd();
    ApplyDemoAiMode(GetSelectedDemoAiMode(), false);

    g_app.currentAudioDevice = -1;
    if (audioComboIndex > 0) {
        g_app.currentAudioDevice = audioComboIndex - 1;
        g_app.audioIn = HWS_CreateAudioInput();
        if (g_app.audioIn) {
            HWS_SetAudioInputDevice(g_app.audioIn, g_app.currentAudioDevice);
            HWS_SetAudioInputPCM(g_app.audioIn, 48000, 2, 16);
            HWS_SetAudioFrameCallback(g_app.audioIn, OnSdkAudioFrame, &g_app);
            HWS_Start(g_app.audioIn);
        }
    }

    HWS_RESULT r = HWS_Start(g_app.videoIn);
    if (r != HWS_OK) {
        StopPreview();
        MessageBoxW(g_app.hwnd, L"Failed to start video preview.", L"HWS AI Demo", MB_ICONERROR);
        return false;
    }

    g_app.previewRunning = true;

    wchar_t log[512] = {};
    swprintf_s(log, L"Preview started: device=%d %dx%d %dfps %S", videoIndex, cfg.width, cfg.height, cfg.fps, FormatName(cfg.pixelFormat));
    AppendLog(log);
    return true;
}

static void StartAi()
{
    if (g_app.aiRunning) return;
    if (!g_app.previewRunning && !StartPreview()) return;

    g_app.demoMode = GetSelectedDemoAiMode();
    ApplyDemoAiMode(g_app.demoMode, false);

    AiTransportType transport = GetSelectedAiTransport();
    std::wstring endpoint = GetWindowTextString(g_app.endpointEdit);
    if (endpoint.empty()) {
        endpoint = DefaultEndpointForTransport(transport);
        SetWindowTextW(g_app.endpointEdit, endpoint.c_str());
    }

    if (transport == AI_TRANSPORT_SHARED_MEMORY) {
        AppendLog(L"Shared Memory transport is reserved for a later version.");
        MessageBoxW(g_app.hwnd, L"Shared Memory transport is reserved for a later version. Please select WS or HTTP.", L"HWS AI Demo", MB_ICONINFORMATION);
        return;
    }

    // Audio ASR is a streaming function, so it requires WebSocket.
    // YOLO can use either WS or HTTP.
    if (g_app.demoMode == DEMO_AI_MODE_ASR_EN_ZH && transport != AI_TRANSPORT_WEBSOCKET) {
        AppendLog(L"ASR EN->CN requires WS transport. HTTP is for YOLO frame analysis only.");
        MessageBoxW(g_app.hwnd, L"ASR EN->CN requires WS transport. Please select WS.", L"HWS AI Demo", MB_ICONWARNING);
        return;
    }

    g_app.aiInput.SetResultCallback([](const AiDemoResult& result) {
        AiDemoResult* heapResult = new AiDemoResult(result);
        PostMessageW(g_app.hwnd, WM_HWS_AI_RESULT, 0, (LPARAM)heapResult);
    });
    g_app.audioAiInput.SetResultCallback([](const AiDemoResult& result) {
        AiDemoResult* heapResult = new AiDemoResult(result);
        PostMessageW(g_app.hwnd, WM_HWS_AI_RESULT, 0, (LPARAM)heapResult);
    });

    g_app.aiOutput.ClearDetections();
    g_app.aiOutput.ClearEvents();

    if (g_app.demoMode == DEMO_AI_MODE_YOLO) {
        AiInputConfig cfg;
        cfg.slot = 0;
        cfg.endpoint = endpoint;
        cfg.transport = transport;
        wchar_t fpsText[32] = {};
        GetWindowTextW(g_app.aiFpsEdit, fpsText, _countof(fpsText));
        cfg.targetFps = _wtoi(fpsText);
        if (cfg.targetFps <= 0) cfg.targetFps = 1;
        if (cfg.targetFps > 30) cfg.targetFps = 30;
        cfg.jpegQuality = 90;
        cfg.modeUtf8 = "yolo";
        cfg.promptUtf8 = "Detect multiple objects and generate event JSON when object counts change. Do not generate trajectories.";
        g_app.aiInput.Start(cfg);
        g_app.aiOutput.SetStatusText(L"YOLO: running, waiting for detection...");
        AppendLog(L"YOLO Objects demo started. Audio ASR stream is disabled in this mode.");
    } else {
        if (!g_app.audioIn) {
            AppendLog(L"ASR EN->CN demo requires an audio input device.");
            MessageBoxW(g_app.hwnd, L"Please select an audio input device before starting ASR EN->CN demo.", L"HWS AI Demo", MB_ICONWARNING);
            return;
        }
        AiAudioInputConfig audioCfg;
        audioCfg.audioSource = 0;
        audioCfg.endpoint = endpoint;
        audioCfg.targetSampleRate = 16000;
        audioCfg.channels = 1;
        audioCfg.bitsPerSample = 16;
        audioCfg.chunkMs = 200;
        audioCfg.srcLangUtf8 = "en";
        audioCfg.dstLangUtf8 = "zh";
        g_app.audioAiInput.Start(audioCfg);
        g_app.aiOutput.SetVideoOverlayEnabled(false);
        g_app.aiOutput.SetSubtitleText(L"ASR EN->CN: listening...");
        AppendLog(L"ASR EN->CN demo started. YOLO video detection OSD is disabled. Audio stream uses normalized PCM16 mono 16k / 200ms chunks.");
    }

    g_app.aiRunning = true;
    {
        std::wstring log = L"AI started with ";
        log += AiTransportName(transport);
        log += L" transport. Use ai_service\\start_ai_service_customer_demo.bat.";
        AppendLog(log.c_str());
    }
}

static void StopAi()
{
    if (!g_app.aiRunning) return;
    g_app.aiInput.Stop();
    g_app.audioAiInput.Stop();
    g_app.aiRunning = false;
    g_app.aiOutput.ClearDetections();
    g_app.aiOutput.ClearEvents();
    if (g_app.demoMode == DEMO_AI_MODE_ASR_EN_ZH) {
        g_app.aiOutput.SetVideoOverlayEnabled(false);
        g_app.aiOutput.SetSubtitleText(L"ASR EN->CN: stopped");
    } else {
        g_app.aiOutput.SetVideoOverlayEnabled(true);
        g_app.aiOutput.SetStatusText(L"AI: stopped");
        g_app.aiOutput.SetSubtitleText(L"");
    }
    AppendLog(L"AI stopped.");
}

static void CaptureSnapshot()
{
    if (!g_app.previewRunning || !g_app.renderAi) return;
    std::wstring path = FormatTimeFileName(L"ai_demo_snapshot", L"jpg");
    HWS_RESULT r = HWS_SaveVideoRenderSnapshot(g_app.renderAi, path.c_str(), HWS_IMAGE_FORMAT_JPG);
    if (r == HWS_OK) {
        std::wstring log = L"Snapshot saved: " + path;
        AppendLog(log.c_str());
    } else {
        AppendLog(L"Snapshot failed.");
    }
}

static void StartRecording()
{
    if (g_app.recording) return;
    if (!g_app.previewRunning && !StartPreview()) return;

    std::wstring pathW = FormatTimeFileName(L"ai_demo_ai_result_record", L"mp4");
    std::string pathA = AiWideToUtf8(pathW);
    g_app.recorder = HWS_CreateRecorder(pathA.c_str());
    if (!g_app.recorder) {
        AppendLog(L"Create recorder failed.");
        return;
    }

    HWS_RECORDER_CONFIG rc = {};
    wcsncpy_s(rc.filePath, pathW.c_str(), _TRUNCATE);
    rc.container = HWS_CONTAINER_MP4;
    rc.videoCodec = HWS_VIDEO_CODEC_H264;
    rc.width = g_app.currentVideoConfig.width;
    rc.height = g_app.currentVideoConfig.height;
    rc.fps = g_app.currentVideoConfig.fps > 0 ? g_app.currentVideoConfig.fps : 30;
    rc.recordFps = rc.fps;
    rc.videoBitrate = 8000000;
    rc.enableAudio = g_app.audioIn ? 1 : 0;
    rc.inputMode = HWS_RECORDER_INPUT_AUTO;
    rc.splitMode = HWS_RECORDER_SPLIT_NONE;
    HWS_SetRecorderConfig(g_app.recorder, &rc);

    // Record the AI result stream by default: video + OSD text/boxes from the right preview.
    // This makes the saved MP4 suitable for demo sharing. If osdAi is unavailable, fall back to raw videoIn.
    bool usingAiResultVideo = false;
    if (g_app.osdAi) {
        HWS_RESULT cr = HWS_ConnectVideoOsdOutput(g_app.osdAi, HWS_OSD_OUTPUT_OSD, g_app.recorder);
        usingAiResultVideo = (cr == HWS_OK);
    }
    if (!usingAiResultVideo) {
        HWS_Connect(g_app.videoIn, g_app.recorder);
    }
    if (g_app.audioIn) HWS_Connect(g_app.audioIn, g_app.recorder);

    HWS_RESULT r = HWS_Start(g_app.recorder);
    if (r != HWS_OK) {
        if (g_app.osdAi) HWS_Disconnect(g_app.osdAi, g_app.recorder);
        HWS_Disconnect(g_app.videoIn, g_app.recorder);
        if (g_app.audioIn) HWS_Disconnect(g_app.audioIn, g_app.recorder);
        HWS_DestroyPort(g_app.recorder);
        g_app.recorder = NULL;
        AppendLog(L"Start recorder failed.");
        return;
    }

    g_app.recording = true;
    wcsncpy_s(g_app.recordPath, pathW.c_str(), _TRUNCATE);
    std::wstring log = L"Recording started: " + pathW;
    log += usingAiResultVideo ? L" [AI result video]" : L" [raw input fallback]";
    AppendLog(log.c_str());
}

static void LayoutControls(HWND hwnd)
{
    RECT rc = {};
    GetClientRect(hwnd, &rc);
    int width = rc.right - rc.left;
    int height = rc.bottom - rc.top;
    int margin = 8;
    int topH = 126;
    int logH = 130;
    int gap = 8;

    int x = margin;
    int y = 8;
    int labelW = 88;
    int comboW = 260;
    int h = 24;

    SetWindowPos(g_app.videoCombo, NULL, x + labelW, y, comboW, 200, SWP_NOZORDER);
    x += labelW + comboW + 12;
    SetWindowPos(g_app.formatCombo, NULL, x + 54, y, 230, 200, SWP_NOZORDER);
    x += 54 + 230 + 12;
    SetWindowPos(g_app.audioCombo, NULL, x + 54, y, 220, 200, SWP_NOZORDER);
    SetWindowPos(g_app.inputAudioPlayButton, NULL, x + 54 + 226, y, 96, h, SWP_NOZORDER);
    SetWindowPos(g_app.outputAudioPlayButton, NULL, x + 54 + 328, y, 96, h, SWP_NOZORDER);

    x = margin;
    y = 42;
    SetWindowPos(g_app.endpointEdit, NULL, x + labelW, y, 360, h, SWP_NOZORDER);
    x += labelW + 360 + 12;
    SetWindowPos(g_app.transportCombo, NULL, x + 74, y, 126, 100, SWP_NOZORDER);
    x += 74 + 126 + 12;
    SetWindowPos(g_app.aiFpsEdit, NULL, x + 54, y, 50, h, SWP_NOZORDER);

    x += 54 + 60 + 8;
    HWND buttons[] = {
        GetDlgItem(hwnd, IDC_START_PREVIEW), GetDlgItem(hwnd, IDC_STOP_PREVIEW),
        GetDlgItem(hwnd, IDC_START_AI), GetDlgItem(hwnd, IDC_STOP_AI),
        GetDlgItem(hwnd, IDC_SNAPSHOT), GetDlgItem(hwnd, IDC_REC_START), GetDlgItem(hwnd, IDC_REC_STOP)
    };
    const int bw[] = { 80, 80, 70, 70, 80, 80, 80 };
    for (int i = 0; i < 7; ++i) {
        SetWindowPos(buttons[i], NULL, x, y, bw[i], h, SWP_NOZORDER);
        x += bw[i] + 6;
    }

    x = margin;
    y = 76;
    HWND demoLabel = GetDlgItem(hwnd, IDC_DEMO_MODE + 10000);
    SetWindowPos(demoLabel, NULL, x, y + 3, 52, 18, SWP_NOZORDER);
    SetWindowPos(g_app.demoModeCombo, NULL, x + 54, y, 170, 120, SWP_NOZORDER);
    x += 236;
    SetWindowPos(g_app.viewModeButton, NULL, x, y, 122, h, SWP_NOZORDER);
    x += 132;
    SetWindowPos(g_app.clearLogButton, NULL, x, y, 90, h, SWP_NOZORDER);

    int videoTop = topH;
    int videoBottom = height - logH - margin;
    int videoH = videoBottom - videoTop;
    if (videoH < 100) videoH = 100;
    int fullW = width - margin * 2;
    int paneW = (fullW - gap) / 2;

    if (g_app.viewMode == DEMO_VIEW_ORIGINAL_ONLY) {
        SetWindowPos(g_app.originalWnd, NULL, margin, videoTop, fullW, videoH, SWP_NOZORDER | SWP_SHOWWINDOW);
        SetWindowPos(g_app.aiWnd, NULL, margin, videoTop, fullW, videoH, SWP_NOZORDER | SWP_HIDEWINDOW);
    } else if (g_app.viewMode == DEMO_VIEW_AI_ONLY) {
        SetWindowPos(g_app.originalWnd, NULL, margin, videoTop, fullW, videoH, SWP_NOZORDER | SWP_HIDEWINDOW);
        SetWindowPos(g_app.aiWnd, NULL, margin, videoTop, fullW, videoH, SWP_NOZORDER | SWP_SHOWWINDOW);
    } else {
        SetWindowPos(g_app.originalWnd, NULL, margin, videoTop, paneW, videoH, SWP_NOZORDER | SWP_SHOWWINDOW);
        SetWindowPos(g_app.aiWnd, NULL, margin + paneW + gap, videoTop, paneW, videoH, SWP_NOZORDER | SWP_SHOWWINDOW);
    }

    SetWindowPos(g_app.logEdit, NULL, margin, height - logH, width - margin * 2, logH - margin, SWP_NOZORDER);
}

static void CreateUi(HWND hwnd)
{
    CreateWindowW(L"STATIC", L"Video:", WS_CHILD | WS_VISIBLE, 8, 10, 80, 22, hwnd, NULL, NULL, NULL);
    g_app.videoCombo = CreateWindowW(L"COMBOBOX", NULL, WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                     96, 8, 260, 200, hwnd, (HMENU)IDC_VIDEO_DEVICE, NULL, NULL);
    CreateWindowW(L"STATIC", L"Format:", WS_CHILD | WS_VISIBLE, 368, 10, 54, 22, hwnd, NULL, NULL, NULL);
    g_app.formatCombo = CreateWindowW(L"COMBOBOX", NULL, WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                      422, 8, 230, 200, hwnd, (HMENU)IDC_VIDEO_FORMAT, NULL, NULL);
    CreateWindowW(L"STATIC", L"Audio:", WS_CHILD | WS_VISIBLE, 664, 10, 54, 22, hwnd, NULL, NULL, NULL);
    g_app.audioCombo = CreateWindowW(L"COMBOBOX", NULL, WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                     718, 8, 220, 200, hwnd, (HMENU)IDC_AUDIO_DEVICE, NULL, NULL);
    g_app.inputAudioPlayButton = CreateWindowW(L"BUTTON", L"Input Play", WS_CHILD | WS_VISIBLE,
                                               944, 8, 96, 24, hwnd, (HMENU)IDC_INPUT_AUDIO_PLAY, NULL, NULL);
    g_app.outputAudioPlayButton = CreateWindowW(L"BUTTON", L"Out Play", WS_CHILD | WS_VISIBLE,
                                                1046, 8, 96, 24, hwnd, (HMENU)IDC_OUTPUT_AUDIO_PLAY, NULL, NULL);

    CreateWindowW(L"STATIC", L"AI URL:", WS_CHILD | WS_VISIBLE, 8, 44, 80, 22, hwnd, NULL, NULL, NULL);
    g_app.endpointEdit = CreateWindowW(L"EDIT", L"ws://127.0.0.1:8766/v1/ai/stream",
                                       WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
                                       96, 42, 360, 24, hwnd, (HMENU)IDC_AI_ENDPOINT, NULL, NULL);

    CreateWindowW(L"STATIC", L"Transport:", WS_CHILD | WS_VISIBLE, 466, 44, 74, 22, hwnd, NULL, NULL, NULL);
    g_app.transportCombo = CreateWindowW(L"COMBOBOX", NULL, WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                         540, 42, 126, 100, hwnd, (HMENU)IDC_AI_TRANSPORT, NULL, NULL);
    AddComboString(g_app.transportCombo, L"WS");
    AddComboString(g_app.transportCombo, L"HTTP");
    AddComboString(g_app.transportCombo, L"Shared Memory");
    SendMessageW(g_app.transportCombo, CB_SETCURSEL, 0, 0);

    CreateWindowW(L"STATIC", L"AI FPS:", WS_CHILD | WS_VISIBLE, 676, 44, 54, 22, hwnd, NULL, NULL, NULL);
    g_app.aiFpsEdit = CreateWindowW(L"EDIT", L"5", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_NUMBER,
                                    730, 42, 50, 24, hwnd, (HMENU)IDC_AI_FPS, NULL, NULL);

    CreateWindowW(L"BUTTON", L"Preview", WS_CHILD | WS_VISIBLE, 792, 42, 80, 24, hwnd, (HMENU)IDC_START_PREVIEW, NULL, NULL);
    CreateWindowW(L"BUTTON", L"Stop", WS_CHILD | WS_VISIBLE, 878, 42, 80, 24, hwnd, (HMENU)IDC_STOP_PREVIEW, NULL, NULL);
    CreateWindowW(L"BUTTON", L"AI Start", WS_CHILD | WS_VISIBLE, 964, 42, 70, 24, hwnd, (HMENU)IDC_START_AI, NULL, NULL);
    CreateWindowW(L"BUTTON", L"AI Stop", WS_CHILD | WS_VISIBLE, 1040, 42, 70, 24, hwnd, (HMENU)IDC_STOP_AI, NULL, NULL);
    CreateWindowW(L"BUTTON", L"Snapshot", WS_CHILD | WS_VISIBLE, 1116, 42, 80, 24, hwnd, (HMENU)IDC_SNAPSHOT, NULL, NULL);
    CreateWindowW(L"BUTTON", L"Rec Start", WS_CHILD | WS_VISIBLE, 1202, 42, 80, 24, hwnd, (HMENU)IDC_REC_START, NULL, NULL);
    CreateWindowW(L"BUTTON", L"Rec Stop", WS_CHILD | WS_VISIBLE, 1288, 42, 80, 24, hwnd, (HMENU)IDC_REC_STOP, NULL, NULL);
    CreateWindowW(L"STATIC", L"Demo:", WS_CHILD | WS_VISIBLE,
                  8, 79, 52, 18, hwnd, (HMENU)(IDC_DEMO_MODE + 10000), NULL, NULL);
    g_app.demoModeCombo = CreateWindowW(L"COMBOBOX", NULL, WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                        62, 76, 170, 120, hwnd, (HMENU)IDC_DEMO_MODE, NULL, NULL);
    AddComboString(g_app.demoModeCombo, L"YOLO Objects");
    AddComboString(g_app.demoModeCombo, L"ASR EN->CN");
    SendMessageW(g_app.demoModeCombo, CB_SETCURSEL, 0, 0);

    g_app.viewModeButton = CreateWindowW(L"BUTTON", L"View: Split", WS_CHILD | WS_VISIBLE,
                                         244, 76, 122, 24, hwnd, (HMENU)IDC_VIEW_MODE, NULL, NULL);
    g_app.clearLogButton = CreateWindowW(L"BUTTON", L"Clear Log", WS_CHILD | WS_VISIBLE,
                                         376, 76, 90, 24, hwnd, (HMENU)IDC_CLEAR_LOG, NULL, NULL);

    g_app.originalWnd = CreateWindowExW(WS_EX_CLIENTEDGE, L"STATIC", L"Original Render",
                                        WS_CHILD | WS_VISIBLE | SS_BLACKRECT | SS_NOTIFY,
                                        8, 126, 400, 300, hwnd, (HMENU)IDC_ORIGINAL_VIEW, NULL, NULL);
    g_app.aiWnd = CreateWindowExW(WS_EX_CLIENTEDGE, L"STATIC", L"AI Render",
                                  WS_CHILD | WS_VISIBLE | SS_BLACKRECT | SS_NOTIFY,
                                  416, 126, 400, 300, hwnd, (HMENU)IDC_AI_VIEW, NULL, NULL);
    g_app.logEdit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", NULL,
                                    WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL,
                                    8, 500, 800, 120, hwnd, (HMENU)IDC_LOG_EDIT, NULL, NULL);
    LayoutControls(hwnd);
}

static LRESULT CALLBACK MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
    case WM_CREATE:
        g_app.hwnd = hwnd;
        CreateUi(hwnd);
        RefreshDevices();
        AppendLog(L"HWS AI Video Inspector Demo started.");
        AppendLog(L"Left: original SDK render. Right: AI render with OSD result.");
        AppendLog(L"Default mode is YOLO Objects. Transport defaults to WS; HTTP is available for YOLO only. Shared Memory is reserved.");
        AppendLog(L"Input Play monitors captured input audio. Out Play speaks translated text with Windows SAPI and is mutually exclusive with Input Play.");
        AppendLog(L"Double-click Original/AI preview to switch between split and single-view mode. Clear Log can clear this log window at any time.");
        UpdateViewModeButtonText();
        return 0;

    case WM_SIZE:
        LayoutControls(hwnd);
        return 0;

    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case IDC_DEMO_MODE:
            if (HIWORD(wParam) == CBN_SELCHANGE) {
                if (g_app.aiRunning) {
                    AppendLog(L"Please stop AI before switching demo mode.");
                    SendMessageW(g_app.demoModeCombo, CB_SETCURSEL, (g_app.demoMode == DEMO_AI_MODE_ASR_EN_ZH) ? 1 : 0, 0);
                } else {
                    ApplyDemoAiMode(GetSelectedDemoAiMode(), true);
                }
            }
            break;
        case IDC_AI_TRANSPORT:
            if (HIWORD(wParam) == CBN_SELCHANGE) {
                if (g_app.aiRunning) {
                    AppendLog(L"Please stop AI before switching transport.");
                    ApplyAiTransport(GetSelectedAiTransport(), false, false);
                } else {
                    ApplyAiTransport(GetSelectedAiTransport(), true, true);
                }
            }
            break;
        case IDC_VIDEO_DEVICE:
            if (HIWORD(wParam) == CBN_SELCHANGE && !g_app.previewRunning) {
                int videoIndex = (int)SendMessageW(g_app.videoCombo, CB_GETCURSEL, 0, 0);
                if (videoIndex >= 0) RefreshVideoFormats(videoIndex);
            }
            break;
        case IDC_START_PREVIEW:
            StartPreview();
            break;
        case IDC_STOP_PREVIEW:
            StopPreview();
            break;
        case IDC_START_AI:
            StartAi();
            break;
        case IDC_STOP_AI:
            StopAi();
            break;
        case IDC_SNAPSHOT:
            CaptureSnapshot();
            break;
        case IDC_REC_START:
            StartRecording();
            break;
        case IDC_REC_STOP:
            StopRecording();
            break;
        case IDC_INPUT_AUDIO_PLAY:
            ToggleInputAudioPlayback();
            break;
        case IDC_OUTPUT_AUDIO_PLAY:
            ToggleOutputAudioPlayback();
            break;
        case IDC_VIEW_MODE:
            CycleViewMode();
            break;
        case IDC_CLEAR_LOG:
            ClearLogWindow();
            AppendLog(L"Log cleared.");
            break;
        case IDC_ORIGINAL_VIEW:
            if (HIWORD(wParam) == STN_DBLCLK) {
                ToggleSingleViewFromPane(false);
            }
            break;
        case IDC_AI_VIEW:
            if (HIWORD(wParam) == STN_DBLCLK) {
                ToggleSingleViewFromPane(true);
            }
            break;
        }
        return 0;

    case WM_HWS_AI_RESULT:
    {
        AiDemoResult* result = reinterpret_cast<AiDemoResult*>(lParam);
        if (result) {
            g_app.aiOutput.ApplyResult(*result);
            std::wstring log = L"AI result: " + AiUtf8ToWide(result->summaryUtf8);
            AppendLog(log.c_str());
            if (result->typeUtf8.find("audio.") == 0 || !result->textUtf8.empty() || !result->translationUtf8.empty()) {
                std::wstring a = (result->typeUtf8 == "audio.debug") ? L"AI AUDIO DEBUG: " : L"AI AUDIO: ";
                if (!result->textUtf8.empty()) { a += L"ASR="; a += AiUtf8ToWide(result->textUtf8); }
                if (!result->translationUtf8.empty()) { a += L" TR="; a += AiUtf8ToWide(result->translationUtf8); }
                AppendLog(a.c_str());
                PlayTranslatedOutputAudioIfEnabled(*result);
            }
            for (size_t i = 0; i < result->events.size(); ++i) {
                const AiEvent& ev = result->events[i];
                std::wstring evLog = L"AI EVENT: ";
                evLog += AiUtf8ToWide(ev.typeUtf8);
                if (!ev.labelUtf8.empty()) {
                    evLog += L" label=";
                    evLog += AiUtf8ToWide(ev.labelUtf8);
                }
                if (ev.prevCount >= 0 || ev.currentCount >= 0) {
                    wchar_t countText[64] = {};
                    swprintf_s(countText, L" count=%d->%d", ev.prevCount, ev.currentCount);
                    evLog += countText;
                }
                if (!ev.messageUtf8.empty()) {
                    evLog += L" msg=";
                    evLog += AiUtf8ToWide(ev.messageUtf8);
                }
                AppendLog(evLog.c_str());
            }
            if (!result->rawJsonUtf8.empty()) {
                std::string shortJson = result->rawJsonUtf8;
                const size_t maxRawJson = (result->typeUtf8.find("audio.") == 0) ? 1600 : 500;
                if (shortJson.size() > maxRawJson) shortJson.resize(maxRawJson);
                AppendLogUtf8(shortJson);
            }
            delete result;
        }
        return 0;
    }

    case WM_CLOSE:
        StopPreview();
        DestroyWindow(hwnd);
        return 0;

    case WM_DESTROY:
        HWS_Shutdown();
        CoUninitialize();
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow)
{
    CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (HWS_Init() != HWS_OK) {
        MessageBoxW(NULL, L"HWS_Init failed.", L"HWS AI Demo", MB_ICONERROR);
        return 1;
    }

    WNDCLASSW wc = {};
    wc.lpfnWndProc = MainWndProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    wc.lpszClassName = L"HwsAiVideoInspectorDemoWnd";
    RegisterClassW(&wc);

    HWND hwnd = CreateWindowW(wc.lpszClassName,
                              L"HWS AI Demo - YOLO Objects / ASR EN->CN",
                              WS_OVERLAPPEDWINDOW,
                              CW_USEDEFAULT, CW_USEDEFAULT, 1280, 800,
                              NULL, NULL, hInstance, NULL);
    if (!hwnd) {
        HWS_Shutdown();
        CoUninitialize();
        return 1;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg = {};
    while (GetMessageW(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    return (int)msg.wParam;
}
