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

   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_LV2

#include <juce_audio_processors_headless/format_types/juce_LV2Common.h>
#include <juce_audio_processors_headless/format_types/juce_LV2Resources.h>
#include <juce_audio_processors_headless/utilities/juce_FlagCache.h>

#include <thread>

namespace juce
{
namespace lv2_host
{

template <typename Struct, typename Value>
auto with (Struct s, Value Struct::* member, Value value) noexcept
{
    s.*member = std::move (value);
    return s;
}

/*  Converts a void* to an LV2_Atom* if the buffer looks like it holds a well-formed Atom, or
    returns nullptr otherwise.
*/
static const LV2_Atom* convertToAtomPtr (const void* ptr, size_t size)
{
    if (size < sizeof (LV2_Atom))
    {
        jassertfalse;
        return nullptr;
    }

    const auto header = readUnaligned<LV2_Atom> (ptr);

    if (size < header.size + sizeof (LV2_Atom))
    {
        jassertfalse;
        return nullptr;
    }

    // This is UB _if_ the ptr doesn't really point to an LV2_Atom.
    return reinterpret_cast<const LV2_Atom*> (ptr);
}

// Allows mutable access to the items in a vector, without allowing the vector itself
// to be modified.
template <typename T>
class SimpleSpan
{
public:
    constexpr SimpleSpan (T* beginIn, T* endIn) : b (beginIn), e (endIn) {}

    constexpr auto begin() const { return b; }
    constexpr auto end()   const { return e; }

    JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4814)
    constexpr auto& operator[] (size_t index)       { return b[index]; }
    JUCE_END_IGNORE_WARNINGS_MSVC

    constexpr auto size() const { return (size_t) (e - b); }

private:
    T* b;
    T* e;
};

template <typename T>
constexpr auto makeSimpleSpan (T* b, T* e) { return SimpleSpan<T> { b, e }; }

template <typename R>
constexpr auto makeSimpleSpan (R& r) { return makeSimpleSpan (r.data(), r.data() + r.size()); }

struct PhysicalResizeListener
{
    virtual ~PhysicalResizeListener() = default;
    virtual void viewRequestedResizeInPhysicalPixels (int width, int height) = 0;
};

struct LogicalResizeListener
{
    virtual ~LogicalResizeListener() = default;
    virtual void viewRequestedResizeInLogicalPixels (int width, int height) = 0;
};

#if JUCE_WINDOWS
class WindowSizeChangeDetector
{
public:
    WindowSizeChangeDetector()
        : hook (SetWindowsHookEx (WH_CALLWNDPROC,
                                  callWndProc,
                                  (HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(),
                                  GetCurrentThreadId()))
    {}

    ~WindowSizeChangeDetector() noexcept
    {
        UnhookWindowsHookEx (hook);
    }

    static void addListener (HWND hwnd, PhysicalResizeListener& listener)
    {
        getActiveEditors().emplace (hwnd, &listener);
    }

    static void removeListener (HWND hwnd)
    {
        getActiveEditors().erase (hwnd);
    }

private:
    static std::map<HWND, PhysicalResizeListener*>& getActiveEditors()
    {
        static std::map<HWND, PhysicalResizeListener*> map;
        return map;
    }

    static void processMessage (int nCode, const CWPSTRUCT* info)
    {
        if (nCode < 0 || info == nullptr)
            return;

        constexpr UINT events[] { WM_SIZING, WM_SIZE, WM_WINDOWPOSCHANGING, WM_WINDOWPOSCHANGED };

        if (std::find (std::begin (events), std::end (events), info->message) == std::end (events))
            return;

        auto& map = getActiveEditors();
        auto iter = map.find (info->hwnd);

        if (iter == map.end())
            return;

        RECT rect;
        GetWindowRect (info->hwnd, &rect);
        iter->second->viewRequestedResizeInPhysicalPixels (rect.right - rect.left, rect.bottom - rect.top);
    }

    static LRESULT CALLBACK callWndProc (int nCode, WPARAM wParam, LPARAM lParam)
    {
        processMessage (nCode, lv2_shared::wordCast<CWPSTRUCT*> (lParam));
        return CallNextHookEx ({}, nCode, wParam, lParam);
    }

    HHOOK hook;
};

class WindowSizeChangeListener
{
public:
    WindowSizeChangeListener (HWND hwndIn, PhysicalResizeListener& l)
        : hwnd (hwndIn)
    {
        detector->addListener (hwnd, l);
    }

    ~WindowSizeChangeListener()
    {
        detector->removeListener (hwnd);
    }

private:
    SharedResourcePointer<WindowSizeChangeDetector> detector;
    HWND hwnd;

    JUCE_LEAK_DETECTOR (WindowSizeChangeListener)
};
#endif

struct FreeLilvNode
{
    void operator() (LilvNode* ptr) const noexcept { lilv_node_free (ptr); }
};

using OwningNode = std::unique_ptr<LilvNode, FreeLilvNode>;

template <typename Traits>
class TypesafeLilvNode
{
public:
    template <typename... Ts>
    TypesafeLilvNode (Ts&&... ts)
        : node (Traits::construct (std::forward<Ts> (ts)...)) {}

    bool equals (const TypesafeLilvNode& other) const noexcept
    {
        return lilv_node_equals (node.get(), other.node.get());
    }

    const LilvNode* get() const noexcept { return node.get(); }

    auto getTyped() const noexcept -> decltype (Traits::access (nullptr))
    {
        return Traits::access (node.get());
    }

    static TypesafeLilvNode claim (LilvNode* node)
    {
        return TypesafeLilvNode { node };
    }

    static TypesafeLilvNode copy (const LilvNode* node)
    {
        return TypesafeLilvNode { lilv_node_duplicate (node) };
    }

private:
    explicit TypesafeLilvNode (LilvNode* ptr)
        : node (ptr)
    {
        jassert (ptr == nullptr || Traits::verify (node.get()));
    }

    OwningNode node;

    JUCE_LEAK_DETECTOR (TypesafeLilvNode)
};

struct UriConstructorTrait
{
    static LilvNode* construct (LilvWorld* world, const char* uri) noexcept
    {
        return lilv_new_uri (world, uri);
    }

    static LilvNode* construct (LilvWorld* world, const char* host, const char* path) noexcept
    {
        return lilv_new_file_uri (world, host, path);
    }

    static constexpr auto verify = lilv_node_is_uri;
    static constexpr auto access = lilv_node_as_uri;
};

struct StringConstructorTrait { static constexpr auto construct  = lilv_new_string;
                                static constexpr auto verify     = lilv_node_is_string;
                                static constexpr auto access     = lilv_node_as_string; };

using NodeUri    = TypesafeLilvNode<UriConstructorTrait>;
using NodeString = TypesafeLilvNode<StringConstructorTrait>;

struct UsefulUris
{
    explicit UsefulUris (LilvWorld* worldIn)
        : world (worldIn) {}

    LilvWorld* const world = nullptr;

   #define X(str) const NodeUri m##str { world, str };
    X (LV2_ATOM__AtomPort)
    X (LV2_ATOM__atomTransfer)
    X (LV2_ATOM__eventTransfer)
    X (LV2_CORE__AudioPort)
    X (LV2_CORE__CVPort)
    X (LV2_CORE__ControlPort)
    X (LV2_CORE__GeneratorPlugin)
    X (LV2_CORE__InputPort)
    X (LV2_CORE__InstrumentPlugin)
    X (LV2_CORE__OutputPort)
    X (LV2_CORE__enumeration)
    X (LV2_CORE__integer)
    X (LV2_CORE__toggled)
    X (LV2_RESIZE_PORT__minimumSize)
    X (LV2_UI__floatProtocol)
    X (LV2_WORKER__interface)
   #undef X
};

template <typename Ptr, typename Free>
struct OwningPtrTraits
{
    using type = std::unique_ptr<Ptr, Free>;
    static const Ptr* get (const type& t) noexcept { return t.get(); }
};

template <typename Ptr>
struct NonOwningPtrTraits
{
    using type = const Ptr*;
    static const Ptr* get (const type& t) noexcept { return t; }
};

struct PluginsIteratorTraits
{
    using Container                 = const LilvPlugins*;
    using Iter                      = LilvIter*;
    static constexpr auto begin     = lilv_plugins_begin;
    static constexpr auto next      = lilv_plugins_next;
    static constexpr auto isEnd     = lilv_plugins_is_end;
    static constexpr auto get       = lilv_plugins_get;
};

using PluginsIterator = lv2_shared::Iterator<PluginsIteratorTraits>;

struct PluginClassesIteratorTraits
{
    using Container                 = const LilvPluginClasses*;
    using Iter                      = LilvIter*;
    static constexpr auto begin     = lilv_plugin_classes_begin;
    static constexpr auto next      = lilv_plugin_classes_next;
    static constexpr auto isEnd     = lilv_plugin_classes_is_end;
    static constexpr auto get       = lilv_plugin_classes_get;
};

using PluginClassesIterator = lv2_shared::Iterator<PluginClassesIteratorTraits>;

struct NodesIteratorTraits
{
    using Container                 = const LilvNodes*;
    using Iter                      = LilvIter*;
    static constexpr auto begin     = lilv_nodes_begin;
    static constexpr auto next      = lilv_nodes_next;
    static constexpr auto isEnd     = lilv_nodes_is_end;
    static constexpr auto get       = lilv_nodes_get;
};

using NodesIterator = lv2_shared::Iterator<NodesIteratorTraits>;

struct ScalePointsIteratorTraits
{
    using Container                 = const LilvScalePoints*;
    using Iter                      = LilvIter*;
    static constexpr auto begin     = lilv_scale_points_begin;
    static constexpr auto next      = lilv_scale_points_next;
    static constexpr auto isEnd     = lilv_scale_points_is_end;
    static constexpr auto get       = lilv_scale_points_get;
};

using ScalePointsIterator = lv2_shared::Iterator<ScalePointsIteratorTraits>;

struct UisIteratorTraits
{
    using Container                 = const LilvUIs*;
    using Iter                      = LilvIter*;
    static constexpr auto begin     = lilv_uis_begin;
    static constexpr auto next      = lilv_uis_next;
    static constexpr auto isEnd     = lilv_uis_is_end;
    static constexpr auto get       = lilv_uis_get;
};

using UisIterator = lv2_shared::Iterator<UisIteratorTraits>;

template <typename PtrTraits>
class NodesImpl
{
public:
    using type = typename PtrTraits::type;

    template <typename Ptr>
    explicit NodesImpl (Ptr* ptr)
        : nodes (type { ptr }) {}

    explicit NodesImpl (type ptr)
        : nodes (std::move (ptr)) {}

    unsigned size() const noexcept { return lilv_nodes_size (PtrTraits::get (nodes)); }

    NodesIterator begin() const noexcept
    {
        return nodes == nullptr ? NodesIterator{}
                                : NodesIterator { PtrTraits::get (nodes) };
    }

    NodesIterator end()   const noexcept { return {}; }

private:
    type nodes{};
};

struct NodesFree
{
    void operator() (LilvNodes* ptr) const noexcept { lilv_nodes_free (ptr); }
};

using OwningNodes    = NodesImpl<OwningPtrTraits<LilvNodes, NodesFree>>;
using NonOwningNodes = NodesImpl<NonOwningPtrTraits<LilvNodes>>;

class ScalePoints
{
public:
    explicit ScalePoints (const LilvScalePoints* pt)
        : points (pt) {}

    ScalePointsIterator begin() const noexcept
    {
        return points == nullptr ? ScalePointsIterator{}
                                 : ScalePointsIterator { points };
    }

    ScalePointsIterator end() const noexcept { return {}; }

private:
    const LilvScalePoints* points = nullptr;
};

class ScalePoint
{
public:
    explicit ScalePoint (const LilvScalePoint* pt)
        : point (pt) {}

    const LilvNode* getLabel() const noexcept { return lilv_scale_point_get_label (point); }
    const LilvNode* getValue() const noexcept { return lilv_scale_point_get_value (point); }

private:
    const LilvScalePoint* point = nullptr;
};

struct PortRange
{
    float defaultValue, min, max;
};

class Port
{
public:
    enum class Kind
    {
        control,
        audio,
        cv,
        atom,
        unknown,
    };

    enum class Direction
    {
        input,
        output,
        unknown,
    };

    Port (const LilvPlugin* pluginIn, const LilvPort* portIn)
        : plugin (pluginIn), port (portIn) {}

    Direction getDirection (const UsefulUris& uris) const noexcept
    {
        if (isA (uris.mLV2_CORE__InputPort))
            return Direction::input;

        if (isA (uris.mLV2_CORE__OutputPort))
            return Direction::output;

        return Direction::unknown;
    }

    Kind getKind (const UsefulUris& uris) const noexcept
    {
        if (isA (uris.mLV2_CORE__ControlPort))
            return Kind::control;

        if (isA (uris.mLV2_CORE__AudioPort))
            return Kind::audio;

        if (isA (uris.mLV2_CORE__CVPort))
            return Kind::cv;

        if (isA (uris.mLV2_ATOM__AtomPort))
            return Kind::atom;

        return Kind::unknown;
    }

    OwningNode get (const LilvNode* predicate) const noexcept
    {
        return OwningNode { lilv_port_get (plugin, port, predicate) };
    }

    NonOwningNodes getClasses() const noexcept
    {
        return NonOwningNodes { lilv_port_get_classes (plugin, port) };
    }

    NodeString getName() const noexcept
    {
        return NodeString::claim (lilv_port_get_name (plugin, port));
    }

    NodeString getSymbol() const noexcept
    {
        return NodeString::copy (lilv_port_get_symbol (plugin, port));
    }

    OwningNodes getProperties() const noexcept
    {
        return OwningNodes { lilv_port_get_properties (plugin, port) };
    }

    ScalePoints getScalePoints() const noexcept
    {
        return ScalePoints { lilv_port_get_scale_points (plugin, port) };
    }

    bool hasProperty (const NodeUri& uri) const noexcept
    {
        return lilv_port_has_property (plugin, port, uri.get());
    }

    uint32_t getIndex() const noexcept { return lilv_port_get_index (plugin, port); }

    static float getFloatValue (const LilvNode* node, float fallback)
    {
        if (lilv_node_is_float (node) || lilv_node_is_int (node))
            return lilv_node_as_float (node);

        return fallback;
    }

    bool supportsEvent (const LilvNode* node) const noexcept
    {
        return lilv_port_supports_event (plugin, port, node);
    }

    PortRange getRange() const noexcept
    {
        LilvNode* def = nullptr;
        LilvNode* min = nullptr;
        LilvNode* max = nullptr;

        lilv_port_get_range (plugin, port, &def, &min, &max);

        const OwningNode defOwner { def };
        const OwningNode minOwner { min };
        const OwningNode maxOwner { max };

        return { getFloatValue (def, 0.0f),
                 getFloatValue (min, 0.0f),
                 getFloatValue (max, 1.0f) };
    }

    bool isValid() const noexcept { return port != nullptr; }

private:
    bool isA (const NodeUri& uri) const noexcept
    {
        return lilv_port_is_a (plugin, port, uri.get());
    }

    const LilvPlugin* plugin = nullptr;
    const LilvPort* port = nullptr;

    JUCE_LEAK_DETECTOR (Port)
};

class Plugin
{
public:
    explicit Plugin (const LilvPlugin* p) : plugin (p) {}

    bool verify() const noexcept              { return lilv_plugin_verify (plugin); }
    NodeUri getUri() const noexcept           { return NodeUri::copy (lilv_plugin_get_uri (plugin)); }
    NodeUri getBundleUri() const noexcept     { return NodeUri::copy (lilv_plugin_get_bundle_uri (plugin)); }
    NodeUri getLibraryUri() const noexcept    { return NodeUri::copy (lilv_plugin_get_library_uri (plugin)); }
    NodeString getName() const noexcept       { return NodeString::claim (lilv_plugin_get_name (plugin)); }
    NodeString getAuthorName() const noexcept { return NodeString::claim (lilv_plugin_get_author_name (plugin)); }
    uint32_t getNumPorts() const noexcept { return lilv_plugin_get_num_ports (plugin); }
    const LilvPluginClass* getClass() const noexcept { return lilv_plugin_get_class (plugin); }
    OwningNodes getValue (const LilvNode* predicate) const noexcept { return OwningNodes { lilv_plugin_get_value (plugin, predicate) }; }

    Port getPortByIndex (uint32_t index) const noexcept
    {
        return Port { plugin, lilv_plugin_get_port_by_index (plugin, index) };
    }

    Port getPortByDesignation (const LilvNode* portClass, const LilvNode* designation) const noexcept
    {
        return Port { plugin, lilv_plugin_get_port_by_designation (plugin, portClass, designation) };
    }

    OwningNodes getRequiredFeatures() const noexcept
    {
        return OwningNodes { lilv_plugin_get_required_features (plugin) };
    }

    OwningNodes getOptionalFeatures() const noexcept
    {
        return OwningNodes { lilv_plugin_get_optional_features (plugin) };
    }

    bool hasExtensionData (const NodeUri& uri) const noexcept
    {
        return lilv_plugin_has_extension_data (plugin, uri.get());
    }

    bool hasFeature (const NodeUri& uri) const noexcept
    {
        return lilv_plugin_has_feature (plugin, uri.get());
    }

    template <typename... Classes>
    uint32_t getNumPortsOfClass (const Classes&... classes) const noexcept
    {
        return lilv_plugin_get_num_ports_of_class (plugin, classes.get()..., 0);
    }

    const LilvPlugin* get() const noexcept { return plugin; }

    bool hasLatency() const noexcept { return lilv_plugin_has_latency (plugin); }
    uint32_t getLatencyPortIndex() const noexcept { return lilv_plugin_get_latency_port_index (plugin); }

private:
    const LilvPlugin* plugin = nullptr;

    JUCE_LEAK_DETECTOR (Plugin)
};

/*
    This is very similar to the symap implementation in jalv.
*/
class SymbolMap
{
public:
    SymbolMap() = default;

    SymbolMap (std::initializer_list<const char*> uris)
    {
        for (const auto* str : uris)
            map (str);
    }

