// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include <AppKit/AppKit.h>

#include "qcocoaglcontext.h"
#include "qcocoawindow.h"
#include "qcocoahelpers.h"
#include "qcocoascreen.h"

#include <QtCore/private/qcore_mac_p.h>

#include <qdebug.h>
#include <dlfcn.h>

static inline QByteArray getGlString(GLenum param)
{
    if (const GLubyte *s = glGetString(param))
        return QByteArray(reinterpret_cast<const char*>(s));
    return QByteArray();
}

QT_BEGIN_NAMESPACE

Q_LOGGING_CATEGORY(lcQpaOpenGLContext, "qt.qpa.openglcontext", QtWarningMsg);

QCocoaGLContext::QCocoaGLContext(QOpenGLContext *context)
    : QPlatformOpenGLContext()
    , m_format(context->format())
{
}

QCocoaGLContext::QCocoaGLContext(NSOpenGLContext *nativeContext)
    : QPlatformOpenGLContext()
{
    m_context = [nativeContext retain];
}

void QCocoaGLContext::initialize()
{
    if (m_context) {
       // Note: We have no way of knowing whether the NSOpenGLContext was created with the
       // share context as reported by the QOpenGLContext, but we just have to trust that
       // it was. It's okey, as the only thing we're using it for is to report isShared().
       if (QPlatformOpenGLContext *shareContext = context()->shareHandle())
           m_shareContext = static_cast<QCocoaGLContext *>(shareContext)->nativeContext();

       updateSurfaceFormat();
       return;
    }

    // ----------- Default case, we own the NSOpenGLContext -----------

    // We only support OpenGL contexts under Cocoa
    if (m_format.renderableType() == QSurfaceFormat::DefaultRenderableType)
        m_format.setRenderableType(QSurfaceFormat::OpenGL);
    if (m_format.renderableType() != QSurfaceFormat::OpenGL)
        return;

    if (QPlatformOpenGLContext *shareContext = context()->shareHandle()) {
        m_shareContext = static_cast<QCocoaGLContext *>(shareContext)->nativeContext();

        // Allow sharing between 3.2 Core and 4.1 Core profile versions in
        // cases where NSOpenGLContext creates a 4.1 context where a 3.2
        // context was requested. Due to the semantics of QSurfaceFormat
        // this 4.1 version can find its way onto the format for the new
        // context, even though it was at no point requested by the user.
        GLint shareContextRequestedProfile;
        [m_shareContext.pixelFormat getValues:&shareContextRequestedProfile
            forAttribute:NSOpenGLPFAOpenGLProfile forVirtualScreen:0];
        auto shareContextActualProfile = shareContext->format().version();

        if (shareContextRequestedProfile == NSOpenGLProfileVersion3_2Core
            && shareContextActualProfile >= qMakePair(4, 1)) {
            // There is a mismatch. Downgrade requested format to make the
            // NSOpenGLPFAOpenGLProfile attributes match. (NSOpenGLContext
            // will fail to create a new context if there is a mismatch).
            if (m_format.version() >= qMakePair(4, 1))
                m_format.setVersion(3, 2);
        }
    }

    // ------------------------- Create NSOpenGLContext -------------------------

    NSOpenGLPixelFormat *pixelFormat = [pixelFormatForSurfaceFormat(m_format) autorelease];
    m_context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:m_shareContext];

    if (!m_context && m_shareContext) {
        qCWarning(lcQpaOpenGLContext, "Could not create NSOpenGLContext with shared context, "
            "falling back to unshared context.");
        m_context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
        m_shareContext = nil;
    }

    if (!m_context) {
        qCWarning(lcQpaOpenGLContext, "Failed to create NSOpenGLContext");
        return;
    }

    // --------------------- Set NSOpenGLContext properties ---------------------

    const GLint interval = m_format.swapInterval() >= 0 ? m_format.swapInterval() : 1;
    [m_context setValues:&interval forParameter:NSOpenGLContextParameterSwapInterval];

    if (m_format.alphaBufferSize() > 0) {
        int zeroOpacity = 0;
        [m_context setValues:&zeroOpacity forParameter:NSOpenGLContextParameterSurfaceOpacity];
    }

    // OpenGL surfaces can be ordered either above(default) or below the NSWindow
    // FIXME: Promote to QSurfaceFormat option or property
    const GLint order = qt_mac_resolveOption(1, "QT_MAC_OPENGL_SURFACE_ORDER");
    [m_context setValues:&order forParameter:NSOpenGLContextParameterSurfaceOrder];

    updateSurfaceFormat();

    qCDebug(lcQpaOpenGLContext).verbosity(3) << "Created" << this << "based on requested" << context()->format();
}

