//
// Copyright 2016 Pixar
//
// Licensed under the terms set forth in the LICENSE.txt file available at
// https://openusd.org/license.
//
#ifndef PXR_IMAGING_GLF_GL_CONTEXT_H
#define PXR_IMAGING_GLF_GL_CONTEXT_H

#include "pxr/pxr.h"
#include "pxr/imaging/glf/api.h"
#include "pxr/base/arch/threads.h"

#include <memory>

PXR_NAMESPACE_OPEN_SCOPE


typedef std::shared_ptr<class GlfGLContext> GlfGLContextSharedPtr;

/// \class GlfGLContext
///
/// Provides window system independent access to GL contexts.
///
/// All OpenGL operation occurs within a current GL Context.  The GL
/// contexts used by an application are allocated and managed by the window
/// system interface layer, i.e. Qt, GLUT, GLX, etc.
///
/// This class provides a way for lower-level OpenGL framework code to
/// get useful information about the GL contexts in use by the application.
///
/// This mechanism depends on the application code registering callbacks to
/// provide access to its GL contexts.
///
class GlfGLContext
{
public:
    GLF_API
    virtual ~GlfGLContext();

    // Disallow copies
    GlfGLContext(const GlfGLContext&) = delete;
    GlfGLContext& operator=(const GlfGLContext&) = delete;

    /// Returns an instance for the current GL context.
    GLF_API
    static GlfGLContextSharedPtr GetCurrentGLContext();

    /// Returns an instance for the shared GL context.
    GLF_API
    static GlfGLContextSharedPtr GetSharedGLContext();

    /// Makes \p context current if valid, otherwise makes no context current.
    GLF_API
    static void MakeCurrent(const GlfGLContextSharedPtr& context);

    /// Returns \c true if \a context1 and \a context2 are sharing.
    GLF_API
    static bool AreSharing(GlfGLContextSharedPtr const & context1,
                           GlfGLContextSharedPtr const & context2);

    /// Returns whether this interface has been initialized.
    GLF_API
    static bool IsInitialized();

    /// Returns \c true if this context is current.
    GLF_API
    bool IsCurrent() const;

private:
    /// Makes this context current.
    ///
    /// If the context is not valid this does nothing.
    void MakeCurrent();

public:
    /// Makes no context current.
    GLF_API
    static void DoneCurrent();

    /// Returns \c true if this context is sharing with \a otherContext.
    GLF_API
    bool IsSharing(GlfGLContextSharedPtr const & otherContext);

    /// Returns \c true if this context is valid.
    virtual bool IsValid() const = 0;

    /// Creates a new GlfContext that shares GL resources with this context.
    /// The purpose of this function is to be able to create a new GL context
    /// on a second thread that shares with the context on the main-thread.
    /// If the GlfContext implementation does not support sharing contexts
    /// null is returned. Example usage:
    /// Main-thread:
    ///     RegisterGLContextCallbacks();
    ///     GlfGLContext::MakeCurrent(...);
    /// Second-thread:
    ///     s = GetCurrentGLContext();
    ///     c = s->CreateSharingContext();
    virtual GlfGLContextSharedPtr CreateSharingContext() {return nullptr;}

protected:
    GLF_API
    GlfGLContext();

    /// Makes this context current.
    virtual void _MakeCurrent() = 0;

    /// Returns \c true if this context is sharing with \a rhs.
    virtual bool _IsSharing(const GlfGLContextSharedPtr& rhs) const = 0;

    /// Returns \c true if this context is equal to \p rhs.
    virtual bool _IsEqual(const GlfGLContextSharedPtr& rhs) const = 0;
};

/// \class GlfGLContextScopeHolder
///
/// Helper class to make a GL context current.
///
/// It is often useful to wrap a dynamic GL resource with a class interface.
///
/// In addition to providing API to make it more convenient to use the
/// underlying GL resource, the lifetime of the underlying resource can
/// be tied to the lifetime of a wrapper object instance, e.g. allocate
/// the GL resource during construction, delete the GL resource during
/// destruction.
///
/// While the construction and use of these kinds of wrapper objects is
/// usually pretty safe and straightforward, it can be more difficult to
/// manage destruction.  Specifically, it can be hard to guarantee that
/// a suitable GL context is current at the time that a wrapper object
/// instance is destroyed.  If a suitable context is not current, then
/// it will not be possible to delete the underlying resource, which
/// may cause the resource to remain allocated, which will then result
/// in a resource leak.
///
/// Typically, these GL resources are allocated from contexts which are
/// sharing the GL resources.  In which case it is sufficient for any one
/// one of the sharing contexts to be current in order to be able to safely
/// delete the GL resource from the destructor of a wrapper object.
///
/// GlfGLContext and GlfGLContextScopeHolder can help.
///
/// When GlfGLContext has been initialized, i.e. when suitable context
/// callbacks have been registered, we can use GlfGLContext to make
/// a GL context current.
///
/// GlfGLContextScopeHolder does this automatically for the duration
/// of a code block.
///
/// The underlying calls to make GL contexts current can be moderately
/// expensive.  So, this mechanism should be used carefully.
///
class GlfGLContextScopeHolder
{
public:
    /// Make the given context current and restore the current context
    /// when this object is destroyed.
    GLF_API
    explicit GlfGLContextScopeHolder(const GlfGLContextSharedPtr& newContext);

    GLF_API
    ~GlfGLContextScopeHolder();

