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

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

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-9-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

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

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

#pragma once

#if JUCE_INTERNAL_HAS_VST3

#if (JUCE_LINUX || JUCE_BSD)
 #include <sys/utsname.h>
#endif

#include <juce_audio_processors_headless/format_types/juce_VST3Headers.h>
#include <juce_audio_processors_headless/format_types/juce_VST3Utilities.h>
#include <juce_audio_processors_headless/format_types/juce_VST3Common.h>
#include <juce_audio_processors_headless/format_types/juce_ARACommon.h>
#include <juce_audio_processors_headless/format_types/juce_ARACommonInternal.h>

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 ModuleInfo& info)
{
    std::vector<PluginDescription> result;

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

       #if JUCE_INTERNAL_HAS_ARA
        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;
    }
}

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

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

    //==============================================================================
    tresult PLUGIN_API registerEventHandler (Linux::IEventHandler* handler,
                                             Linux::FileDescriptor fd) override
    {
        return impl->registerEventHandler (handler, fd);
    }

    tresult PLUGIN_API unregisterEventHandler (Linux::IEventHandler* handler) override
    {
        return impl->unregisterEventHandler (handler);
    }

    //==============================================================================
    tresult PLUGIN_API registerTimer (Linux::ITimerHandler* handler, Linux::TimerInterval milliseconds) override
    {
        return impl->registerTimer (handler, milliseconds);
    }

    tresult PLUGIN_API unregisterTimer (Linux::ITimerHandler* handler) override
    {
        return impl->unregisterTimer (handler);
    }

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;
    };

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

        //==============================================================================
        tresult registerEventHandler (Linux::IEventHandler* handler, Linux::FileDescriptor fd)
        {
            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 unregisterEventHandler (Linux::IEventHandler* handler)
        {
            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 registerTimer (Linux::ITimerHandler* handler, Linux::TimerInterval milliseconds)
        {
            if (handler == nullptr || milliseconds <= 0)
                return kInvalidArgument;

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

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

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

            timerCallers.erase (iter);
            return kResultTrue;
        }

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

    SharedResourcePointer<Impl> impl;

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

#else

class RunLoop {};

#endif

//==============================================================================
class VST3PluginInstanceHeadless;

struct VST3HostContextHeadless : public Vst::IComponentHandler,
                                 public Vst::IComponentHandler2,
                                 public Vst::IComponentHandler3,
                                 public Vst::IContextMenuTarget,
                                 public Vst::IHostApplication,
                                 public Vst::IUnitHandler,
                                 public RunLoop,
                                 private ComponentRestarter::Listener
{
    VST3HostContextHeadless()
    {
        appName = File::getSpecialLocation (File::currentApplicationFile).getFileNameWithoutExtension();
    }

    ~VST3HostContextHeadless() 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;
    }

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

    inline tresult PLUGIN_API restartComponent (Steinberg::int32 flags) override;
    inline 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 (VST3PluginInstanceHeadless* instance)
    {
        jassert (plugin == nullptr);
        plugin = instance;
    }

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

    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 iidToCreate, void** obj) override
    {
        *obj = nullptr;

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

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

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

        jassertfalse;
        return kNotImplemented;
    }

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

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

    //==============================================================================
    tresult PLUGIN_API queryInterface (const TUID iidToQuery, void** obj) override
    {
        return testForMultiple (*this,
                                iidToQuery,
                                UniqueBase<Vst::IComponentHandler>{},
                                UniqueBase<Vst::IComponentHandler2>{},
                                UniqueBase<Vst::IComponentHandler3>{},
                                UniqueBase<Vst::IContextMenuTarget>{},
                                UniqueBase<Vst::IHostApplication>{},
                                UniqueBase<Vst::IUnitHandler>{},
                               #if JUCE_LINUX || JUCE_BSD
                                UniqueBase<Linux::IRunLoop>{},
                               #endif
                                SharedBase<FUnknown, Vst::IComponentHandler>{}).extract (obj);
    }

    VST3PluginInstanceHeadless* getPlugin() const
    {
        return plugin;
    }

private:
    //==============================================================================
    VST3PluginInstanceHeadless* plugin = nullptr;
    Atomic<int> refCount { 1 };
    String appName;

    ComponentRestarter componentRestarter { *this };

    inline 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 (VST3HostContextHeadless)
};

