// native_bridge/src/Vst3Instrument.cpp
// VST3 host via the Steinberg VST3 SDK (submodule vst3sdk/).
//
// Without the SDK (HAVE_VST3SDK undefined — vst3sdk submodule missing) every
// method is a no-op so the bridge still builds for SF2/SF3/SFZ only.
//
// With the SDK: load the .vst3 module, create the component + edit controller,
// connect them, process MIDI events + audio blocks, and attach the editor
// view to a native HWND (openGUI) for the floating GUI (B9).
#include "Vst3Instrument.h"

#ifdef HAVE_VST3SDK
#ifdef _WIN32
#include <windows.h>
#endif
#include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/hosting/hostclasses.h"
#include "public.sdk/source/vst/hosting/processdata.h"
#include "public.sdk/source/vst/hosting/eventlist.h"
#include "public.sdk/source/vst/hosting/parameterchanges.h"

#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivstcomponent.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstmidicontrollers.h"
#include "pluginterfaces/vst/ivstprocesscontext.h"
#include "pluginterfaces/vst/ivstevents.h"
#include "pluginterfaces/vst/ivstmessage.h"
#include "pluginterfaces/gui/iplugview.h"

#include <cstring>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#endif

// ---------------------------------------------------------------------------
// With-SDK implementation
// ---------------------------------------------------------------------------
#ifdef HAVE_VST3SDK
namespace {

using Steinberg::tresult;
using Steinberg::kResultOk;
using Steinberg::kResultTrue;
using Steinberg::kResultFalse;
using Steinberg::kNoInterface;
using Steinberg::FUnknownPtr;
using Steinberg::IPtr;
using Steinberg::owned;
using Steinberg::FIDString;
using Steinberg::IPlugView;
using Steinberg::kPlatformTypeHWND;
using Steinberg::int16;
using Steinberg::int32;
using Steinberg::uint32;
using Steinberg::uint16;

using Steinberg::Vst::IComponent;
using Steinberg::Vst::IEditController;
using Steinberg::Vst::IAudioProcessor;
using Steinberg::Vst::IComponentHandler;
using Steinberg::IPluginBase;
using Steinberg::Vst::IConnectionPoint;
using Steinberg::Vst::IParameterChanges;
using Steinberg::Vst::Event;
using Steinberg::Vst::ProcessSetup;
using Steinberg::Vst::ProcessContext;
using Steinberg::Vst::HostProcessData;
using Steinberg::Vst::EventList;
using Steinberg::Vst::ParameterChanges;
using Steinberg::Vst::HostApplication;
using Steinberg::Vst::ParamID;
using Steinberg::Vst::ParamValue;
using Steinberg::Vst::IMidiMapping;
using Steinberg::Vst::IParamValueQueue;
using Steinberg::Vst::kNoParamId;
using Steinberg::Vst::BusInfo;
using Steinberg::Vst::kAudio;
using Steinberg::Vst::kEvent;
using Steinberg::Vst::kInput;
using Steinberg::Vst::kOutput;
using Steinberg::Vst::kRealtime;
using Steinberg::Vst::kSample32;
using Steinberg::Vst::kPitchBend;
using Steinberg::Vst::CtrlNumber;

// VST3 spec §MIDI: host-side tag scheme for MIDI CC / pitch bend / program
// change parameters. NOT provided by the SDK (verified 3.8.1) — the host
// defines them. Note: using-declaration of the real VST3 constants would not
// compile; these are the spec values.
constexpr ParamID kHostMidiCC = 0x1000;           // + controller number
constexpr ParamID kHostMidiPitchBend = 0x2000;    // + channel
constexpr ParamID kHostMidiProgramChange = 0x3000;

// Minimal IComponentHandler so the plugin can inform the host of param edits.
class HostComponentHandler : public IComponentHandler {
public:
    tresult queryInterface(const char*, void** v) override {
        *v = nullptr;
        return kNoInterface;
    }
    Steinberg::uint32 addRef() override { return 1; }
    Steinberg::uint32 release() override { return 1; }
    tresult beginEdit(ParamID) override { return kResultOk; }
    tresult performEdit(ParamID, ParamValue) override { return kResultOk; }
    tresult endEdit(ParamID) override { return kResultOk; }
    tresult restartComponent(Steinberg::int32) override { return kResultOk; }
};

// Minimal IPlugFrame so plugins can resize their editor view.
class HostPlugFrame : public Steinberg::IPlugFrame {
public:
    tresult queryInterface(const char*, void** v) override {
        *v = nullptr;
        return kNoInterface;
    }
    Steinberg::uint32 addRef() override { return 1; }
    Steinberg::uint32 release() override { return 1; }
    tresult resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* newSize) override {
        if (view && newSize) view->onSize(newSize);
        return kResultOk;
    }
};

