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

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

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

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

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

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

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

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

#pragma once

#include <juce_core/system/juce_TargetPlatform.h>

#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS)

#include <JuceHeader.h>

#include <ARA_API/ARAInterface.h>
#include <ARA_Library/Dispatch/ARAHostDispatch.h>

class FileAudioSource
{
    auto getAudioSourceProperties() const
    {
        auto properties = ARAHostModel::AudioSource::getEmptyProperties();
        properties.name = formatReader->getFile().getFullPathName().toRawUTF8();
        properties.persistentID = formatReader->getFile().getFullPathName().toRawUTF8();
        properties.sampleCount = formatReader->lengthInSamples;
        properties.sampleRate = formatReader->sampleRate;
        properties.channelCount = (int) formatReader->numChannels;
        properties.merits64BitSamples = false;
        return properties;
    }

public:
    FileAudioSource (ARA::Host::DocumentController& dc, const juce::File& file)
        : formatReader ([&file]
          {
              auto result = rawToUniquePtr (WavAudioFormat().createMemoryMappedReader (file));
              result->mapEntireFile();
              return result;
          }()),
          audioSource (Converter::toHostRef (this), dc, getAudioSourceProperties())
    {
        audioSource.enableAudioSourceSamplesAccess (true);
    }

    bool readAudioSamples (float* const* buffers, int64 startSample, int64 numSamples)
    {
        // TODO: the ARA interface defines numSamples as int64. We should do multiple reads if necessary with the reader.
        if (numSamples > std::numeric_limits<int>::max())
            return false;

        return formatReader->read (buffers, (int) formatReader->numChannels, startSample, (int) (numSamples));
    }

    bool readAudioSamples (double* const* buffers, int64 startSample, int64 numSamples)
    {
        ignoreUnused (buffers, startSample, numSamples);
        return false;
    }

    MemoryMappedAudioFormatReader& getFormatReader() const { return *formatReader; }

    auto getPluginRef() const { return audioSource.getPluginRef(); }

    auto& getSource() { return audioSource; }

    using Converter = ARAHostModel::ConversionFunctions<FileAudioSource*, ARA::ARAAudioSourceHostRef>;

private:
    std::unique_ptr<MemoryMappedAudioFormatReader> formatReader;
    ARAHostModel::AudioSource audioSource;
};

//==============================================================================
class MusicalContext
{
    auto getMusicalContextProperties() const
    {
        auto properties = ARAHostModel::MusicalContext::getEmptyProperties();
        properties.name = "MusicalContext";
        properties.orderIndex = 0;
        properties.color = nullptr;
        return properties;
    }

public:
    MusicalContext (ARA::Host::DocumentController& dc)
        : context (Converter::toHostRef (this), dc, getMusicalContextProperties())
    {
    }

    auto getPluginRef() const { return context.getPluginRef(); }

private:
    using Converter = ARAHostModel::ConversionFunctions<MusicalContext*, ARA::ARAMusicalContextHostRef>;

    ARAHostModel::MusicalContext context;
};

//==============================================================================
class RegionSequence
{
    auto getRegionSequenceProperties() const
    {
        auto properties = ARAHostModel::RegionSequence::getEmptyProperties();
        properties.name = name.toRawUTF8();
        properties.orderIndex = 0;
        properties.musicalContextRef = context.getPluginRef();
        properties.color = nullptr;
        return properties;
    }

public:
    RegionSequence (ARA::Host::DocumentController& dc, MusicalContext& contextIn, String nameIn)
        : context (contextIn),
          name (std::move (nameIn)),
          sequence (Converter::toHostRef (this), dc, getRegionSequenceProperties())
    {
    }

    auto& getMusicalContext() const { return context; }
    auto getPluginRef() const { return sequence.getPluginRef(); }

private:
    using Converter = ARAHostModel::ConversionFunctions<RegionSequence*, ARA::ARARegionSequenceHostRef>;

    MusicalContext& context;
    String name;
    ARAHostModel::RegionSequence sequence;
};

class AudioModification
{
    auto getProperties() const
    {
        auto properties = ARAHostModel::AudioModification::getEmptyProperties();
        properties.persistentID = "x";
        return properties;
    }

public:
    AudioModification (ARA::Host::DocumentController& dc, FileAudioSource& source)
        : modification (Converter::toHostRef (this), dc, source.getSource(), getProperties())
    {
    }

    auto& getModification() { return modification; }

private:
    using Converter = ARAHostModel::ConversionFunctions<AudioModification*, ARA::ARAAudioModificationHostRef>;

    ARAHostModel::AudioModification modification;
};

