// 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 "qopengl.h"
#include "qopengl_p.h"

#include "qopenglcontext.h"
#include "qopenglfunctions.h"
#include "qoperatingsystemversion.h"
#include "qoffscreensurface.h"

#include <QtCore/QDebug>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonValue>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QTextStream>
#include <QtCore/QFile>
#include <QtCore/QDir>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

#if defined(QT_OPENGL_3)
typedef const GLubyte * (QOPENGLF_APIENTRYP qt_glGetStringi)(GLenum, GLuint);
#endif

#ifndef GL_CONTEXT_LOST
#define GL_CONTEXT_LOST                   0x0507
#endif

QOpenGLExtensionMatcher::QOpenGLExtensionMatcher()
{
    QOpenGLContext *ctx = QOpenGLContext::currentContext();
    if (!ctx) {
        qWarning("QOpenGLExtensionMatcher::QOpenGLExtensionMatcher: No context");
        return;
    }
    QOpenGLFunctions *funcs = ctx->functions();
    const char *extensionStr = nullptr;

    if (ctx->isOpenGLES() || ctx->format().majorVersion() < 3)
        extensionStr = reinterpret_cast<const char *>(funcs->glGetString(GL_EXTENSIONS));

    if (extensionStr) {
        QByteArray ba(extensionStr);
        QList<QByteArray> extensions = ba.split(' ');
        m_extensions = QSet<QByteArray>(extensions.constBegin(), extensions.constEnd());
    } else {
#ifdef QT_OPENGL_3
        // clear error state
        while (true) { // Clear error state.
            GLenum error = funcs->glGetError();
            if (error == GL_NO_ERROR)
                break;
            if (error == GL_CONTEXT_LOST)
                return;
        };
        qt_glGetStringi glGetStringi = (qt_glGetStringi)ctx->getProcAddress("glGetStringi");

        if (!glGetStringi)
            return;

        GLint numExtensions = 0;
        funcs->glGetIntegerv(GL_NUM_EXTENSIONS, &numExtensions);

        for (int i = 0; i < numExtensions; ++i) {
            const char *str = reinterpret_cast<const char *>(glGetStringi(GL_EXTENSIONS, i));
            m_extensions.insert(str);
        }
#endif // QT_OPENGL_3
    }
}

/* Helpers to read out the list of features matching a device from
 * a Chromium driver bug list. Note that not all keys are supported and
 * some may behave differently: gl_vendor is a substring match instead of regex.
 {
  "entries": [
 {
      "id": 20,
      "description": "Disable EXT_draw_buffers on GeForce GT 650M on Linux due to driver bugs",
      "os": {
        "type": "linux"
      },
      // Optional: "exceptions" list
      "vendor_id": "0x10de",
      "device_id": ["0x0fd5"],
      "multi_gpu_category": "any",
      "features": [
        "disable_ext_draw_buffers"
      ]
    },
   ....
   }
*/

QDebug operator<<(QDebug d, const QOpenGLConfig::Gpu &g)
{
    QDebugStateSaver s(d);
    d.nospace();
    d << "Gpu(";
    if (g.isValid()) {
        d << "vendor=" << Qt::hex << Qt::showbase <<g.vendorId << ", device=" << g.deviceId
          << "version=" << g.driverVersion;
    } else {
        d << 0;
    }
    d << ')';
    return d;
}

typedef QJsonArray::ConstIterator JsonArrayConstIt;

static inline bool contains(const QJsonArray &haystack, unsigned needle)
{
    for (JsonArrayConstIt it = haystack.constBegin(), cend = haystack.constEnd(); it != cend; ++it) {
        if (needle == it->toString().toUInt(nullptr, /* base */ 0))
            return true;
    }
    return false;
}

static inline bool contains(const QJsonArray &haystack, const QString &needle)
{
    for (JsonArrayConstIt it = haystack.constBegin(), cend = haystack.constEnd(); it != cend; ++it) {
        if (needle == it->toString())
            return true;
    }
    return false;
}

