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

   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-8-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.

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

#if JUCE_INTERNAL_HAS_ARA

#include <ARA_Library/Dispatch/ARAHostDispatch.cpp>

namespace juce
{
struct ARAEditGuardState
{
public:
    /*  Returns true if this controller wasn't previously present. */
    bool add (ARA::Host::DocumentController& dc)
    {
        const std::lock_guard<std::mutex> lock (mutex);
        return ++counts[&dc] == 1;
    }

    /*  Returns true if this controller is no longer present. */
    bool remove (ARA::Host::DocumentController& dc)
    {
        const std::lock_guard<std::mutex> lock (mutex);
        return --counts[&dc] == 0;
    }

private:
    std::map<ARA::Host::DocumentController*, int> counts;
    std::mutex mutex;
};

static ARAEditGuardState editGuardState;

ARAEditGuard::ARAEditGuard (ARA::Host::DocumentController& dcIn) : dc (dcIn)
{
    if (editGuardState.add (dc))
        dc.beginEditing();
}

ARAEditGuard::~ARAEditGuard()
{
    if (editGuardState.remove (dc))
        dc.endEditing();
}

//==============================================================================
namespace ARAHostModel
{

//==============================================================================
AudioSource::AudioSource (ARA::ARAAudioSourceHostRef hostRef,
                          ARA::Host::DocumentController& dc,
                          const ARA::ARAAudioSourceProperties& props)
    : ManagedARAHandle (dc, [&]
                        {
                            const ARAEditGuard guard (dc);
                            return dc.createAudioSource (hostRef, &props);
                        }())
{
}

void AudioSource::update (const ARA::ARAAudioSourceProperties& props)
{
    const ARAEditGuard guard (getDocumentController());
    getDocumentController().updateAudioSourceProperties (getPluginRef(), &props);
}

void AudioSource::enableAudioSourceSamplesAccess (bool x)
{
    const ARAEditGuard guard (getDocumentController());
    getDocumentController().enableAudioSourceSamplesAccess (getPluginRef(), x);
}

void AudioSource::destroy (ARA::Host::DocumentController& dc, Ptr ptr)
{
    dc.destroyAudioSource (ptr);
}

//==============================================================================
AudioModification::AudioModification (ARA::ARAAudioModificationHostRef hostRef,
                                      ARA::Host::DocumentController& dc,
                                      AudioSource& s,
                                      const ARA::ARAAudioModificationProperties& props)
    : ManagedARAHandle (dc, [&]
                        {
                            const ARAEditGuard guard (dc);
                            return dc.createAudioModification (s.getPluginRef(), hostRef, &props);
                        }()),
      source (s)
{
}

void AudioModification::update (const ARA::ARAAudioModificationProperties& props)
{
    const ARAEditGuard guard (getDocumentController());
    getDocumentController().updateAudioModificationProperties (getPluginRef(), &props);
}

void AudioModification::destroy (ARA::Host::DocumentController& dc, Ptr ptr)
{
    dc.destroyAudioModification (ptr);
}

//==============================================================================
class PlaybackRegion::Impl  : public ManagedARAHandle<Impl, ARA::ARAPlaybackRegionRef>,
                              public DeletionListener
{
public:
    Impl (ARA::ARAPlaybackRegionHostRef hostRef,
          ARA::Host::DocumentController& dc,
          AudioModification& m,
          const ARA::ARAPlaybackRegionProperties& props);

    ~Impl() override
    {
        for (const auto& l : listeners)
            l->removeListener (*this);
    }

    /*  Updates the state of the corresponding %ARA model object.

        Places the DocumentController in editable state.

        You can use getEmptyProperties() to acquire a properties struct where the `structSize`
        field has already been correctly set.
    */
    void update (const ARA::ARAPlaybackRegionProperties& props);

    auto& getAudioModification() const { return modification; }

    static void destroy (ARA::Host::DocumentController&, Ptr);

    void addListener    (DeletionListener& l)                   { listeners.insert (&l); }
    void removeListener (DeletionListener& l) noexcept override { listeners.erase (&l); }

private:
    AudioModification* modification = nullptr;
    std::unordered_set<DeletionListener*> listeners;
};

PlaybackRegion::Impl::Impl (ARA::ARAPlaybackRegionHostRef hostRef,
                            ARA::Host::DocumentController& dc,
                            AudioModification& m,
                            const ARA::ARAPlaybackRegionProperties& props)
    : ManagedARAHandle (dc, [&]
                        {
                            const ARAEditGuard guard (dc);
                            return dc.createPlaybackRegion (m.getPluginRef(), hostRef, &props);
                        }()),
      modification (&m)
{
}

PlaybackRegion::~PlaybackRegion() = default;

void PlaybackRegion::Impl::update (const ARA::ARAPlaybackRegionProperties& props)
{
    const ARAEditGuard guard (getDocumentController());
    getDocumentController().updatePlaybackRegionProperties (getPluginRef(), &props);
}

void PlaybackRegion::Impl::destroy (ARA::Host::DocumentController& dc, Ptr ptr)
{
    dc.destroyPlaybackRegion (ptr);
}

PlaybackRegion::PlaybackRegion (ARA::ARAPlaybackRegionHostRef hostRef,
                                ARA::Host::DocumentController& dc,
                                AudioModification& m,
                                const ARA::ARAPlaybackRegionProperties& props)
    : impl (std::make_unique<Impl> (hostRef, dc, m, props))
{
}

void PlaybackRegion::update (const ARA::ARAPlaybackRegionProperties& props) { impl->update (props); }

void PlaybackRegion::addListener (DeletionListener& x) { impl->addListener (x); }

auto& PlaybackRegion::getAudioModification() const { return impl->getAudioModification(); }

ARA::ARAPlaybackRegionRef PlaybackRegion::getPluginRef() const noexcept { return impl->getPluginRef(); }

DeletionListener& PlaybackRegion::getDeletionListener() const noexcept { return *impl.get(); }

//==============================================================================
MusicalContext::MusicalContext (ARA::ARAMusicalContextHostRef hostRef,
                                ARA::Host::DocumentController& dc,
                                const ARA::ARAMusicalContextProperties& props)
    : ManagedARAHandle (dc, [&]
                        {
                            const ARAEditGuard guard (dc);
                            return dc.createMusicalContext (hostRef, &props);
                        }())
{
}

void MusicalContext::update (const ARA::ARAMusicalContextProperties& props)
{
    const ARAEditGuard guard (getDocumentController());
    return getDocumentController().updateMusicalContextProperties (getPluginRef(), &props);
}

void MusicalContext::destroy (ARA::Host::DocumentController& dc, Ptr ptr)
{
    dc.destroyMusicalContext (ptr);
}

//==============================================================================
RegionSequence::RegionSequence (ARA::ARARegionSequenceHostRef hostRef,
                                ARA::Host::DocumentController& dc,
                                const ARA::ARARegionSequenceProperties& props)
    : ManagedARAHandle (dc, [&]
                        {
                            const ARAEditGuard guard (dc);
                            return dc.createRegionSequence (hostRef, &props);
                        }())
{
}

void RegionSequence::update (const ARA::ARARegionSequenceProperties& props)
{
    const ARAEditGuard guard (getDocumentController());
    return getDocumentController().updateRegionSequenceProperties (getPluginRef(), &props);
}

void RegionSequence::destroy (ARA::Host::DocumentController& dc, Ptr ptr)
{
    dc.destroyRegionSequence (ptr);
}

//==============================================================================
PlaybackRendererInterface PlugInExtensionInstance::getPlaybackRendererInterface() const
{
    if (instance != nullptr)
        return PlaybackRendererInterface (instance->playbackRendererRef, instance->playbackRendererInterface);

    return {};
}

EditorRendererInterface PlugInExtensionInstance::getEditorRendererInterface() const
{
    if (instance != nullptr)
        return EditorRendererInterface (instance->editorRendererRef, instance->editorRendererInterface);

    return {};
}

} // namespace ARAHostModel

//==============================================================================
class ARAHostDocumentController::Impl
{
public:
    Impl (ARAFactoryWrapper araFactoryIn,
          std::unique_ptr<ARA::Host::DocumentControllerHostInstance>&& dcHostInstanceIn,
          const ARA::ARADocumentControllerInstance* documentControllerInstance,
          std::unique_ptr<ARA::Host::AudioAccessControllerInterface>&& audioAccessControllerIn,
          std::unique_ptr<ARA::Host::ArchivingControllerInterface>&& archivingControllerIn,
          std::unique_ptr<ARA::Host::ContentAccessControllerInterface>&& contentAccessControllerIn,
          std::unique_ptr<ARA::Host::ModelUpdateControllerInterface>&& modelUpdateControllerIn,
          std::unique_ptr<ARA::Host::PlaybackControllerInterface>&& playbackControllerIn)
        : araFactory (std::move (araFactoryIn)),
          audioAccessController (std::move (audioAccessControllerIn)),
          archivingController (std::move (archivingControllerIn)),
          contentAccessController (std::move (contentAccessControllerIn)),
          modelUpdateController (std::move (modelUpdateControllerIn)),
          playbackController (std::move (playbackControllerIn)),
          dcHostInstance (std::move (dcHostInstanceIn)),
          documentController (documentControllerInstance)
    {
    }

