/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

#if JUCE_PLUGINHOST_VST3 && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD)

#include "juce_VST3Headers.h"
#include "juce_VST3Common.h"
#include "juce_ARACommon.h"

#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
#include <ARA_API/ARAVST3.h>

namespace ARA
{
DEF_CLASS_IID (IMainFactory)
DEF_CLASS_IID (IPlugInEntryPoint)
DEF_CLASS_IID (IPlugInEntryPoint2)
}
#endif

namespace juce
{

// UB Sanitizer doesn't necessarily have instrumentation for loaded plugins, so
// it won't recognize the dynamic types of pointers to the plugin's interfaces.
JUCE_BEGIN_NO_SANITIZE ("vptr")

using namespace Steinberg;

//==============================================================================
#ifndef JUCE_VST3_DEBUGGING
 #define JUCE_VST3_DEBUGGING 0
#endif

#if JUCE_VST3_DEBUGGING
 #define VST3_DBG(a) Logger::writeToLog (a);
#else
 #define VST3_DBG(a)
#endif

#if JUCE_DEBUG
static int warnOnFailure (int result) noexcept
{
    const char* message = "Unknown result!";

    switch (result)
    {
        case kResultOk:         return result;
        case kNotImplemented:   message = "kNotImplemented";  break;
        case kNoInterface:      message = "kNoInterface";     break;
        case kResultFalse:      message = "kResultFalse";     break;
        case kInvalidArgument:  message = "kInvalidArgument"; break;
        case kInternalError:    message = "kInternalError";   break;
        case kNotInitialized:   message = "kNotInitialized";  break;
        case kOutOfMemory:      message = "kOutOfMemory";     break;
        default:                break;
    }

    DBG (message);
    return result;
}

static int warnOnFailureIfImplemented (int result) noexcept
{
    if (result != kResultOk && result != kNotImplemented)
        return warnOnFailure (result);

    return result;
}
#else
 #define warnOnFailure(x) x
 #define warnOnFailureIfImplemented(x) x
#endif

enum class MediaKind { audio, event };

static Vst::MediaType toVstType (MediaKind x) { return x == MediaKind::audio ? Vst::kAudio : Vst::kEvent; }
static Vst::BusDirection toVstType (Direction x) { return x == Direction::input ? Vst::kInput : Vst::kOutput; }

static std::vector<Vst::ParamID> getAllParamIDs (Vst::IEditController& controller)
{
    std::vector<Vst::ParamID> result;

    auto count = controller.getParameterCount();

    for (decltype (count) i = 0; i < count; ++i)
    {
        Vst::ParameterInfo info{};
        controller.getParameterInfo (i, info);
        result.push_back (info.id);
    }

    return result;
}

//==============================================================================
/*  Allows parameter updates to be queued up without blocking,
    and automatically dispatches these updates on the main thread.
*/
class EditControllerParameterDispatcher final : private Timer
{
public:
    ~EditControllerParameterDispatcher() override { stopTimer(); }

    void push (Steinberg::int32 index, float value)
    {
        if (controller == nullptr)
            return;

        if (MessageManager::getInstance()->isThisTheMessageThread())
            controller->setParamNormalized (cache.getParamID (index), value);
        else
            cache.set (index, value);
    }

    void start (Vst::IEditController& controllerIn)
    {
        controller = &controllerIn;
        cache = CachedParamValues { getAllParamIDs (controllerIn) };
        startTimerHz (60);
    }

    void flush()
    {
        cache.ifSet ([this] (Steinberg::int32 index, float value)
        {
            controller->setParamNormalized (cache.getParamID (index), value);
        });
    }

private:
    void timerCallback() override
    {
        flush();
    }

    CachedParamValues cache;
    Vst::IEditController* controller = nullptr;
};

//==============================================================================
static std::array<uint32, 4> getNormalisedTUID (const TUID& tuid) noexcept
{
    const FUID fuid { tuid };
    return { { fuid.getLong1(), fuid.getLong2(), fuid.getLong3(), fuid.getLong4() } };
}

template <typename Range>
static int getHashForRange (Range&& range) noexcept
{
    uint32 value = 0;

    for (const auto& item : range)
        value = (value * 31) + (uint32) item;

    return (int) value;
}

template <typename ObjectType>
static void fillDescriptionWith (PluginDescription& description, ObjectType& object)
{
    description.version  = toString (object.version).trim();
    description.category = toString (object.subCategories).trim();

    if (description.manufacturerName.trim().isEmpty())
        description.manufacturerName = toString (object.vendor).trim();
}

static std::vector<PluginDescription> createPluginDescriptions (const File& pluginFile, const Steinberg::ModuleInfo& info)
{
    std::vector<PluginDescription> result;

    const auto araMainFactoryClassNames = [&]
    {
        std::unordered_set<String> factories;

       #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
        for (const auto& c : info.classes)
            if (c.category == kARAMainFactoryClass)
                factories.insert (CharPointer_UTF8 (c.name.c_str()));
       #endif

        return factories;
    }();

    for (const auto& c : info.classes)
    {
        if (c.category != kVstAudioEffectClass)
            continue;

        PluginDescription description;

        description.fileOrIdentifier    = pluginFile.getFullPathName();
        description.lastFileModTime     = pluginFile.getLastModificationTime();
        description.lastInfoUpdateTime  = Time::getCurrentTime();
        description.manufacturerName    = CharPointer_UTF8 (info.factoryInfo.vendor.c_str());
        description.name                = CharPointer_UTF8 (c.name.c_str());
        description.descriptiveName     = CharPointer_UTF8 (c.name.c_str());
        description.pluginFormatName    = "VST3";
        description.numInputChannels    = 0;
        description.numOutputChannels   = 0;
        description.hasARAExtension     = araMainFactoryClassNames.find (description.name) != araMainFactoryClassNames.end();
        description.version             = CharPointer_UTF8 (c.version.c_str());

        const auto uid = VST3::UID::fromString (c.cid);

        if (! uid)
            continue;

        description.deprecatedUid       = getHashForRange (uid->data());
        description.uniqueId            = getHashForRange (getNormalisedTUID (uid->data()));

        StringArray categories;

        for (const auto& category : c.subCategories)
            categories.add (CharPointer_UTF8 (category.c_str()));

        description.category = categories.joinIntoString ("|");

        description.isInstrument = std::any_of (c.subCategories.begin(),
                                                c.subCategories.end(),
                                                [] (const auto& subcategory) { return subcategory == "Instrument"; });

        result.push_back (description);
    }

    return result;
}

static void createPluginDescription (PluginDescription& description,
                                     const File& pluginFile, const String& company, const String& name,
                                     const PClassInfo& info, PClassInfo2* info2, PClassInfoW* infoW,
                                     int numInputs, int numOutputs)
{
    description.fileOrIdentifier    = pluginFile.getFullPathName();
    description.lastFileModTime     = pluginFile.getLastModificationTime();
    description.lastInfoUpdateTime  = Time::getCurrentTime();
    description.manufacturerName    = company;
    description.name                = name;
    description.descriptiveName     = name;
    description.pluginFormatName    = "VST3";
    description.numInputChannels    = numInputs;
    description.numOutputChannels   = numOutputs;

    description.deprecatedUid       = getHashForRange (info.cid);
    description.uniqueId            = getHashForRange (getNormalisedTUID (info.cid));

    if (infoW != nullptr)      fillDescriptionWith (description, *infoW);
    else if (info2 != nullptr) fillDescriptionWith (description, *info2);

    if (description.category.isEmpty())
        description.category = toString (info.category).trim();

    description.isInstrument = description.category.containsIgnoreCase ("Instrument"); // This seems to be the only way to find that out! ARGH!
}

static int getNumSingleDirectionBusesFor (Vst::IComponent* component,
                                          MediaKind kind,
                                          Direction direction)
{
    jassert (component != nullptr);
    JUCE_ASSERT_MESSAGE_THREAD
    return (int) component->getBusCount (toVstType (kind), toVstType (direction));
}

/** Gives the total number of channels for a particular type of bus direction and media type */
static int getNumSingleDirectionChannelsFor (Vst::IComponent* component, Direction busDirection)
{
    jassert (component != nullptr);
    JUCE_ASSERT_MESSAGE_THREAD

    const auto direction = toVstType (busDirection);
    const Steinberg::int32 numBuses = component->getBusCount (Vst::kAudio, direction);

    int numChannels = 0;

    for (Steinberg::int32 i = numBuses; --i >= 0;)
    {
        Vst::BusInfo busInfo;
        warnOnFailure (component->getBusInfo (Vst::kAudio, direction, i, busInfo));
        numChannels += ((busInfo.flags & Vst::BusInfo::kDefaultActive) != 0 ? (int) busInfo.channelCount : 0);
    }

    return numChannels;
}

static void setStateForAllEventBuses (Vst::IComponent* component,
                                      bool state,
                                      Direction busDirection)
{
    jassert (component != nullptr);
    JUCE_ASSERT_MESSAGE_THREAD

    const auto direction = toVstType (busDirection);
    const Steinberg::int32 numBuses = component->getBusCount (Vst::kEvent, direction);

    for (Steinberg::int32 i = numBuses; --i >= 0;)
        warnOnFailure (component->activateBus (Vst::kEvent, direction, i, state));
}

//==============================================================================
static void toProcessContext (Vst::ProcessContext& context,
                              AudioPlayHead* playHead,
                              double sampleRate)
{
    jassert (sampleRate > 0.0); //Must always be valid, as stated by the VST3 SDK

    using namespace Vst;

    zerostruct (context);
    context.sampleRate = sampleRate;

    const auto position = playHead != nullptr ? playHead->getPosition()
                                              : nullopt;

    if (position.hasValue())
    {
        if (const auto timeInSamples = position->getTimeInSamples())
            context.projectTimeSamples = *timeInSamples;
        else
            jassertfalse; // The time in samples *must* be valid.

        if (const auto tempo = position->getBpm())
        {
            context.state |= ProcessContext::kTempoValid;
            context.tempo = *tempo;
        }

        if (const auto loop = position->getLoopPoints())
        {
            context.state |= ProcessContext::kCycleValid;
            context.cycleStartMusic     = loop->ppqStart;
            context.cycleEndMusic       = loop->ppqEnd;
        }

        if (const auto sig = position->getTimeSignature())
        {
            context.state |= ProcessContext::kTimeSigValid;
            context.timeSigNumerator    = sig->numerator;
            context.timeSigDenominator  = sig->denominator;
        }

        if (const auto pos = position->getPpqPosition())
        {
            context.state |= ProcessContext::kProjectTimeMusicValid;
            context.projectTimeMusic = *pos;
        }

        if (const auto barStart = position->getPpqPositionOfLastBarStart())
        {
            context.state |= ProcessContext::kBarPositionValid;
            context.barPositionMusic = *barStart;
        }

        if (const auto frameRate = position->getFrameRate())
        {
            if (const auto offset = position->getEditOriginTime())
            {
                context.state |= ProcessContext::kSmpteValid;
                context.smpteOffsetSubframes = (Steinberg::int32) (80.0 * *offset * frameRate->getEffectiveRate());
                context.frameRate.framesPerSecond = (Steinberg::uint32) frameRate->getBaseRate();
                context.frameRate.flags = (Steinberg::uint32) ((frameRate->isDrop()     ? FrameRate::kDropRate     : 0)
                                                             | (frameRate->isPullDown() ? FrameRate::kPullDownRate : 0));
            }
        }

        if (const auto hostTime = position->getHostTimeNs())
        {
            context.state |= ProcessContext::kSystemTimeValid;
            context.systemTime = (int64_t) *hostTime;
            jassert (context.systemTime >= 0);
        }

        if (position->getIsPlaying())     context.state |= ProcessContext::kPlaying;
        if (position->getIsRecording())   context.state |= ProcessContext::kRecording;
        if (position->getIsLooping())     context.state |= ProcessContext::kCycleActive;
    }
}

//==============================================================================
class VST3PluginInstance;

struct VST3HostContext final : public Vst::IComponentHandler,  // From VST V3.0.0
                               public Vst::IComponentHandler2, // From VST V3.1.0 (a very well named class, of course!)
                               public Vst::IComponentHandler3, // From VST V3.5.0 (also very well named!)
                               public Vst::IContextMenuTarget,
                               public Vst::IHostApplication,
                               public Vst::IUnitHandler,
                               private ComponentRestarter::Listener
{
    VST3HostContext()
    {
        appName = File::getSpecialLocation (File::currentApplicationFile).getFileNameWithoutExtension();
    }

    ~VST3HostContext() override = default;

    JUCE_DECLARE_VST3_COM_REF_METHODS

    FUnknown* getFUnknown()     { return static_cast<Vst::IComponentHandler*> (this); }

    static bool hasFlag (Steinberg::int32 source, Steinberg::int32 flag) noexcept
    {
        return (source & flag) == flag;
    }

    //==============================================================================
    tresult PLUGIN_API beginEdit (Vst::ParamID paramID) override;
    tresult PLUGIN_API performEdit (Vst::ParamID paramID, Vst::ParamValue valueNormalized) override;
    tresult PLUGIN_API endEdit (Vst::ParamID paramID) override;

    tresult PLUGIN_API restartComponent (Steinberg::int32 flags) override;
    tresult PLUGIN_API setDirty (TBool) override;

    //==============================================================================
    tresult PLUGIN_API requestOpenEditor ([[maybe_unused]] FIDString name) override
    {
        // This request cannot currently be surfaced in the JUCE public API
        return kResultFalse;
    }

    tresult PLUGIN_API startGroupEdit() override
    {
        jassertfalse;
        return kResultFalse;
    }

    tresult PLUGIN_API finishGroupEdit() override
    {
        jassertfalse;
        return kResultFalse;
    }

    void setPlugin (VST3PluginInstance* instance)
    {
        jassert (plugin == nullptr);
        plugin = instance;
    }

    //==============================================================================
    struct ContextMenu final : public Vst::IContextMenu
    {
        ContextMenu (VST3PluginInstance& pluginInstance)  : owner (pluginInstance) {}
        virtual ~ContextMenu() {}

        JUCE_DECLARE_VST3_COM_REF_METHODS
        JUCE_DECLARE_VST3_COM_QUERY_METHODS

        Steinberg::int32 PLUGIN_API getItemCount() override     { return (Steinberg::int32) items.size(); }

        tresult PLUGIN_API addItem (const Item& item, IContextMenuTarget* target) override
        {
            jassert (target != nullptr);

            ItemAndTarget newItem;
            newItem.item = item;
            newItem.target = addVSTComSmartPtrOwner (target);

            items.add (newItem);
            return kResultOk;
        }

        tresult PLUGIN_API removeItem (const Item& toRemove, IContextMenuTarget* target) override
        {
            for (int i = items.size(); --i >= 0;)
            {
                auto& item = items.getReference (i);

                if (item.item.tag == toRemove.tag && item.target.get() == target)
                    items.remove (i);
            }

            return kResultOk;
        }

        tresult PLUGIN_API getItem (Steinberg::int32 tag, Item& result, IContextMenuTarget** target) override
        {
            for (int i = 0; i < items.size(); ++i)
            {
                auto& item = items.getReference (i);

                if (item.item.tag == tag)
                {
                    result = item.item;

                    if (target != nullptr)
                        *target = item.target.get();

                    return kResultTrue;
                }
            }

            zerostruct (result);
            return kResultFalse;
        }

        tresult PLUGIN_API popup (Steinberg::UCoord x, Steinberg::UCoord y) override;

       #if ! JUCE_MODAL_LOOPS_PERMITTED
        static void menuFinished (int modalResult, VSTComSmartPtr<ContextMenu> menu)  { menu->handleResult (modalResult); }
       #endif

    private:
        enum { zeroTagReplacement = 0x7fffffff };

        Atomic<int> refCount;
        VST3PluginInstance& owner;

        struct ItemAndTarget
        {
            Item item;
            VSTComSmartPtr<IContextMenuTarget> target;
        };

        Array<ItemAndTarget> items;

        void handleResult (int result)
        {
            if (result == 0)
                return;

            if (result == zeroTagReplacement)
                result = 0;

            for (int i = 0; i < items.size(); ++i)
            {
                auto& item = items.getReference (i);

                if ((int) item.item.tag == result)
                {
                    if (item.target != nullptr)
                        item.target->executeMenuItem ((Steinberg::int32) result);

                    break;
                }
            }
        }

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ContextMenu)
    };