namespace {
enum Operator { NotEqual, LessThan, LessEqualThan, Equals, GreaterThan, GreaterEqualThan };
static const char operators[][3] = {"!=", "<", "<=", "=", ">", ">="};

// VersionTerm describing a version term consisting of number and operator
// found in os.version and driver_version.
struct VersionTerm {
    VersionTerm() : op(NotEqual) {}
    static VersionTerm fromJson(const QJsonValue &v);
    bool isNull() const { return number.isNull(); }
    bool matches(const QVersionNumber &other) const;

    QVersionNumber number;
    Operator op;
};

bool VersionTerm::matches(const QVersionNumber &other) const
{
    if (isNull() || other.isNull()) {
        qWarning("called with invalid parameters");
        return false;
    }
    switch (op) {
    case NotEqual:
        return other != number;
    case LessThan:
        return other < number;
    case LessEqualThan:
        return other <= number;
    case Equals:
        return other == number;
    case GreaterThan:
        return other > number;
    case GreaterEqualThan:
        return other >= number;
    }
    return false;
}

VersionTerm VersionTerm::fromJson(const QJsonValue &v)
{
    VersionTerm result;
    if (!v.isObject())
        return result;
    const QJsonObject o = v.toObject();
    result.number = QVersionNumber::fromString(o.value("value"_L1).toStringView());
    const auto opS = o.value("op"_L1).toStringView();
    for (size_t i = 0; i < sizeof(operators) / sizeof(operators[0]); ++i) {
        if (opS == QLatin1StringView(operators[i])) {
            result.op = static_cast<Operator>(i);
            break;
        }
    }
    return result;
}

// OS term consisting of name and optional version found in
// under "os" in main array and in "exceptions" lists.
struct OsTypeTerm
{
    static OsTypeTerm fromJson(const QJsonValue &v);
    static QString hostOs();
    static QVersionNumber hostKernelVersion() { return QVersionNumber::fromString(QSysInfo::kernelVersion()); }
    static QString hostOsRelease() {
#ifdef Q_OS_WIN
        if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11)
            return u"11"_s;
        return u"10"_s;
#else
        return {};
#endif
    }

    bool isNull() const { return type.isEmpty(); }
    bool matches(const QString &osName, const QVersionNumber &kernelVersion, const QString &osRelease) const
    {
        if (isNull() || osName.isEmpty() || kernelVersion.isNull()) {
            qWarning("called with invalid parameters");
            return false;
        }
        if (type != osName)
            return false;
        if (!versionTerm.isNull() && !versionTerm.matches(kernelVersion))
            return false;
        // release is a list of Windows versions where the rule should match
        if (!release.isEmpty() && !contains(release, osRelease))
            return false;
        return true;
    }

    QString type;
    VersionTerm versionTerm;
    QJsonArray release;
};

OsTypeTerm OsTypeTerm::fromJson(const QJsonValue &v)
{
    OsTypeTerm result;
    if (!v.isObject())
        return result;
    const QJsonObject o = v.toObject();
    result.type = o.value("type"_L1).toString();
    result.versionTerm = VersionTerm::fromJson(o.value("version"_L1));
    result.release = o.value("release"_L1).toArray();
    return result;
}

QString OsTypeTerm::hostOs()
{
    // Determine Host OS.
#if defined(Q_OS_WIN)
    return  QStringLiteral("win");
#elif defined(Q_OS_LINUX)
    return QStringLiteral("linux");
#elif defined(Q_OS_MACOS)
    return  QStringLiteral("macosx");
#elif defined(Q_OS_ANDROID)
    return  QStringLiteral("android");
#else
    return QString();
#endif
}
} // anonymous namespace

static QString msgSyntaxWarning(const QJsonObject &object, const QString &what)
{
    QString result;
    QTextStream(&result) << "Id " << object.value("id"_L1).toInt()
        << " (\"" << object.value("description"_L1).toString()
        << "\"): " << what;
    return result;
}

// Check whether an entry matches. Called recursively for
// "exceptions" list.

