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

#include "drawer_p.h"
#include "shaderhelper_p.h"
#include "surfaceobject_p.h"
#include "utils_p.h"
#include "texturehelper_p.h"
#include "abstract3drenderer_p.h"
#include "scatterpointbufferhelper_p.h"

#include <QtGui/QMatrix4x4>
#include <QtGui/qquaternion.h>

#include <QtCore/qmath.h>

static void initResources()
{
    Q_INIT_RESOURCE(datavisualizationshaders);
    Q_INIT_RESOURCE(datavisualizationmeshes);
}

Q_CONSTRUCTOR_FUNCTION(initResources)

QT_BEGIN_NAMESPACE

// Vertex array buffer for point
const GLfloat point_data[] = {0.0f, 0.0f, 0.0f};

// Vertex array buffer for line
const GLfloat line_data[] = {
    -1.0f, 0.0f, 0.0f,
    1.0f, 0.0f, 0.0f,
};

Drawer::Drawer(Q3DTheme *theme)
    : m_theme(theme),
      m_textureHelper(0),
      m_pointbuffer(0),
      m_linebuffer(0),
      m_scaledFontSize(0.0f)
{
}

Drawer::~Drawer()
{
    delete m_textureHelper;
    if (QOpenGLContext::currentContext()) {
        glDeleteBuffers(1, &m_pointbuffer);
        glDeleteBuffers(1, &m_linebuffer);
    }
}

void Drawer::initializeOpenGL()
{
    initializeOpenGLFunctions();
    if (!m_textureHelper)
        m_textureHelper = new TextureHelper();
}

void Drawer::setTheme(Q3DTheme *theme)
{
    m_theme = theme;
    m_scaledFontSize = 0.05f + m_theme->font().pointSizeF() / 500.0f;
    emit drawerChanged();
}

Q3DTheme *Drawer::theme() const
{
    return m_theme;
}

QFont Drawer::font() const
{
    return m_theme->font();
}

void Drawer::drawObject(ShaderHelper *shader, AbstractObjectHelper *object, GLuint textureId,
                        GLuint depthTextureId, GLuint textureId3D)
{
#if QT_CONFIG(opengles2)
    Q_UNUSED(textureId3D);
#endif
    if (textureId) {
        // Activate texture
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureId);
        shader->setUniformValue(shader->texture(), 0);
    }

    if (depthTextureId) {
        // Activate depth texture
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, depthTextureId);
        shader->setUniformValue(shader->shadow(), 1);
    }
#if !QT_CONFIG(opengles2)
    if (textureId3D) {
        // Activate texture
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_3D, textureId3D);
        shader->setUniformValue(shader->texture(), 2);
    }
#endif

    // 1st attribute buffer : vertices
    glEnableVertexAttribArray(shader->posAtt());
    glBindBuffer(GL_ARRAY_BUFFER, object->vertexBuf());
    glVertexAttribPointer(shader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

    // 2nd attribute buffer : normals
    if (shader->normalAtt() >= 0) {
        glEnableVertexAttribArray(shader->normalAtt());
        glBindBuffer(GL_ARRAY_BUFFER, object->normalBuf());
        glVertexAttribPointer(shader->normalAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    }

    // 3rd attribute buffer : UVs
    if (shader->uvAtt() >= 0) {
        glEnableVertexAttribArray(shader->uvAtt());
        glBindBuffer(GL_ARRAY_BUFFER, object->uvBuf());
        glVertexAttribPointer(shader->uvAtt(), 2, GL_FLOAT, GL_FALSE, 0, (void*)0);
    }

    // Index buffer
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, object->elementBuf());

    // Draw the triangles
    glDrawElements(GL_TRIANGLES, object->indexCount(), GL_UNSIGNED_INT, (void*)0);

    // Free buffers
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    if (shader->uvAtt() >= 0)
        glDisableVertexAttribArray(shader->uvAtt());
    if (shader->normalAtt() >= 0)
        glDisableVertexAttribArray(shader->normalAtt());
    glDisableVertexAttribArray(shader->posAtt());

    // Release textures
#if !QT_CONFIG(opengles2)
    if (textureId3D) {
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_3D, 0);
    }
#endif
    if (depthTextureId) {
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, 0);
    }
    if (textureId) {
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, 0);
    }
}

