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

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <shlwapi.h>
#include <stdio.h>
#include <vector>
#include <string>
#include <atomic>
#include <algorithm>

#include "../include/hwscapture.h"
#include "OpenCvFrameProcessor.h"

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "comdlg32.lib")
#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib, "ole32.lib")

#define IDC_VIDEO_DEVICE      1001
#define IDC_VIDEO_FORMAT      1002
#define IDC_PROCESS_MODE      1003
#define IDC_START_PREVIEW     1101
#define IDC_STOP_PREVIEW      1102
#define IDC_REC_START         1103
#define IDC_REC_STOP          1104
#define IDC_SNAPSHOT          1105
#define IDC_REFRESH           1106
#define IDC_ORIGINAL_VIEW     1201
#define IDC_OPENCV_VIEW       1202
#define IDC_LOG_EDIT          1301
#define WM_DEMO_LOG           (WM_APP + 20)

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

struct DemoApp
{
    HWND hwnd = NULL;
    HWND deviceCombo = NULL;
    HWND formatCombo = NULL;
    HWND modeCombo = NULL;
    HWND originalWnd = NULL;
    HWND opencvWnd = NULL;
    HWND logEdit = NULL;

    HWS_PORT videoIn = NULL;
    HWS_PORT renderOriginal = NULL;
    HWS_PORT rgbCallbackRender = NULL;
    HWS_PORT processedSource = NULL;
    HWS_PORT renderProcessed = NULL;
    HWS_PORT recorder = NULL;

    bool previewRunning = false;
    bool recording = false;
    HWS_VIDEO_INPUT_CONFIG currentConfig = {};
    wchar_t recordPath[MAX_PATH] = {};

    std::vector<std::string> deviceNames;
    std::vector<FormatItem> formats;
    OpenCvFrameProcessor processor;
    std::vector<unsigned char> processedBuffer;
    std::atomic<long> processingGuard { 0 };
    long long pushedFrameIndex = 0;
};

static DemoApp g_app;

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 IsDemoInputFormat(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 Utf8ToWide(const std::string& s)
{
    if (s.empty()) return std::wstring();
    int len = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, NULL, 0);
    if (len <= 0) return std::wstring();
    std::wstring out((size_t)len - 1, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &out[0], len);
    return out;
}

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 AppendLogA(const char* text)
{
    AppendLog(Utf8ToWide(text ? text : "").c_str());
}

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 LayoutControls(HWND hwnd)
{
    RECT rc = {};
    GetClientRect(hwnd, &rc);
    const int width = rc.right - rc.left;
    const int height = rc.bottom - rc.top;
    const int margin = 8;
    const int topH = 32;
    const int logH = 130;
    const int gap = 6;

    int x = margin;
    MoveWindow(g_app.deviceCombo, x, margin, 260, 260, TRUE); x += 266;
    MoveWindow(g_app.formatCombo, x, margin, 230, 260, TRUE); x += 236;
    MoveWindow(g_app.modeCombo, x, margin, 160, 220, TRUE); x += 166;
    MoveWindow(GetDlgItem(hwnd, IDC_REFRESH), x, margin, 70, 24, TRUE); x += 76;
    MoveWindow(GetDlgItem(hwnd, IDC_START_PREVIEW), x, margin, 72, 24, TRUE); x += 78;
    MoveWindow(GetDlgItem(hwnd, IDC_STOP_PREVIEW), x, margin, 72, 24, TRUE); x += 78;
    MoveWindow(GetDlgItem(hwnd, IDC_REC_START), x, margin, 72, 24, TRUE); x += 78;
    MoveWindow(GetDlgItem(hwnd, IDC_REC_STOP), x, margin, 72, 24, TRUE); x += 78;
    MoveWindow(GetDlgItem(hwnd, IDC_SNAPSHOT), x, margin, 88, 24, TRUE);

    const int viewTop = margin + topH;
    const int viewH = std::max(80, height - viewTop - logH - margin - gap);
    const int viewW = std::max(80, (width - margin * 2 - gap) / 2);
    MoveWindow(g_app.originalWnd, margin, viewTop, viewW, viewH, TRUE);
    MoveWindow(g_app.opencvWnd, margin + viewW + gap, viewTop, viewW, viewH, TRUE);
    MoveWindow(g_app.logEdit, margin, viewTop + viewH + gap, width - margin * 2, logH, TRUE);
}