NSOpenGLPixelFormat *QCocoaGLContext::pixelFormatForSurfaceFormat(const QSurfaceFormat &format)
{
    QVector<NSOpenGLPixelFormatAttribute> attrs;

    attrs << NSOpenGLPFAOpenGLProfile;
    if (format.profile() == QSurfaceFormat::CoreProfile) {
        if (format.version() >= qMakePair(4, 1))
            attrs << NSOpenGLProfileVersion4_1Core;
        else if (format.version() >= qMakePair(3, 2))
            attrs << NSOpenGLProfileVersion3_2Core;
        else
            attrs << NSOpenGLProfileVersionLegacy;
    } else {
        attrs << NSOpenGLProfileVersionLegacy;
    }

    switch (format.swapBehavior()) {
    case QSurfaceFormat::SingleBuffer:
        break; // The NSOpenGLPixelFormat default, no attribute to set
    case QSurfaceFormat::DefaultSwapBehavior:
        // Technically this should be single-buffered, but we force double-buffered
        // FIXME: Why do we force double-buffered?
        Q_FALLTHROUGH();
    case QSurfaceFormat::DoubleBuffer:
        attrs.append(NSOpenGLPFADoubleBuffer);
        break;
    case QSurfaceFormat::TripleBuffer:
        attrs.append(NSOpenGLPFATripleBuffer);
        break;
    }

    if (format.depthBufferSize() > 0)
        attrs <<  NSOpenGLPFADepthSize << format.depthBufferSize();
    if (format.stencilBufferSize() > 0)
        attrs << NSOpenGLPFAStencilSize << format.stencilBufferSize();
    if (format.alphaBufferSize() > 0)
        attrs << NSOpenGLPFAAlphaSize << format.alphaBufferSize();

    auto rbz = format.redBufferSize();
    auto gbz = format.greenBufferSize();
    auto bbz = format.blueBufferSize();
    if (rbz > 0 || gbz > 0 || bbz > 0) {
        auto fallbackSize = qMax(rbz, qMax(gbz, bbz));
        auto colorSize = (rbz > 0 ? rbz : fallbackSize)
                       + (gbz > 0 ? gbz : fallbackSize)
                       + (bbz > 0 ? bbz : fallbackSize);
        attrs << NSOpenGLPFAColorSize << colorSize << NSOpenGLPFAMinimumPolicy;
    }

    if (format.samples() > 0) {
        attrs << NSOpenGLPFAMultisample
              << NSOpenGLPFASampleBuffers << NSOpenGLPixelFormatAttribute(1)
              << NSOpenGLPFASamples << NSOpenGLPixelFormatAttribute(format.samples());
    }

    //Workaround for problems with Chromium and offline renderers on the lat 2013 MacPros.
    //FIXME: Think if this could be solved via QSurfaceFormat in the future.
    static bool offlineRenderersAllowed = qEnvironmentVariableIsEmpty("QT_MAC_PRO_WEBENGINE_WORKAROUND");
    if (offlineRenderersAllowed) {
        // Allow rendering on GPUs without a connected display
        attrs << NSOpenGLPFAAllowOfflineRenderers;
    }

    if (qGuiApp->testAttribute(Qt::AA_UseSoftwareOpenGL)) {
        // kCGLRendererGenericFloatID is the modern software renderer on macOS,
        // as opposed to kCGLRendererGenericID, which is deprecated.
        attrs << NSOpenGLPFARendererID << kCGLRendererGenericFloatID;
    }

    attrs << 0; // 0-terminate array
    return [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs.constData()];
}