    Vst::IContextMenu* PLUGIN_API createContextMenu (IPlugView*, const Vst::ParamID*) override
    {
        if (plugin == nullptr)
            return nullptr;

        auto* result = new ContextMenu (*plugin);
        result->addRef();
        return result;
    }

    tresult PLUGIN_API executeMenuItem (Steinberg::int32) override
    {
        jassertfalse;
        return kResultFalse;
    }

    //==============================================================================
    tresult PLUGIN_API getName (Vst::String128 name) override
    {
        Steinberg::String str (appName.toUTF8());
        str.copyTo (name, 0, 127);
        return kResultOk;
    }

    tresult PLUGIN_API createInstance (TUID cid, TUID iid, void** obj) override
    {
        *obj = nullptr;

        if (! doUIDsMatch (cid, iid))
        {
            jassertfalse;
            return kInvalidArgument;
        }

        if (doUIDsMatch (cid, Vst::IMessage::iid) && doUIDsMatch (iid, Vst::IMessage::iid))
        {
            *obj = new Message;
            return kResultOk;
        }

        if (doUIDsMatch (cid, Vst::IAttributeList::iid) && doUIDsMatch (iid, Vst::IAttributeList::iid))
        {
            *obj = new AttributeList;
            return kResultOk;
        }

        jassertfalse;
        return kNotImplemented;
    }

    //==============================================================================
    tresult PLUGIN_API notifyUnitSelection (Vst::UnitID) override
    {
        jassertfalse;
        return kResultFalse;
    }

    tresult PLUGIN_API notifyProgramListChange (Vst::ProgramListID, Steinberg::int32) override;

    //==============================================================================
    tresult PLUGIN_API queryInterface (const TUID iid, void** obj) override
    {
        return testForMultiple (*this,
                                iid,
                                UniqueBase<Vst::IComponentHandler>{},
                                UniqueBase<Vst::IComponentHandler2>{},
                                UniqueBase<Vst::IComponentHandler3>{},
                                UniqueBase<Vst::IContextMenuTarget>{},
                                UniqueBase<Vst::IHostApplication>{},
                                UniqueBase<Vst::IUnitHandler>{},
                                SharedBase<FUnknown, Vst::IComponentHandler>{}).extract (obj);
    }

private:
    //==============================================================================
    VST3PluginInstance* plugin = nullptr;
    Atomic<int> refCount;
    String appName;

    ComponentRestarter componentRestarter { *this };

    void restartComponentOnMessageThread (int32 flags) override;

    //==============================================================================
    class Attribute
    {
    public:
        using Int    = Steinberg::int64;
        using Float  = double;
        using String = std::vector<Vst::TChar>;
        using Binary = std::vector<char>;

        explicit Attribute (Int    x) noexcept { constructFrom (std::move (x)); }
        explicit Attribute (Float  x) noexcept { constructFrom (std::move (x)); }
        explicit Attribute (String x) noexcept { constructFrom (std::move (x)); }
        explicit Attribute (Binary x) noexcept { constructFrom (std::move (x)); }

        Attribute (Attribute&& other) noexcept
        {
            moveFrom (std::move (other));
        }

        Attribute& operator= (Attribute&& other) noexcept
        {
            reset();
            moveFrom (std::move (other));
            return *this;
        }

        ~Attribute() noexcept
        {
            reset();
        }

        tresult getInt (Steinberg::int64& result) const
        {
            if (kind != Kind::tagInt)
                return kResultFalse;

            result = storage.storedInt;
            return kResultTrue;
        }

        tresult getFloat (double& result) const
        {
            if (kind != Kind::tagFloat)
                return kResultFalse;

            result = storage.storedFloat;
            return kResultTrue;
        }

        tresult getString (Vst::TChar* data, Steinberg::uint32 numBytes) const
        {
            if (kind != Kind::tagString)
                return kResultFalse;

            std::memcpy (data,
                         storage.storedString.data(),
                         jmin (sizeof (Vst::TChar) * storage.storedString.size(), (size_t) numBytes));
            return kResultTrue;
        }

        tresult getBinary (const void*& data, Steinberg::uint32& numBytes) const
        {
            if (kind != Kind::tagBinary)
                return kResultFalse;

            data = storage.storedBinary.data();
            numBytes = (Steinberg::uint32) storage.storedBinary.size();
            return kResultTrue;
        }

    private:
        void constructFrom (Int    x) noexcept { kind = Kind::tagInt;    new (&storage.storedInt)    Int    (std::move (x)); }
        void constructFrom (Float  x) noexcept { kind = Kind::tagFloat;  new (&storage.storedFloat)  Float  (std::move (x)); }
        void constructFrom (String x) noexcept { kind = Kind::tagString; new (&storage.storedString) String (std::move (x)); }
        void constructFrom (Binary x) noexcept { kind = Kind::tagBinary; new (&storage.storedBinary) Binary (std::move (x)); }

        void reset() noexcept
        {
            switch (kind)
            {
                case Kind::tagInt:                                    break;
                case Kind::tagFloat:                                  break;
                case Kind::tagString: storage.storedString.~vector(); break;
                case Kind::tagBinary: storage.storedBinary.~vector(); break;
            }
        }

        void moveFrom (Attribute&& other) noexcept
        {
            switch (other.kind)
            {
                case Kind::tagInt:    constructFrom (std::move (other.storage.storedInt));    break;
                case Kind::tagFloat:  constructFrom (std::move (other.storage.storedFloat));  break;
                case Kind::tagString: constructFrom (std::move (other.storage.storedString)); break;
                case Kind::tagBinary: constructFrom (std::move (other.storage.storedBinary)); break;
            }
        }

        enum class Kind { tagInt, tagFloat, tagString, tagBinary };

        union Storage
        {
            Storage() {}
            ~Storage() {}

            Steinberg::int64              storedInt;
            double                        storedFloat;
            std::vector<Vst::TChar>       storedString;
            std::vector<char>             storedBinary;
        };

        Storage storage;
        Kind kind;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Attribute)
    };

    //==============================================================================
    class AttributeList final : public Vst::IAttributeList
    {
    public:
        AttributeList() = default;
        virtual ~AttributeList() = default;

        JUCE_DECLARE_VST3_COM_REF_METHODS
        JUCE_DECLARE_VST3_COM_QUERY_METHODS

        //==============================================================================
        tresult PLUGIN_API setInt (AttrID attr, Steinberg::int64 value) override
        {
            return set (attr, value);
        }

        tresult PLUGIN_API setFloat (AttrID attr, double value) override
        {
            return set (attr, value);
        }

        tresult PLUGIN_API setString (AttrID attr, const Vst::TChar* string) override
        {
            return set (attr, std::vector<Vst::TChar> (string, string + 1 + tstrlen (string)));
        }

        tresult PLUGIN_API setBinary (AttrID attr, const void* data, Steinberg::uint32 size) override
        {
            const auto* ptr = static_cast<const char*> (data);
            return set (attr, std::vector<char> (ptr, ptr + size));
        }

        tresult PLUGIN_API getInt (AttrID attr, Steinberg::int64& result) override
        {
            return get (attr, [&] (const auto& x) { return x.getInt (result); });
        }

        tresult PLUGIN_API getFloat (AttrID attr, double& result) override
        {
            return get (attr, [&] (const auto& x) { return x.getFloat (result); });
        }

        tresult PLUGIN_API getString (AttrID attr, Vst::TChar* result, Steinberg::uint32 length) override
        {
            return get (attr, [&] (const auto& x) { return x.getString (result, length); });
        }

        tresult PLUGIN_API getBinary (AttrID attr, const void*& data, Steinberg::uint32& size) override
        {
            return get (attr, [&] (const auto& x) { return x.getBinary (data, size); });
        }

    private:
        template <typename Value>
        tresult set (AttrID attr, Value&& value)
        {
            if (attr == nullptr)
                return kInvalidArgument;

            const auto iter = attributes.find (attr);

            if (iter != attributes.end())
                iter->second = Attribute (std::forward<Value> (value));
            else
                attributes.emplace (attr, Attribute (std::forward<Value> (value)));

            return kResultTrue;
        }

        template <typename Visitor>
        tresult get (AttrID attr, Visitor&& visitor)
        {
            if (attr == nullptr)
                return kInvalidArgument;

            const auto iter = attributes.find (attr);

            if (iter == attributes.cend())
                return kResultFalse;

            return visitor (iter->second);
        }

        std::map<std::string, Attribute> attributes;
        Atomic<int> refCount { 1 };

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AttributeList)
    };

    struct Message final : public Vst::IMessage
    {
        Message() = default;
        virtual ~Message() = default;

        JUCE_DECLARE_VST3_COM_REF_METHODS
        JUCE_DECLARE_VST3_COM_QUERY_METHODS

        FIDString PLUGIN_API getMessageID() override              { return messageId.toRawUTF8(); }
        void PLUGIN_API setMessageID (FIDString id) override      { messageId = toString (id); }
        Vst::IAttributeList* PLUGIN_API getAttributes() override  { return &attributeList; }

    private:
        AttributeList attributeList;
        String messageId;
        Atomic<int> refCount { 1 };

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Message)
    };

    VSTComSmartPtr<AttributeList> attributeList;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3HostContext)
};

//==============================================================================
struct DescriptionLister
{
    static std::vector<PluginDescription> tryLoadFast (const File& file, const File& moduleinfo)
    {
        if (! moduleinfo.existsAsFile())
            return {};

        MemoryBlock mb;

        if (! moduleinfo.loadFileAsData (mb))
            return {};

        const std::string_view blockAsStringView (static_cast<const char*> (mb.getData()), mb.getSize());
        const auto parsed = Steinberg::ModuleInfoLib::parseJson (blockAsStringView, nullptr);

        if (! parsed)
            return {};

        return createPluginDescriptions (file, *parsed);
    }

    static std::vector<PluginDescription> findDescriptionsFast (const File& file)
    {
        const auto moduleinfoNewLocation = file.getChildFile ("Contents").getChildFile ("Resources").getChildFile ("moduleinfo.json");

        if (const auto loaded = tryLoadFast (file, moduleinfoNewLocation); ! loaded.empty())
            return loaded;

        return tryLoadFast (file, file.getChildFile ("Contents").getChildFile ("moduleinfo.json"));
    }

    static std::vector<PluginDescription> findDescriptionsSlow (VST3HostContext& host,
                                                                IPluginFactory& factory,
                                                                const File& file)
    {
        std::vector<PluginDescription> result;

        StringArray foundNames;
        PFactoryInfo factoryInfo;
        factory.getFactoryInfo (&factoryInfo);
        auto companyName = toString (factoryInfo.vendor).trim();

        auto numClasses = factory.countClasses();

        // Every ARA::IMainFactory must have a matching Steinberg::IComponent.
        // The match is determined by the two classes having the same name.
        std::unordered_set<String> araMainFactoryClassNames;

       #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
        for (Steinberg::int32 i = 0; i < numClasses; ++i)
        {
            PClassInfo info;
            factory.getClassInfo (i, &info);
            if (std::strcmp (info.category, kARAMainFactoryClass) == 0)
                araMainFactoryClassNames.insert (info.name);
        }
       #endif

        for (Steinberg::int32 i = 0; i < numClasses; ++i)
        {
            PClassInfo info;
            factory.getClassInfo (i, &info);

            if (std::strcmp (info.category, kVstAudioEffectClass) != 0)
                continue;

            const String name (toString (info.name).trim());

            if (foundNames.contains (name, true))
                continue;

            std::unique_ptr<PClassInfo2> info2;
            std::unique_ptr<PClassInfoW> infoW;

            {
                VSTComSmartPtr<IPluginFactory2> pf2;
                VSTComSmartPtr<IPluginFactory3> pf3;

                if (pf2.loadFrom (&factory))
                {
                    info2.reset (new PClassInfo2());
                    pf2->getClassInfo2 (i, info2.get());
                }

                if (pf3.loadFrom (&factory))
                {
                    infoW.reset (new PClassInfoW());
                    pf3->getClassInfoUnicode (i, infoW.get());
                }
            }

            foundNames.add (name);

            PluginDescription desc;

            {
                VSTComSmartPtr<Vst::IComponent> component;

                if (component.loadFrom (&factory, info.cid))
                {
                    if (component->initialize (host.getFUnknown()) == kResultOk)
                    {
                        auto numInputs  = getNumSingleDirectionChannelsFor (component.get(), Direction::input);
                        auto numOutputs = getNumSingleDirectionChannelsFor (component.get(), Direction::output);

                        createPluginDescription (desc, file, companyName, name,
                                                 info, info2.get(), infoW.get(), numInputs, numOutputs);

                        component->terminate();
                    }
                    else
                    {
                        jassertfalse;
                    }
                }
                else
                {
                    jassertfalse;
                }
            }

            if (araMainFactoryClassNames.find (name) != araMainFactoryClassNames.end())
                desc.hasARAExtension = true;

            if (desc.uniqueId != 0)
                result.push_back (desc);
        }

        return result;
    }
};

//==============================================================================
struct DLLHandle
{
    DLLHandle (const File& fileToOpen)
       : dllFile (fileToOpen)
    {
        open();
    }

    ~DLLHandle()
    {
       #if JUCE_MAC
        if (bundleRef != nullptr)
       #endif
        {
            if (factory != nullptr)
                factory->release();

            using ExitModuleFn = bool (PLUGIN_API*)();

            if (auto* exitFn = (ExitModuleFn) getFunction (exitFnName))
                exitFn();

           #if JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD
            library.close();
           #endif
        }
    }

    //==============================================================================
    /** The factory should begin with a refCount of 1, so don't increment the reference count
        (ie: don't use a VSTComSmartPtr in here)! Its lifetime will be handled by this DLLHandle.
    */
    IPluginFactory* JUCE_CALLTYPE getPluginFactory()
    {
        if (factory == nullptr)
            if (auto* proc = (GetFactoryProc) getFunction (factoryFnName))
                factory = proc();

        // The plugin NEEDS to provide a factory to be able to be called a VST3!
        // Most likely you are trying to load a 32-bit VST3 from a 64-bit host
        // or vice versa.
        jassert (factory != nullptr);
        return factory;
    }

