// native_bridge/src/main.cpp
// Entry point of daw_vst_bridge.exe — opens shared memory created by the DAW
// (Rust/Tauri side), runs the real-time MIDI->audio loop, watches the parent.
#include "INativeInstrument.h"
#include "SharedMemoryIPC.h"
#include "NativeInstrumentEngine.h"

#ifdef _WIN32
#include <windows.h>
#include <mmsystem.h>
#include <process.h>
#include <thread>
#else
#include <unistd.h>
#include <cstdlib>
#include <thread>
#endif

#include <chrono>
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>

// --- platform helpers -------------------------------------------------------
static bool parent_alive(uint32_t pid) {
#ifdef _WIN32
    HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
    if (!h) return false;
    CloseHandle(h);
    return true;
#else
    return pid == 0 || (kill(pid, 0) == 0);
#endif
}

static void sleep_ms(uint32_t ms) {
#ifdef _WIN32
    Sleep(ms);
#else
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
#endif
}

#ifdef _WIN32
// Native VST editor windows registry — global de WM_DESTROY (chay tren worker
// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG
// luu con tro inst truc tiep: assign() thay inst moi moi lan load — con tro cu
// bi huy → WM_DESTROY tren con tro dangling → crash/hang bridge).
class ChannelWorker;  // fwd — WM_DESTROY posts closeGUI() to the channel worker
static void post_close_gui(uint32_t ch, HWND hwnd);  // defined after ChannelWorker
static InstrumentEngineManager* g_engine = nullptr;
static std::mutex g_guiMutex;
static std::map<uint32_t, void*> g_guiWindows;  // channel -> HWND (keep window alive)
static std::map<HWND, uint32_t> g_hwndToCh;     // HWND -> channel (WM_DESTROY cleanup)
static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;

static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    if (uMsg == WM_DESTROY) {
        uint32_t ch = UINT32_MAX;
        {
            std::lock_guard<std::mutex> lock(g_guiMutex);
            auto it = g_hwndToCh.find(hwnd);
            if (it != g_hwndToCh.end()) ch = it->second;
        }
        if (ch != UINT32_MAX) post_close_gui(ch, hwnd);
    }
    return DefWindowProcA(hwnd, uMsg, wParam, lParam);
}
#endif

// Serialize plugin loads across channels: concurrent VST3 createInstance on
// separate worker threads (Nexus) deadlocks inside the SDK factory - all
// stuck forever (observed: 3 concurrent Nexus loads, zero assign returned).
// One load at a time; SF2/SFZ loads are fast, contention negligible.
static std::mutex g_loadMutex;
// Pending OPEN_GUI requests whose channel instrument was not loaded yet.
// The main loop fulfils them as soon as the load job completes (assign ok),
// so the GUI button works regardless of JS retry timing.
static std::mutex g_pendingGuiMutex;
static std::map<uint32_t, std::pair<void*, std::string>> g_pendingGui;

// B9: native Win32 window for the VST editor (replaces the WebView2 surface —
// the HTML window was drawn ON TOP of the plugin GUI). MUST be created on the
// ChannelWorker thread so the worker's idle message pump services its messages.
static void* create_native_vst_window(const char* title) {
#ifdef _WIN32
    static const char* kWndClass = "SonicForge_Native_VST3_Class";
    static bool registered = false;
    if (!registered) {
        WNDCLASSA wc = {};
        wc.lpfnWndProc = VstWindowProc;
        wc.hInstance = GetModuleHandleA(nullptr);
        wc.lpszClassName = kWndClass;
        RegisterClassA(&wc);
        registered = true;
    }
    HWND hwnd = CreateWindowExA(0, kWndClass, title ? title : "VST",
                                WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                                CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
                                nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
    return (void*)hwnd;
#else
    (void)title;
    return nullptr;
#endif
}

// Per-channel persistent worker: ONE thread owns the COM STA apartment for that
// channel's instrument for its whole lifetime. loadPlugin and openGUI MUST run
// on the same thread — if the loading thread exits, its apartment dies and
// VST3 plugins that marshal internally (Nexus) hang forever in
// view->attached(). Verified with gui_probe: `bridge_like` (load thread exits,
// openGUI on another) hangs; `same_thread` (load+openGUI on one alive thread)
// returns attached=OK.
class ChannelWorker {
public:
    ChannelWorker() {
        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 job = std::move(jobs_.front());
                    jobs_.pop_front();
                    lk.unlock();
                    job();
                    lk.lock();
                    continue;
                }
                cv_.wait_for(lk, std::chrono::milliseconds(5));
                // Pump THIS thread's message queue while idle — VST editor
                // windows are created on this thread, their messages must be
                // dispatched here or the editor freezes after attach.
                lk.unlock();
                MSG msg;
                while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
                    TranslateMessage(&msg);
                    DispatchMessageW(&msg);
                }
                lk.lock();
            }