    LV2_URID map (const char* uri)
    {
        const auto comparator = [this] (size_t index, const String& str)
        {
            return strings[index] < str;
        };

        const auto uriString = String::fromUTF8 (uri);
        const auto it = std::lower_bound (indices.cbegin(), indices.cend(), uriString, comparator);

        if (it != indices.cend() && strings[*it] == uriString)
            return static_cast<LV2_URID> (*it + 1);

        const auto index = strings.size();
        indices.insert (it, index);
        strings.push_back (uriString);
        return static_cast<LV2_URID> (index + 1);
    }

    const char* unmap (LV2_URID urid) const
    {
        const auto index = urid - 1;
        return index < strings.size() ? strings[index].toRawUTF8()
                                      : nullptr;
    }

    static LV2_URID mapUri (LV2_URID_Map_Handle handle, const char* uri)
    {
        return static_cast<SymbolMap*> (handle)->map (uri);
    }

    static const char* unmapUri (LV2_URID_Unmap_Handle handle, LV2_URID urid)
    {
        return static_cast<SymbolMap*> (handle)->unmap (urid);
    }

    LV2_URID_Map   getMapFeature()      { return { this, mapUri }; }
    LV2_URID_Unmap getUnmapFeature()    { return { this, unmapUri }; }

private:
    std::vector<String> strings;
    std::vector<size_t> indices;

    JUCE_LEAK_DETECTOR (SymbolMap)
};

struct UsefulUrids
{
    explicit UsefulUrids (SymbolMap& m) : symap (m) {}

    SymbolMap& symap;

   #define X(token) const LV2_URID m##token = symap.map (token);
    X (LV2_ATOM__Bool)
    X (LV2_ATOM__Double)
    X (LV2_ATOM__Float)
    X (LV2_ATOM__Int)
    X (LV2_ATOM__Long)
    X (LV2_ATOM__Object)
    X (LV2_ATOM__Sequence)
    X (LV2_ATOM__atomTransfer)
    X (LV2_ATOM__beatTime)
    X (LV2_ATOM__eventTransfer)
    X (LV2_ATOM__frameTime)
    X (LV2_LOG__Error)
    X (LV2_LOG__Note)
    X (LV2_LOG__Trace)
    X (LV2_LOG__Warning)
    X (LV2_MIDI__MidiEvent)
    X (LV2_PATCH__Set)
    X (LV2_PATCH__property)
    X (LV2_PATCH__value)
    X (LV2_STATE__StateChanged)
    X (LV2_TIME__Position)
    X (LV2_TIME__barBeat)
    X (LV2_TIME__beat)
    X (LV2_TIME__beatUnit)
    X (LV2_TIME__beatsPerBar)
    X (LV2_TIME__beatsPerMinute)
    X (LV2_TIME__frame)
    X (LV2_TIME__speed)
    X (LV2_TIME__bar)
    X (LV2_UI__floatProtocol)
    X (LV2_UNITS__beat)
    X (LV2_UNITS__frame)
   #undef X
};

class Log
{
public:
    explicit Log (const UsefulUrids* u) : urids (u) {}

    LV2_Log_Log* getLogFeature() { return &logFeature; }

private:
    int vprintfCallback ([[maybe_unused]] LV2_URID type, const char* fmt, va_list ap) const
    {
        // If this is hit, the plugin has encountered some kind of error
        ignoreUnused (urids);
        jassert (type != urids->mLV2_LOG__Error && type != urids->mLV2_LOG__Warning);
        return std::vfprintf (stderr, fmt, ap);
    }

    static int vprintfCallback (LV2_Log_Handle handle,
                                LV2_URID type,
                                const char* fmt,
                                va_list ap)
    {
        return static_cast<const Log*> (handle)->vprintfCallback (type, fmt, ap);
    }

    static int printfCallback (LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...)
    {
        va_list list;
        va_start (list, fmt);
        auto result = vprintfCallback (handle, type, fmt, list);
        va_end (list);
        return result;
    }

    const UsefulUrids* urids = nullptr;
    LV2_Log_Log logFeature { this, printfCallback, vprintfCallback };

    JUCE_LEAK_DETECTOR (Log)
};

struct Features
{
    explicit Features (std::vector<LV2_Feature>&& f)
        : features (std::move (f)) {}

    static std::vector<String> getUris (const std::vector<LV2_Feature>& features)
    {
        std::vector<String> result;
        result.reserve (features.size());

        for (const auto& feature : features)
            result.push_back (String::fromUTF8 (feature.URI));

        return result;
    }

    std::vector<LV2_Feature> features;
    std::vector<const LV2_Feature*> pointers = makeNullTerminatedArray();

private:
    std::vector<const LV2_Feature*> makeNullTerminatedArray()
    {
        std::vector<const LV2_Feature*> result;
        result.reserve (features.size() + 1);

        for (const auto& feature : features)
            result.push_back (&feature);

        result.push_back (nullptr);

        return result;
    }

    JUCE_LEAK_DETECTOR (Features)
};

template <typename Extension>
struct OptionalExtension
{
    OptionalExtension() = default;

    explicit OptionalExtension (Extension extensionIn) : extension (extensionIn), valid (true) {}

    Extension extension;
    bool valid = false;
};

class Instance
{
    struct Free
    {
        void operator() (LilvInstance* ptr) const noexcept { lilv_instance_free (ptr); }
    };

public:
    using Ptr = std::unique_ptr<LilvInstance, Free>;
    using GetExtensionData = const void* (*) (const char*);

    Instance (const Plugin& pluginIn, double sampleRate, const LV2_Feature* const* features)
        : plugin (pluginIn),
          instance (lilv_plugin_instantiate (plugin.get(), sampleRate, features)) {}

    void activate() { lilv_instance_activate (instance.get()); }
    void run (uint32_t sampleCount) { lilv_instance_run (instance.get(), sampleCount); }
    void deactivate() { lilv_instance_deactivate (instance.get()); }

    const char* getUri() const noexcept { return lilv_instance_get_uri (instance.get()); }

    LV2_Handle getHandle() const noexcept { return lilv_instance_get_handle (instance.get()); }

    LilvInstance* get() const noexcept { return instance.get(); }

    void connectPort (uint32_t index, void* data)
    {
        lilv_instance_connect_port (instance.get(), index, data);
    }

    template <typename Extension>
    OptionalExtension<Extension> getExtensionData (const NodeUri& uri) const noexcept
    {
        if (plugin.get() == nullptr || ! plugin.hasExtensionData (uri) || instance.get() == nullptr)
            return {};

        return OptionalExtension<Extension> { readUnaligned<Extension> (lilv_instance_get_extension_data (instance.get(), uri.getTyped())) };
    }

    GetExtensionData getExtensionDataCallback() const noexcept
    {
        return instance->lv2_descriptor->extension_data;
    }

    bool operator== (std::nullptr_t) const noexcept { return instance == nullptr; }
    bool operator!= (std::nullptr_t) const noexcept { return ! (*this == nullptr); }

private:
    Plugin plugin;
    Ptr instance;

    JUCE_LEAK_DETECTOR (Instance)
};

enum class Realtime { no, yes };

// Must be trivial!
struct WorkResponder
{
    static WorkResponder getDefault() { return { nullptr, nullptr }; }

    LV2_Worker_Status processResponse (uint32_t size, const void* data) const
    {
        return worker->work_response (handle, size, data);
    }

    bool isValid() const { return handle != nullptr && worker != nullptr; }

    LV2_Handle handle;
    const LV2_Worker_Interface* worker;
};

struct WorkerResponseListener
{
    virtual ~WorkerResponseListener() = default;
    virtual LV2_Worker_Status responseGenerated (WorkResponder, uint32_t, const void*) = 0;
};

struct RespondHandle
{
    LV2_Worker_Status respond (uint32_t size, const void* data) const
    {
        if (realtime == Realtime::yes)
            return listener.responseGenerated (responder, size, data);

        return responder.processResponse (size, data);
    }

    static LV2_Worker_Status respond (LV2_Worker_Respond_Handle handle,
                                      uint32_t size,
                                      const void* data)
    {
        return static_cast<const RespondHandle*> (handle)->respond (size, data);
    }

    WorkResponder responder;
    WorkerResponseListener& listener;
    Realtime realtime;
};

// Must be trivial!
struct WorkSubmitter
{
    static WorkSubmitter getDefault() { return { nullptr, nullptr, nullptr, nullptr }; }

    LV2_Worker_Status doWork (Realtime realtime, uint32_t size, const void* data) const
    {
        // The Worker spec says that the host "MUST NOT make concurrent calls to [work] from
        // several threads".
        // Taking the work mutex here ensures that only one piece of work is done at a time.
        // If we didn't take the work mutex, there would be a danger of work happening
        // simultaneously on the worker thread and the render thread when switching between
        // realtime/offline modes (in realtime mode, work happens on the worker thread; in
        // offline mode, work happens immediately on the render/audio thread).
        const ScopedLock lock (*workMutex);

        RespondHandle respondHandle { WorkResponder { handle, worker }, *listener, realtime };
        return worker->work (handle, RespondHandle::respond, &respondHandle, size, data);
    }

    bool isValid() const { return handle != nullptr && worker != nullptr && listener != nullptr && workMutex != nullptr; }

    LV2_Handle handle;
    const LV2_Worker_Interface* worker;
    WorkerResponseListener* listener;
    CriticalSection* workMutex;
};

template <typename Trivial>
static auto toChars (Trivial value)
{
    static_assert (std::is_trivial_v<Trivial>);
    std::array<char, sizeof (Trivial)> result;
    writeUnaligned (result.data(), value);
    return result;
}

template <typename Context>
class WorkQueue
{
public:
    static_assert (std::is_trivial_v<Context>, "Context must be copyable as bytes");

    explicit WorkQueue (int size)
        : fifo (size), data (static_cast<size_t> (size)) {}

    LV2_Worker_Status push (Context context, size_t size, const void* contents)
    {
        const auto* bytes = static_cast<const char*> (contents);
        const auto numToWrite = sizeof (Header) + size;

        if (static_cast<size_t> (fifo.getFreeSpace()) < numToWrite)
            return LV2_WORKER_ERR_NO_SPACE;

        Header header { size, context };
        const auto headerBuffer = toChars (header);

        const auto scope = fifo.write (static_cast<int> (numToWrite));
        jassert (scope.blockSize1 + scope.blockSize2 == static_cast<int> (numToWrite));

        size_t index = 0;
        scope.forEach ([&] (int i)
        {
            data[static_cast<size_t> (i)] = index < headerBuffer.size() ? headerBuffer[index]
                                                                        : bytes[index - headerBuffer.size()];
            ++index;
        });

        return LV2_WORKER_SUCCESS;
    }

    Context pop (std::vector<char>& dest)
    {
        // If the vector is too small we'll have to resize it on the audio thread
        jassert (dest.capacity() >= data.size());
        dest.clear();

        const auto numReady = fifo.getNumReady();

        if (static_cast<size_t> (numReady) < sizeof (Header))
        {
            jassert (numReady == 0);
            return Context::getDefault();
        }

        std::array<char, sizeof (Header)> headerBuffer;

        {
            size_t index = 0;
            fifo.read (sizeof (Header)).forEach ([&] (int i)
            {
                headerBuffer[index++] = data[static_cast<size_t> (i)];
            });
        }

        const auto header = readUnaligned<Header> (headerBuffer.data());

        jassert (static_cast<size_t> (fifo.getNumReady()) >= header.size);

        dest.resize (header.size);

        {
            size_t index = 0;
            fifo.read (static_cast<int> (header.size)).forEach ([&] (int i)
            {
                dest[index++] = data[static_cast<size_t> (i)];
            });
        }

        return header.context;
    }

private:
    struct Header
    {
        size_t size;
        Context context;
    };

    AbstractFifo fifo;
    std::vector<char> data;

    JUCE_LEAK_DETECTOR (WorkQueue)
};

/*
    Keeps track of active plugin instances, so that we can avoid sending work
    messages to dead plugins.
*/
class HandleRegistry
{
public:
    void insert (LV2_Handle handle)
    {
        const SpinLock::ScopedLockType lock (mutex);
        handles.insert (handle);
    }

    void erase (LV2_Handle handle)
    {
        const SpinLock::ScopedLockType lock (mutex);
        handles.erase (handle);
    }

    template <typename Fn>
    LV2_Worker_Status ifContains (LV2_Handle handle, Fn&& callback)
    {
        const SpinLock::ScopedLockType lock (mutex);

        if (handles.find (handle) != handles.cend())
            return callback();

        return LV2_WORKER_ERR_UNKNOWN;
    }

private:
    std::set<LV2_Handle> handles;
    SpinLock mutex;

    JUCE_LEAK_DETECTOR (HandleRegistry)
};

/*
    Implements an LV2 Worker, allowing work to be scheduled in realtime
    by the plugin instance.

    IMPORTANT this will die pretty hard if `getExtensionData (LV2_WORKER__interface)`
    returns garbage, so make sure to check that the plugin `hasExtensionData` before
    constructing one of these!
*/
class SharedThreadedWorker final : public WorkerResponseListener
{
public:
    ~SharedThreadedWorker() noexcept override
    {
        shouldExit = true;
        thread.join();
    }

    LV2_Worker_Status schedule (WorkSubmitter submitter,
                                uint32_t size,
                                const void* data)
    {
        return registry.ifContains (submitter.handle, [&]
        {
            return incoming.push (submitter, size, data);
        });
    }

    LV2_Worker_Status responseGenerated (WorkResponder responder,
                                         uint32_t size,
                                         const void* data) override
    {
        return registry.ifContains (responder.handle, [&]
        {
            return outgoing.push (responder, size, data);
        });
    }

    void processResponses()
    {
        for (;;)
        {
            auto workerResponder = outgoing.pop (message);

            if (! message.empty() && workerResponder.isValid())
                workerResponder.processResponse (static_cast<uint32_t> (message.size()), message.data());
            else
                break;
        }
    }

    void registerHandle   (LV2_Handle handle) { registry.insert (handle); }
    void deregisterHandle (LV2_Handle handle) { registry.erase  (handle); }

private:
    static constexpr auto queueSize = 8192;
    std::atomic<bool> shouldExit { false };
    WorkQueue<WorkSubmitter> incoming { queueSize };
    WorkQueue<WorkResponder> outgoing { queueSize };
    std::vector<char> message = std::vector<char> (queueSize);
    std::thread thread { [this]
    {
        std::vector<char> buffer (queueSize);

        while (! shouldExit)
        {
            const auto submitter = incoming.pop (buffer);

            if (! buffer.empty() && submitter.isValid())
                submitter.doWork (Realtime::yes, (uint32_t) buffer.size(), buffer.data());
            else
                std::this_thread::sleep_for (std::chrono::milliseconds (1));
        }
    } };
    HandleRegistry registry;

    JUCE_LEAK_DETECTOR (SharedThreadedWorker)
};

struct HandleHolder
{
    virtual ~HandleHolder() = default;
    virtual LV2_Handle getHandle() const = 0;
    virtual const LV2_Worker_Interface* getWorkerInterface() const = 0;
};

class WorkScheduler
{
public:
    explicit WorkScheduler (HandleHolder& handleHolderIn)
        : handleHolder (handleHolderIn) {}

    void processResponses() { workerThread->processResponses(); }

    LV2_Worker_Schedule& getWorkerSchedule() { return schedule; }

    void setNonRealtime (bool nonRealtime) { realtime = ! nonRealtime; }

    void registerHandle   (LV2_Handle handle) { workerThread->registerHandle   (handle); }
    void deregisterHandle (LV2_Handle handle) { workerThread->deregisterHandle (handle); }

private:
    LV2_Worker_Status scheduleWork (uint32_t size, const void* data)
    {
        WorkSubmitter submitter { handleHolder.getHandle(),
                                  handleHolder.getWorkerInterface(),
                                  workerThread,
                                  &workMutex };

        // If we're in realtime mode, the work should go onto a background thread,
        // and we'll process it later.
        // If we're offline, we can just do the work immediately, without worrying about
        // drop-outs
        return realtime ? workerThread->schedule (submitter, size, data)
                        : submitter.doWork (Realtime::no, size, data);
    }

    static LV2_Worker_Status scheduleWork (LV2_Worker_Schedule_Handle handle,
                                           uint32_t size,
                                           const void* data)
    {
        return static_cast<WorkScheduler*> (handle)->scheduleWork (size, data);
    }

    SharedResourcePointer<SharedThreadedWorker> workerThread;
    HandleHolder& handleHolder;
    LV2_Worker_Schedule schedule { this, scheduleWork };
    CriticalSection workMutex;
    bool realtime = true;

    JUCE_LEAK_DETECTOR (WorkScheduler)
};

struct FeaturesDataListener
{
    virtual ~FeaturesDataListener() = default;
    virtual LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) = 0;
};

class Resize
{
public:
    explicit Resize (FeaturesDataListener& l)
        : listener (l) {}

    LV2_Resize_Port_Resize& getFeature() { return resize; }

private:
    LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size)
    {
        return listener.resizeCallback (index, size);
    }

    static LV2_Resize_Port_Status resizeCallback (LV2_Resize_Port_Feature_Data data, uint32_t index, size_t size)
    {
        return static_cast<Resize*> (data)->resizeCallback (index, size);
    }

    FeaturesDataListener& listener;
    LV2_Resize_Port_Resize resize { this, resizeCallback };
};

class FeaturesData
{
public:
    FeaturesData (HandleHolder& handleHolder,
                  FeaturesDataListener& l,
                  int32_t maxBlockSizeIn,
                  int32_t sequenceSizeIn,
                  const UsefulUrids* u)
        : urids (u),
          resize (l),
          maxBlockSize (maxBlockSizeIn),
          sequenceSize (sequenceSizeIn),
          workScheduler (handleHolder)
    {}

    LV2_Options_Option* getOptions() noexcept { return options.data(); }

    int32_t getMaxBlockSize() const noexcept { return maxBlockSize; }

    void setNonRealtime (bool newValue) { realtime = ! newValue; }