    void* getFunction (const char* functionName)
    {
       #if JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD
        return library.getFunction (functionName);
       #elif JUCE_MAC
        if (bundleRef == nullptr)
            return nullptr;

        CFUniquePtr<CFStringRef> name (String (functionName).toCFString());
        return CFBundleGetFunctionPointerForName (bundleRef.get(), name.get());
       #endif
    }

    File getFile() const noexcept  { return dllFile; }

private:
    File dllFile;
    IPluginFactory* factory = nullptr;

    static constexpr const char* factoryFnName = "GetPluginFactory";

   #if JUCE_WINDOWS
    static constexpr const char* entryFnName = "InitDll";
    static constexpr const char* exitFnName  = "ExitDll";

    using EntryProc = bool (PLUGIN_API*)();
   #elif JUCE_LINUX || JUCE_BSD
    static constexpr const char* entryFnName = "ModuleEntry";
    static constexpr const char* exitFnName  = "ModuleExit";

    using EntryProc = bool (PLUGIN_API*) (void*);
   #elif JUCE_MAC
    static constexpr const char* entryFnName = "bundleEntry";
    static constexpr const char* exitFnName  = "bundleExit";

    using EntryProc = bool (*) (CFBundleRef);
   #endif

    //==============================================================================
   #if JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD
    DynamicLibrary library;

    bool open()
    {
        if (library.open (dllFile.getFullPathName()))
        {
            if (auto* proc = (EntryProc) getFunction (entryFnName))
            {
               #if JUCE_WINDOWS
                if (proc())
               #else
                if (proc (library.getNativeHandle()))
               #endif
                    return true;
            }
            else
            {
                // this is required for some plug-ins which don't export the dll entry point function
                return true;
            }

            library.close();
        }

        return false;
    }
   #elif JUCE_MAC
    CFUniquePtr<CFBundleRef> bundleRef;

    bool open()
    {
        auto* utf8 = dllFile.getFullPathName().toRawUTF8();

        if (auto url = CFUniquePtr<CFURLRef> (CFURLCreateFromFileSystemRepresentation (nullptr,
                                                                                       (const UInt8*) utf8,
                                                                                       (CFIndex) std::strlen (utf8),
                                                                                       dllFile.isDirectory())))
        {
            bundleRef.reset (CFBundleCreate (kCFAllocatorDefault, url.get()));

            if (bundleRef != nullptr)
            {
                CFObjectHolder<CFErrorRef> error;

                if (CFBundleLoadExecutableAndReturnError (bundleRef.get(), &error.object))
                    if (auto* proc = (EntryProc) getFunction (entryFnName))
                        if (proc (bundleRef.get()))
                            return true;

                if (error.object != nullptr)
                    if (auto failureMessage = CFUniquePtr<CFStringRef> (CFErrorCopyFailureReason (error.object)))
                        DBG (String::fromCFString (failureMessage.get()));

                bundleRef = nullptr;
            }
        }

        return false;
    }
   #endif

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DLLHandle)
};

struct DLLHandleCache final : public DeletedAtShutdown
{
    DLLHandleCache() = default;
    ~DLLHandleCache() override { clearSingletonInstance(); }

    JUCE_DECLARE_SINGLETON (DLLHandleCache, false)

    DLLHandle& findOrCreateHandle (const String& modulePath)
    {
       #if JUCE_LINUX || JUCE_BSD
        File file (getDLLFileFromBundle (modulePath));
       #else
        File file (modulePath);
       #endif

        auto it = std::find_if (openHandles.begin(), openHandles.end(),
                                [&] (const std::unique_ptr<DLLHandle>& handle)
                                {
                                     return file == handle->getFile();
                                });

        if (it != openHandles.end())
            return *it->get();

        openHandles.push_back (std::make_unique<DLLHandle> (file));
        return *openHandles.back().get();
    }

private:
   #if JUCE_LINUX || JUCE_BSD
    File getDLLFileFromBundle (const String& bundlePath) const
    {
        auto machineName = []() -> String
        {
            struct utsname unameData;
            auto res = uname (&unameData);

            if (res != 0)
                return {};

            return unameData.machine;
        }();

        File file (bundlePath);

        return file.getChildFile ("Contents")
                   .getChildFile (machineName + "-linux")
                   .getChildFile (file.getFileNameWithoutExtension() + ".so");
    }
   #endif

    std::vector<std::unique_ptr<DLLHandle>> openHandles;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DLLHandleCache)
};


JUCE_IMPLEMENT_SINGLETON (DLLHandleCache)

//==============================================================================
#if JUCE_LINUX || JUCE_BSD

class RunLoop  final  : public Steinberg::Linux::IRunLoop
{
public:
    RunLoop() = default;

    ~RunLoop()
    {
        for (const auto& h : eventHandlerMap)
            LinuxEventLoop::unregisterFdCallback (h.first);
    }

    //==============================================================================
    tresult PLUGIN_API registerEventHandler (Linux::IEventHandler* handler,
                                             Linux::FileDescriptor fd) override
    {
        if (handler == nullptr)
            return kInvalidArgument;

        auto& handlers = eventHandlerMap[fd];

        if (handlers.empty())
        {
            LinuxEventLoop::registerFdCallback (fd, [this] (int descriptor)
            {
                for (auto* h : eventHandlerMap[descriptor])
                    h->onFDIsSet (descriptor);

                return true;
            });
        }

        handlers.push_back (handler);

        return kResultTrue;
    }

    tresult PLUGIN_API unregisterEventHandler (Linux::IEventHandler* handler) override
    {
        if (handler == nullptr)
            return kInvalidArgument;

        for (auto iter = eventHandlerMap.begin(), end = eventHandlerMap.end(); iter != end;)
        {
            auto& handlers = iter->second;

            auto handlersIter = std::find (std::begin (handlers), std::end (handlers), handler);

            if (handlersIter != std::end (handlers))
            {
                handlers.erase (handlersIter);

                if (handlers.empty())
                {
                    LinuxEventLoop::unregisterFdCallback (iter->first);
                    iter = eventHandlerMap.erase (iter);
                    continue;
                }
            }

            ++iter;
        }

        return kResultTrue;
    }

    //==============================================================================
    tresult PLUGIN_API registerTimer (Linux::ITimerHandler* handler, Linux::TimerInterval milliseconds) override
    {
        if (handler == nullptr || milliseconds <= 0)
            return kInvalidArgument;

        timerCallers.emplace_back (handler, (int) milliseconds);
        return kResultTrue;
    }

    tresult PLUGIN_API unregisterTimer (Linux::ITimerHandler* handler) override
    {
        auto iter = std::find (timerCallers.begin(), timerCallers.end(), handler);

        if (iter == timerCallers.end())
            return kInvalidArgument;

        timerCallers.erase (iter);
        return kResultTrue;
    }

    //==============================================================================
    uint32 PLUGIN_API addRef() override                                { return 1000; }
    uint32 PLUGIN_API release() override                               { return 1000; }
    tresult PLUGIN_API queryInterface (const TUID, void**) override    { return kNoInterface; }

private:
    //==============================================================================
    struct TimerCaller final : private Timer
    {
        TimerCaller (Linux::ITimerHandler* h, int interval)  : handler (h)  { startTimer (interval); }
        ~TimerCaller() override { stopTimer(); }

        void timerCallback() override  { handler->onTimer(); }

        bool operator== (Linux::ITimerHandler* other) const noexcept { return handler == other; }

        Linux::ITimerHandler* handler = nullptr;
    };

    std::unordered_map<Linux::FileDescriptor, std::vector<Linux::IEventHandler*>> eventHandlerMap;
    std::list<TimerCaller> timerCallers;

    //==============================================================================
    JUCE_DECLARE_NON_MOVEABLE (RunLoop)
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RunLoop)
};

#endif

//==============================================================================
struct VST3ModuleHandle final : public ReferenceCountedObject
{
    explicit VST3ModuleHandle (const File& pluginFile, const PluginDescription& pluginDesc)
        : file (pluginFile)
    {
        if (open (pluginDesc))
        {
            isOpen = true;
            getActiveModules().add (this);
        }
    }

    ~VST3ModuleHandle()
    {
        if (isOpen)
            getActiveModules().removeFirstMatchingValue (this);
    }

    //==============================================================================
    using Ptr = ReferenceCountedObjectPtr<VST3ModuleHandle>;

    static VST3ModuleHandle::Ptr findOrCreateModule (const File& file,
                                                     const PluginDescription& description)
    {
        for (auto* module : getActiveModules())
        {
            // VST3s are basically shells, you must therefore check their name along with their file:
            if (module->file == file && module->name == description.name)
                return module;
        }

        VST3ModuleHandle::Ptr modulePtr (new VST3ModuleHandle (file, description));

        if (! modulePtr->isOpen)
            modulePtr = nullptr;

        return modulePtr;
    }

    //==============================================================================
    IPluginFactory* getPluginFactory()
    {
        return DLLHandleCache::getInstance()->findOrCreateHandle (file.getFullPathName()).getPluginFactory();
    }

    File getFile() const noexcept    { return file; }
    String getName() const noexcept  { return name; }

private:
    //==============================================================================
    static Array<VST3ModuleHandle*>& getActiveModules()
    {
        static Array<VST3ModuleHandle*> activeModules;
        return activeModules;
    }

    //==============================================================================
    bool open (const PluginDescription& description)
    {
        auto pluginFactory = addVSTComSmartPtrOwner (DLLHandleCache::getInstance()->findOrCreateHandle (file.getFullPathName()).getPluginFactory());

        if (pluginFactory != nullptr)
        {
            auto numClasses = pluginFactory->countClasses();

            for (Steinberg::int32 i = 0; i < numClasses; ++i)
            {
                PClassInfo info;
                pluginFactory->getClassInfo (i, &info);

                if (std::strcmp (info.category, kVstAudioEffectClass) != 0)
                    continue;

                const auto uniqueId = getHashForRange (getNormalisedTUID (info.cid));
                const auto deprecatedUid = getHashForRange (info.cid);

                if (toString (info.name).trim() == description.name
                    && (uniqueId == description.uniqueId || deprecatedUid == description.deprecatedUid))
                {
                    name = description.name;
                    return true;
                }
            }
        }

        return false;
    }

    File file;
    String name;
    bool isOpen = false;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3ModuleHandle)
};

template <typename Type, size_t N>
static int compareWithString (Type (&charArray)[N], const String& str)
{
    return std::strncmp (str.toRawUTF8(),
                         charArray,
                         std::min (str.getNumBytesAsUTF8(), (size_t) numElementsInArray (charArray)));
}

template <typename Callback>
static void forEachARAFactory ([[maybe_unused]] IPluginFactory* pluginFactory, [[maybe_unused]] Callback&& cb)
{
   #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
    const auto numClasses = pluginFactory->countClasses();
    for (Steinberg::int32 i = 0; i < numClasses; ++i)
    {
        PClassInfo info;
        pluginFactory->getClassInfo (i, &info);

        if (std::strcmp (info.category, kARAMainFactoryClass) == 0)
        {
            const bool keepGoing = cb (info);
            if (! keepGoing)
                break;
        }
    }
   #endif
}

static std::shared_ptr<const ARA::ARAFactory> getARAFactory ([[maybe_unused]] Steinberg::IPluginFactory* pluginFactory,
                                                             [[maybe_unused]] const String& pluginName)
{
    std::shared_ptr<const ARA::ARAFactory> factory;

   #if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)
    forEachARAFactory (pluginFactory,
                       [&pluginFactory, &pluginName, &factory] (const auto& pcClassInfo)
                       {
                           if (compareWithString (pcClassInfo.name, pluginName) == 0)
                           {
                               ARA::IMainFactory* source;
                               if (pluginFactory->createInstance (pcClassInfo.cid, ARA::IMainFactory::iid, (void**) &source)
                                   == Steinberg::kResultOk)
                               {
                                   factory = getOrCreateARAFactory (source->getFactory(),
                                                                    [source]() { source->release(); });
                                   return false;
                               }
                               jassert (source == nullptr);
                           }

                           return true;
                       });
   #endif

    return factory;
}

static std::shared_ptr<const ARA::ARAFactory> getARAFactory (VST3ModuleHandle& module)
{
    auto* pluginFactory = module.getPluginFactory();
    return getARAFactory (pluginFactory, module.getName());
}

//==============================================================================
struct VST3PluginWindow final : public AudioProcessorEditor,
                                private ComponentMovementWatcher,
                                private ComponentBoundsConstrainer,
                                private IPlugFrame
{
    VST3PluginWindow (AudioPluginInstance* owner, VSTComSmartPtr<IPlugView> pluginView)
        : AudioProcessorEditor (owner),
          ComponentMovementWatcher (this),
          view (pluginView)
         #if JUCE_MAC
        , embeddedComponent (*owner)
         #endif
    {
        setSize (10, 10);
        setOpaque (true);
        setVisible (true);
        setConstrainer (this);

        warnOnFailure (view->setFrame (this));
        view->queryInterface (Steinberg::IPlugViewContentScaleSupport::iid, (void**) &scaleInterface);

        setContentScaleFactor();
        resizeToFit();

        setResizable (view->canResize() == kResultTrue, false);
    }

    ~VST3PluginWindow() override
    {
        if (scaleInterface != nullptr)
            scaleInterface->release();

        #if JUCE_LINUX || JUCE_BSD
         embeddedComponent.removeClient();
        #endif

        if (attachedCalled)
            warnOnFailure (view->removed());

        warnOnFailure (view->setFrame (nullptr));

        processor.editorBeingDeleted (this);

       #if JUCE_MAC
        embeddedComponent.setView (nullptr);
       #endif

        view = nullptr;
    }

   #if JUCE_LINUX || JUCE_BSD
    Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID queryIid, void** obj) override
    {
        if (doUIDsMatch (queryIid, Steinberg::Linux::IRunLoop::iid))
        {
            *obj = &runLoop.get();
            return kResultTrue;
        }

        jassertfalse;
        *obj = nullptr;

        return Steinberg::kNotImplemented;
    }
   #else
    JUCE_DECLARE_VST3_COM_QUERY_METHODS
   #endif

    JUCE_DECLARE_VST3_COM_REF_METHODS

    void paint (Graphics& g) override
    {
        g.fillAll (Colours::black);
    }

    void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
    {
        view->onWheel (wheel.deltaY);
    }

    void focusGained (FocusChangeType) override     { view->onFocus (true); }
    void focusLost (FocusChangeType) override       { view->onFocus (false); }

    /** It seems that most, if not all, plugins do their own keyboard hooks,
        but IPlugView does have a set of keyboard related methods...
    */
    bool keyStateChanged (bool /*isKeyDown*/) override  { return true; }
    bool keyPressed (const KeyPress& /*key*/) override  { return true; }

