#include "pw/visual/renderer.hpp"

#include "pw/core/debug.hpp"
#include "pw/core/geometry.hpp"
#include "pw/core/matrix.hpp"
#include "pw/core/size.hpp"

#include "glad/glad.h"

#include <algorithm>

namespace pw {

struct renderer::impl {

    uint64_t _change_count{0};

    uint32_t _vao{0};
    uint32_t _ebo{0};
    std::vector<uint32_t> _vbos{};

    GLint _mesh_elements = {0};

    impl() = default;

    ~impl() { release(); }

    bool ready() const {
        return glIsVertexArray != nullptr && GL_TRUE == glIsVertexArray(_vao);
    }

    bool update(const geometry& m) {
        if (_change_count == m.change_count())
            return false;

        // reset if the renderer already in use
        if (ready())
            release();

        _mesh_elements = m.indices().size();

        //
        glGenVertexArrays(1, &_vao);
        glBindVertexArray(_vao);

        glGenBuffers(1, &_ebo);

        // TODO binding separate VBOs to the
        // vertexarray should be avoided

        // indices
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                     m.indices().size() * sizeof(uint32_t), m.indices().data(),
                     GL_STATIC_DRAW);

        _vbos.resize(_vbos.size() + 1);
        glGenBuffers(1, &_vbos.back());

        // vertices
        glBindBuffer(GL_ARRAY_BUFFER, _vbos.back());
        glVertexAttribPointer(_vbos.size() - 1, vector3::coefficients, GL_FLOAT,
                              GL_FALSE, 0, nullptr);
        glBufferData(GL_ARRAY_BUFFER, m.vertices().size() * sizeof(vector3),
                     m.vertices().data(), GL_STATIC_DRAW);
        glEnableVertexAttribArray(_vbos.size() - 1);

        debug::d() << "vertices at " << _vbos.size() - 1;

        if (!m.normals().empty()) {
            _vbos.resize(_vbos.size() + 1);
            glGenBuffers(1, &_vbos.back());

            // normals
            glBindBuffer(GL_ARRAY_BUFFER, _vbos.back());
            glVertexAttribPointer(_vbos.size() - 1, vector3::coefficients,
                                  GL_FLOAT, GL_FALSE, 0, nullptr);
            glBufferData(GL_ARRAY_BUFFER, m.normals().size() * sizeof(vector3),
                         m.normals().data(), GL_STATIC_DRAW);
            glEnableVertexAttribArray(_vbos.size() - 1);

            debug::d() << "normals at " << _vbos.size() - 1;
        }

        if (!m.texture_coordinates().empty()) {
            for (const auto& tc : m.texture_coordinates()) {
                _vbos.resize(_vbos.size() + 1);
                glGenBuffers(1, &_vbos.back());

                // texture coordinates
                glBindBuffer(GL_ARRAY_BUFFER, _vbos.back());
                glVertexAttribPointer(_vbos.size() - 1, vector2::coefficients,
                                      GL_FLOAT, GL_FALSE, 0, nullptr);
                glBufferData(GL_ARRAY_BUFFER, tc.size() * sizeof(vector2),
                             tc.data(), GL_STATIC_DRAW);
                glEnableVertexAttribArray(_vbos.size() - 1);

                debug::d() << "texture coordinates at " << _vbos.size() - 1;
            }
        }

        // stop binding
        glBindVertexArray(0);

        this->_change_count = m.change_count();

#if 1
        // get errors
        auto error = glGetError();
        if (error != GL_NO_ERROR) {
            debug::e() << __PRETTY_FUNCTION__ << " GL error: " << error;
        }

#endif

        return ready();
    }

    void release() {

        for (auto & vbo : _vbos)
            glDeleteBuffers(1, &vbo);
        glDeleteBuffers(1, &_ebo);

        glDeleteVertexArrays(1, &_vao);
    }

    void draw() {
        glBindVertexArray(_vao);
        glDrawElements(GL_TRIANGLES, _mesh_elements, GL_UNSIGNED_INT, nullptr);
        glBindVertexArray(0);

#if 0
        auto error = glGetError();
        if (error != GL_NO_ERROR)
        {
            debug::e() << __PRETTY_FUNCTION__ << " OpenGL error: " << error;
        }
#endif
    }

    //	GLint get_mode(vertex_array::)
};

//
// Outer wrapper
//

renderer::renderer() : _impl(std::make_unique<renderer::impl>()) {}

renderer::renderer(const geometry& m) {
    renderer();
    // directly update
    _impl->update(m);
}

renderer::~renderer() = default;

bool renderer::ready() const { return _impl->ready(); }

bool renderer::update(const geometry& m) { return _impl->update(m); }

void renderer::release() { _impl->release(); }

void renderer::draw() { _impl->draw(); }

uint64_t renderer::change_count() const { return _impl->_change_count; }

void renderer::set_change_count(uint64_t n) { _impl->_change_count = n; }

} // namespace pw