static void PopulateDevices()
{
    SendMessageW(g_app.deviceCombo, CB_RESETCONTENT, 0, 0);
    g_app.deviceNames.clear();

    HWS_RefreshDeviceList();
    int count = HWS_GetVideoDeviceCount();
    for (int i = 0; i < count; ++i) {
        HWS_DEVICE_INFO info = {};
        if (HWS_GetVideoDeviceInfo(i, &info) != HWS_OK) continue;
        g_app.deviceNames.push_back(info.name);
        std::wstring name = Utf8ToWide(info.name);
        SendMessageW(g_app.deviceCombo, CB_ADDSTRING, 0, (LPARAM)name.c_str());
    }
    if (count > 0) SendMessageW(g_app.deviceCombo, CB_SETCURSEL, 0, 0);
}

static void PopulateFormatsForSelectedDevice()
{
    SendMessageW(g_app.formatCombo, CB_RESETCONTENT, 0, 0);
    g_app.formats.clear();

    int dev = (int)SendMessageW(g_app.deviceCombo, CB_GETCURSEL, 0, 0);
    if (dev < 0) return;

    int count = HWS_GetVideoCapabilityCount(dev);
    for (int i = 0; i < count; ++i) {
        HWS_VIDEO_CAP cap = {};
        if (HWS_GetVideoCapability(dev, i, &cap) != HWS_OK) continue;
        if (!IsDemoInputFormat(cap.pixelFormat)) continue;
        FormatItem item;
        item.width = cap.width;
        item.height = cap.height;
        item.fps = cap.fpsDen ? (cap.fpsNum / cap.fpsDen) : cap.fpsNum;
        if (item.fps <= 0) item.fps = 30;
        item.fmt = cap.pixelFormat;

        bool exists = false;
        for (size_t k = 0; k < g_app.formats.size(); ++k) {
            const FormatItem& f = g_app.formats[k];
            if (f.width == item.width && f.height == item.height && f.fps == item.fps && f.fmt == item.fmt) {
                exists = true;
                break;
            }
        }
        if (exists) continue;
        g_app.formats.push_back(item);
    }

    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);
    }

    for (size_t i = 0; i < g_app.formats.size(); ++i) {
        const FormatItem& f = g_app.formats[i];
        wchar_t text[128] = {};
        swprintf_s(text, L"%dx%d @ %dfps %S", f.width, f.height, f.fps, PixelFormatName(f.fmt));
        SendMessageW(g_app.formatCombo, CB_ADDSTRING, 0, (LPARAM)text);
    }
    SendMessageW(g_app.formatCombo, CB_SETCURSEL, 0, 0);
}

static OpenCvProcessMode GetSelectedMode()
{
    int sel = (int)SendMessageW(g_app.modeCombo, CB_GETCURSEL, 0, 0);
    if (sel < 0) sel = 0;
    if (sel > 3) sel = 0;
    return (OpenCvProcessMode)sel;
}

static void __stdcall OnRgbFrameForOpenCv(const HWS_VIDEO_FRAME* frame, void* userData)
{
    DemoApp* app = (DemoApp*)userData;
    if (!app || !app->previewRunning || !frame) return;
    if (!app->processedSource) return;

    if (app->processingGuard.exchange(1) != 0) {
        return;
    }

    int dstStride = 0;
    OpenCvProcessMode mode = GetSelectedMode();
    bool ok = app->processor.ProcessRgb32(frame, mode, app->processedBuffer, &dstStride);
    if (ok && !app->processedBuffer.empty() && dstStride > 0) {
        HWS_VIDEO_FRAME out = {};
        out.data[0] = app->processedBuffer.data();
        out.stride[0] = dstStride;
        out.width = frame->width;
        out.height = frame->height;
        out.pixelFormat = HWS_PIXFMT_RGB32;
        out.timestamp100ns = frame->timestamp100ns;
        out.frameIndex = ++app->pushedFrameIndex;
        HWS_PushVideoFrame(app->processedSource, &out);
    }

    app->processingGuard.store(0);
}

static void DestroyPort(HWS_PORT* port)
{
    if (!port || !*port) return;
    HWS_DestroyPort(*port);
    *port = NULL;
}

static void StopRecording()
{
    if (!g_app.recording) return;
    if (g_app.recorder) {
        if (g_app.processedSource) HWS_Disconnect(g_app.processedSource, g_app.recorder);
        HWS_Stop(g_app.recorder);
        DestroyPort(&g_app.recorder);
    }
    g_app.recording = false;
    AppendLog(L"Recording stopped");
}