private:
    void checkBounds (Rectangle<int>& bounds,
                      const Rectangle<int>&,
                      const Rectangle<int>&,
                      bool,
                      bool,
                      bool,
                      bool) override
    {
        auto rect = componentToVST3Rect (bounds);
        view->checkSizeConstraint (&rect);
        bounds = vst3ToComponentRect (rect);
    }

    //==============================================================================
    void componentPeerChanged() override {}

    /*  Convert from the component's coordinate system to the hosted VST3's coordinate system. */
    ViewRect componentToVST3Rect (Rectangle<int> r) const
    {
        const auto physical = localAreaToGlobal (r) * nativeScaleFactor * getDesktopScaleFactor();
        return { 0, 0, physical.getWidth(), physical.getHeight() };
    }

    /*  Convert from the hosted VST3's coordinate system to the component's coordinate system. */
    Rectangle<int> vst3ToComponentRect (const ViewRect& vr) const
    {
        return getLocalArea (nullptr, Rectangle<int> { vr.right, vr.bottom } / (nativeScaleFactor * getDesktopScaleFactor()));
    }

    void componentMovedOrResized (bool, bool wasResized) override
    {
        if (recursiveResize || ! wasResized || getTopLevelComponent()->getPeer() == nullptr)
            return;

        if (view->canResize() == kResultTrue)
        {
            auto rect = componentToVST3Rect (getLocalBounds());
            view->checkSizeConstraint (&rect);

            {
                const ScopedValueSetter<bool> recursiveResizeSetter (recursiveResize, true);

                const auto logicalSize = vst3ToComponentRect (rect);
                setSize (logicalSize.getWidth(), logicalSize.getHeight());
            }

            embeddedComponent.setBounds (getLocalBounds());

            view->onSize (&rect);
        }
        else
        {
            ViewRect rect;
            warnOnFailure (view->getSize (&rect));

            resizeWithRect (embeddedComponent, rect);
        }

        // Some plugins don't update their cursor correctly when mousing out the window
        Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate();
    }

    using ComponentMovementWatcher::componentMovedOrResized;

    void componentVisibilityChanged() override
    {
        attachPluginWindow();
        resizeToFit();
        componentMovedOrResized (true, true);
    }

    using ComponentMovementWatcher::componentVisibilityChanged;

    void resizeToFit()
    {
        ViewRect rect;
        warnOnFailure (view->getSize (&rect));
        resizeWithRect (*this, rect);
    }

    tresult PLUGIN_API resizeView (IPlugView* incomingView, ViewRect* newSize) override
    {
        const ScopedValueSetter<bool> recursiveResizeSetter (recursiveResize, true);

        if (incomingView != nullptr && newSize != nullptr && incomingView == view.get())
        {
            const auto oldPhysicalSize = componentToVST3Rect (getLocalBounds());
            const auto logicalSize = vst3ToComponentRect (*newSize);
            setSize (logicalSize.getWidth(), logicalSize.getHeight());
            embeddedComponent.setSize (logicalSize.getWidth(), logicalSize.getHeight());

           #if JUCE_WINDOWS
            embeddedComponent.updateHWNDBounds();
           #elif JUCE_LINUX || JUCE_BSD
            embeddedComponent.updateEmbeddedBounds();
           #endif

            // According to the VST3 Workflow Diagrams, a resizeView from the plugin should
            // always trigger a response from the host which confirms the new size.
            auto currentPhysicalSize = componentToVST3Rect (getLocalBounds());

            if (currentPhysicalSize.getWidth() != oldPhysicalSize.getWidth()
                || currentPhysicalSize.getHeight() != oldPhysicalSize.getHeight()
                || ! isInOnSize)
            {
                // Guard against plug-ins immediately calling resizeView() with the same size
                const ScopedValueSetter<bool> inOnSizeSetter (isInOnSize, true);
                view->onSize (&currentPhysicalSize);
            }

            return kResultTrue;
        }

        jassertfalse;
        return kInvalidArgument;
    }

    //==============================================================================
    void resizeWithRect (Component& comp, const ViewRect& rect) const
    {
        const auto logicalSize = vst3ToComponentRect (rect);
        comp.setSize (jmax (10, logicalSize.getWidth()),
                      jmax (10, logicalSize.getHeight()));
    }

    void attachPluginWindow()
    {
        if (pluginHandle == HandleFormat{})
        {
            #if JUCE_WINDOWS
             pluginHandle = static_cast<HWND> (embeddedComponent.getHWND());
            #endif

             embeddedComponent.setBounds (getLocalBounds());
             addAndMakeVisible (embeddedComponent);

            #if JUCE_MAC
             pluginHandle = (HandleFormat) embeddedComponent.getView();
            #elif JUCE_LINUX || JUCE_BSD
             pluginHandle = (HandleFormat) embeddedComponent.getHostWindowID();
            #endif

            if (pluginHandle == HandleFormat{})
            {
                jassertfalse;
                return;
            }

            [[maybe_unused]] const auto attachedResult = view->attached ((void*) pluginHandle, defaultVST3WindowType);
            [[maybe_unused]] const auto warning = warnOnFailure (attachedResult);

            if (attachedResult == kResultOk)
                attachedCalled = true;

            updatePluginScale();
        }
    }

    void updatePluginScale()
    {
        if (scaleInterface != nullptr)
            setContentScaleFactor();
        else
            resizeToFit();
    }

    void setContentScaleFactor()
    {
        if (scaleInterface != nullptr)
        {
            [[maybe_unused]] const auto result = scaleInterface->setContentScaleFactor ((Steinberg::IPlugViewContentScaleSupport::ScaleFactor) getEffectiveScale());

           #if ! JUCE_MAC
            [[maybe_unused]] const auto warning = warnOnFailure (result);
           #endif
        }
    }

    void setScaleFactor (float s) override
    {
        userScaleFactor = s;
        setContentScaleFactor();
        resizeToFit();
    }

    float getEffectiveScale() const
    {
        return nativeScaleFactor * userScaleFactor;
    }

    //==============================================================================
    Atomic<int> refCount { 1 };
    VSTComSmartPtr<IPlugView> view;

   #if JUCE_WINDOWS
    using HandleFormat = HWND;

    struct ViewComponent final : public HWNDComponent
    {
        ViewComponent()
        {
            setOpaque (true);
            inner.addToDesktop (0);

            if (auto* peer = inner.getPeer())
                setHWND (peer->getNativeHandle());
        }

        void paint (Graphics& g) override { g.fillAll (Colours::black); }

    private:
        struct Inner final : public Component
        {
            Inner() { setOpaque (true); }
            void paint (Graphics& g) override { g.fillAll (Colours::black); }
        };

        Inner inner;
    };

    ViewComponent embeddedComponent;
   #elif JUCE_MAC
    NSViewComponentWithParent embeddedComponent;
    using HandleFormat = NSView*;
   #elif JUCE_LINUX || JUCE_BSD
    SharedResourcePointer<RunLoop> runLoop;
    XEmbedComponent embeddedComponent { true, false };
    using HandleFormat = Window;
   #else
    Component embeddedComponent;
    using HandleFormat = void*;
   #endif

    HandleFormat pluginHandle = {};
    bool recursiveResize = false, isInOnSize = false, attachedCalled = false;

    Steinberg::IPlugViewContentScaleSupport* scaleInterface = nullptr;
    float nativeScaleFactor = 1.0f;
    float userScaleFactor = 1.0f;

    struct ScaleNotifierCallback
    {
        VST3PluginWindow& window;

        void operator() (float platformScale) const
        {
            MessageManager::callAsync ([ref = Component::SafePointer<VST3PluginWindow> (&window), platformScale]
            {
                if (auto* r = ref.getComponent())
                {
                    r->nativeScaleFactor = platformScale;
                    r->setContentScaleFactor();
                    r->resizeToFit();

                   #if JUCE_WINDOWS
                    r->embeddedComponent.updateHWNDBounds();
                   #elif JUCE_LINUX || JUCE_BSD
                    r->embeddedComponent.updateEmbeddedBounds();
                   #endif
                }
            });
        }
    };

    NativeScaleFactorNotifier scaleNotifier { this, ScaleNotifierCallback { *this } };

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3PluginWindow)
};

JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) // warning about overriding deprecated methods

//==============================================================================
static bool hasARAExtension (IPluginFactory* pluginFactory, const String& pluginClassName)
{
    bool result = false;

    forEachARAFactory (pluginFactory,
                       [&pluginClassName, &result] (const auto& pcClassInfo)
                       {
                           if (compareWithString (pcClassInfo.name, pluginClassName) == 0)
                           {
                               result = true;

                               return false;
                           }

                           return true;
                       });

    return result;
}

//==============================================================================
struct VST3ComponentHolder
{
    VST3ComponentHolder (const VST3ModuleHandle::Ptr& m)  : module (m)
    {
        host = addVSTComSmartPtrOwner (new VST3HostContext());
    }

    ~VST3ComponentHolder()
    {
        terminate();
    }

    bool isIComponentAlsoIEditController() const
    {
        if (component == nullptr)
        {
            jassertfalse;
            return false;
        }

        return VSTComSmartPtr<Vst::IEditController>().loadFrom (component.get());
    }

    bool fetchController (VSTComSmartPtr<Vst::IEditController>& editController)
    {
        if (! isComponentInitialised && ! initialise())
            return false;

        editController.loadFrom (component.get());

        // Get the IEditController:
        TUID controllerCID = { 0 };

        if (editController == nullptr
            && component->getControllerClassId (controllerCID) == kResultTrue
            && FUID (controllerCID).isValid())
        {
            editController.loadFrom (factory.get(), controllerCID);
        }

        if (editController == nullptr)
        {
            // Try finding the IEditController the long way around:
            auto numClasses = factory->countClasses();

            for (Steinberg::int32 i = 0; i < numClasses; ++i)
            {
                PClassInfo classInfo;
                factory->getClassInfo (i, &classInfo);

                if (std::strcmp (classInfo.category, kVstComponentControllerClass) == 0)
                    editController.loadFrom (factory.get(), classInfo.cid);
            }
        }

        return (editController != nullptr);
    }

    //==============================================================================
    void fillInPluginDescription (PluginDescription& description) const
    {
        jassert (module != nullptr && isComponentInitialised);

        PFactoryInfo factoryInfo;
        factory->getFactoryInfo (&factoryInfo);

        auto classIdx = getClassIndex (module->getName());

        if (classIdx >= 0)
        {
            PClassInfo info;
            [[maybe_unused]] bool success = (factory->getClassInfo (classIdx, &info) == kResultOk);
            jassert (success);

            VSTComSmartPtr<IPluginFactory2> pf2;
            VSTComSmartPtr<IPluginFactory3> pf3;

            std::unique_ptr<PClassInfo2> info2;
            std::unique_ptr<PClassInfoW> infoW;

            if (pf2.loadFrom (factory.get()))
            {
                info2.reset (new PClassInfo2());
                pf2->getClassInfo2 (classIdx, info2.get());
            }
            else
            {
                info2.reset();
            }

            if (pf3.loadFrom (factory.get()))
            {
                pf3->setHostContext (host->getFUnknown());
                infoW.reset (new PClassInfoW());
                pf3->getClassInfoUnicode (classIdx, infoW.get());
            }
            else
            {
                infoW.reset();
            }

            Vst::BusInfo bus;
            int totalNumInputChannels = 0, totalNumOutputChannels = 0;

            int n = component->getBusCount (Vst::kAudio, Vst::kInput);
            for (int i = 0; i < n; ++i)
                if (component->getBusInfo (Vst::kAudio, Vst::kInput, i, bus) == kResultOk)
                    totalNumInputChannels += ((bus.flags & Vst::BusInfo::kDefaultActive) != 0 ? bus.channelCount : 0);

            n = component->getBusCount (Vst::kAudio, Vst::kOutput);
            for (int i = 0; i < n; ++i)
                if (component->getBusInfo (Vst::kAudio, Vst::kOutput, i, bus) == kResultOk)
                    totalNumOutputChannels += ((bus.flags & Vst::BusInfo::kDefaultActive) != 0 ? bus.channelCount : 0);

            createPluginDescription (description, module->getFile(),
                                     factoryInfo.vendor, module->getName(),
                                     info, info2.get(), infoW.get(),
                                     totalNumInputChannels,
                                     totalNumOutputChannels);

            description.hasARAExtension = hasARAExtension (factory.get(), description.name);

            return;
        }

        jassertfalse;
    }

    //==============================================================================
    bool initialise()
    {
        if (isComponentInitialised)
            return true;

        // It's highly advisable to create your plugins using the message thread.
        // The VST3 spec requires that many of the functions called during
        // initialisation are only called from the message thread.
        JUCE_ASSERT_MESSAGE_THREAD

        factory = addVSTComSmartPtrOwner (module->getPluginFactory());

        int classIdx;
        if ((classIdx = getClassIndex (module->getName())) < 0)
            return false;

        PClassInfo info;
        if (factory->getClassInfo (classIdx, &info) != kResultOk)
            return false;

        if (! component.loadFrom (factory.get(), info.cid) || component == nullptr)
            return false;

        cidOfComponent = FUID (info.cid);

        if (warnOnFailure (component->initialize (host->getFUnknown())) != kResultOk)
            return false;

        isComponentInitialised = true;

        return true;
    }

    void terminate()
    {
        if (isComponentInitialised)
        {
            component->terminate();
            isComponentInitialised = false;
        }

        component = nullptr;
    }

    //==============================================================================
    int getClassIndex (const String& className) const
    {
        PClassInfo info;
        const Steinberg::int32 numClasses = factory->countClasses();

        for (Steinberg::int32 j = 0; j < numClasses; ++j)
            if (factory->getClassInfo (j, &info) == kResultOk
                 && std::strcmp (info.category, kVstAudioEffectClass) == 0
                 && toString (info.name).trim() == className)
                return j;

        return -1;
    }

    //==============================================================================
    VST3ModuleHandle::Ptr module;
    VSTComSmartPtr<IPluginFactory> factory;
    VSTComSmartPtr<VST3HostContext> host;
    VSTComSmartPtr<Vst::IComponent> component;
    FUID cidOfComponent;

    bool isComponentInitialised = false;
};

//==============================================================================
/*  A queue which can store up to one element.

    This is more memory-efficient than storing large vectors of
    parameter changes that we'll just throw away.
*/
class ParamValueQueue final : public Vst::IParamValueQueue
{
public:
    ParamValueQueue (Vst::ParamID idIn, Steinberg::int32 parameterIndexIn)
        : paramId (idIn), parameterIndex (parameterIndexIn) {}

    virtual ~ParamValueQueue() = default;

    JUCE_DECLARE_VST3_COM_REF_METHODS
    JUCE_DECLARE_VST3_COM_QUERY_METHODS

    Vst::ParamID PLUGIN_API getParameterId() override { return paramId; }

    Steinberg::int32 getParameterIndex() const noexcept { return parameterIndex; }

    Steinberg::int32 PLUGIN_API getPointCount() override { return size; }

    tresult PLUGIN_API getPoint (Steinberg::int32 index,
                                 Steinberg::int32& sampleOffset,
                                 Vst::ParamValue& value) override
    {
        if (! isPositiveAndBelow (index, size))
            return kResultFalse;

        sampleOffset = 0;
        value = cachedValue;

        return kResultTrue;
    }

    tresult PLUGIN_API addPoint (Steinberg::int32,
                                 Vst::ParamValue value,
                                 Steinberg::int32& index) override
    {
        index = size++;
        set ((float) value);

        return kResultTrue;
    }

    void set (float valueIn)
    {
        cachedValue = valueIn;
        size = 1;
    }

    void clear() { size = 0; }

    float get() const noexcept
    {
        jassert (size > 0);
        return cachedValue;
    }

private:
    const Vst::ParamID paramId;
    const Steinberg::int32 parameterIndex;
    float cachedValue;
    Steinberg::int32 size = 0;
    Atomic<int> refCount;
};

