/**
 *
 * @file plugins/MatrixVisualizer/Windows/MatrixGLWidget.cpp
 *
 * @copyright 2008-2024 Bordeaux INP, CNRS (LaBRI UMR 5800), Inria,
 *                      Univ. Bordeaux. All rights reserved.
 *
 * @author Camille Ordronneau
 * @author Arthur Chevalier
 * @author Johnny Jazeix
 * @author Mathieu Faverge
 * @author Hamza Benmendil
 *
 * @date 2024-07-17
 */
#include "Common/Zooming.hpp"
#include "Common/Quadtree.hpp"

#include "MatrixGLWidget.hpp"
#include "MatrixWindow.hpp"

#define BACKGROUND_COLOR_R 0.9f
#define BACKGROUND_COLOR_G 0.9f
#define BACKGROUND_COLOR_B 0.9f

/* Helpers */
static void drawSquare(GLfloat x, GLfloat y, GLfloat dx, GLfloat dy, GLfloat r, GLfloat g, GLfloat b) {
    glBegin(GL_QUADS);

    // Setup color
    glColor3f(r, g, b);

    // Setup square corners
    glVertex2f(x, y);
    glVertex2f(x + dx, y);
    glVertex2f(x + dx, y + dy);
    glVertex2f(x, y + dy);

    glEnd();
}

static void drawTemporarySelection(int x, int y, int dx, int dy) {
    glBegin(GL_QUADS);

    // Setup color
    glColor4f(1.f, 1.f, 1.f, .5f);

    // Setup vertex
    glVertex2f(x, y);
    glVertex2f(x + dx, y);
    glVertex2f(x + dx, y + dy);
    glVertex2f(x, y + dy);

    glEnd();
}

MatrixGLWidget::MatrixGLWidget(QWidget *parent, symbol_matrix_t *matrix, QLabel *label, bool quadtree_checked) :
    QOpenGLWidget(parent), m_frameCount(0), m_label(label) {
    this->setFixedSize(MATRIX_WINDOW_LENGTH, MATRIX_WINDOW_HEIGHT);

    // choose one of the zoom methods depending on whether quadtree button is checked ot not
    if (quadtree_checked) {
        m_zoom = new Quadtree(matrix);
    }
    else {
        m_zoom = new Zooming(matrix);
    }

    m_qtToGLWidthCoeff = 1.f;
    m_qtToGLHeightCoeff = 1.f;

    m_camera.m_cameraX = 0.f;
    m_camera.m_cameraY = 0.f;
    m_camera.m_cameraDx = 1.f;
    m_camera.m_cameraDy = 1.f;

    m_mouseXClicked = -1;
    m_mouseYClicked = -1;
}

MatrixGLWidget::~MatrixGLWidget() {
    delete m_zoom;
}

void MatrixGLWidget::initializeGL() {
    initializeOpenGLFunctions();

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);

    glClearColor(BACKGROUND_COLOR_R, BACKGROUND_COLOR_G, BACKGROUND_COLOR_B, 1.f);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, Zoom::DEFAULT_LEVEL, Zoom::DEFAULT_LEVEL, 0, -1, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0, 2, 0);
}

void MatrixGLWidget::resizeGL(int w, int h) {
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, Zoom::DEFAULT_LEVEL, Zoom::DEFAULT_LEVEL, 0, -1, 1);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0, 2, 0);

    m_qtToGLWidthCoeff = (double)Zoom::DEFAULT_LEVEL / (double)w;
    m_qtToGLHeightCoeff = (double)Zoom::DEFAULT_LEVEL / (double)h;
}

void MatrixGLWidget::paintGL() {
    int i, j;

    initializeOpenGLFunctions();

    if (m_frameCount == 0) {
        m_time.start();
    }
    else {
        sprintf(m_fpsString, "FPS: %f ms", m_time.elapsed() / float(m_frameCount));
        m_label->setText(QString::fromStdString(m_fpsString));
    }

    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(BACKGROUND_COLOR_R,
                 BACKGROUND_COLOR_G,
                 BACKGROUND_COLOR_B,
                 0.f);

    for (i = 0; i < Zoom::DEFAULT_LEVEL; ++i) {
        for (j = 0; j < Zoom::DEFAULT_LEVEL; ++j) {
            GLfloat grey = m_zoom->getColor(i, j);

            if (grey != 1.f) {
                if (grey != 0.f) {
                    drawSquare(i, j, 1, 1, 0., 0., grey);
                }
                else {
                    drawSquare(i, j, 1, 1, grey, grey, grey);
                }
            }
        }
    }

    if (m_drawTempSelection == 1) {
        drawTemporarySelection(m_tempSelectionX, m_tempSelectionY, m_tempSelectionDx, m_tempSelectionDy);
    }

    m_frameCount++;
}