    const LV2_Feature* const* getFeatureArray() const noexcept { return features.pointers.data(); }

    static std::vector<String> getFeatureUris()
    {
        return Features::getUris (makeFeatures ({}, {}, {}, {}, {}, {}));
    }

    void processResponses() { workScheduler.processResponses(); }

    void registerHandle   (LV2_Handle handle) { workScheduler.registerHandle   (handle); }
    void deregisterHandle (LV2_Handle handle) { workScheduler.deregisterHandle (handle); }

private:
    static std::vector<LV2_Feature> makeFeatures (LV2_URID_Map* map,
                                                  LV2_URID_Unmap* unmap,
                                                  LV2_Options_Option* options,
                                                  LV2_Worker_Schedule* schedule,
                                                  LV2_Resize_Port_Resize* resize,
                                                  [[maybe_unused]] LV2_Log_Log* log)
    {
        return { LV2_Feature { LV2_STATE__loadDefaultState,         nullptr },
                 LV2_Feature { LV2_BUF_SIZE__boundedBlockLength,    nullptr },
                 LV2_Feature { LV2_URID__map,                       map },
                 LV2_Feature { LV2_URID__unmap,                     unmap },
                 LV2_Feature { LV2_OPTIONS__options,                options },
                 LV2_Feature { LV2_WORKER__schedule,                schedule },
                 LV2_Feature { LV2_STATE__threadSafeRestore,        nullptr },
                #if JUCE_DEBUG
                 LV2_Feature { LV2_LOG__log,                        log },
                #endif
                 LV2_Feature { LV2_RESIZE_PORT__resize,             resize } };
    }

    LV2_Options_Option makeOption (const char* uid, const int32_t* ptr)
    {
        return { LV2_OPTIONS_INSTANCE,
                 0,                         // INSTANCE kinds must have a subject of 0
                 urids->symap.map (uid),
                 sizeof (int32_t),
                 urids->symap.map (LV2_ATOM__Int),
                 ptr };
    }

    const UsefulUrids* urids;
    Resize resize;
    Log log { urids };

    const int32_t minBlockSize = 0, maxBlockSize = 0, sequenceSize = 0;

    std::vector<LV2_Options_Option> options
    {
        makeOption (LV2_BUF_SIZE__minBlockLength, &minBlockSize),
        makeOption (LV2_BUF_SIZE__maxBlockLength, &maxBlockSize),
        makeOption (LV2_BUF_SIZE__sequenceSize,   &sequenceSize),
        { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr }, // The final entry must be nulled out
    };

    WorkScheduler workScheduler;

    LV2_URID_Map        map       = urids->symap.getMapFeature();
    LV2_URID_Unmap      unmap     = urids->symap.getUnmapFeature();
    Features features { makeFeatures (&map,
                                      &unmap,
                                      options.data(),
                                      &workScheduler.getWorkerSchedule(),
                                      &resize.getFeature(),
                                      log.getLogFeature()) };

    bool realtime = true;

    JUCE_LEAK_DETECTOR (FeaturesData)
};

//==============================================================================
struct TryLockAndCall
{
    template <typename Fn>
    void operator() (SpinLock& mutex, Fn&& fn)
    {
        const SpinLock::ScopedTryLockType lock (mutex);

        if (lock.isLocked())
            fn();
    }
};

struct LockAndCall
{
    template <typename Fn>
    void operator() (SpinLock& mutex, Fn&& fn)
    {
        const SpinLock::ScopedLockType lock (mutex);
        fn();
    }
};

struct RealtimeReadTrait
{
    using Read  = TryLockAndCall;
    using Write = LockAndCall;
};

struct RealtimeWriteTrait
{
    using Read  = LockAndCall;
    using Write = TryLockAndCall;
};

struct MessageHeader
{
    uint32_t portIndex;
    uint32_t protocol;
};

template <typename Header>
struct MessageBufferInterface
{
    virtual ~MessageBufferInterface() = default;
    virtual void pushMessage (Header header, uint32_t size, const void* buffer) = 0;
};

template <typename Header, typename LockTraits>
class Messages final : public MessageBufferInterface<Header>
{
    using Read  = typename LockTraits::Read;
    using Write = typename LockTraits::Write;

    struct FullHeader
    {
        Header header;
        uint32_t size;
    };

public:
    Messages() { data.reserve (initialBufferSize); }

    void pushMessage (Header header, uint32_t size, const void* buffer) override
    {
        Write{} (mutex, [&]
        {
            const auto chars = toChars (FullHeader { header, size });
            const auto bufferAsChars = static_cast<const char*> (buffer);
            data.insert (data.end(), chars.begin(), chars.end());
            data.insert (data.end(), bufferAsChars, bufferAsChars + size);
        });
    }

    template <typename Callback>
    void readAllAndClear (Callback&& callback)
    {
        Read{} (mutex, [&]
        {
            if (data.empty())
                return;

            const auto end = data.data() + data.size();

            for (auto ptr = data.data(); ptr < end;)
            {
                const auto header = readUnaligned<FullHeader> (ptr);
                callback (header.header, header.size, ptr + sizeof (header));
                ptr += sizeof (header) + header.size;
            }

            data.clear();
        });
    }

private:
    static constexpr auto initialBufferSize = 8192;
    SpinLock mutex;
    std::vector<char> data;

    JUCE_LEAK_DETECTOR (Messages)
};

//==============================================================================
struct UiEventListener : public MessageBufferInterface<MessageHeader>
{
    virtual int idle() = 0;
};

struct UiMessageHeader
{
    UiEventListener* listener;
    MessageHeader header;
};

class ProcessorToUi final : public MessageBufferInterface<UiMessageHeader>
{
public:
    ProcessorToUi() { timer.startTimerHz (60); }

    void addUi    (UiEventListener& l)      { JUCE_ASSERT_MESSAGE_THREAD; activeUis.insert (&l); }
    void removeUi (UiEventListener& l)      { JUCE_ASSERT_MESSAGE_THREAD; activeUis.erase (&l); }

    void pushMessage (UiMessageHeader header, uint32_t size, const void* buffer) override
    {
        processorToUi.pushMessage (header, size, buffer);
    }

private:
    Messages<UiMessageHeader, RealtimeWriteTrait> processorToUi;
    std::set<UiEventListener*> activeUis;
    TimedCallback timer { [this]
    {
        for (auto* l : activeUis)
            if (l->idle() != 0)
                return;

        processorToUi.readAllAndClear ([&] (const UiMessageHeader& header, uint32_t size, const char* data)
        {
            if (activeUis.find (header.listener) != activeUis.cend())
                header.listener->pushMessage (header.header, size, data);
        });
    } };
};

/*  These type identifiers may be used to check the type of the incoming data. */
struct StatefulPortUrids
{
    explicit StatefulPortUrids (SymbolMap& map)
        : Float  (map.map (LV2_ATOM__Float)),
          Double (map.map (LV2_ATOM__Double)),
          Int    (map.map (LV2_ATOM__Int)),
          Long   (map.map (LV2_ATOM__Long))
    {}

    const LV2_URID Float, Double, Int, Long;
};

/*
    A bit like SortedSet, but only requires `operator<` and not `operator==`, so
    it behaves a bit more like a std::set.
*/
template <typename Value>
class SafeSortedSet
{
public:
    using       iterator = typename std::vector<Value>::      iterator;
    using const_iterator = typename std::vector<Value>::const_iterator;

    template <typename Other>
    const_iterator find (const Other& other) const noexcept
    {
        const auto it = std::lower_bound (storage.cbegin(), storage.cend(), other);

        if (it != storage.cend() && ! (other < *it))
            return it;

        return storage.cend();
    }

    void insert (Value&& value)      { insertImpl (std::move (value)); }
    void insert (const Value& value) { insertImpl (value); }

    size_t size()  const noexcept { return storage.size(); }
    bool   empty() const noexcept { return storage.empty(); }

    iterator        begin()       noexcept { return storage. begin(); }
    const_iterator  begin() const noexcept { return storage. begin(); }
    const_iterator cbegin() const noexcept { return storage.cbegin(); }

    iterator          end()       noexcept { return storage. end(); }
    const_iterator    end() const noexcept { return storage. end(); }
    const_iterator   cend() const noexcept { return storage.cend(); }

    auto& operator[] (size_t index) const { return storage[index]; }

private:
    template <typename Arg>
    void insertImpl (Arg&& value)
    {
        const auto it = std::lower_bound (storage.cbegin(), storage.cend(), value);

        if (it == storage.cend() || value < *it)
            storage.insert (it, std::forward<Arg> (value));
    }

    std::vector<Value> storage;
};

struct StoredScalePoint
{
    String label;
    float value;

    bool operator< (const StoredScalePoint& other) const noexcept { return value < other.value; }
};

inline bool operator< (const StoredScalePoint& a, float b) noexcept { return a.value < b; }
inline bool operator< (float a, const StoredScalePoint& b) noexcept { return a < b.value; }

struct ParameterInfo
{
    ParameterInfo() = default;

    ParameterInfo (SafeSortedSet<StoredScalePoint> scalePointsIn,
                   String identifierIn,
                   float defaultValueIn,
                   float minIn,
                   float maxIn,
                   bool isToggleIn,
                   bool isIntegerIn,
                   bool isEnumIn)
        : scalePoints (std::move (scalePointsIn)),
          identifier (std::move (identifierIn)),
          defaultValue (defaultValueIn),
          min (minIn),
          max (maxIn),
          isToggle (isToggleIn),
          isInteger (isIntegerIn),
          isEnum (isEnumIn)
    {}

    static SafeSortedSet<StoredScalePoint> getScalePoints (const Port& port)
    {
        SafeSortedSet<StoredScalePoint> scalePoints;

        for (const LilvScalePoint* p : port.getScalePoints())
        {
            const ScalePoint wrapper { p };
            const auto value = wrapper.getValue();
            const auto label = wrapper.getLabel();

            if (lilv_node_is_float (value) || lilv_node_is_int (value))
                scalePoints.insert ({ lilv_node_as_string (label), lilv_node_as_float (value) });
        }

        return scalePoints;
    }

    static ParameterInfo getInfoForPort (const UsefulUris& uris, const Port& port)
    {
        const auto range = port.getRange();

        return { getScalePoints (port),
                 "sym:" + String::fromUTF8 (port.getSymbol().getTyped()),
                 range.defaultValue,
                 range.min,
                 range.max,
                 port.hasProperty (uris.mLV2_CORE__toggled),
                 port.hasProperty (uris.mLV2_CORE__integer),
                 port.hasProperty (uris.mLV2_CORE__enumeration) };
    }

    SafeSortedSet<StoredScalePoint> scalePoints;

    /*  This is the 'symbol' of a port, or the 'designation' of a parameter without a symbol. */
    String identifier;

    float defaultValue = 0.0f, min = 0.0f, max = 1.0f;
    bool isToggle = false, isInteger = false, isEnum = false;

    JUCE_LEAK_DETECTOR (ParameterInfo)
};

struct PortHeader
{
    String name;
    String symbol;
    uint32_t index;
    Port::Direction direction;
};

struct ControlPort
{
    ControlPort (const PortHeader& headerIn, const ParameterInfo& infoIn)
        : header (headerIn), info (infoIn) {}

    PortHeader header;
    ParameterInfo info;
    float currentValue = info.defaultValue;
};

struct CVPort
{
    PortHeader header;
};

struct AudioPort
{
    PortHeader header;
};

template <size_t Alignment>
class SingleSizeAlignedStorage
{
public:
    SingleSizeAlignedStorage() = default;

    explicit SingleSizeAlignedStorage (size_t sizeInBytes)
        : storage (new char[sizeInBytes + Alignment]),
          alignedPointer (storage.get()),
          space (sizeInBytes + Alignment)
    {
        alignedPointer = std::align (Alignment, sizeInBytes, alignedPointer, space);
    }

    void*  data() const     { return alignedPointer; }
    size_t size() const     { return space; }

private:
    std::unique_ptr<char[]> storage;
    void* alignedPointer = nullptr;
    size_t space = 0;
};

template <size_t Alignment>
static SingleSizeAlignedStorage<Alignment> grow (SingleSizeAlignedStorage<Alignment> storage, size_t size)
{
    if (size <= storage.size())
        return storage;

    SingleSizeAlignedStorage<Alignment> newStorage { jmax (size, (storage.size() * 3) / 2) };
    std::memcpy (newStorage.data(), storage.data(), storage.size());
    return newStorage;
}

enum class SupportsTime { no, yes };

class AtomPort
{
public:
    AtomPort (PortHeader h, size_t bytes, SymbolMap& map, SupportsTime supportsTime)
        : header (h), contents (bytes), forge (map.getMapFeature()), time (supportsTime) {}

    PortHeader header;

    void replaceWithChunk()
    {
        forge.setBuffer (data(), size());
        forge.writeChunk ((uint32_t) (size() - sizeof (LV2_Atom)));
    }

    void replaceBufferWithAtom (const LV2_Atom* atom)
    {
        const auto totalSize = atom->size + sizeof (LV2_Atom);

        if (totalSize <= size())
            std::memcpy (data(), atom, totalSize);
        else
            replaceWithChunk();
    }

    void beginSequence()
    {
        forge.setBuffer (data(), size());
        lv2_atom_forge_sequence_head (forge.get(), &frame, 0);
    }

    void endSequence()
    {
        lv2_atom_forge_pop (forge.get(), &frame);
    }

    /*  For this to work, the 'atom' pointer must be well-formed.

        It must be followed by an atom header, then at least 'size' bytes of body.
    */
    void addAtomToSequence (int64_t timestamp, const LV2_Atom* atom)
    {
        // This reinterpret_cast is not UB, casting to a char* is acceptable.
        // Doing arithmetic on this pointer is dubious, but I can't think of a better alternative
        // given that we don't have any way of knowing the concrete type of the atom.
        addEventToSequence (timestamp,
                            atom->type,
                            atom->size,
                            reinterpret_cast<const char*> (atom) + sizeof (LV2_Atom));
    }

    void addEventToSequence (int64_t timestamp, uint32_t type, uint32_t size, const void* content)
    {
        lv2_atom_forge_frame_time (forge.get(), timestamp);
        lv2_atom_forge_atom (forge.get(), size, type);
        lv2_atom_forge_write (forge.get(), content, size);
    }

    void ensureSizeInBytes (size_t size)
    {
        contents = grow (std::move (contents), size);
    }

          char* data()       noexcept { return data (*this); }
    const char* data() const noexcept { return data (*this); }

    size_t size() const noexcept { return contents.size(); }

          lv2_shared::AtomForge& getForge()       { return forge; }
    const lv2_shared::AtomForge& getForge() const { return forge; }

    bool getSupportsTime() const { return time == SupportsTime::yes; }

private:
    template <typename This>
    static auto data (This& t) -> decltype (t.data())
    {
        return unalignedPointerCast<decltype (t.data())> (t.contents.data());
    }

    // Atoms are required to be 64-bit aligned
    SingleSizeAlignedStorage<8> contents;
    lv2_shared::AtomForge forge;
    LV2_Atom_Forge_Frame frame;
    SupportsTime time = SupportsTime::no;
};

struct FreeString { void operator() (void* ptr) const noexcept { lilv_free (ptr); } };

static File bundlePathFromUri (const char* uri)
{
    return File { std::unique_ptr<char, FreeString> { lilv_file_uri_parse (uri, nullptr) }.get() };
}

class Plugins
{
public:
    explicit Plugins (const LilvPlugins* list) noexcept : plugins (list) {}

    unsigned size() const noexcept { return lilv_plugins_size (plugins); }

    PluginsIterator begin() const noexcept { return PluginsIterator { plugins }; }
    PluginsIterator end()   const noexcept { return PluginsIterator{}; }

    const LilvPlugin* getByUri (const NodeUri& uri) const
    {
        return lilv_plugins_get_by_uri (plugins, uri.get());
    }

    void getByFile (const File& file, std::vector<const LilvPlugin*>& result) const
    {
        for (const auto* plugin : *this)
        {
            if (bundlePathFromUri (lilv_node_as_uri (lilv_plugin_get_bundle_uri (plugin))) == file)
                result.push_back (plugin);
        }
    }

private:
    const LilvPlugins* plugins = nullptr;
};

template <typename PtrTraits>
class PluginClassesImpl
{
public:
    using type = typename PtrTraits::type;

    explicit PluginClassesImpl (type ptr)
            : classes (std::move (ptr)) {}

    unsigned size() const noexcept { return lilv_plugin_classes_size (PtrTraits::get (classes)); }

    PluginClassesIterator begin() const noexcept { return PluginClassesIterator { PtrTraits::get (classes) }; }
    PluginClassesIterator end()   const noexcept { return PluginClassesIterator{}; }

    const LilvPluginClass* getByUri (const NodeUri& uri) const noexcept
    {
        return lilv_plugin_classes_get_by_uri (PtrTraits::get (classes), uri.get());
    }

private:
    type classes{};
};

struct PluginClassesFree
{
    void operator() (LilvPluginClasses* ptr) const noexcept { lilv_plugin_classes_free (ptr); }
};

using OwningPluginClasses    = PluginClassesImpl<OwningPtrTraits<LilvPluginClasses, PluginClassesFree>>;
using NonOwningPluginClasses = PluginClassesImpl<NonOwningPtrTraits<LilvPluginClasses>>;

class World
{
public:
    World() : world (lilv_world_new()) {}

    void loadAllFromPaths (const NodeString& paths)
    {
        lilv_world_set_option (world.get(), LILV_OPTION_LV2_PATH, paths.get());
        lilv_world_load_all (world.get());
    }

    void loadBundle   (const NodeUri& uri)      { lilv_world_load_bundle   (world.get(), uri.get()); }
    void unloadBundle (const NodeUri& uri)      { lilv_world_unload_bundle (world.get(), uri.get()); }

    void loadResource   (const NodeUri& uri)    { lilv_world_load_resource   (world.get(), uri.get()); }
    void unloadResource (const NodeUri& uri)    { lilv_world_unload_resource (world.get(), uri.get()); }

