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

#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses

#ifndef QT_NO_OPENGL

#include "private/glwidget_p.h"
#include "private/glxyseriesdata_p.h"
#include "private/qabstractseries_p.h"
#include <QtOpenGL/QOpenGLShaderProgram>
#include <QtGui/QOpenGLContext>
#include <QtOpenGL/QOpenGLBuffer>

//#define QDEBUG_TRACE_GL_FPS
#ifdef QDEBUG_TRACE_GL_FPS
#  include <QElapsedTimer>
#endif

QT_BEGIN_NAMESPACE

GLWidget::GLWidget(GLXYSeriesDataManager *xyDataManager, QChart *chart,
                   QGraphicsView *parent)
    : QOpenGLWidget(parent->viewport()),
      m_program(nullptr),
      m_shaderAttribLoc(-1),
      m_colorUniformLoc(-1),
      m_minUniformLoc(-1),
      m_deltaUniformLoc(-1),
      m_pointSizeUniformLoc(-1),
      m_xyDataManager(xyDataManager),
      m_antiAlias(parent->renderHints().testFlag(QPainter::Antialiasing)),
      m_view(parent),
      m_selectionFbo(nullptr),
      m_chart(chart),
      m_recreateSelectionFbo(true),
      m_selectionRenderNeeded(true),
      m_mousePressed(false),
      m_lastPressSeries(nullptr),
      m_lastHoverSeries(nullptr)
{
    setAttribute(Qt::WA_TranslucentBackground);
    setAttribute(Qt::WA_AlwaysStackOnTop);

    QSurfaceFormat surfaceFormat;
    surfaceFormat.setDepthBufferSize(0);
    surfaceFormat.setStencilBufferSize(0);
    surfaceFormat.setRedBufferSize(8);
    surfaceFormat.setGreenBufferSize(8);
    surfaceFormat.setBlueBufferSize(8);
    surfaceFormat.setAlphaBufferSize(8);
    surfaceFormat.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
    surfaceFormat.setRenderableType(QSurfaceFormat::DefaultRenderableType);
    surfaceFormat.setSamples(m_antiAlias ? 4 : 0);
    setFormat(surfaceFormat);

    connect(xyDataManager, &GLXYSeriesDataManager::seriesRemoved,
            this, &GLWidget::cleanXYSeriesResources);

    setMouseTracking(true);
}

GLWidget::~GLWidget()
{
    cleanup();
}

void GLWidget::cleanup()
{
    makeCurrent();

    delete m_program;
    m_program = 0;

    foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values())
        delete buffer;
    m_seriesBufferMap.clear();

    doneCurrent();

    context()->disconnect(this);
}

void GLWidget::cleanXYSeriesResources(const QXYSeries *series)
{
    makeCurrent();
    if (series) {
        delete m_seriesBufferMap.take(series);
    } else {
        // Null series means all series were removed
        foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values())
            delete buffer;
        m_seriesBufferMap.clear();
    }
    doneCurrent();
}

static const char *vertexSource =
        "attribute highp vec2 points;\n"
        "uniform highp vec2 min;\n"
        "uniform highp vec2 delta;\n"
        "uniform highp float pointSize;\n"
        "uniform highp mat4 matrix;\n"
        "void main() {\n"
        "  vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"
        "  gl_Position = matrix * vec4(normalPoint, 0, 1);\n"
        "  gl_PointSize = pointSize;\n"
        "}";
static const char *fragmentSource =
        "uniform highp vec3 color;\n"
        "void main() {\n"
        "  gl_FragColor = vec4(color,1);\n"
        "}\n";