// All Steinberg SDK objects live here (pimpl — Vst3Instrument.h stays SDK-free).
struct Vst3HostState {
    // module must be destroyed LAST: it owns the plugin factory that created
    // component/controller (member order ⇒ destroyed in reverse).
    VST3::Hosting::Module::Ptr module;
    IPtr<IComponent> component;
    IPtr<IEditController> controller;
    IPtr<HostApplication> hostApp;
    HostProcessData processData;
    EventList eventList;
    ParameterChanges paramChanges;
    ProcessContext processContext;
    IPtr<IPlugView> view;
    HostPlugFrame plugFrame;
    HostComponentHandler componentHandler;
    int32 outputChannels = 2;
    bool controllerIsComponent = false;  // single-component plugin: controller == component
};

// Resolve the plugin's ParamID for a MIDI CC / pitch-bend, preferring the
// plugin's IMidiMapping assignment (audiohost/miditovst.h pattern), falling
// back to the VST3 legacy tag scheme. Returns kNoParamId when unmapped.
ParamID midiControllerTag(IPtr<IEditController>& controller, int16 channel,
                          int16 ctrlNumber, ParamID legacyTag) {
    if (controller) {
        FUnknownPtr<IMidiMapping> mm(controller.get());
        ParamID tag = kNoParamId;
        if (mm &&
            mm->getMidiControllerAssignment(0, channel, (CtrlNumber)ctrlNumber, tag) ==
                kResultTrue &&
            tag != kNoParamId)
            return tag;
    }
    return legacyTag;
}

}  // namespace
#endif

Vst3Instrument::Vst3Instrument()
    : state_(nullptr),
      path_(),
      sampleRate_(44100.0),
      maxBlockSize_(256),
      loaded_(false),
      guiAttached_(false),
      hasAttachedOnce_(false),
      reloading_(false) {}

Vst3Instrument::~Vst3Instrument() {
#ifdef HAVE_VST3SDK
    if (!state_) return;
    closeGUI();
    auto* s = static_cast<Vst3HostState*>(state_);
    if (s->component) {
        FUnknownPtr<IAudioProcessor> processor(s->component);
        if (processor) processor->setProcessing(false);
        s->component->setActive(false);
        s->component->terminate();
    }
    // Single-component plugins: controller == component, already terminated.
    if (s->controller && !s->controllerIsComponent) s->controller->terminate();
    s->processData.unprepare();
    delete s;
    state_ = nullptr;
#endif
}

bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
#ifndef HAVE_VST3SDK
    (void)path; (void)sampleRate;
    return false;  // vst3sdk submodule missing — VST3 disabled
