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

#include "qopenxrgraphics_opengl_p.h"
#include "qopenxrhelpers_p.h"

#include <QtGui/QOpenGLContext>
#include <QtQuick/QQuickWindow>
#include <QtQuick/private/qquickrendertarget_p.h>

#include <rhi/qrhi.h>

#if defined(XR_USE_PLATFORM_XLIB) || defined(XR_USE_PLATFORM_XCB)
#include <GL/glx.h>
#endif

#include <qpa/qplatformnativeinterface.h>

QT_BEGIN_NAMESPACE

#ifndef GL_RGBA8
#define GL_RGBA8                          0x8058
#endif

#ifndef GL_SRGB8_ALPHA8_EXT
#define GL_SRGB8_ALPHA8_EXT               0x8C43
#endif

#ifndef GL_DEPTH_COMPONENT16
#define GL_DEPTH_COMPONENT16              0x81A5
#endif

#ifndef GL_DEPTH_COMPONENT24
#define GL_DEPTH_COMPONENT24              0x81A6
#endif

#ifndef GL_DEPTH_COMPONENT32F
#define GL_DEPTH_COMPONENT32F             0x8CAC
#endif

#ifndef GL_DEPTH24_STENCIL8
#define GL_DEPTH24_STENCIL8               0x88F0
#endif

template<typename T>
T *typed_calloc()
{
    return static_cast<T *>(calloc(1, sizeof(T)));
}

QOpenXRGraphicsOpenGL::QOpenXRGraphicsOpenGL()
{
    m_graphicsRequirements.type = XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR;
}

bool QOpenXRGraphicsOpenGL::initialize(const QVector<XrExtensionProperties> &extensions)
{

#ifdef XR_USE_PLATFORM_EGL
    if (hasExtension(extensions, XR_MNDX_EGL_ENABLE_EXTENSION_NAME)) {

        // To choose XR_MNDX_egl_enable we need to be absolutely sure that
        // we will get access to EGLContext later and at this point we have no window/rhi yet.
        QOpenGLContext ctx;
        if (ctx.create() && ctx.nativeInterface<QNativeInterface::QEGLContext>()) {
            m_selectedPlatform = PLATFORM_EGL;
            m_requiredExtensions = { XR_MNDX_EGL_ENABLE_EXTENSION_NAME };

            // XR_MNDX_egl_enable does not require XR_KHR_opengl_enable
            // However, some OpenXR runtimes still want it (old Monado/SteamVR)
            if (hasExtension(extensions, XR_KHR_OPENGL_ENABLE_EXTENSION_NAME)) {
                m_requiredExtensions.append(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME);
            }
            return true;
        }
    }
#endif

    // Standard OpenGL platforms
    if (hasExtension(extensions, XR_KHR_OPENGL_ENABLE_EXTENSION_NAME)) {

#ifdef XR_USE_PLATFORM_WAYLAND
        auto wlApp = qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
        if (wlApp && wlApp->display()) {
            m_selectedPlatform = PLATFORM_WAYLAND;
            m_requiredExtensions = { XR_KHR_OPENGL_ENABLE_EXTENSION_NAME };
            return true;
        }
#endif

#ifdef XR_USE_PLATFORM_XLIB
        auto x11App = qApp->nativeInterface<QNativeInterface::QX11Application>();
        if (x11App && x11App->display()) {
            m_selectedPlatform = PLATFORM_XLIB;
            m_requiredExtensions = { XR_KHR_OPENGL_ENABLE_EXTENSION_NAME };
            return true;
        }
#endif
#ifdef XR_USE_PLATFORM_WIN32
        m_selectedPlatform = PLATFORM_WIN32;
        m_requiredExtensions = { XR_KHR_OPENGL_ENABLE_EXTENSION_NAME };
        return true;
#endif
    }

    qWarning("Qt Quick 3D XR (OpenGL): no supported platforms");
    return false;
}

QVector<const char *> QOpenXRGraphicsOpenGL::getRequiredExtensions() const
{
    return m_requiredExtensions;
}


const XrBaseInStructure *QOpenXRGraphicsOpenGL::handle() const
{
    return m_graphicsBinding.get();
}


bool QOpenXRGraphicsOpenGL::setupGraphics(const XrInstance &instance, XrSystemId &systemId, const QQuickGraphicsConfiguration &)
{
    // Extension function must be loaded by name
    PFN_xrGetOpenGLGraphicsRequirementsKHR pfnGetOpenGLGraphicsRequirementsKHR = nullptr;
    OpenXRHelpers::checkXrResult(xrGetInstanceProcAddr(instance, "xrGetOpenGLGraphicsRequirementsKHR",
                                                       reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetOpenGLGraphicsRequirementsKHR)),
                                 instance);
    if (!pfnGetOpenGLGraphicsRequirementsKHR) {
        qWarning("Could not resolve xrGetOpenGLGraphicsRequirementsKHR; perhaps the OpenXR implementation does not support OpenGL?");
        return false;
    }
    OpenXRHelpers::checkXrResult(pfnGetOpenGLGraphicsRequirementsKHR(instance, systemId, &m_graphicsRequirements),
                                 instance);
    return true;
}