#ifdef _WIN32
            OleUninitialize();
#endif
        });
    }
    ~ChannelWorker() {
        {
            std::lock_guard<std::mutex> lk(mu_);
            stop_ = true;
        }
        cv_.notify_all();
        if (th_.joinable()) th_.join();
    }
    void post(std::function<void()> job) {
        {
            std::lock_guard<std::mutex> lk(mu_);
            jobs_.push_back(std::move(job));
        }
        cv_.notify_all();
    }

private:
    std::thread th_;
    std::mutex mu_;
    std::condition_variable cv_;
    std::deque<std::function<void()>> jobs_;
    bool stop_ = false;
};

// closeGUI() MUST run on the channel worker thread (its COM STA apartment) —
// the view was attached there. Calling view->removed() from the main thread
// (WM_DESTROY handler) is a cross-apartment COM call that corrupts the plugin;
// Nexus then hangs on the NEXT view->attached(). Erase the registry inside the
// job so Option B (closing another editor before attach) waits for closeGUI to
// actually finish.
static void post_close_gui(uint32_t ch, HWND hwnd) {
    ChannelWorker* w = nullptr;
    if (g_workers) {
        auto wit = g_workers->find(ch);
        if (wit != g_workers->end()) w = wit->second.get();
    }
    if (w) {
        w->post([ch, hwnd]() {
            if (auto* i = g_engine->get(ch)) i->closeGUI();
            std::lock_guard<std::mutex> lock(g_guiMutex);
            g_hwndToCh.erase(hwnd);
            g_guiWindows.erase(ch);
        });
    } else {
        std::lock_guard<std::mutex> lock(g_guiMutex);
        g_hwndToCh.erase(hwnd);
        g_guiWindows.erase(ch);
    }
}

int main(int argc, char* argv[]) {
    std::cout << "[NativeBridge] Starting DAW Host Bridge Engine..." << std::endl;

    // 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
    std::string shmName = "SonicForge_DAW_IPC";
    for (int i = 1; i + 1 < argc; ++i) {
        if (std::strcmp(argv[i], "--shm") == 0) shmName = argv[i + 1];
    }
    if (const char* e = std::getenv("SF_SHM_NAME")) shmName = e;

    // Parent watchdog PID (set by Tauri sidecar spawner)
    uint32_t parentPid = 0;
    if (const char* e = std::getenv("SF_PARENT_PID")) parentPid = (uint32_t)std::atoi(e);

    // V8 bug 2: watchdog THREAD rieng - main loop chi check parent_alive giua
    // cac block; neu VST process() chan loop thi khong bao gio thoat -> orphan.
    // Thread nay chay doc lap, parent chet -> TerminateProcess ngay. Kem
    // start-time check chong PID reuse (OpenProcess tra handle cua process
    // khac chiem lai PID -> tuong parent con song mai).
    if (parentPid != 0) {
        std::thread([parentPid]() {
            auto proc_birth = [](HANDLE h) -> uint64_t {
                FILETIME c, e, k, u;
                if (GetProcessTimes(h, &c, &e, &k, &u))
                    return (uint64_t(c.dwHighDateTime) << 32) | c.dwLowDateTime;
                return 0;
            };
            uint64_t birth = 0;
            {
                HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid);
                if (h) { birth = proc_birth(h); CloseHandle(h); }
            }
            for (;;) {
                Sleep(2000);
                HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, parentPid);
                if (!h) { TerminateProcess(GetCurrentProcess(), 0); return; }
                uint64_t nowBirth = proc_birth(h);
                CloseHandle(h);
                if (birth != 0 && nowBirth != 0 && nowBirth != birth) {
                    TerminateProcess(GetCurrentProcess(), 0); return;
                }
            }
        }).detach();
    }