void Drawer::drawSelectionObject(ShaderHelper *shader, AbstractObjectHelper *object)
{
    glEnableVertexAttribArray(shader->posAtt());
    glBindBuffer(GL_ARRAY_BUFFER, object->vertexBuf());
    glVertexAttribPointer(shader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void *)0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, object->elementBuf());
    glDrawElements(GL_TRIANGLES, object->indexCount(), GL_UNSIGNED_INT, (void *)0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDisableVertexAttribArray(shader->posAtt());
}

void Drawer::drawSurfaceGrid(ShaderHelper *shader, SurfaceObject *object)
{
    // Get grid line color
    QVector4D lineColor = Utils::vectorFromColor(object->wireframeColor());
    shader->setUniformValue(shader->color(), lineColor);

    // 1st attribute buffer : vertices
    glEnableVertexAttribArray(shader->posAtt());
    glBindBuffer(GL_ARRAY_BUFFER, object->vertexBuf());
    glVertexAttribPointer(shader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

    // Index buffer
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, object->gridElementBuf());

    // Draw the lines
    glDrawElements(GL_LINES, object->gridIndexCount(), GL_UNSIGNED_INT, (void*)0);

    // Free buffers
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    glDisableVertexAttribArray(shader->posAtt());
}

void Drawer::drawPoint(ShaderHelper *shader)
{
    // Draw a single point

    // Generate vertex buffer for point if it does not exist
    if (!m_pointbuffer) {
        glGenBuffers(1, &m_pointbuffer);
        glBindBuffer(GL_ARRAY_BUFFER, m_pointbuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(point_data), point_data, GL_STATIC_DRAW);
    }

    // 1st attribute buffer : vertices
    glEnableVertexAttribArray(shader->posAtt());
    glBindBuffer(GL_ARRAY_BUFFER, m_pointbuffer);
    glVertexAttribPointer(shader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

    // Draw the point
    glDrawArrays(GL_POINTS, 0, 1);

    // Free buffers
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glDisableVertexAttribArray(shader->posAtt());
}

void Drawer::drawPoints(ShaderHelper *shader, ScatterPointBufferHelper *object, GLuint textureId)
{
    if (textureId) {
        // Activate texture
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureId);
        shader->setUniformValue(shader->texture(), 0);
    }

    // 1st attribute buffer : vertices
    glEnableVertexAttribArray(shader->posAtt());
    glBindBuffer(GL_ARRAY_BUFFER, object->pointBuf());
    glVertexAttribPointer(shader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

    // 2nd attribute buffer : UVs
    if (textureId) {
        glEnableVertexAttribArray(shader->uvAtt());
        glBindBuffer(GL_ARRAY_BUFFER, object->uvBuf());
        glVertexAttribPointer(shader->uvAtt(), 2, GL_FLOAT, GL_FALSE, 0, (void*)0);
    }

    // Draw the points
    glDrawArrays(GL_POINTS, 0, object->indexCount());

    // Free buffers
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glDisableVertexAttribArray(shader->posAtt());

    if (textureId) {
        glDisableVertexAttribArray(shader->uvAtt());
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, 0);
    }
}