#else
    if (loaded_) return true;
    using namespace VST3::Hosting;

    std::string err;
    std::cerr << "[dbg] loadPlugin: Module::create ..." << std::endl;
    Module::Ptr module = Module::create(path, err);
    if (!module) {
        std::cerr << "[Vst3Instrument] Module::create failed: " << err << std::endl;
        return false;
    }
    const PluginFactory& factory = module->getFactory();
    std::cerr << "[dbg] loadPlugin: Module::create OK" << std::endl;

    // Pick the first Audio Module class; prefer an Instrument subcategory.
    // classInfos() returns a TEMPORARY vector (by value) — never keep a
    // pointer into it; it dangles after the range-for and reading
    // chosen->ID()/name() is UB (crashed 0xC0000005). Copy instead.
    ClassInfo chosen;
    bool haveChosen = false;
    {
        auto infos = factory.classInfos();
        for (const ClassInfo& ci : infos) {
            if (ci.category() != kVstAudioEffectClass) continue;
            if (!haveChosen) { chosen = ci; haveChosen = true; }
            if (ci.subCategoriesString().find("Instrument") != std::string::npos) {
                chosen = ci;
                break;
            }
        }
    }
    if (!haveChosen) {
        std::cerr << "[Vst3Instrument] no Audio Module class in " << path << std::endl;
        return false;
    }

    std::cerr << "[dbg] loadPlugin: createInstance ..." << std::endl;
    std::cerr << "[dbg] loadPlugin: chosen name=" << chosen.name() << " category=" << chosen.category()
              << " subcat=" << chosen.subCategoriesString() << " idbytes=";
    {
        const unsigned char* cid2 = (const unsigned char*)chosen.ID().data();
        for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)cid2[b] << ' ';
        std::cerr << std::dec << std::endl;
    }
    IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
    if (!component) {
        std::cerr << "[Vst3Instrument] createInstance<IComponent> failed" << std::endl;
        return false;
    }
    IPtr<HostApplication> hostApp = owned(new HostApplication());
    std::cerr << "[dbg] loadPlugin: initialize ..." << std::endl;
    FUnknownPtr<IPluginBase> plugBase(component.get());
    if (!plugBase || plugBase->initialize(hostApp) != kResultOk) {
        std::cerr << "[Vst3Instrument] component initialize failed" << std::endl;
        return false;
    }

    // Edit controller: either the component itself (single-component) or a
    // separate factory instance (plugprovider.cpp pattern).
    IPtr<IEditController> controller;
    bool isSingle = false;
    if (component->queryInterface(IEditController::iid, (void**)&controller) == kResultTrue) {
        isSingle = true;
    } else {
        Steinberg::TUID cid = {};
        tresult cidRes = component->getControllerClassId(cid);
        std::cerr << "[dbg] loadPlugin: getControllerClassId=" << (int)cidRes
                  << " cid=";
        for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)(unsigned char)cid[b] << ' ';
        std::cerr << std::dec << std::endl;
        if (cidRes == kResultTrue || cidRes == kResultOk) {
            controller = factory.createInstance<IEditController>(VST3::UID(cid));
            std::cerr << "[dbg] loadPlugin: controller from factory=" << (controller ? 1 : 0) << std::endl;
            if (controller) {
                FUnknownPtr<IPluginBase> ctrlBase(controller.get());
                if (!ctrlBase || ctrlBase->initialize(hostApp) != kResultOk) controller = nullptr;
            }
        }
    }
    if (!controller) {
        std::cerr << "[Vst3Instrument] no edit controller for " << path << std::endl;
        return false;
    }

    // State allocated up-front so the component handler (which the controller
    // keeps a pointer to) lives in the final state object.
    std::unique_ptr<Vst3HostState> s(new Vst3HostState());
    s->controllerIsComponent = isSingle;
    controller->setComponentHandler(&s->componentHandler);
    // Connect component <-> controller for parameter sync (both directions).
    FUnknownPtr<IConnectionPoint> compCP(component);
    FUnknownPtr<IConnectionPoint> ctrlCP(controller);
    if (compCP && ctrlCP) {
        compCP->connect(ctrlCP);
        ctrlCP->connect(compCP);
    }

    // Activate all audio and event buses (MIDI & Audio) - required by VST3 specification.
    {
        int32 numAudioInputs = component->getBusCount(kAudio, kInput);
        for (int32 i = 0; i < numAudioInputs; ++i) {
            component->activateBus(kAudio, kInput, i, true);
        }
        int32 numAudioOutputs = component->getBusCount(kAudio, kOutput);
        for (int32 i = 0; i < numAudioOutputs; ++i) {
            component->activateBus(kAudio, kOutput, i, true);
        }
        int32 numEventInputs = component->getBusCount(kEvent, kInput);
        for (int32 i = 0; i < numEventInputs; ++i) {
            component->activateBus(kEvent, kInput, i, true);
        }
        int32 numEventOutputs = component->getBusCount(kEvent, kOutput);
        for (int32 i = 0; i < numEventOutputs; ++i) {
            component->activateBus(kEvent, kOutput, i, true);
        }
    }

    // Audio processor: setup → activate → start processing (audioclient.cpp).
    FUnknownPtr<IAudioProcessor> processor(component);
    if (!processor) {
        std::cerr << "[Vst3Instrument] no IAudioProcessor" << std::endl;
        return false;
    }
    std::cerr << "[dbg] loadPlugin: setupProcessing ..." << std::endl;
    ProcessSetup setup{kRealtime, kSample32, (int32)maxBlockSize_, sampleRate};
    if (processor->setupProcessing(setup) != kResultOk) {
        std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl;
        return false;
    }
    std::cerr << "[dbg] loadPlugin: setActive ..." << std::endl;
    if (component->setActive(true) != kResultOk) {
        std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
        return false;
    }
    processor->setProcessing(true);

    // Build per-bus buffers sized maxBlockSize_ (HostProcessData owns them;
    // we must NOT override channelBuffers with external pointers — unprepare
    // would delete[] them).
    std::cerr << "[dbg] loadPlugin: processData.prepare ..." << std::endl;
    if (!s->processData.prepare(*component, (int32)maxBlockSize_, kSample32)) {
        std::cerr << "[Vst3Instrument] processData.prepare failed" << std::endl;
        return false;
    }

    // Count output channels of bus 0 (mono plugins duplicate ch0 → both outs).
    int32 outChannels = 2;
    if (component->getBusCount(kAudio, kOutput) > 0) {
        BusInfo bi = {};
        if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0)
            outChannels = bi.channelCount;
    }

    std::cerr << "[dbg] loadPlugin: prepare OK, wiring state" << std::endl;
    s->module = std::move(module);
    s->component = std::move(component);
    s->controller = std::move(controller);
    s->hostApp = std::move(hostApp);
    s->outputChannels = outChannels;
    s->controllerIsComponent = isSingle;
    s->processContext.sampleRate = sampleRate;
    s->processContext.tempo = 120.0;
    s->processContext.timeSigNumerator = 4;
    s->processContext.timeSigDenominator = 4;
    s->processContext.state =
        ProcessContext::kPlaying | ProcessContext::kTempoValid | ProcessContext::kTimeSigValid |
        ProcessContext::kProjectTimeMusicValid;

    state_ = s.release();
    path_ = path;
    sampleRate_ = sampleRate;
    loaded_ = true;
    std::cout << "[Vst3Instrument] loaded " << path << " (" << chosen.name()
              << ") outCh=" << outChannels << std::endl;
    return true;