//==============================================================================
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 = 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 (VST3HostContextHeadless& 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 IComponent.
        // The match is determined by the two classes having the same name.
        std::unordered_set<String> araMainFactoryClassNames;

       #if JUCE_INTERNAL_HAS_ARA
        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
{
    explicit DLLHandle (const File& fileToOpen)
        : dllFile (fileToOpen)
    {
        open();
    }

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

            using ExitModuleFn = bool (PLUGIN_API*)();

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

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

    //==============================================================================
    VSTComSmartPtr<IPluginFactory> getPluginFactory()
    {
        if (factory == nullptr)
            if (auto* proc = (GetFactoryProc) getFunction (factoryFnName))
                factory = VSTComSmartPtr (proc(), IncrementRef::no);

        // 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;
    VSTComSmartPtr<IPluginFactory> factory;

    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 RefCountedDllHandle final : public ReferenceCountedObject
{
public:
    using Ptr = ReferenceCountedObjectPtr<RefCountedDllHandle>;

    ~RefCountedDllHandle() override
    {
        getHandles().erase (this);
    }

    VSTComSmartPtr<IPluginFactory> getPluginFactory()
    {
        return handle.getPluginFactory();
    }

    File getFile() const
    {
        return handle.getFile();
    }

    static Ptr getHandle (const String& modulePath)
    {
        const auto f = getDLLFileFromBundle (modulePath);

        auto& bundles = getHandles();

        const auto iter = std::find_if (bundles.begin(), bundles.end(), [&] (Ptr x)
        {
            return x->handle.getFile() == f;
        });

        if (iter != bundles.end())
            return *iter;

        return new RefCountedDllHandle { f };
    }

private:
    explicit RefCountedDllHandle (const File& f)
        : handle (f)
    {
        getHandles().insert (this);
    }

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

            if (res != 0)
                return {};

            return unameData.machine;
        }();

        const File file { bundlePath };

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

    static std::set<RefCountedDllHandle*>& getHandles()
    {
        static std::set<RefCountedDllHandle*> bundles;
        return bundles;
    }

    DLLHandle handle;
};

//==============================================================================
//==============================================================================
struct VST3ModuleHandle final
{
public:
    static VST3ModuleHandle create (const File& pluginFile, const PluginDescription& desc)
    {
        VST3ModuleHandle result;
        result.handle = RefCountedDllHandle::getHandle (pluginFile.getFullPathName());

        if (result.handle == nullptr)
            return {};

        auto factory = result.handle->getPluginFactory();

        if (factory == nullptr)
            return {};

        const auto numClasses = factory->countClasses();
        result.classIndex = findClassMatchingDescription (factory, desc);

        if (result.classIndex == numClasses)
            return {};

        return result;
    }

    VSTComSmartPtr<IPluginFactory> getPluginFactory() const
    {
        return handle != nullptr ? handle->getPluginFactory() : VSTComSmartPtr<IPluginFactory>{};
    }

    Steinberg::int32 getClassIndex() const
    {
        return classIndex;
    }

    String getName() const
    {
        auto factory = getPluginFactory();

        if (factory == nullptr)
            return {};

        PClassInfo info{};
        factory->getClassInfo (classIndex, &info);

        return toString (info.name).trim();
    }

    File getFile() const
    {
        return handle != nullptr ? handle->getFile() : File{};
    }

    bool isValid() const
    {
        if (handle == nullptr)
            return false;

        const auto factory = handle->getPluginFactory();

        if (factory == nullptr)
            return false;

        return isPositiveAndBelow (classIndex, factory->countClasses());
    }

private:
    static Steinberg::int32 findClassMatchingDescription (VSTComSmartPtr<IPluginFactory> factory, const PluginDescription& desc)
    {
        const auto numClasses = factory->countClasses();

        for (auto i = decltype (numClasses){}; i < numClasses; ++i)
        {
            PClassInfo info{};
            factory->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() != desc.name)
                continue;

            if (uniqueId != desc.uniqueId && deprecatedUid != desc.deprecatedUid)
                continue;

            return i;
        }

        return numClasses;
    }

    RefCountedDllHandle::Ptr handle;
    Steinberg::int32 classIndex{};

    //==============================================================================
    JUCE_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_INTERNAL_HAS_ARA
    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]] IPluginFactory* pluginFactory,
                                                             [[maybe_unused]] const String& pluginName)
{
    std::shared_ptr<const ARA::ARAFactory> factory;

   #if JUCE_INTERNAL_HAS_ARA
    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)
                                   == 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)
{
    return getARAFactory (module.getPluginFactory().get(), module.getName());
}

