/*
 *  nextpnr -- Next Generation Place and Route
 *
 *  Copyright (C) 2018  Serge Bazanski <q3k@symbioticeda.com>
 *
 *  Permission to use, copy, modify, and/or distribute this software for any
 *  purpose with or without fee is hereby granted, provided that the above
 *  copyright notice and this permission notice appear in all copies.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 *  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 *  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 *  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 *  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 *  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include <cstdio>
#include <math.h>

#include <QApplication>
#include <QCoreApplication>
#include <QMouseEvent>
#include <QWidget>

#include "fpgaviewwidget.h"
#include "log.h"
#include "mainwindow.h"

NEXTPNR_NAMESPACE_BEGIN

void PolyLine::buildPoint(LineShaderData *building, const QVector2D *prev,
                          const QVector2D *cur, const QVector2D *next) const
{
    // buildPoint emits two vertices per line point, along with normals to move
    // them the right directio when rendering and miter to compensate for
    // bends.

    if (cur == nullptr) {
        // BUG
        return;
    }

    if (prev == nullptr && next == nullptr) {
        // BUG
        return;
    }

    // TODO(q3k): fast path for vertical/horizontal lines?

    // TODO(q3k): consider moving some of the linear algebra to the GPU,
    // they're better at this than poor old CPUs.

    // Build two unit vectors pointing in the direction of the two segments
    // defined by (prev, cur) and (cur, next)
    QVector2D dprev, dnext;
    if (prev == nullptr) {
        dnext = *next - *cur;
        dprev = dnext;
    } else if (next == nullptr) {
        dprev = *cur - *prev;
        dnext = dprev;
    } else {
        dprev = *cur - *prev;
        dnext = *next - *cur;
    }
    dprev.normalize();
    dnext.normalize();

    // Calculate tangent unit vector.
    QVector2D tangent(dprev + dnext);
    tangent.normalize();

    // Calculate normal to tangent - this is the line on which the vectors need
    // to be pushed to build a thickened line.
    const QVector2D tangent_normal = QVector2D(-tangent.y(), tangent.x());

    // Calculate normal to one of the lines.
    const QVector2D dprev_normal = QVector2D(-dprev.y(), dprev.x());
    // https://people.eecs.berkeley.edu/~sequin/CS184/IMGS/Sweep_PolyLine.jpg
    // (the ^-1 is performed in the shader)
    const float miter = QVector2D::dotProduct(tangent_normal, dprev_normal);

    const float x = cur->x();
    const float y = cur->y();
    const float mx = tangent_normal.x();
    const float my = tangent_normal.y();

    // Push back 'left' vertex.
    building->vertices.push_back(Vertex2DPOD(x, y));
    building->normals.push_back(Vertex2DPOD(mx, my));
    building->miters.push_back(miter);

    // Push back 'right' vertex.
    building->vertices.push_back(Vertex2DPOD(x, y));
    building->normals.push_back(Vertex2DPOD(mx, my));
    building->miters.push_back(-miter);
}

void PolyLine::build(LineShaderData &target) const
{
    if (points_.size() < 2) {
        return;
    }
    const QVector2D *first = &points_.front();
    const QVector2D *last = &points_.back();

    // Index number of vertices, used to build the index buffer.
    unsigned int startIndex = target.vertices.size();
    unsigned int index = startIndex;

    // For every point on the line, call buildPoint with (prev, point, next).
    // If we're building a closed line, prev/next wrap around. Otherwise
    // they are passed as nullptr and buildPoint interprets that accordinglu.
    const QVector2D *prev = nullptr;

    // Loop iterator used to ensure next is valid.
    unsigned int i = 0;
    for (const QVector2D &point : points_) {
        const QVector2D *next = nullptr;
        if (++i < points_.size()) {
            next = (&point + 1);
        }

        // If the line is closed, wrap around. Otherwise, pass nullptr.
        if (prev == nullptr && closed_) {
            buildPoint(&target, last, &point, next);
        } else if (next == nullptr && closed_) {
            buildPoint(&target, prev, &point, first);
        } else {
            buildPoint(&target, prev, &point, next);
        }

        // If we have a prev point relative to cur, build a pair of triangles
        // to render vertices into lines.
        if (prev != nullptr) {
            target.indices.push_back(index);
            target.indices.push_back(index + 1);
            target.indices.push_back(index + 2);

            target.indices.push_back(index + 2);
            target.indices.push_back(index + 1);
            target.indices.push_back(index + 3);

            index += 2;
        }
        prev = &point;
    }

    // If we're closed, build two more vertices that loop the line around.
    if (closed_) {
        target.indices.push_back(index);
        target.indices.push_back(index + 1);
        target.indices.push_back(startIndex);

        target.indices.push_back(startIndex);
        target.indices.push_back(index + 1);
        target.indices.push_back(startIndex + 1);
    }
}

bool LineShader::compile(void)
{
    program_ = new QOpenGLShaderProgram(parent_);
    program_->addShaderFromSourceCode(QOpenGLShader::Vertex,
                                      vertexShaderSource_);
    program_->addShaderFromSourceCode(QOpenGLShader::Fragment,
                                      fragmentShaderSource_);
    if (!program_->link()) {
        printf("could not link program: %s\n",
               program_->log().toStdString().c_str());
        return false;
    }
    attributes_.position = program_->attributeLocation("position");
    attributes_.normal = program_->attributeLocation("normal");
    attributes_.miter = program_->attributeLocation("miter");
    uniforms_.thickness = program_->uniformLocation("thickness");
    uniforms_.projection = program_->uniformLocation("projection");
    uniforms_.color = program_->uniformLocation("color");

    return true;
}

void LineShader::draw(const LineShaderData &line, const QMatrix4x4 &projection)
{
    auto gl = QOpenGLContext::currentContext()->functions();
    program_->bind();

    program_->setUniformValue(uniforms_.projection, projection);
    program_->setUniformValue(uniforms_.thickness, line.thickness);
    program_->setUniformValue(uniforms_.color, line.color.r, line.color.g,
                              line.color.b, line.color.a);

    gl->glVertexAttribPointer(attributes_.position, 2, GL_FLOAT, GL_FALSE, 0,
                              &line.vertices[0]);
    gl->glVertexAttribPointer(attributes_.normal, 2, GL_FLOAT, GL_FALSE, 0,
                              &line.normals[0]);
    gl->glVertexAttribPointer(attributes_.miter, 1, GL_FLOAT, GL_FALSE, 0,
                              &line.miters[0]);

    gl->glEnableVertexAttribArray(0);
    gl->glEnableVertexAttribArray(1);
    gl->glEnableVertexAttribArray(2);

    gl->glDrawElements(GL_TRIANGLES, line.indices.size(), GL_UNSIGNED_INT,
                       &line.indices[0]);

    gl->glDisableVertexAttribArray(2);
    gl->glDisableVertexAttribArray(1);
    gl->glDisableVertexAttribArray(0);
    program_->release();
}

FPGAViewWidget::FPGAViewWidget(QWidget *parent)
        : QOpenGLWidget(parent), moveX_(0), moveY_(0), zoom_(10.0f),
          lineShader_(this)
{
    ctx = qobject_cast<BaseMainWindow *>(getMainWindow())->getContext();
}

QMainWindow *FPGAViewWidget::getMainWindow()
{
    QWidgetList widgets = qApp->topLevelWidgets();
    for (QWidgetList::iterator i = widgets.begin(); i != widgets.end(); ++i)
        if ((*i)->objectName() == "BaseMainWindow")
            return (QMainWindow *)(*i);
    return NULL;
}

FPGAViewWidget::~FPGAViewWidget() {}

QSize FPGAViewWidget::minimumSizeHint() const { return QSize(640, 480); }

QSize FPGAViewWidget::sizeHint() const { return QSize(640, 480); }

void FPGAViewWidget::setXTranslation(float t_x)
{
    if (t_x == moveX_)
        return;

    moveX_ = t_x;
    update();
}

void FPGAViewWidget::setYTranslation(float t_y)
{
    if (t_y == moveY_)
        return;

    moveY_ = t_y;
    update();
}

void FPGAViewWidget::setZoom(float t_z)
{
    if (t_z == zoom_)
        return;
    zoom_ = t_z;

    if (zoom_ < 1.0f)
        zoom_ = 1.0f;
    if (zoom_ > 100.f)
        zoom_ = 100.0f;

    update();
}

void FPGAViewWidget::initializeGL()
{
    if (!lineShader_.compile()) {
        log_error("Could not compile shader.\n");
    }
    initializeOpenGLFunctions();
    glClearColor(1.0, 1.0, 1.0, 0.0);
}

void FPGAViewWidget::drawElement(LineShaderData &out, const GraphicElement &el)
{
    const float scale = 1.0, offset = 0.0;

    if (el.type == GraphicElement::G_BOX) {
        auto line = PolyLine(true);
        line.point(offset + scale * el.x1, offset + scale * el.y1);
        line.point(offset + scale * el.x2, offset + scale * el.y1);
        line.point(offset + scale * el.x2, offset + scale * el.y2);
        line.point(offset + scale * el.x1, offset + scale * el.y2);
        line.build(out);
    }

    if (el.type == GraphicElement::G_LINE) {
        PolyLine(offset + scale * el.x1, offset + scale * el.y1,
                 offset + scale * el.x2, offset + scale * el.y2)
                .build(out);
    }
}

void FPGAViewWidget::paintGL()
{
    auto gl = QOpenGLContext::currentContext()->functions();
    const qreal retinaScale = devicePixelRatio();
    gl->glViewport(0, 0, width() * retinaScale, height() * retinaScale);
    gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    const float aspect = float(width()) / float(height());

    QMatrix4x4 matrix;
    matrix.ortho(QRectF(-aspect / 2.0, -0.5, aspect, 1.0f));
    matrix.translate(moveX_, moveY_, -0.5);
    matrix.scale(zoom_ * 0.01f, zoom_ * 0.01f, 0);

    // Draw grid.
    auto grid = LineShaderData(0.01f, QColor("#DDD"));
    for (float i = -100.0f; i < 100.0f; i += 1.0f) {
        PolyLine(-100.0f, i, 100.0f, i).build(grid);
        PolyLine(i, -100.0f, i, 100.0f).build(grid);
    }
    lineShader_.draw(grid, matrix);

    // Draw Bels.
    auto bels = LineShaderData(0.02f, QColor("#b000ba"));
    for (auto bel : ctx->getBels()) {
        for (auto &el : ctx->getBelGraphics(bel))
            drawElement(bels, el);
    }
    lineShader_.draw(bels, matrix);

    // Draw Frame Graphics.
    auto frames = LineShaderData(0.02f, QColor("#0066ba"));
    for (auto &el : ctx->getFrameGraphics()) {
        drawElement(frames, el);
    }
    lineShader_.draw(frames, matrix);
}

void FPGAViewWidget::resizeGL(int width, int height) {}

void FPGAViewWidget::mousePressEvent(QMouseEvent *event)
{
    startDragX_ = moveX_;
    startDragY_ = moveY_;
    lastPos_ = event->pos();
}

void FPGAViewWidget::mouseMoveEvent(QMouseEvent *event)
{
    const int dx = event->x() - lastPos_.x();
    const int dy = event->y() - lastPos_.y();

    const qreal retinaScale = devicePixelRatio();
    float aspect = float(width()) / float(height());
    const float dx_scale = dx * (1 / (float)width() * retinaScale * aspect);
    const float dy_scale = dy * (1 / (float)height() * retinaScale);

    float xpos = dx_scale + startDragX_;
    float ypos = dy_scale + startDragY_;

    setXTranslation(xpos);
    setYTranslation(ypos);
}

void FPGAViewWidget::wheelEvent(QWheelEvent *event)
{
    QPoint degree = event->angleDelta() / 8;

    if (!degree.isNull()) {
        float steps = degree.y() / 15.0;
        setZoom(zoom_ + steps);
    }
}

NEXTPNR_NAMESPACE_END