#ifdef _WIN32
    // Real-time-ish timing: 1ms scheduler resolution
    timeBeginPeriod(1);
    HANDLE hMapFile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName.c_str());
    if (!hMapFile) {
        std::cerr << "[NativeBridge] Failed to open Shared Memory mapping: " << shmName << std::endl;
        return 1;
    }
    auto* shmIPC = (SharedAudioBufferIPC*)MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedAudioBufferIPC));
    if (!shmIPC) { CloseHandle(hMapFile); return 1; }
#else
    (void)shmName; // POSIX shm mapping (shm_open) added when porting off Windows
    auto* shmIPC = (SharedAudioBufferIPC*)std::calloc(1, sizeof(SharedAudioBufferIPC));
    if (!shmIPC) return 1;
#endif

    InstrumentEngineManager instruments;
#ifdef _WIN32
    g_engine = &instruments;
#endif
    // Per-channel persistent workers: loadPlugin + openGUI run on the SAME
    // thread whose COM STA apartment stays alive for the channel's lifetime
    // (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
    std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers;
    g_workers = &workers;
    // B9: native editor windows per channel — keep alive (HWND outlives the job).
    // Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc.
    // B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
    // Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
    // is accepted but must match, otherwise warned and ignored.
    double sampleRate = 44100.0;
    if (const char* e = std::getenv("SF_SAMPLE_RATE")) {
        double sr = (double)std::atoi(e);
        if (sr > 0) sampleRate = sr;
    }
    if (const char* e = std::getenv("SF_BLOCK_SIZE")) {
        uint32_t b = (uint32_t)std::atoi(e);
        if (b != AUDIO_BLOCK_SIZE)
            std::cerr << "[NativeBridge] SHM block size fixed at " << AUDIO_BLOCK_SIZE
                      << " (SF_BLOCK_SIZE=" << b << " ignored)" << std::endl;
    }
    const uint32_t block = AUDIO_BLOCK_SIZE;
    uint64_t playheadSamples = 0;
    // V8 bug 3: khi STOP da xu ly, bo qua NOTE_ON (velocity>0) den sau - JS
    // note-on timer co the bay toi sau STOP (guardPlay tre do React re-render)
    // -> retrigger note -> VST loop am. Chi PLAY moi nhan note-on lai.
    bool transportStopped = false;

    auto dispatch = [&](const SharedAudioBufferIPC::MidiEventIPC& evt) {
        auto* inst = instruments.get(evt.channel);
        if (!inst) return; // channel chưa gán instrument → silent (A10)
        switch (evt.command) {
            case 0x9:
                // sampleOffset LUON LUON = 0 khi den day: events duoc dispatch ngay
                // truoc segment chua no (A11 splitting), nen offset tuong doi la 0.
                // Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
                if (evt.velocity > 0) {
                    if (transportStopped) return; // V8 bug 3: drop note-on sau STOP
                    inst->noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f, 0);
                } else
                    inst->noteOff(evt.channel, evt.pitch, 0);
                break;
            case 0x8:
                inst->noteOff(evt.channel, evt.pitch, 0);
                break;
            case 0xB: // CC: controller number in pitch, value in data2
                // V9 bug 4: sau STOP, drop sustain-down (CC64>0) — JS co the
                // gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu
                // note khi pedal down -> am treo loop. CC64=0 (sustain-up)
                // van cho qua.
                if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return;
                inst->controlChange(evt.channel, evt.pitch, evt.data2);
                break;
            case 0xC: // program change: program in data2
                inst->programChange(evt.channel, evt.data2);
                break;
            case 0xE: // 14-bit pitch bend: data2 = LSB, data3 = MSB
                inst->pitchBend(evt.channel, evt.data2 | (uint32_t(evt.data3) << 7));
                break;
            default: break;
        }
    };

    // Render only [from, to) of the block — used by sample-accurate splitting.
    auto renderSegment = [&](uint32_t from, uint32_t to) {
        if (to <= from) return;
        instruments.renderAll(shmIPC->masterLeft + from, shmIPC->masterRight + from, to - from);
    };

    double blockDurationMs = (double)block / sampleRate * 1000.0;
    auto startTime = std::chrono::steady_clock::now();
    uint64_t blockCount = 0;

    // OPEN_GUI handling - shared by the control queue and the pending-GUI
    // sweep. Gate on the MAIN thread before creating a window: instrument
    // load is ASYNC (worker thread); OPEN_GUI too early would attach to an
    // empty channel and leave a blank window. Not loaded yet => remember the
    // request; the sweep retries once the load job completes. Window MUST
    // belong to the MAIN thread (audio loop pump dispatches its messages);
    // a window on a worker whose pump is idle during jobs hangs
    // view->attached() (gui_probe: two_workers_close TIMEOUT). The attach
    // job runs on the channel worker (COM STA apartment stays alive).
    auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) {
        if (!instruments.get(guiCh)) {
            std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh
                      << " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl;
            std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
            g_pendingGui[guiCh] = { (void*)arg1, pluginId };
            return;
        }
        if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
        void* hwnd = (void*)arg1;
#ifdef _WIN32
        // Window PHAI thuoc MAIN thread (audio loop pump nay dispatch
        // messages cua no moi vong lap). Tao/cap nhat window ngay tai day.
        HWND nativeHwnd = nullptr;
        {
            std::lock_guard<std::mutex> lock(g_guiMutex);
            auto it = g_guiWindows.find(guiCh);
            if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;
        }
        if (hwnd == 0) {
            if (nativeHwnd && IsWindow(nativeHwnd)) {
                hwnd = (void*)nativeHwnd;
                SetWindowTextA(nativeHwnd, pluginId.c_str());
                ShowWindow(nativeHwnd, SW_SHOW);
                SetForegroundWindow(nativeHwnd);
                // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay
                // the boi assign() -> WM_DESTROY sau nay lookup inst MOI.
                SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
            } else {
                nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str());
                if (!nativeHwnd) {
                    std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << pluginId << std::endl;
                    return;
                }
                {
                    std::lock_guard<std::mutex> lock(g_guiMutex);
                    g_guiWindows[guiCh] = nativeHwnd;      // keep window alive
                    g_hwndToCh[nativeHwnd] = guiCh;       // WM_DESTROY cleanup
                }
                SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
                hwnd = (void*)nativeHwnd;
            }
        }