void GLWidget::initializeGL()
{
    connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &GLWidget::cleanup);

    initializeOpenGLFunctions();
    glClearColor(0, 0, 0, 0);

    m_program = new QOpenGLShaderProgram;
    m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSource);
    m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSource);
    m_program->bindAttributeLocation("points", 0);
    m_program->link();

    m_program->bind();
    m_colorUniformLoc = m_program->uniformLocation("color");
    m_minUniformLoc = m_program->uniformLocation("min");
    m_deltaUniformLoc = m_program->uniformLocation("delta");
    m_pointSizeUniformLoc = m_program->uniformLocation("pointSize");
    m_matrixUniformLoc = m_program->uniformLocation("matrix");


    // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x
    // implementations this is optional and support may not be present
    // at all. Nonetheless the below code works in all cases and makes
    // sure there is a VAO when one is needed.
    m_vao.create();
    QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao);

    glEnableVertexAttribArray(0);

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_STENCIL_TEST);

#if !QT_CONFIG(opengles2)
    if (!QOpenGLContext::currentContext()->isOpenGLES()) {
        // Make it possible to change point primitive size and use textures with them in
        // the shaders. These are implicitly enabled in ES2.
        glEnable(GL_PROGRAM_POINT_SIZE);
    }
#endif

    m_program->release();
}

void GLWidget::paintGL()
{
    render(false);

#ifdef QDEBUG_TRACE_GL_FPS
    static QElapsedTimer stopWatch;
    static int frameCount = -1;
    if (frameCount == -1) {
        stopWatch.start();
        frameCount = 0;
    }
    frameCount++;
    int elapsed = stopWatch.elapsed();
    if (elapsed >= 1000) {
        elapsed = stopWatch.restart();
        qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed))));
        qDebug() << "FPS:" << fps;
        frameCount = 0;
    }
#endif
}

void GLWidget::resizeGL(int w, int h)
{
    m_fboSize.setWidth(w);
    m_fboSize.setHeight(h);
    m_recreateSelectionFbo = true;
    m_selectionRenderNeeded = true;
}

void GLWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
    QXYSeries *series = findSeriesAtEvent(event);
    if (series)
        emit series->doubleClicked(series->d_ptr->domain()->calculateDomainPoint(event->pos()));
}

void GLWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (m_view->hasMouseTracking() && !event->buttons()) {
        QXYSeries *series = findSeriesAtEvent(event);
        if (series != m_lastHoverSeries) {
            if (m_lastHoverSeries) {
                if (chartSeries(m_lastHoverSeries)) {
                    emit m_lastHoverSeries->hovered(
                                m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint(
                                    event->pos()), false);
                }
            }
            if (series) {
                emit series->hovered(
                            series->d_ptr->domain()->calculateDomainPoint(event->pos()), true);
            }
            m_lastHoverSeries = series;
        }
    } else {
        event->ignore();
    }
}

void GLWidget::mousePressEvent(QMouseEvent *event)
{
    QXYSeries *series = findSeriesAtEvent(event);
    if (series) {
        m_mousePressed = true;
        m_mousePressPos = event->pos();
        m_lastPressSeries = series;
        emit series->pressed(series->d_ptr->domain()->calculateDomainPoint(event->pos()));
    }
}

void GLWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (chartSeries(m_lastPressSeries)) {
        emit m_lastPressSeries->released(
                    m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(m_mousePressPos));
        if (m_mousePressed) {
            emit m_lastPressSeries->clicked(
                        m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(m_mousePressPos));
        }
        if (m_lastHoverSeries == m_lastPressSeries
                && m_lastHoverSeries != findSeriesAtEvent(event)) {
            if (chartSeries(m_lastHoverSeries)) {
                emit m_lastHoverSeries->hovered(
                            m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint(
                                event->pos()), false);
            }
            m_lastHoverSeries = nullptr;
        }
        m_lastPressSeries = nullptr;
        m_mousePressed = false;
    } else {
        event->ignore();
    }
}