bool QOpenXRGraphicsOpenGL::finializeGraphics(QRhi *rhi)
{
    const QRhiGles2NativeHandles *openglRhi = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());

    QOpenGLContext *openGLContext = openglRhi->context;

    const XrVersion desiredApiVersion = XR_MAKE_VERSION(openGLContext->format().majorVersion(),
                                                        openGLContext->format().minorVersion(),
                                                        0);
    if (m_graphicsRequirements.minApiVersionSupported > desiredApiVersion) {
        qWarning("Qt Quick 3D XR (OpenGL): Runtime does not support desired graphics API and/or version");
        return false;
    }

    switch (m_selectedPlatform) {

#ifdef XR_USE_PLATFORM_EGL
    case PLATFORM_EGL: {
        auto eglContext = openGLContext->nativeInterface<QNativeInterface::QEGLContext>();
        if (!eglContext) {
            qWarning("Qt Quick 3D XR (OpenGL): Failed to get QNativeInterface::QEGLContext");
            return false;
        }
        auto binding = typed_calloc<XrGraphicsBindingEGLMNDX>();
        binding->type = XR_TYPE_GRAPHICS_BINDING_EGL_MNDX;
        binding->getProcAddress = &eglGetProcAddress;
        binding->display = eglContext->display();
        binding->config = eglContext->config();
        binding->context = eglContext->nativeContext();
        m_graphicsBinding.reset(reinterpret_cast<XrBaseInStructure *>(binding));
        break;
    }
#endif

#ifdef XR_USE_PLATFORM_WAYLAND
    case PLATFORM_WAYLAND: {
        auto wlApp = qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
        auto wlDisplay = wlApp->display();
        auto binding = typed_calloc<XrGraphicsBindingOpenGLWaylandKHR>();
        binding->type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WAYLAND_KHR;
        binding->display = wlDisplay;
        m_graphicsBinding.reset(reinterpret_cast<XrBaseInStructure *>(binding));
        break;
    }
#endif

#ifdef XR_USE_PLATFORM_XLIB
    case PLATFORM_XLIB: {
        auto glxContext = openGLContext->nativeInterface<QNativeInterface::QGLXContext>();
        if (!glxContext) {
            qWarning("Qt Quick 3D XR (OpenGL): Failed to get QNativeInterface::QGLXContext");
            return false;
        }

        auto x11App = qApp->nativeInterface<QNativeInterface::QX11Application>();
        auto xDisplay = x11App->display();
        auto glxDrawable = m_window->winId();

        QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
        auto glxFBConfig = static_cast<GLXFBConfig>(native->nativeResourceForContext("glxconfig", openGLContext));
        if (!glxFBConfig) {
            qWarning("Qt Quick 3D XR (OpenGL): Failed to get GLXFBConfig");
            return false;
        }

        int visualId = 0;
        if (glXGetFBConfigAttrib(xDisplay, glxFBConfig, GLX_VISUAL_ID, &visualId) != Success) {
            qWarning("Qt Quick 3D XR (OpenGL): Failed to get visualId");
            return false;
        }

        auto binding = typed_calloc<XrGraphicsBindingOpenGLXlibKHR>();
        binding->type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR;
        binding->xDisplay = xDisplay;
        binding->visualid = visualId;
        binding->glxFBConfig = glxFBConfig;
        binding->glxDrawable = glxDrawable;
        binding->glxContext = glxContext->nativeContext();
        m_graphicsBinding.reset(reinterpret_cast<XrBaseInStructure *>(binding));
        break;
    }
#endif

#ifdef XR_USE_PLATFORM_WIN32
    case PLATFORM_WIN32: {
        auto wglContext = openGLContext->nativeInterface<QNativeInterface::QWGLContext>();
        if (!wglContext) {
            qWarning("Qt Quick 3D XR (OpenGL): Failed to get QNativeInterface::QWGLContext");
            return false;
        }
        auto binding = typed_calloc<XrGraphicsBindingOpenGLWin32KHR>();
        binding->type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR;
        binding->hDC = GetDC(reinterpret_cast<HWND>(m_window->winId()));
        binding->hGLRC = wglContext->nativeContext();
        m_graphicsBinding.reset(reinterpret_cast<XrBaseInStructure *>(binding));
        break;
    }
#endif

    default:
        return false;
        break;
    }

    m_rhi = rhi;

    return true;
}