//==============================================================================
/*  An implementation of IParameterChanges with some important characteristics:
    - Lookup by index is O(1)
    - Lookup by paramID is also O(1)
    - addParameterData never allocates, as long you pass a paramID already passed to initialise
*/
class ParameterChanges final : public Vst::IParameterChanges
{
    static constexpr Steinberg::int32 notInVector = -1;

    struct Entry
    {
        explicit Entry (std::unique_ptr<ParamValueQueue> queue) : ptr (addVSTComSmartPtrOwner (queue.release())) {}

        VSTComSmartPtr<ParamValueQueue> ptr;
        Steinberg::int32 index = notInVector;
    };

    using Map = std::unordered_map<Vst::ParamID, Entry>;
    using Queues = std::vector<Entry*>;

public:
    virtual ~ParameterChanges() = default;

    JUCE_DECLARE_VST3_COM_REF_METHODS
    JUCE_DECLARE_VST3_COM_QUERY_METHODS

    Steinberg::int32 PLUGIN_API getParameterCount() override
    {
        return (Steinberg::int32) queues.size();
    }

    ParamValueQueue* PLUGIN_API getParameterData (Steinberg::int32 index) override
    {
        if (isPositiveAndBelow (index, queues.size()))
        {
            auto& entry = queues[(size_t) index];
            // If this fails, our container has become internally inconsistent
            jassert (entry->index == index);
            return entry->ptr.get();
        }

        return nullptr;
    }

    ParamValueQueue* PLUGIN_API addParameterData (const Vst::ParamID& id,
                                                  Steinberg::int32& index) override
    {
        const auto it = map.find (id);

        if (it == map.end())
            return nullptr;

        auto& result = it->second;

        if (result.index == notInVector)
        {
            result.index = (Steinberg::int32) queues.size();
            queues.push_back (&result);
        }

        index = result.index;
        return result.ptr.get();
    }

    void set (Vst::ParamID id, float value)
    {
        Steinberg::int32 indexOut = notInVector;

        if (auto* queue = addParameterData (id, indexOut))
            queue->set (value);
    }

    void clear()
    {
        for (auto* item : queues)
            item->index = notInVector;

        queues.clear();
    }

    void initialise (const std::vector<Vst::ParamID>& idsIn)
    {
        for (const auto [index, id] : enumerate (idsIn))
            map.emplace (id, Entry { std::make_unique<ParamValueQueue> (id, (Steinberg::int32) index) });

        queues.reserve (map.size());
        queues.clear();
    }

    template <typename Callback>
    void forEach (Callback&& callback) const
    {
        for (const auto* item : queues)
        {
            auto* ptr = item->ptr.get();
            callback (ptr->getParameterIndex(), ptr->getParameterId(), ptr->get());
        }
    }

private:
    Map map;
    Queues queues;
    Atomic<int> refCount;
};

//==============================================================================
class VST3PluginInstance final  : public AudioPluginInstance
{
public:
    //==============================================================================
    struct VST3Parameter final  : public Parameter
    {
        VST3Parameter (VST3PluginInstance& parent,
                       Steinberg::int32 vstParameterIndex,
                       Steinberg::Vst::ParamID parameterID,
                       bool parameterIsAutomatable)
            : pluginInstance (parent),
              vstParamIndex (vstParameterIndex),
              paramID (parameterID),
              automatable (parameterIsAutomatable)
        {
        }

        float getValue() const override
        {
            return pluginInstance.cachedParamValues.get (vstParamIndex);
        }

        /*  The 'normal' setValue call, which will update both the processor and editor.
        */
        void setValue (float newValue) override
        {
            pluginInstance.cachedParamValues.set (vstParamIndex, newValue);
            pluginInstance.parameterDispatcher.push (vstParamIndex, newValue);
        }

        /*  If we're syncing the editor to the processor, the processor won't need to
            be notified about the parameter updates, so we can avoid flagging the
            change when updating the float cache.
        */
        void setValueWithoutUpdatingProcessor (float newValue)
        {
            pluginInstance.cachedParamValues.setWithoutNotifying (vstParamIndex, newValue);
            sendValueChangedMessageToListeners (newValue);
        }

        String getText (float value, int maximumLength) const override
        {
            MessageManagerLock lock;

            if (pluginInstance.editController != nullptr)
            {
                Vst::String128 result;

                if (pluginInstance.editController->getParamStringByValue (paramID, value, result) == kResultOk)
                    return toString (result).substring (0, maximumLength);
            }

            return Parameter::getText (value, maximumLength);
        }

        float getValueForText (const String& text) const override
        {
            MessageManagerLock lock;

            if (pluginInstance.editController != nullptr)
            {
                Vst::ParamValue result;

                if (pluginInstance.editController->getParamValueByString (paramID, toString (text), result) == kResultOk)
                    return (float) result;
            }

            return Parameter::getValueForText (text);
        }

        float getDefaultValue() const override
        {
            return (float) getParameterInfo().defaultNormalizedValue;
        }

        String getName (int /*maximumStringLength*/) const override
        {
            return toString (getParameterInfo().title);
        }

        String getLabel() const override
        {
            return toString (getParameterInfo().units);
        }

        bool isAutomatable() const override
        {
            return automatable;
        }

        bool isDiscrete() const override
        {
            return discrete;
        }

        int getNumSteps() const override
        {
            return numSteps;
        }

        StringArray getAllValueStrings() const override
        {
            return {};
        }

        String getParameterID() const override
        {
            return String (paramID);
        }

        Steinberg::Vst::ParamID getParamID() const noexcept { return paramID; }

    private:
        Vst::ParameterInfo getParameterInfo() const
        {
            return pluginInstance.getParameterInfoForIndex (vstParamIndex);
        }

        VST3PluginInstance& pluginInstance;
        const Steinberg::int32 vstParamIndex;
        const Steinberg::Vst::ParamID paramID;
        const bool automatable;
        const int numSteps = [&]
        {
            auto stepCount = getParameterInfo().stepCount;
            return stepCount == 0 ? AudioProcessor::getDefaultNumParameterSteps()
                                  : stepCount + 1;
        }();
        const bool discrete = getNumSteps() != AudioProcessor::getDefaultNumParameterSteps();
    };

    //==============================================================================
    explicit VST3PluginInstance (std::unique_ptr<VST3ComponentHolder> componentHolder)
        : AudioPluginInstance (getBusProperties (componentHolder->component)),
          holder (std::move (componentHolder))
    {
        jassert (holder->isComponentInitialised);
        holder->host->setPlugin (this);
    }

    ~VST3PluginInstance() override
    {
        callOnMessageThread ([this] { cleanup(); });
    }

    void cleanup()
    {
        jassert (getActiveEditor() == nullptr); // You must delete any editors before deleting the plugin instance!

        releaseResources();

        if (editControllerConnection != nullptr && componentConnection != nullptr)
        {
            editControllerConnection->disconnect (componentConnection.get());
            componentConnection->disconnect (editControllerConnection.get());
        }

        editController->setComponentHandler (nullptr);

        if (isControllerInitialised && ! holder->isIComponentAlsoIEditController())
            editController->terminate();

        holder->terminate();

        componentConnection = nullptr;
        editControllerConnection = nullptr;
        unitData = nullptr;
        unitInfo = nullptr;
        programListData = nullptr;
        componentHandler2 = nullptr;
        componentHandler = nullptr;
        processor = nullptr;
        midiMapping = nullptr;
        editController2 = nullptr;
        editController = nullptr;
    }

    //==============================================================================
    bool initialise()
    {
        // It's highly advisable to create your plugins using the message thread.
        // The VST3 spec requires that many of the functions called during
        // initialisation are only called from the message thread.
        JUCE_ASSERT_MESSAGE_THREAD

        if (! holder->initialise())
            return false;

        if (! (isControllerInitialised || holder->fetchController (editController)))
            return false;

        // If the IComponent and IEditController are the same, we will have
        // already initialized the object at this point and should avoid doing so again.
        if (! holder->isIComponentAlsoIEditController())
            editController->initialize (holder->host->getFUnknown());

        isControllerInitialised = true;
        editController->setComponentHandler (holder->host.get());
        grabInformationObjects();
        interconnectComponentAndController();

        auto configureParameters = [this]
        {
            initialiseParameterList();
            synchroniseStates();
            syncProgramNames();
        };

        configureParameters();
        setupIO();

        // Some plug-ins don't present their parameters until after the IO has been
        // configured, so we need to jump though all these hoops again
        if (getParameters().isEmpty() && editController->getParameterCount() > 0)
            configureParameters();

        updateMidiMappings();

        parameterDispatcher.start (*editController);

        return true;
    }

    void getExtensions (ExtensionsVisitor& visitor) const override
    {
        struct Extensions final :  public ExtensionsVisitor::VST3Client,
                                   public ExtensionsVisitor::ARAClient
        {
            explicit Extensions (const VST3PluginInstance* instanceIn) : instance (instanceIn) {}

            Steinberg::Vst::IComponent* getIComponentPtr() const noexcept override   { return instance->holder->component.get(); }

            MemoryBlock getPreset() const override             { return instance->getStateForPresetFile(); }

            bool setPreset (const MemoryBlock& rawData) const override
            {
                return instance->setStateFromPresetFile (rawData);
            }

            void createARAFactoryAsync (std::function<void (ARAFactoryWrapper)> cb) const noexcept override
            {
                cb (ARAFactoryWrapper { ::juce::getARAFactory (*(instance->holder->module)) });
            }

            const VST3PluginInstance* instance = nullptr;
        };

        Extensions extensions { this };
        visitor.visitVST3Client (extensions);

        if (::juce::getARAFactory (*(holder->module)))
        {
            visitor.visitARAClient (extensions);
        }
    }

    void* getPlatformSpecificData() override   { return holder->component.get(); }

    void updateMidiMappings()
    {
        // MIDI mappings will always be updated on the main thread, but we need to ensure
        // that we're not simultaneously reading them on the audio thread.
        const SpinLock::ScopedLockType processLock (processMutex);

        if (midiMapping != nullptr)
            storedMidiMapping.storeMappings (*midiMapping);
    }

    //==============================================================================
    const String getName() const override
    {
        auto& module = holder->module;
        return module != nullptr ? module->getName() : String();
    }

    std::vector<Vst::SpeakerArrangement> getActualArrangements (bool isInput) const
    {
        std::vector<Vst::SpeakerArrangement> result;

        const auto numBuses = getBusCount (isInput);

        for (auto i = 0; i < numBuses; ++i)
            result.push_back (getArrangementForBus (processor.get(), isInput, i));

        return result;
    }

    std::optional<std::vector<Vst::SpeakerArrangement>> busLayoutsToArrangements (bool isInput) const
    {
        std::vector<Vst::SpeakerArrangement> result;

        const auto numBuses = getBusCount (isInput);

        for (auto i = 0; i < numBuses; ++i)
        {
            if (const auto arr = getVst3SpeakerArrangement (getBus (isInput, i)->getLastEnabledLayout()))
                result.push_back (*arr);
            else
                return {};
        }

        return result;
    }

    void prepareToPlay (double newSampleRate, int estimatedSamplesPerBlock) override
    {
        // The VST3 spec requires that IComponent::setupProcessing() is called on the message
        // thread. If you call it from a different thread, some plugins may break.
        JUCE_ASSERT_MESSAGE_THREAD
        MessageManagerLock lock;

        const SpinLock::ScopedLockType processLock (processMutex);

        // Avoid redundantly calling things like setActive, which can be a heavy-duty call for some plugins:
        if (isActive
              && approximatelyEqual (getSampleRate(), newSampleRate)
              && getBlockSize() == estimatedSamplesPerBlock)
            return;

        using namespace Vst;

        // If the plugin has already been activated (prepareToPlay has been called twice without
        // a matching releaseResources call) deactivate it so that the speaker layout and bus
        // activation can be updated safely.
        deactivate();

        ProcessSetup setup;
        setup.symbolicSampleSize    = isUsingDoublePrecision() ? kSample64 : kSample32;
        setup.maxSamplesPerBlock    = estimatedSamplesPerBlock;
        setup.sampleRate            = newSampleRate;
        setup.processMode           = isNonRealtime() ? kOffline : kRealtime;

        warnOnFailure (processor->setupProcessing (setup));

        holder->initialise();

        auto inArrangements  = busLayoutsToArrangements (true) .value_or (std::vector<SpeakerArrangement>{});
        auto outArrangements = busLayoutsToArrangements (false).value_or (std::vector<SpeakerArrangement>{});

        // Some plug-ins will crash if you pass a nullptr to setBusArrangements!
        SpeakerArrangement nullArrangement = {};
        auto* inData  = inArrangements .empty() ? &nullArrangement : inArrangements .data();
        auto* outData = outArrangements.empty() ? &nullArrangement : outArrangements.data();

        warnOnFailure (processor->setBusArrangements (inData,  static_cast<int32> (inArrangements .size()),
                                                      outData, static_cast<int32> (outArrangements.size())));

        const auto inArrActual  = getActualArrangements (true);
        const auto outArrActual = getActualArrangements (false);

        jassert (inArrActual == inArrangements && outArrActual == outArrangements);

        // Needed for having the same sample rate in processBlock(); some plugins need this!
        setRateAndBufferSizeDetails (newSampleRate, estimatedSamplesPerBlock);

        auto numInputBuses  = getBusCount (true);
        auto numOutputBuses = getBusCount (false);

        for (int i = 0; i < numInputBuses; ++i)
            warnOnFailure (holder->component->activateBus (Vst::kAudio, Vst::kInput,  i, getBus (true,  i)->isEnabled() ? 1 : 0));

        for (int i = 0; i < numOutputBuses; ++i)
            warnOnFailure (holder->component->activateBus (Vst::kAudio, Vst::kOutput, i, getBus (false, i)->isEnabled() ? 1 : 0));

        setLatencySamples (jmax (0, (int) processor->getLatencySamples()));

        inputBusMap .prepare (createChannelMappings (true));
        outputBusMap.prepare (createChannelMappings (false));

        setStateForAllMidiBuses (true);

        warnOnFailure (holder->component->setActive (true));
        warnOnFailureIfImplemented (processor->setProcessing (true));

        isActive = true;
    }

    void releaseResources() override
    {
        const SpinLock::ScopedLockType lock (processMutex);
        deactivate();
    }

    bool supportsDoublePrecisionProcessing() const override
    {
        return (processor->canProcessSampleSize (Vst::kSample64) == kResultTrue);
    }

    //==============================================================================
    /*  Important: It is strongly recommended to use this function if you need to
        find the JUCE parameter corresponding to a particular IEditController
        parameter.

        Note that a parameter at a given index in the IEditController does not
        necessarily correspond to the parameter at the same index in
        AudioProcessor::getParameters().
    */
    VST3Parameter* getParameterForID (Vst::ParamID paramID) const
    {
        const auto it = idToParamMap.find (paramID);
        return it != idToParamMap.end() ? it->second : nullptr;
    }

    //==============================================================================
    void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
    {
        jassert (! isUsingDoublePrecision());

        const SpinLock::ScopedLockType processLock (processMutex);

        if (isActive && processor != nullptr)
            processAudio (buffer, midiMessages, Vst::kSample32, false);
    }