    void loadSpecifications() { lilv_world_load_specifications (world.get()); }
    void loadPluginClasses()  { lilv_world_load_plugin_classes (world.get()); }

    Plugins getAllPlugins()                   const { return Plugins { lilv_world_get_all_plugins (world.get()) }; }
    NonOwningPluginClasses getPluginClasses() const { return NonOwningPluginClasses { lilv_world_get_plugin_classes (world.get()) }; }

    NodeUri newUri (const char* uri)                        { return NodeUri    { world.get(), uri }; }
    NodeUri newFileUri (const char* host, const char* path) { return NodeUri    { world.get(), host, path }; }
    NodeString newString (const char* str)                  { return NodeString { world.get(), str }; }

    bool ask (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const
    {
        return lilv_world_ask (world.get(), subject, predicate, object);
    }

    OwningNode get (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const
    {
        return OwningNode { lilv_world_get (world.get(), subject, predicate, object) };
    }

    OwningNodes findNodes (const LilvNode* subject, const LilvNode* predicate, const LilvNode* object) const
    {
        return OwningNodes { lilv_world_find_nodes (world.get(), subject, predicate, object) };
    }

    LilvWorld* get() const { return world.get(); }

private:
    struct Free
    {
        void operator() (LilvWorld* ptr) const noexcept { lilv_world_free (ptr); }
    };

    std::unique_ptr<LilvWorld, Free> world;
};

class Ports
{
public:
    static constexpr auto sequenceSize = 8192;

    template <typename Callback>
    void forEachPort (Callback&& callback) const
    {
        for (const auto& port : controlPorts)
            callback (port.header);

        for (const auto& port : cvPorts)
            callback (port.header);

        for (const auto& port : audioPorts)
            callback (port.header);

        for (const auto& port : atomPorts)
            callback (port.header);
    }

    auto getControlPorts()       { return makeSimpleSpan (controlPorts); }
    auto getControlPorts() const { return makeSimpleSpan (controlPorts); }
    auto getCvPorts()            { return makeSimpleSpan (cvPorts); }
    auto getCvPorts()      const { return makeSimpleSpan (cvPorts); }
    auto getAudioPorts()         { return makeSimpleSpan (audioPorts); }
    auto getAudioPorts()   const { return makeSimpleSpan (audioPorts); }
    auto getAtomPorts()          { return makeSimpleSpan (atomPorts); }
    auto getAtomPorts()    const { return makeSimpleSpan (atomPorts); }

    static Optional<Ports> getPorts (World& world, const UsefulUris& uris, const Plugin& plugin, SymbolMap& symap)
    {
        Ports value;
        bool successful = true;

        const auto numPorts = plugin.getNumPorts();
        const auto timeNode = world.newUri (LV2_TIME__Position);

        for (uint32_t i = 0; i != numPorts; ++i)
        {
            const auto port = plugin.getPortByIndex (i);

            const PortHeader header { String::fromUTF8 (port.getName().getTyped()),
                                      String::fromUTF8 (port.getSymbol().getTyped()),
                                      i,
                                      port.getDirection (uris) };

            switch (port.getKind (uris))
            {
                case Port::Kind::control:
                {
                    value.controlPorts.push_back ({ header, ParameterInfo::getInfoForPort (uris, port) });
                    break;
                }

                case Port::Kind::cv:
                    value.cvPorts.push_back ({ header });
                    break;

                case Port::Kind::audio:
                {
                    value.audioPorts.push_back ({ header });
                    break;
                }

                case Port::Kind::atom:
                {
                    const auto supportsTime = port.supportsEvent (timeNode.get());
                    value.atomPorts.push_back ({ header,
                                                 (size_t) Ports::sequenceSize,
                                                 symap,
                                                 supportsTime ? SupportsTime::yes : SupportsTime::no });
                    break;
                }

                case Port::Kind::unknown:
                    successful = false;
                    break;
            }
        }

        for (auto& atomPort : value.atomPorts)
        {
            const auto port    = plugin.getPortByIndex (atomPort.header.index);
            const auto minSize = port.get (uris.mLV2_RESIZE_PORT__minimumSize.get());

            if (minSize != nullptr)
                atomPort.ensureSizeInBytes ((size_t) lilv_node_as_int (minSize.get()));
        }

        return successful ? makeOptional (std::move (value)) : nullopt;
    }

private:
    std::vector<ControlPort> controlPorts;
    std::vector<CVPort> cvPorts;
    std::vector<AudioPort> audioPorts;
    std::vector<AtomPort> atomPorts;
};

class InstanceWithSupports final : private FeaturesDataListener,
                                   private HandleHolder
{
public:
    InstanceWithSupports (World& world,
                          std::unique_ptr<SymbolMap>&& symapIn,
                          const Plugin& plugin,
                          Ports portsIn,
                          int32_t initialBufferSize,
                          double sampleRate)
        : symap (std::move (symapIn)),
          ports (std::move (portsIn)),
          features (*this, *this, initialBufferSize, lv2_host::Ports::sequenceSize, &urids),
          instance (plugin, sampleRate, features.getFeatureArray()),
          workerInterface (instance.getExtensionData<LV2_Worker_Interface> (world.newUri (LV2_WORKER__interface)))
    {
        if (instance == nullptr)
            return;

        for (auto& port : ports.getControlPorts())
            instance.connectPort (port.header.index, &port.currentValue);

        for (auto& port : ports.getAtomPorts())
            instance.connectPort (port.header.index, port.data());

        for (auto& port : ports.getCvPorts())
            instance.connectPort (port.header.index, nullptr);

        for (auto& port : ports.getAudioPorts())
            instance.connectPort (port.header.index, nullptr);

        features.registerHandle (instance.getHandle());
    }

    ~InstanceWithSupports() override
    {
        if (instance != nullptr)
            features.deregisterHandle (instance.getHandle());
    }

    std::unique_ptr<SymbolMap> symap;
    const UsefulUrids urids { *symap };
    Ports ports;
    FeaturesData features;
    Instance instance;
    Messages<MessageHeader, RealtimeReadTrait> uiToProcessor;

private:
    LV2_Handle handle = instance == nullptr ? nullptr : instance.getHandle();
    OptionalExtension<LV2_Worker_Interface> workerInterface;

    LV2_Handle getHandle() const override { return handle; }
    const LV2_Worker_Interface* getWorkerInterface() const override { return workerInterface.valid ? &workerInterface.extension : nullptr; }

    LV2_Resize_Port_Status resizeCallback (uint32_t index, size_t size) override
    {
        if (ports.getAtomPorts().size() <= index)
            return LV2_RESIZE_PORT_ERR_UNKNOWN;

        auto& port = ports.getAtomPorts()[index];

        if (port.header.direction != Port::Direction::output)
            return LV2_RESIZE_PORT_ERR_UNKNOWN;

        port.ensureSizeInBytes (size);
        instance.connectPort (port.header.index, port.data());

        return LV2_RESIZE_PORT_SUCCESS;
    }

    JUCE_DECLARE_NON_COPYABLE (InstanceWithSupports)
    JUCE_DECLARE_NON_MOVEABLE (InstanceWithSupports)
    JUCE_LEAK_DETECTOR (InstanceWithSupports)
};

struct PortState
{
    const void* data;
    uint32_t size;
    uint32_t kind;
};

class PortMap
{
public:
    explicit PortMap (Ports& ports)
    {
        for (auto& port : ports.getControlPorts())
            symbolToControlPortMap.emplace (port.header.symbol, &port);
    }

    PortState getState (const String& symbol, const StatefulPortUrids& urids)
    {
        if (auto* port = getControlPortForSymbol (symbol))
            return { &port->currentValue, sizeof (float), urids.Float };

        // At time of writing, lilv_state_new_from_instance did not attempt to store
        // the state of non-control ports. Perhaps that has changed?
        jassertfalse;
        return { nullptr, 0, 0 };
    }

    void restoreState (const String& symbol, const StatefulPortUrids& urids, PortState ps)
    {
        if (auto* port = getControlPortForSymbol (symbol))
        {
            port->currentValue = [&]() -> float
            {
                if (ps.kind == urids.Float)
                    return getValueFrom<float> (ps.data, ps.size);

                if (ps.kind == urids.Double)
                    return getValueFrom<double> (ps.data, ps.size);

                if (ps.kind == urids.Int)
                    return getValueFrom<int32_t> (ps.data, ps.size);

                if (ps.kind == urids.Long)
                    return getValueFrom<int64_t> (ps.data, ps.size);

                jassertfalse;
                return {};
            }();
        }
        else
            jassertfalse; // Restoring state for non-control ports is not currently supported.
    }

private:
    template <typename Value>
    static float getValueFrom (const void* data, [[maybe_unused]] uint32_t size)
    {
        jassert (size == sizeof (Value));
        return (float) readUnaligned<Value> (data);
    }

    ControlPort* getControlPortForSymbol (const String& symbol) const
    {
        const auto iter = symbolToControlPortMap.find (symbol);
        return iter != symbolToControlPortMap.cend() ? iter->second : nullptr;
    }

    std::map<String, ControlPort*> symbolToControlPortMap;
    JUCE_LEAK_DETECTOR (PortMap)
};

class PluginState
{
public:
    PluginState() = default;

    explicit PluginState (LilvState* ptr)
        : state (ptr) {}

    const LilvState* get() const noexcept { return state.get(); }

    void restore (InstanceWithSupports& instance, PortMap& portMap) const
    {
        if (state != nullptr)
            SaveRestoreHandle { instance, portMap }.restore (state.get());
    }

    std::string toString (LilvWorld* world, LV2_URID_Map* map, LV2_URID_Unmap* unmap, const char* uri) const
    {
        std::unique_ptr<char, FreeString> result { lilv_state_to_string (world,
                                                                         map,
                                                                         unmap,
                                                                         state.get(),
                                                                         uri,
                                                                         nullptr) };
        return std::string { result.get() };
    }

    String getLabel() const
    {
        return String::fromUTF8 (lilv_state_get_label (state.get()));
    }

    void setLabel (const String& label)
    {
        lilv_state_set_label (state.get(), label.toRawUTF8());
    }

    class SaveRestoreHandle
    {
    public:
        explicit SaveRestoreHandle (InstanceWithSupports& instanceIn, PortMap& portMap)
            : instance (instanceIn.instance.get()),
              features (instanceIn.features.getFeatureArray()),
              urids (*instanceIn.symap),
              map (portMap)
        {}

        PluginState save (const LilvPlugin* plugin, LV2_URID_Map* mapFeature)
        {
            return PluginState { lilv_state_new_from_instance (plugin,
                                                               instance,
                                                               mapFeature,
                                                               nullptr,
                                                               nullptr,
                                                               nullptr,
                                                               nullptr,
                                                               getPortValue,
                                                               this,
                                                               LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE,
                                                               features ) };
        }

        void restore (const LilvState* stateIn)
        {
            lilv_state_restore (stateIn,
                                instance,
                                setPortValue,
                                this,
                                0,
                                features);
        }

    private:
        static const void* getPortValue (const char* portSymbol,
                                         void* userData,
                                         uint32_t* size,
                                         uint32_t* type)
        {
            auto& handle = *static_cast<SaveRestoreHandle*> (userData);

            const auto state = handle.map.getState (portSymbol, handle.urids);
            *size = state.size;
            *type = state.kind;
            return state.data;
        }

        static void setPortValue (const char* portSymbol,
                                  void* userData,
                                  const void* value,
                                  uint32_t size,
                                  uint32_t type)
        {
            const auto& handle = *static_cast<const SaveRestoreHandle*> (userData);
            handle.map.restoreState (portSymbol, handle.urids, { static_cast<const char*> (value), size, type });
        }

        LilvInstance* instance = nullptr;
        const LV2_Feature* const* features = nullptr;
        const StatefulPortUrids urids;
        PortMap& map;
    };

private:
    struct Free
    {
        void operator() (LilvState* ptr) const noexcept { lilv_state_free (ptr); }
    };

    std::unique_ptr<LilvState, Free> state;

    JUCE_LEAK_DETECTOR (PluginState)
};

/*
    Wraps an LV2 UI bundle, providing access to the descriptor (if available).
*/
struct UiDescriptorLibrary
{
    using GetDescriptor = LV2UI_Descriptor* (*) (uint32_t);

    UiDescriptorLibrary() = default;

    explicit UiDescriptorLibrary (const String& libraryPath)
        : library (std::make_unique<DynamicLibrary> (libraryPath)),
          getDescriptor (lv2_shared::wordCast<GetDescriptor> (library->getFunction ("lv2ui_descriptor"))) {}

    std::shared_ptr<DynamicLibrary> library;
    GetDescriptor getDescriptor = nullptr;
};

class UiDescriptorArgs
{
public:
    String libraryPath;
    String uiUri;

    auto withLibraryPath (String v) const noexcept { return with (&UiDescriptorArgs::libraryPath, v); }
    auto withUiUri       (String v) const noexcept { return with (&UiDescriptorArgs::uiUri,       v); }

private:
    UiDescriptorArgs with (String UiDescriptorArgs::* member, String value) const noexcept
    {
        return juce::lv2_host::with (*this, member, std::move (value));
    }
};

/*
    Stores a pointer to the descriptor for a specific UI bundle and UI URI.
*/
class UiDescriptor
{
public:
    UiDescriptor() = default;

    explicit UiDescriptor (const UiDescriptorArgs& args)
        : library (args.libraryPath),
          descriptor (extractUiDescriptor (library, args.uiUri.toRawUTF8()))
    {}

    void portEvent (LV2UI_Handle ui,
                    uint32_t portIndex,
                    uint32_t bufferSize,
                    uint32_t format,
                    const void* buffer) const
    {
        JUCE_ASSERT_MESSAGE_THREAD

        if (auto* lv2Descriptor = get())
            if (auto* callback = lv2Descriptor->port_event)
                callback (ui, portIndex, bufferSize, format, buffer);
    }

    bool hasExtensionData (World& world, const char* uid) const
    {
        return world.ask (world.newUri (descriptor->URI).get(),
                          world.newUri (LV2_CORE__extensionData).get(),
                          world.newUri (uid).get());
    }

    template <typename Extension>
    OptionalExtension<Extension> getExtensionData (World& world, const char* uid) const
    {
        if (! hasExtensionData (world, uid))
            return {};

        if (auto* lv2Descriptor = get())
            if (auto* extension = lv2Descriptor->extension_data)
                return OptionalExtension<Extension> (readUnaligned<Extension> (extension (uid)));

        return {};
    }

    const LV2UI_Descriptor* get() const noexcept { return descriptor; }

private:
    static const LV2UI_Descriptor* extractUiDescriptor (const UiDescriptorLibrary& lib, const char* uiUri)
    {
        if (lib.getDescriptor == nullptr)
            return nullptr;

        for (uint32_t i = 0;; ++i)
        {
            const auto* descriptor = lib.getDescriptor (i);

            if (descriptor == nullptr)
                return nullptr;

            if (strcmp (uiUri, descriptor->URI) == 0)
                return descriptor;
        }
    }

    UiDescriptorLibrary library;
    const LV2UI_Descriptor* descriptor = nullptr;

    JUCE_LEAK_DETECTOR (UiDescriptor)
};

enum class Update { no, yes };

/*  A bit like the FlaggedFloatCache used by the VST3 host/client.

    While the FlaggedFloatCache always clears all set flags during the ifSet() call,
    this class stores the "value changed" flags for the processor and UI separately,
    so that they can be read at different rates.
*/
class ParameterValuesAndFlags
{
public:
    ParameterValuesAndFlags() = default;

    explicit ParameterValuesAndFlags (size_t sizeIn)
        : values (sizeIn),
          needsUiUpdate (sizeIn),
          needsProcessorUpdate (sizeIn)
    {
        std::fill (values.begin(), values.end(), 0.0f);
    }

    size_t size() const noexcept { return values.size(); }

    void set (size_t index, float value, Update update)
    {
        jassert (index < size());
        values[index].store (value, std::memory_order_relaxed);
        needsUiUpdate       .set (index, update == Update::yes ? 1 : 0);
        needsProcessorUpdate.set (index, update == Update::yes ? 1 : 0);
    }

    float get (size_t index) const noexcept
    {
        jassert (index < size());
        return values[index].load (std::memory_order_relaxed);
    }

    template <typename Callback>
    void ifProcessorValuesChanged (Callback&& callback)
    {
        ifChanged (needsProcessorUpdate, std::forward<Callback> (callback));
    }

    template <typename Callback>
    void ifUiValuesChanged (Callback&& callback)
    {
        ifChanged (needsUiUpdate, std::forward<Callback> (callback));
    }

    void clearUiFlags() { needsUiUpdate.clear(); }

private:
    template <typename Callback>
    void ifChanged (FlagCache<1>& flags, Callback&& callback)
    {
        flags.ifSet ([this, &callback] (size_t groupIndex, uint32_t)
        {
            callback (groupIndex, values[groupIndex].load (std::memory_order_relaxed));
        });
    }

    std::vector<std::atomic<float>> values;
    FlagCache<1> needsUiUpdate;
    FlagCache<1> needsProcessorUpdate;

    JUCE_LEAK_DETECTOR (ParameterValuesAndFlags)
};

class LV2Parameter : public AudioPluginInstance::HostedParameter
{
public:
    LV2Parameter (const String& nameIn,
                  const ParameterInfo& infoIn,
                  ParameterValuesAndFlags& floatCache)
        : cache (floatCache),
          info (infoIn),
          range (info.min, info.max),
          name (nameIn),
          normalisedDefault (range.convertTo0to1 (infoIn.defaultValue))
    {}

    float getValue() const noexcept override
    {
        return range.convertTo0to1 (getDenormalisedValue());
    }

    void setValue (float f) override
    {
        cache.set ((size_t) getParameterIndex(), range.convertFrom0to1 (f), Update::yes);
    }

    void setDenormalisedValue (float denormalised)
    {
        cache.set ((size_t) getParameterIndex(), denormalised, Update::yes);
        sendValueChangedMessageToListeners (range.convertTo0to1 (denormalised));
    }

    void setDenormalisedValueWithoutTriggeringUpdate (float denormalised)
    {
        cache.set ((size_t) getParameterIndex(), denormalised, Update::no);
        sendValueChangedMessageToListeners (range.convertTo0to1 (denormalised));
    }

    float getDenormalisedValue() const noexcept
    {
        return cache.get ((size_t) getParameterIndex());
    }

    float getDefaultValue() const override { return normalisedDefault; }
    float getDenormalisedDefaultValue() const { return info.defaultValue; }

    float getValueForText (const String& text) const override
    {
        if (! info.isEnum)
            return range.convertTo0to1 (text.getFloatValue());

        const auto it = std::find_if (info.scalePoints.begin(),
                                      info.scalePoints.end(),
                                      [&] (const StoredScalePoint& stored) { return stored.label == text; });
        return it != info.scalePoints.end() ? range.convertTo0to1 (it->value) : normalisedDefault;
    }

    int getNumSteps() const override
    {
        if (info.isToggle)
            return 2;

        if (info.isEnum)
            return static_cast<int> (info.scalePoints.size());

        if (info.isInteger)
            return static_cast<int> (range.getRange().getLength()) + 1;

        return AudioProcessorParameter::getNumSteps();
    }

    bool isDiscrete() const override { return info.isEnum || info.isInteger || info.isToggle; }
    bool isBoolean() const override { return info.isToggle; }

    StringArray getAllValueStrings() const override
    {
        if (! info.isEnum)
            return {};

        return AudioProcessorParameter::getAllValueStrings();
    }

    String getText (float normalisedValue, int) const override
    {
        const auto denormalised = range.convertFrom0to1 (normalisedValue);

        if (info.isEnum && ! info.scalePoints.empty())
        {
            // The normalised value might not correspond to the exact value of a scale point.
            // In this case, we find the closest label by searching the midpoints of the scale
            // point values.
            const auto index = std::distance (midPoints.begin(),
                                              std::lower_bound (midPoints.begin(), midPoints.end(), denormalised));
            jassert (isPositiveAndBelow (index, info.scalePoints.size()));
            return info.scalePoints[(size_t) index].label;
        }

        return getFallbackParameterString (denormalised);
    }

    String getParameterID() const override
    {
        return info.identifier;
    }

    String getName (int maxLength) const override
    {
        return name.substring (0, maxLength);
    }

    String getLabel() const override
    {
        // TODO
        return {};
    }

private:
    String getFallbackParameterString (float denormalised) const
    {
        if (info.isToggle)
            return denormalised > 0.0f ? "On" : "Off";

        if (info.isInteger)
            return String { static_cast<int> (denormalised) };

        return String { denormalised };
    }

    static std::vector<float> findScalePointMidPoints (const SafeSortedSet<StoredScalePoint>& set)
    {
        if (set.size() < 2)
            return {};

        std::vector<float> result;
        result.reserve (set.size() - 1);

        for (auto it = std::next (set.begin()); it != set.end(); ++it)
            result.push_back ((std::prev (it)->value + it->value) * 0.5f);

        jassert (std::is_sorted (result.begin(), result.end()));
        jassert (result.size() + 1 == set.size());
        return result;
    }

    ParameterValuesAndFlags& cache;
    const ParameterInfo info;
    const std::vector<float> midPoints = findScalePointMidPoints (info.scalePoints);
    const NormalisableRange<float> range;
    const String name;
    const float normalisedDefault;

    JUCE_LEAK_DETECTOR (LV2Parameter)
};

class UiInstanceArgs
{
public:
    File bundlePath;
    URL pluginUri;

    auto withBundlePath (File v) const noexcept { return withMember (*this, &UiInstanceArgs::bundlePath, std::move (v)); }
    auto withPluginUri  (URL v)  const noexcept { return withMember (*this, &UiInstanceArgs::pluginUri,  std::move (v)); }
};

struct TouchListener
{
    virtual ~TouchListener() = default;
    virtual void controlGrabbed (uint32_t port, bool grabbed) = 0;
};

class UiFeaturesDataOptions
{
public:
    float initialScaleFactor = 0.0f, sampleRate = 0.0f;

    auto withInitialScaleFactor (float v) const { return with (&UiFeaturesDataOptions::initialScaleFactor, v); }
    auto withSampleRate         (float v) const { return with (&UiFeaturesDataOptions::sampleRate,         v); }

private:
    UiFeaturesDataOptions with (float UiFeaturesDataOptions::* member, float value) const
    {
        return juce::lv2_host::with (*this, member, value);
    }
};

class UiFeatureUris
{
public:
    static std::vector<String> getFeatureUris()
    {
        return Features::getUris (makeFeatures ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}));
    }