int64_t QOpenXRGraphicsOpenGL::colorSwapchainFormat(const QVector<int64_t> &swapchainFormats) const
{
    // List of supported color swapchain formats.
    constexpr int64_t supportedColorSwapchainFormats[] = {
        GL_RGBA8,
        GL_RGBA8_SNORM
    };

    auto swapchainFormatIt = std::find_first_of(std::begin(supportedColorSwapchainFormats),
                                                std::end(supportedColorSwapchainFormats),
                                                swapchainFormats.begin(),
                                                swapchainFormats.end());
    return *swapchainFormatIt;
}

int64_t QOpenXRGraphicsOpenGL::depthSwapchainFormat(const QVector<int64_t> &swapchainFormats) const
{
    // in order of preference
    constexpr int64_t supportedDepthSwapchainFormats[] = {
        GL_DEPTH24_STENCIL8,
        GL_DEPTH_COMPONENT32F,
        GL_DEPTH_COMPONENT24,
        GL_DEPTH_COMPONENT16
    };

    return *std::find_first_of(std::begin(supportedDepthSwapchainFormats),
                               std::end(supportedDepthSwapchainFormats),
                               swapchainFormats.begin(),
                               swapchainFormats.end());
}

QVector<XrSwapchainImageBaseHeader*> QOpenXRGraphicsOpenGL::allocateSwapchainImages(int count, XrSwapchain swapchain)
{
    QVector<XrSwapchainImageBaseHeader*> swapchainImages;
    QVector<XrSwapchainImageOpenGLKHR> swapchainImageBuffer(count);
    for (XrSwapchainImageOpenGLKHR& image : swapchainImageBuffer) {
        image.type = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR;
        swapchainImages.push_back(reinterpret_cast<XrSwapchainImageBaseHeader*>(&image));
    }
    m_swapchainImageBuffer.insert(swapchain, swapchainImageBuffer);
    return swapchainImages;
}


QQuickRenderTarget QOpenXRGraphicsOpenGL::renderTarget(const XrSwapchainSubImage &subImage,
                                                       const XrSwapchainImageBaseHeader *swapchainImage,
                                                       quint64 swapchainFormat,
                                                       int samples,
                                                       int arraySize,
                                                       const XrSwapchainImageBaseHeader *depthSwapchainImage,
                                                       quint64 depthSwapchainFormat) const
{
    const uint32_t colorTexture = reinterpret_cast<const XrSwapchainImageOpenGLKHR*>(swapchainImage)->image;

    switch (swapchainFormat) {
    case GL_SRGB8_ALPHA8_EXT:
        swapchainFormat = GL_RGBA8;
        break;
    default:
        break;
    }

    QQuickRenderTarget::Flags flags;
    if (samples > 1)
        flags |= QQuickRenderTarget::Flag::MultisampleResolve;

    const QSize pixelSize(subImage.imageRect.extent.width, subImage.imageRect.extent.height);
    QQuickRenderTarget rt = QQuickRenderTarget::fromOpenGLTexture(colorTexture,
                                                                  swapchainFormat,
                                                                  pixelSize,
                                                                  samples,
                                                                  arraySize,
                                                                  flags);
    if (depthSwapchainImage) {
        QRhiTexture::Format format = QRhiTexture::D24S8;
        switch (depthSwapchainFormat) {
        case GL_DEPTH_COMPONENT32F:
            format = QRhiTexture::D32F;
            break;
        case GL_DEPTH_COMPONENT24:
            format = QRhiTexture::D24;
            break;
        case GL_DEPTH_COMPONENT16:
            format = QRhiTexture::D16;
            break;
        }
        GLuint depthImage = reinterpret_cast<const XrSwapchainImageOpenGLKHR*>(depthSwapchainImage)->image;
        if (m_depthTexture && (m_depthTexture->format() != format || m_depthTexture->pixelSize() != pixelSize || m_depthTexture->arraySize() != arraySize)) {
            delete m_depthTexture;
            m_depthTexture = nullptr;
        }
        if (!m_depthTexture) {
            // this is never multisample, QQuickRt takes care of resolving depth-stencil
            if (arraySize > 1)
                m_depthTexture = m_rhi->newTextureArray(format, arraySize, pixelSize, 1, QRhiTexture::RenderTarget);
            else
                m_depthTexture = m_rhi->newTexture(format, pixelSize, 1, QRhiTexture::RenderTarget);
        }
        m_depthTexture->createFrom({ depthImage, 0 });
        rt.setDepthTexture(m_depthTexture);
    }
    return rt;
}

void QOpenXRGraphicsOpenGL::setupWindow(QQuickWindow *window)
{
    m_window = window;
}

void QOpenXRGraphicsOpenGL::releaseResources()
{
    delete m_depthTexture;
    m_depthTexture = nullptr;
}

QT_END_NAMESPACE
