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

#include "utils_p.h"

#include <QtGui/QPainter>
#include <QtGui/QOpenGLContext>
#include <QtGui/QOffscreenSurface>
#include <QtGui/qquaternion.h>

#include <QtCore/QCoreApplication>
#include <QtCore/QRegularExpression>
#include <QLocale>

QT_BEGIN_NAMESPACE

#define NUM_IN_POWER(y, x) for (;y<x;y<<=1)
#define MIN_POWER 2

// Some values that only need to be resolved once
static bool staticsResolved = false;
static GLint maxTextureSize = 0;
static bool isES = false;

GLuint Utils::getNearestPowerOfTwo(GLuint value)
{
    GLuint powOfTwoValue = MIN_POWER;
    NUM_IN_POWER(powOfTwoValue, value);
    return powOfTwoValue;
}

QVector4D Utils::vectorFromColor(const QColor &color)
{
    return QVector4D(color.redF(), color.greenF(), color.blueF(), color.alphaF());
}

QColor Utils::colorFromVector(const QVector3D &colorVector)
{
    return QColor(colorVector.x() * 255.0f, colorVector.y() * 255.0f,
                  colorVector.z() * 255.0f, 255.0f);
}

QColor Utils::colorFromVector(const QVector4D &colorVector)
{
    return QColor(colorVector.x() * 255.0f, colorVector.y() * 255.0f,
                  colorVector.z() * 255.0f, colorVector.w() * 255.0f);
}

QImage Utils::printTextToImage(const QFont &font, const QString &text, const QColor &bgrColor,
                               const QColor &txtColor, bool labelBackground,
                               bool borders, int maxLabelWidth)
{
    if (!staticsResolved)
        resolveStatics();

    GLuint paddingWidth = 20;
    GLuint paddingHeight = 20;
    GLuint prePadding = 20;
    GLint targetWidth = maxTextureSize;

    // Calculate text dimensions
    QFont valueFont = font;
    valueFont.setPointSize(textureFontSize);
    QFontMetrics valueFM(valueFont);
    int valueStrWidth = valueFM.horizontalAdvance(text);

    // ES2 needs to use maxLabelWidth always (when given) because of the power of 2 -issue.
    if (maxLabelWidth && (labelBackground || Utils::isOpenGLES()))
        valueStrWidth = maxLabelWidth;
    int valueStrHeight = valueFM.height();
    valueStrWidth += paddingWidth / 2; // Fix clipping problem with skewed fonts (italic or italic-style)
    QSize labelSize;
    qreal fontRatio = 1.0;

    if (Utils::isOpenGLES()) {
        // Test if text with slighly smaller font would fit into one step smaller texture
        // ie. if the text is just exceeded the smaller texture boundary, it would
        // make a label with large empty space
        uint testWidth = getNearestPowerOfTwo(valueStrWidth + prePadding) >> 1;
        int diffToFit = (valueStrWidth + prePadding) - testWidth;
        int maxSqueeze = int((valueStrWidth + prePadding) * 0.25f);
        if (diffToFit < maxSqueeze && maxTextureSize > GLint(testWidth))
            targetWidth = testWidth;
    }

    bool sizeOk = false;
    int currentFontSize = textureFontSize;
    do {
        if (Utils::isOpenGLES()) {
            // ES2 can't handle textures with dimensions not in power of 2. Resize labels accordingly.
            // Add some padding before converting to power of two to avoid too tight fit
            labelSize = QSize(valueStrWidth + prePadding, valueStrHeight + prePadding);
            labelSize.setWidth(getNearestPowerOfTwo(labelSize.width()));
            labelSize.setHeight(getNearestPowerOfTwo(labelSize.height()));
        } else {
            if (!labelBackground)
                labelSize = QSize(valueStrWidth, valueStrHeight);
            else
                labelSize = QSize(valueStrWidth + paddingWidth * 2, valueStrHeight + paddingHeight * 2);
        }

        if (!maxTextureSize || (labelSize.width() <= maxTextureSize
                                && (labelSize.width() <= targetWidth || !Utils::isOpenGLES()))) {
            // Make sure the label is not too wide
            sizeOk = true;
        } else if (--currentFontSize == 4) {
            qCritical() << "Label" << text << "is too long to be generated.";
            return QImage();
        } else {
            fontRatio = (qreal)currentFontSize / (qreal)textureFontSize;
            // Reduce font size and try again
            valueFont.setPointSize(currentFontSize);
            QFontMetrics currentValueFM(valueFont);
            if (maxLabelWidth && (labelBackground || Utils::isOpenGLES()))
                valueStrWidth = maxLabelWidth * fontRatio;
            else
                valueStrWidth = currentValueFM.horizontalAdvance(text);
            valueStrHeight = currentValueFM.height();
            valueStrWidth += paddingWidth / 2;
        }
    } while (!sizeOk);

    return [&] {
    // Create image
    QImage image = QImage(labelSize, QImage::Format_ARGB32);
    image.fill(Qt::transparent);

    // Init painter
    QPainter painter(&image);
    // Paint text
    painter.setRenderHint(QPainter::Antialiasing, true);
    painter.setCompositionMode(QPainter::CompositionMode_Source);
    painter.setFont(valueFont);
    if (!labelBackground) {
        painter.setPen(txtColor);
        if (Utils::isOpenGLES()) {
            painter.drawText((labelSize.width() - valueStrWidth) / 2.0f,
                             (labelSize.height() - valueStrHeight) / 2.0f,
                             valueStrWidth, valueStrHeight,
                             Qt::AlignCenter | Qt::AlignVCenter,
                             text);
        } else {
            painter.drawText(0, 0,
                             valueStrWidth, valueStrHeight,
                             Qt::AlignCenter | Qt::AlignVCenter,
                             text);
        }
    } else {
        painter.setBrush(QBrush(bgrColor));
        qreal radius = 10.0 * fontRatio;
        if (borders) {
            painter.setPen(QPen(QBrush(txtColor), 5.0 * fontRatio,
                                Qt::SolidLine, Qt::SquareCap, Qt::RoundJoin));
            painter.drawRoundedRect(5, 5,
                                    labelSize.width() - 10, labelSize.height() - 10,
                                    radius, radius);
        } else {
            painter.setPen(bgrColor);
            painter.drawRoundedRect(0, 0, labelSize.width(), labelSize.height(), radius, radius);
        }
        painter.setPen(txtColor);
        painter.drawText((labelSize.width() - valueStrWidth) / 2.0f,
                         (labelSize.height() - valueStrHeight) / 2.0f,
                         valueStrWidth, valueStrHeight,
                         Qt::AlignCenter | Qt::AlignVCenter,
                         text);
    }
    return image;
    }();
}

