/*
    KWin - the KDE window manager
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "egldisplay.h"
#include "core/drm_formats.h"
#include "core/drmdevice.h"
#include "core/gpumanager.h"
#include "core/graphicsbuffer.h"
#include "opengl/eglutils_p.h"
#include "opengl/glutils.h"
#include "utils/common.h"
#include "utils/envvar.h"

#include <QOpenGLContext>
#include <drm_fourcc.h>

#ifndef EGL_DRM_RENDER_NODE_FILE_EXT
#define EGL_DRM_RENDER_NODE_FILE_EXT 0x3377
#endif

namespace KWin
{

bool EglDisplay::shouldUseOpenGLES()
{
    if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) {
        return true;
    }
    return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES;
}

static void epoxyFailureWorkaround()
{
    // do nothing
}

static epoxy_resolver_stub_t epoxyFailureHandler(const char *functionName)
{
    // Since epoxy calls glGetString(GL_VERSION) when resolving functions, and
    // glGetString fails after a GPU reset, failing to resolve functions is expected
    // and there's nothing we can or should do about it other than log it and move on.
    qCWarning(KWIN_OPENGL, "Failed to resolve OpenGL function %s", functionName);
    return epoxyFailureWorkaround;
}

std::unique_ptr<EglDisplay> EglDisplay::create(::EGLDisplay display, DrmDevice *drmDevice)
{
    if (!display) {
        return nullptr;
    }

    epoxy_set_resolver_failure_handler(epoxyFailureHandler);

    EGLint major, minor;
    if (eglInitialize(display, &major, &minor) == EGL_FALSE) {
        qCWarning(KWIN_OPENGL) << "eglInitialize failed";
        EGLint error = eglGetError();
        if (error != EGL_SUCCESS) {
            qCWarning(KWIN_OPENGL) << "Error during eglInitialize " << getEglErrorString(error);
        }
        return nullptr;
    }
    EGLint error = eglGetError();
    if (error != EGL_SUCCESS) {
        qCWarning(KWIN_OPENGL) << "Error during eglInitialize " << getEglErrorString(error);
        return nullptr;
    }
    qCDebug(KWIN_OPENGL) << "Egl Initialize succeeded";
    if (eglBindAPI(shouldUseOpenGLES() ? EGL_OPENGL_ES_API : EGL_OPENGL_API) == EGL_FALSE) {
        qCCritical(KWIN_OPENGL) << "bind OpenGL API failed";
        return nullptr;
    }
    qCDebug(KWIN_OPENGL) << "EGL version: " << major << "." << minor;

    const auto extensions = QByteArray(eglQueryString(display, EGL_EXTENSIONS)).split(' ');

    const QByteArray requiredExtensions[] = {
        QByteArrayLiteral("EGL_KHR_no_config_context"),
        QByteArrayLiteral("EGL_KHR_surfaceless_context"),
    };
    for (const QByteArray &extensionName : requiredExtensions) {
        if (!extensions.contains(extensionName)) {
            qCWarning(KWIN_OPENGL) << extensionName << "extension is unsupported";
            return nullptr;
        }
    }

    return std::make_unique<EglDisplay>(display, extensions, drmDevice);
}

static std::optional<dev_t> devIdForFileName(const QString &path)
{
    auto device = DrmDevice::open(path);
    if (device) {
        return device->deviceId();
    } else {
        qCWarning(KWIN_OPENGL, "couldn't find dev node for drm device %s", qPrintable(path));
        return std::nullopt;
    }
}

static bool checkSoftwareDevice(::EGLDisplay display)
{
    EGLAttrib deviceValue = 0;
    if (!eglQueryDisplayAttribEXT(display, EGL_DEVICE_EXT, &deviceValue)) {
        return false;
    }
    EGLDeviceEXT device = reinterpret_cast<EGLDeviceEXT>(deviceValue);
    return QByteArrayView(eglQueryDeviceStringEXT(device, EGL_EXTENSIONS)).contains("EGL_MESA_device_software");
}

EglDisplay::EglDisplay(::EGLDisplay display, const QList<QByteArray> &extensions, DrmDevice *drmDevice)
    : m_handle(display)
    , m_clientExtensions(QByteArray(eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS)).split(' '))
    , m_extensions(extensions)
    , m_renderNode(determineRenderNode())
    , m_renderDevNode(devIdForFileName(m_renderNode))
    , m_drmDevice(drmDevice)
    , m_supportsBufferAge(extensions.contains(QByteArrayLiteral("EGL_EXT_buffer_age")) && qgetenv("KWIN_USE_BUFFER_AGE") != "0")
    , m_supportsNativeFence(extensions.contains(QByteArrayLiteral("EGL_ANDROID_native_fence_sync"))
                            && extensions.contains(QByteArrayLiteral("EGL_KHR_wait_sync")))
    , m_isSoftwareRenderer(m_clientExtensions.contains("EGL_EXT_device_query") && checkSoftwareDevice(display))
{
    m_functions.createImageKHR = reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
    m_functions.destroyImageKHR = reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
    m_functions.queryDmaBufFormatsEXT = reinterpret_cast<PFNEGLQUERYDMABUFFORMATSEXTPROC>(eglGetProcAddress("eglQueryDmaBufFormatsEXT"));
    m_functions.queryDmaBufModifiersEXT = reinterpret_cast<PFNEGLQUERYDMABUFMODIFIERSEXTPROC>(eglGetProcAddress("eglQueryDmaBufModifiersEXT"));

    auto formats = queryImportFormats();
    m_nonExternalOnlyFormats = std::move(formats.nonExternalOnly);
    m_allFormats = std::move(formats.all);
}

EglDisplay::~EglDisplay()
{
    for (const auto &image : m_importCache) {
        destroyImage(image);
    }
    eglTerminate(m_handle);
}

QList<QByteArray> EglDisplay::extensions() const
{
    return m_extensions;
}

::EGLDisplay EglDisplay::handle() const
{
    return m_handle;
}

bool EglDisplay::hasExtension(const QByteArray &name) const
{
    return m_extensions.contains(name);
}

static bool checkExtension(const QByteArrayView extensions, const QByteArrayView extension)
{
    for (int i = 0; i < extensions.size();) {
        if (extensions[i] == ' ') {
            i++;
            continue;
        }
        int next = extensions.indexOf(' ', i);
        if (next == -1) {
            next = extensions.size();
        }

        const int size = next - i;
        if (extension.size() == size && extensions.sliced(i, size) == extension) {
            return true;
        }

        i = next;
    }

    return false;
}

QString EglDisplay::renderNode() const
{
    return m_renderNode;
}

bool EglDisplay::supportsBufferAge() const
{
    return m_supportsBufferAge;
}

bool EglDisplay::supportsNativeFence() const
{
    return m_supportsNativeFence;
}

bool EglDisplay::isSoftwareRenderer() const
{
    return m_isSoftwareRenderer;
}

EGLImageKHR EglDisplay::importDmaBufAsImage(const DmaBufAttributes &dmabuf) const
{
    QList<EGLint> attribs;
    attribs.reserve(6 + dmabuf.planeCount * 10 + 1);

    attribs << EGL_WIDTH << dmabuf.width
            << EGL_HEIGHT << dmabuf.height
            << EGL_LINUX_DRM_FOURCC_EXT << dmabuf.format;

    attribs << EGL_DMA_BUF_PLANE0_FD_EXT << dmabuf.fd[0].get()
            << EGL_DMA_BUF_PLANE0_OFFSET_EXT << dmabuf.offset[0]
            << EGL_DMA_BUF_PLANE0_PITCH_EXT << dmabuf.pitch[0];
    if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) {
        attribs << EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff)
                << EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32);
    }

    if (dmabuf.planeCount > 1) {
        attribs << EGL_DMA_BUF_PLANE1_FD_EXT << dmabuf.fd[1].get()
                << EGL_DMA_BUF_PLANE1_OFFSET_EXT << dmabuf.offset[1]
                << EGL_DMA_BUF_PLANE1_PITCH_EXT << dmabuf.pitch[1];
        if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) {
            attribs << EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff)
                    << EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32);
        }
    }

    if (dmabuf.planeCount > 2) {
        attribs << EGL_DMA_BUF_PLANE2_FD_EXT << dmabuf.fd[2].get()
                << EGL_DMA_BUF_PLANE2_OFFSET_EXT << dmabuf.offset[2]
                << EGL_DMA_BUF_PLANE2_PITCH_EXT << dmabuf.pitch[2];
        if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) {
            attribs << EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff)
                    << EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32);
        }
    }

    if (dmabuf.planeCount > 3) {
        attribs << EGL_DMA_BUF_PLANE3_FD_EXT << dmabuf.fd[3].get()
                << EGL_DMA_BUF_PLANE3_OFFSET_EXT << dmabuf.offset[3]
                << EGL_DMA_BUF_PLANE3_PITCH_EXT << dmabuf.pitch[3];
        if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) {
            attribs << EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff)
                    << EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32);
        }
    }

    attribs << EGL_NONE;

    return createImage(EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data());
}

EGLImageKHR EglDisplay::importDmaBufAsImage(const DmaBufAttributes &dmabuf, int plane, int format, const QSize &size) const
{
    QList<EGLint> attribs;
    attribs.reserve(6 + 1 * 10 + 1);

    attribs << EGL_WIDTH << size.width()
            << EGL_HEIGHT << size.height()
            << EGL_LINUX_DRM_FOURCC_EXT << format;

    attribs << EGL_DMA_BUF_PLANE0_FD_EXT << dmabuf.fd[plane].get()
            << EGL_DMA_BUF_PLANE0_OFFSET_EXT << dmabuf.offset[plane]
            << EGL_DMA_BUF_PLANE0_PITCH_EXT << dmabuf.pitch[plane];
    if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) {
        attribs << EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff)
                << EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32);
    }
    attribs << EGL_NONE;

    return createImage(EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data());
}

const FormatModifierMap &EglDisplay::allSupportedDrmFormats() const
{
    return m_allFormats;
}

const FormatModifierMap &EglDisplay::nonExternalOnlySupportedDrmFormats() const
{
    return m_nonExternalOnlyFormats;
}

bool EglDisplay::isExternalOnly(uint32_t format, uint64_t modifier) const
{
    const auto it = m_nonExternalOnlyFormats.find(format);
    return it == m_nonExternalOnlyFormats.end() || !it->contains(modifier);
}

EglDisplay::Formats EglDisplay::queryImportFormats() const
{
    if (!hasExtension(QByteArrayLiteral("EGL_EXT_image_dma_buf_import")) || !hasExtension(QByteArrayLiteral("EGL_EXT_image_dma_buf_import_modifiers"))) {
        return {};
    }

    if (m_functions.queryDmaBufFormatsEXT == nullptr) {
        return {};
    }

    EGLint count = 0;
    EGLBoolean success = m_functions.queryDmaBufFormatsEXT(m_handle, 0, nullptr, &count);
    if (!success || count == 0) {
        qCCritical(KWIN_OPENGL) << "eglQueryDmaBufFormatsEXT failed!" << getEglErrorString();
        return {};
    }
    QList<uint32_t> formats(count);
    if (!m_functions.queryDmaBufFormatsEXT(m_handle, count, (EGLint *)formats.data(), &count)) {
        qCCritical(KWIN_OPENGL) << "eglQueryDmaBufFormatsEXT with count" << count << "failed!" << getEglErrorString();
        return {};
    }
    Formats ret;
    const ModifierList implicitModifiers = {DRM_FORMAT_MOD_INVALID, DRM_FORMAT_MOD_LINEAR};
    for (const auto format : std::as_const(formats)) {
        EGLint count = 0;
        const bool success = m_functions.queryDmaBufModifiersEXT
            && m_functions.queryDmaBufModifiersEXT(m_handle, format, 0, nullptr, nullptr, &count);
        if (success && count > 0) {
            QList<uint64_t> tmp;
            tmp.resize(count);
            QList<EGLBoolean> externalOnly(count);
            if (m_functions.queryDmaBufModifiersEXT(m_handle, format, count, tmp.data(), externalOnly.data(), &count)) {
                auto &allModifiers = ret.all[format];
                auto &nonExternalOnly = ret.nonExternalOnly[format];
                allModifiers = ModifierList(tmp);
                nonExternalOnly = allModifiers;
                for (int i = allModifiers.size() - 1; i >= 0; i--) {
                    if (externalOnly[i]) {
                        nonExternalOnly.erase(tmp[i]);
                    }
                }
                if (!allModifiers.contains(DRM_FORMAT_MOD_INVALID)) {
                    allModifiers.insert(DRM_FORMAT_MOD_INVALID);
                    if (!nonExternalOnly.empty()) {
                        nonExternalOnly.insert(DRM_FORMAT_MOD_INVALID);
                    }
                }
            }
        } else {
            // no explicit modifiers are supported -> fall back to implicit modifiers
            ret.all[format] = implicitModifiers;
            ret.nonExternalOnly[format] = implicitModifiers;
        }
    }
    return ret;
}

QString EglDisplay::determineRenderNode() const
{
    const char *clientExtensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
    if (checkExtension(clientExtensions, "EGL_EXT_device_query")) {
        EGLAttrib eglDeviceAttrib;
        if (eglQueryDisplayAttribEXT(m_handle, EGL_DEVICE_EXT, &eglDeviceAttrib)) {
            EGLDeviceEXT eglDevice = reinterpret_cast<EGLDeviceEXT>(eglDeviceAttrib);

            const char *deviceExtensions = eglQueryDeviceStringEXT(eglDevice, EGL_EXTENSIONS);
            if (checkExtension(deviceExtensions, "EGL_EXT_device_drm_render_node")) {
                if (const char *node = eglQueryDeviceStringEXT(eglDevice, EGL_DRM_RENDER_NODE_FILE_EXT)) {
                    return QString::fromLocal8Bit(node);
                }
            }
            if (checkExtension(deviceExtensions, "EGL_EXT_device_drm")) {
                // Fallback to display device.
                if (const char *node = eglQueryDeviceStringEXT(eglDevice, EGL_DRM_DEVICE_FILE_EXT)) {
                    return QString::fromLocal8Bit(node);
                }
            }
        }
    }
    return QString();
}

std::optional<dev_t> EglDisplay::renderDevNode() const
{
    return m_renderDevNode;
}

EGLImageKHR EglDisplay::createImage(EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list) const
{
    Q_ASSERT(m_functions.createImageKHR);
    return m_functions.createImageKHR(m_handle, ctx, target, buffer, attrib_list);
}

void EglDisplay::destroyImage(EGLImageKHR image) const
{
    Q_ASSERT(m_functions.destroyImageKHR);
    m_functions.destroyImageKHR(m_handle, image);
}

static const auto s_disableUdmabuf = environmentVariableBoolValue("KWIN_DISABLE_UDMABUF_IMPORT");

EGLImageKHR EglDisplay::importBufferAsImage(GraphicsBuffer *buffer)
{
    Q_ASSERT(buffer->dmabufAttributes() || buffer->shmAttributes());

    std::pair key(buffer, 0);
    auto it = m_importCache.constFind(key);
    if (Q_LIKELY(it != m_importCache.constEnd())) {
        return *it;
    }

    EGLImageKHR image = EGL_NO_IMAGE;
    if (buffer->dmabufAttributes()) {
        image = importDmaBufAsImage(*buffer->dmabufAttributes());
        // On Nvidia, sampling from udmabuf just results in black,
        // and on i915 there are glitches on some systems
    } else if (buffer->udmabufAttributes() && m_drmDevice && !s_disableUdmabuf.value_or(m_drmDevice->isNvidia() || m_drmDevice->isI915())) {
        image = importDmaBufAsImage(*buffer->udmabufAttributes());
    }
    m_importCache[key] = image;
    connect(buffer, &QObject::destroyed, this, [this, key]() {
        destroyImage(m_importCache.take(key));
    });
    return image;
}

EGLImageKHR EglDisplay::importBufferAsImage(GraphicsBuffer *buffer, int plane, int format, const QSize &size)
{
    Q_ASSERT(buffer->dmabufAttributes());

    std::pair key(buffer, plane);
    auto it = m_importCache.constFind(key);
    if (Q_LIKELY(it != m_importCache.constEnd())) {
        return *it;
    }

    EGLImageKHR image = importDmaBufAsImage(*buffer->dmabufAttributes(), plane, format, size);
    m_importCache[key] = image;
    connect(buffer, &QObject::destroyed, this, [this, key]() {
        destroyImage(m_importCache.take(key));
    });
    return image;
}

}

#include "moc_egldisplay.cpp"
