/*
 * Copyright 2023 Hartmut Seichter
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */
#include "paradiso/renderer.hpp"
#include "paradiso/bitmap.hpp"
#include "paradiso/shader.hpp"
#include "paradiso/sprite.hpp"

#include "glad/glad.h"

#include <algorithm>
#include <array>
#include <iomanip>
#include <iostream>
#include <vector>
#include <limits>

#if defined(_WIN32) 
#undef max
#endif

namespace paradiso {

struct Renderer::impl {

    uint64_t change_count{std::numeric_limits<uint64_t>::max()};
    uint64_t change_count_texture{std::numeric_limits<uint64_t>::max()};

    // below corresponds to GL state
    uint32_t vertex_array_obj{};
    uint32_t element_buffer_obj{};
    std::vector<uint32_t> vertex_buffer_ob{};
    uint32_t texture_id{};

    ~impl() { release(); }

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

    bool build(const Sprite& sprite) {
        // reset if the Renderer already in use
        if (ready())
            release();
        //
        glGenVertexArrays(1, &vertex_array_obj);
        glBindVertexArray(vertex_array_obj);

        // element buffer
        glGenBuffers(1, &element_buffer_obj);

        // indices -> elements
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer_obj);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                     sprite.indices.size() * sizeof(uint32_t),
                     sprite.indices.data(), GL_STATIC_DRAW);

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

        // vertices
        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_ob.back());
        glVertexAttribPointer(vertex_buffer_ob.size() - 1, 3, GL_FLOAT,
                              GL_FALSE, 0, nullptr);
        glBufferData(GL_ARRAY_BUFFER,
                     sprite.vertices.size() * sizeof(float) * 3,
                     sprite.vertices.data(), GL_STATIC_DRAW);
        glEnableVertexAttribArray(vertex_buffer_ob.size() - 1);

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

        // normals
        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_ob.back());
        glVertexAttribPointer(vertex_buffer_ob.size() - 1, 3, GL_FLOAT,
                              GL_FALSE, 0, nullptr);
        glBufferData(GL_ARRAY_BUFFER, sprite.normals.size() * sizeof(float) * 3,
                     sprite.normals.data(), GL_STATIC_DRAW);
        glEnableVertexAttribArray(vertex_buffer_ob.size() - 1);

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

        // texture coordinates
        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_ob.back());
        glVertexAttribPointer(vertex_buffer_ob.size() - 1, 2, GL_FLOAT,
                              GL_FALSE, 0, nullptr);
        glBufferData(GL_ARRAY_BUFFER,
                     sprite.texture_coordinates.size() * sizeof(float) * 2,
                     sprite.texture_coordinates.data(), GL_STATIC_DRAW);
        glEnableVertexAttribArray(vertex_buffer_ob.size() - 1);

        // stop binding
        glBindVertexArray(0);

        change_count = sprite.change_count;

        return ready();
    }

    void texture_bind() {
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture_id);
    }

    void texture_unbind() { glBindTexture(GL_TEXTURE_2D, 0); }

    void texture_update(const Bitmap& image) {
        if (GL_FALSE == glIsTexture(texture_id)) {
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

            glGenTextures(1, &texture_id);

            glActiveTexture(GL_TEXTURE0 + 0);
            glBindTexture(GL_TEXTURE_2D, texture_id);

            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

            // setup new texture
            glTexImage2D(GL_TEXTURE_2D,     // target
                         0,                 // level
                         GL_RGBA,           // internal format
                         image.size.width,  // width
                         image.size.height, // height
                         0,                 // border
                         GL_BGRA,           // format
                         GL_UNSIGNED_BYTE,  // type
                         image.data.data()  // pointer to data
            );

            // generate MipMaps
            glGenerateMipmap(GL_TEXTURE_2D);
            glGenerateTextureMipmap(texture_id);

        } else if (image.change_count != change_count_texture) {

            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, texture_id);

            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

            glTexSubImage2D(GL_TEXTURE_2D,
                            0,                  // level
                            0,                  // x-offset
                            0,                  // y-offset
                            image.size.width,   // width
                            image.size.height,  // height
                            GL_BGRA,            // format
                            GL_UNSIGNED_BYTE,   // type
                            image.data.data()); // pointer

            change_count_texture = image.change_count;
        }

        glBindTexture(GL_TEXTURE_2D, 0);
    }

    void texture_release() { glDeleteTextures(1, &texture_id); }

    void release() {

        for (auto vbo : vertex_buffer_ob)
            glDeleteBuffers(1, &vbo);
        glDeleteBuffers(1, &element_buffer_obj);

        glDeleteVertexArrays(1, &vertex_array_obj);

        texture_release();
    }

    void just_draw(const Sprite& sprite) {
        glBindVertexArray(vertex_array_obj);
        glDrawElements(GL_TRIANGLES, sprite.indices.size(), GL_UNSIGNED_INT,
                       nullptr);
        glBindVertexArray(0);
    }

    static void fetch_errors(std::string_view tag) {
        std::cout << tag << '\n';
        GLenum e{};
        while ((e = glGetError()) != GL_NO_ERROR) {
            std::cerr << '\t' << " OpenGL error 0x" << std::hex << e << '\n';
        }
    }
};

//
// Outer wrapper
//

Renderer::Renderer() : impl_(std::make_unique<Renderer::impl>()) {}

Renderer::~Renderer() {}

bool Renderer::ready() const { return impl_->ready(); }

bool Renderer::draw(const Sprite& sprite, const Shader& shader) {

    // if interna are not ready or sprite has been altered - rebuild
    if (!impl_->ready() || sprite.change_count != impl_->change_count) {
        impl_->build(sprite);
        impl_->texture_update(sprite.bitmap);
    }

    // render the sprite with the shader
    if (impl_->ready() && sprite.change_count == impl_->change_count) {

        shader.use();

        shader.set_uniform("tex_color", 0u); // texture unit 0

        impl_->texture_bind();

        impl_->just_draw(sprite);

        // Renderer::impl::fetch_errors(__PRETTY_FUNCTION__);

        impl_->texture_unbind(); // overkill but some cards are finicky

        return true;
    }

    return false;
}

} // namespace paradiso
