/*
 *  nextpnr -- Next Generation Place and Route
 *
 *  Copyright (C) 2018  Serge Bazanski <q3k@q3k.org>
 *
 *  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.
 *
 */

#ifndef LINESHADER_H
#define LINESHADER_H

#include <QOpenGLBuffer>
#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLWidget>
#include <array>

#include "log.h"
#include "nextpnr.h"

NEXTPNR_NAMESPACE_BEGIN

// Vertex2DPOD is a structure of X, Y coordinates that can be passed to OpenGL
// directly.
NPNR_PACKED_STRUCT(struct Vertex2DPOD {
    GLfloat x;
    GLfloat y;

    Vertex2DPOD(GLfloat X, GLfloat Y) : x(X), y(Y) {}
});

// LineShaderData is a built set of vertices that can be rendered by the
// LineShader.
// Each LineShaderData can have its' own color and thickness.
struct LineShaderData
{
    std::vector<Vertex2DPOD> vertices;
    std::vector<Vertex2DPOD> normals;
    std::vector<GLfloat> miters;
    std::vector<GLuint> indices;

    int last_render = 0;

    void clear(void)
    {
        vertices.clear();
        normals.clear();
        miters.clear();
        indices.clear();
    }
};

// PolyLine is a set of segments defined by points, that can be built to a
// ShaderLine for GPU rendering.
class PolyLine
{
  private:
    std::vector<QVector2D> points_;
    bool closed_;

    void buildPoint(LineShaderData *building, const QVector2D *prev, const QVector2D *cur, const QVector2D *next) const;

  public:
    // Create an empty PolyLine.
    PolyLine(bool closed = false) : closed_(closed) {}

    // Create a non-closed polyline consisting of one segment.
    PolyLine(float x0, float y0, float x1, float y1) : closed_(false)
    {
        point(x0, y0);
        point(x1, y1);
    }

    // Add a point to the PolyLine.
    void point(float x, float y) { points_.push_back(QVector2D(x, y)); }

    // Built PolyLine to shader data.
    void build(LineShaderData &target) const;

    // Set whether line is closed (ie. a loop).
    void setClosed(bool closed) { closed_ = closed; }
};

// LineShader is an OpenGL shader program that renders LineShaderData on the
// GPU.
// The LineShader expects two vertices per line point. It will push those
// vertices along the given normal * miter. This is used to 'stretch' the line
// to be as wide as the given thickness. The normal and miter are calculated
// by the PolyLine build method in order to construct a constant thickness line
// with miter edge joints.
//
//        +------+------+
//
//               |
//        PolyLine.build()
//               |
//               V
//
//        ^      ^      ^
//        |      |      |    <--- normal vectors (x2, pointing in the same
//       +/+----+/+----+/+        direction)
//
//               |
//         vertex shader
//               |
//               V
//
//        +------+------+ ^ by normal * miter * thickness/2
//        |      |      |
//        +------+------+ V by normal * miter * thickness/2
//
//                         (miter is flipped for every second vertex generated)
class LineShader
{
  private:
    QObject *parent_;
    QOpenGLShaderProgram *program_;

    // GL attribute locations.
    struct
    {
        // original position of line vertex
        GLuint position;
        // normal by which vertex should be translated
        GLuint normal;
        // scalar defining:
        // - how stretched the normal vector should be to
        //   compensate for bends
        // - which way the normal should be applied (+1 for one vertex, -1
        //   for the other)
        GLuint miter;
    } attributes_;

    // GL buffers
    struct Buffers
    {
        QOpenGLBuffer position;
        QOpenGLBuffer normal;
        QOpenGLBuffer miter;
        QOpenGLBuffer index;
        QOpenGLVertexArrayObject vao;
        int indices = 0;

        int last_vbo_update = 0;
    };
    std::array<Buffers, GraphicElement::STYLE_MAX> buffers_;

    // GL uniform locations.
    struct
    {
        // combines m/v/p matrix to apply
        GLuint projection;
        // desired thickness of line
        GLuint thickness;
        // color of line
        GLuint color;
    } uniforms_;

  public:
    LineShader(QObject *parent) : parent_(parent), program_(nullptr) {}

    static constexpr const char *vertexShaderSource_ =
            "#version 150\n"
            "in highp vec2  position;\n"
            "in highp vec2  normal;\n"
            "in highp float miter;\n"
            "uniform   highp float thickness;\n"
            "uniform   highp mat4  projection;\n"
            "void main() {\n"
            "   vec2 p = position.xy + vec2(normal * thickness/2.0 / miter);\n"
            "   gl_Position = projection * vec4(p, 0.0, 1.0);\n"
            "}\n";

    static constexpr const char *fragmentShaderSource_ = "#version 150\n"
                                                         "uniform   lowp  vec4  color;\n"
                                                         "out vec4 Out_Color;\n"
                                                         "void main() {\n"
                                                         "   Out_Color = color;\n"
                                                         "}\n";

    // Must be called on initialization.
    bool compile(void);

    void update_vbos(enum GraphicElement::style_t style, const LineShaderData &line);

    // Render a LineShaderData with a given M/V/P transformation.
    void draw(enum GraphicElement::style_t style, const QColor &color, float thickness, const QMatrix4x4 &projection);
};

NEXTPNR_NAMESPACE_END

#endif