    void processBlock (AudioBuffer<double>& buffer, MidiBuffer& midiMessages) override
    {
        jassert (isUsingDoublePrecision());

        const SpinLock::ScopedLockType processLock (processMutex);

        if (isActive && processor != nullptr)
            processAudio (buffer, midiMessages, Vst::kSample64, false);
    }

    void processBlockBypassed (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
    {
        jassert (! isUsingDoublePrecision());

        const SpinLock::ScopedLockType processLock (processMutex);

        if (bypassParam != nullptr)
        {
            if (isActive && processor != nullptr)
                processAudio (buffer, midiMessages, Vst::kSample32, true);
        }
        else
        {
            AudioProcessor::processBlockBypassed (buffer, midiMessages);
        }
    }

    void processBlockBypassed (AudioBuffer<double>& buffer, MidiBuffer& midiMessages) override
    {
        jassert (isUsingDoublePrecision());

        const SpinLock::ScopedLockType processLock (processMutex);

        if (bypassParam != nullptr)
        {
            if (isActive && processor != nullptr)
                processAudio (buffer, midiMessages, Vst::kSample64, true);
        }
        else
        {
            AudioProcessor::processBlockBypassed (buffer, midiMessages);
        }
    }

    //==============================================================================
    template <typename FloatType>
    void processAudio (AudioBuffer<FloatType>& buffer, MidiBuffer& midiMessages,
                       Vst::SymbolicSampleSizes sampleSize, bool isProcessBlockBypassedCall)
    {
        using namespace Vst;
        auto numSamples = buffer.getNumSamples();

        auto numInputAudioBuses  = getBusCount (true);
        auto numOutputAudioBuses = getBusCount (false);

        updateBypass (isProcessBlockBypassedCall);

        ProcessData data;
        data.processMode            = isNonRealtime() ? kOffline : kRealtime;
        data.symbolicSampleSize     = sampleSize;
        data.numInputs              = numInputAudioBuses;
        data.numOutputs             = numOutputAudioBuses;
        data.inputParameterChanges  = inputParameterChanges.get();
        data.outputParameterChanges = outputParameterChanges.get();
        data.numSamples             = (Steinberg::int32) numSamples;

        updateTimingInformation (data, getSampleRate());

        for (int i = getTotalNumInputChannels(); i < buffer.getNumChannels(); ++i)
            buffer.clear (i, 0, numSamples);

        inputParameterChanges->clear();
        outputParameterChanges->clear();

        associateWith (data, buffer);
        associateWith (data, midiMessages);

        cachedParamValues.ifSet ([&] (Steinberg::int32 index, float value)
        {
            inputParameterChanges->set (cachedParamValues.getParamID (index), value);
        });

        processor->process (data);

        outputParameterChanges->forEach ([&] (Steinberg::int32 index, Vst::ParamID id, float value)
        {
            cachedParamValues.setWithoutNotifying (index, value);

            if (auto* param = getParameterForID (id))
                param->setValueWithoutUpdatingProcessor (value);
        });

        midiMessages.clear();
        MidiEventList::toMidiBuffer (midiMessages, *midiOutputs);
    }

    //==============================================================================
    bool canAddBus (bool) const override                                       { return false; }
    bool canRemoveBus (bool) const override                                    { return false; }

    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        const SpinLock::ScopedLockType processLock (processMutex);

        // if the processor is not active, we ask the underlying plug-in if the
        // layout is actually supported
        if (! isActive)
            return canApplyBusesLayout (layouts);

        // not much we can do to check the layout while the audio processor is running
        // Let's at least check if it is a VST3 compatible layout
        for (const auto isInput : { true, false })
        {
            auto n = getBusCount (isInput);

            for (int i = 0; i < n; ++i)
                if (getChannelLayoutOfBus (isInput, i).isDiscreteLayout())
                    return false;
        }

        return true;
    }

    bool syncBusLayouts (const BusesLayout& layouts) const
    {
        for (const auto isInput : { true, false })
        {
            auto n = getBusCount (isInput);
            const Vst::BusDirection vstDir = (isInput ? Vst::kInput : Vst::kOutput);

            for (int busIdx = 0; busIdx < n; ++busIdx)
            {
                const bool isEnabled = (! layouts.getChannelSet (isInput, busIdx).isDisabled());

                if (holder->component->activateBus (Vst::kAudio, vstDir, busIdx, (isEnabled ? 1 : 0)) != kResultOk)
                    return false;
            }
        }

        const auto getPotentialArrangements = [&] (bool isInput) -> std::optional<std::vector<Vst::SpeakerArrangement>>
        {
            std::vector<Vst::SpeakerArrangement> result;

            for (int i = 0; i < layouts.getBuses (isInput).size(); ++i)
            {
                const auto& requested = layouts.getChannelSet (isInput, i);

                if (const auto arr = getVst3SpeakerArrangement (requested.isDisabled() ? getBus (isInput, i)->getLastEnabledLayout() : requested))
                    result.push_back (*arr);
                else
                    return {};
            }

            return result;
        };

        auto inArrangements  = getPotentialArrangements (true);
        auto outArrangements = getPotentialArrangements (false);

        if (! inArrangements.has_value() || ! outArrangements.has_value())
        {
            // This bus layout can't be represented as a VST3 speaker arrangement
            return false;
        }

        auto& inputArrangements  = *inArrangements;
        auto& outputArrangements = *outArrangements;

        // Some plug-ins will crash if you pass a nullptr to setBusArrangements!
        Vst::SpeakerArrangement nullArrangement = {};
        auto* inputArrangementData  = inputArrangements .empty() ? &nullArrangement : inputArrangements .data();
        auto* outputArrangementData = outputArrangements.empty() ? &nullArrangement : outputArrangements.data();

        if (processor->setBusArrangements (inputArrangementData,  static_cast<int32> (inputArrangements .size()),
                                           outputArrangementData, static_cast<int32> (outputArrangements.size())) != kResultTrue)
            return false;

        // check if the layout matches the request
        const auto inArrActual  = getActualArrangements (true);
        const auto outArrActual = getActualArrangements (false);

        return (inArrActual == inputArrangements && outArrActual == outputArrangements);
    }

    bool canApplyBusesLayout (const BusesLayout& layouts) const override
    {
        // someone tried to change the layout while the AudioProcessor is running
        // call releaseResources first!
        jassert (! isActive);

        const auto previousLayout = getBusesLayout();
        const auto result = syncBusLayouts (layouts);
        syncBusLayouts (previousLayout);
        return result;
    }

    //==============================================================================
    void updateTrackProperties (const TrackProperties& properties) override
    {
        if (trackInfoListener != nullptr)
        {
            auto l = addVSTComSmartPtrOwner (new TrackPropertiesAttributeList (properties));
            trackInfoListener->setChannelContextInfos (l.get());
        }
    }

    struct TrackPropertiesAttributeList final : public Vst::IAttributeList
    {
        TrackPropertiesAttributeList (const TrackProperties& properties) : props (properties) {}
        virtual ~TrackPropertiesAttributeList() {}

        JUCE_DECLARE_VST3_COM_REF_METHODS

        tresult PLUGIN_API queryInterface (const TUID queryIid, void** obj) override
        {
            return testForMultiple (*this,
                                    queryIid,
                                    UniqueBase<Vst::IAttributeList>{},
                                    SharedBase<FUnknown, Vst::IAttributeList>{}).extract (obj);
        }

        tresult PLUGIN_API setInt    (AttrID, Steinberg::int64) override                 { return kOutOfMemory; }
        tresult PLUGIN_API setFloat  (AttrID, double) override                           { return kOutOfMemory; }
        tresult PLUGIN_API setString (AttrID, const Vst::TChar*) override                { return kOutOfMemory; }
        tresult PLUGIN_API setBinary (AttrID, const void*, Steinberg::uint32) override   { return kOutOfMemory; }
        tresult PLUGIN_API getFloat  (AttrID, double&) override                          { return kResultFalse; }
        tresult PLUGIN_API getBinary (AttrID, const void*&, Steinberg::uint32&) override { return kResultFalse; }

        tresult PLUGIN_API getString (AttrID id, Vst::TChar* string, Steinberg::uint32 size) override
        {
            if (! std::strcmp (id, Vst::ChannelContext::kChannelNameKey))
            {
                Steinberg::String str (props.name.toRawUTF8());
                str.copyTo (string, 0, (Steinberg::int32) jmin (size, (Steinberg::uint32) std::numeric_limits<Steinberg::int32>::max()));

                return kResultTrue;
            }

            return kResultFalse;
        }

        tresult PLUGIN_API getInt (AttrID id, Steinberg::int64& value) override
        {
            if      (! std::strcmp (Vst::ChannelContext::kChannelNameLengthKey, id)) value = props.name.length();
            else if (! std::strcmp (Vst::ChannelContext::kChannelColorKey,      id)) value = static_cast<Steinberg::int64> (props.colour.getARGB());
            else return kResultFalse;

            return kResultTrue;
        }

        Atomic<int> refCount;
        TrackProperties props;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TrackPropertiesAttributeList)
    };

    //==============================================================================
    String getChannelName (int channelIndex, Direction direction) const
    {
        auto numBuses = getNumSingleDirectionBusesFor (holder->component.get(), MediaKind::audio, direction);

        int numCountedChannels = 0;

        for (int i = 0; i < numBuses; ++i)
        {
            auto busInfo = getBusInfo (MediaKind::audio, direction, i);

            numCountedChannels += busInfo.channelCount;

            if (channelIndex < numCountedChannels)
                return toString (busInfo.name);
        }

        return {};
    }

    const String getInputChannelName  (int channelIndex) const override   { return getChannelName (channelIndex, Direction::input); }
    const String getOutputChannelName (int channelIndex) const override   { return getChannelName (channelIndex, Direction::output); }

    bool isInputChannelStereoPair (int channelIndex) const override
    {
        int busIdx;
        return getOffsetInBusBufferForAbsoluteChannelIndex (true, channelIndex, busIdx) >= 0
                 && getBusInfo (MediaKind::audio, Direction::input, busIdx).channelCount == 2;
    }

    bool isOutputChannelStereoPair (int channelIndex) const override
    {
        int busIdx;
        return getOffsetInBusBufferForAbsoluteChannelIndex (false, channelIndex, busIdx) >= 0
                 && getBusInfo (MediaKind::audio, Direction::output, busIdx).channelCount == 2;
    }

    bool acceptsMidi() const override    { return hasMidiInput; }
    bool producesMidi() const override   { return hasMidiOutput; }

    //==============================================================================
    AudioProcessorParameter* getBypassParameter() const override         { return bypassParam; }

    //==============================================================================
    /** May return a negative value as a means of informing us that the plugin has "infinite tail," or 0 for "no tail." */
    double getTailLengthSeconds() const override
    {
        if (processor != nullptr)
        {
            auto sampleRate = getSampleRate();

            if (sampleRate > 0.0)
            {
                auto tailSamples = processor->getTailSamples();

                if (tailSamples == Vst::kInfiniteTail)
                    return std::numeric_limits<double>::infinity();

                return jlimit (0, 0x7fffffff, (int) processor->getTailSamples()) / sampleRate;
            }
        }

        return 0.0;
    }

    //==============================================================================
    AudioProcessorEditor* createEditor() override
    {
        if (auto view = becomeVSTComSmartPtrOwner (tryCreatingView()))
            return new VST3PluginWindow (this, view);

        return nullptr;
    }

    bool hasEditor() const override
    {
        // (if possible, avoid creating a second instance of the editor, because that crashes some plugins)
        if (getActiveEditor() != nullptr)
            return true;

        auto view = becomeVSTComSmartPtrOwner (tryCreatingView());
        return view != nullptr;
    }

    //==============================================================================
    int getNumPrograms() override                        { return programNames.size(); }
    const String getProgramName (int index) override     { return index >= 0 ? programNames[index] : String(); }
    void changeProgramName (int, const String&) override {}

    int getCurrentProgram() override
    {
        if (programNames.size() > 0 && editController != nullptr)
            if (auto* param = getParameterForID (programParameterID))
                return jmax (0, roundToInt (param->getValue() * (float) (programNames.size() - 1)));

        return 0;
    }

    void setCurrentProgram (int program) override
    {
        if (programNames.size() > 0 && editController != nullptr)
        {
            auto value = static_cast<Vst::ParamValue> (program) / static_cast<Vst::ParamValue> (jmax (1, programNames.size() - 1));

            if (auto* param = getParameterForID (programParameterID))
                param->setValueNotifyingHost ((float) value);
        }
    }

    //==============================================================================
    void reset() override
    {
        const SpinLock::ScopedLockType lock (processMutex);

        if (holder->component != nullptr && processor != nullptr)
        {
            processor->setProcessing (false);
            holder->component->setActive (false);

            holder->component->setActive (true);
            processor->setProcessing (true);
        }
    }

    //==============================================================================
    void getStateInformation (MemoryBlock& destData) override
    {
        // The VST3 plugin format requires that get/set state calls are made
        // from the message thread.
        // We'll lock the message manager here as a safety precaution, but some
        // plugins may still misbehave!

        JUCE_ASSERT_MESSAGE_THREAD
        MessageManagerLock lock;

        parameterDispatcher.flush();

        XmlElement state ("VST3PluginState");

        appendStateFrom (state, holder->component, "IComponent");
        appendStateFrom (state, editController, "IEditController");

        AudioProcessor::copyXmlToBinary (state, destData);
    }

    void setStateInformation (const void* data, int sizeInBytes) override
    {
        // The VST3 plugin format requires that get/set state calls are made
        // from the message thread.
        // We'll lock the message manager here as a safety precaution, but some
        // plugins may still misbehave!

        JUCE_ASSERT_MESSAGE_THREAD
        MessageManagerLock lock;

        parameterDispatcher.flush();

        if (auto head = AudioProcessor::getXmlFromBinary (data, sizeInBytes))
        {
            auto componentStream (createMemoryStreamForState (*head, "IComponent"));

            if (componentStream != nullptr && holder->component != nullptr)
                holder->component->setState (componentStream.get());

            if (editController != nullptr)
            {
                if (componentStream != nullptr)
                {
                    Steinberg::int64 result;
                    componentStream->seek (0, IBStream::kIBSeekSet, &result);
                    setComponentStateAndResetParameters (*componentStream);
                }

                auto controllerStream (createMemoryStreamForState (*head, "IEditController"));

                if (controllerStream != nullptr)
                    editController->setState (controllerStream.get());
            }
        }
    }

    void setComponentStateAndResetParameters (Steinberg::MemoryStream& stream)
    {
        jassert (editController != nullptr);

        warnOnFailureIfImplemented (editController->setComponentState (&stream));
        resetParameters();
    }

    void resetParameters()
    {
        for (auto* parameter : getParameters())
        {
            auto* vst3Param = static_cast<VST3Parameter*> (parameter);
            const auto value = (float) editController->getParamNormalized (vst3Param->getParamID());
            vst3Param->setValueWithoutUpdatingProcessor (value);
        }
    }

    MemoryBlock getStateForPresetFile() const
    {
        auto memoryStream = becomeVSTComSmartPtrOwner (new Steinberg::MemoryStream());

        if (memoryStream == nullptr || holder->component == nullptr)
            return {};

        const auto saved = Steinberg::Vst::PresetFile::savePreset (memoryStream.get(),
                                                                   holder->cidOfComponent,
                                                                   holder->component.get(),
                                                                   editController.get());

        if (saved)
            return { memoryStream->getData(), static_cast<size_t> (memoryStream->getSize()) };

        return {};
    }

    bool setStateFromPresetFile (const MemoryBlock& rawData) const
    {
        auto rawDataCopy = rawData;
        auto memoryStream = becomeVSTComSmartPtrOwner (new Steinberg::MemoryStream (rawDataCopy.getData(), (int) rawDataCopy.getSize()));

        if (memoryStream == nullptr || holder->component == nullptr)
            return false;

        return Steinberg::Vst::PresetFile::loadPreset (memoryStream.get(), holder->cidOfComponent,
                                                       holder->component.get(), editController.get(), nullptr);
    }

    //==============================================================================
    void fillInPluginDescription (PluginDescription& description) const override
    {
        holder->fillInPluginDescription (description);
    }

    /** @note Not applicable to VST3 */
    void getCurrentProgramStateInformation (MemoryBlock& destData) override
    {
        destData.setSize (0, true);
    }

    /** @note Not applicable to VST3 */
    void setCurrentProgramStateInformation ([[maybe_unused]] const void* data,
                                            [[maybe_unused]] int sizeInBytes) override
    {
    }