//==============================================================================
class PlaybackRegion
{
    auto getPlaybackRegionProperties() const
    {
        auto properties = ARAHostModel::PlaybackRegion::getEmptyProperties();
        properties.transformationFlags = ARA::kARAPlaybackTransformationNoChanges;
        properties.startInModificationTime = 0.0;
        const auto& formatReader = audioSource.getFormatReader();
        properties.durationInModificationTime = formatReader.lengthInSamples / formatReader.sampleRate;
        properties.startInPlaybackTime = 0.0;
        properties.durationInPlaybackTime = properties.durationInModificationTime;
        properties.musicalContextRef = sequence.getMusicalContext().getPluginRef();
        properties.regionSequenceRef = sequence.getPluginRef();

        properties.name = nullptr;
        properties.color = nullptr;
        return properties;
    }

public:
    PlaybackRegion (ARA::Host::DocumentController& dc,
                    RegionSequence& s,
                    AudioModification& m,
                    FileAudioSource& source)
        : sequence (s),
          audioSource (source),
          region (Converter::toHostRef (this), dc, m.getModification(), getPlaybackRegionProperties())
    {
        jassert (source.getPluginRef() == m.getModification().getAudioSource().getPluginRef());
    }

    auto& getPlaybackRegion() { return region; }

private:
    using Converter = ARAHostModel::ConversionFunctions<PlaybackRegion*, ARA::ARAPlaybackRegionHostRef>;

    RegionSequence& sequence;
    FileAudioSource& audioSource;
    ARAHostModel::PlaybackRegion region;
};

//==============================================================================
class AudioAccessController : public ARA::Host::AudioAccessControllerInterface
{
public:
    ARA::ARAAudioReaderHostRef createAudioReaderForSource (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                                           bool use64BitSamples) noexcept override
    {
        auto audioReader = std::make_unique<AudioReader> (audioSourceHostRef, use64BitSamples);
        auto audioReaderHostRef = Converter::toHostRef (audioReader.get());
        auto* readerPtr = audioReader.get();
        audioReaders.emplace (readerPtr, std::move (audioReader));
        return audioReaderHostRef;
    }

    bool readAudioSamples (ARA::ARAAudioReaderHostRef readerRef,
                           ARA::ARASamplePosition samplePosition,
                           ARA::ARASampleCount samplesPerChannel,
                           void* const* buffers) noexcept override
    {
        const auto use64BitSamples = Converter::fromHostRef (readerRef)->use64Bit;
        auto* audioSource = FileAudioSource::Converter::fromHostRef (Converter::fromHostRef (readerRef)->sourceHostRef);

        if (use64BitSamples)
            return audioSource->readAudioSamples (
                reinterpret_cast<double* const*> (buffers), samplePosition, samplesPerChannel);

        return audioSource->readAudioSamples (
            reinterpret_cast<float* const*> (buffers), samplePosition, samplesPerChannel);
    }

    void destroyAudioReader (ARA::ARAAudioReaderHostRef readerRef) noexcept override
    {
        audioReaders.erase (Converter::fromHostRef (readerRef));
    }

private:
    struct AudioReader
    {
        AudioReader (ARA::ARAAudioSourceHostRef source, bool use64BitSamples)
            : sourceHostRef (source), use64Bit (use64BitSamples)
        {
        }

        ARA::ARAAudioSourceHostRef sourceHostRef;
        bool use64Bit;
    };

    using Converter = ARAHostModel::ConversionFunctions<AudioReader*, ARA::ARAAudioReaderHostRef>;

    std::map<AudioReader*, std::unique_ptr<AudioReader>> audioReaders;
};

class ArchivingController : public ARA::Host::ArchivingControllerInterface
{
public:
    using ReaderConverter = ARAHostModel::ConversionFunctions<MemoryBlock*, ARA::ARAArchiveReaderHostRef>;
    using WriterConverter = ARAHostModel::ConversionFunctions<MemoryOutputStream*, ARA::ARAArchiveWriterHostRef>;