#endif
}

bool Vst3Instrument::init(double sampleRate, uint32_t maxBlockSize) {
    sampleRate_ = sampleRate;
    maxBlockSize_ = maxBlockSize;
    return loaded_;
}

void Vst3Instrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
    (void)channel; (void)bank; (void)program;
    // ponytail: needs IEditController::setParamNormalized on program list params
}

void Vst3Instrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
    (void)channel; (void)pitch; (void)velocity; (void)sampleOffset;
#ifndef HAVE_VST3SDK
    return;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->component) return;
    Event e = {};
    e.busIndex = 0;
    e.sampleOffset = (int32)sampleOffset;
    e.ppqPosition = 0;
    e.flags = Event::kIsLive;
    e.type = Event::kNoteOnEvent;
    e.noteOn.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility
    e.noteOn.pitch = (int16)pitch;
    e.noteOn.tuning = 0.f;
    e.noteOn.velocity = velocity;
    e.noteOn.length = 0;
    e.noteOn.noteId = 0;  // some plugins (DUNE3) track notes by id; -1 may be ignored
    s->eventList.addEvent(e);
#endif
}

void Vst3Instrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
    (void)channel; (void)pitch; (void)sampleOffset;
#ifndef HAVE_VST3SDK
    return;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->component) return;
    Event e = {};
    e.busIndex = 0;
    e.sampleOffset = (int32)sampleOffset;
    e.ppqPosition = 0;
    e.flags = Event::kIsLive;
    e.type = Event::kNoteOffEvent;
    e.noteOff.channel = 0; // Force to MIDI Channel 1 (0) for VSTi compatibility
    e.noteOff.pitch = (int16)pitch;
    e.noteOff.velocity = 0.f;
    // noteId phai MATCH noteOn (0): Nexus/DUNE3 track notes bang id, -1
    // khong khop voi note-on -> plugin giu note vo han (am thanh khong dung
    // cho den khi kill bridge).
    e.noteOff.noteId = 0;
    e.noteOff.tuning = 0.f;
    s->eventList.addEvent(e);