//==============================================================================

JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS

//==============================================================================
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& m, VSTComSmartPtr<VST3HostContextHeadless> h)
        : module (m), host (h)
    {
    }

    ~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())
        {
            auto factory = module.getPluginFactory();
            editController.loadFrom (factory.get(), controllerCID);
        }

        if (editController == nullptr)
        {
            // Try finding the IEditController the long way around:
            auto factory = module.getPluginFactory();
            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.isValid() && isComponentInitialised);

        const auto factory = module.getPluginFactory();

        if (factory == nullptr)
        {
            jassertfalse;
            return;
        }

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

        const auto classIdx = module.getClassIndex();

        if (classIdx == factory->countClasses())
        {
            jassertfalse;
            return;
        }

        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);
    }

    //==============================================================================
    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

        const auto factory = module.getPluginFactory();

        if (factory == nullptr)
            return false;

        VSTComSmartPtr<IPluginFactory3> pf3;
        pf3.loadFrom (factory.get());

        if (pf3 != nullptr)
            pf3->setHostContext (host->getFUnknown());

        const auto classIdx = module.getClassIndex();

        if (classIdx == factory->countClasses())
            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;
    }

    //==============================================================================
    VST3ModuleHandle module;
    VSTComSmartPtr<VST3HostContextHeadless> host;
    VSTComSmartPtr<Vst::IComponent> component;
    FUID cidOfComponent;

    bool isComponentInitialised = false;
};

//==============================================================================
class HostToClientParamQueue final : public Vst::IParamValueQueue
{
public:
    struct Item
    {
        Steinberg::int32 offset{};
        float value{};
    };

    using ItemsByIndex = std::map<Steinberg::int32, Item>;
    using Node = ItemsByIndex::node_type;
    using NodeStorage = std::vector<Node>;

    static Node makeNode()
    {
        ItemsByIndex container { {} };
        return container.extract (container.begin());
    }

    static NodeStorage makeStorage (size_t numItems)
    {
        NodeStorage result (numItems);
        std::generate (result.begin(), result.end(), makeNode);
        return result;
    }

    HostToClientParamQueue (Vst::ParamID idIn, Steinberg::int32 parameterIndexIn, NodeStorage& items)
        : paramId (idIn), parameterIndex (parameterIndexIn), sharedStorage (items)
    {
    }

    virtual ~HostToClientParamQueue() = 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 (Steinberg::int32) list.size();
    }

    tresult PLUGIN_API getPoint (Steinberg::int32 index,
                                 Steinberg::int32& offset,
                                 Vst::ParamValue& value) override
    {
        const auto item = getItem (index);

        if (! item.has_value())
            return kResultFalse;

        std::tie (offset, value) = std::tie (item->offset, item->value);
        return kResultTrue;
    }

    tresult PLUGIN_API addPoint (Steinberg::int32, Vst::ParamValue, Steinberg::int32&) override
    {
        // The VST3 SDK uses the IParamValueQueue interface for both input and output of parameter
        // change information. This interface includes the addPoint() function, which allows for
        // new parameter points to be added. However, when communicating parameter information from
        // host to plugin, it doesn't make sense for the plugin to add extra parameter change points
        // to the incoming queues. To enforce that the plugin doesn't attempt to mutate the
        // incoming queues, we always return false from this function. The host adds points to the
        // queue by calling append(), which is not exposed to the plugin, and is therefore
        // effectively private to the host.
        jassertfalse;
        return kResultFalse;
    }

    void append (Item item)
    {
        // The host *must* add points in sample-offset order
        jassert (list.empty() || std::prev (list.end())->second.offset <= item.offset);

        auto node = getNodeFromStorage();
        node.key() = (Steinberg::int32) list.size();
        node.mapped() = item;
        list.insert (std::move (node));
    }

    void clear()
    {
        while (! list.empty())
            sharedStorage.push_back (list.extract (list.begin()));
    }

