// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2026 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

#include "plugin-proxy.h"

#include <pluginterfaces/vst/ivstmidicontrollers.h>

#include "plug-view-proxy.h"

/**
 * When the host tries to connect two plugin instances with connection proxies,
 * we'll first try to bypass that proxy. This goes against the idea of yabridge,
 * but these proxies can make things very difficult when plugins start sending
 * messages from the GUI thread. If we cannot figure out what we're connected
 * to, we'll still proxy the host's connection proxy.
 */
constexpr char other_instance_message_id[] = "yabridge_other_instance";
/**
 * In the message described above we'll use this attribute to pass through a
 * pointer to the sender of the message. This will the other side set the
 * `connected_instance_id_` field on the other object to the instance ID of that
 * connected object. This will let us bypass the connection proxy since we can
 * then just connect the two objects directly.
 */
constexpr char other_instance_pointer_attribute[] = "other_proxy_ptr";

Vst3PluginProxyImpl::ContextMenu::ContextMenu(
    Steinberg::IPtr<Steinberg::Vst::IContextMenu> menu)
    : menu(menu) {}

Vst3PluginProxyImpl::Vst3PluginProxyImpl(Vst3PluginBridge& bridge,
                                         Vst3PluginProxy::ConstructArgs&& args)
    : Vst3PluginProxy(std::move(args)), bridge_(bridge) {
    bridge.register_plugin_proxy(*this);
}

Vst3PluginProxyImpl::~Vst3PluginProxyImpl() noexcept {
    // NOTE: This can actually throw (e.g. out of memory or the socket got
    //       closed). But if that were to happen, then we wouldn't be able to
    //       recover from it anyways.
    bridge_.send_message(
        Vst3PluginProxy::Destruct{.instance_id = instance_id()});
    bridge_.unregister_plugin_proxy(*this);
}

tresult PLUGIN_API
Vst3PluginProxyImpl::queryInterface(const Steinberg::TUID _iid, void** obj) {
    const tresult result = Vst3PluginProxy::queryInterface(_iid, obj);
    bridge_.logger_.log_query_interface("In FUnknown::queryInterface()", result,
                                        Steinberg::FUID::fromTUID(_iid));

    return result;
}

size_t Vst3PluginProxyImpl::register_context_menu(
    Steinberg::IPtr<Steinberg::Vst::IContextMenu> menu) {
    std::lock_guard lock(context_menus_mutex_);

    const size_t context_menu_id = current_context_menu_id_.fetch_add(1);
    context_menus_.emplace(context_menu_id, menu);

    return context_menu_id;
}

/**
 * Unregister a context menu using the ID generated by a previous call to
 * `register_context_menu()`. This will release the context menu object
 * returned by the host.
 */
bool Vst3PluginProxyImpl::unregister_context_menu(size_t context_menu_id) {
    std::lock_guard lock(context_menus_mutex_);
    return context_menus_.erase(context_menu_id);
}

void Vst3PluginProxyImpl::clear_caches() noexcept {
    clear_bus_cache();

    std::lock_guard lock(function_result_cache_mutex_);
    function_result_cache_ = FunctionResultCache{};
}

tresult PLUGIN_API Vst3PluginProxyImpl::setAudioPresentationLatencySamples(
    Steinberg::Vst::BusDirection dir,
    int32 busIndex,
    uint32 latencyInSamples) {
    return bridge_.send_message(
        YaAudioPresentationLatency::SetAudioPresentationLatencySamples{
            .instance_id = instance_id(),
            .dir = dir,
            .bus_index = busIndex,
            .latency_in_samples = latencyInSamples});
}

tresult PLUGIN_API Vst3PluginProxyImpl::setBusArrangements(
    Steinberg::Vst::SpeakerArrangement* inputs,
    int32 numIns,
    Steinberg::Vst::SpeakerArrangement* outputs,
    int32 numOuts) {
    clear_bus_cache();

    // NOTE: Ardour passes a null pointer when `numIns` or `numOuts` is 0, so we
    //       need to work around that
    return bridge_.send_audio_processor_message(
        YaAudioProcessor::SetBusArrangements{
            .instance_id = instance_id(),
            .inputs =
                (inputs ? std::vector<Steinberg::Vst::SpeakerArrangement>(
                              inputs, &inputs[numIns])
                        : std::vector<Steinberg::Vst::SpeakerArrangement>()),
            .num_ins = numIns,
            .outputs =
                (outputs ? std::vector<Steinberg::Vst::SpeakerArrangement>(
                               outputs, &outputs[numOuts])
                         : std::vector<Steinberg::Vst::SpeakerArrangement>()),
            .num_outs = numOuts,
        });
}

tresult PLUGIN_API Vst3PluginProxyImpl::getBusArrangement(
    Steinberg::Vst::BusDirection dir,
    int32 index,
    Steinberg::Vst::SpeakerArrangement& arr) {
    const GetBusArrangementResponse response =
        bridge_.send_audio_processor_message(
            YaAudioProcessor::GetBusArrangement{
                .instance_id = instance_id(), .dir = dir, .index = index});

    arr = response.arr;

    return response.result;
}

tresult PLUGIN_API
Vst3PluginProxyImpl::canProcessSampleSize(int32 symbolicSampleSize) {
    const auto request = YaAudioProcessor::CanProcessSampleSize{
        .instance_id = instance_id(),
        .symbolic_sample_size = symbolicSampleSize};

    {
        std::lock_guard lock(function_result_cache_mutex_);
        if (auto it = function_result_cache_.can_process_sample_size.find(
                symbolicSampleSize);
            it != function_result_cache_.can_process_sample_size.end()) {
            const bool log_response =
                bridge_.logger_.log_request(true, request);
            if (log_response) {
                bridge_.logger_.log_response(
                    true,
                    YaAudioProcessor::CanProcessSampleSize::Response(
                        it->second),
                    true);
            }

            return it->second;
        }
    }

    const tresult result = bridge_.send_audio_processor_message(request);

    {
        std::lock_guard lock(function_result_cache_mutex_);
        function_result_cache_.can_process_sample_size[symbolicSampleSize] =
            result;
    }

    return result;
}