#endif
}

void Vst3Instrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
    (void)channel; (void)cc; (void)value;
#ifndef HAVE_VST3SDK
    return;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->controller) return;
    ParamID tag = midiControllerTag(s->controller, 0, (int16)cc,
                                    kHostMidiCC);
    ParamValue v = value / 127.0;
    // setParamNormalized covers single-component plugins; the param queue
    // reaches split plugins that read inputParameterChanges inside process().
    s->controller->setParamNormalized(tag, v);
    int32 idx = 0;
    if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx);
#endif
}

void Vst3Instrument::programChange(uint32_t channel, uint32_t program) {
    (void)channel; (void)program;
#ifndef HAVE_VST3SDK
    return;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->controller) return;
    ParamID tag = kHostMidiProgramChange;
    // ponytail: normalized value should be program/(count-1) from the program
    // list param stepCount; 127ths is a reasonable approximation.
    ParamValue v = (program & 0x7F) / 127.0;
    s->controller->setParamNormalized(tag, v);
    int32 idx = 0;
    if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx);
#endif
}

void Vst3Instrument::pitchBend(uint32_t channel, uint32_t bend14) {
    (void)channel; (void)bend14;
#ifndef HAVE_VST3SDK
    return;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->controller) return;
    ParamID tag = midiControllerTag(s->controller, 0, (int16)kPitchBend,
                                    kHostMidiPitchBend);
    ParamValue v = bend14 / 16383.0;
    s->controller->setParamNormalized(tag, v);
    int32 idx = 0;
    if (IParamValueQueue* q = s->paramChanges.addParameterData(tag, idx)) q->addPoint(0, v, idx);
#endif
}

bool Vst3Instrument::openGUI(void* parentWindowHandle) {
#ifndef HAVE_VST3SDK
    (void)parentWindowHandle;
    return false;
#else
    if (!reloadForGUI()) return false;
    return attachView(parentWindowHandle);
#endif
}

bool Vst3Instrument::reloadForGUI() {
#ifndef HAVE_VST3SDK
    return false;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->controller) return false;
    if (s->view && guiAttached_) return true;
    if (hasAttachedOnce_ && !guiAttached_) {
        // Reopen GUI sau khi dong: mot so plugin (Nexus...) hang o
        // view->attached() LAN 2 tren cung component instance. Tao lai inst
        // moi tren CUNG worker thread (COM STA con song) roi attach lai.
        // PHAN NAY PHAI chay tren worker thread — apartment cua channel song
        // o day; chay tren thread tam thi apartment moi chet ngay sau do
        // (reopen lan sau -> treo nhu bridge_like).
        std::cerr << "[dbg] openGUI: re-attach - reloading fresh plugin instance" << std::endl;
        if (!reload()) return false;
        s = static_cast<Vst3HostState*>(state_);
        if (!s || !s->controller) return false;
    }
    return true;
#endif
}