#endif
        workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() {
            if (!instruments.get(guiCh)) {
                std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
                          << " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
                return;
            }
            std::cerr << "[dbg] openGUI thread start hwnd=" << hwnd
                      << " plugin=" << arg2 << " ch=" << guiCh << std::endl;
#ifdef _WIN32
            // Option B: chi 1 editor VST mo tai 1 thoi diem toan
            // bridge. Instance thu 2 cua CUNG plugin (Nexus) attach
            // view o apartment/worker khac -> treo. Dong editor cua
            // channel khac TRUOC khi attach: WM_CLOSE -> main pump
            // (audio loop) destroy window -> WM_DESTROY -> closeGUI()
            // + xoa registry. Chay tren worker job de khong stall
            // writeIndex cua real-time loop.
            {
                std::vector<uint32_t> others;
                {
                    std::lock_guard<std::mutex> lock(g_guiMutex);
                    for (const auto& kv : g_guiWindows)
                        if (kv.first != guiCh) others.push_back(kv.first);
                }
                for (uint32_t y : others) {
                    HWND yHwnd = nullptr;
                    {
                        std::lock_guard<std::mutex> lock(g_guiMutex);
                        auto it = g_guiWindows.find(y);
                        if (it != g_guiWindows.end()) yHwnd = (HWND)it->second;
                    }
                    if (!yHwnd || !IsWindow(yHwnd)) continue;
                    PostMessage(yHwnd, WM_CLOSE, 0, 0);
                    std::cerr << "[dbg] openGUI: closing editor ch=" << y
                              << " before attach ch=" << guiCh << std::endl;
                    bool closed = false;
                    for (int i = 0; i < 500; ++i) {
                        {
                            std::lock_guard<std::mutex> lock(g_guiMutex);
                            if (g_guiWindows.find(y) == g_guiWindows.end()) { closed = true; break; }
                        }
                        Sleep(10);
                    }
                    if (!closed)
                        std::cerr << "[dbg] openGUI: editor ch=" << y
                                  << " not closed in 5s, proceeding" << std::endl;
                }
            }
#else
            (void)0;
#endif
            if (auto* inst = instruments.get(guiCh)) {
                // Reopen sau khi dong: reload() (terminate + loadPlugin)
                // PHAI chay tren worker thread nay - COM STA apartment
                // cua channel song o day. view->attached() cung chay o
                // day; window thuoc main thread nen main pump (audio
                // loop) dispatch messages cua no - khong can pump worker.
                // Chan UAF bang flag reloading_ (set/clear duoi engine
                // mutex; renderAll giu mutex khi process).
                bool guard = inst->needsReload();
                if (guard) instruments.setReloading(guiCh, true);
                bool ok = false;
                {
                    // Serialize createInstance (reload) with other loads.
                    std::lock_guard<std::mutex> lg(g_loadMutex);
                    ok = inst->reloadForGUI();
                }
                if (ok) ok = inst->attachView(hwnd);
                if (guard) instruments.setReloading(guiCh, false);
                if (ok)
                    std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
                              << " plugin=" << arg2 << " ch=" << guiCh << std::endl;
                else
                    std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << hwnd
                              << " plugin=" << arg2 << std::endl;
            }
        });
    };

    // 2. REAL-TIME AUDIO PROCESSING ENGINE LOOP
    while (true) {
#ifdef _WIN32
        // Message pump: VST editors (Nexus, JUCE-based...) block inside
        // view->attached() until the host dispatches messages — openGUI runs
        // on a worker thread, so THIS loop must pump concurrently (verified
        // with gui_probe: worker-thread openGUI + concurrent pump → attached
        // returns kResultOk; without it → hangs forever).
        MSG msg;
        while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
#endif
        // A. Control events — non-rt safe, drained first
        for (uint32_t i = 0; i < shmIPC->controlQueueCount; ++i) {
            const auto& c = shmIPC->controlQueue[i];
            if (c.type == 2) { // LOAD_INSTRUMENT (A10: assign per MIDI channel)
                // Robust path length: do not trust arg1 (Rust passes 0 for LOAD).
                // Chay tren persistent worker thread cua channel: VST3 init
                // (loadPlugin) co the mat giay — chay dong bo tren audio loop
                // lam writeIndex stall > 3s -> Rust tuong bridge chet va restart
                // nham (2 bridge cung map SHM -> race control queue / double
                // load). assign() chi giu mutex khi ghi map nen renderAll khong
                // bao gio stall. openGUI sau nay chay tren CUNG thread nay
                // (ChannelWorker) — thread khong bao gio exit nen COM STA
                // apartment cua plugin con song (xem ChannelWorker comment).
                size_t plen = 0;
                while (plen < sizeof(c.arg2) && c.arg2[plen]) ++plen;
                std::string path(c.arg2, plen);
                InstrumentType t = (InstrumentType)c.arg0;
                uint32_t ch = c.channel & 0xF;
                if (!workers[ch]) workers[ch] = std::make_unique<ChannelWorker>();
                workers[ch]->post([&instruments, t, ch, path, sampleRate, block]() {
#ifdef _WIN32
                    // DONG cua so editor dang mo cua channel TRUOC khi assign():
                    // thay the inst (VST3 -> SF2/inst khac) ma editor con song ->
                    // old inst destructor goi view->removed() tren HWND con hoat
                    // dong -> plugin block -> treo bridge. DestroyWindow chay tren
                    // CUNG worker thread so huu window; WM_DESTROY goi closeGUI()
                    // dung thu tu. Phai erase map TRUOC DestroyWindow (WM_DESTROY
                    // handler lay g_guiMutex lai — khong duoc giu lock khi destroy).
                    HWND hToDestroy = nullptr;
                    {
                        std::lock_guard<std::mutex> lock(g_guiMutex);
                        auto git = g_guiWindows.find(ch);
                        if (git != g_guiWindows.end()) {
                            hToDestroy = (HWND)git->second;
                            g_guiWindows.erase(git);
                            g_hwndToCh.erase(hToDestroy);
                        }
                    }
                    if (hToDestroy && IsWindow(hToDestroy)) DestroyWindow(hToDestroy);
#endif
                    std::cerr << "[dbg] load thread start ch=" << ch
                              << " type=" << (int)t << " path=" << path << std::endl;
                    std::lock_guard<std::mutex> lg(g_loadMutex);
                    bool ok = instruments.assign(ch, t, path, sampleRate, block);
                    std::cerr << "[dbg] assign returned ch=" << ch << " ok=" << (ok ? 1 : 0) << std::endl;
                    if (ok) {
                        std::cout << "[NativeBridge] instrument loaded ch=" << ch
                                  << " type=" << (int)t << " " << path << std::endl;
                    } else {
                        std::cerr << "[NativeBridge] instrument load FAILED ch=" << ch
                                  << " type=" << (int)t << " " << path << std::endl;
                    }
                });
            } else if (c.type == 1) { // PANIC
                instruments.allNotesOff();
                std::cout << "[NativeBridge] PANIC — all notes off" << std::endl;
            } else if (c.type == 3) { // TRANSPORT (A13)
                if (c.arg0 == 0) { // STOP → flush every note immediately
                    transportStopped = true;
                    // Release sustain pedal TRUOC (CC64=0) — nhieu VSTi giu note
                    // khi pedal con down -> note-off cua allNotesOff bi bo qua
                    // -> am treo loop (V8 bug 3).
                    for (uint32_t ch = 0; ch < 16; ++ch) {
                        if (auto* inst = instruments.get(ch)) inst->controlChange(ch, 64, 0);
                    }
                    instruments.allNotesOff();
                    std::cout << "[NativeBridge] transport STOP — all notes off" << std::endl;
                } else if (c.arg0 == 1) { // PLAY
                    transportStopped = false;
                    playheadSamples = c.arg1;
                    std::cout << "[NativeBridge] transport PLAY playhead=" << playheadSamples << std::endl;
                } else if (c.arg0 == 2) { // SET_POSITION (seek while stopped)
                    playheadSamples = c.arg1;
                }
            } else if (c.type == 4) { // OPEN_GUI (A7): arg1 = parent HWND (0 → bridge tự tạo native window), arg2 = plugin id
                uint32_t guiCh = c.channel;
                if (guiCh >= 16) guiCh = 0;
                handleOpenGui(guiCh, (uintptr_t)c.arg1, std::string(c.arg2));
            }
        }
        shmIPC->controlQueueCount = 0;

        // Pending OPEN_GUI requests: fulfilled as soon as the channel's
        // instrument becomes available (load job completed on its worker).
        {
            std::vector<std::pair<uint32_t, std::pair<void*, std::string>>> readyGui;
            {
                std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
                for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) {
                    if (instruments.get(it->first)) {
                        readyGui.push_back({ it->first, it->second });
                        it = g_pendingGui.erase(it);
                    } else ++it;
                }
            }
            for (auto& g : readyGui)
                handleOpenGui(g.first, (uintptr_t)g.second.first, g.second.second);
        }


        // B. Snapshot queued MIDI events (bounded copy, queue reset immediately)
        uint32_t nEvents = shmIPC->midiQueueCount > 64 ? 64 : shmIPC->midiQueueCount;
        SharedAudioBufferIPC::MidiEventIPC evts[64];
        for (uint32_t i = 0; i < nEvents; ++i) evts[i] = shmIPC->midiQueue[i];
        shmIPC->midiQueueCount = 0;

        // A11 sample-accurate: sort by sampleOffset, dispatch at each boundary,
        // render the sub-block before the boundary. Events with offset 0 (live
        // keyboard, current JS) are dispatched first and shape the whole block.
        std::stable_sort(evts, evts + nEvents,
                         [](const SharedAudioBufferIPC::MidiEventIPC& a,
                            const SharedAudioBufferIPC::MidiEventIPC& b) {
                             return a.sampleOffset < b.sampleOffset;
                         });
        uint32_t cursor = 0;
        uint32_t ei = 0;
        while (ei < nEvents && evts[ei].sampleOffset <= cursor) { dispatch(evts[ei]); ++ei; }
        for (; ei < nEvents; ++ei) {
            uint32_t off = evts[ei].sampleOffset < block ? evts[ei].sampleOffset : block;
            if (off > cursor) { renderSegment(cursor, off); cursor = off; }
            dispatch(evts[ei]);
        }
        if (cursor < block) renderSegment(cursor, block);
        shmIPC->bridgeWriteIndex++;

        // D. Parent died / window closed -> exit (no orphan process)
        if (parentPid != 0 && !parent_alive(parentPid)) {
            std::cout << "[NativeBridge] parent gone — exiting." << std::endl;
            break;
        }

        // E. Synchronize with real-time audio playback
        blockCount++;
        auto targetTime = startTime + std::chrono::microseconds(static_cast<int64_t>(blockCount * blockDurationMs * 1000.0));
        auto now = std::chrono::steady_clock::now();
        if (now < targetTime) {
            auto diff = std::chrono::duration_cast<std::chrono::microseconds>(targetTime - now).count();
            if (diff > 1000) {
                sleep_ms(diff / 1000);
            }
            while (std::chrono::steady_clock::now() < targetTime) {
                std::this_thread::yield();
            }
        }
    }

#ifdef _WIN32
    UnmapViewOfFile(shmIPC);
    CloseHandle(hMapFile);
    timeEndPeriod(1);
#else
    std::free(shmIPC);
#endif
    return 0;
}