QVector4D Utils::getSelection(QPoint mousepos, int height)
{
    // This is the only one that works with OpenGL ES 2.0, so we're forced to use it
    // Item count will be limited to 256*256*256
    GLubyte pixel[4] = {255, 255, 255, 255};
    QOpenGLContext::currentContext()->functions()->glReadPixels(mousepos.x(), height - mousepos.y(),
                                                                1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                                                                (void *)pixel);
    QVector4D selectedColor(pixel[0], pixel[1], pixel[2], pixel[3]);
    return selectedColor;
}

QImage Utils::getGradientImage(QLinearGradient &gradient)
{
    QImage image(QSize(gradientTextureWidth, gradientTextureHeight), QImage::Format_RGB32);
    gradient.setFinalStop(qreal(gradientTextureWidth), qreal(gradientTextureHeight));
    gradient.setStart(0.0, 0.0);

    QPainter pmp(&image);
    pmp.setBrush(QBrush(gradient));
    pmp.setPen(Qt::NoPen);
    pmp.drawRect(0, 0, int(gradientTextureWidth), int(gradientTextureHeight));
    return image;
}

Utils::ParamType Utils::preParseFormat(const QString &format, QString &preStr, QString &postStr,
                                       int &precision, char &formatSpec)
{
    static QRegularExpression formatMatcher(QStringLiteral("^([^%]*)%([\\-\\+#\\s\\d\\.lhjztL]*)([dicuoxfegXFEG])(.*)$"));
    static QRegularExpression precisionMatcher(QStringLiteral("\\.(\\d+)"));

    Utils::ParamType retVal;

    QRegularExpressionMatch formatMatch = formatMatcher.match(format, 0);

    if (formatMatch.hasMatch()) {
        preStr = formatMatch.captured(1);
        // Six and 'g' are defaults in Qt API
        precision = 6;
        if (!formatMatch.captured(2).isEmpty()) {
            QRegularExpressionMatch precisionMatch = precisionMatcher.match(formatMatch.captured(2),
                                                                            0);
            if (precisionMatch.hasMatch())
                precision = precisionMatch.captured(1).toInt();
        }
        if (formatMatch.captured(3).isEmpty())
            formatSpec = 'g';
        else
            formatSpec = formatMatch.captured(3).at(0).toLatin1();
        postStr = formatMatch.captured(4);
        retVal = mapFormatCharToParamType(formatSpec);
    } else {
        retVal = ParamTypeUnknown;
        // The out parameters are irrelevant in unknown case
    }

    return retVal;
}