bool Vst3Instrument::attachView(void* parentWindowHandle) {
#ifndef HAVE_VST3SDK
    (void)parentWindowHandle;
    return false;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    std::cerr << "[dbg] openGUI hwnd=" << (void*)(uintptr_t)parentWindowHandle
              << " controller=" << (s ? (s->controller ? 1 : 0) : -1) << std::endl;
    if (!s || !s->controller || !parentWindowHandle) return false;
    IPlugView* rawView = nullptr;
    tresult qi = s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
    {
        const unsigned char* iidb = (const unsigned char*)&IPlugView::iid;
        std::cerr << "[dbg] openGUI: IPlugView::iid bytes=";
        for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)iidb[b] << ' ';
        std::cerr << std::dec << std::endl;
        const unsigned char* eidb = (const unsigned char*)&IEditController::iid;
        std::cerr << "[dbg] openGUI: IEditController::iid bytes=";
        for (int b = 0; b < 16; ++b) std::cerr << std::hex << (int)eidb[b] << ' ';
        std::cerr << std::dec << std::endl;
    }
    std::cerr << "[dbg] openGUI: controller qi IPlugView=" << (int)qi << " raw=" << (void*)rawView << " isSingle=" << (s->controllerIsComponent ? 1 : 0) << std::endl;
    FUnknownPtr<IPlugView> view(rawView);
    if (!view) {
        // Mot so plugin khong expose IPlugView tren edit controller; thu component.
        std::cerr << "[dbg] openGUI: controller no IPlugView, trying component" << std::endl;
        IPlugView* rawViewC = nullptr;
        tresult qic = s->component->queryInterface(IPlugView::iid, (void**)&rawViewC);
        std::cerr << "[dbg] openGUI: component qi IPlugView=" << (int)qic << " raw=" << (void*)rawViewC << std::endl;
        view = FUnknownPtr<IPlugView>(rawViewC);
    }
    if (!view) {
        // Official editorhost.cpp pattern: IEditController::createView(kEditor).
        // JUCE-based plugins (Scaler2) expose the editor only this way.
        std::cerr << "[dbg] openGUI: qi failed, trying controller->createView(kEditor)" << std::endl;
        view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor));
        std::cerr << "[dbg] openGUI: createView view=" << (view ? "ok" : "null") << std::endl;
    }
    if (!view) { std::cerr << "[dbg] openGUI: no IPlugView" << std::endl; return false; }
    view->setFrame(&s->plugFrame);
    tresult ts = view->isPlatformTypeSupported(kPlatformTypeHWND);
    std::cerr << "[dbg] openGUI: isPlatformTypeSupported=" << (int)ts << std::endl;
    // Log only — proceed to attach anyway to support non-compliant plugins.
    tresult ta = view->attached(parentWindowHandle, kPlatformTypeHWND);
    std::cerr << "[dbg] openGUI: attached=" << (int)ta << std::endl;
    if (ta != kResultOk) return false;
#ifdef _WIN32
    HWND hwnd = (HWND)parentWindowHandle;
    Steinberg::ViewRect rect;
    if (view->getSize(&rect) == kResultOk) {
        int w = rect.right - rect.left;
        int h = rect.bottom - rect.top;
        RECT r = { 0, 0, w, h };
        AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
        SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
    }
#endif
    s->view = view;
    guiAttached_ = true;
    hasAttachedOnce_ = true;
    // ponytail: the bridge loop is a worker thread without a Windows message
    // pump — some editors may not repaint until the first native event; a
    // future version can spin a dedicated UI thread + pump.
    return true;
#endif
}

bool Vst3Instrument::reload() {
#ifndef HAVE_VST3SDK
    return false;
#else
    if (!state_) return false;
    closeGUI();
    auto* s = static_cast<Vst3HostState*>(state_);
    if (s->component) {
        FUnknownPtr<IAudioProcessor> processor(s->component);
        if (processor) processor->setProcessing(false);
        s->component->setActive(false);
        s->component->terminate();
    }
    // Single-component plugins: controller == component, already terminated.
    if (s->controller && !s->controllerIsComponent) s->controller->terminate();
    s->processData.unprepare();
    delete s;
    state_ = nullptr;
    loaded_ = false;
    guiAttached_ = false;
    hasAttachedOnce_ = false;
    return loadPlugin(path_, sampleRate_);
#endif
}