private:
    std::optional<Item> getItem (Steinberg::int32 index) const
    {
        if (! isPositiveAndBelow (index, list.size()))
            return {};

        const auto iter = list.find (index);

        if (iter == list.end())
        {
            // Invariant violation
            jassertfalse;
            return {};
        }

        return iter->second;
    }

    Node getNodeFromStorage()
    {
        if (! sharedStorage.empty())
        {
            auto result = std::move (sharedStorage.back());
            sharedStorage.pop_back();
            return result;
        }

        // Allocating!
        jassertfalse;
        return makeNode();
    }

    const Vst::ParamID paramId;
    const Steinberg::int32 parameterIndex;
    NodeStorage& sharedStorage;
    ItemsByIndex list;
    Atomic<int> refCount;
};

class ClientToHostParamQueue final : public Vst::IParamValueQueue
{
public:
    ClientToHostParamQueue (Vst::ParamID idIn, Steinberg::int32 parameterIndexIn)
        : paramId (idIn), parameterIndex (parameterIndexIn)
    {
    }

    virtual ~ClientToHostParamQueue() = 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; }

    std::optional<float> getValue() const
    {
        return size > 0 ? std::optional<float> (cachedValue) : std::nullopt;
    }

private:
    const Vst::ParamID paramId;
    const Steinberg::int32 parameterIndex;
    float cachedValue{};
    Steinberg::int32 size{};
    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
*/
template <typename Queue>
class ParameterChanges final : public Vst::IParameterChanges
{
    static constexpr Steinberg::int32 notInVector = -1;

    struct Entry
    {
        explicit Entry (std::unique_ptr<Queue> queue)
            : ptr (queue.release(), IncrementRef::yes)
        {}

        VSTComSmartPtr<Queue> 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();
    }

    Queue* 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;
    }

    Queue* 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 offset)
    {
        Steinberg::int32 indexOut = notInVector;

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

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

        queues.clear();
    }

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

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

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

            if (ptr == nullptr)
                continue;

            if (const auto finalValue = ptr->getValue())
                callback (ptr->getParameterIndex(), ptr->getParameterId(), *finalValue);
        }
    }

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