/*!
    Updates the surface format of this context based on properties of
    the native context and GL state, so that the result of creating
    the context is reflected back in QOpenGLContext.
*/
void QCocoaGLContext::updateSurfaceFormat()
{
    NSOpenGLContext *oldContext = [NSOpenGLContext currentContext];
    [m_context makeCurrentContext];

    // --------------------- Query GL state ---------------------

    int major = 0, minor = 0;
    QByteArray versionString(getGlString(GL_VERSION));
    if (QPlatformOpenGLContext::parseOpenGLVersion(versionString, major, minor)) {
        m_format.setMajorVersion(major);
        m_format.setMinorVersion(minor);
    }

    m_format.setProfile(QSurfaceFormat::NoProfile);
    if (m_format.version() >= qMakePair(3, 2)) {
        GLint value = 0;
        glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &value);
        if (value & GL_CONTEXT_CORE_PROFILE_BIT)
            m_format.setProfile(QSurfaceFormat::CoreProfile);
        else if (value & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT)
            m_format.setProfile(QSurfaceFormat::CompatibilityProfile);
    }

    m_format.setOption(QSurfaceFormat::DeprecatedFunctions, [&]() {
        if (m_format.version() < qMakePair(3, 0)) {
            return true;
        } else {
            GLint value = 0;
            glGetIntegerv(GL_CONTEXT_FLAGS, &value);
            return !(value & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT);
        }
    }());

    // Debug contexts not supported on macOS
    m_format.setOption(QSurfaceFormat::DebugContext, false);

    // Nor are stereo buffers (deprecated in macOS 10.12)
    m_format.setOption(QSurfaceFormat::StereoBuffers, false);

    // ------------------ Query the pixel format ------------------

    NSOpenGLPixelFormat *pixelFormat = m_context.pixelFormat;

    GLint virtualScreen = [&, this]() {
        auto *platformScreen = static_cast<QCocoaScreen*>(context()->screen()->handle());
        auto displayId = platformScreen->nativeScreen().qt_displayId;
        auto requestedDisplay = CGDisplayIDToOpenGLDisplayMask(displayId);
        for (int i = 0; i < pixelFormat.numberOfVirtualScreens; ++i) {
            GLint supportedDisplays;
            [pixelFormat getValues:&supportedDisplays forAttribute:NSOpenGLPFAScreenMask forVirtualScreen:i];
            // Note: The mask returned for NSOpenGLPFAScreenMask is a bit mask of
            // physical displays that the renderer can drive, while the one returned
            // from CGDisplayIDToOpenGLDisplayMask has a single bit set, representing
            // that particular display.
            if (requestedDisplay & supportedDisplays)
                return i;
        }
        qCWarning(lcQpaOpenGLContext) << "Could not find virtual screen for"
                    << platformScreen << "with displayId" << displayId;
        return 0;
    }();

    auto pixelFormatAttribute = [&](NSOpenGLPixelFormatAttribute attribute) {
        int value = 0;
        [pixelFormat getValues:&value forAttribute:attribute forVirtualScreen:virtualScreen];
        return value;
    };

    int colorSize = pixelFormatAttribute(NSOpenGLPFAColorSize);
    colorSize /= 4; // The attribute includes the alpha component
    m_format.setRedBufferSize(colorSize);
    m_format.setGreenBufferSize(colorSize);
    m_format.setBlueBufferSize(colorSize);

    // Surfaces on macOS always have an alpha channel, but unless the user requested
    // one via setAlphaBufferSize(), which triggered setting NSOpenGLCPSurfaceOpacity
    // to make the surface non-opaque, we don't want to report back the actual alpha
    // size, as that will make the user believe the alpha channel can be used for
    // something useful, when in reality it can't, due to the surface being opaque.
    if (m_format.alphaBufferSize() > 0)
        m_format.setAlphaBufferSize(pixelFormatAttribute(NSOpenGLPFAAlphaSize));

    m_format.setDepthBufferSize(pixelFormatAttribute(NSOpenGLPFADepthSize));
    m_format.setStencilBufferSize(pixelFormatAttribute(NSOpenGLPFAStencilSize));
    m_format.setSamples(pixelFormatAttribute(NSOpenGLPFASamples));

    if (pixelFormatAttribute(NSOpenGLPFATripleBuffer))
        m_format.setSwapBehavior(QSurfaceFormat::TripleBuffer);
    else if (pixelFormatAttribute(NSOpenGLPFADoubleBuffer))
        m_format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
    else
        m_format.setSwapBehavior(QSurfaceFormat::SingleBuffer);

    // ------------------- Query the context -------------------

    auto glContextParameter = [&](NSOpenGLContextParameter parameter) {
        int value = 0;
        [m_context getValues:&value forParameter:parameter];
        return value;
    };

    m_format.setSwapInterval(glContextParameter(NSOpenGLContextParameterSwapInterval));

    if (oldContext)
        [oldContext makeCurrentContext];
    else
        [NSOpenGLContext clearCurrentContext];
}

