#include "marker.hpp"
#include "canvas_gl.hpp"
#include "gl_util.hpp"
#include <algorithm>
#include <glm/gtc/type_ptr.hpp>

namespace horizon {

static GLuint create_vao(GLuint program, GLuint &vbo_out)
{
    GL_CHECK_ERROR
    GLuint position_index = glGetAttribLocation(program, "position");
    GLuint color_index = glGetAttribLocation(program, "color");
    GLuint flags_index = glGetAttribLocation(program, "flags");

    GLuint vao, buffer;

    /* we need to create a VAO to store the other buffers */
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    /* this is the VBO that holds the vertex data */
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    // data is buffered lateron

    GLfloat vertices[] = {//   Position
                          0, 0, 1, 0, 1, 0};
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    /* enable and set the position attribute */
    glEnableVertexAttribArray(position_index);
    glVertexAttribPointer(position_index, 2, GL_FLOAT, GL_FALSE, sizeof(Marker), (void *)offsetof(Marker, x));

    glEnableVertexAttribArray(color_index);
    glVertexAttribPointer(color_index, 3, GL_FLOAT, GL_FALSE, sizeof(Marker), (void *)offsetof(Marker, r));

    glEnableVertexAttribArray(flags_index);
    glVertexAttribIPointer(flags_index, 1, GL_UNSIGNED_BYTE, sizeof(Marker), (void *)offsetof(Marker, flags));

    /* enable and set the color attribute */
    /* reset the state; we will re-enable the VAO when needed */
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // glDeleteBuffers (1, &buffer);
    vbo_out = buffer;

    return vao;
}

Markers::Markers(CanvasGL &c) : ca(c)
{
    for (auto &it : domains) {
        it.visible = false;
    }
}

void Markers::set_domain_visible(MarkerDomain dom, bool vis)
{
    domains.at(static_cast<int>(dom)).visible = vis;
    ca.update_markers();
    ca.set_has_tooltip(std::any_of(domains.begin(), domains.end(), [](auto &x) { return x.visible; }));
}

std::deque<MarkerRef> &Markers::get_domain(MarkerDomain dom)
{
    return domains.at(static_cast<int>(dom)).markers;
}

void Markers::update()
{
    ca.update_markers();
    ca.request_push(CanvasGL::PF_MARKER);
}

void Markers::set_sheet_filter(const UUIDVec &uu)
{
    sheet_filter = uu;
    update();
}

bool Markers::marker_is_visible(const MarkerRef &mkr) const
{
    return mkr.visible && (mkr.sheet == sheet_filter || mkr.sheet.size() == 0);
}

bool Markers::hit_test_marker_ref(const MarkerRef &ref, const Coordf &p) const
{
    const auto size_px = ref.size == MarkerRef::Size::DEFAULT ? 40 : 20; // see geometry shader
    const auto size_canvas = size_px / ca.scale;
    const auto a = ref.position;
    const auto b = ref.position + Coordf(size_canvas, -size_canvas);
    return p.x >= a.x && p.x <= b.x && p.y >= b.y && p.y <= a.y;
}

std::vector<const MarkerRef *> Markers::get_markers_at_screen_pos(int x, int y) const
{
    const auto canvas_pos = ca.screen2canvas(Coordf(x, y));
    std::vector<const MarkerRef *> r;
    for (const auto &dom : domains) {
        if (dom.visible) {
            for (const auto &mrk : dom.markers) {
                if (marker_is_visible(mrk) && hit_test_marker_ref(mrk, canvas_pos)) {
                    r.push_back(&mrk);
                }
            }
        }
    }
    return r;
}


MarkerRenderer::MarkerRenderer(const CanvasGL &c, Markers &ma) : ca(c), markers_ref(ma)
{
}

void MarkerRenderer::realize()
{
    program = gl_create_program_from_resource("/org/horizon-eda/horizon/canvas/shaders/marker-vertex.glsl",
                                              "/org/horizon-eda/horizon/canvas/shaders/marker-fragment.glsl",
                                              "/org/horizon-eda/horizon/canvas/shaders/"
                                              "marker-geometry.glsl");
    vao = create_vao(program, vbo);
    GET_LOC(this, screenmat);
    GET_LOC(this, viewmat);
    GET_LOC(this, alpha);
    GET_LOC(this, border_color);
}

void MarkerRenderer::render()
{
    glUseProgram(program);
    glBindVertexArray(vao);
    glUniformMatrix3fv(screenmat_loc, 1, GL_FALSE, glm::value_ptr(ca.screenmat));
    glUniformMatrix3fv(viewmat_loc, 1, GL_FALSE, glm::value_ptr(ca.viewmat));
    glUniform1f(alpha_loc, ca.property_layer_opacity() / 100);
    gl_color_to_uniform_3f(border_color_loc, ca.get_color(ColorP::MARKER_BORDER));
    glDrawArrays(GL_POINTS, 0, markers.size());

    glBindVertexArray(0);
    glUseProgram(0);
}

void MarkerRenderer::update()
{
    markers.clear();
    for (const auto &dom : markers_ref.domains) {
        if (dom.visible) {
            for (const auto &mkr : dom.markers) {
                if (ca.markers.marker_is_visible(mkr)) {
                    uint8_t flags = 0;
                    if (mkr.size == MarkerRef::Size::SMALL)
                        flags = 1;
                    markers.emplace_back(mkr.position, mkr.color, flags);
                }
            }
        }
    }
}

void MarkerRenderer::push()
{
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Marker) * markers.size(), markers.data(), GL_STREAM_DRAW);
}
} // namespace horizon