    static std::vector<LV2_Feature> makeFeatures (LV2UI_Resize* resize,
                                                  LV2UI_Widget parent,
                                                  LV2_Handle handle,
                                                  LV2_Extension_Data_Feature* data,
                                                  LV2_URID_Map* map,
                                                  LV2_URID_Unmap* unmap,
                                                  LV2UI_Port_Map* portMap,
                                                  LV2UI_Touch* touch,
                                                  LV2_Options_Option* options,
                                                  LV2_Log_Log* log)
    {
        return { LV2_Feature { LV2_UI__resize,          resize },
                 LV2_Feature { LV2_UI__parent,          parent },
                 LV2_Feature { LV2_UI__idleInterface,   nullptr },
                 LV2_Feature { LV2_INSTANCE_ACCESS_URI, handle },
                 LV2_Feature { LV2_DATA_ACCESS_URI,     data },
                 LV2_Feature { LV2_URID__map,           map },
                 LV2_Feature { LV2_URID__unmap,         unmap},
                 LV2_Feature { LV2_UI__portMap,         portMap },
                 LV2_Feature { LV2_UI__touch,           touch },
                 LV2_Feature { LV2_OPTIONS__options,    options },
                 LV2_Feature { LV2_LOG__log,            log } };
    }
};

struct RequiredFeatures
{
    explicit RequiredFeatures (OwningNodes nodes)
        : values (std::move (nodes)) {}

    OwningNodes values;
};

struct OptionalFeatures
{
    explicit OptionalFeatures (OwningNodes nodes)
        : values (std::move (nodes)) {}

    OwningNodes values;
};

template <typename Range, typename Predicate>
static bool noneOf (Range&& range, Predicate&& pred)
{
    // Not a mistake, this is for ADL
    using std::begin;
    using std::end;
    return std::none_of (begin (range), end (range), std::forward<Predicate> (pred));
}

class Uis
{
public:
    explicit Uis (const LilvPlugin* plugin) noexcept : uis (lilv_plugin_get_uis (plugin)) {}

    unsigned size() const noexcept { return lilv_uis_size (uis.get()); }

    UisIterator begin() const noexcept { return UisIterator { uis.get() }; }
    UisIterator end()   const noexcept { return UisIterator{}; }

    const LilvUI* getByUri (const NodeUri& uri) const
    {
        return lilv_uis_get_by_uri (uis.get(), uri.get());
    }

private:
    struct Free
    {
        void operator() (LilvUIs* ptr) const noexcept { lilv_uis_free (ptr); }
    };

    std::unique_ptr<LilvUIs, Free> uis;
};

//==============================================================================
class PluginClass
{
public:
    explicit PluginClass (const LilvPluginClass* c) : pluginClass (c) {}

    NodeUri getParentUri() const noexcept   { return NodeUri::copy (lilv_plugin_class_get_parent_uri (pluginClass)); }
    NodeUri getUri() const noexcept         { return NodeUri::copy (lilv_plugin_class_get_uri (pluginClass)); }
    NodeString getLabel() const noexcept    { return NodeString::copy (lilv_plugin_class_get_label (pluginClass)); }
    OwningPluginClasses getChildren() const noexcept
    {
        return OwningPluginClasses { OwningPluginClasses::type { lilv_plugin_class_get_children (pluginClass) } };
    }

private:
    const LilvPluginClass* pluginClass = nullptr;
};

using FloatWriter = void (*) (LV2_Atom_Forge*, float);

struct ParameterWriterUrids
{
    LV2_URID mLV2_PATCH__Set;
    LV2_URID mLV2_PATCH__property;
    LV2_URID mLV2_PATCH__value;
    LV2_URID mLV2_ATOM__eventTransfer;
};

struct MessageHeaderAndSize
{
    MessageHeader header;
    uint32_t size;
};

class ParameterWriter
{
public:
    ParameterWriter (ControlPort* p)
        : data (PortBacking { p }), kind (Kind::port) {}

    ParameterWriter (FloatWriter write, LV2_URID urid, uint32_t controlPortIndex)
        : data (PatchBacking { write, urid, controlPortIndex }), kind (Kind::patch) {}

    void writeToProcessor (const ParameterWriterUrids urids, LV2_Atom_Forge* forge, float value) const
    {
        switch (kind)
        {
            case Kind::patch:
            {
                if (forge != nullptr)
                {
                    lv2_atom_forge_frame_time (forge, 0);
                    writeSetToForge (urids, *forge, value);
                }

                break;
            }

            case Kind::port:
                data.port.port->currentValue = value;
                break;
        }
    }

    MessageHeaderAndSize writeToUi (const ParameterWriterUrids urids, LV2_Atom_Forge& forge, float value) const
    {
        const auto getWrittenBytes = [&]() -> uint32_t
        {
            if (const auto* atom = convertToAtomPtr (forge.buf, forge.size))
                return (uint32_t) (atom->size + sizeof (LV2_Atom));

            jassertfalse;
            return 0;
        };

        switch (kind)
        {
            case Kind::patch:
                writeSetToForge (urids, forge, value);
                return { { data.patch.controlPortIndex, urids.mLV2_ATOM__eventTransfer }, getWrittenBytes() };

            case Kind::port:
                lv2_atom_forge_raw (&forge, &value, sizeof (value));
                return { { data.port.port->header.index, 0 }, sizeof (value) };
        }

        return { { 0, 0 }, 0 };
    }

    const LV2_URID* getUrid() const
    {
        return kind == Kind::patch ? &data.patch.urid : nullptr;
    }

    const uint32_t* getPortIndex() const
    {
        return kind == Kind::port ? &data.port.port->header.index : nullptr;
    }

private:
    void writeSetToForge (const ParameterWriterUrids urids, LV2_Atom_Forge& forge, float value) const
    {
        lv2_shared::ObjectFrame object { &forge, (uint32_t) 0, urids.mLV2_PATCH__Set };

        lv2_atom_forge_key (&forge, urids.mLV2_PATCH__property);
        lv2_atom_forge_urid (&forge, data.patch.urid);

        lv2_atom_forge_key (&forge, urids.mLV2_PATCH__value);
        data.patch.write (&forge, value);
    }

    struct PortBacking
    {
        ControlPort* port;
    };

    struct PatchBacking
    {
        FloatWriter write;
        LV2_URID urid;
        uint32_t controlPortIndex;
    };

    union Data
    {
        static_assert (std::is_trivial_v<PortBacking>,  "PortBacking must be trivial");
        static_assert (std::is_trivial_v<PatchBacking>, "PatchBacking must be trivial");

        explicit Data (PortBacking p)  : port  (p) {}
        explicit Data (PatchBacking p) : patch (p) {}

        PortBacking port;
        PatchBacking patch;
    };

    enum class Kind { port, patch };

    Data data;
    Kind kind;

    JUCE_LEAK_DETECTOR (ParameterWriter)
};

static String lilvNodeToUriString (const LilvNode* node)
{
    return node != nullptr ? String::fromUTF8 (lilv_node_as_uri (node)) : String{};
}

static String lilvNodeToString (const LilvNode* node)
{
    return node != nullptr ? String::fromUTF8 (lilv_node_as_string (node)) : String{};
}

/*  This holds all of the discovered groups in the plugin's manifest, and allows us to
    add parameters to these groups as we discover them.

    Once all the parameters have been added with addParameter(), you can call
    getTree() to convert this class' contents (which are optimised for fast lookup
    and modification) into a plain old AudioProcessorParameterGroup.
*/
class IntermediateParameterTree
{
public:
    explicit IntermediateParameterTree (World& worldIn)
        : world (worldIn)
    {
        const auto groups = getGroups (world);
        const auto symbolNode = world.newUri (LV2_CORE__symbol);
        const auto nameNode   = world.newUri (LV2_CORE__name);

        for (const auto& group : groups)
        {
            const auto symbol = lilvNodeToString (world.get (group.get(), symbolNode.get(), nullptr).get());
            const auto name   = lilvNodeToString (world.get (group.get(), nameNode  .get(), nullptr).get());
            owning.emplace (lilvNodeToUriString (group.get()),
                            std::make_unique<AudioProcessorParameterGroup> (symbol, name, "|"));
        }
    }

    void addParameter (StringRef group, std::unique_ptr<LV2Parameter> param)
    {
        if (param == nullptr)
            return;

        const auto it = owning.find (group);
        (it != owning.cend() ? *it->second : topLevel).addChild (std::move (param));
    }

    static AudioProcessorParameterGroup getTree (IntermediateParameterTree tree)
    {
        std::map<String, AudioProcessorParameterGroup*> nonowning;

        for (const auto& pair : tree.owning)
            nonowning.emplace (pair.first, pair.second.get());

        const auto groups = getGroups (tree.world);
        const auto subgroupNode = tree.world.newUri (LV2_PORT_GROUPS__subGroupOf);

        for (const auto& group : groups)
        {
            const auto innerIt = tree.owning.find (lilvNodeToUriString (group.get()));

            if (innerIt == tree.owning.cend())
                continue;

            const auto outer = lilvNodeToUriString (tree.world.get (group.get(), subgroupNode.get(), nullptr).get());
            const auto outerIt = nonowning.find (outer);

            if (outerIt != nonowning.cend() && containsParameters (outerIt->second))
                outerIt->second->addChild (std::move (innerIt->second));
        }

        for (auto& subgroup : tree.owning)
            if (containsParameters (subgroup.second.get()))
                tree.topLevel.addChild (std::move (subgroup.second));

        return std::move (tree.topLevel);
    }

private:
    static std::vector<OwningNode> getGroups (World& world)
    {
        std::vector<OwningNode> names;

        for (auto* uri : { LV2_PORT_GROUPS__Group, LV2_PORT_GROUPS__InputGroup, LV2_PORT_GROUPS__OutputGroup })
            for (const auto* group : world.findNodes (nullptr, world.newUri (LILV_NS_RDF "type").get(), world.newUri (uri).get()))
                names.push_back (OwningNode { lilv_node_duplicate (group) });

        return names;
    }

    static bool containsParameters (const AudioProcessorParameterGroup* g)
    {
        if (g == nullptr)
            return false;

        for (auto* node : *g)
        {
            if (node->getParameter() != nullptr)
                return true;

            if (auto* group = node->getGroup())
                if (containsParameters (group))
                    return true;
        }

        return false;
    }

    World& world;
    AudioProcessorParameterGroup topLevel;
    std::map<String, std::unique_ptr<AudioProcessorParameterGroup>> owning;

    JUCE_LEAK_DETECTOR (IntermediateParameterTree)
};

struct BypassParameter final : public LV2Parameter
{
    BypassParameter (const ParameterInfo& parameterInfo, ParameterValuesAndFlags& cacheIn)
        : LV2Parameter ("Bypass", parameterInfo, cacheIn) {}

    float getValue() const noexcept override
    {
        return LV2Parameter::getValue() > 0.0f ? 0.0f : 1.0f;
    }

    void setValue (float newValue) override
    {
        LV2Parameter::setValue (newValue > 0.0f ? 0.0f : 1.0f);
    }

    float getDefaultValue() const override                              { return 0.0f; }
    bool isAutomatable() const override                                 { return true; }
    bool isDiscrete() const override                                    { return true; }
    bool isBoolean() const override                                     { return true; }
    int getNumSteps() const override                                    { return 2; }
    StringArray getAllValueStrings() const override                     { return { TRANS ("Off"), TRANS ("On") }; }
};

struct ParameterData
{
    ParameterInfo info;
    ParameterWriter writer;
    String group;
    String name;
};

template <typename T>
static auto getPortPointers (SimpleSpan<T> range)
{
    using std::begin;
    std::vector<decltype (&(*begin (range)))> result;

    for (auto& port : range)
    {
        result.resize (std::max ((size_t) (port.header.index + 1), result.size()), nullptr);
        result[port.header.index] = &port;
    }

    return result;
}

static std::unique_ptr<LV2Parameter> makeParameter (const uint32_t* enabledPortIndex,
                                                    const ParameterData& data,
                                                    ParameterValuesAndFlags& cache)
{
    // The bypass parameter is a bit special, in that JUCE expects the parameter to be a bypass
    // (where 0 is active, 1 is inactive), but the LV2 version is called "enabled" and has
    // different semantics (0 is inactive, 1 is active).
    // To work around this, we wrap the LV2 parameter in a special inverting JUCE parameter.

    if (enabledPortIndex != nullptr)
        if (auto* index = data.writer.getPortIndex())
            if (*index == *enabledPortIndex)
                return std::make_unique<BypassParameter> (data.info, cache);

    return std::make_unique<LV2Parameter> (data.name, data.info, cache);
}

class ControlPortAccelerationStructure
{
public:
    ControlPortAccelerationStructure (SimpleSpan<ControlPort> controlPorts)
        : indexedControlPorts (getPortPointers (controlPorts))
    {
        for (const auto& port : controlPorts)
            if (port.header.direction == Port::Direction::output)
                outputPorts.push_back (&port);
    }

    const std::vector<ControlPort*>& getIndexedControlPorts() { return indexedControlPorts; }

    ControlPort* getControlPortByIndex (uint32_t index) const
    {
        if (isPositiveAndBelow (index, indexedControlPorts.size()))
            return indexedControlPorts[index];

        return nullptr;
    }

    void writeOutputPorts (UiEventListener* target, MessageBufferInterface<UiMessageHeader>& uiMessages) const
    {
        if (target == nullptr)
            return;

        for (const auto* port : outputPorts)
        {
            const auto chars = toChars (port->currentValue);
            uiMessages.pushMessage ({ target, { port->header.index, 0 } }, (uint32_t) chars.size(), chars.data());
        }
    }

private:
    std::vector<ControlPort*> indexedControlPorts;
    std::vector<const ControlPort*> outputPorts;
};