static bool StartRecording()
{
    if (!g_app.previewRunning || !g_app.processedSource) {
        AppendLog(L"Start preview first");
        return false;
    }
    if (g_app.recording) return true;

    std::wstring path = FormatTimeFileName(L"opencv_processed", L"mp4");
    WideCharToMultiByte(CP_UTF8, 0, path.c_str(), -1, NULL, 0, NULL, NULL);
    char pathA[MAX_PATH] = {};
    WideCharToMultiByte(CP_UTF8, 0, path.c_str(), -1, pathA, MAX_PATH, NULL, NULL);

    g_app.recorder = HWS_CreateRecorder(pathA);
    if (!g_app.recorder) {
        AppendLog(L"HWS_CreateRecorder failed");
        return false;
    }

    HWS_RECORDER_CONFIG cfg = {};
    wcsncpy_s(cfg.filePath, path.c_str(), _TRUNCATE);
    cfg.container = HWS_CONTAINER_MP4;
    cfg.videoCodec = HWS_VIDEO_CODEC_H264;
    cfg.width = g_app.currentConfig.width;
    cfg.height = g_app.currentConfig.height;
    cfg.fps = g_app.currentConfig.fps;
    cfg.recordFps = g_app.currentConfig.fps;
    cfg.videoBitrate = 8000000;
    cfg.enableAudio = 0;
    cfg.inputMode = HWS_RECORDER_INPUT_REENCODE;
    cfg.splitMode = HWS_RECORDER_SPLIT_NONE;

    if (HWS_SetRecorderConfig(g_app.recorder, &cfg) != HWS_OK) {
        AppendLog(L"HWS_SetRecorderConfig failed");
        DestroyPort(&g_app.recorder);
        return false;
    }
    if (HWS_Connect(g_app.processedSource, g_app.recorder) != HWS_OK) {
        AppendLog(L"Connect processed source to recorder failed");
        DestroyPort(&g_app.recorder);
        return false;
    }
    if (HWS_Start(g_app.recorder) != HWS_OK) {
        AppendLog(L"HWS_Start recorder failed");
        HWS_Disconnect(g_app.processedSource, g_app.recorder);
        DestroyPort(&g_app.recorder);
        return false;
    }

    wcsncpy_s(g_app.recordPath, path.c_str(), _TRUNCATE);
    g_app.recording = true;
    wchar_t msg[512] = {};
    swprintf_s(msg, L"Recording processed OpenCV video: %s", path.c_str());
    AppendLog(msg);
    return true;
}

static void StopPreview()
{
    StopRecording();

    g_app.previewRunning = false;
    if (g_app.videoIn) HWS_Stop(g_app.videoIn);

    if (g_app.videoIn && g_app.renderOriginal) HWS_Disconnect(g_app.videoIn, g_app.renderOriginal);
    if (g_app.videoIn && g_app.rgbCallbackRender) HWS_Disconnect(g_app.videoIn, g_app.rgbCallbackRender);
    if (g_app.processedSource && g_app.renderProcessed) HWS_Disconnect(g_app.processedSource, g_app.renderProcessed);

    DestroyPort(&g_app.videoIn);
    DestroyPort(&g_app.renderOriginal);
    DestroyPort(&g_app.rgbCallbackRender);
    DestroyPort(&g_app.processedSource);
    DestroyPort(&g_app.renderProcessed);

    g_app.processedBuffer.clear();
    g_app.pushedFrameIndex = 0;
    AppendLog(L"Preview stopped");
}

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

    int dev = (int)SendMessageW(g_app.deviceCombo, CB_GETCURSEL, 0, 0);
    int fmtSel = (int)SendMessageW(g_app.formatCombo, CB_GETCURSEL, 0, 0);
    if (dev < 0 || fmtSel < 0 || fmtSel >= (int)g_app.formats.size()) {
        AppendLog(L"Select a video device and format first");
        return false;
    }

    FormatItem f = g_app.formats[(size_t)fmtSel];
    HWS_VIDEO_INPUT_CONFIG cfg = {};
    cfg.deviceIndex = dev;
    cfg.width = f.width;
    cfg.height = f.height;
    cfg.fps = f.fps;
    cfg.pixelFormat = f.fmt;
    cfg.colorSpace = HWS_COLOR_SPACE_AUTO;
    cfg.colorRange = HWS_COLOR_RANGE_AUTO;
    cfg.bufferCount = 4;
    g_app.currentConfig = cfg;

    g_app.videoIn = HWS_CreateVideoInput();
    g_app.renderOriginal = HWS_CreateVideoRender(g_app.originalWnd);
    g_app.rgbCallbackRender = HWS_CreateVideoRender(NULL);
    g_app.processedSource = HWS_CreateVideoFrameSource();
    g_app.renderProcessed = HWS_CreateVideoRender(g_app.opencvWnd);

    if (!g_app.videoIn || !g_app.renderOriginal || !g_app.rgbCallbackRender || !g_app.processedSource || !g_app.renderProcessed) {
        AppendLog(L"Create SDK ports failed");
        StopPreview();
        return false;
    }

    HWS_SetVideoRenderInterpolationMode(g_app.renderOriginal, HWS_RENDER_INTERPOLATION_AUTO);
    HWS_SetVideoRenderInterpolationMode(g_app.renderProcessed, HWS_RENDER_INTERPOLATION_AUTO);

    HWS_VIDEO_RENDER_CONFIG rgbCfg = {};
    rgbCfg.width = cfg.width;
    rgbCfg.height = cfg.height;
    rgbCfg.pixelFormat = HWS_PIXFMT_RGB32;
    HWS_SetVideoRenderConfig(g_app.rgbCallbackRender, &rgbCfg);
    HWS_SetVideoRenderConfig(g_app.renderProcessed, &rgbCfg);
    HWS_SetVideoFrameCallback(g_app.rgbCallbackRender, OnRgbFrameForOpenCv, &g_app);

    if (HWS_SetVideoInputConfig(g_app.videoIn, &cfg) != HWS_OK) {
        AppendLog(L"HWS_SetVideoInputConfig failed");
        StopPreview();
        return false;
    }
    if (HWS_Connect(g_app.videoIn, g_app.renderOriginal) != HWS_OK ||
        HWS_Connect(g_app.videoIn, g_app.rgbCallbackRender) != HWS_OK ||
        HWS_Connect(g_app.processedSource, g_app.renderProcessed) != HWS_OK) {
        AppendLog(L"Connect SDK graph failed");
        StopPreview();
        return false;
    }

    g_app.previewRunning = true;
    if (HWS_Start(g_app.videoIn) != HWS_OK) {
        AppendLog(L"HWS_Start video input failed");
        StopPreview();
        return false;
    }

    wchar_t msg[256] = {};
    swprintf_s(msg, L"Preview started: %dx%d @ %dfps %S", cfg.width, cfg.height, cfg.fps, PixelFormatName(cfg.pixelFormat));
    AppendLog(msg);
    return true;
}