    ~Impl()
    {
        documentController.destroyDocumentController();
    }

    static std::unique_ptr<Impl>
        createImpl (ARAFactoryWrapper araFactory,
                    const String& documentName,
                    std::unique_ptr<ARA::Host::AudioAccessControllerInterface>&& audioAccessController,
                    std::unique_ptr<ARA::Host::ArchivingControllerInterface>&& archivingController,
                    std::unique_ptr<ARA::Host::ContentAccessControllerInterface>&& contentAccessController,
                    std::unique_ptr<ARA::Host::ModelUpdateControllerInterface>&& modelUpdateController,
                    std::unique_ptr<ARA::Host::PlaybackControllerInterface>&& playbackController)
    {
        std::unique_ptr<ARA::Host::DocumentControllerHostInstance> dcHostInstance =
            std::make_unique<ARA::Host::DocumentControllerHostInstance> (audioAccessController.get(),
                                                                         archivingController.get(),
                                                                         contentAccessController.get(),
                                                                         modelUpdateController.get(),
                                                                         playbackController.get());

        const auto documentProperties = makeARASizedStruct (&ARA::ARADocumentProperties::name, documentName.toRawUTF8());

        if (auto* dci = araFactory.get()->createDocumentControllerWithDocument (dcHostInstance.get(), &documentProperties))
            return std::make_unique<Impl> (std::move (araFactory),
                                           std::move (dcHostInstance),
                                           dci,
                                           std::move (audioAccessController),
                                           std::move (archivingController),
                                           std::move (contentAccessController),
                                           std::move (modelUpdateController),
                                           std::move (playbackController));

        return {};
    }