static bool matches(const QJsonObject &object,
                    const QString &osName,
                    const QVersionNumber &kernelVersion,
                    const QString &osRelease,
                    const QOpenGLConfig::Gpu &gpu)
{
    const OsTypeTerm os = OsTypeTerm::fromJson(object.value("os"_L1));
    if (!os.isNull() && !os.matches(osName, kernelVersion, osRelease))
        return false;

    const QJsonValue exceptionsV = object.value("exceptions"_L1);
    if (exceptionsV.isArray()) {
        const QJsonArray exceptionsA = exceptionsV.toArray();
        for (JsonArrayConstIt it = exceptionsA.constBegin(), cend = exceptionsA.constEnd(); it != cend; ++it) {
            if (matches(it->toObject(), osName, kernelVersion, osRelease, gpu))
                return false;
        }
    }

    const QJsonValue vendorV = object.value("vendor_id"_L1);
    if (vendorV.isString()) {
        if (gpu.vendorId != vendorV.toString().toUInt(nullptr, /* base */ 0))
            return false;
    } else {
        if (object.contains("gl_vendor"_L1)) {
            const QByteArray glVendorV = object.value("gl_vendor"_L1).toString().toUtf8();
            if (!gpu.glVendor.contains(glVendorV))
                return false;
        }
    }

    if (gpu.deviceId) {
        const QJsonValue deviceIdV = object.value("device_id"_L1);
        switch (deviceIdV.type()) {
        case QJsonValue::Array:
            if (!contains(deviceIdV.toArray(), gpu.deviceId))
                return false;
            break;
        case QJsonValue::Undefined:
        case QJsonValue::Null:
            break;
        default:
            qWarning().noquote()
                << msgSyntaxWarning(object, "Device ID must be of type array."_L1);
        }
    }
    if (!gpu.driverVersion.isNull()) {
        const QJsonValue driverVersionV = object.value("driver_version"_L1);
        switch (driverVersionV.type()) {
        case QJsonValue::Object:
            if (!VersionTerm::fromJson(driverVersionV).matches(gpu.driverVersion))
                return false;
            break;
        case QJsonValue::Undefined:
        case QJsonValue::Null:
            break;
        default:
            qWarning().noquote()
                << msgSyntaxWarning(object, "Driver version must be of type object."_L1);
        }
    }

    if (!gpu.driverDescription.isEmpty()) {
        const QJsonValue driverDescriptionV = object.value("driver_description"_L1);
        if (driverDescriptionV.isString()) {
            if (!gpu.driverDescription.contains(driverDescriptionV.toString().toUtf8()))
                return false;
        }
    }

    return true;
}

static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu,
                            const QString &osName,
                            const QVersionNumber &kernelVersion,
                            const QString &osRelease,
                            const QJsonDocument &doc,
                            QSet<QString> *result,
                            QString *errorMessage)
{
    result->clear();
    errorMessage->clear();
    const QJsonValue entriesV = doc.object().value("entries"_L1);
    if (!entriesV.isArray()) {
        *errorMessage = "No entries read."_L1;
        return false;
    }

    const QJsonArray entriesA = entriesV.toArray();
    for (JsonArrayConstIt eit = entriesA.constBegin(), ecend = entriesA.constEnd(); eit != ecend; ++eit) {
        if (eit->isObject()) {
            const QJsonObject object = eit->toObject();
            if (matches(object, osName, kernelVersion, osRelease, gpu)) {
                const QJsonValue featuresListV = object.value("features"_L1);
                if (featuresListV.isArray()) {
                    const QJsonArray featuresListA = featuresListV.toArray();
                    for (JsonArrayConstIt fit = featuresListA.constBegin(), fcend = featuresListA.constEnd(); fit != fcend; ++fit)
                        result->insert(fit->toString());
                }
            }
        }
    }
    return true;
}