QXYSeries *GLWidget::findSeriesAtEvent(QMouseEvent *event)
{
    QXYSeries *series = nullptr;
    int index = -1;

    if (m_xyDataManager->dataMap().size()) {
        makeCurrent();

        if (m_recreateSelectionFbo)
            recreateSelectionFbo();

        m_selectionFbo->bind();

        if (m_selectionRenderNeeded) {
            m_selectionList.resize(m_xyDataManager->dataMap().size());
            render(true);
            m_selectionRenderNeeded = false;
        }

        GLubyte pixel[4] = {0, 0, 0, 0};
        glReadPixels(event->pos().x(), m_fboSize.height() - event->pos().y(),
                     1, 1, GL_RGBA, GL_UNSIGNED_BYTE,
                     (void *)pixel);
        if (pixel[3] == 0xff)
            index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16);

        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());

        doneCurrent();
    }

    if (index >= 0) {
        const QXYSeries *cSeries = nullptr;
        if (index < m_selectionList.size())
            cSeries = m_selectionList.at(index);

        series = chartSeries(cSeries);
    }
    if (series)
        event->accept();
    else
        event->ignore();

    return series;
}

void GLWidget::render(bool selection)
{
    glClear(GL_COLOR_BUFFER_BIT);

    QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao);
    m_program->bind();

    int counter = 0;
    const auto &dataMap = m_xyDataManager->dataMap();
    for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) {
        QOpenGLBuffer *vbo = m_seriesBufferMap.value(i.key());
        GLXYSeriesData *data = i.value();

        if (data->visible) {
            if (selection) {
                m_selectionList[counter] = i.key();
                m_program->setUniformValue(m_colorUniformLoc, QVector3D((counter & 0xff) / 255.0f,
                                                                        ((counter & 0xff00) >> 8) / 255.0f,
                                                                        ((counter & 0xff0000) >> 16) / 255.0f));
                counter++;
            } else {
                m_program->setUniformValue(m_colorUniformLoc, data->color);
            }
            m_program->setUniformValue(m_minUniformLoc, data->min);
            m_program->setUniformValue(m_deltaUniformLoc, data->delta);
            m_program->setUniformValue(m_matrixUniformLoc, data->matrix);
            bool dirty = data->dirty;
            if (!vbo) {
                vbo = new QOpenGLBuffer;
                m_seriesBufferMap.insert(i.key(), vbo);
                vbo->create();
                dirty = true;
            }
            vbo->bind();
            if (dirty) {
                vbo->allocate(data->array.constData(), int(data->array.size() * sizeof(GLfloat)));
                dirty = false;
                m_selectionRenderNeeded = true;
            }

            glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
            if (data->type == QAbstractSeries::SeriesTypeLine) {
                glLineWidth(data->width);
                glDrawArrays(GL_LINE_STRIP, 0, data->array.size() / 2);
            } else { // Scatter
                m_program->setUniformValue(m_pointSizeUniformLoc, data->width);
                glDrawArrays(GL_POINTS, 0, data->array.size() / 2);
            }
            vbo->release();
        }
    }
    m_program->release();
}

void GLWidget::recreateSelectionFbo()
{
    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment);

    delete m_selectionFbo;

    const QSize deviceSize = m_fboSize * devicePixelRatio();
    m_selectionFbo = new QOpenGLFramebufferObject(deviceSize, fboFormat);
    m_recreateSelectionFbo = false;
    m_selectionRenderNeeded = true;
}

// This function makes sure the series we are dealing with has not been removed from the
// chart since we stored the pointer.
QXYSeries *GLWidget::chartSeries(const QXYSeries *cSeries)
{
    QXYSeries *series = nullptr;
    if (cSeries) {
        const auto allSeries = m_chart->series();
        for (QAbstractSeries *chartSeries : allSeries) {
            if (cSeries == chartSeries) {
                series = qobject_cast<QXYSeries *>(chartSeries);
                break;
            }
        }
    }
    return series;
}

bool GLWidget::needsReset() const
{
    return m_view->renderHints().testFlag(QPainter::Antialiasing) != m_antiAlias;
}

QT_END_NAMESPACE

#endif