private:
    void deactivate()
    {
        if (! isActive)
            return;

        isActive = false;

        if (processor != nullptr)
            warnOnFailureIfImplemented (processor->setProcessing (false));

        if (holder->component != nullptr)
            warnOnFailure (holder->component->setActive (false));

        setStateForAllMidiBuses (false);
    }

    //==============================================================================
   #if JUCE_LINUX || JUCE_BSD
    SharedResourcePointer<RunLoop> runLoop;
   #endif

    std::unique_ptr<VST3ComponentHolder> holder;

    friend VST3HostContext;

    // Information objects:
    String company;
    std::unique_ptr<PClassInfo> info;
    std::unique_ptr<PClassInfo2> info2;
    std::unique_ptr<PClassInfoW> infoW;

    // Rudimentary interfaces:
    VSTComSmartPtr<Vst::IEditController> editController;
    VSTComSmartPtr<Vst::IEditController2> editController2;
    VSTComSmartPtr<Vst::IMidiMapping> midiMapping;
    VSTComSmartPtr<Vst::IAudioProcessor> processor;
    VSTComSmartPtr<Vst::IComponentHandler> componentHandler;
    VSTComSmartPtr<Vst::IComponentHandler2> componentHandler2;
    VSTComSmartPtr<Vst::IUnitInfo> unitInfo;
    VSTComSmartPtr<Vst::IUnitData> unitData;
    VSTComSmartPtr<Vst::IProgramListData> programListData;
    VSTComSmartPtr<Vst::IConnectionPoint> componentConnection, editControllerConnection;
    VSTComSmartPtr<Vst::ChannelContext::IInfoListener> trackInfoListener;

    /** The number of IO buses MUST match that of the plugin,
        even if there aren't enough channels to process,
        as very poorly specified by the Steinberg SDK
    */
    HostBufferMapper inputBusMap, outputBusMap;

    StringArray programNames;
    Vst::ParamID programParameterID = (Vst::ParamID) -1;

    std::map<Vst::ParamID, VST3Parameter*> idToParamMap;
    EditControllerParameterDispatcher parameterDispatcher;
    StoredMidiMapping storedMidiMapping;

    /*  The plugin may request a restart during playback, which may in turn
        attempt to call functions such as setProcessing and setActive. It is an
        error to call these functions simultaneously with
        IAudioProcessor::process, so we use this mutex to ensure that this
        scenario is impossible.
    */
    SpinLock processMutex;

    //==============================================================================
    template <typename Type>
    static void appendStateFrom (XmlElement& head, VSTComSmartPtr<Type>& object, const String& identifier)
    {
        if (object != nullptr)
        {
            Steinberg::MemoryStream stream;

            const auto result = object->getState (&stream);

            if (result == kResultTrue)
            {
                MemoryBlock info (stream.getData(), (size_t) stream.getSize());
                head.createNewChildElement (identifier)->addTextElement (info.toBase64Encoding());
            }
        }
    }

    static VSTComSmartPtr<Steinberg::MemoryStream> createMemoryStreamForState (XmlElement& head, StringRef identifier)
    {
        if (auto* state = head.getChildByName (identifier))
        {
            MemoryBlock mem;

            if (mem.fromBase64Encoding (state->getAllSubText()))
            {
                auto stream = becomeVSTComSmartPtrOwner (new Steinberg::MemoryStream());
                stream->setSize ((TSize) mem.getSize());
                mem.copyTo (stream->getData(), 0, mem.getSize());
                return stream;
            }
        }

        return {};
    }

    CachedParamValues cachedParamValues;
    VSTComSmartPtr<ParameterChanges> inputParameterChanges  = addVSTComSmartPtrOwner (new ParameterChanges);
    VSTComSmartPtr<ParameterChanges> outputParameterChanges = addVSTComSmartPtrOwner (new ParameterChanges);
    VSTComSmartPtr<MidiEventList> midiInputs  = addVSTComSmartPtrOwner (new MidiEventList);
    VSTComSmartPtr<MidiEventList> midiOutputs = addVSTComSmartPtrOwner (new MidiEventList);
    Vst::ProcessContext timingInfo; //< Only use this in processBlock()!
    bool isControllerInitialised = false, isActive = false, lastProcessBlockCallWasBypass = false;
    const bool hasMidiInput  = getNumSingleDirectionBusesFor (holder->component.get(), MediaKind::event, Direction::input) > 0,
               hasMidiOutput = getNumSingleDirectionBusesFor (holder->component.get(), MediaKind::event, Direction::output) > 0;
    VST3Parameter* bypassParam = nullptr;

    //==============================================================================
    /** Some plugins need to be "connected" to intercommunicate between their implemented classes */
    void interconnectComponentAndController()
    {
        componentConnection.loadFrom (holder->component.get());
        editControllerConnection.loadFrom (editController.get());

        if (componentConnection != nullptr && editControllerConnection != nullptr)
        {
            warnOnFailure (componentConnection->connect (editControllerConnection.get()));
            warnOnFailure (editControllerConnection->connect (componentConnection.get()));
        }
    }

    void initialiseParameterList()
    {
        AudioProcessorParameterGroup newParameterTree;

        // We're going to add parameter groups to the tree recursively in the same order as the
        // first parameters contained within them.
        std::map<Vst::UnitID, Vst::UnitInfo> infoMap;
        std::map<Vst::UnitID, AudioProcessorParameterGroup*> groupMap;
        groupMap[Vst::kRootUnitId] = &newParameterTree;

        if (unitInfo != nullptr)
        {
            const auto numUnits = unitInfo->getUnitCount();

            for (int i = 1; i < numUnits; ++i)
            {
                Vst::UnitInfo ui{};
                unitInfo->getUnitInfo (i, ui);
                infoMap[ui.id] = std::move (ui);
            }
        }

        {
            auto allIds = getAllParamIDs (*editController);
            inputParameterChanges ->initialise (allIds);
            outputParameterChanges->initialise (allIds);
            cachedParamValues = CachedParamValues { std::move (allIds) };
        }

        for (int i = 0; i < editController->getParameterCount(); ++i)
        {
            auto paramInfo = getParameterInfoForIndex (i);
            auto* param = new VST3Parameter (*this,
                                             i,
                                             paramInfo.id,
                                             (paramInfo.flags & Vst::ParameterInfo::kCanAutomate) != 0);

            if ((paramInfo.flags & Vst::ParameterInfo::kIsBypass) != 0)
                bypassParam = param;

            std::function<AudioProcessorParameterGroup* (Vst::UnitID)> findOrCreateGroup;
            findOrCreateGroup = [&groupMap, &infoMap, &findOrCreateGroup] (Vst::UnitID groupID)
            {
                auto existingGroup = groupMap.find (groupID);

                if (existingGroup != groupMap.end())
                    return existingGroup->second;

                auto groupInfo = infoMap.find (groupID);

                if (groupInfo == infoMap.end())
                    return groupMap[Vst::kRootUnitId];

                auto* group = new AudioProcessorParameterGroup (String (groupInfo->first),
                                                                toString (groupInfo->second.name),
                                                                {});
                groupMap[groupInfo->first] = group;

                auto* parentGroup = findOrCreateGroup (groupInfo->second.parentUnitId);
                parentGroup->addChild (std::unique_ptr<AudioProcessorParameterGroup> (group));

                return group;
            };

            auto* group = findOrCreateGroup (paramInfo.unitId);
            group->addChild (std::unique_ptr<AudioProcessorParameter> (param));
        }

        setHostedParameterTree (std::move (newParameterTree));

        idToParamMap = [this]
        {
            std::map<Vst::ParamID, VST3Parameter*> result;

            for (auto* parameter : getParameters())
            {
                auto* vst3Param = static_cast<VST3Parameter*> (parameter);
                result.emplace (vst3Param->getParamID(), vst3Param);
            }

            return result;
        }();
    }

    void synchroniseStates()
    {
        Steinberg::MemoryStream stream;

        if (holder->component->getState (&stream) == kResultTrue)
            if (stream.seek (0, Steinberg::IBStream::kIBSeekSet, nullptr) == kResultTrue)
                setComponentStateAndResetParameters (stream);
    }

    void grabInformationObjects()
    {
        processor.loadFrom (holder->component.get());
        unitInfo.loadFrom (holder->component.get());
        programListData.loadFrom (holder->component.get());
        unitData.loadFrom (holder->component.get());
        editController2.loadFrom (holder->component.get());
        midiMapping.loadFrom (holder->component.get());
        componentHandler.loadFrom (holder->component.get());
        componentHandler2.loadFrom (holder->component.get());
        trackInfoListener.loadFrom (holder->component.get());

        if (processor == nullptr)           processor.loadFrom (editController.get());
        if (unitInfo == nullptr)            unitInfo.loadFrom (editController.get());
        if (programListData == nullptr)     programListData.loadFrom (editController.get());
        if (unitData == nullptr)            unitData.loadFrom (editController.get());
        if (editController2 == nullptr)     editController2.loadFrom (editController.get());
        if (midiMapping == nullptr)         midiMapping.loadFrom (editController.get());
        if (componentHandler == nullptr)    componentHandler.loadFrom (editController.get());
        if (componentHandler2 == nullptr)   componentHandler2.loadFrom (editController.get());
        if (trackInfoListener == nullptr)   trackInfoListener.loadFrom (editController.get());
    }

    void setStateForAllMidiBuses (bool newState)
    {
        setStateForAllEventBuses (holder->component.get(), newState, Direction::input);
        setStateForAllEventBuses (holder->component.get(), newState, Direction::output);
    }

    std::vector<ChannelMapping> createChannelMappings (bool isInput) const
    {
        std::vector<ChannelMapping> result;
        result.reserve ((size_t) getBusCount (isInput));

        for (auto i = 0; i < getBusCount (isInput); ++i)
            result.emplace_back (*getBus (isInput, i));

        return result;
    }

    void setupIO()
    {
        setStateForAllMidiBuses (true);

        Vst::ProcessSetup setup;
        setup.symbolicSampleSize   = Vst::kSample32;
        setup.maxSamplesPerBlock   = 1024;
        setup.sampleRate           = 44100.0;
        setup.processMode          = Vst::kRealtime;

        warnOnFailure (processor->setupProcessing (setup));

        inputBusMap .prepare (createChannelMappings (true));
        outputBusMap.prepare (createChannelMappings (false));
        setRateAndBufferSizeDetails (setup.sampleRate, (int) setup.maxSamplesPerBlock);
    }

    static AudioProcessor::BusesProperties getBusProperties (VSTComSmartPtr<Vst::IComponent>& component)
    {
        AudioProcessor::BusesProperties busProperties;
        VSTComSmartPtr<Vst::IAudioProcessor> processor;
        processor.loadFrom (component.get());

        for (const auto isInput : { true, false })
        {
            const Vst::BusDirection dir = (isInput ? Vst::kInput : Vst::kOutput);
            const int numBuses = component->getBusCount (Vst::kAudio, dir);

            for (int i = 0; i < numBuses; ++i)
            {
                Vst::BusInfo info;

                if (component->getBusInfo (Vst::kAudio, dir, (Steinberg::int32) i, info) != kResultOk)
                    continue;

                AudioChannelSet layout = (info.channelCount == 0 ? AudioChannelSet::disabled()
                                                                 : AudioChannelSet::discreteChannels (info.channelCount));

                Vst::SpeakerArrangement arr;
                if (processor != nullptr && processor->getBusArrangement (dir, i, arr) == kResultOk)
                    if (const auto set = getChannelSetForSpeakerArrangement (arr))
                        layout = *set;

                busProperties.addBus (isInput, toString (info.name), layout,
                                      (info.flags & Vst::BusInfo::kDefaultActive) != 0);
            }
        }

        return busProperties;
    }

    //==============================================================================
    Vst::BusInfo getBusInfo (MediaKind kind, Direction direction, int index = 0) const
    {
        Vst::BusInfo busInfo;
        busInfo.mediaType = toVstType (kind);
        busInfo.direction = toVstType (direction);
        busInfo.channelCount = 0;

        holder->component->getBusInfo (busInfo.mediaType, busInfo.direction,
                                       (Steinberg::int32) index, busInfo);
        return busInfo;
    }

    //==============================================================================
    void updateBypass (bool processBlockBypassedCalled)
    {
        // to remain backward compatible, the logic needs to be the following:
        // - if processBlockBypassed was called then definitely bypass the VST3
        // - if processBlock was called then only un-bypass the VST3 if the previous
        //   call was processBlockBypassed, otherwise do nothing
        if (processBlockBypassedCalled)
        {
            if (bypassParam != nullptr && (approximatelyEqual (bypassParam->getValue(), 0.0f) || ! lastProcessBlockCallWasBypass))
                bypassParam->setValue (1.0f);
        }
        else
        {
            if (lastProcessBlockCallWasBypass && bypassParam != nullptr)
                bypassParam->setValue (0.0f);

        }

        lastProcessBlockCallWasBypass = processBlockBypassedCalled;
    }

    //==============================================================================
    /** @note An IPlugView, when first created, should start with a ref-count of 1! */
    IPlugView* tryCreatingView() const
    {
        JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED

        IPlugView* v = editController->createView (Vst::ViewType::kEditor);

        if (v == nullptr) v = editController->createView (nullptr);
        if (v == nullptr) editController->queryInterface (IPlugView::iid, (void**) &v);

        return v;
    }

    //==============================================================================
    template <typename FloatType>
    void associateWith (Vst::ProcessData& destination, AudioBuffer<FloatType>& buffer)
    {
        destination.inputs  = inputBusMap .getVst3LayoutForJuceBuffer (buffer);
        destination.outputs = outputBusMap.getVst3LayoutForJuceBuffer (buffer);
    }

    void associateWith (Vst::ProcessData& destination, MidiBuffer& midiBuffer)
    {
        midiInputs->clear();
        midiOutputs->clear();

        if (acceptsMidi())
        {
            MidiEventList::hostToPluginEventList (*midiInputs,
                                                  midiBuffer,
                                                  storedMidiMapping,
                                                  [this] (const auto controlID, const auto paramValue)
                                                  {
                                                      if (auto* param = this->getParameterForID (controlID))
                                                          param->setValueNotifyingHost ((float) paramValue);
                                                  });
        }

        destination.inputEvents = midiInputs.get();
        destination.outputEvents = midiOutputs.get();
    }

    void updateTimingInformation (Vst::ProcessData& destination, double processSampleRate)
    {
        toProcessContext (timingInfo, getPlayHead(), processSampleRate);
        destination.processContext = &timingInfo;
    }

    Vst::ParameterInfo getParameterInfoForIndex (Steinberg::int32 index) const
    {
        Vst::ParameterInfo paramInfo{};

        if (editController != nullptr)
            editController->getParameterInfo ((int32) index, paramInfo);

        return paramInfo;
    }

    Vst::ProgramListInfo getProgramListInfo (int index) const
    {
        Vst::ProgramListInfo paramInfo{};

        if (unitInfo != nullptr)
            unitInfo->getProgramListInfo (index, paramInfo);

        return paramInfo;
    }

    void syncProgramNames()
    {
        programNames.clear();

        if (processor == nullptr || editController == nullptr)
            return;

        Vst::UnitID programUnitID;
        Vst::ParameterInfo paramInfo{};

        {
            int idx, num = editController->getParameterCount();

            for (idx = 0; idx < num; ++idx)
                if (editController->getParameterInfo (idx, paramInfo) == kResultOk
                     && (paramInfo.flags & Steinberg::Vst::ParameterInfo::kIsProgramChange) != 0)
                    break;

            if (idx >= num)
                return;

            programParameterID = paramInfo.id;
            programUnitID = paramInfo.unitId;
        }

        if (unitInfo != nullptr)
        {
            Vst::UnitInfo uInfo{};
            const int unitCount = unitInfo->getUnitCount();

            for (int idx = 0; idx < unitCount; ++idx)
            {
                if (unitInfo->getUnitInfo (idx, uInfo) == kResultOk
                      && uInfo.id == programUnitID)
                {
                    const int programListCount = unitInfo->getProgramListCount();

                    for (int j = 0; j < programListCount; ++j)
                    {
                        Vst::ProgramListInfo programListInfo{};

                        if (unitInfo->getProgramListInfo (j, programListInfo) == kResultOk
                              && programListInfo.id == uInfo.programListId)
                        {
                            Vst::String128 name;

                            for (int k = 0; k < programListInfo.programCount; ++k)
                                if (unitInfo->getProgramName (programListInfo.id, k, name) == kResultOk)
                                    programNames.add (toString (name));

                            return;
                        }
                    }

                    break;
                }
            }
        }

        if (editController != nullptr && paramInfo.stepCount > 0)
        {
            auto numPrograms = paramInfo.stepCount + 1;

            for (int i = 0; i < numPrograms; ++i)
            {
                auto valueNormalized = static_cast<Vst::ParamValue> (i) / static_cast<Vst::ParamValue> (paramInfo.stepCount);

                Vst::String128 programName;
                if (editController->getParamStringByValue (paramInfo.id, valueNormalized, programName) == kResultOk)
                    programNames.add (toString (programName));
            }
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VST3PluginInstance)
};