QCocoaGLContext::~QCocoaGLContext()
{
    [m_context release];
}

bool QCocoaGLContext::makeCurrent(QPlatformSurface *surface)
{
    QMacAutoReleasePool pool;

    qCDebug(lcQpaOpenGLContext) << "Making" << this << "current"
        << "in" << QThread::currentThread() << "for" << surface;

    Q_ASSERT(surface->surface()->supportsOpenGL());

    if (!setDrawable(surface))
        return false;

    [m_context makeCurrentContext];

    if (surface->surface()->surfaceClass() == QSurface::Window) {
        if (m_needsUpdate.fetchAndStoreRelaxed(false))
            update();
    }

    return true;
}

void QCocoaGLContext::beginFrame()
{
    QMacAutoReleasePool pool;

    Q_ASSERT(context() && context()->surface());
    auto *surface = context()->surface()->surfaceHandle();
    Q_ASSERT(surface);

    qCDebug(lcQpaOpenGLContext) << "Beginning frame for" << this
        << "in" << QThread::currentThread() << "for" << surface;

    Q_ASSERT(surface->surface()->supportsOpenGL());

    if (surface->surface()->surfaceClass() == QSurface::Window) {
        if (m_needsUpdate.fetchAndStoreRelaxed(false))
            update();
    }
}

/*!
    Sets the drawable object of the NSOpenGLContext, which is the
    frame buffer that is the target of OpenGL drawing operations.
*/
bool QCocoaGLContext::setDrawable(QPlatformSurface *surface)
{
    // Make sure any surfaces released during this process are deallocated
    // straight away, otherwise we may run out of surfaces when spinning a
    // render-loop that doesn't return to one of the outer pools.
    QMacAutoReleasePool pool;

    if (!surface || surface->surface()->surfaceClass() == QSurface::Offscreen) {
        // Clear the current drawable and reset the active window, so that GL
        // commands that don't target a specific FBO will not end up stomping
        // on the previously set drawable.
        qCDebug(lcQpaOpenGLContext) << "Clearing current drawable" << QT_IGNORE_DEPRECATIONS(m_context.view) << "for" << m_context;
        [m_context clearDrawable];
        return true;
    }

    Q_ASSERT(surface->surface()->surfaceClass() == QSurface::Window);
    auto *cocoaWindow = static_cast<QCocoaWindow *>(surface);
    QNSView *view = qnsview_cast(cocoaWindow->view());

    if (view == QT_IGNORE_DEPRECATIONS(m_context.view))
        return true;

    // We generally want high-DPI GL surfaces, unless the user has explicitly disabled them.
    // According to the documentation, layer-backed views ignore wantsBestResolutionOpenGLSurface
    // and configure their own backing surface at an appropriate resolution, but in some cases
    // we've seen this fail (plugin views embedded in surface-backed hosts), so we do it anyways.
    QT_IGNORE_DEPRECATIONS(view.wantsBestResolutionOpenGLSurface) = qt_mac_resolveOption(YES,
        cocoaWindow->window(), "_q_mac_wantsBestResolutionOpenGLSurface",
        "QT_MAC_WANTS_BEST_RESOLUTION_OPENGL_SURFACE");

    // Setting the drawable may happen on a separate thread as a result of
    // a call to makeCurrent, so we need to set up the observers before we
    // associate the view with the context. That way we will guarantee that
    // as long as the view is the drawable of the context we will know about
    // any updates to the view that require surface invalidation.

    auto updateCallback = [this, view]() {
        Q_ASSERT(QThread::currentThread() == qApp->thread());
        if (QT_IGNORE_DEPRECATIONS(m_context.view) != view)
            return;
        m_needsUpdate = true;
    };

    m_updateObservers.clear();

    m_updateObservers.append(QMacNotificationObserver(view, NSViewFrameDidChangeNotification, updateCallback));
    m_updateObservers.append(QMacNotificationObserver(view.window, NSWindowDidChangeScreenNotification, updateCallback));

    m_updateObservers.append(QMacNotificationObserver([NSApplication sharedApplication],
        NSApplicationDidChangeScreenParametersNotification, updateCallback));

    m_updateObservers.append(QMacNotificationObserver(view,
        QCocoaWindowWillReleaseQNSViewNotification, [this, view] {
            if (QT_IGNORE_DEPRECATIONS(m_context.view) != view)
                return;
            qCDebug(lcQpaOpenGLContext) << view << "about to be released."
                << "Clearing current drawable for" << m_context;
            [m_context clearDrawable];
        }));

    // If any of the observers fire at this point it's fine. We check the
    // view association (atomically) in the update callback, and skip the
    // update if we haven't associated yet. Setting the drawable below will
    // have the same effect as an update.

    // Now we are ready to associate the view with the context
    QT_IGNORE_DEPRECATIONS(m_context.view) = view;
    if (QT_IGNORE_DEPRECATIONS(m_context.view) != view) {
        qCInfo(lcQpaOpenGLContext) << "Failed to set" << view << "as drawable for" << m_context;
        m_updateObservers.clear();
        return false;
    }

    qCInfo(lcQpaOpenGLContext) << "Set drawable for" << m_context << "to" << QT_IGNORE_DEPRECATIONS(m_context.view);
    return true;
}