    GlfGLContextScopeHolder(const GlfGLContextScopeHolder&) = delete;
    GlfGLContextScopeHolder& operator=(const GlfGLContextScopeHolder) = delete;

protected:
    GLF_API
    void _MakeNewContextCurrent();
    GLF_API
    void _RestoreOldContext();

private:
    GlfGLContextSharedPtr _newContext;
    GlfGLContextSharedPtr _oldContext;
};

/// \class GlfSharedGLContextScopeHolder
///
/// Helper class to make the shared GL context current.
///
/// Example:
///
/// \code
///     class MyTexture {
///     public:
///         MyTexture() : _textureId(0) {
///             // allocate from the shared context pool.
///             GlfSharedGLContextScopeHolder sharedContextScopeHolder;
///             glGenTextures(1, &_textureId);
///         }
///
///         ~MyTexture() {
///             // delete from the shared context pool.
///             GlfSharedGLContextScopeHolder sharedContextScopeHolder;
///             glDeleteTextures(1, &_texureId);
///             _textureId = 0;
///         }
///
///         // The caller is responsible for making sure that a suitable
///         // GL context is current before calling other methods.
///
///         void Bind() {
///             glBindTexture(GL_TEXTURE_2D, _textureId);
///         }
///
///         void Unbind() {
///             glBindTexture(GL_TEXTURE_2D, 0);
///         }
///
///         ...
///
///     private:
///         GLuint _textureId;
///
///     };
/// \endcode
///
class GlfSharedGLContextScopeHolder final : private GlfGLContextScopeHolder
{
public:
    GlfSharedGLContextScopeHolder() :
        GlfGLContextScopeHolder(_GetSharedContext())
    {
        // Do nothing
    }

private:
    static GlfGLContextSharedPtr _GetSharedContext()
    {
        if (GlfGLContext::IsInitialized() && ArchIsMainThread()) {
            return GlfGLContext::GetSharedGLContext();
        }
        return GlfGLContextSharedPtr();
    }
};

#define API_GLF_HAS_ANY_GL_CONTEXT_SCOPE_HOLDER 1

/// \class GlfAnyGLContextScopeHolder
///
/// Helper class to make the shared GL context current if there is no
/// valid current context, or when the current context isn't sharing with the
/// shared context.
/// This mechanism lets us skip context switches to the shared context (see
/// GlfSharedGLContextScopeHolder) when possible, while ensuring that we have a
/// valid GL context prior to any resource allocation or destruction.
///
/// Example:
///
/// \code
///     class MyTexture {
///     public:
///         MyTexture() : _textureId(0) {
///             // Ensure we have valid GL context to allocate
///             GlfAnyGLContextScopeHolder anyContextScopeHolder;
///             glGenTextures(1, &_textureId);
///         }
///
///         ~MyTexture() {
///             // Ensure we have valid GL context to delete
///             GlfAnyGLContextScopeHolder anyContextScopeHolder;
///             glDeleteTextures(1, &_texureId);
///             _textureId = 0;
///         }
///
///         // The caller is responsible for making sure that a suitable
///         // GL context is current before calling other methods.
///
///         void Bind() {
///             glBindTexture(GL_TEXTURE_2D, _textureId);
///         }
///
///         void Unbind() {
///             glBindTexture(GL_TEXTURE_2D, 0);
///         }
///
///         ...
///
///     private:
///         GLuint _textureId;
///
///     };
/// \endcode
///
class GlfAnyGLContextScopeHolder final : private GlfGLContextScopeHolder
{
public:
    GlfAnyGLContextScopeHolder() :
        GlfGLContextScopeHolder(_GetAnyContext())
    {
        // Do nothing
    }

private:
    static GlfGLContextSharedPtr _GetAnyContext()
    {
        if (GlfGLContext::IsInitialized() && ArchIsMainThread()) {
            GlfGLContextSharedPtr const current =
                GlfGLContext::GetCurrentGLContext();
            if (!(current &&
                  current->IsValid() &&
                  current->IsSharing(GlfGLContext::GetSharedGLContext()))) {
                return GlfGLContext::GetSharedGLContext();
            }
        }
        return GlfGLContextSharedPtr();
    }
};

/// \class GlfGLContextRegistrationInterface
///
/// Interface for registering a GlfGLContext system.
///
/// If you subclass GlfGLContext you should subclass this type and
/// instantiate an instance on the heap.  It will be cleaned up
/// automatically.
class GlfGLContextRegistrationInterface
{
public:
    GLF_API
    virtual ~GlfGLContextRegistrationInterface();

    // Disallow copies
    GlfGLContextRegistrationInterface(
        const GlfGLContextRegistrationInterface&) = delete;
    GlfGLContextRegistrationInterface& operator=(
        const GlfGLContextRegistrationInterface&) = delete;

    /// If this GLContext system supports a shared context this should
    /// return it.  This will be called at most once.
    virtual GlfGLContextSharedPtr GetShared() = 0;

    /// Whatever your GLContext system thinks is the current GL context
    /// may not really be the current context if another system has since
    /// changed the context.  This method should return what it thinks is
    /// the current context.  If it thinks there is no current context it
    /// should return \c NULL.
    virtual GlfGLContextSharedPtr GetCurrent() = 0;

protected:
    GLF_API
    GlfGLContextRegistrationInterface();
};


PXR_NAMESPACE_CLOSE_SCOPE

#endif