Utils::ParamType Utils::mapFormatCharToParamType(char formatSpec)
{
    ParamType retVal = ParamTypeUnknown;
    if (formatSpec == 'd' || formatSpec == 'i' || formatSpec == 'c') {
        retVal = ParamTypeInt;
    } else if (formatSpec == 'u' || formatSpec == 'o'
               || formatSpec == 'x'|| formatSpec == 'X') {
        retVal = ParamTypeUInt;
    } else if (formatSpec == 'f' || formatSpec == 'F'
               || formatSpec == 'e' || formatSpec == 'E'
               || formatSpec == 'g' || formatSpec == 'G') {
        retVal = ParamTypeReal;
    }

    return retVal;
}

QString Utils::formatLabelSprintf(const QByteArray &format, Utils::ParamType paramType, qreal value)
{
    switch (paramType) {
    case ParamTypeInt:
        return QString::asprintf(format.constData(), qint64(value));
    case ParamTypeUInt:
        return QString::asprintf(format.constData(), quint64(value));
    case ParamTypeReal:
        return QString::asprintf(format.constData(), value);
    default:
        // Return format string to detect errors. Bars selection label logic also depends on this.
        return QString::fromUtf8(format);
    }
}

QString Utils::formatLabelLocalized(Utils::ParamType paramType, qreal value,
                                    const QLocale &locale, const QString &preStr, const QString &postStr,
                                    int precision, char formatSpec, const QByteArray &format)
{
    switch (paramType) {
    case ParamTypeInt:
    case ParamTypeUInt:
        return preStr + locale.toString(qint64(value)) + postStr;
    case ParamTypeReal:
        return preStr + locale.toString(value, formatSpec, precision) + postStr;
    default:
        // Return format string to detect errors. Bars selection label logic also depends on this.
        return QString::fromUtf8(format);
    }
}

QString Utils::defaultLabelFormat()
{
    static const QString defaultFormat(QStringLiteral("%.2f"));
    return defaultFormat;
}

float Utils::wrapValue(float value, float min, float max)
{
    if (value > max) {
        value = min + (value - max);

        // In case single wrap fails, jump to opposite end.
        if (value > max)
            value = min;
    }

    if (value < min) {
        value = max + (value - min);

        // In case single wrap fails, jump to opposite end.
        if (value < min)
            value = max;
    }

    return value;
}

QQuaternion Utils::calculateRotation(const QVector3D &xyzRotations)
{
    QQuaternion rotQuatX = QQuaternion::fromAxisAndAngle(1.0f, 0.0f, 0.0f, xyzRotations.x());
    QQuaternion rotQuatY = QQuaternion::fromAxisAndAngle(0.0f, 1.0f, 0.0f, xyzRotations.y());
    QQuaternion rotQuatZ = QQuaternion::fromAxisAndAngle(0.0f, 0.0f, 1.0f, xyzRotations.z());
    QQuaternion totalRotation = rotQuatY * rotQuatZ * rotQuatX;
    return totalRotation;
}

bool Utils::isOpenGLES()
{
    if (!staticsResolved)
        resolveStatics();
    return isES;
}

void Utils::resolveStatics()
{
    QOpenGLContext *ctx = QOpenGLContext::currentContext();
    QOffscreenSurface *dummySurface = 0;
    if (!ctx) {
        QSurfaceFormat surfaceFormat;
        dummySurface = new QOffscreenSurface();
        dummySurface->setFormat(surfaceFormat);
        dummySurface->create();
        ctx = new QOpenGLContext;
        ctx->setFormat(surfaceFormat);
        ctx->create();
        ctx->makeCurrent(dummySurface);
    }

#if QT_CONFIG(opengles2)
    isES = true;
#elif (QT_VERSION < QT_VERSION_CHECK(5, 3, 0))
    isES = false;
#else
    isES = ctx->isOpenGLES();
#endif

    ctx->functions()->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);

#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
    // We support only ES2 emulation with software renderer for now
    QString versionStr;
#ifdef Q_OS_WIN
    const GLubyte *openGLVersion = ctx->functions()->glGetString(GL_VERSION);
    versionStr = QString::fromLatin1(reinterpret_cast<const char *>(openGLVersion)).toLower();
#endif
    if (versionStr.contains(QStringLiteral("mesa"), Qt::CaseInsensitive)
            || QCoreApplication::testAttribute(Qt::AA_UseSoftwareOpenGL)) {
        qWarning("Only OpenGL ES2 emulation is available for software rendering.");
        isES = true;
    }
#endif

    if (dummySurface) {
        ctx->doneCurrent();
        delete ctx;
        delete dummySurface;
    }

    staticsResolved = true;
}

QT_END_NAMESPACE