    ARAHostModel::PlugInExtensionInstance bindDocumentToPluginInstance (AudioPluginInstance& instance,
                                                                        ARA::ARAPlugInInstanceRoleFlags knownRoles,
                                                                        ARA::ARAPlugInInstanceRoleFlags assignedRoles)
    {

        const auto makeVisitor = [] (auto vst3Fn, auto auFn)
        {
            using Vst3Fn = decltype (vst3Fn);
            using AuFn = decltype (auFn);

            struct Visitor final : public ExtensionsVisitor, Vst3Fn, AuFn
            {
                explicit Visitor (Vst3Fn vst3Fn, AuFn auFn) : Vst3Fn (std::move (vst3Fn)), AuFn (std::move (auFn)) {}
                void visitVST3Client (const VST3Client& x) override { Vst3Fn::operator() (x); }
                void visitAudioUnitClient (const AudioUnitClient& x) override { AuFn::operator() (x); }
            };

            return Visitor { std::move (vst3Fn), std::move (auFn) };
        };

        const ARA::ARAPlugInExtensionInstance* pei = nullptr;
        auto visitor = makeVisitor ([this, &pei, knownRoles, assignedRoles] (const ExtensionsVisitor::VST3Client& vst3Client)
                                    {
                                        auto* iComponentPtr = vst3Client.getIComponentPtr();
                                        VSTComSmartPtr<ARA::IPlugInEntryPoint2> araEntryPoint;

                                        if (araEntryPoint.loadFrom (iComponentPtr))
                                            pei = araEntryPoint->bindToDocumentControllerWithRoles (documentController.getRef(), knownRoles, assignedRoles);
                                    },
                                   #if JUCE_INTERNAL_HAS_AU
                                    [this, &pei, knownRoles, assignedRoles] (const ExtensionsVisitor::AudioUnitClient& auClient)
                                    {
                                        auto audioUnit = auClient.getAudioUnitHandle();
                                        auto propertySize = (UInt32) sizeof (ARA::ARAAudioUnitPlugInExtensionBinding);
                                        const auto expectedPropertySize = propertySize;
                                        ARA::ARAAudioUnitPlugInExtensionBinding audioUnitBinding { ARA::kARAAudioUnitMagic,
                                                                                                   documentController.getRef(),
                                                                                                   nullptr,
                                                                                                   knownRoles,
                                                                                                   assignedRoles };

                                        auto status = AudioUnitGetProperty (audioUnit,
                                                                            ARA::kAudioUnitProperty_ARAPlugInExtensionBindingWithRoles,
                                                                            kAudioUnitScope_Global,
                                                                            0,
                                                                            &audioUnitBinding,
                                                                            &propertySize);

                                        if (status == noErr
                                            && propertySize == expectedPropertySize
                                            && audioUnitBinding.inOutMagicNumber == ARA::kARAAudioUnitMagic
                                            && audioUnitBinding.inDocumentControllerRef == documentController.getRef()
                                            && audioUnitBinding.outPlugInExtension != nullptr)
                                        {
                                            pei = audioUnitBinding.outPlugInExtension;
                                        }
                                        else
                                            jassertfalse;
                                    }
                                   #else
                                    [] (const auto&) {}
                                   #endif
                                    );

        instance.getExtensions (visitor);
        return ARAHostModel::PlugInExtensionInstance { pei };
    }