// NSOpenGLContext is not re-entrant. Even when using separate contexts per thread,
// view, and window, calls into the API will still deadlock. For more information
// see https://openradar.appspot.com/37064579
Q_CONSTINIT static QMutex s_reentrancyMutex;

void QCocoaGLContext::update()
{
    // Make sure any surfaces released during this process are deallocated
    // straight away, otherwise we may run out of surfaces when spinning a
    // render-loop that doesn't return to one of the outer pools.
    QMacAutoReleasePool pool;

    QMutexLocker locker(&s_reentrancyMutex);
    qCInfo(lcQpaOpenGLContext) << "Updating" << m_context << "for" << QT_IGNORE_DEPRECATIONS(m_context.view);
    [m_context update];
}

void QCocoaGLContext::swapBuffers(QPlatformSurface *surface)
{
    QMacAutoReleasePool pool;

    qCDebug(lcQpaOpenGLContext) << "Swapping" << m_context
        << "in" << QThread::currentThread() << "to" << surface;

    if (surface->surface()->surfaceClass() == QSurface::Offscreen)
        return; // Nothing to do

    if (!setDrawable(surface)) {
        qCWarning(lcQpaOpenGLContext) << "Can't flush" << m_context
            << "without" << surface << "as drawable";
        return;
    }

    // Flushing an NSOpenGLContext will hit the screen immediately, ignoring
    // any Core Animation transactions in place. This may result in major
    // visual artifacts if the flush happens out of sync with the size
    // of the layer, view, and window reflected by other parts of the UI,
    // e.g. if the application flushes in the resize event or a timer during
    // window resizing, instead of in the expose event.
    auto *cocoaWindow = static_cast<QCocoaWindow *>(surface);
    if (cocoaWindow->geometry().size() != cocoaWindow->m_exposedRect.size()) {
        qCInfo(lcQpaOpenGLContext) << "Window exposed size does not match geometry (yet)."
            << "Skipping flush to avoid visual artifacts.";
        return;
    }

    QMutexLocker locker(&s_reentrancyMutex);
    [m_context flushBuffer];
}

void QCocoaGLContext::doneCurrent()
{
    QMacAutoReleasePool pool;

    qCDebug(lcQpaOpenGLContext) << "Clearing current context"
        << [NSOpenGLContext currentContext] << "in" << QThread::currentThread();

    // Note: We do not need to clear the current drawable here.
    // As long as there is no current context, GL calls will
    // do nothing.

    [NSOpenGLContext clearCurrentContext];
}

QSurfaceFormat QCocoaGLContext::format() const
{
    return m_format;
}

bool QCocoaGLContext::isValid() const
{
    return m_context != nil;
}

bool QCocoaGLContext::isSharing() const
{
    return m_shareContext != nil;
}

NSOpenGLContext *QCocoaGLContext::nativeContext() const
{
    return m_context;
}

QFunctionPointer QCocoaGLContext::getProcAddress(const char *procName)
{
    return (QFunctionPointer)dlsym(RTLD_NEXT, procName);
}

#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug debug, const QCocoaGLContext *context)
{
    QDebugStateSaver saver(debug);
    debug.nospace();
    debug << "QCocoaGLContext(" << (const void *)context;
    if (context) {
        if (debug.verbosity() > QDebug::DefaultVerbosity)
            debug << ", " << context->format();
        debug << ", " << context->nativeContext();
    }
    debug << ')';
    return debug;
}
#endif // !QT_NO_DEBUG_STREAM

QT_END_NAMESPACE