uint32 PLUGIN_API Vst3PluginProxyImpl::getLatencySamples() {
    return bridge_.send_audio_processor_message(
        YaAudioProcessor::GetLatencySamples{.instance_id = instance_id()});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setupProcessing(Steinberg::Vst::ProcessSetup& setup) {
    return bridge_.send_audio_processor_message(
        YaAudioProcessor::SetupProcessing{.instance_id = instance_id(),
                                          .setup = setup});
}

tresult PLUGIN_API Vst3PluginProxyImpl::setProcessing(TBool state) {
    // REAPER used to repeatedly query the plugin for its bus information on
    // every processing cycle. Because this really adds up in terms of latency
    // we sadly have to deviate from yabridge's principles and implement a
    // cache. We keep this in because it can still help performance a little in
    // some DAWs.
    {
        std::lock_guard lock(processing_bus_cache_mutex_);
        if (state) {
            processing_bus_cache_.emplace();
        } else {
            processing_bus_cache_.reset();
        }
    }

    return bridge_.send_audio_processor_message(YaAudioProcessor::SetProcessing{
        .instance_id = instance_id(), .state = state});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::process(Steinberg::Vst::ProcessData& data) {
    // We'll synchronize the scheduling priority of the audio thread on the Wine
    // plugin host with that of the host's audio thread every once in a while
    std::optional<int> new_realtime_priority = std::nullopt;
    time_t now = time(nullptr);
    if (now > last_audio_thread_priority_synchronization_ +
                  audio_thread_priority_synchronization_interval) {
        new_realtime_priority = get_realtime_priority();
        last_audio_thread_priority_synchronization_ = now;
    }

    // We reuse this existing object to avoid allocations.
    // `YaProcessData::repopulate()` will write the input audio to the shared
    // audio buffers, so they're not stored within the request object itself.
    assert(process_buffers_);
    process_request_.instance_id = instance_id();
    process_request_.data.repopulate(data, *process_buffers_);
    process_request_.new_realtime_priority = new_realtime_priority;

    // HACK: This is a bit ugly. This `YaProcessData::Response` object actually
    //       contains pointers to the corresponding `YaProcessData` fields in
    //       this object, so we can only send back the fields that are actually
    //       relevant. This is necessary to avoid allocating copies or moves on
    //       the Wine side. This `create_response()` function creates a response
    //       object that points to the fields in `process_request.data`, so when
    //       we deserialize into `process_response` we end up actually writing
    //       to the actual `process_request.data` object. Thus we can also call
    //       `process_request.data.write_back_outputs()` later.
    //
    //       `YaProcessData::Response::serialize()` should make this a lot
    //       clearer.
    process_response_.output_data = process_request_.data.create_response();

    // We'll also receive the response into an existing object so we can also
    // avoid heap allocations there
    bridge_.receive_audio_processor_message_into(
        MessageReference<YaAudioProcessor::Process>(process_request_),
        process_response_);

    // At this point the shared audio buffers should contain the output audio,
    // so we'll write that back to the host along with any metadata (which in
    // practice are only the silence flags), as well as any output parameter
    // changes and events
    process_request_.data.write_back_outputs(data, *process_buffers_);

    return process_response_.result;
}

uint32 PLUGIN_API Vst3PluginProxyImpl::getTailSamples() {
    return bridge_.send_audio_processor_message(
        YaAudioProcessor::GetTailSamples{.instance_id = instance_id()});
}

tresult PLUGIN_API Vst3PluginProxyImpl::setAutomationState(int32 state) {
    return bridge_.send_message(YaAutomationState::SetAutomationState{
        .instance_id = instance_id(), .state = state});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getControllerClassId(Steinberg::TUID classId) {
    if (classId) {
        const GetControllerClassIdResponse response =
            bridge_.send_audio_processor_message(
                YaComponent::GetControllerClassId{.instance_id =
                                                      instance_id()});

        ArrayUID native_uid = response.editor_cid.get_native_uid();
        std::copy(native_uid.begin(), native_uid.end(), classId);

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IComponent::getControllerClassId()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::setIoMode(Steinberg::Vst::IoMode mode) {
    return bridge_.send_audio_processor_message(
        YaComponent::SetIoMode{.instance_id = instance_id(), .mode = mode});
}

int32 PLUGIN_API
Vst3PluginProxyImpl::getBusCount(Steinberg::Vst::MediaType type,
                                 Steinberg::Vst::BusDirection dir) {
    const auto request = YaComponent::GetBusCount{
        .instance_id = instance_id(), .type = type, .dir = dir};

    std::tuple<Steinberg::Vst::MediaType, Steinberg::Vst::BusDirection> args{
        type, dir};
    {
        std::lock_guard lock(processing_bus_cache_mutex_);
        if (processing_bus_cache_) {
            if (auto it = processing_bus_cache_->bus_count.find(args);
                it != processing_bus_cache_->bus_count.end()) {
                const bool log_response =
                    bridge_.logger_.log_request(true, request);
                if (log_response) {
                    bridge_.logger_.log_response(
                        true, YaComponent::GetBusCount::Response(it->second),
                        true);
                }

                return it->second;
            }
        }
    }

    const int32 result = bridge_.send_audio_processor_message(request);

    {
        std::lock_guard lock(processing_bus_cache_mutex_);
        if (processing_bus_cache_) {
            processing_bus_cache_->bus_count[args] = result;
        }
    }

    return result;
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getBusInfo(Steinberg::Vst::MediaType type,
                                Steinberg::Vst::BusDirection dir,
                                int32 index,
                                Steinberg::Vst::BusInfo& bus /*out*/) {
    const auto request = YaComponent::GetBusInfo{
        .instance_id = instance_id(), .type = type, .dir = dir, .index = index};

    std::tuple<Steinberg::Vst::MediaType, Steinberg::Vst::BusDirection, int32>
        args{type, dir, index};
    {
        std::lock_guard lock(processing_bus_cache_mutex_);
        if (processing_bus_cache_) {
            if (auto it = processing_bus_cache_->bus_info.find(args);
                it != processing_bus_cache_->bus_info.end()) {
                const bool log_response =
                    bridge_.logger_.log_request(true, request);
                if (log_response) {
                    bridge_.logger_.log_response(
                        false,
                        YaComponent::GetBusInfo::Response{
                            .result = Steinberg::kResultOk, .bus = it->second},
                        true);
                }

                bus = it->second;

                return Steinberg::kResultOk;
            }
        }
    }

    const GetBusInfoResponse response =
        bridge_.send_audio_processor_message(request);

    bus = response.bus;

    {
        std::lock_guard lock(processing_bus_cache_mutex_);
        if (processing_bus_cache_) {
            processing_bus_cache_->bus_info[args] = response.bus;
        }
    }

    return response.result;
}

tresult PLUGIN_API Vst3PluginProxyImpl::getRoutingInfo(
    // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
    Steinberg::Vst::RoutingInfo& inInfo,
    Steinberg::Vst::RoutingInfo& outInfo /*out*/) {
    const GetRoutingInfoResponse response =
        bridge_.send_audio_processor_message(YaComponent::GetRoutingInfo{
            .instance_id = instance_id(), .in_info = inInfo});

    outInfo = response.out_info;

    return response.result;
}

tresult PLUGIN_API
Vst3PluginProxyImpl::activateBus(Steinberg::Vst::MediaType type,
                                 Steinberg::Vst::BusDirection dir,
                                 int32 index,
                                 TBool state) {
    return bridge_.send_audio_processor_message(
        YaComponent::ActivateBus{.instance_id = instance_id(),
                                 .type = type,
                                 .dir = dir,
                                 .index = index,
                                 .state = state});
}

tresult PLUGIN_API Vst3PluginProxyImpl::setActive(TBool state) {
    // HACK: Even though we initially implemented this cache specifically for
    //       REAPER, REAPER doesn't use `IComponent::setProcessing()` properly
    //       and calls it before doing setting up input and output busses. So
    //       now our workaround to get acceptable performance in REAPER needs a
    //       workaround of its own. Great!
    clear_bus_cache();

    const SetActiveResponse response = bridge_.send_audio_processor_message(
        YaComponent::SetActive{.instance_id = instance_id(), .state = state});

    // NOTE: REAPER may (and will) change a plugin's channel layout after
    //       calling `IAudioProcessor::setupProcessing()`. Because of that, we
    //       need to test whether this has happened any time the plugin gets
    //       reactivated.  It's technically legal, so we need to support it.
    if (response.updated_audio_buffers_config) {
        if (!process_buffers_) {
            process_buffers_.emplace(*response.updated_audio_buffers_config);
        } else {
            process_buffers_->resize(*response.updated_audio_buffers_config);
        }
    }

    return response.result;
}

tresult PLUGIN_API Vst3PluginProxyImpl::setState(Steinberg::IBStream* state) {
    if (state) {
        // Since both interfaces contain this function, this is used for both
        // `IComponent::setState()` as well as `IEditController::setState()`
        // NOTE: This will likely be called from the GUI thread, and some
        //       plugins will try to resize as part of setting their new state.
        //       That `IPlugFrame::resizeView()` _also_ has to be handled on the
        //       GUI thread. So if the GUI is active, we'll use the mutual
        //       recursion mechanism to allow this resize call to also be
        //       performed from the GUI thread.
        return bridge_.send_mutually_recursive_message(
            Vst3PluginProxy::SetState{.instance_id = instance_id(),
                                      .state = state});
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'I{Component,EditController}::setState()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getState(Steinberg::IBStream* state) {
    if (state) {
        // Since both interfaces contain this function, this is used for both
        // `IComponent::getState()` as well as `IEditController::getState()`
        // NOTE: This will likely be called from the GUI thread, if the plugin
        //       somehow ends up sending a resize while this happens, we should
        //       end up in a deadlock. Normally this wouldn't be an issue, but
        //       REAPER will fetch the complete plugin state from time to time,
        //       so when changing a parameter also resizes the GUI we can run
        //       into a situation where we need mutually recursive function
        //       calls.
        const GetStateResponse response =
            bridge_.send_mutually_recursive_message(Vst3PluginProxy::GetState{
                .instance_id = instance_id(), .state = state});

        assert(response.state.write_back(state) == Steinberg::kResultOk);

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'I{Component,EditController}::getState()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::connect(IConnectionPoint* other) {
    if (!other) {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IConnectionPointProxy::connect()'");
        return Steinberg::kInvalidArgument;
    }

    // When the host is trying to connect two plugin proxy objects, we can just
    // identify the other object by its instance IDs and then connect the
    // objects in the Wine plugin host directly. If this is not possible, we'll
    // still try to bypass the proxy and connect the object directly. That goes
    // against the principles of yabridge, but the alternative is nearly
    // impossible to pull off correctly because FabFilter VST3 plugins will call
    // `IConnectionPoint::notify()` from the GUI thread to communicate between
    // the processor and the edit controller. If we try to handle those mutually
    // recursive function calls from the GUI thread, then we'll still run into
    // issues when using multiple instances of the plugin. If we cannot figure
    // out which object the plugins are connected to, we'll still proxy the
    // host's connection proxy.
    if (auto other_instance = dynamic_cast<Vst3PluginProxy*>(other)) {
        connected_instance_id_ = other_instance->instance_id();

        return bridge_.send_message(
            YaConnectionPoint::Connect{.instance_id = instance_id(),
                                       .other = other_instance->instance_id()});
    }

    // As mentioned above, we'll first try to bypass the connection point proxy
    // and connect the objects directly
    if (host_application_) {
        Steinberg::IPtr<Steinberg::Vst::IMessage> message = Steinberg::owned(
            Steinberg::Vst::allocateMessage(host_application_));
        if (message) {
            message->setMessageID(other_instance_message_id);

            Steinberg::IPtr<Steinberg::Vst::IAttributeList> attributes =
                message->getAttributes();
            if (attributes) {
                attributes->setInt(
                    other_instance_pointer_attribute,
                    static_cast<int64>(reinterpret_cast<size_t>(this)));
            }

            // If we are connected with another object instance from this
            // plugin, `connected_instance_id` should now be set
            other->notify(message);
            if (connected_instance_id_) {
                return bridge_.send_message(YaConnectionPoint::Connect{
                    .instance_id = instance_id(),
                    .other = *connected_instance_id_});
            }
        }
    }

    // If we cannot bypass the proxy, we'll just proxy the host's proxy
    connection_point_proxy_ = other;

    return bridge_.send_message(YaConnectionPoint::Connect{
        .instance_id = instance_id(),
        .other =
            Vst3ConnectionPointProxy::ConstructArgs(other, instance_id())});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::disconnect(IConnectionPoint* /*other*/) {
    // See `Vst3PluginProxyImpl::connect()`, if we directly connected two
    // instances we'll also disconnect them again
    if (connected_instance_id_) {
        return bridge_.send_message(YaConnectionPoint::Disconnect{
            .instance_id = instance_id(),
            .other_instance_id = *connected_instance_id_});
    } else {
        const tresult result = bridge_.send_message(
            YaConnectionPoint::Disconnect{.instance_id = instance_id(),
                                          .other_instance_id = std::nullopt});
        connection_point_proxy_.reset();

        return result;
    }
}

tresult PLUGIN_API
Vst3PluginProxyImpl::notify(Steinberg::Vst::IMessage* message) {
    // Since there is no way to enumerate over all values in an
    // `IAttributeList`, we can only support relaying messages that were sent by
    // our own objects. Additionally, the `IMessage*` we end up passing to the
    // plugin needs to have the same lifetime as the original object, because
    // some plugins are being a bit naughty. That's why we pass around a pointer
    // to the original message object.
    // All of this is only needed to support hosts that place a connection proxy
    // between two objects instead of connecting them directly.  If the objects
    // are connected directly we also connected them directly on the Wine side,
    // so we don't have to do any additional when those objects pass through
    // messages.
    if (auto message_ptr = dynamic_cast<YaMessagePtr*>(message)) {
        return bridge_.send_message(YaConnectionPoint::Notify{
            .instance_id = instance_id(), .message_ptr = *message_ptr});
    }

    // NOTE: As mentioned above, when the host (or specifically, Ardour or
    //       Mixbus) calls `IConnectionPoint::connect()`, we'll try to bypass
    //       the connection proxy since this creates some difficult situations
    //       when plugins start calling `IConnectionPoint::notify()` from the
    //       GUI thread
    if (message &&
        strcmp(message->getMessageID(), other_instance_message_id) == 0) {
        Steinberg::IPtr<Steinberg::Vst::IAttributeList> attributes =
            message->getAttributes();
        if (attributes) {
            int64 other_object_ptr;
            if (attributes->getInt(other_instance_pointer_attribute,
                                   other_object_ptr) == Steinberg::kResultOk &&
                other_object_ptr != 0) {
                Vst3PluginProxyImpl& other_object =
                    *reinterpret_cast<Vst3PluginProxyImpl*>(
                        static_cast<size_t>(other_object_ptr));

                other_object.connected_instance_id_ = instance_id();

                return Steinberg::kResultOk;
            }
        }
    }

    bridge_.logger_.log(
        "WARNING: Unknown message type passed to "
        "'IConnectionPoint::notify()', ignoring");
    return Steinberg::kNotImplemented;
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setComponentState(Steinberg::IBStream* state) {
    if (state) {
        return bridge_.send_message(YaEditController::SetComponentState{
            .instance_id = instance_id(), .state = state});
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IEditController::setComponentState()'");
        return Steinberg::kInvalidArgument;
    }
}

int32 PLUGIN_API Vst3PluginProxyImpl::getParameterCount() {
    // Parameter information is queried all at once to work around a Kontakt
    // bug, see <https://github.com/robbert-vdh/yabridge/issues/236>. The first
    // time either of these two functions is called we'll fetch the infos for
    // all parameters. These are cleared when the plugin triggers a component
    // restart.
    maybe_query_parameter_info();

    std::lock_guard lock(function_result_cache_mutex_);
    return static_cast<int32>(function_result_cache_.parameter_info.size());
}

tresult PLUGIN_API Vst3PluginProxyImpl::getParameterInfo(
    int32 paramIndex,
    Steinberg::Vst::ParameterInfo& info /*out*/) {
    // The integer parameter indices are _fun_
    if (paramIndex < 0) {
        return Steinberg::kInvalidArgument;
    }

    // See above
    maybe_query_parameter_info();

    std::lock_guard lock(function_result_cache_mutex_);
    if (paramIndex <
        static_cast<int32>(function_result_cache_.parameter_info.size())) {
        if (const auto& result =
                function_result_cache_.parameter_info[paramIndex]) {
            info = *result;
            return Steinberg::kResultOk;
        } else {
            return Steinberg::kResultFalse;
        }
    } else {
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getParamStringByValue(
    Steinberg::Vst::ParamID id,
    Steinberg::Vst::ParamValue valueNormalized /*in*/,
    Steinberg::Vst::String128 string /*out*/) {
    if (string) {
        const GetParamStringByValueResponse response =
            bridge_.send_message(YaEditController::GetParamStringByValue{
                .instance_id = instance_id(),
                .id = id,
                .value_normalized = valueNormalized});

        std::copy(response.string.begin(), response.string.end(), string);
        string[response.string.size()] = 0;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IEditController::getParamStringByValue()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getParamValueByString(
    Steinberg::Vst::ParamID id,
    Steinberg::Vst::TChar* string /*in*/,
    Steinberg::Vst::ParamValue& valueNormalized /*out*/) {
    if (string) {
        const GetParamValueByStringResponse response =
            bridge_.send_message(YaEditController::GetParamValueByString{
                .instance_id = instance_id(), .id = id, .string = string});

        valueNormalized = response.value_normalized;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IEditController::getParamValueByString()'");
        return Steinberg::kInvalidArgument;
    }
}

Steinberg::Vst::ParamValue PLUGIN_API
Vst3PluginProxyImpl::normalizedParamToPlain(
    Steinberg::Vst::ParamID id,
    Steinberg::Vst::ParamValue valueNormalized) {
    return bridge_.send_message(YaEditController::NormalizedParamToPlain{
        .instance_id = instance_id(),
        .id = id,
        .value_normalized = valueNormalized});
}

Steinberg::Vst::ParamValue PLUGIN_API
Vst3PluginProxyImpl::plainParamToNormalized(
    Steinberg::Vst::ParamID id,
    Steinberg::Vst::ParamValue plainValue) {
    return bridge_.send_message(YaEditController::PlainParamToNormalized{
        .instance_id = instance_id(), .id = id, .plain_value = plainValue});
}

Steinberg::Vst::ParamValue PLUGIN_API
Vst3PluginProxyImpl::getParamNormalized(Steinberg::Vst::ParamID id) {
    return bridge_.send_message(YaEditController::GetParamNormalized{
        .instance_id = instance_id(), .id = id});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setParamNormalized(Steinberg::Vst::ParamID id,
                                        Steinberg::Vst::ParamValue value) {
    return bridge_.send_message(YaEditController::SetParamNormalized{
        .instance_id = instance_id(), .id = id, .value = value});
}

tresult PLUGIN_API Vst3PluginProxyImpl::setComponentHandler(
    Steinberg::Vst::IComponentHandler* handler) {
    // Null pointers are valid here going from the reference implementations in
    // the SDK
    if (handler) {
        // We'll store the pointer for when the plugin later makes a callback to
        // this component handler
        component_handler_ = handler;

        // Automatically converted smart pointers for when the plugin performs a
        // callback later
        component_handler_2_ = component_handler_;
        component_handler_3_ = component_handler_;
        component_handler_bus_activation_ = component_handler_;
        progress_ = component_handler_;
        unit_handler_ = component_handler_;
        unit_handler_2_ = component_handler_;

        return bridge_.send_message(YaEditController::SetComponentHandler{
            .instance_id = instance_id(),
            .component_handler_proxy_args =
                Vst3ComponentHandlerProxy::ConstructArgs(component_handler_,
                                                         instance_id())});
    } else {
        component_handler_ = nullptr;

        component_handler_2_ = nullptr;
        component_handler_3_ = nullptr;
        component_handler_bus_activation_ = nullptr;
        progress_ = nullptr;
        unit_handler_ = nullptr;
        unit_handler_2_ = nullptr;

        return bridge_.send_message(YaEditController::SetComponentHandler{
            .instance_id = instance_id(),
            .component_handler_proxy_args = std::nullopt});
    }
}

Steinberg::IPlugView* PLUGIN_API
Vst3PluginProxyImpl::createView(Steinberg::FIDString name) {
    if (name) {
        CreateViewResponse response =
            bridge_.send_message(YaEditController::CreateView{
                .instance_id = instance_id(), .name = name});

        if (response.plug_view_args) {
            // The host should manage this. Returning raw pointers feels scary.
            auto plug_view_proxy = new Vst3PlugViewProxyImpl(
                bridge_, std::move(*response.plug_view_args));

            // We also need to store an (unmanaged, since we don't want to
            // affect the reference counting) pointer to this to be able to
            // handle calls to `IPlugFrame::resizeView()` in the future
            last_created_plug_view_ = plug_view_proxy;

            return plug_view_proxy;
        } else {
            return nullptr;
        }
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IEditController::createView()'");
        return nullptr;
    }
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setKnobMode(Steinberg::Vst::KnobMode mode) {
    return bridge_.send_message(YaEditController2::SetKnobMode{
        .instance_id = instance_id(), .mode = mode});
}

tresult PLUGIN_API Vst3PluginProxyImpl::openHelp(TBool onlyCheck) {
    return bridge_.send_message(YaEditController2::OpenHelp{
        .instance_id = instance_id(), .only_check = onlyCheck});
}

tresult PLUGIN_API Vst3PluginProxyImpl::openAboutBox(TBool onlyCheck) {
    return bridge_.send_message(YaEditController2::OpenAboutBox{
        .instance_id = instance_id(), .only_check = onlyCheck});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::beginEditFromHost(Steinberg::Vst::ParamID paramID) {
    return bridge_.send_message(YaEditControllerHostEditing::BeginEditFromHost{
        .instance_id = instance_id(), .param_id = paramID});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::endEditFromHost(Steinberg::Vst::ParamID paramID) {
    return bridge_.send_message(YaEditControllerHostEditing::EndEditFromHost{
        .instance_id = instance_id(), .param_id = paramID});
}

tresult PLUGIN_API Vst3PluginProxyImpl::setChannelContextInfos(
    Steinberg::Vst::IAttributeList* list) {
    if (list) {
        // NOTE: After getting and setting state, the plugin may want to resize
        //       to match its old size. The DMG plugins tend to delay this
        //       request until after the state has been set, but at that point
        //       in time REAPER will also send channel context info. Both of
        //       these things need to be handled on the GUI thread on their
        //       receiving sides, resulting in a deadlock without this mutual
        //       recursion.
        return bridge_.send_mutually_recursive_message(
            YaInfoListener::SetChannelContextInfos{
                .instance_id = instance_id(),
                .list = YaAttributeList::read_channel_context(list)});
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IInfoListener::setChannelContextInfos()'");
        return Steinberg::kInvalidArgument;
    }
}

int32 PLUGIN_API Vst3PluginProxyImpl::getKeyswitchCount(int32 busIndex,
                                                        int16 channel) {
    return bridge_.send_message(
        YaKeyswitchController::GetKeyswitchCount{.instance_id = instance_id(),
                                                 .bus_index = busIndex,
                                                 .channel = channel});
}

tresult PLUGIN_API Vst3PluginProxyImpl::getKeyswitchInfo(
    int32 busIndex,
    int16 channel,
    int32 keySwitchIndex,
    Steinberg::Vst::KeyswitchInfo& info /*out*/) {
    const GetKeyswitchInfoResponse response =
        bridge_.send_message(YaKeyswitchController::GetKeyswitchInfo{
            .instance_id = instance_id(),
            .bus_index = busIndex,
            .channel = channel,
            .key_switch_index = keySwitchIndex});

    info = response.info;

    return response.result;
}

tresult PLUGIN_API Vst3PluginProxyImpl::onLiveMIDIControllerInput(
    int32 busIndex,
    int16 channel,
    Steinberg::Vst::CtrlNumber midiCC) {
    return bridge_.send_message(
        YaMidiLearn::OnLiveMIDIControllerInput{.instance_id = instance_id(),
                                               .bus_index = busIndex,
                                               .channel = channel,
                                               .midi_cc = midiCC});
}

tresult PLUGIN_API Vst3PluginProxyImpl::getMidiControllerAssignment(
    int32 busIndex,
    int16 channel,
    Steinberg::Vst::CtrlNumber midiControllerNumber,
    Steinberg::Vst::ParamID& id /*out*/) {
    const GetMidiControllerAssignmentResponse response =
        bridge_.send_message(YaMidiMapping::GetMidiControllerAssignment{
            .instance_id = instance_id(),
            .bus_index = busIndex,
            .channel = channel,
            .midi_controller_number = midiControllerNumber});

    id = response.id;

    return response.result;
}

int32 PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionCount(int32 busIndex,
                                                             int16 channel) {
    return bridge_.send_message(
        YaNoteExpressionController::GetNoteExpressionCount{
            .instance_id = instance_id(),
            .bus_index = busIndex,
            .channel = channel});
}

tresult PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionInfo(
    int32 busIndex,
    int16 channel,
    int32 noteExpressionIndex,
    Steinberg::Vst::NoteExpressionTypeInfo& info /*out*/) {
    const GetNoteExpressionInfoResponse response =
        bridge_.send_message(YaNoteExpressionController::GetNoteExpressionInfo{
            .instance_id = instance_id(),
            .bus_index = busIndex,
            .channel = channel,
            .note_expression_index = noteExpressionIndex});

    info = response.info;

    return response.result;
}

tresult PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionStringByValue(
    int32 busIndex,
    int16 channel,
    Steinberg::Vst::NoteExpressionTypeID id,
    Steinberg::Vst::NoteExpressionValue valueNormalized /*in*/,
    Steinberg::Vst::String128 string /*out*/) {
    if (string) {
        const GetNoteExpressionStringByValueResponse response =
            bridge_.send_message(
                YaNoteExpressionController::GetNoteExpressionStringByValue{
                    .instance_id = instance_id(),
                    .bus_index = busIndex,
                    .channel = channel,
                    .id = id,
                    .value_normalized = valueNormalized});

        std::copy(response.string.begin(), response.string.end(), string);
        string[response.string.size()] = 0;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'INoteExpressionController::getNoteExpressionStringByValue()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getNoteExpressionValueByString(
    int32 busIndex,
    int16 channel,
    Steinberg::Vst::NoteExpressionTypeID id,
    const Steinberg::Vst::TChar* string /*in*/,
    Steinberg::Vst::NoteExpressionValue& valueNormalized /*out*/) {
    if (string) {
        const GetNoteExpressionValueByStringResponse response =
            bridge_.send_message(
                YaNoteExpressionController::GetNoteExpressionValueByString{
                    .instance_id = instance_id(),
                    .bus_index = busIndex,
                    .channel = channel,
                    .id = id,
                    .string = string});

        valueNormalized = response.value_normalized;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'INoteExpressionController::getNoteExpressionValueByString()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getPhysicalUIMapping(
    int32 busIndex,
    int16 channel,
    Steinberg::Vst::PhysicalUIMapList& list) {
    const GetNotePhysicalUIMappingResponse response = bridge_.send_message(
        YaNoteExpressionPhysicalUIMapping::GetNotePhysicalUIMapping{
            .instance_id = instance_id(),
            .bus_index = busIndex,
            .channel = channel,
            .list = list});

    response.list.write_back(list);

    return response.result;
}

tresult PLUGIN_API Vst3PluginProxyImpl::getParameterIDFromFunctionName(
    Steinberg::Vst::UnitID unitID,
    Steinberg::FIDString functionName,
    Steinberg::Vst::ParamID& paramID) {
    if (functionName) {
        const GetParameterIDFromFunctionNameResponse response =
            bridge_.send_message(
                YaParameterFunctionName::GetParameterIDFromFunctionName{
                    .instance_id = instance_id(),
                    .unit_id = unitID,
                    .function_name = functionName});

        paramID = response.param_id;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IParameterFunctionName::getParameterIDFromFunctionName()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::initialize(FUnknown* context) {
    if (context) {
        // We will create a proxy object that that supports all the same
        // interfaces as `context`, and then we'll store `context` in this
        // object. We can then use it to handle callbacks made by the Windows
        // VST3 plugin to this context.
        host_context_ = context;

        // Automatically converted smart pointers for when the plugin performs a
        // callback later
        host_application_ = host_context_;
        plug_interface_support_ = host_context_;

        InitializeResponse response =
            bridge_.send_message(Vst3PluginProxy::Initialize{
                .instance_id = instance_id(),
                .host_context_args = Vst3HostContextProxy::ConstructArgs(
                    host_context_, instance_id())});

        // HACK: For some reason, Waves plugins will only allow querying the
        //       `IEditController` interface after this point, so we need to
        //       update the list of interfaces we support for this object.
        update_supported_interfaces(
            std::move(response.updated_plugin_interfaces));

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to 'IPluginBase::initialize()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::terminate() {
    return bridge_.send_message(
        YaPluginBase::Terminate{.instance_id = instance_id()});
}

tresult PLUGIN_API Vst3PluginProxyImpl::getPrefetchableSupport(
    Steinberg::Vst::PrefetchableSupport& prefetchable /*out*/) {
    const GetPrefetchableSupportResponse response =
        bridge_.send_audio_processor_message(
            YaPrefetchableSupport::GetPrefetchableSupport{.instance_id =
                                                              instance_id()});

    prefetchable = response.prefetchable;

    return response.result;
}

uint32 PLUGIN_API Vst3PluginProxyImpl::getProcessContextRequirements() {
    return bridge_.send_message(
        YaProcessContextRequirements::GetProcessContextRequirements{
            .instance_id = instance_id()});
}

tresult PLUGIN_API Vst3PluginProxyImpl::programDataSupported(
    Steinberg::Vst::ProgramListID listId) {
    return bridge_.send_message(YaProgramListData::ProgramDataSupported{
        .instance_id = instance_id(), .list_id = listId});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getProgramData(Steinberg::Vst::ProgramListID listId,
                                    int32 programIndex,
                                    Steinberg::IBStream* data) {
    if (data) {
        const GetProgramDataResponse response = bridge_.send_message(
            YaProgramListData::GetProgramData{.instance_id = instance_id(),
                                              .list_id = listId,
                                              .program_index = programIndex,
                                              .data = data});

        assert(response.data.write_back(data) == Steinberg::kResultOk);

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IProgramListData::getProgramData()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setProgramData(Steinberg::Vst::ProgramListID listId,
                                    int32 programIndex,
                                    Steinberg::IBStream* data) {
    if (data) {
        return bridge_.send_message(
            YaProgramListData::SetProgramData{.instance_id = instance_id(),
                                              .list_id = listId,
                                              .program_index = programIndex,
                                              .data = data});
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IProgramListData::setProgramData()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API
Vst3PluginProxyImpl::unitDataSupported(Steinberg::Vst::UnitID unitId) {
    return bridge_.send_message(YaUnitData::UnitDataSupported{
        .instance_id = instance_id(), .unit_id = unitId});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getUnitData(Steinberg::Vst::UnitID unitId,
                                 Steinberg::IBStream* data) {
    if (data) {
        const GetUnitDataResponse response =
            bridge_.send_message(YaUnitData::GetUnitData{
                .instance_id = instance_id(), .unit_id = unitId, .data = data});

        assert(response.data.write_back(data) == Steinberg::kResultOk);

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to 'IUnitData::getUnitData()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setUnitData(Steinberg::Vst::UnitID unitId,
                                 Steinberg::IBStream* data) {
    if (data) {
        return bridge_.send_message(YaUnitData::SetUnitData{
            .instance_id = instance_id(), .unit_id = unitId, .data = data});
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to 'IUnitData::setUnitData()'");
        return Steinberg::kInvalidArgument;
    }
}

int32 PLUGIN_API Vst3PluginProxyImpl::getUnitCount() {
    return bridge_.send_message(
        YaUnitInfo::GetUnitCount{.instance_id = instance_id()});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getUnitInfo(int32 unitIndex,
                                 Steinberg::Vst::UnitInfo& info /*out*/) {
    const GetUnitInfoResponse response =
        bridge_.send_message(YaUnitInfo::GetUnitInfo{
            .instance_id = instance_id(), .unit_index = unitIndex});

    info = response.info;

    return response.result;
}

int32 PLUGIN_API Vst3PluginProxyImpl::getProgramListCount() {
    return bridge_.send_message(
        YaUnitInfo::GetProgramListCount{.instance_id = instance_id()});
}

tresult PLUGIN_API Vst3PluginProxyImpl::getProgramListInfo(
    int32 listIndex,
    Steinberg::Vst::ProgramListInfo& info /*out*/) {
    const GetProgramListInfoResponse response =
        bridge_.send_message(YaUnitInfo::GetProgramListInfo{
            .instance_id = instance_id(), .list_index = listIndex});

    info = response.info;

    return response.result;
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getProgramName(Steinberg::Vst::ProgramListID listId,
                                    int32 programIndex,
                                    Steinberg::Vst::String128 name /*out*/) {
    if (name) {
        const GetProgramNameResponse response = bridge_.send_message(
            YaUnitInfo::GetProgramName{.instance_id = instance_id(),
                                       .list_id = listId,
                                       .program_index = programIndex});

        std::copy(response.name.begin(), response.name.end(), name);
        name[response.name.size()] = 0;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to 'IUnitInfo::getProgramName()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getProgramInfo(
    Steinberg::Vst::ProgramListID listId,
    int32 programIndex,
    Steinberg::Vst::CString attributeId /*in*/,
    Steinberg::Vst::String128 attributeValue /*out*/) {
    if (attributeId && attributeValue) {
        const GetProgramInfoResponse response = bridge_.send_message(
            YaUnitInfo::GetProgramInfo{.instance_id = instance_id(),
                                       .list_id = listId,
                                       .program_index = programIndex,
                                       .attribute_id = attributeId});

        std::copy(response.attribute_value.begin(),
                  response.attribute_value.end(), attributeValue);
        attributeValue[response.attribute_value.size()] = 0;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to 'IUnitInfo::getProgramInfo()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API
Vst3PluginProxyImpl::hasProgramPitchNames(Steinberg::Vst::ProgramListID listId,
                                          int32 programIndex) {
    return bridge_.send_message(
        YaUnitInfo::HasProgramPitchNames{.instance_id = instance_id(),
                                         .list_id = listId,
                                         .program_index = programIndex});
}

tresult PLUGIN_API Vst3PluginProxyImpl::getProgramPitchName(
    Steinberg::Vst::ProgramListID listId,
    int32 programIndex,
    int16 midiPitch,
    Steinberg::Vst::String128 name /*out*/) {
    if (name) {
        const GetProgramPitchNameResponse response = bridge_.send_message(
            YaUnitInfo::GetProgramPitchName{.instance_id = instance_id(),
                                            .list_id = listId,
                                            .program_index = programIndex,
                                            .midi_pitch = midiPitch});

        std::copy(response.name.begin(), response.name.end(), name);
        name[response.name.size()] = 0;

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IUnitInfo::getProgramPitchName()'");
        return Steinberg::kInvalidArgument;
    }
}

Steinberg::Vst::UnitID PLUGIN_API Vst3PluginProxyImpl::getSelectedUnit() {
    return bridge_.send_message(
        YaUnitInfo::GetSelectedUnit{.instance_id = instance_id()});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::selectUnit(Steinberg::Vst::UnitID unitId) {
    return bridge_.send_message(YaUnitInfo::SelectUnit{
        .instance_id = instance_id(), .unit_id = unitId});
}

tresult PLUGIN_API
Vst3PluginProxyImpl::getUnitByBus(Steinberg::Vst::MediaType type,
                                  Steinberg::Vst::BusDirection dir,
                                  int32 busIndex,
                                  int32 channel,
                                  Steinberg::Vst::UnitID& unitId /*out*/) {
    const GetUnitByBusResponse response = bridge_.send_message(
        YaUnitInfo::GetUnitByBus{.instance_id = instance_id(),
                                 .type = type,
                                 .dir = dir,
                                 .bus_index = busIndex,
                                 .channel = channel});

    unitId = response.unit_id;

    return response.result;
}

tresult PLUGIN_API
Vst3PluginProxyImpl::setUnitProgramData(int32 listOrUnitId,
                                        int32 programIndex,
                                        Steinberg::IBStream* data) {
    if (data) {
        return bridge_.send_message(
            YaUnitInfo::SetUnitProgramData{.instance_id = instance_id(),
                                           .list_or_unit_id = listOrUnitId,
                                           .program_index = programIndex,
                                           .data = data});
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IUnitInfo::setUnitProgramData()'");
        return Steinberg::kInvalidArgument;
    }
}

tresult PLUGIN_API Vst3PluginProxyImpl::getXmlRepresentationStream(
    Steinberg::Vst::RepresentationInfo& info /*in*/,
    Steinberg::IBStream* stream /*out*/) {
    if (stream) {
        const GetXmlRepresentationStreamResponse response =
            bridge_.send_message(
                YaXmlRepresentationController::GetXmlRepresentationStream{
                    .instance_id = instance_id(),
                    .info = info,
                    .stream = stream});

        response.stream.write_back(stream);

        return response.result;
    } else {
        bridge_.logger_.log(
            "WARNING: Null pointer passed to "
            "'IXmlRepresentationController::getXmlRepresentationStream()'");
        return Steinberg::kInvalidArgument;
    }
}

void Vst3PluginProxyImpl::maybe_query_parameter_info() {
    std::lock_guard lock(function_result_cache_mutex_);

    // We'll assume that the plugin has at least one parameter. If it does not
    // have any parameters then everything will work as expected, except that
    // the parameter count is not cached.
    if (function_result_cache_.parameter_info.empty()) {
        const GetParameterInfosResponse response = bridge_.send_message(
            YaEditController::GetParameterInfos{.instance_id = instance_id()});
        function_result_cache_.parameter_info = std::move(response.infos);
    }
}

void Vst3PluginProxyImpl::clear_bus_cache() noexcept {
    std::lock_guard lock(processing_bus_cache_mutex_);
    if (processing_bus_cache_) {
        processing_bus_cache_.emplace();
    }
}