class ParameterValueCache
{
public:
    /*  This takes some information about all the parameters that this plugin wants to expose,
        then builds and installs the actual parameters.
    */
    ParameterValueCache (AudioPluginInstance& processor,
                         World& world,
                         LV2_URID_Map mapFeature,
                         const std::vector<ParameterData>& data,
                         ControlPort* enabledPort)
        : uiForge (mapFeature),
          cache (data.size())
    {
        // Parameter indices are unknown until we add the parameters to the processor.
        // This map lets us keep track of which ParameterWriter corresponds to each parameter.
        // After the parameters have been added to the processor, we'll convert this
        // to a simple vector that stores each ParameterWriter at the same index
        // as the corresponding parameter.
        std::map<AudioProcessorParameter*, ParameterWriter> writerForParameter;

        IntermediateParameterTree tree { world };

        const auto* enabledPortIndex = enabledPort != nullptr ? &enabledPort->header.index
                                                              : nullptr;

        for (const auto& item : data)
        {
            auto param = makeParameter (enabledPortIndex, item, cache);

            if (auto* urid = item.writer.getUrid())
                urids.emplace (*urid, param.get());

            if (auto* index = item.writer.getPortIndex())
                portIndices.emplace (*index, param.get());

            writerForParameter.emplace (param.get(), item.writer);

            tree.addParameter (item.group, std::move (param));
        }

        processor.setHostedParameterTree (IntermediateParameterTree::getTree (std::move (tree)));

        // Build the vector of writers
        writers.reserve (data.size());

        for (auto* param : processor.getParameters())
        {
            const auto it = writerForParameter.find (param);
            jassert (it != writerForParameter.end());
            writers.push_back (it->second); // The writer must exist at the same index as the parameter!
        }

        // Duplicate port indices or urids?
        jassert (processor.getParameters().size() == (int) (urids.size() + portIndices.size()));

        // Set parameters to default values
        const auto setToDefault = [] (auto& container)
        {
            for (auto& item : container)
                item.second->setDenormalisedValueWithoutTriggeringUpdate (item.second->getDenormalisedDefaultValue());
        };

        setToDefault (urids);
        setToDefault (portIndices);
    }

    void postChangedParametersToProcessor (const ParameterWriterUrids helperUrids,
                                           LV2_Atom_Forge* forge)
    {
        cache.ifProcessorValuesChanged ([&] (size_t index, float value)
                                        {
                                            writers[index].writeToProcessor (helperUrids, forge, value);
                                        });
    }

    void postChangedParametersToUi (UiEventListener* target,
                                    const ParameterWriterUrids helperUrids,
                                    MessageBufferInterface<UiMessageHeader>& uiMessages)
    {
        if (target == nullptr)
            return;

        cache.ifUiValuesChanged ([&] (size_t index, float value)
                                 {
                                     writeParameterToUi (target, writers[index], value, helperUrids, uiMessages);
                                 });
    }

    void postAllParametersToUi (UiEventListener* target,
                                const ParameterWriterUrids helperUrids,
                                MessageBufferInterface<UiMessageHeader>& uiMessages)
    {
        if (target == nullptr)
            return;

        const auto numWriters = writers.size();

        for (size_t i = 0; i < numWriters; ++i)
            writeParameterToUi (target, writers[i], cache.get (i), helperUrids, uiMessages);

        cache.clearUiFlags();
    }

    LV2Parameter* getParamByUrid (LV2_URID urid) const
    {
        const auto it = urids.find (urid);
        return it != urids.end() ? it->second : nullptr;
    }

    LV2Parameter* getParamByPortIndex (uint32_t portIndex) const
    {
        const auto it = portIndices.find (portIndex);
        return it != portIndices.end() ? it->second : nullptr;
    }

    void updateFromControlPorts (const ControlPortAccelerationStructure& ports) const
    {
        for (const auto& pair : portIndices)
            if (auto* port = ports.getControlPortByIndex (pair.first))
                if (auto* param = pair.second)
                    param->setDenormalisedValueWithoutTriggeringUpdate (port->currentValue);
    }

private:
    void writeParameterToUi (UiEventListener* target,
                             const ParameterWriter& writer,
                             float value,
                             const ParameterWriterUrids helperUrids,
                             MessageBufferInterface<UiMessageHeader>& uiMessages)
    {
        JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED

        uiForge.setBuffer (forgeStorage.data(), forgeStorage.size());
        const auto messageHeader = writer.writeToUi (helperUrids, *uiForge.get(), value);
        uiMessages.pushMessage ({ target, messageHeader.header }, messageHeader.size, forgeStorage.data());
    }

    SingleSizeAlignedStorage<8> forgeStorage { 256 };
    lv2_shared::AtomForge uiForge;

    ParameterValuesAndFlags cache;
    std::vector<ParameterWriter> writers;
    std::map<LV2_URID, LV2Parameter*> urids;
    std::map<uint32_t, LV2Parameter*> portIndices;

    JUCE_LEAK_DETECTOR (ParameterValueCache)
};

struct PatchSetCallback
{
    explicit PatchSetCallback (ParameterValueCache& x) : cache (x) {}

    // If we receive a patch set from the processor, we can assume that the UI will
    // put itself into the correct state when it receives the message.
    void setParameter (LV2_URID property, float value) const noexcept
    {
        if (auto* param = cache.getParamByUrid (property))
            param->setDenormalisedValueWithoutTriggeringUpdate (value);
    }

    // TODO gesture support will probably go here, once it's part of the LV2 spec

    ParameterValueCache& cache;
};

struct SupportedParameter
{
    ParameterInfo info;
    bool supported;
    LV2_URID type;
};

static SupportedParameter getInfoForPatchParameter (World& worldIn,
                                                    const UsefulUrids& urids,
                                                    const NodeUri& property)
{
    const auto rangeUri = worldIn.newUri (LILV_NS_RDFS "range");
    const auto type = worldIn.get (property.get(), rangeUri.get(), nullptr);

    if (type == nullptr)
        return { {}, false, {} };

    const auto typeUrid = urids.symap.map (lilv_node_as_uri (type.get()));

    const LV2_URID types[] { urids.mLV2_ATOM__Int,
                             urids.mLV2_ATOM__Long,
                             urids.mLV2_ATOM__Float,
                             urids.mLV2_ATOM__Double,
                             urids.mLV2_ATOM__Bool };

    if (std::find (std::begin (types), std::end (types), typeUrid) == std::end (types))
        return { {}, false, {} };

    const auto getValue = [&] (const char* uri, float fallback)
    {
        return Port::getFloatValue (worldIn.get (property.get(), worldIn.newUri (uri).get(), nullptr).get(), fallback);
    };

    const auto hasPortProperty = [&] (const char* uri)
    {
        return worldIn.ask (property.get(),
                            worldIn.newUri (LV2_CORE__portProperty).get(),
                            worldIn.newUri (uri).get());
    };

    const auto metadataScalePoints = worldIn.findNodes (property.get(),
                                                        worldIn.newUri (LV2_CORE__scalePoint).get(),
                                                        nullptr);
    SafeSortedSet<StoredScalePoint> parsedScalePoints;

    for (const auto* scalePoint : metadataScalePoints)
    {
        const auto label = worldIn.get (scalePoint, worldIn.newUri (LILV_NS_RDFS "label").get(), nullptr);
        const auto value = worldIn.get (scalePoint, worldIn.newUri (LILV_NS_RDF "value").get(), nullptr);

        if (label != nullptr && value != nullptr)
            parsedScalePoints.insert ({ lilv_node_as_string (label.get()), lilv_node_as_float (value.get()) });
        else
            jassertfalse; // A ScalePoint must have both a rdfs:label and a rdf:value
    }

    const auto minimum = getValue (LV2_CORE__minimum, 0.0f);
    const auto maximum = getValue (LV2_CORE__maximum, 1.0f);

    return { { std::move (parsedScalePoints),
               "des:" + String::fromUTF8 (property.getTyped()),
               getValue (LV2_CORE__default, (minimum + maximum) * 0.5f),
               minimum,
               maximum,
               typeUrid == urids.mLV2_ATOM__Bool || hasPortProperty (LV2_CORE__toggled),
               typeUrid == urids.mLV2_ATOM__Int || typeUrid == urids.mLV2_ATOM__Long,
               hasPortProperty (LV2_CORE__enumeration) },
             true,
             typeUrid };
}

static std::vector<ParameterData> getPortBasedParameters (World& world,
                                                          const Plugin& plugin,
                                                          std::initializer_list<const ControlPort*> hiddenPorts,
                                                          SimpleSpan<ControlPort> controlPorts)
{
    std::vector<ParameterData> result;

    const auto groupNode = world.newUri (LV2_PORT_GROUPS__group);

    for (auto& port : controlPorts)
    {
        if (port.header.direction != Port::Direction::input)
            continue;

        if (std::find (std::begin (hiddenPorts), std::end (hiddenPorts), &port) != std::end (hiddenPorts))
            continue;

        const auto lilvPort = plugin.getPortByIndex (port.header.index);
        const auto group = lilvNodeToUriString (lilvPort.get (groupNode.get()).get());

        result.push_back ({ port.info, ParameterWriter { &port }, group, port.header.name });
    }

    return result;
}

static void writeFloatToForge  (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_float  (forge, value); }
static void writeDoubleToForge (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_double (forge, (double) value); }
static void writeIntToForge    (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_int    (forge, (int32_t) value); }
static void writeLongToForge   (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_long   (forge, (int64_t) value); }
static void writeBoolToForge   (LV2_Atom_Forge* forge, float value) { lv2_atom_forge_bool   (forge, value > 0.5f); }

static std::vector<ParameterData> getPatchBasedParameters (World& world,
                                                           const Plugin& plugin,
                                                           const UsefulUrids& urids,
                                                           uint32_t controlPortIndex)
{
    // This returns our writable parameters in an indeterminate order.
    // We want our parameters to be in a consistent order between runs, so
    // we'll create all the parameters in one pass, sort them, and then
    // add them in a separate pass.
    const auto writableControls = world.findNodes (plugin.getUri().get(),
                                                   world.newUri (LV2_PATCH__writable).get(),
                                                   nullptr);

    struct DataAndUri
    {
        ParameterData data;
        String uri;
    };

    std::vector<DataAndUri> resultWithUris;

    const auto groupNode = world.newUri (LV2_PORT_GROUPS__group);

    for (auto* ctrl : writableControls)
    {
        const auto labelString = [&]
        {
            if (auto label = world.get (ctrl, world.newUri (LILV_NS_RDFS "label").get(), nullptr))
                return String::fromUTF8 (lilv_node_as_string (label.get()));

            return String();
        }();

        const auto uri = String::fromUTF8 (lilv_node_as_uri (ctrl));
        const auto info = getInfoForPatchParameter (world, urids, world.newUri (uri.toRawUTF8()));

        if (! info.supported)
            continue;

        const auto write = [&]
        {
            if (info.type == urids.mLV2_ATOM__Int)
                return writeIntToForge;

            if (info.type == urids.mLV2_ATOM__Long)
                return writeLongToForge;

            if (info.type == urids.mLV2_ATOM__Double)
                return writeDoubleToForge;

            if (info.type == urids.mLV2_ATOM__Bool)
                return writeBoolToForge;

            return writeFloatToForge;
        }();

        const auto group = lilvNodeToUriString (world.get (ctrl, groupNode.get(), nullptr).get());
        resultWithUris.push_back ({ { info.info,
                                      ParameterWriter { write, urids.symap.map (uri.toRawUTF8()), controlPortIndex },
                                      group,
                                      labelString },
                                    uri });
    }

    const auto compareUris = [] (const DataAndUri& a, const DataAndUri& b) { return a.uri < b.uri; };
    std::sort (resultWithUris.begin(), resultWithUris.end(), compareUris);

    std::vector<ParameterData> result;

    for (const auto& item : resultWithUris)
        result.push_back (item.data);

    return result;
}

static std::vector<ParameterData> getJuceParameterInfo (World& world,
                                                        const Plugin& plugin,
                                                        const UsefulUrids& urids,
                                                        std::initializer_list<const ControlPort*> hiddenPorts,
                                                        SimpleSpan<ControlPort> controlPorts,
                                                        uint32_t controlPortIndex)
{
    auto port  = getPortBasedParameters  (world, plugin, hiddenPorts, controlPorts);
    auto patch = getPatchBasedParameters (world, plugin, urids, controlPortIndex);

    port.insert (port.end(), patch.begin(), patch.end());
    return port;
}

// Rather than sprinkle #ifdef everywhere, risking the wrath of the entire C++
// standards committee, we put all of our conditionally-compiled stuff into a
// specialised template that compiles away to nothing when editor support is
// not available.
#if JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX || JUCE_BSD
 constexpr auto editorFunctionalityEnabled = true;
#else
 constexpr auto editorFunctionalityEnabled = false;
#endif

//==============================================================================
class LV2AudioPluginInstanceHeadless : public AudioPluginInstance
{
public:
    LV2AudioPluginInstanceHeadless (std::shared_ptr<World> worldIn,
                                    const Plugin& pluginIn,
                                    const UsefulUris& uris,
                                    std::unique_ptr<InstanceWithSupports>&& in,
                                    PluginDescription&& desc,
                                    std::vector<String> knownPresetUris,
                                    PluginState stateToApply,
                                    String,
                                    UiDescriptor)
        : LV2AudioPluginInstanceHeadless (worldIn,
                                          pluginIn,
                                          std::move (in),
                                          std::move (desc),
                                          std::move (knownPresetUris),
                                          std::move (stateToApply),
                                          getParsedBuses (*worldIn, pluginIn, uris))
    {
    }

    void fillInPluginDescription (PluginDescription& d) const override { d = description; }

    const String getName() const override { return description.name; }

    void prepareToPlay (double sampleRate, int numSamples) override
    {
        // In REAPER, changing the sample rate will deactivate the plugin,
        // save its state, destroy it, create a new instance, restore the
        // state, and then activate the new instance.
        // We'll do the same, because there's no way to retroactively change the
        // plugin sample rate.
        // This is a bit expensive, so try to avoid changing the sample rate too
        // frequently.

        // In addition to the above, we also need to destroy the custom view,
        // and recreate it after creating the new plugin instance.
        // Ideally this should all happen in the same Component.

        deactivate();

        MemoryBlock mb;
        getStateInformation (mb);

        instance = std::make_unique<InstanceWithSupports> (*world,
                                                           std::move (instance->symap),
                                                           plugin,
                                                           std::move (instance->ports),
                                                           numSamples,
                                                           sampleRate);

        // prepareToPlay is *guaranteed* not to be called concurrently with processBlock
        setStateInformationImpl (mb.getData(), (int) mb.getSize(), ConcurrentWithAudioCallback::no);

        jassert (numSamples == instance->features.getMaxBlockSize());
        activate();
    }

    void releaseResources() override { deactivate(); }

    using AudioPluginInstance::processBlock;
    using AudioPluginInstance::processBlockBypassed;

    void processBlock (AudioBuffer<float>& audio, MidiBuffer& midi) override
    {
        processBlockImpl (audio, midi);
    }

    void processBlockBypassed (AudioBuffer<float>& audio, MidiBuffer& midi) override
    {
        if (bypassParam != nullptr)
            processBlockImpl (audio, midi);
        else
            AudioPluginInstance::processBlockBypassed (audio, midi);
    }

    double getTailLengthSeconds() const override { return {}; } // TODO

    bool acceptsMidi() const override
    {
        if (instance == nullptr)
            return false;

        auto ports = instance->ports.getAtomPorts();

        return std::any_of (ports.begin(), ports.end(), [&] (const AtomPort& a)
        {
            if (a.header.direction != Port::Direction::input)
                return false;

            return portAtIndexSupportsMidi (a.header.index);
        });
    }

    bool producesMidi() const override
    {
        if (instance == nullptr)
            return false;

        auto ports = instance->ports.getAtomPorts();

        return std::any_of (ports.begin(), ports.end(), [&] (const AtomPort& a)
        {
            if (a.header.direction != Port::Direction::output)
                return false;

            return portAtIndexSupportsMidi (a.header.index);
        });
    }

    AudioProcessorEditor* createEditor() override { return nullptr; }
    bool hasEditor() const override { return false; }

    int getNumPrograms() override { return (int) presetUris.size(); }

    int getCurrentProgram() override
    {
        return lastAppliedPreset;
    }

    void setCurrentProgram (int newProgram) override
    {
        JUCE_ASSERT_MESSAGE_THREAD;

        if (! isPositiveAndBelow (newProgram, presetUris.size()))
            return;

        lastAppliedPreset = newProgram;
        applyStateWithAppropriateLocking (loadStateWithUri (presetUris[(size_t) newProgram]),
                                          ConcurrentWithAudioCallback::yes);
    }

    const String getProgramName (int program) override
    {
        JUCE_ASSERT_MESSAGE_THREAD;

        if (isPositiveAndBelow (program, presetUris.size()))
            return loadStateWithUri (presetUris[(size_t) program]).getLabel();

        return {};
    }

    void changeProgramName (int program, const String& label) override
    {
        JUCE_ASSERT_MESSAGE_THREAD;

        if (isPositiveAndBelow (program, presetUris.size()))
            loadStateWithUri (presetUris[(size_t) program]).setLabel (label);
    }

    void getStateInformation (MemoryBlock& block) override
    {
        JUCE_ASSERT_MESSAGE_THREAD;

        // TODO where should the state URI come from?
        PortMap portStateManager (instance->ports);
        const auto stateUri = String::fromUTF8 (instance->instance.getUri()) + "/savedState";
        auto mapFeature = instance->symap->getMapFeature();
        auto unmapFeature = instance->symap->getUnmapFeature();
        const auto state = PluginState::SaveRestoreHandle (*instance, portStateManager).save (plugin.get(), &mapFeature);
        const auto string = state.toString (world->get(), &mapFeature, &unmapFeature, stateUri.toRawUTF8());
        block.replaceAll (string.data(), string.size());
    }