static void SaveProcessedSnapshot()
{
    if (!g_app.previewRunning || !g_app.renderProcessed) {
        AppendLog(L"Start preview first");
        return;
    }
    std::wstring path = FormatTimeFileName(L"opencv_processed", L"jpg");
    HWS_RESULT r = HWS_SaveVideoRenderSnapshot(g_app.renderProcessed, path.c_str(), HWS_IMAGE_FORMAT_JPG);
    if (r == HWS_OK) {
        wchar_t msg[512] = {};
        swprintf_s(msg, L"Snapshot saved: %s", path.c_str());
        AppendLog(msg);
    } else {
        AppendLog(L"Snapshot failed");
    }
}

static LRESULT CALLBACK ViewWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
    case WM_ERASEBKGND:
        return 1;
    case WM_PAINT:
    {
        PAINTSTRUCT ps = {};
        HDC hdc = BeginPaint(hwnd, &ps);
        RECT rc = {};
        GetClientRect(hwnd, &rc);
        HBRUSH br = CreateSolidBrush(RGB(12, 12, 12));
        FillRect(hdc, &rc, br);
        DeleteObject(br);
        SetBkMode(hdc, TRANSPARENT);
        SetTextColor(hdc, RGB(190, 190, 190));
        wchar_t title[64] = {};
        GetWindowTextW(hwnd, title, ARRAYSIZE(title));
        DrawTextW(hdc, title, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
        EndPaint(hwnd, &ps);
        return 0;
    }
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

static LRESULT CALLBACK MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
    case WM_CREATE:
        g_app.hwnd = hwnd;
        CreateWindowW(L"STATIC", L"Device:", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, NULL, NULL, NULL);
        g_app.deviceCombo = CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                          0, 0, 0, 0, hwnd, (HMENU)IDC_VIDEO_DEVICE, NULL, NULL);
        g_app.formatCombo = CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                                          0, 0, 0, 0, hwnd, (HMENU)IDC_VIDEO_FORMAT, NULL, NULL);
        g_app.modeCombo = CreateWindowW(L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST,
                                        0, 0, 0, 0, hwnd, (HMENU)IDC_PROCESS_MODE, NULL, NULL);
        SendMessageW(g_app.modeCombo, CB_ADDSTRING, 0, (LPARAM)L"Canny Edge");
        SendMessageW(g_app.modeCombo, CB_ADDSTRING, 0, (LPARAM)L"Sharpen");
        SendMessageW(g_app.modeCombo, CB_ADDSTRING, 0, (LPARAM)L"Gray");
        SendMessageW(g_app.modeCombo, CB_ADDSTRING, 0, (LPARAM)L"Gaussian Blur");
        SendMessageW(g_app.modeCombo, CB_SETCURSEL, 0, 0);

        CreateWindowW(L"BUTTON", L"Refresh", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)IDC_REFRESH, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Start", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)IDC_START_PREVIEW, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Stop", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)IDC_STOP_PREVIEW, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Rec", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)IDC_REC_START, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Rec Stop", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)IDC_REC_STOP, NULL, NULL);
        CreateWindowW(L"BUTTON", L"Snapshot", WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)IDC_SNAPSHOT, NULL, NULL);

        g_app.originalWnd = CreateWindowW(L"HWS_OCV_VIEW", L"Original SDK preview", WS_CHILD | WS_VISIBLE | WS_BORDER,
                                          0, 0, 0, 0, hwnd, (HMENU)IDC_ORIGINAL_VIEW, NULL, NULL);
        g_app.opencvWnd = CreateWindowW(L"HWS_OCV_VIEW", L"OpenCV processed preview", WS_CHILD | WS_VISIBLE | WS_BORDER,
                                        0, 0, 0, 0, hwnd, (HMENU)IDC_OPENCV_VIEW, NULL, NULL);
        g_app.logEdit = CreateWindowW(L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL,
                                      0, 0, 0, 0, hwnd, (HMENU)IDC_LOG_EDIT, NULL, NULL);

        PopulateDevices();
        PopulateFormatsForSelectedDevice();
        LayoutControls(hwnd);
        AppendLogA("OpenCV video processor demo ready");
        return 0;

    case WM_SIZE:
        LayoutControls(hwnd);
        return 0;

    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case IDC_VIDEO_DEVICE:
            if (HIWORD(wParam) == CBN_SELCHANGE) PopulateFormatsForSelectedDevice();
            return 0;
        case IDC_REFRESH:
            if (!g_app.previewRunning) {
                PopulateDevices();
                PopulateFormatsForSelectedDevice();
                AppendLog(L"Device list refreshed");
            }
            return 0;
        case IDC_START_PREVIEW:
            StartPreview();
            return 0;
        case IDC_STOP_PREVIEW:
            StopPreview();
            return 0;
        case IDC_REC_START:
            StartRecording();
            return 0;
        case IDC_REC_STOP:
            StopRecording();
            return 0;
        case IDC_SNAPSHOT:
            SaveProcessedSnapshot();
            return 0;
        }
        break;

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

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

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow)
{
    INITCOMMONCONTROLSEX icc = {};
    icc.dwSize = sizeof(icc);
    icc.dwICC = ICC_STANDARD_CLASSES;
    InitCommonControlsEx(&icc);

    HRESULT hrCo = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    bool needUninit = SUCCEEDED(hrCo);

    WNDCLASSEXW viewWc = {};
    viewWc.cbSize = sizeof(viewWc);
    viewWc.lpfnWndProc = ViewWndProc;
    viewWc.hInstance = hInstance;
    viewWc.hCursor = LoadCursor(NULL, IDC_ARROW);
    viewWc.lpszClassName = L"HWS_OCV_VIEW";
    RegisterClassExW(&viewWc);

    WNDCLASSEXW wc = {};
    wc.cbSize = sizeof(wc);
    wc.lpfnWndProc = MainWndProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    wc.lpszClassName = L"HWS_OpenCV_Demo_Main";
    RegisterClassExW(&wc);

    if (HWS_Init() != HWS_OK) {
        MessageBoxW(NULL, L"HWS_Init failed", L"OpenCV demo", MB_ICONERROR);
        if (needUninit) CoUninitialize();
        return 1;
    }

    HWND hwnd = CreateWindowW(wc.lpszClassName,
                              L"HWS Capture SDK - OpenCV DLL Processor Demo",
                              WS_OVERLAPPEDWINDOW,
                              CW_USEDEFAULT,
                              CW_USEDEFAULT,
                              1280,
                              760,
                              NULL,
                              NULL,
                              hInstance,
                              NULL);
    if (!hwnd) {
        HWS_Shutdown();
        if (needUninit) CoUninitialize();
        return 1;
    }

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

    MSG msg = {};
    while (GetMessageW(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    if (needUninit) CoUninitialize();
    return (int)msg.wParam;
}
