#pragma once

#include <array>
#include <cstdint>

#include "log.h"
#include "renderer_gl/opengl_capabilities.h"

namespace pp::legacy::gl_mesh {

inline void gen_opengl_buffers(std::uint32_t count, std::uint32_t* ids) noexcept
{
    glGenBuffers(static_cast<GLsizei>(count), reinterpret_cast<GLuint*>(ids));
}

inline void delete_opengl_buffers(std::uint32_t count, const std::uint32_t* ids) noexcept
{
    glDeleteBuffers(static_cast<GLsizei>(count), reinterpret_cast<const GLuint*>(ids));
}

inline void bind_opengl_buffer(std::uint32_t target, std::uint32_t buffer) noexcept
{
    glBindBuffer(static_cast<GLenum>(target), static_cast<GLuint>(buffer));
}

inline void set_opengl_buffer_data(
    std::uint32_t target,
    std::intptr_t byte_count,
    const void* data,
    std::uint32_t usage) noexcept
{
    glBufferData(
        static_cast<GLenum>(target),
        static_cast<GLsizeiptr>(byte_count),
        data,
        static_cast<GLenum>(usage));
}

inline void gen_opengl_vertex_arrays(std::uint32_t count, std::uint32_t* ids) noexcept
{
    glGenVertexArrays(static_cast<GLsizei>(count), reinterpret_cast<GLuint*>(ids));
}

inline void delete_opengl_vertex_arrays(std::uint32_t count, const std::uint32_t* ids) noexcept
{
    glDeleteVertexArrays(static_cast<GLsizei>(count), reinterpret_cast<const GLuint*>(ids));
}

inline void bind_opengl_vertex_array(std::uint32_t vertex_array) noexcept
{
    glBindVertexArray(static_cast<GLuint>(vertex_array));
}

inline void enable_opengl_vertex_attrib_array(std::uint32_t index) noexcept
{
    glEnableVertexAttribArray(static_cast<GLuint>(index));
}

inline void set_opengl_vertex_attrib_pointer(
    std::uint32_t index,
    std::int32_t component_count,
    std::uint32_t component_type,
    std::uint8_t normalized,
    std::int32_t stride,
    const void* offset) noexcept
{
    glVertexAttribPointer(
        static_cast<GLuint>(index),
        static_cast<GLint>(component_count),
        static_cast<GLenum>(component_type),
        static_cast<GLboolean>(normalized),
        static_cast<GLsizei>(stride),
        offset);
}

inline void draw_opengl_elements(
    std::uint32_t mode,
    std::int32_t count,
    std::uint32_t index_type,
    const void* index_offset) noexcept
{
    glDrawElements(
        static_cast<GLenum>(mode),
        static_cast<GLsizei>(count),
        static_cast<GLenum>(index_type),
        index_offset);
}

inline void draw_opengl_arrays(std::uint32_t mode, std::int32_t first, std::int32_t count) noexcept
{
    glDrawArrays(static_cast<GLenum>(mode), static_cast<GLint>(first), static_cast<GLsizei>(count));
}

inline pp::renderer::gl::OpenGlMeshCreateDispatch mesh_create_dispatch() noexcept
{
    return pp::renderer::gl::OpenGlMeshCreateDispatch {
        .gen_buffers = gen_opengl_buffers,
        .bind_buffer = bind_opengl_buffer,
        .buffer_data = set_opengl_buffer_data,
        .gen_vertex_arrays = gen_opengl_vertex_arrays,
        .bind_vertex_array = bind_opengl_vertex_array,
        .enable_vertex_attrib_array = enable_opengl_vertex_attrib_array,
        .vertex_attrib_pointer = set_opengl_vertex_attrib_pointer,
    };
}

inline pp::renderer::gl::OpenGlBufferUploadDispatch buffer_upload_dispatch() noexcept
{
    return pp::renderer::gl::OpenGlBufferUploadDispatch {
        .bind_buffer = bind_opengl_buffer,
        .buffer_data = set_opengl_buffer_data,
    };
}

inline pp::renderer::gl::OpenGlMeshDrawDispatch mesh_draw_dispatch() noexcept
{
    return pp::renderer::gl::OpenGlMeshDrawDispatch {
        .bind_vertex_array = bind_opengl_vertex_array,
        .draw_elements = draw_opengl_elements,
        .draw_arrays = draw_opengl_arrays,
    };
}

inline pp::renderer::gl::OpenGlMeshDeleteDispatch mesh_delete_dispatch() noexcept
{
    return pp::renderer::gl::OpenGlMeshDeleteDispatch {
        .delete_buffers = delete_opengl_buffers,
        .delete_vertex_arrays = delete_opengl_vertex_arrays,
    };
}

inline pp::renderer::gl::OpenGlMeshObjects create_mesh_objects(
    pp::renderer::gl::OpenGlMeshUpload upload,
    const char* context)
{
    const auto mesh = pp::renderer::gl::create_opengl_mesh_objects(upload, mesh_create_dispatch());
    if (!mesh.ok()) {
        LOG("%s failed because: %s", context, mesh.status().message);
        return {};
    }
    return mesh.value();
}

inline bool upload_buffer_data(pp::renderer::gl::OpenGlBufferUpload upload, const char* context)
{
    const auto status = pp::renderer::gl::upload_opengl_buffer_data(upload, buffer_upload_dispatch());
    if (!status.ok()) {
        LOG("%s failed because: %s", context, status.message);
        return false;
    }
    return true;
}

inline bool draw_mesh(pp::renderer::gl::OpenGlMeshDraw draw, const char* context)
{
    const auto status = pp::renderer::gl::draw_opengl_mesh(draw, mesh_draw_dispatch());
    if (!status.ok()) {
        LOG("%s failed because: %s", context, status.message);
        return false;
    }
    return true;
}

inline bool delete_mesh_objects(
    std::array<std::uint32_t, 2> buffers,
    std::array<std::uint32_t, 2> vertex_arrays,
    const char* context)
{
    const auto status = pp::renderer::gl::delete_opengl_mesh_objects(
        pp::renderer::gl::OpenGlMeshDelete {
            .buffers = buffers,
            .vertex_arrays = vertex_arrays,
        },
        mesh_delete_dispatch());
    if (!status.ok()) {
        LOG("%s failed because: %s", context, status.message);
        return false;
    }
    return true;
}

} // namespace pp::legacy::gl_mesh