    void setStateInformation (const void* data, int size) override
    {
        setStateInformationImpl (data, size, ConcurrentWithAudioCallback::yes);
    }

    void setNonRealtime (bool newValue) noexcept override
    {
        JUCE_ASSERT_MESSAGE_THREAD;

        AudioPluginInstance::setNonRealtime (newValue);
        instance->features.setNonRealtime (newValue);
    }

    bool isBusesLayoutSupported (const BusesLayout& layout) const override
    {
        for (const auto& pair : { std::make_tuple (&layout.inputBuses,  &declaredBusLayout.inputs),
                                  std::make_tuple (&layout.outputBuses, &declaredBusLayout.outputs) })
        {
            const auto& requested = *std::get<0> (pair);
            const auto& allowed   = *std::get<1> (pair);

            if ((size_t) requested.size() != allowed.size())
                return false;

            for (size_t busIndex = 0; busIndex < allowed.size(); ++busIndex)
            {
                const auto& requestedBus = requested[(int) busIndex];
                const auto& allowedBus = allowed[busIndex];

                if (! allowedBus.isCompatible (requestedBus))
                    return false;
            }
        }

        return true;
    }

    void processorLayoutsChanged() override { ioMap = lv2_shared::PortToAudioBufferMap { getBusesLayout(), declaredBusLayout }; }

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

    void postAllParametersToUi (ProcessorToUi& channel, UiEventListener* uiEventListener)
    {
        parameterValues.postAllParametersToUi (uiEventListener, getParameterWriterUrids(), channel);
        controlPortStructure.writeOutputPorts (uiEventListener, channel);
    }

    void postChangedParametersToUi (ProcessorToUi& channel, UiEventListener* uiEventListener)
    {
        parameterValues.postChangedParametersToUi (uiEventListener, getParameterWriterUrids(), channel);
        controlPortStructure.writeOutputPorts (uiEventListener, channel);
    }

    /*  The current instance will be *recreated* every time the sample rate changes! */
    InstanceWithSupports* getCurrentInstance() const
    {
        return instance.get();
    }

    AudioProcessorParameter* getParamByPortIndex (uint32_t portIndex) const
    {
        return parameterValues.getParamByPortIndex (portIndex);
    }

private:
    enum class ConcurrentWithAudioCallback { no, yes };

    virtual void sendOutgoingPortMessageToUi (UiMessageHeader, uint32_t, const void*) {}

    LV2AudioPluginInstanceHeadless (std::shared_ptr<World> worldIn,
                                    const Plugin& pluginIn,
                                    std::unique_ptr<InstanceWithSupports>&& in,
                                    PluginDescription&& desc,
                                    std::vector<String> knownPresetUris,
                                    PluginState stateToApply,
                                    const lv2_shared::ParsedBuses& parsedBuses)
        : AudioPluginInstance (getBusesProperties (parsedBuses, *worldIn)),
          declaredBusLayout (parsedBuses),
          world (std::move (worldIn)),
          plugin (pluginIn),
          description (std::move (desc)),
          presetUris (std::move (knownPresetUris)),
          instance (std::move (in))
    {
        applyStateWithAppropriateLocking (std::move (stateToApply), ConcurrentWithAudioCallback::no);
    }

    void setStateInformationImpl (const void* data, int size, ConcurrentWithAudioCallback concurrent)
    {
        JUCE_ASSERT_MESSAGE_THREAD;

        if (data == nullptr || size == 0)
            return;

        auto begin = static_cast<const char*> (data);
        std::vector<char> copy (begin, begin + size);
        copy.push_back (0);
        auto mapFeature = instance->symap->getMapFeature();
        applyStateWithAppropriateLocking (PluginState { lilv_state_new_from_string (world->get(), &mapFeature, copy.data()) },
                                          concurrent);
    }

    void activate()
    {
        if (! active)
            instance->instance.activate();

        active = true;
    }

    void deactivate()
    {
        if (active)
            instance->instance.deactivate();

        active = false;
    }

    void processBlockImpl (AudioBuffer<float>& audio, MidiBuffer& midi)
    {
        preparePortsForRun (audio, midi);

        instance->instance.run (static_cast<uint32_t> (audio.getNumSamples()));
        instance->features.processResponses();

        processPortsAfterRun (midi);
    }

    bool portAtIndexSupportsMidi (uint32_t index) const noexcept
    {
        const auto port = plugin.getPortByIndex (index);

        if (! port.isValid())
            return false;

        return port.supportsEvent (world->newUri (LV2_MIDI__MidiEvent).get());
    }

    ParameterWriterUrids getParameterWriterUrids() const
    {
        return { instance->urids.mLV2_PATCH__Set,
                 instance->urids.mLV2_PATCH__property,
                 instance->urids.mLV2_PATCH__value,
                 instance->urids.mLV2_ATOM__eventTransfer };
    }

    void applyStateWithAppropriateLocking (PluginState&& state, ConcurrentWithAudioCallback concurrent)
    {
        PortMap portStateManager (instance->ports);

        // If a plugin supports threadSafeRestore, its restore method is thread-safe
        // and may be called concurrently with audio class functions.
        if (hasThreadSafeRestore || concurrent == ConcurrentWithAudioCallback::no)
        {
            state.restore (*instance, portStateManager);
        }
        else
        {
            const ScopedLock lock (getCallbackLock());
            state.restore (*instance, portStateManager);
        }

        parameterValues.updateFromControlPorts (controlPortStructure);
    }

    PluginState loadStateWithUri (const String& str)
    {
        auto mapFeature = instance->symap->getMapFeature();
        const auto presetUri = world->newUri (str.toRawUTF8());
        lilv_world_load_resource (world->get(), presetUri.get());
        return PluginState { lilv_state_new_from_world (world->get(), &mapFeature, presetUri.get()) };
    }

    void connectPorts (AudioBuffer<float>& audio)
    {
        // Plugins that cannot process in-place will require the feature "inPlaceBroken".
        // We don't support that feature, so if we made it to this point we can assume that
        // in-place processing works.
        for (const auto& port : instance->ports.getAudioPorts())
        {
            const auto channel = ioMap.getChannelForPort (port.header.index);
            auto* ptr = isPositiveAndBelow (channel, audio.getNumChannels()) ? audio.getWritePointer (channel)
                                                                             : nullptr;
            instance->instance.connectPort (port.header.index, ptr);
        }

        for (const auto& port : instance->ports.getCvPorts())
            instance->instance.connectPort (port.header.index, nullptr);

        for (auto& port : instance->ports.getAtomPorts())
            instance->instance.connectPort (port.header.index, port.data());
    }

    void writeTimeInfoToPort (AtomPort& port)
    {
        if (port.header.direction != Port::Direction::input || ! port.getSupportsTime())
            return;

        auto* forge = port.getForge().get();
        auto* playhead = getPlayHead();

        if (playhead == nullptr)
            return;

        // Write timing info to the control port
        const auto info = playhead->getPosition();

        if (! info.hasValue())
            return;

        const auto& urids = instance->urids;

        lv2_atom_forge_frame_time (forge, 0);

        lv2_shared::ObjectFrame object { forge, (uint32_t) 0, urids.mLV2_TIME__Position };

        lv2_atom_forge_key (forge, urids.mLV2_TIME__speed);
        lv2_atom_forge_float (forge, info->getIsPlaying() ? 1.0f : 0.0f);

        if (const auto samples = info->getTimeInSamples())
        {
            lv2_atom_forge_key (forge, urids.mLV2_TIME__frame);
            lv2_atom_forge_long (forge, *samples);
        }

        if (const auto bar = info->getBarCount())
        {
            lv2_atom_forge_key (forge, urids.mLV2_TIME__bar);
            lv2_atom_forge_long (forge, *bar);
        }

        if (const auto beat = info->getPpqPosition())
        {
            if (const auto barStart = info->getPpqPositionOfLastBarStart())
            {
                lv2_atom_forge_key (forge, urids.mLV2_TIME__barBeat);
                lv2_atom_forge_float (forge, (float) (*beat - *barStart));
            }

            lv2_atom_forge_key (forge, urids.mLV2_TIME__beat);
            lv2_atom_forge_double (forge, *beat);
        }

        if (const auto sig = info->getTimeSignature())
        {
            lv2_atom_forge_key (forge, urids.mLV2_TIME__beatUnit);
            lv2_atom_forge_int (forge, sig->denominator);

            lv2_atom_forge_key (forge, urids.mLV2_TIME__beatsPerBar);
            lv2_atom_forge_float (forge, (float) sig->numerator);
        }

        if (const auto bpm = info->getBpm())
        {
            lv2_atom_forge_key (forge, urids.mLV2_TIME__beatsPerMinute);
            lv2_atom_forge_float (forge, (float) *bpm);
        }
    }

    void preparePortsForRun (AudioBuffer<float>& audio, MidiBuffer& midiBuffer)
    {
        connectPorts (audio);

        for (auto& port : instance->ports.getAtomPorts())
        {
            switch (port.header.direction)
            {
                case Port::Direction::input:
                    port.beginSequence();
                    break;

                case Port::Direction::output:
                    port.replaceWithChunk();
                    break;

                case Port::Direction::unknown:
                    jassertfalse;
                    break;
            }
        }

        for (auto& port : instance->ports.getAtomPorts())
            writeTimeInfoToPort (port);

        const auto controlPortForge = controlPort != nullptr ? controlPort->getForge().get()
                                                             : nullptr;

        parameterValues.postChangedParametersToProcessor (getParameterWriterUrids(), controlPortForge);

        instance->uiToProcessor.readAllAndClear ([this] (MessageHeader header, uint32_t size, const void* buffer)
        {
            pushMessage (header, size, buffer);
        });

        for (auto& port : instance->ports.getAtomPorts())
        {
            if (port.header.direction == Port::Direction::input)
            {
                for (const auto meta : midiBuffer)
                {
                    port.addEventToSequence (meta.samplePosition,
                                             instance->urids.mLV2_MIDI__MidiEvent,
                                             static_cast<uint32_t> (meta.numBytes),
                                             meta.data);
                }

                port.endSequence();
            }
        }

        if (freeWheelingPort != nullptr)
            freeWheelingPort->currentValue = isNonRealtime() ? freeWheelingPort->info.max
                                                             : freeWheelingPort->info.min;
    }

    void pushMessage (MessageHeader header, [[maybe_unused]] uint32_t size, const void* data)
    {
        if (header.protocol == 0 || header.protocol == instance->urids.mLV2_UI__floatProtocol)
        {
            const auto value = readUnaligned<float> (data);

            if (auto* param = parameterValues.getParamByPortIndex (header.portIndex))
            {
                param->setDenormalisedValue (value);
            }
            else if (auto* port = controlPortStructure.getControlPortByIndex (header.portIndex))
            {
                // No parameter corresponds to this port, write to the port directly
                port->currentValue = value;
            }
        }
        else if (auto* atomPort = header.portIndex < atomPorts.size() ? atomPorts[header.portIndex] : nullptr)
        {
            if (header.protocol == instance->urids.mLV2_ATOM__eventTransfer)
            {
                if (const auto* atom = convertToAtomPtr (data, (size_t) size))
                {
                    atomPort->addAtomToSequence (0, atom);

                    // Not UB; LV2_Atom_Object has LV2_Atom as its first member
                    if (atom->type == instance->urids.mLV2_ATOM__Object)
                        patchSetHelper.processPatchSet (reinterpret_cast<const LV2_Atom_Object*> (data), PatchSetCallback { parameterValues });
                }
            }
            else if (header.protocol == instance->urids.mLV2_ATOM__atomTransfer)
            {
                if (const auto* atom = convertToAtomPtr (data, (size_t) size))
                    atomPort->replaceBufferWithAtom (atom);
            }
        }
    }

    void processPortsAfterRun (MidiBuffer& midi)
    {
        midi.clear();

        for (auto& port : instance->ports.getAtomPorts())
            processAtomPort (port, midi);

        if (latencyPort != nullptr)
            setLatencySamples ((int) latencyPort->currentValue);
    }

    void processAtomPort (const AtomPort& port, MidiBuffer& midi)
    {
        if (port.header.direction != Port::Direction::output)
            return;

        // The port holds an Atom, by definition
        const auto* atom = reinterpret_cast<const LV2_Atom*> (port.data());

        if (atom->type != instance->urids.mLV2_ATOM__Sequence)
            return;

        // The Atom said that it was of sequence type, so this isn't UB
        const auto* sequence = reinterpret_cast<const LV2_Atom_Sequence*> (port.data());

        // http://lv2plug.in/ns/ext/atom#Sequence - run() stamps are always audio frames
        jassert (sequence->body.unit == 0 || sequence->body.unit == instance->urids.mLV2_UNITS__frame);

        for (const auto* event : lv2_shared::SequenceIterator { lv2_shared::SequenceWithSize { sequence } })
        {
            sendOutgoingPortMessageToUi ({ nullptr, { port.header.index, instance->urids.mLV2_ATOM__eventTransfer } },
                                         (uint32_t) (event->body.size + sizeof (LV2_Atom)),
                                         &event->body);

            if (event->body.type == instance->urids.mLV2_MIDI__MidiEvent)
                midi.addEvent (event + 1, static_cast<int> (event->body.size), static_cast<int> (event->time.frames));

            if (lv2_atom_forge_is_object_type (port.getForge().get(), event->body.type))
                if (reinterpret_cast<const LV2_Atom_Object_Body*> (event + 1)->otype == instance->urids.mLV2_STATE__StateChanged)
                    updateHostDisplay (ChangeDetails{}.withNonParameterStateChanged (true));

            patchSetHelper.processPatchSet (event, PatchSetCallback { parameterValues });
        }
    }

    // Check for duplicate channel designations, and convert the set to a discrete channel layout
    // if any designations are duplicated.
    static std::set<lv2_shared::SinglePortInfo> validateAndRedesignatePorts (std::set<lv2_shared::SinglePortInfo> info)
    {
        const auto channelSet = lv2_shared::ParsedGroup::getEquivalentSet (info);

        if ((int) info.size() == channelSet.size())
            return info;

        std::set<lv2_shared::SinglePortInfo> result;
        auto designation = (int) AudioChannelSet::discreteChannel0;

        for (auto& item : info)
        {
            auto copy = item;
            copy.designation = (AudioChannelSet::ChannelType) designation++;
            result.insert (copy);
        }

        return result;
    }

    static AudioChannelSet::ChannelType getPortDesignation (World& world, const Port& port, size_t indexInGroup)
    {
        const auto defaultResult = (AudioChannelSet::ChannelType) (AudioChannelSet::discreteChannel0 + indexInGroup);
        const auto node = port.get (world.newUri (LV2_CORE__designation).get());

        if (node == nullptr)
            return defaultResult;

        const auto it = lv2_shared::channelDesignationMap.find (lilvNodeToUriString (node.get()));

        if (it == lv2_shared::channelDesignationMap.end())
            return defaultResult;

        return it->second;
    }

    static lv2_shared::ParsedBuses getParsedBuses (World& world, const Plugin& p, const UsefulUris& uris)
    {
        const auto groupPropertyUri = world.newUri (LV2_PORT_GROUPS__group);
        const auto optionalUri = world.newUri (LV2_CORE__connectionOptional);

        std::map<String, std::set<lv2_shared::SinglePortInfo>> inputGroups, outputGroups;
        std::set<lv2_shared::SinglePortInfo> ungroupedInputs, ungroupedOutputs;

        for (uint32_t i = 0, numPorts = p.getNumPorts(); i < numPorts; ++i)
        {
            const auto port = p.getPortByIndex (i);

            if (port.getKind (uris) != Port::Kind::audio)
                continue;

            const auto groupUri = lilvNodeToUriString (port.get (groupPropertyUri.get()).get());

            auto& set = [&]() -> auto&
            {
                if (groupUri.isEmpty())
                    return port.getDirection (uris) == Port::Direction::input ? ungroupedInputs : ungroupedOutputs;

                auto& group = port.getDirection (uris) == Port::Direction::input ? inputGroups : outputGroups;
                return group[groupUri];
            }();

            set.insert ({ port.getIndex(), getPortDesignation (world, port, set.size()), port.hasProperty (optionalUri) });
        }

        for (auto* groups : { &inputGroups, &outputGroups })
            for (auto& pair : *groups)
                pair.second = validateAndRedesignatePorts (std::move (pair.second));

        JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4702)
        const auto getMainGroupName = [&] (const char* propertyName)
        {
            for (const auto* item : p.getValue (world.newUri (propertyName).get()))
                return lilvNodeToUriString (item);

            return String{};
        };
        JUCE_END_IGNORE_WARNINGS_MSVC

