// native_bridge/src/gui_probe.cpp — standalone VST3 GUI attach probe.
// Debug tool (NOT shipped with the DAW): loads a VST3 with Vst3Instrument and
// tests openGUI under different thread/pump/parent-window setups so we can
// find why view->attached() hangs inside the bridge.
#include "Vst3Instrument.h"

#ifdef _WIN32
#include <windows.h>
#endif
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <functional>
#include <mutex>
#include <string>
#include <thread>

static void pump_for(int secs) {
    auto t0 = std::chrono::steady_clock::now();
    while (std::chrono::duration_cast<std::chrono::seconds>(
               std::chrono::steady_clock::now() - t0).count() < secs) {
        MSG msg;
        while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
        Sleep(5);
    }
}

static void sleep_for(int secs) { Sleep((DWORD)(secs * 1000)); }

// Per-channel persistent worker replicating the bridge (round-3 probe).
class ProbeWorker {
public:
    void start() {
        th = std::thread([this] {
#ifdef _WIN32
            OleInitialize(nullptr);
#endif
            std::unique_lock<std::mutex> lk(mu);
            for (;;) {
                if (stop && jobs.empty()) break;
                if (!jobs.empty()) {
                    auto j = std::move(jobs.front());
                    jobs.pop_front();
                    lk.unlock();
                    j();
                    lk.lock();
                    continue;
                }
                cv.wait_for(lk, std::chrono::milliseconds(5));
                lk.unlock();
                MSG msg;
                while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                lk.lock();
            }
#ifdef _WIN32
            OleUninitialize();
#endif
        });
    }
    void post(std::function<void()> j) {
        { std::lock_guard<std::mutex> lk(mu); jobs.push_back(std::move(j)); }
        cv.notify_all();
    }
    std::thread th;
    std::mutex mu;
    std::condition_variable cv;
    std::deque<std::function<void()>> jobs;
    bool stop = false;
};

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("usage: gui_probe <plugin.vst3> [variant] [secs] [hwnd]\n"
               "variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread | two_instances | two_instances_close | close_reopen | bridge_two | shared_worker | two_workers_close\n");
        return 2;
    }
    std::string path = argv[1];
    std::string variant = argc > 2 ? argv[2] : "main_own";
    int secs = argc > 3 ? atoi(argv[3]) : 20;

    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);

    // Watchdog: never let a stuck plugin hang this probe forever.
    std::thread([secs]() {
        Sleep((DWORD)(secs + 10) * 1000);
        printf("WATCHDOG-KILL\n");
        fflush(stdout);
        TerminateProcess(GetCurrentProcess(), 0);
    }).detach();

    HWND ownHwnd = 0;
    {
        WNDCLASSEX wc = {};
        wc.cbSize = sizeof(wc);
        wc.lpfnWndProc = DefWindowProc;
        wc.hInstance = GetModuleHandle(nullptr);
        wc.lpszClassName = "GuiProbeClass";
        RegisterClassEx(&wc);
        ownHwnd = CreateWindowEx(0, "GuiProbeClass", "GuiProbe", WS_OVERLAPPEDWINDOW,
                                 0, 0, 800, 600, nullptr, nullptr, wc.hInstance, nullptr);
        printf("ownHwnd=%p\n", (void*)ownHwnd);
    }

    void* targetHwnd = (void*)ownHwnd;
    if (argc > 4) targetHwnd = (void*)(uintptr_t)strtoull(argv[4], nullptr, 0);

    printf("loading %s\n", path.c_str());
    fflush(stdout);
    Vst3Instrument inst;
    Vst3Instrument inst2;
    bool isDeferred = (variant == "bridge_like" || variant == "same_thread" ||
                       variant == "two_instances" || variant == "two_instances_close" ||
                       variant == "close_reopen" || variant == "bridge_two" ||
                       variant == "shared_worker");
    bool ok = true;
    if (!isDeferred) {
        ok = inst.loadPlugin(path, 44100.0);
        printf("load=%d\n", ok ? 1 : 0);
        fflush(stdout);
        if (!ok) return 1;
    }

    std::atomic<bool> done{false};
    std::atomic<bool> result{false};

    auto attach_job = [&]() {
        CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
        printf("[%s] openGUI start hwnd=%p\n", variant.c_str(), targetHwnd);
        fflush(stdout);
        bool r = inst.openGUI(targetHwnd);
        printf("[%s] openGUI returned=%d\n", variant.c_str(), r ? 1 : 0);
        fflush(stdout);
        result = r;
        done = true;
    };

    if (variant == "main_own") {
        attach_job();  // editorhost pattern: attached on main thread, pump after
        pump_for(secs);
    } else if (variant == "worker_own_nopump" || variant == "worker_foreign_nopump") {
        std::thread t(attach_job);
        sleep_for(secs);
        if (!done.load()) { printf("RESULT: TIMEOUT (no pump)\n"); return 3; }
    } else if (variant == "worker_own_pump" || variant == "worker_foreign_pump") {
        std::thread t(attach_job);
        pump_for(secs);
        if (!done.load()) { printf("RESULT: TIMEOUT (pump concurrent)\n"); return 3; }
    } else if (variant == "bridge_like") {
        // Bridge pattern: plugin loaded on thread A which then EXITS (COM
        // apartment dies), openGUI on separate thread B, main pumps.
        std::thread loadA([&]() {
            CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
            printf("[bridge_like] loadPlugin on thread A\n");
            fflush(stdout);
            bool r = inst.loadPlugin(path, 44100.0);
            printf("[bridge_like] load=%d (thread A exiting)\n", r ? 1 : 0);
            fflush(stdout);
            CoUninitialize();
        });
        loadA.join();
        std::thread t(attach_job);  // thread B, after A exited
        pump_for(secs);
        if (!done.load()) { printf("RESULT: TIMEOUT (apartment dead)\n"); return 3; }
    } else if (variant == "same_thread") {
        // Load and openGUI on the SAME worker thread, kept alive; main pumps.
        std::thread t([&]() {
            CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
            printf("[same_thread] loadPlugin on worker\n");
            fflush(stdout);
            bool r = inst.loadPlugin(path, 44100.0);
            printf("[same_thread] load=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) attach_job();
        });
        pump_for(secs);
        if (!done.load()) { printf("RESULT: TIMEOUT (same_thread)\n"); return 3; }
    } else if (variant == "shared_worker") {
        // Proposed round-3 fix: ALL VST3 instances live in ONE apartment —
        // one persistent worker thread hosts load + openGUI for every channel.
        ProbeWorker w;
        w.start();
        std::atomic<bool> done{false};
        int aRes = 0, bRes = 0;
        w.post([&]() {
            bool r = inst.loadPlugin(path, 44100.0);
            printf("[shared_worker] load inst1=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) {
                HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeA", WS_OVERLAPPEDWINDOW,
                                        0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
                r = inst.openGUI(h);
                printf("[shared_worker] attach inst1=%d hwnd=%p\n", r ? 1 : 0, (void*)h);
                fflush(stdout);
                aRes = r ? 1 : 0;
            }
            r = inst2.loadPlugin(path, 44100.0);
            printf("[shared_worker] load inst2=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) {
                HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeB", WS_OVERLAPPEDWINDOW,
                                        0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
                r = inst2.openGUI(h);
                printf("[shared_worker] attach inst2=%d hwnd=%p\n", r ? 1 : 0, (void*)h);
                fflush(stdout);
                bRes = r ? 1 : 0;
            }
            done = true;
        });
        auto t0 = std::chrono::steady_clock::now();
        while (!done.load() && std::chrono::duration_cast<std::chrono::seconds>(
                   std::chrono::steady_clock::now() - t0).count() < secs) {
            MSG msg;
            while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            }
            Sleep(2);
        }
        // Stay alive 5s with BOTH editors attached and the worker pumping,
        // while MAIN KEEPS PUMPING like the bridge audio loop: distinguishes a
        // live crash (bad) from a stop-pumping probe artifact.
        auto t1 = std::chrono::steady_clock::now();
        while (std::chrono::duration_cast<std::chrono::seconds>(
                   std::chrono::steady_clock::now() - t1).count() < 5) {
            MSG msg;
            while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            }
            Sleep(2);
        }
        printf("RESULT: done=%d aRes=%d bRes=%d\n", done.load() ? 1 : 0, aRes, bRes);
        return (done.load() && aRes && bRes) ? 0 : 3;
    } else if (variant == "bridge_two") {
        // Exact bridge architecture: per-channel persistent worker threads with
        // own COM STA apartments and idle message pumps; inst1 loaded+attached
        // on worker A, then inst2 loaded+attached on worker B; main pumps.
        ProbeWorker wa, wb;
        wa.start();
        wb.start();
        std::atomic<bool> aDone{false}, aOk{false}, bDone{false}, bOk{false};
        wa.post([&]() {
            printf("[bridge_two] workerA load inst1\n");
            fflush(stdout);
            bool r = inst.loadPlugin(path, 44100.0);
            printf("[bridge_two] workerA load inst1=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) {
                HWND h = CreateWindowEx(0, "GuiProbeClass", "GuiProbeA", WS_OVERLAPPEDWINDOW,
                                        0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
                printf("[bridge_two] workerA hwnd=%p\n", (void*)h);
                fflush(stdout);
                printf("[bridge_two] workerA openGUI inst1\n");
                fflush(stdout);
                r = inst.openGUI(h);
                printf("[bridge_two] workerA openGUI inst1=%d\n", r ? 1 : 0);
                fflush(stdout);
                aOk = r;
            }
            aDone = true;
        });
        auto t0 = std::chrono::steady_clock::now();
        auto elapsed = [&]() {
            return std::chrono::duration_cast<std::chrono::seconds>(
                std::chrono::steady_clock::now() - t0).count();
        };
        // main pump like the bridge audio loop
        std::thread pump([&]() {
            while (elapsed() < secs) {
                MSG msg;
                while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                Sleep(2);
            }
        });
        while (!aDone.load() && elapsed() < secs) Sleep(50);
        if (!aDone.load()) {
            printf("RESULT: TIMEOUT inst1 attach (aDone=0)\n");
            return 3;
        }
        wb.post([&]() {
            printf("[bridge_two] workerB load inst2\n");
            fflush(stdout);
            bool r = inst2.loadPlugin(path, 44100.0);
            printf("[bridge_two] workerB load inst2=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) {
                HWND h = CreateWindowEx(0, "GuiProbeClass", "GuiProbeB", WS_OVERLAPPEDWINDOW,
                                        0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
                printf("[bridge_two] workerB hwnd=%p\n", (void*)h);
                fflush(stdout);
                printf("[bridge_two] workerB openGUI inst2\n");
                fflush(stdout);
                r = inst2.openGUI(h);
                printf("[bridge_two] workerB openGUI inst2=%d\n", r ? 1 : 0);
                fflush(stdout);
                bOk = r;
            }
            bDone = true;
        });
        while ((!aDone.load() || !bDone.load()) && elapsed() < secs) Sleep(50);
        pump.join();
        printf("RESULT: aDone=%d aOk=%d bDone=%d bOk=%d\n",
               aDone.load() ? 1 : 0, aOk.load() ? 1 : 0,
               bDone.load() ? 1 : 0, bOk.load() ? 1 : 0);
        return (aDone.load() && aOk.load() && bDone.load() && bOk.load()) ? 0 : 3;
    } else if (variant == "two_workers_close") {
        // Round-4 exact bridge pattern: per-channel workers (own STA
        // apartments); attach via temp thread + WORKER nested pump (the bridge
        // main.cpp job pumps the worker queue while attachView runs on a
        // temporary thread); close editor inst1 before attaching inst2 on its
        // own worker.
        ProbeWorker wa, wb;
        wa.start();
        wb.start();
        std::atomic<bool> aDone{false}, aOk{false}, bDone{false}, bOk{false};
        wa.post([&]() {
            printf("[two_workers_close] workerA load inst1\n");
            fflush(stdout);
            bool r = inst.loadPlugin(path, 44100.0);
            printf("[two_workers_close] workerA load inst1=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) {
                HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeA", WS_OVERLAPPEDWINDOW,
                                        0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
                printf("[two_workers_close] workerA hwnd=%p\n", (void*)h);
                fflush(stdout);
                bool r2 = false;
                std::atomic<bool> d{false};
                std::thread t([&]() {
                    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
                    r2 = inst.attachView(h);
                    CoUninitialize();
                    d = true;
                });
                while (!d.load()) {
                    MSG msg;
                    while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                        TranslateMessage(&msg);
                        DispatchMessageW(&msg);
                    }
                    Sleep(1);
                }
                t.join();
                printf("[two_workers_close] workerA attach inst1=%d\n", r2 ? 1 : 0);
                fflush(stdout);
                aOk = r2;
                inst.closeGUI();  // view->removed() on worker A
                printf("[two_workers_close] workerA closeGUI inst1\n");
                fflush(stdout);
            }
            aDone = true;
        });
        auto t0 = std::chrono::steady_clock::now();
        auto elapsed = [&]() {
            return std::chrono::duration_cast<std::chrono::seconds>(
                std::chrono::steady_clock::now() - t0).count();
        };
        while (!aDone.load() && elapsed() < secs) Sleep(50);
        if (!aDone.load()) { printf("RESULT: TIMEOUT inst1 (aDone=0)\n"); return 3; }
        wb.post([&]() {
            printf("[two_workers_close] workerB load inst2\n");
            fflush(stdout);
            bool r = inst2.loadPlugin(path, 44100.0);
            printf("[two_workers_close] workerB load inst2=%d\n", r ? 1 : 0);
            fflush(stdout);
            if (r) {
                HWND h = CreateWindowEx(0, "GuiProbeClass", "ProbeB", WS_OVERLAPPEDWINDOW,
                                        0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
                printf("[two_workers_close] workerB hwnd=%p\n", (void*)h);
                fflush(stdout);
                bool r2 = false;
                std::atomic<bool> d{false};
                std::thread t([&]() {
                    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
                    r2 = inst2.attachView(h);
                    CoUninitialize();
                    d = true;
                });
                while (!d.load()) {
                    MSG msg;
                    while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                        TranslateMessage(&msg);
                        DispatchMessageW(&msg);
                    }
                    Sleep(1);
                }
                t.join();
                printf("[two_workers_close] workerB attach inst2=%d\n", r2 ? 1 : 0);
                fflush(stdout);
                bOk = r2;
            }
            bDone = true;
        });
        while ((!aDone.load() || !bDone.load()) && elapsed() < secs) Sleep(50);
        printf("RESULT: aDone=%d aOk=%d bDone=%d bOk=%d\n",
               aDone.load() ? 1 : 0, aOk.load() ? 1 : 0,
               bDone.load() ? 1 : 0, bOk.load() ? 1 : 0);
        return (aDone.load() && aOk.load() && bDone.load() && bOk.load()) ? 0 : 3;
    } else if (variant == "two_instances" || variant == "two_instances_close" || variant == "close_reopen") {
        // Bridge round-3 probes: does a SECOND instance of the same plugin hang
        // in view->attached() while the first instance's view is attached?
        // Does closing the first view first (two_instances_close) or the
        // close-then-reopen reload path (close_reopen) avoid the hang?
        auto attachOnWorker = [&](Vst3Instrument* i, void* hwnd, const char* tag) -> bool {
            std::atomic<bool> d{false};
            std::atomic<bool> r{false};
            std::thread t([&]() {
                CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
                printf("[%s] openGUI(%s) start hwnd=%p\n", variant.c_str(), tag, hwnd);
                fflush(stdout);
                bool rr = i->openGUI(hwnd);
                printf("[%s] openGUI(%s) returned=%d\n", variant.c_str(), tag, rr ? 1 : 0);
                fflush(stdout);
                r = rr;
                d = true;
            });
            auto t0 = std::chrono::steady_clock::now();
            while (!d.load() && std::chrono::duration_cast<std::chrono::seconds>(
                       std::chrono::steady_clock::now() - t0).count() < secs) {
                MSG msg;
                while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                Sleep(2);
            }
            t.detach();
            if (!d.load()) {
                printf("[%s] TIMEOUT openGUI(%s)\n", variant.c_str(), tag);
                return false;
            }
            return r.load();
        };
        auto loadOnMain = [&](Vst3Instrument* i, const char* tag) -> bool {
            printf("[%s] loadPlugin(%s) on main\n", variant.c_str(), tag);
            fflush(stdout);
            bool rr = i->loadPlugin(path, 44100.0);
            printf("[%s] load(%s)=%d\n", variant.c_str(), tag, rr ? 1 : 0);
            fflush(stdout);
            return rr;
        };
        // second parent window for inst2
        HWND hwnd2 = CreateWindowEx(0, "GuiProbeClass", "GuiProbe2", WS_OVERLAPPEDWINDOW,
                                    0, 0, 800, 600, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
        printf("hwnd2=%p\n", (void*)hwnd2);
        if (!loadOnMain(&inst, "inst1")) return 1;
        if (!attachOnWorker(&inst, targetHwnd, "inst1")) return 3;
        if (variant == "two_instances_close") {
            printf("[%s] closeGUI(inst1) before inst2 attach\n", variant.c_str());
            inst.closeGUI();
        }
        if (variant == "close_reopen") {
            printf("[%s] closeGUI(inst1) then reopen same instance (reload path)\n", variant.c_str());
            inst.closeGUI();
            if (!attachOnWorker(&inst, targetHwnd, "inst1-reopen")) {
                printf("RESULT: done=%d attached_ok=%d\n", 0, 0);
                return 3;
            }
            printf("RESULT: done=%d attached_ok=%d\n", 1, 1);
            return 0;
        }
        if (!loadOnMain(&inst2, "inst2")) return 1;
        if (!attachOnWorker(&inst2, hwnd2, "inst2")) {
            printf("RESULT: done=%d attached_ok=%d\n", 0, 0);
            return 3;
        }
        // 5s survival with both editors attached (main keeps pumping).
        auto t1 = std::chrono::steady_clock::now();
        while (std::chrono::duration_cast<std::chrono::seconds>(
                   std::chrono::steady_clock::now() - t1).count() < 5) {
            MSG msg;
            while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            }
            Sleep(2);
        }
        printf("RESULT: done=%d attached_ok=%d\n", 1, 1);
        return 0;
    } else {
        printf("unknown variant %s\n", variant.c_str());
        return 2;
    }

    printf("RESULT: done=%d attached_ok=%d\n", done.load() ? 1 : 0, result.load() ? 1 : 0);
    return 0;
}