    ARA::ARASize getArchiveSize (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override
    {
        return (ARA::ARASize) ReaderConverter::fromHostRef (archiveReaderHostRef)->getSize();
    }

    bool readBytesFromArchive (ARA::ARAArchiveReaderHostRef archiveReaderHostRef,
                               ARA::ARASize position,
                               ARA::ARASize length,
                               ARA::ARAByte* buffer) noexcept override
    {
        auto* archiveReader = ReaderConverter::fromHostRef (archiveReaderHostRef);

        if ((position + length) <= archiveReader->getSize())
        {
            std::memcpy (buffer, addBytesToPointer (archiveReader->getData(), position), length);
            return true;
        }

        return false;
    }

    bool writeBytesToArchive (ARA::ARAArchiveWriterHostRef archiveWriterHostRef,
                              ARA::ARASize position,
                              ARA::ARASize length,
                              const ARA::ARAByte* buffer) noexcept override
    {
        auto* archiveWriter = WriterConverter::fromHostRef (archiveWriterHostRef);

        if (archiveWriter->setPosition ((int64) position) && archiveWriter->write (buffer, length))
            return true;

        return false;
    }

    void notifyDocumentArchivingProgress (float value) noexcept override { ignoreUnused (value); }

    void notifyDocumentUnarchivingProgress (float value) noexcept override { ignoreUnused (value); }

    ARA::ARAPersistentID getDocumentArchiveID (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override
    {
        ignoreUnused (archiveReaderHostRef);

        return nullptr;
    }
};

class ContentAccessController : public ARA::Host::ContentAccessControllerInterface
{
public:
    using Converter = ARAHostModel::ConversionFunctions<ARA::ARAContentType, ARA::ARAContentReaderHostRef>;

    bool isMusicalContextContentAvailable (ARA::ARAMusicalContextHostRef musicalContextHostRef,
                                           ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (musicalContextHostRef);

        return (type == ARA::kARAContentTypeTempoEntries || type == ARA::kARAContentTypeBarSignatures);
    }

    ARA::ARAContentGrade getMusicalContextContentGrade (ARA::ARAMusicalContextHostRef musicalContextHostRef,
                                                        ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (musicalContextHostRef, type);

        return ARA::kARAContentGradeInitial;
    }

    ARA::ARAContentReaderHostRef
        createMusicalContextContentReader (ARA::ARAMusicalContextHostRef musicalContextHostRef,
                                           ARA::ARAContentType type,
                                           const ARA::ARAContentTimeRange* range) noexcept override
    {
        ignoreUnused (musicalContextHostRef, range);

        return Converter::toHostRef (type);
    }

    bool isAudioSourceContentAvailable (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                        ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (audioSourceHostRef, type);

        return false;
    }

    ARA::ARAContentGrade getAudioSourceContentGrade (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                                     ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (audioSourceHostRef, type);

        return 0;
    }

    ARA::ARAContentReaderHostRef
        createAudioSourceContentReader (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                        ARA::ARAContentType type,
                                        const ARA::ARAContentTimeRange* range) noexcept override
    {
        ignoreUnused (audioSourceHostRef, type, range);

        return nullptr;
    }

    ARA::ARAInt32 getContentReaderEventCount (ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept override
    {
        const auto contentType = Converter::fromHostRef (contentReaderHostRef);

        if (contentType == ARA::kARAContentTypeTempoEntries || contentType == ARA::kARAContentTypeBarSignatures)
            return 2;

        return 0;
    }

    const void* getContentReaderDataForEvent (ARA::ARAContentReaderHostRef contentReaderHostRef,
                                              ARA::ARAInt32 eventIndex) noexcept override
    {
        if (Converter::fromHostRef (contentReaderHostRef) == ARA::kARAContentTypeTempoEntries)
        {
            if (eventIndex == 0)
            {
                tempoEntry.timePosition = 0.0;
                tempoEntry.quarterPosition = 0.0;
            }
            else if (eventIndex == 1)
            {
                tempoEntry.timePosition = 2.0;
                tempoEntry.quarterPosition = 4.0;
            }

            return &tempoEntry;
        }
        else if (Converter::fromHostRef (contentReaderHostRef) == ARA::kARAContentTypeBarSignatures)
        {
            if (eventIndex == 0)
            {
                barSignature.position = 0.0;
                barSignature.numerator = 4;
                barSignature.denominator = 4;
            }

            if (eventIndex == 1)
            {
                barSignature.position = 1.0;
                barSignature.numerator = 4;
                barSignature.denominator = 4;
            }

            return &barSignature;
        }

        jassertfalse;
        return nullptr;
    }

    void destroyContentReader (ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept override
    {
        ignoreUnused (contentReaderHostRef);
    }

    ARA::ARAContentTempoEntry tempoEntry;
    ARA::ARAContentBarSignature barSignature;
};

class ModelUpdateController : public ARA::Host::ModelUpdateControllerInterface
{
public:
    void notifyAudioSourceAnalysisProgress (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                            ARA::ARAAnalysisProgressState state,
                                            float value) noexcept override
    {
        ignoreUnused (audioSourceHostRef, state, value);
    }

    void notifyAudioSourceContentChanged (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                          const ARA::ARAContentTimeRange* range,
                                          ARA::ContentUpdateScopes scopeFlags) noexcept override
    {
        ignoreUnused (audioSourceHostRef, range, scopeFlags);
    }

    void notifyAudioModificationContentChanged (ARA::ARAAudioModificationHostRef audioModificationHostRef,
                                                const ARA::ARAContentTimeRange* range,
                                                ARA::ContentUpdateScopes scopeFlags) noexcept override
    {
        ignoreUnused (audioModificationHostRef, range, scopeFlags);
    }

    void notifyPlaybackRegionContentChanged (ARA::ARAPlaybackRegionHostRef playbackRegionHostRef,
                                             const ARA::ARAContentTimeRange* range,
                                             ARA::ContentUpdateScopes scopeFlags) noexcept override
    {
        ignoreUnused (playbackRegionHostRef, range, scopeFlags);
    }
};

class PlaybackController : public ARA::Host::PlaybackControllerInterface
{
public:
    void requestStartPlayback() noexcept override {}
    void requestStopPlayback() noexcept override {}

    void requestSetPlaybackPosition (ARA::ARATimePosition timePosition) noexcept override
    {
        ignoreUnused (timePosition);
    }

    void requestSetCycleRange (ARA::ARATimePosition startTime, ARA::ARATimeDuration duration) noexcept override
    {
        ignoreUnused (startTime, duration);
    }

    void requestEnableCycle (bool enable) noexcept override { ignoreUnused (enable); }
};

struct SimplePlayHead  : public juce::AudioPlayHead
{
    Optional<PositionInfo> getPosition() const override
    {
        PositionInfo result;
        result.setTimeInSamples (timeInSamples.load());
        result.setIsPlaying (isPlaying.load());
        return result;
    }

    std::atomic<int64_t> timeInSamples { 0 };
    std::atomic<bool> isPlaying { false };
};

struct HostPlaybackController
{
    virtual ~HostPlaybackController() = default;

    virtual void setPlaying (bool isPlaying) = 0;
    virtual void goToStart() = 0;
    virtual File getAudioSource() const = 0;
    virtual void setAudioSource (File audioSourceFile) = 0;
    virtual void clearAudioSource() = 0;
};

class AudioSourceComponent  : public Component,
                              public FileDragAndDropTarget,
                              public ChangeListener
{
public:
    explicit AudioSourceComponent (HostPlaybackController& controller, juce::ChangeBroadcaster& bc)
        : hostPlaybackController (controller),
          broadcaster (bc),
          waveformComponent (*this)
    {
        audioSourceLabel.setText ("You can drag and drop .wav files here", NotificationType::dontSendNotification);

        addAndMakeVisible (audioSourceLabel);
        addAndMakeVisible (waveformComponent);

        playButton.setButtonText ("Play / Pause");
        playButton.onClick = [this]
        {
            isPlaying = ! isPlaying;
            hostPlaybackController.setPlaying (isPlaying);
        };

        goToStartButton.setButtonText ("Go to start");
        goToStartButton.onClick = [this] { hostPlaybackController.goToStart(); };

        addAndMakeVisible (goToStartButton);
        addAndMakeVisible (playButton);

        broadcaster.addChangeListener (this);

        update();
    }

    ~AudioSourceComponent() override
    {
        broadcaster.removeChangeListener (this);
    }

    void changeListenerCallback (ChangeBroadcaster*) override
    {
        update();
    }

    void resized() override
    {
        auto localBounds = getLocalBounds();
        auto buttonsArea = localBounds.removeFromBottom (40).reduced (5);
        auto waveformArea = localBounds.removeFromBottom (150).reduced (5);

        juce::FlexBox fb;
        fb.justifyContent = juce::FlexBox::JustifyContent::center;
        fb.alignContent = juce::FlexBox::AlignContent::center;

        fb.items = { juce::FlexItem (goToStartButton).withMinWidth (100.0f).withMinHeight ((float) buttonsArea.getHeight()),
                     juce::FlexItem (playButton).withMinWidth (100.0f).withMinHeight ((float) buttonsArea.getHeight()) };

        fb.performLayout (buttonsArea);

        waveformComponent.setBounds (waveformArea);

        audioSourceLabel.setBounds (localBounds);
    }

    bool isInterestedInFileDrag (const StringArray& files) override
    {
        if (files.size() != 1)
            return false;

        if (files.getReference (0).endsWithIgnoreCase (".wav"))
            return true;

        return false;
    }

    void update()
    {
        const auto currentAudioSource = hostPlaybackController.getAudioSource();

        if (currentAudioSource.existsAsFile())
        {
            waveformComponent.setSource (currentAudioSource);
            audioSourceLabel.setText (currentAudioSource.getFullPathName(),
                                      NotificationType::dontSendNotification);
        }
        else
        {
            waveformComponent.clearSource();
            audioSourceLabel.setText ("You can drag and drop .wav files here", NotificationType::dontSendNotification);
        }
    }

    void filesDropped (const StringArray& files, int, int) override
    {
        hostPlaybackController.setAudioSource (files.getReference (0));
        update();
    }

private:
    class WaveformComponent  : public Component,
                               public ChangeListener
    {
    public:
        WaveformComponent (AudioSourceComponent& p)
            : parent (p),
              thumbCache (7),
              audioThumb (128, formatManager, thumbCache)
        {
            setWantsKeyboardFocus (true);
            formatManager.registerBasicFormats();
            audioThumb.addChangeListener (this);
        }

        ~WaveformComponent() override
        {
            audioThumb.removeChangeListener (this);
        }

        void mouseDown (const MouseEvent&) override
        {
            isSelected = true;
            repaint();
        }

        void changeListenerCallback (ChangeBroadcaster*) override
        {
            repaint();
        }

        void paint (juce::Graphics& g) override
        {
            if (! isEmpty)
            {
                auto rect = getLocalBounds();

                const auto waveformColour = Colours::cadetblue;

                if (rect.getWidth() > 2)
                {
                    g.setColour (isSelected ? juce::Colours::yellow : juce::Colours::black);
                    g.drawRect (rect);
                    rect.reduce (1, 1);
                    g.setColour (waveformColour.darker (1.0f));
                    g.fillRect (rect);
                }

                g.setColour (Colours::cadetblue);
                audioThumb.drawChannels (g, rect, 0.0, audioThumb.getTotalLength(), 1.0f);
            }
        }

        void setSource (const File& source)
        {
            isEmpty = false;
            audioThumb.setSource (new FileInputSource (source));
        }

        void clearSource()
        {
            isEmpty = true;
            isSelected = false;
            audioThumb.clear();
        }

        bool keyPressed (const KeyPress& key) override
        {
            if (isSelected && key == KeyPress::deleteKey)
            {
                parent.hostPlaybackController.clearAudioSource();
                return true;
            }

            return false;
        }

    private:
        AudioSourceComponent& parent;

        bool isEmpty = true;
        bool isSelected = false;
        AudioFormatManager formatManager;
        AudioThumbnailCache thumbCache;
        AudioThumbnail audioThumb;
    };

    HostPlaybackController& hostPlaybackController;
    juce::ChangeBroadcaster& broadcaster;
    Label audioSourceLabel;
    WaveformComponent waveformComponent;
    bool isPlaying { false };
    TextButton playButton, goToStartButton;
};

class ARAPluginInstanceWrapper  : public AudioPluginInstance
{
public:
    class ARATestHost  : public HostPlaybackController,
                         public juce::ChangeBroadcaster
    {
    public:
        class Editor  : public AudioProcessorEditor
        {
        public:
            explicit Editor (ARATestHost& araTestHost)
                : AudioProcessorEditor (araTestHost.getAudioPluginInstance()),
                  audioSourceComponent (araTestHost, araTestHost)
            {
                audioSourceComponent.update();
                addAndMakeVisible (audioSourceComponent);
                setSize (512, 220);
            }

            ~Editor() override { getAudioProcessor()->editorBeingDeleted (this); }

            void resized() override { audioSourceComponent.setBounds (getLocalBounds()); }

        private:
            AudioSourceComponent audioSourceComponent;
        };

        explicit ARATestHost (ARAPluginInstanceWrapper& instanceIn)
            : instance (instanceIn)
        {
            if (instance.inner->getPluginDescription().hasARAExtension)
            {
                instance.inner->setPlayHead (&playHead);

                createARAFactoryAsync (*instance.inner, [this] (ARAFactoryWrapper araFactory)
                                                        {
                                                            init (std::move (araFactory));
                                                        });
            }
        }

        void init (ARAFactoryWrapper araFactory)
        {
            if (araFactory.get() != nullptr)
            {
                documentController = ARAHostDocumentController::create (std::move (araFactory),
                                                                        "AudioPluginHostDocument",
                                                                        std::make_unique<AudioAccessController>(),
                                                                        std::make_unique<ArchivingController>(),
                                                                        std::make_unique<ContentAccessController>(),
                                                                        std::make_unique<ModelUpdateController>(),
                                                                        std::make_unique<PlaybackController>());

                if (documentController != nullptr)
                {
                    const auto allRoles = ARA::kARAPlaybackRendererRole | ARA::kARAEditorRendererRole | ARA::kARAEditorViewRole;
                    const auto plugInExtensionInstance = documentController->bindDocumentToPluginInstance (*instance.inner,
                                                                                                           allRoles,
                                                                                                           allRoles);
                    playbackRenderer = plugInExtensionInstance.getPlaybackRendererInterface();
                    editorRenderer   = plugInExtensionInstance.getEditorRendererInterface();
                    synchronizeStateWithDocumentController();
                }
                else
                    jassertfalse;
            }
            else
                jassertfalse;
        }

        void getStateInformation (juce::MemoryBlock& b)
        {
            std::lock_guard<std::mutex> configurationLock (instance.innerMutex);

            if (context != nullptr)
                context->getStateInformation (b);
        }

        void setStateInformation (const void* d, int s)
        {
            {
                std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };
                contextUpdateSource = ContextUpdateSource { d, s };
            }

            synchronise();
        }

        ~ARATestHost() override { instance.inner->releaseResources(); }

        void afterProcessBlock (int numSamples)
        {
            const auto isPlayingNow = isPlaying.load();
            playHead.isPlaying.store (isPlayingNow);

            if (isPlayingNow)
            {
                const auto currentAudioSourceLength = audioSourceLength.load();
                const auto currentPlayHeadPosition = playHead.timeInSamples.load();

                // Rudimentary attempt to not seek beyond our sample data, assuming a fairly stable numSamples
                // value. We should gain control over calling the AudioProcessorGraph's processBlock() calls so
                // that we can do sample precise looping.
                if (currentAudioSourceLength - currentPlayHeadPosition < numSamples)
                    playHead.timeInSamples.store (0);
                else
                    playHead.timeInSamples.fetch_add (numSamples);
            }

            if (goToStartSignal.exchange (false))
                playHead.timeInSamples.store (0);
        }

        File getAudioSource() const override
        {
            std::lock_guard<std::mutex> lock { instance.innerMutex };

            if (context != nullptr)
                return context->audioFile;

            return {};
        }

        void setAudioSource (File audioSourceFile) override
        {
            if (audioSourceFile.existsAsFile())
            {
                {
                    std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };
                    contextUpdateSource = ContextUpdateSource (std::move (audioSourceFile));
                }

                synchronise();
            }
        }

        void clearAudioSource() override
        {
            {
                std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };
                contextUpdateSource = ContextUpdateSource (ContextUpdateSource::Type::reset);
            }

            synchronise();
        }

        void setPlaying (bool isPlayingIn) override { isPlaying.store (isPlayingIn); }

        void goToStart() override { goToStartSignal.store (true); }

        Editor* createEditor() { return new Editor (*this); }

        AudioPluginInstance& getAudioPluginInstance() { return instance; }

    private:
        /**  Use this to put the plugin in an unprepared state for the duration of adding and removing PlaybackRegions
             to and from Renderers.
        */
        class ScopedPluginDeactivator
        {
        public:
            explicit ScopedPluginDeactivator (ARAPluginInstanceWrapper& inst) : instance (inst)
            {
                if (instance.prepareToPlayParams.isValid)
                    instance.inner->releaseResources();
            }

            ~ScopedPluginDeactivator()
            {
                if (instance.prepareToPlayParams.isValid)
                    instance.inner->prepareToPlay (instance.prepareToPlayParams.sampleRate,
                                                   instance.prepareToPlayParams.samplesPerBlock);
            }

        private:
            ARAPluginInstanceWrapper& instance;

            JUCE_DECLARE_NON_COPYABLE (ScopedPluginDeactivator)
        };

        class ContextUpdateSource
        {
        public:
            enum class Type
            {
                empty,
                audioSourceFile,
                stateInformation,
                reset
            };

            ContextUpdateSource() = default;

            explicit ContextUpdateSource (const File& file)
                : type (Type::audioSourceFile),
                  audioSourceFile (file)
            {
            }

            ContextUpdateSource (const void* d, int s)
                : type (Type::stateInformation),
                  stateInformation (d, (size_t) s)
            {
            }

            ContextUpdateSource (Type t) : type (t)
            {
                jassert (t == Type::reset);
            }

            Type getType() const { return type; }

            const File& getAudioSourceFile() const
            {
                jassert (type == Type::audioSourceFile);

                return audioSourceFile;
            }

            const MemoryBlock& getStateInformation() const
            {
                jassert (type == Type::stateInformation);

                return stateInformation;
            }

        private:
            Type type = Type::empty;

            File audioSourceFile;
            MemoryBlock stateInformation;
        };

        void synchronise()
        {
            const SpinLock::ScopedLockType scope (instance.innerProcessBlockFlag);
            std::lock_guard<std::mutex> configurationLock (instance.innerMutex);
            synchronizeStateWithDocumentController();
        }

        void synchronizeStateWithDocumentController()
        {
            bool resetContext = false;

            auto newContext = [&]() -> std::unique_ptr<Context>
            {
                std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };

                switch (contextUpdateSource.getType())
                {
                    case ContextUpdateSource::Type::empty:
                        return {};

                    case ContextUpdateSource::Type::audioSourceFile:
                        if (! (contextUpdateSource.getAudioSourceFile().existsAsFile()))
                            return {};

                        {
                            const ARAEditGuard editGuard (documentController->getDocumentController());
                            return std::make_unique<Context> (documentController->getDocumentController(),
                                                              contextUpdateSource.getAudioSourceFile());
                        }

                    case ContextUpdateSource::Type::stateInformation:
                        jassert (contextUpdateSource.getStateInformation().getSize() <= std::numeric_limits<int>::max());

                        return Context::createFromStateInformation (documentController->getDocumentController(),
                                                                    contextUpdateSource.getStateInformation().getData(),
                                                                    (int) contextUpdateSource.getStateInformation().getSize());

                    case ContextUpdateSource::Type::reset:
                        resetContext = true;
                        return {};
                }

                jassertfalse;
                return {};
            }();

            if (newContext != nullptr)
            {
                {
                    ScopedPluginDeactivator deactivator (instance);

                    context = std::move (newContext);
                    audioSourceLength.store (context->fileAudioSource.getFormatReader().lengthInSamples);

                    auto& region = context->playbackRegion.getPlaybackRegion();
                    playbackRenderer.add (region);
                    editorRenderer.add (region);
                }

                sendChangeMessage();
            }

            if (resetContext)
            {
                {
                    ScopedPluginDeactivator deactivator (instance);

                    context.reset();
                    audioSourceLength.store (0);
                }

                sendChangeMessage();
            }
        }

        struct Context
        {
            Context (ARA::Host::DocumentController& dc, const File& audioFileIn)
                : audioFile (audioFileIn),
                  musicalContext    (dc),
                  regionSequence    (dc, musicalContext, "track 1"),
                  fileAudioSource   (dc, audioFile),
                  audioModification (dc, fileAudioSource),
                  playbackRegion    (dc, regionSequence, audioModification, fileAudioSource)
            {
            }

            static std::unique_ptr<Context> createFromStateInformation (ARA::Host::DocumentController& dc, const void* d, int s)
            {
                if (auto xml = getXmlFromBinary (d, s))
                {
                    if (xml->hasTagName (xmlRootTag))
                    {
                        File file { xml->getStringAttribute (xmlAudioFileAttrib) };

                        if (file.existsAsFile())
                            return std::make_unique<Context> (dc, std::move (file));
                    }
                }

                return {};
            }

            void getStateInformation (juce::MemoryBlock& b)
            {
                XmlElement root { xmlRootTag };
                root.setAttribute (xmlAudioFileAttrib, audioFile.getFullPathName());
                copyXmlToBinary (root, b);
            }

            const static Identifier xmlRootTag;
            const static Identifier xmlAudioFileAttrib;

            File audioFile;

            MusicalContext musicalContext;
            RegionSequence regionSequence;
            FileAudioSource fileAudioSource;
            AudioModification audioModification;
            PlaybackRegion playbackRegion;
        };

        SimplePlayHead playHead;
        ARAPluginInstanceWrapper& instance;

        std::unique_ptr<ARAHostDocumentController> documentController;
        ARAHostModel::PlaybackRendererInterface playbackRenderer;
        ARAHostModel::EditorRendererInterface editorRenderer;

        std::unique_ptr<Context> context;

        mutable std::mutex contextUpdateSourceMutex;
        ContextUpdateSource contextUpdateSource;

        std::atomic<bool> isPlaying { false };
        std::atomic<bool> goToStartSignal { false };
        std::atomic<int64> audioSourceLength { 0 };
    };