        return { findStableBusOrder (getMainGroupName (LV2_PORT_GROUPS__mainInput),  inputGroups,  ungroupedInputs),
                 findStableBusOrder (getMainGroupName (LV2_PORT_GROUPS__mainOutput), outputGroups, ungroupedOutputs) };
    }

    static String getNameForUri (World& world, StringRef uri)
    {
        if (uri.isEmpty())
            return String();

        const auto node = world.get (world.newUri (uri).get(),
                                     world.newUri (LV2_CORE__name).get(),
                                     nullptr);

        if (node == nullptr)
            return String();

        return String::fromUTF8 (lilv_node_as_string (node.get()));
    }

    static BusesProperties getBusesProperties (const lv2_shared::ParsedBuses& parsedBuses, World& world)
    {
        BusesProperties result;

        for (const auto& pair : { std::make_tuple (&parsedBuses.inputs,  &result.inputLayouts),
                                  std::make_tuple (&parsedBuses.outputs, &result.outputLayouts) })
        {
            const auto& buses = *std::get<0> (pair);
            auto& layout = *std::get<1> (pair);

            for (const auto& bus : buses)
            {
                layout.add (AudioProcessor::BusProperties { getNameForUri (world, bus.uid),
                                                            bus.getEquivalentSet(),
                                                            bus.isRequired() });
            }
        }

        return result;
    }

    LV2_URID map (const char* str) const
    {
        return instance != nullptr ? instance->symap->map (str)
                                   : LV2_URID();
    }

    ControlPort* findControlPortWithIndex (uint32_t index) const
    {
        auto ports = instance->ports.getControlPorts();
        const auto indexMatches = [&] (const ControlPort& p) { return p.header.index == index; };
        const auto it = std::find_if (ports.begin(), ports.end(), indexMatches);

        return it != ports.end() ? &(*it) : nullptr;
    }

    const lv2_shared::ParsedBuses declaredBusLayout;
    lv2_shared::PortToAudioBufferMap ioMap { getBusesLayout(), declaredBusLayout };
    std::shared_ptr<World> world;
    Plugin plugin;
    PluginDescription description;
    std::vector<String> presetUris;
    std::unique_ptr<InstanceWithSupports> instance;

    std::vector<AtomPort*> atomPorts = getPortPointers (instance->ports.getAtomPorts());

    AtomPort* const controlPort = [&]() -> AtomPort*
    {
        const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(),
                                                       world->newUri (LV2_CORE__control).get());

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

        const auto index = port.getIndex();

        if (! isPositiveAndBelow (index, atomPorts.size()))
            return nullptr;

        return atomPorts[index];
    }();

    ControlPort* const latencyPort = [&]() -> ControlPort*
    {
        if (! plugin.hasLatency())
            return nullptr;

        return findControlPortWithIndex (plugin.getLatencyPortIndex());
    }();

    ControlPort* const freeWheelingPort = [&]() -> ControlPort*
    {
        const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(),
                                                       world->newUri (LV2_CORE__freeWheeling).get());

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

        return findControlPortWithIndex (port.getIndex());
    }();

    ControlPort* const enabledPort = [&]() -> ControlPort*
    {
        const auto port = plugin.getPortByDesignation (world->newUri (LV2_CORE__InputPort).get(),
                                                       world->newUri (LV2_CORE_PREFIX "enabled").get());

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

        return findControlPortWithIndex (port.getIndex());
    }();

    lv2_shared::PatchSetHelper patchSetHelper { instance->symap->getMapFeature(), plugin.getUri().getTyped() };
    ControlPortAccelerationStructure controlPortStructure { instance->ports.getControlPorts() };
    ParameterValueCache parameterValues { *this,
                                          *world,
                                          instance->symap->getMapFeature(),
                                          getJuceParameterInfo (*world,
                                                                plugin,
                                                                instance->urids,
                                                                { latencyPort, freeWheelingPort },
                                                                instance->ports.getControlPorts(),
                                                                controlPort != nullptr ? controlPort->header.index : 0),
                                          enabledPort };
    LV2Parameter* bypassParam = enabledPort != nullptr ? parameterValues.getParamByPortIndex (enabledPort->header.index)
                                                       : nullptr;

    int lastAppliedPreset = 0;
    bool hasThreadSafeRestore = plugin.hasExtensionData (world->newUri (LV2_STATE__threadSafeRestore));
    bool active { false };

    JUCE_LEAK_DETECTOR (LV2AudioPluginInstanceHeadless)
};

} // namespace lv2_host

//==============================================================================
class LV2PluginFormatHeadless::Pimpl
{
public:
    Pimpl()
    {
        loadAllPluginsFromPaths (getDefaultLocationsToSearch());

        const auto tempFile = lv2ResourceFolder.getFile();

        if (tempFile.createDirectory())
        {
            for (const auto& bundle : lv2::Bundle::getAllBundles())
            {
                const auto pathToBundle = tempFile.getChildFile (bundle.name + String (".lv2"));

                if (! pathToBundle.createDirectory())
                    continue;

                for (const auto& resource : bundle.contents)
                    pathToBundle.getChildFile (resource.name).replaceWithText (resource.contents);

                const auto pathString = File::addTrailingSeparator (pathToBundle.getFullPathName());
                world->loadBundle (world->newFileUri (nullptr, pathString.toRawUTF8()));
            }
        }
    }

    ~Pimpl()
    {
        lv2ResourceFolder.getFile().deleteRecursively();
    }

    void findAllTypesForFile (OwnedArray<PluginDescription>& result,
                              const String& identifier)
    {
        if (File::isAbsolutePath (identifier))
            world->loadBundle (world->newFileUri (nullptr, File::addTrailingSeparator (identifier).toRawUTF8()));

        std::vector<const LilvPlugin*> plugins { findPluginByUri (identifier) };
        findPluginsByFile (identifier, plugins);

        for (const auto& plugin : plugins)
        {
            if (auto desc = getDescription (plugin); desc.fileOrIdentifier.isNotEmpty())
            {
                result.add (std::make_unique<PluginDescription> (desc));
            }
        }
    }

    bool fileMightContainThisPluginType (const String& file) const
    {
        // If the string looks like a URI, then it could be a valid LV2 identifier
        const auto* data = file.toRawUTF8();
        const auto numBytes = file.getNumBytesAsUTF8();
        std::vector<uint8_t> vec (numBytes + 1, 0);
        std::copy (data, data + numBytes, vec.begin());
        return serd_uri_string_has_scheme (vec.data()) || file.endsWith (".lv2");
    }

    String getNameOfPluginFromIdentifier (const String& identifier)
    {
        // We would have to actually load the bundle to get its name,
        // and the bundle may contain multiple plugins
        return identifier;
    }

    bool pluginNeedsRescanning (const PluginDescription&)
    {
        return true;
    }

    bool doesPluginStillExist (const PluginDescription& description)
    {
        return findPluginByUri (description.fileOrIdentifier) != nullptr;
    }

    StringArray searchPathsForPlugins (const FileSearchPath& paths, bool, bool)
    {
        loadAllPluginsFromPaths (paths);

        StringArray result;

        for (const auto* plugin : world->getAllPlugins())
            result.add (URL { lv2_host::Plugin { plugin }.getBundleUri().getTyped() }.getLocalFile().getFullPathName());

        return result;
    }

    FileSearchPath getDefaultLocationsToSearch()
    {
        auto defaults = std::invoke ([]() -> FileSearchPath
        {
          #if JUCE_MAC
            return { "~/Library/Audio/Plug-Ins/LV2;"
                     "~/.lv2;"
                     "/Library/Audio/Plug-Ins/LV2;"
                     "/usr/local/lib/lv2;"
                     "/usr/lib/lv2;" };
          #elif JUCE_WINDOWS
            const auto localAppData = File::getSpecialLocation (File::userApplicationDataDirectory).getFullPathName();
            const auto programFiles = File::getSpecialLocation (File::windowsProgramFilesCommon).getFullPathName();
            return { localAppData + "\\LV2;" + programFiles + "\\LV2" };
          #else
           #if JUCE_64BIT
            if (File ("/usr/lib64/lv2").exists() || File ("/usr/local/lib64/lv2").exists())
                return { "~/.lv2;"
                         "/usr/local/lib64/lv2;"
                         "/usr/lib64/lv2" };
           #endif

            return { "~/.lv2;"
                     "/usr/local/lib/lv2;"
                     "/usr/lib/lv2" };
          #endif
        });

        FileSearchPath result { SystemStats::getEnvironmentVariable ("LV2_PATH", "").replace (":", ";") };
        result.addPath (defaults);
        return result;
    }

    const LilvUI* findEmbeddableUi (const lv2_host::Uis* pluginUis, std::true_type)
    {
        if (pluginUis == nullptr)
            return nullptr;

        const std::vector<const LilvUI*> allUis (pluginUis->begin(), pluginUis->end());

        if (allUis.empty())
            return nullptr;

        constexpr const char* rawUri =
               #if JUCE_MAC
                LV2_UI__CocoaUI;
               #elif JUCE_WINDOWS
                LV2_UI__WindowsUI;
               #elif JUCE_LINUX || JUCE_BSD
                LV2_UI__X11UI;
               #else
                nullptr;
               #endif

        jassert (rawUri != nullptr);
        const auto nativeUiUri = world->newUri (rawUri);

        struct UiWithSuitability
        {
            const LilvUI* ui;
            unsigned suitability;

            bool operator< (const UiWithSuitability& other) const noexcept
            {
                return suitability < other.suitability;
            }

            static unsigned uiIsSupported (const char* hostUri, const char* pluginUri)
            {
                if (strcmp (hostUri, pluginUri) == 0)
                    return 1;

                return 0;
            }
        };

        std::vector<UiWithSuitability> uisWithSuitability;
        uisWithSuitability.reserve (allUis.size());

        std::transform (allUis.cbegin(), allUis.cend(), std::back_inserter (uisWithSuitability), [&] (const LilvUI* ui)
        {
            const LilvNode* type = nullptr;
            return UiWithSuitability { ui, lilv_ui_is_supported (ui, UiWithSuitability::uiIsSupported, nativeUiUri.get(), &type) };
        });

        std::sort (uisWithSuitability.begin(), uisWithSuitability.end());

        if (uisWithSuitability.back().suitability != 0)
            return uisWithSuitability.back().ui;

        return nullptr;
    }

    const LilvUI* findEmbeddableUi (const lv2_host::Uis*, std::false_type)
    {
        return nullptr;
    }

    const LilvUI* findEmbeddableUi (const lv2_host::Uis* pluginUis)
    {
        return findEmbeddableUi (pluginUis, std::integral_constant<bool, lv2_host::editorFunctionalityEnabled>{});
    }

    static lv2_host::UiDescriptor getUiDescriptor (const LilvUI* ui)
    {
        if (ui == nullptr)
            return {};

        const auto libraryFile = StringPtr { lilv_file_uri_parse (lilv_node_as_uri (lilv_ui_get_binary_uri (ui)), nullptr) };

        return lv2_host::UiDescriptor { lv2_host::UiDescriptorArgs{}.withLibraryPath (libraryFile.get())
                                                                    .withUiUri (lilv_node_as_uri (lilv_ui_get_uri (ui))) };
    }

    // Returns the name of a missing feature, if any.
    template <typename RequiredFeatures, typename AvailableFeatures>
    static std::vector<String> findMissingFeatures (RequiredFeatures&& required,
                                                    AvailableFeatures&& available)
    {
        std::vector<String> result;

        for (const auto* node : required)
        {
            const auto nodeString = String::fromUTF8 (lilv_node_as_uri (node));

            if (std::find (std::begin (available), std::end (available), nodeString) == std::end (available))
                result.push_back (nodeString);
        }

        return result;
    }

    template <typename TypeToCreate>
    static void createPluginInstance (LV2PluginFormatHeadless& format,
                                      const PluginDescription& desc,
                                      double initialSampleRate,
                                      int initialBufferSize,
                                      PluginCreationCallback callback)
    {
        const auto* pluginPtr = format.pimpl->findPluginByUri (desc.fileOrIdentifier);

        if (pluginPtr == nullptr)
            return callback (nullptr, "Unable to locate plugin with the requested URI");

        const lv2_host::Plugin plugin { pluginPtr };

        auto symap = std::make_unique<lv2_host::SymbolMap>();

        const auto missingFeatures = findMissingFeatures (plugin.getRequiredFeatures(),
                                                          lv2_host::FeaturesData::getFeatureUris());

        if (! missingFeatures.empty())
        {
            const auto missingFeaturesString = StringArray (missingFeatures.data(), (int) missingFeatures.size()).joinIntoString (", ");

            return callback (nullptr, "plugin requires missing features: " + missingFeaturesString);
        }

        auto world = format.pimpl->world;
        auto& uris = format.pimpl->uris;

        auto stateToApply = [&]
        {
            if (! plugin.hasFeature (world->newUri (LV2_STATE__loadDefaultState)))
                return lv2_host::PluginState{};

            auto map = symap->getMapFeature();
            return lv2_host::PluginState { lilv_state_new_from_world (world->get(), &map, plugin.getUri().get()) };
        }();

        auto ports = lv2_host::Ports::getPorts (*world, uris, plugin, *symap);

        if (! ports.hasValue())
            return callback (nullptr, "Plugin has ports of an unsupported type");

        auto instance = std::make_unique<lv2_host::InstanceWithSupports> (*world,
                                                                          std::move (symap),
                                                                          plugin,
                                                                          std::move (*ports),
                                                                          (int32_t) initialBufferSize,
                                                                          initialSampleRate);

        if (instance->instance == nullptr)
            return callback (nullptr, "Plugin was located, but could not be opened");

        auto potentialPresets = world->findNodes (nullptr,
                                                  world->newUri (LV2_CORE__appliesTo).get(),
                                                  plugin.getUri().get());

        const lv2_host::Uis pluginUis { plugin.get() };

        const auto uiToUse = [&]() -> const LilvUI*
        {
            const auto bestMatch = format.pimpl->findEmbeddableUi (&pluginUis);

            if (bestMatch == nullptr)
                return bestMatch;

            const auto uiUri = lilv_ui_get_uri (bestMatch);
            lilv_world_load_resource (world->get(), uiUri);

            const auto queryUi = [&] (const char* featureUri)
            {
                const auto featureUriNode = world->newUri (featureUri);
                return world->findNodes (uiUri, featureUriNode.get(), nullptr);
            };

            const auto missingUiFeatures = findMissingFeatures (queryUi (LV2_CORE__requiredFeature),
                                                                lv2_host::UiFeatureUris::getFeatureUris());

            return missingUiFeatures.empty() ? bestMatch : nullptr;
        }();

        auto uiBundleUri = uiToUse != nullptr ? String::fromUTF8 (lilv_node_as_uri (lilv_ui_get_bundle_uri (uiToUse)))
                                              : String();

        auto wrapped = std::make_unique<TypeToCreate> (world,
                                                       plugin,
                                                       uris,
                                                       std::move (instance),
                                                       format.pimpl->getDescription (pluginPtr),
                                                       format.pimpl->findPresetUrisForPlugin (plugin.get()),
                                                       std::move (stateToApply),
                                                       std::move (uiBundleUri),
                                                       getUiDescriptor (uiToUse));
        callback (std::move (wrapped), {});
    }

private:
    void loadAllPluginsFromPaths (const FileSearchPath& path)
    {
        const auto joined = path.toStringWithSeparator (LILV_PATH_SEP);
        world->loadAllFromPaths (world->newString (joined.toRawUTF8()));
    }

    struct Free { void operator() (char* ptr) const noexcept { free (ptr); } };
    using StringPtr = std::unique_ptr<char, Free>;

    const LilvPlugin* findPluginByUri (const String& s)
    {
        return world->getAllPlugins().getByUri (world->newUri (s.toRawUTF8()));
    }

    void findPluginsByFile (const File& f, std::vector<const LilvPlugin*>& result)
    {
        return world->getAllPlugins().getByFile (f, result);
    }

    template <typename Fn>
    void visitParentClasses (const LilvPluginClass* c, Fn&& fn) const
    {
        if (c == nullptr)
            return;

        const lv2_host::PluginClass wrapped { c };
        fn (wrapped);

        const auto parentUri = wrapped.getParentUri();

        if (parentUri.get() != nullptr)
            visitParentClasses (world->getPluginClasses().getByUri (parentUri), fn);
    }

    std::vector<lv2_host::NodeUri> collectPluginClassUris (const LilvPluginClass* c) const
    {
        std::vector<lv2_host::NodeUri> results;

        visitParentClasses (c, [&results] (const lv2_host::PluginClass& wrapped)
        {
            results.emplace_back (wrapped.getUri());
        });

        return results;
    }

    PluginDescription getDescription (const LilvPlugin* plugin)
    {
        if (plugin == nullptr)
            return {};

        const auto wrapped      = lv2_host::Plugin { plugin };
        const auto bundle       = wrapped.getBundleUri().getTyped();
        const auto bundleFile   = File { StringPtr { lilv_file_uri_parse (bundle, nullptr) }.get() };

        const auto numInputs  = wrapped.getNumPortsOfClass (uris.mLV2_CORE__AudioPort, uris.mLV2_CORE__InputPort);
        const auto numOutputs = wrapped.getNumPortsOfClass (uris.mLV2_CORE__AudioPort, uris.mLV2_CORE__OutputPort);

        PluginDescription result;
        result.name                 = wrapped.getName().getTyped();
        result.descriptiveName      = wrapped.getName().getTyped();
        result.lastFileModTime      = bundleFile.getLastModificationTime();
        result.lastInfoUpdateTime   = Time::getCurrentTime();
        result.manufacturerName     = wrapped.getAuthorName().getTyped();
        result.pluginFormatName     = LV2PluginFormatHeadless::getFormatName();
        result.numInputChannels     = static_cast<int> (numInputs);
        result.numOutputChannels    = static_cast<int> (numOutputs);

        const auto classPtr     = wrapped.getClass();
        const auto classes      = collectPluginClassUris (classPtr);
        const auto isInstrument = std::any_of (classes.cbegin(),
                                               classes.cend(),
                                               [this] (const lv2_host::NodeUri& uri)
                                               {
                                                   return uri.equals (uris.mLV2_CORE__GeneratorPlugin);
                                               });

        result.category         = lv2_host::PluginClass { classPtr }.getLabel().getTyped();
        result.isInstrument     = isInstrument;

        // The plugin URI is required to be globally unique, so a hash of it should be too
        result.fileOrIdentifier = wrapped.getUri().getTyped();

        const auto uid = DefaultHashFunctions::generateHash (result.fileOrIdentifier, std::numeric_limits<int>::max());;
        result.deprecatedUid = result.uniqueId = uid;
        return result;
    }

    std::vector<String> findPresetUrisForPlugin (const LilvPlugin* plugin)
    {
        std::vector<String> presetUris;

        lv2_host::OwningNodes potentialPresets { lilv_plugin_get_related (plugin, world->newUri (LV2_PRESETS__Preset).get()) };

        for (const auto* potentialPreset : potentialPresets)
            presetUris.push_back (lilv_node_as_string (potentialPreset));

        return presetUris;
    }

    TemporaryFile lv2ResourceFolder;
    std::shared_ptr<lv2_host::World> world = std::make_shared<lv2_host::World>();
    lv2_host::UsefulUris uris { world->get() };
};

} // namespace juce

#endif