void Vst3Instrument::closeGUI() {
#ifndef HAVE_VST3SDK
    return;
#else
    auto* s = static_cast<Vst3HostState*>(state_);
    if (s && s->view && guiAttached_) s->view->removed();
    if (s) s->view = nullptr;
    guiAttached_ = false;
#endif
}

void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
    (void)outputL; (void)outputR; (void)numSamples;
#ifndef HAVE_VST3SDK
    return;
#else
    // GUI reopen rebuilds the instance on the worker thread; while the
    // teardown runs, the audio loop must not touch state_ (flag toggled under
    // InstrumentEngineManager::mu_ by setReloading — renderAll holds the same
    // mutex during processAudioBlock, so the check is race-free).
    if (reloading_) return;
    auto* s = static_cast<Vst3HostState*>(state_);
    if (!s || !s->component || numSamples == 0) return;
    FUnknownPtr<IAudioProcessor> processor(s->component);
    if (!processor) return;

    s->processData.processMode = kRealtime;
    s->processData.numSamples = (int32)numSamples;
    s->processData.inputEvents = &s->eventList;
    s->processData.inputParameterChanges = &s->paramChanges;
    s->processData.processContext = &s->processContext;
    s->processContext.projectTimeSamples += numSamples;
    s->processContext.projectTimeMusic =
        (double)s->processContext.projectTimeSamples / s->processContext.sampleRate *
        (s->processContext.tempo / 60.0);

    static uint32_t dbgN = 0;
    static int dbgMaxShown = 0;
    // VST3: host buffers must be zeroed (silence) before process — plugins
    // that skip output leave garbage otherwise (EZkeys 2 -> huge noise).
    if (s->processData.numOutputs > 0) {
        for (int32 b = 0; b < s->processData.numOutputs; ++b) {
            const Steinberg::Vst::AudioBusBuffers& ob = s->processData.outputs[b];
            for (int32 c = 0; c < ob.numChannels; ++c)
                if (ob.channelBuffers32[c])
                    std::memset(ob.channelBuffers32[c], 0, numSamples * sizeof(float));
        }
    }
    tresult pr = processor->process(s->processData);
    float mx = 0.f;
    if (pr >= 0 && s->processData.numOutputs > 0) {
        const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0];
        const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
        const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
        if (buf0) std::memcpy(outputL, buf0, numSamples * sizeof(float));
        else std::memset(outputL, 0, numSamples * sizeof(float));
        if (buf1) std::memcpy(outputR, buf1, numSamples * sizeof(float));
        else if (buf0) std::memcpy(outputR, buf0, numSamples * sizeof(float));  // mono → stereo
        else std::memset(outputR, 0, numSamples * sizeof(float));
        for (uint32_t i = 0; i < numSamples; ++i) {
            float v = outputL[i] < 0 ? -outputL[i] : outputL[i];
            if (v > mx) mx = v;
        }
    } else {
        std::memset(outputL, 0, numSamples * sizeof(float));
        std::memset(outputR, 0, numSamples * sizeof(float));
    }
    uint32_t evc = s->eventList.getEventCount();
    int evt = -1;
    if (evc > 0) {
        const Event* e0 = s->eventList.getEventByIndex(0);
        evt = e0 ? (int)e0->type : -2;
    }
    if ((++dbgN % 250) == 0 || (evc > 0 && dbgMaxShown < 30)) {
        if (evc > 0) ++dbgMaxShown;
        std::cerr << "[dbg] n=" << numSamples << " ev=" << evc << " evt=" << evt
                  << " pr=" << (int)pr << " nOut=" << s->processData.numOutputs
                  << " mx=" << mx << " ts=" << s->processContext.projectTimeSamples
                  << " nch=" << (s->processData.numOutputs > 0 ? s->processData.outputs[0].numChannels : -1) << std::endl;
    }
    s->eventList.clear();
    s->paramChanges.clearQueue();
#endif
}