    explicit ARAPluginInstanceWrapper (std::unique_ptr<AudioPluginInstance> innerIn)
        : inner (std::move (innerIn)), araHost (*this)
    {
        jassert (inner != nullptr);

        for (auto isInput : { true, false })
            matchBuses (isInput);

        setBusesLayout (inner->getBusesLayout());
    }

    //==============================================================================
    AudioProcessorEditor* createARAHostEditor() { return araHost.createEditor(); }

    //==============================================================================
    const String getName() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getName();
    }

    StringArray getAlternateDisplayNames() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getAlternateDisplayNames();
    }

    double getTailLengthSeconds() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getTailLengthSeconds();
    }

    bool acceptsMidi() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->acceptsMidi();
    }

    bool producesMidi() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->producesMidi();
    }

    AudioProcessorEditor* createEditor() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->createEditorIfNeeded();
    }

    bool hasEditor() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->hasEditor();
    }

    int getNumPrograms() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getNumPrograms();
    }

    int getCurrentProgram() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getCurrentProgram();
    }

    void setCurrentProgram (int i) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setCurrentProgram (i);
    }

    const String getProgramName (int i) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getProgramName (i);
    }

    void changeProgramName (int i, const String& n) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->changeProgramName (i, n);
    }

    void getStateInformation (juce::MemoryBlock& b) override
    {
        XmlElement state ("ARAPluginInstanceWrapperState");

        {
            MemoryBlock m;
            araHost.getStateInformation (m);
            state.createNewChildElement ("host")->addTextElement (m.toBase64Encoding());
        }

        {
            std::lock_guard<std::mutex> lock (innerMutex);

            MemoryBlock m;
            inner->getStateInformation (m);
            state.createNewChildElement ("plugin")->addTextElement (m.toBase64Encoding());
        }

        copyXmlToBinary (state, b);
    }

    void setStateInformation (const void* d, int s) override
    {
        if (auto xml = getXmlFromBinary (d, s))
        {
            if (xml->hasTagName ("ARAPluginInstanceWrapperState"))
            {
                if (auto* hostState = xml->getChildByName ("host"))
                {
                    MemoryBlock m;
                    m.fromBase64Encoding (hostState->getAllSubText());
                    jassert (m.getSize() <= std::numeric_limits<int>::max());
                    araHost.setStateInformation (m.getData(), (int) m.getSize());
                }

                if (auto* pluginState = xml->getChildByName ("plugin"))
                {
                    std::lock_guard<std::mutex> lock (innerMutex);

                    MemoryBlock m;
                    m.fromBase64Encoding (pluginState->getAllSubText());
                    jassert (m.getSize() <= std::numeric_limits<int>::max());
                    inner->setStateInformation (m.getData(), (int) m.getSize());
                }
            }
        }
    }

    void getCurrentProgramStateInformation (juce::MemoryBlock& b) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->getCurrentProgramStateInformation (b);
    }

    void setCurrentProgramStateInformation (const void* d, int s) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setCurrentProgramStateInformation (d, s);
    }

    void prepareToPlay (double sr, int bs) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setRateAndBufferSizeDetails (sr, bs);
        inner->prepareToPlay (sr, bs);
        prepareToPlayParams = { sr, bs };
    }

    void releaseResources() override { inner->releaseResources(); }

    void memoryWarningReceived() override { inner->memoryWarningReceived(); }

    void processBlock (AudioBuffer<float>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlock (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    void processBlock (AudioBuffer<double>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlock (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    void processBlockBypassed (AudioBuffer<float>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlockBypassed (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    void processBlockBypassed (AudioBuffer<double>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlockBypassed (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    bool supportsDoublePrecisionProcessing() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->supportsDoublePrecisionProcessing();
    }

    bool supportsMPE() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->supportsMPE();
    }

    bool isMidiEffect() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->isMidiEffect();
    }

    void reset() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->reset();
    }

    void setNonRealtime (bool b) noexcept override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setNonRealtime (b);
    }

    void refreshParameterList() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->refreshParameterList();
    }

    void numChannelsChanged() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->numChannelsChanged();
    }

    void numBusesChanged() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->numBusesChanged();
    }

    void processorLayoutsChanged() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->processorLayoutsChanged();
    }

    void setPlayHead (AudioPlayHead* p) override { ignoreUnused (p); }

    void updateTrackProperties (const TrackProperties& p) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->updateTrackProperties (p);
    }

    bool isBusesLayoutSupported (const BusesLayout& layout) const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->checkBusesLayoutSupported (layout);
    }

    bool canAddBus (bool) const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return true;
    }
    bool canRemoveBus (bool) const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return true;
    }

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

private:
    void matchBuses (bool isInput)
    {
        const auto inBuses = inner->getBusCount (isInput);

        while (getBusCount (isInput) < inBuses)
            addBus (isInput);

        while (inBuses < getBusCount (isInput))
            removeBus (isInput);
    }

    // Used for mutual exclusion between the audio and other threads
    SpinLock innerProcessBlockFlag;

    // Used for mutual exclusion on non-audio threads
    mutable std::mutex innerMutex;

    std::unique_ptr<AudioPluginInstance> inner;

    ARATestHost araHost;

    struct PrepareToPlayParams
    {
        PrepareToPlayParams() : isValid (false) {}

        PrepareToPlayParams (double sampleRateIn, int samplesPerBlockIn)
            : isValid (true), sampleRate (sampleRateIn), samplesPerBlock (samplesPerBlockIn)
        {
        }

        bool isValid;
        double sampleRate;
        int samplesPerBlock;
    };

    PrepareToPlayParams prepareToPlayParams;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ARAPluginInstanceWrapper)
};
#endif