JUCE_END_IGNORE_WARNINGS_MSVC

//==============================================================================
tresult VST3HostContext::beginEdit (Vst::ParamID paramID)
{
    if (plugin == nullptr)
        return kResultTrue;

    if (auto* param = plugin->getParameterForID (paramID))
    {
        param->beginChangeGesture();
        return kResultTrue;
    }

    return kResultFalse;
}

tresult VST3HostContext::performEdit (Vst::ParamID paramID, Vst::ParamValue valueNormalised)
{
    if (plugin == nullptr)
        return kResultTrue;

    if (auto* param = plugin->getParameterForID (paramID))
    {
        param->setValueNotifyingHost ((float) valueNormalised);

        // did the plug-in already update the parameter internally
        if (! approximatelyEqual (plugin->editController->getParamNormalized (paramID), valueNormalised))
            return plugin->editController->setParamNormalized (paramID, valueNormalised);

        return kResultTrue;
    }

    return kResultFalse;
}

tresult VST3HostContext::endEdit (Vst::ParamID paramID)
{
    if (plugin == nullptr)
        return kResultTrue;

    if (auto* param = plugin->getParameterForID (paramID))
    {
        param->endChangeGesture();
        return kResultTrue;
    }

    return kResultFalse;
}

tresult VST3HostContext::restartComponent (Steinberg::int32 flags)
{
    // If you hit this, the plugin has requested a restart from a thread other than
    // the UI thread. JUCE should be able to cope, but you should consider filing a bug
    // report against the plugin.
    JUCE_ASSERT_MESSAGE_THREAD

    componentRestarter.restart (flags);
    return kResultTrue;
}

tresult PLUGIN_API VST3HostContext::setDirty (TBool needsSave)
{
    if (needsSave)
        plugin->updateHostDisplay (AudioPluginInstance::ChangeDetails{}.withNonParameterStateChanged (true));

    return kResultOk;
}

void VST3HostContext::restartComponentOnMessageThread (int32 flags)
{
    if (plugin == nullptr)
    {
        jassertfalse;
        return;
    }

    if (hasFlag (flags, Vst::kReloadComponent))
        plugin->reset();

    if (hasFlag (flags, Vst::kIoChanged))
    {
        auto sampleRate = plugin->getSampleRate();
        auto blockSize  = plugin->getBlockSize();

        // Have to deactivate here, otherwise prepareToPlay might not pick up the new bus layouts
        plugin->releaseResources();
        plugin->prepareToPlay (sampleRate >= 8000 ? sampleRate : 44100.0,
        blockSize > 0 ? blockSize : 1024);
    }

    if (hasFlag (flags, Vst::kLatencyChanged))
        if (plugin->processor != nullptr)
            plugin->setLatencySamples (jmax (0, (int) plugin->processor->getLatencySamples()));

    if (hasFlag (flags, Vst::kMidiCCAssignmentChanged))
        plugin->updateMidiMappings();

    if (hasFlag (flags, Vst::kParamValuesChanged))
        plugin->resetParameters();

    plugin->updateHostDisplay (AudioProcessorListener::ChangeDetails().withProgramChanged (true)
                                                                      .withParameterInfoChanged (true));
}

//==============================================================================
tresult VST3HostContext::ContextMenu::popup (Steinberg::UCoord x, Steinberg::UCoord y)
{
    Array<const Item*> subItemStack;
    OwnedArray<PopupMenu> menuStack;
    PopupMenu* topLevelMenu = menuStack.add (new PopupMenu());

    for (int i = 0; i < items.size(); ++i)
    {
        auto& item = items.getReference (i).item;
        auto* menuToUse = menuStack.getLast();

        if (hasFlag (item.flags, Item::kIsGroupStart & ~Item::kIsDisabled))
        {
            subItemStack.add (&item);
            menuStack.add (new PopupMenu());
        }
        else if (hasFlag (item.flags, Item::kIsGroupEnd))
        {
            if (auto* subItem = subItemStack.getLast())
            {
                if (auto* m = menuStack [menuStack.size() - 2])
                    m->addSubMenu (toString (subItem->name), *menuToUse,
                                   ! hasFlag (subItem->flags, Item::kIsDisabled),
                                   nullptr,
                                   hasFlag (subItem->flags, Item::kIsChecked));

                menuStack.removeLast (1);
                subItemStack.removeLast (1);
            }
        }
        else if (hasFlag (item.flags, Item::kIsSeparator))
        {
            menuToUse->addSeparator();
        }
        else
        {
            menuToUse->addItem (item.tag != 0 ? (int) item.tag : (int) zeroTagReplacement,
                                toString (item.name),
                                ! hasFlag (item.flags, Item::kIsDisabled),
                                hasFlag (item.flags, Item::kIsChecked));
        }
    }

    PopupMenu::Options options;

    if (auto* ed = owner.getActiveEditor())
    {
       #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE
        if (auto* peer = ed->getPeer())
        {
            auto scale = peer->getPlatformScaleFactor();

            x = roundToInt (x / scale);
            y = roundToInt (y / scale);
        }
       #endif

        options = options.withTargetScreenArea (ed->getScreenBounds().translated ((int) x, (int) y).withSize (1, 1));
    }

   #if JUCE_MODAL_LOOPS_PERMITTED
    // Unfortunately, Steinberg's docs explicitly say this should be modal..
    handleResult (topLevelMenu->showMenu (options));
   #else
    topLevelMenu->showMenuAsync (options, ModalCallbackFunction::create (menuFinished, addVSTComSmartPtrOwner (this)));
   #endif

    return kResultOk;
}

//==============================================================================
tresult VST3HostContext::notifyProgramListChange (Vst::ProgramListID, Steinberg::int32)
{
    if (plugin != nullptr)
        plugin->syncProgramNames();

    return kResultTrue;
}

//==============================================================================
//==============================================================================
VST3PluginFormat::VST3PluginFormat()  = default;
VST3PluginFormat::~VST3PluginFormat() = default;

bool VST3PluginFormat::setStateFromVSTPresetFile (AudioPluginInstance* api, const MemoryBlock& rawData)
{
    if (auto vst3 = dynamic_cast<VST3PluginInstance*> (api))
        return vst3->setStateFromPresetFile (rawData);

    return false;
}

void VST3PluginFormat::findAllTypesForFile (OwnedArray<PluginDescription>& results, const String& fileOrIdentifier)
{
    if (! fileMightContainThisPluginType (fileOrIdentifier))
        return;

    if (const auto fast = DescriptionLister::findDescriptionsFast (File (fileOrIdentifier)); ! fast.empty())
    {
        for (const auto& d : fast)
            results.add (new PluginDescription (d));

        return;
    }

    for (const auto& file : getLibraryPaths (fileOrIdentifier))
    {
        /**
            Since there is no apparent indication if a VST3 plugin is a shell or not,
            we're stuck iterating through a VST3's factory, creating a description
            for every housed plugin.
        */

        auto pluginFactory = addVSTComSmartPtrOwner (DLLHandleCache::getInstance()->findOrCreateHandle (file).getPluginFactory());

        if (pluginFactory == nullptr)
            continue;

        auto host = addVSTComSmartPtrOwner (new VST3HostContext());

        for (const auto& d : DescriptionLister::findDescriptionsSlow (*host, *pluginFactory, File (file)))
            results.add (new PluginDescription (d));
    }
}

void VST3PluginFormat::createARAFactoryAsync (const PluginDescription& description, ARAFactoryCreationCallback callback)
{
    if (! description.hasARAExtension)
    {
        jassertfalse;
        callback ({ {}, "The provided plugin does not support ARA features" });
    }

    File file (description.fileOrIdentifier);
    auto pluginFactory = addVSTComSmartPtrOwner (DLLHandleCache::getInstance()->findOrCreateHandle (file.getFullPathName()).getPluginFactory());
    const auto* pluginName = description.name.toRawUTF8();

    callback ({ ARAFactoryWrapper { ::juce::getARAFactory (pluginFactory.get(), pluginName) }, {} });
}

static std::unique_ptr<AudioPluginInstance> createVST3Instance (VST3PluginFormat& format,
                                                                const PluginDescription& description,
                                                                const File& file)
{
    if (! format.fileMightContainThisPluginType (description.fileOrIdentifier))
        return nullptr;

    struct ScopedWorkingDirectory
    {
        ~ScopedWorkingDirectory() { previousWorkingDirectory.setAsCurrentWorkingDirectory(); }
        File previousWorkingDirectory = File::getCurrentWorkingDirectory();
    };

    const ScopedWorkingDirectory scope;
    file.getParentDirectory().setAsCurrentWorkingDirectory();

    const VST3ModuleHandle::Ptr module { VST3ModuleHandle::findOrCreateModule (file, description) };

    if (module == nullptr)
        return nullptr;

    auto holder = std::make_unique<VST3ComponentHolder> (module);

    if (! holder->initialise())
        return nullptr;

    auto instance = std::make_unique<VST3PluginInstance> (std::move (holder));

    if (! instance->initialise())
        return nullptr;

    return instance;
}

StringArray VST3PluginFormat::getLibraryPaths (const String& fileOrIdentifier)
{
   #if JUCE_WINDOWS
    if (! File (fileOrIdentifier).existsAsFile())
    {
        StringArray files;
        recursiveFileSearch (files, fileOrIdentifier, true);
        return files;
    }
   #endif

    return { fileOrIdentifier };
}

void VST3PluginFormat::createPluginInstance (const PluginDescription& description,
                                             double, int, PluginCreationCallback callback)
{
    for (const auto& file : getLibraryPaths (description.fileOrIdentifier))
    {
        if (auto result = createVST3Instance (*this, description, file))
        {
            callback (std::move (result), {});
            return;
        }
    }

    callback (nullptr, TRANS ("Unable to load XXX plug-in file").replace ("XXX", "VST-3"));
}

bool VST3PluginFormat::requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const
{
    return false;
}

bool VST3PluginFormat::fileMightContainThisPluginType (const String& fileOrIdentifier)
{
    auto f = File::createFileWithoutCheckingPath (fileOrIdentifier);

    return f.hasFileExtension (".vst3") && f.exists();
}

String VST3PluginFormat::getNameOfPluginFromIdentifier (const String& fileOrIdentifier)
{
    return fileOrIdentifier; //Impossible to tell because every VST3 is a type of shell...
}

bool VST3PluginFormat::pluginNeedsRescanning (const PluginDescription& description)
{
    return File (description.fileOrIdentifier).getLastModificationTime() != description.lastFileModTime;
}

bool VST3PluginFormat::doesPluginStillExist (const PluginDescription& description)
{
    return File (description.fileOrIdentifier).exists();
}

StringArray VST3PluginFormat::searchPathsForPlugins (const FileSearchPath& directoriesToSearch, const bool recursive, bool)
{
    StringArray results;

    for (int i = 0; i < directoriesToSearch.getNumPaths(); ++i)
        recursiveFileSearch (results, directoriesToSearch[i], recursive);

    return results;
}

void VST3PluginFormat::recursiveFileSearch (StringArray& results, const File& directory, const bool recursive)
{
    for (const auto& iter : RangedDirectoryIterator (directory, false, "*", File::findFilesAndDirectories))
    {
        auto f = iter.getFile();
        bool isPlugin = false;

        if (fileMightContainThisPluginType (f.getFullPathName()))
        {
            isPlugin = true;
            results.add (f.getFullPathName());
        }

        if (recursive && (! isPlugin) && f.isDirectory())
            recursiveFileSearch (results, f, true);
    }
}

FileSearchPath VST3PluginFormat::getDefaultLocationsToSearch()
{
   #if JUCE_WINDOWS
    const auto localAppData = File::getSpecialLocation (File::windowsLocalAppData)        .getFullPathName();
    const auto programFiles = File::getSpecialLocation (File::globalApplicationsDirectory).getFullPathName();
    return FileSearchPath (localAppData + "\\Programs\\Common\\VST3;" + programFiles + "\\Common Files\\VST3");
   #elif JUCE_MAC
    return FileSearchPath ("~/Library/Audio/Plug-Ins/VST3;/Library/Audio/Plug-Ins/VST3");
   #else
    return FileSearchPath ("~/.vst3/;/usr/lib/vst3/;/usr/local/lib/vst3/");
   #endif
}

JUCE_END_NO_SANITIZE

} // namespace juce

#endif // JUCE_PLUGINHOST_VST3