void Drawer::drawLine(ShaderHelper *shader)
{
    // Draw a single line

    // Generate vertex buffer for line if it does not exist
    if (!m_linebuffer) {
        glGenBuffers(1, &m_linebuffer);
        glBindBuffer(GL_ARRAY_BUFFER, m_linebuffer);
        glBufferData(GL_ARRAY_BUFFER, sizeof(line_data), line_data, GL_STATIC_DRAW);
    }

    // 1st attribute buffer : vertices
    glEnableVertexAttribArray(shader->posAtt());
    glBindBuffer(GL_ARRAY_BUFFER, m_linebuffer);
    glVertexAttribPointer(shader->posAtt(), 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

    // Draw the line
    glDrawArrays(GL_LINES, 0, 2);

    // Free buffers
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glDisableVertexAttribArray(shader->posAtt());
}

void Drawer::drawLabel(const AbstractRenderItem &item, const LabelItem &labelItem,
                       const QMatrix4x4 &viewmatrix, const QMatrix4x4 &projectionmatrix,
                       const QVector3D &positionComp, const QQuaternion &rotation,
                       GLfloat itemHeight, QAbstract3DGraph::SelectionFlags mode,
                       ShaderHelper *shader, ObjectHelper *object,
                       const Q3DCamera *camera, bool useDepth, bool rotateAlong,
                       LabelPosition position, Qt::Alignment alignment, bool isSlicing,
                       bool isSelecting)
{
    // Draw label
    if (!labelItem.textureId())
        return; // No texture, skip

    QSize textureSize = labelItem.size();
    QMatrix4x4 modelMatrix;
    QMatrix4x4 MVPMatrix;
    GLfloat xPosition = 0.0f;
    GLfloat yPosition = 0.0f;
    GLfloat zPosition = positionComp.z();

    switch (position) {
    case LabelBelow: {
        yPosition = item.translation().y() - (positionComp.y() / 2.0f) + itemHeight - 0.1f;
        break;
    }
    case LabelLow: {
        yPosition = -positionComp.y();
        break;
    }
    case LabelMid: {
        yPosition = item.translation().y();
        break;
    }
    case LabelHigh: {
        yPosition = item.translation().y() + itemHeight / 2.0f;
        break;
    }
    case LabelOver: {
        yPosition = item.translation().y() - (positionComp.y() / 2.0f) + itemHeight + 0.1f;
        break;
    }
    case LabelBottom: {
        yPosition = -2.75f + positionComp.y();
        xPosition = 0.0f;
        break;
    }
    case LabelTop: {
        yPosition = 2.75f - positionComp.y();
        xPosition = 0.0f;
        break;
    }
    case LabelLeft: {
        yPosition = 0.0f;
        xPosition = -2.75f;
        break;
    }
    case LabelRight: {
        yPosition = 0.0f;
        xPosition = 2.75f;
        break;
    }
    }

    // Calculate scale factor to get uniform font size
    GLfloat scaleFactor = m_scaledFontSize / (GLfloat)textureSize.height();

    // Apply alignment
    QVector3D anchorPoint;

    if (alignment & Qt::AlignLeft)
        anchorPoint.setX(float(textureSize.width()) * scaleFactor);
    else if (alignment & Qt::AlignRight)
        anchorPoint.setX(float(-textureSize.width()) * scaleFactor);

    if (alignment & Qt::AlignTop)
        anchorPoint.setY(float(-textureSize.height()) * scaleFactor);
    else if (alignment & Qt::AlignBottom)
        anchorPoint.setY(float(textureSize.height()) * scaleFactor);

    if (position < LabelBottom) {
        xPosition = item.translation().x();
        if (useDepth)
            zPosition = item.translation().z();
        else if (mode.testFlag(QAbstract3DGraph::SelectionColumn) && isSlicing)
            xPosition = -(item.translation().z()) + positionComp.z(); // flip first to left
    }

    // Position label
    modelMatrix.translate(xPosition, yPosition, zPosition);

    // Rotate
    if (useDepth && !rotateAlong) {
        float yComp = float(qRadiansToDegrees(qTan(positionComp.y() / cameraDistance)));
        // Apply negative camera rotations to keep labels facing camera
        float camRotationX = camera->xRotation();
        float camRotationY = camera->yRotation();
        modelMatrix.rotate(-camRotationX, 0.0f, 1.0f, 0.0f);
        modelMatrix.rotate(-camRotationY - yComp, 1.0f, 0.0f, 0.0f);
    } else {
        modelMatrix.rotate(rotation);
    }
    modelMatrix.translate(anchorPoint);

    // Scale label based on text size
    modelMatrix.scale(QVector3D((GLfloat)textureSize.width() * scaleFactor,
                                m_scaledFontSize,
                                0.0f));

    MVPMatrix = projectionmatrix * viewmatrix * modelMatrix;

    shader->setUniformValue(shader->MVP(), MVPMatrix);

    if (isSelecting) {
        // Draw the selection object
        drawSelectionObject(shader, object);
    } else {
        // Draw the object
        drawObject(shader, object, labelItem.textureId());
    }
}

void Drawer::generateSelectionLabelTexture(Abstract3DRenderer *renderer)
{
    LabelItem &labelItem = renderer->selectionLabelItem();
    generateLabelItem(labelItem, renderer->selectionLabel());
}

void Drawer::generateLabelItem(LabelItem &item, const QString &text, int widestLabel)
{
    initializeOpenGL();

    item.clear();

    if (!text.isEmpty()) {
        // Create labels
        // Print label into a QImage using QPainter
        QImage label = Utils::printTextToImage(m_theme->font(),
                                               text,
                                               m_theme->labelBackgroundColor(),
                                               m_theme->labelTextColor(),
                                               m_theme->isLabelBackgroundEnabled(),
                                               m_theme->isLabelBorderEnabled(),
                                               widestLabel);

        // Set label size
        item.setSize(label.size());
        // Insert text texture into label (also deletes the old texture)
        item.setTextureId(m_textureHelper->create2DTexture(label, true, true));
    }
}

QT_END_NAMESPACE