    auto& getDocumentController()       { return documentController; }

private:
    ARAFactoryWrapper araFactory;

    std::unique_ptr<ARA::Host::AudioAccessControllerInterface>   audioAccessController;
    std::unique_ptr<ARA::Host::ArchivingControllerInterface>     archivingController;
    std::unique_ptr<ARA::Host::ContentAccessControllerInterface> contentAccessController;
    std::unique_ptr<ARA::Host::ModelUpdateControllerInterface>   modelUpdateController;
    std::unique_ptr<ARA::Host::PlaybackControllerInterface>      playbackController;

    std::unique_ptr<ARA::Host::DocumentControllerHostInstance> dcHostInstance;
    ARA::Host::DocumentController documentController;
};

ARAHostDocumentController::ARAHostDocumentController (std::unique_ptr<Impl>&& implIn)
    : impl { std::move (implIn) }
{}

std::unique_ptr<ARAHostDocumentController> ARAHostDocumentController::create (ARAFactoryWrapper factory,
                                                                              const String& documentName,
                                                                              std::unique_ptr<ARA::Host::AudioAccessControllerInterface> audioAccessController,
                                                                              std::unique_ptr<ARA::Host::ArchivingControllerInterface> archivingController,
                                                                              std::unique_ptr<ARA::Host::ContentAccessControllerInterface> contentAccessController,
                                                                              std::unique_ptr<ARA::Host::ModelUpdateControllerInterface> modelUpdateController,
                                                                              std::unique_ptr<ARA::Host::PlaybackControllerInterface> playbackController)
{
    if (auto impl = Impl::createImpl (std::move (factory),
                                      documentName,
                                      std::move (audioAccessController),
                                      std::move (archivingController),
                                      std::move (contentAccessController),
                                      std::move (modelUpdateController),
                                      std::move (playbackController)))
    {
        return rawToUniquePtr (new ARAHostDocumentController (std::move (impl)));
    }

    return {};
}

ARAHostDocumentController::~ARAHostDocumentController() = default;

ARA::Host::DocumentController& ARAHostDocumentController::getDocumentController() const
{
    return impl->getDocumentController();
}

ARAHostModel::PlugInExtensionInstance ARAHostDocumentController::bindDocumentToPluginInstance (AudioPluginInstance& instance,
                                                                                               ARA::ARAPlugInInstanceRoleFlags knownRoles,
                                                                                               ARA::ARAPlugInInstanceRoleFlags assignedRoles)
{
    return impl->bindDocumentToPluginInstance (instance, knownRoles, assignedRoles);
}

void createARAFactoryAsync (AudioPluginInstance& instance, std::function<void (ARAFactoryWrapper)> cb)
{
    if (! instance.getPluginDescription().hasARAExtension)
        cb (ARAFactoryWrapper{});

    struct Extensions final : public ExtensionsVisitor
    {
        Extensions (std::function<void (ARAFactoryWrapper)> callbackIn)
            : callback (std::move (callbackIn))
        {}

        void visitARAClient (const ARAClient& araClient) override
        {
            araClient.createARAFactoryAsync (std::move (callback));
        }

        std::function<void (ARAFactoryWrapper)> callback;
    };

    Extensions extensions { std::move (cb) };
    instance.getExtensions (extensions);
}

} // namespace juce

#endif