//==============================================================================
class VST3PluginInstanceHeadless : public AudioPluginInstance
                                 , private AudioPluginExtensions::VST3Client
                                #ifdef JUCE_INTERNAL_HAS_ARA
                                 , private AudioPluginExtensions::ARAClient
                                #endif
{
public:
    //==============================================================================
    struct VST3Parameter final  : public Parameter
    {
        VST3Parameter (VST3PluginInstanceHeadless& parent, Steinberg::int32 vstParameterIndex)
            : pluginInstance (parent),
              vstParamIndex (vstParameterIndex)
        {
        }

        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)
        {
            if (! exactlyEqual (pluginInstance.cachedParamValues.exchangeWithoutNotifying (vstParamIndex, newValue), newValue))
                sendValueChangedMessageToListeners (newValue);
        }

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

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

                if (pluginInstance.editController->getParamStringByValue (cachedInfo.id, 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 (cachedInfo.id, toString (text), result) == kResultOk)
                    return (float) result;
            }

            return Parameter::getValueForText (text);
        }

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

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

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

        bool isAutomatable() const override
        {
            return (cachedInfo.flags & Vst::ParameterInfo::kCanAutomate) != 0;
        }

        bool isDiscrete() const override
        {
            return getNumSteps() != AudioProcessor::getDefaultNumParameterSteps();
        }

        int getNumSteps() const override
        {
            const auto stepCount = cachedInfo.stepCount;
            return stepCount == 0 ? AudioProcessor::getDefaultNumParameterSteps()
                                  : stepCount + 1;
        }

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

        String getParameterID() const override
        {
            return String (cachedInfo.id);
        }

        Vst::ParamID getParamID() const noexcept { return cachedInfo.id; }

        void updateCachedInfo()
        {
            cachedInfo = fetchParameterInfo();
        }

        Vst::ParameterInfo getParameterInfo() const
        {
            return cachedInfo;
        }

        Steinberg::int32 getVstParamIndex() const
        {
            return vstParamIndex;
        }

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

        VST3PluginInstanceHeadless& pluginInstance;
        const Steinberg::int32 vstParamIndex;
        Vst::ParameterInfo cachedInfo = fetchParameterInfo();
    };

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

    ~VST3PluginInstanceHeadless() override
    {
        MessageManager::callSync ([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;
    }

          AudioPluginExtensions::VST3Client* getVST3Client()       override { return this; }
    const AudioPluginExtensions::VST3Client* getVST3Client() const override { return this; }

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

   #ifdef JUCE_INTERNAL_HAS_ARA
          AudioPluginExtensions::ARAClient* getARAClient()       override { return this; }
    const AudioPluginExtensions::ARAClient* getARAClient() const override { return this; }

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

    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
    {
        return holder->module.getName();
    }

    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, 0);
        });

        processor->process (data);

        outputParameterChanges->forEach ([&] (Steinberg::int32 vstParamIndex, Vst::ParamID id, float value)
        {
            // Send the parameter value from the processor to the editor
            parameterDispatcher.push (vstParamIndex, value);

            // Update the host's parameter 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;
    }

    std::optional<String> getNameForMidiNoteNumber (int note, int /*midiChannel*/) override
    {
        if (unitInfo == nullptr || unitInfo->getProgramListCount() == 0)
            return std::nullopt;

        Vst::String128 name{};
        Vst::ProgramListInfo programListInfo{};

        const auto nameOk = unitInfo->getProgramListInfo (0, programListInfo)      == kResultOk
                         && unitInfo->hasProgramPitchNames (programListInfo.id, 0) == kResultTrue
                         && unitInfo->getProgramPitchName (programListInfo.id, 0, (Steinberg::int16) note, name) == kResultOk;

        return nameOk ? std::make_optional (toString (name))
                      : std::nullopt;
    }

    //==============================================================================
    void updateTrackProperties (const TrackProperties& properties) override
    {
        if (trackInfoListener != nullptr)
        {
            VSTComSmartPtr l { new TrackPropertiesAttributeList (properties), IncrementRef::yes };
            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))
            {
                if (props.name.has_value())
                {
                    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.value_or (String{}).length();
            else if (! std::strcmp (Vst::ChannelContext::kChannelColorKey, id))
                value = static_cast<Steinberg::int64> (props.colourARGB.value_or (0));
            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
    {
        return nullptr;
    }

    bool hasEditor() const override
    {
        return false;
    }

    //==============================================================================
    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 (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 getPreset() const override
    {
        VSTComSmartPtr memoryStream { new MemoryStream(), IncrementRef::no };

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

        const auto saved = 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 setPreset (const MemoryBlock& rawData) override
    {
        auto rawDataCopy = rawData;
        VSTComSmartPtr memoryStream { new MemoryStream (rawDataCopy.getData(), (int) rawDataCopy.getSize()),
                                      IncrementRef::no };

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

        return 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
    {
    }

    void updateParameterInfo()
    {
        for (auto& pair : idToParamMap)
            if (auto* param = pair.second)
                param->updateCachedInfo();
    }

    //==============================================================================
    /** @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;
    }

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);
    }

    //==============================================================================
    std::unique_ptr<VST3ComponentHolder> holder;

    friend VST3HostContextHeadless;

    // 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;
    HostToClientParamQueue::NodeStorage hostToClientParamQueueStorage;

    /*  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)
        {
            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<MemoryStream> createMemoryStreamForState (XmlElement& head, StringRef identifier)
    {
        if (auto* state = head.getChildByName (identifier))
        {
            MemoryBlock mem;

            if (mem.fromBase64Encoding (state->getAllSubText()))
            {
                VSTComSmartPtr stream { new MemoryStream(), IncrementRef::no };
                stream->setSize ((TSize) mem.getSize());
                mem.copyTo (stream->getData(), 0, mem.getSize());
                return stream;
            }
        }

        return {};
    }

    CachedParamValues cachedParamValues;
    VSTComSmartPtr<ParameterChanges<HostToClientParamQueue>> inputParameterChanges
    {
        new ParameterChanges<HostToClientParamQueue>,
        IncrementRef::yes
    };

    VSTComSmartPtr<ParameterChanges<ClientToHostParamQueue>> outputParameterChanges
    {
        new ParameterChanges<ClientToHostParamQueue>,
        IncrementRef::yes
    };

    VSTComSmartPtr<MidiEventList> midiInputs { new MidiEventList, IncrementRef::yes };
    VSTComSmartPtr<MidiEventList> midiOutputs { new MidiEventList, IncrementRef::yes };
    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);
            }
        }

        {
            hostToClientParamQueueStorage = HostToClientParamQueue::makeStorage (1 << 13);

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

        for (auto end = editController->getParameterCount(), i = 0; i < end; ++i)
        {
            auto* param = new VST3Parameter (*this, i);
            const auto paramInfo = param->getParameterInfo();

            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()
    {
        MemoryStream stream;

        if (holder->component->getState (&stream) == kResultTrue)
            if (stream.seek (0, 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;
    }

    //==============================================================================
    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())
        {
            const auto midiMessageCallback = [&] (auto controlID, float paramValue, auto time)
            {
                Steinberg::int32 queueIndex{};

                if (auto* queue = inputParameterChanges->addParameterData (controlID, queueIndex))
                    queue->append ({ (Steinberg::int32) time, paramValue });

                if (auto* param = getParameterForID (controlID))
                {
                    // Send the parameter value to the editor
                    parameterDispatcher.push (param->getVstParamIndex(), paramValue);

                    // Update the host's view of the parameter value
                    param->setValueWithoutUpdatingProcessor (paramValue);
                }
            };

            MidiEventList::hostToPluginEventList (*midiInputs,
                                                  midiBuffer,
                                                  storedMidiMapping,
                                                  midiMessageCallback);
        }

        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 & 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 (VST3PluginInstanceHeadless)
};

JUCE_END_IGNORE_DEPRECATION_WARNINGS

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

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

    return kResultFalse;
}

tresult VST3HostContextHeadless::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 VST3HostContextHeadless::endEdit (Vst::ParamID paramID)
{
    if (plugin == nullptr)
        return kResultTrue;

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

    return kResultFalse;
}

tresult VST3HostContextHeadless::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 VST3HostContextHeadless::setDirty (TBool needsSave)
{
    if (needsSave)
        plugin->updateHostDisplay (AudioPluginInstance::ChangeDetails{}.withNonParameterStateChanged (true));

    return kResultOk;
}

void VST3HostContextHeadless::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();

    if (hasFlag (flags, Vst::kParamTitlesChanged))
        plugin->updateParameterInfo();

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

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

    return kResultTrue;
}

//==============================================================================
static inline void recursiveFileSearch (VST3PluginFormatHeadless& format, 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 (format.fileMightContainThisPluginType (f.getFullPathName()))
        {
            isPlugin = true;
            results.add (f.getFullPathName());
        }

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

static StringArray getLibraryPaths ([[maybe_unused]] VST3PluginFormatHeadless& format, const String& fileOrIdentifier)
{
   #if JUCE_WINDOWS
    if (! File (fileOrIdentifier).existsAsFile())
    {
        StringArray files;
        recursiveFileSearch (format, files, fileOrIdentifier, true);
        return files;
    }
   #endif

    return { fileOrIdentifier };
}

template <typename TypeToCreate>
static std::unique_ptr<AudioPluginInstance> createVST3Instance (VST3PluginFormatHeadless& format,
                                                                const PluginDescription& description,
                                                                const File& file,
                                                                VSTComSmartPtr<VST3HostContextHeadless> host)
{
    if (! format.fileMightContainThisPluginType (description.fileOrIdentifier))
        return nullptr;

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

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

    const auto module = VST3ModuleHandle::create (file, description);

    if (! module.isValid())
        return nullptr;

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

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

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

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

    return instance;
}

template <typename TypeToCreate>
static void createVst3InstanceImpl (VST3PluginFormatHeadless& format,
                                    VSTComSmartPtr<VST3HostContextHeadless> host,
                                    const PluginDescription& description,
                                    AudioPluginFormat::PluginCreationCallback callback)
{
    for (const auto& file : getLibraryPaths (format, description.fileOrIdentifier))
    {
        if (auto result = createVST3Instance<TypeToCreate> (format, description, file, host))
        {
            callback (std::move (result), {});
            return;
        }
    }

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

JUCE_END_NO_SANITIZE

} // namespace juce

#endif