static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu,
                            const QString &osName,
                            const QVersionNumber &kernelVersion,
                            const QString &osRelease,
                            const QByteArray &jsonAsciiData,
                            QSet<QString> *result, QString *errorMessage)
{
    result->clear();
    errorMessage->clear();
    QJsonParseError error;
    const QJsonDocument document = QJsonDocument::fromJson(jsonAsciiData, &error);
    if (document.isNull()) {
        const qsizetype lineNumber =
                QByteArrayView(jsonAsciiData).left(error.offset).count('\n') + 1;
        QTextStream str(errorMessage);
        str << "Failed to parse data: \"" << error.errorString()
            << "\" at line " << lineNumber << " (offset: "
            << error.offset << ").";
        return false;
    }
    return readGpuFeatures(gpu, osName, kernelVersion, osRelease, document, result, errorMessage);
}

static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu,
                            const QString &osName,
                            const QVersionNumber &kernelVersion,
                            const QString &osRelease,
                            const QString &fileName,
                            QSet<QString> *result, QString *errorMessage)
{
    result->clear();
    errorMessage->clear();
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        QTextStream str(errorMessage);
        str << "Cannot open \"" << QDir::toNativeSeparators(fileName) << "\": "
            << file.errorString();
        return false;
    }
    const bool success = readGpuFeatures(gpu, osName, kernelVersion, osRelease, file.readAll(), result, errorMessage);
    if (!success) {
        errorMessage->prepend("Error reading \""_L1
                              + QDir::toNativeSeparators(fileName)
                              + "\": "_L1);
    }
    return success;
}

QSet<QString> QOpenGLConfig::gpuFeatures(const QOpenGLConfig::Gpu &gpu,
                                         const QString &osName,
                                         const QVersionNumber &kernelVersion,
                                         const QString &osRelease,
                                         const QJsonDocument &doc)
{
    QSet<QString> result;
    QString errorMessage;
    if (!readGpuFeatures(gpu, osName, kernelVersion, osRelease, doc, &result, &errorMessage))
        qWarning().noquote() << errorMessage;
    return result;
}

QSet<QString> QOpenGLConfig::gpuFeatures(const QOpenGLConfig::Gpu &gpu,
                                         const QString &osName,
                                         const QVersionNumber &kernelVersion,
                                         const QString &osRelease,
                                         const QString &fileName)
{
    QSet<QString> result;
    QString errorMessage;
    if (!readGpuFeatures(gpu, osName, kernelVersion, osRelease, fileName, &result, &errorMessage))
        qWarning().noquote() << errorMessage;
    return result;
}

QSet<QString> QOpenGLConfig::gpuFeatures(const Gpu &gpu, const QJsonDocument &doc)
{
    return gpuFeatures(gpu, OsTypeTerm::hostOs(), OsTypeTerm::hostKernelVersion(), OsTypeTerm::hostOsRelease(), doc);
}

QSet<QString> QOpenGLConfig::gpuFeatures(const Gpu &gpu, const QString &fileName)
{
    return gpuFeatures(gpu, OsTypeTerm::hostOs(), OsTypeTerm::hostKernelVersion(), OsTypeTerm::hostOsRelease(), fileName);
}

QOpenGLConfig::Gpu QOpenGLConfig::Gpu::fromContext()
{
    QOpenGLContext *ctx = QOpenGLContext::currentContext();
    QScopedPointer<QOpenGLContext> tmpContext;
    QScopedPointer<QOffscreenSurface> tmpSurface;
    if (!ctx) {
        tmpContext.reset(new QOpenGLContext);
        if (!tmpContext->create()) {
            qWarning("QOpenGLConfig::Gpu::fromContext: Failed to create temporary context");
            return QOpenGLConfig::Gpu();
        }
        tmpSurface.reset(new QOffscreenSurface);
        tmpSurface->setFormat(tmpContext->format());
        tmpSurface->create();
        tmpContext->makeCurrent(tmpSurface.data());
    }

    QOpenGLConfig::Gpu gpu;
    ctx = QOpenGLContext::currentContext();
    const GLubyte *p = ctx->functions()->glGetString(GL_VENDOR);
    if (p)
        gpu.glVendor = QByteArray(reinterpret_cast<const char *>(p));

    return gpu;
}

QT_END_NAMESPACE