void MatrixGLWidget::keyPressEvent(QKeyEvent *keyEvent) {
    switch (keyEvent->key()) {
    default:
        break;
    }
}

void MatrixGLWidget::mousePressEvent(QMouseEvent *mouseEvent) {
    if (mouseEvent->button() == Qt::RightButton) {
        if (m_savedPositions.size() > 0) {
            CameraPosition newCamera = m_savedPositions.top();
            m_savedPositions.pop();

            m_camera = newCamera;

            refreshCamera();
        }
    }
    else if (mouseEvent->button() == Qt::LeftButton) {
        m_mouseXClicked = mouseEvent->x() * m_qtToGLWidthCoeff;
        m_mouseYClicked = mouseEvent->y() * m_qtToGLHeightCoeff;
    }
}

void MatrixGLWidget::mouseMoveEvent(QMouseEvent *mouseEvent) {
    if ((m_mouseXClicked != -1) && (m_mouseYClicked != -1)) {
        int glPosX = mouseEvent->x() * m_qtToGLWidthCoeff;
        int glPosY = mouseEvent->y() * m_qtToGLHeightCoeff;

        m_drawTempSelection = 1;

        m_tempSelectionX = m_mouseXClicked;
        m_tempSelectionY = m_mouseYClicked;
        m_tempSelectionDx = glPosX - m_mouseXClicked;
        m_tempSelectionDy = glPosY - m_mouseYClicked;

        update();
    }
}

void MatrixGLWidget::mouseReleaseEvent(QMouseEvent *mouseEvent) {
    if ((m_mouseXClicked != -1) && (m_mouseYClicked != -1)) {
        int xmin, xmax, ymin, ymax;
        int glPosX = mouseEvent->x() * m_qtToGLWidthCoeff;
        int glPosY = mouseEvent->y() * m_qtToGLHeightCoeff;

        if (glPosX > m_mouseXClicked) {
            xmin = m_mouseXClicked;
            xmax = glPosX;
        }
        else {
            xmin = glPosX;
            xmax = m_mouseXClicked;
        }

        if (glPosY > m_mouseYClicked) {
            ymin = m_mouseYClicked;
            ymax = glPosY;
        }
        else {
            ymin = glPosY;
            ymax = m_mouseYClicked;
        }

        // Compute positions/dimensions in ratio of the current window
        double X = (double)(xmin) / (double)Zoom::DEFAULT_LEVEL;
        double Y = (double)(ymin) / (double)Zoom::DEFAULT_LEVEL;
        double dX = (double)(xmax - xmin) / (double)Zoom::DEFAULT_LEVEL;
        double dY = (double)(ymax - ymin) / (double)Zoom::DEFAULT_LEVEL;

        // Check for out of bounds
        X = (X < 0.) ? 0. : X;
        Y = (Y < 0.) ? 0. : Y;
        dX = ((X + dX) > 1.) ? 1. - X : dX;
        dY = ((Y + dY) > 1.) ? 1. - Y : dY;

        // Translate to the full display
        X = X * m_camera.m_cameraDx + m_camera.m_cameraX;
        Y = Y * m_camera.m_cameraDy + m_camera.m_cameraY;
        dX = dX * m_camera.m_cameraDx;
        dY = dY * m_camera.m_cameraDy;

        m_drawTempSelection = 0;

        // Return if in the upper triangular part
        if ((Y + dY) < X) {
            update();
            return;
        }

        // Position is valid, we save it
        m_savedPositions.push(m_camera);

        m_camera.m_cameraX = X;
        m_camera.m_cameraY = Y;
        m_camera.m_cameraDx = dX;
        m_camera.m_cameraDy = dY;

        refreshCamera();

        m_mouseXClicked = -1;
        m_mouseYClicked = -1;
    }
}

void MatrixGLWidget::refreshCamera() {
    m_zoom->move(m_camera.m_cameraX, m_camera.m_cameraX + m_camera.m_cameraDx,
                 m_camera.m_cameraY, m_camera.m_cameraY + m_camera.m_cameraDy);
    update();
}
