// vertexobject.h
//
// Copyright (C) 2023-present, Celestia Development Team.
//
// VAO wrapper.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

#include "vertexobject.h"

#include <celutil/flag.h>
#include "binder.h"
#include "buffer.h"

#define PTR(p) (reinterpret_cast<const void*>(static_cast<std::intptr_t>(p)))

namespace celestia::gl
{

enum class BufferFlags : std::uint8_t
{
    None = 0,
    Normalized = 1,
    Instance = 2,
};

ENUM_CLASS_BITWISE_OPS(BufferFlags)

struct VertexObject::BufferDesc
{
    BufferDesc(GLsizeiptr               offset,
               const Buffer::SharedPtr &buffer,
               std::uint16_t            type,
               std::int16_t             location,
               std::uint8_t             elemSize,
               std::uint8_t             stride,
               BufferFlags              flags) :
        buffer(buffer),
        offset(offset),
        type(type),
        location(location),
        elemSize(elemSize),
        stride(stride),
        flags(flags)
    {
    }

    Buffer::SharedPtr buffer;
    GLsizeiptr        offset;
    std::uint16_t     type;       // all constants < 0xFFFF
    std::int16_t      location;
    std::uint8_t      elemSize;   // 1, 2, 3, 4
    std::uint8_t      stride;     // WebGL allows only 255 bytes max
    BufferFlags       flags;
};

VertexObject::VertexObject() = default;

VertexObject::VertexObject(VertexObject::Primitive primitive) :
    m_primitive(primitive)
{
    glGenVertexArrays(1, &m_id);
}

VertexObject::VertexObject(VertexObject &&other) noexcept :
    m_bufferDesc(std::move(other.m_bufferDesc)),
    m_indexBuffer(std::move(other.m_indexBuffer)),
    m_indexType(other.m_indexType),
    m_primitive(other.m_primitive),
    m_count(other.m_count),
    m_id(other.m_id),
    m_initialized(other.m_initialized)
{
    other.clear();
}

VertexObject &VertexObject::operator=(VertexObject &&other) noexcept
{
    destroy();

    m_bufferDesc = std::move(other.m_bufferDesc);
    m_indexBuffer = std::move(other.m_indexBuffer);
    m_indexType = other.m_indexType;
    m_primitive = other.m_primitive;
    m_count = other.m_count;
    m_id = other.m_id;
    m_initialized = other.m_initialized;

    other.clear();

    return *this;
}

VertexObject::~VertexObject()
{
    destroy();
}

VertexObject&
VertexObject::setCount(GLsizei count)
{
    m_count = count;
    return *this;
}

VertexObject&
VertexObject::setPrimitive(VertexObject::Primitive primitive)
{
    m_primitive = primitive;
    return *this;
}

void
VertexObject::destroy() noexcept
{
    unbind();
    if (m_id != 0)
        glDeleteVertexArrays(1, &m_id);
    m_id = 0;
}

void
VertexObject::clear()
{
    m_primitive = Primitive::Triangles;
    m_count = 0;
    m_id = 0;
    m_indexType = IndexType::UnsignedShort;
    m_initialized = false;
}

VertexObject&
VertexObject::addVertexBuffer(const Buffer::SharedPtr &buffer,
                              int location,
                              int elemSize,
                              VertexObject::DataType type,
                              bool normalized,
                              int stride,
                              std::ptrdiff_t offset)
{
    if (buffer->targetHint() != Buffer::TargetHint::Array)
        return *this;

    m_bufferDesc.emplace_back(offset,
                              buffer,
                              static_cast<std::uint16_t>(type),
                              static_cast<std::uint16_t>(location),
                              static_cast<std::uint8_t>(elemSize),
                              static_cast<std::uint8_t>(stride),
                              normalized ? BufferFlags::Normalized : BufferFlags::None);

    return *this;
}

VertexObject&
VertexObject::addInstanceBuffer(const Buffer::SharedPtr &buffer,
                                int location,
                                int elemSize,
                                VertexObject::DataType type,
                                bool normalized,
                                int stride,
                                std::ptrdiff_t offset)
{
    if (buffer->targetHint() != Buffer::TargetHint::Array)
        return *this;

    auto flags = BufferFlags::Instance;
    if (normalized)
        flags |= BufferFlags::Normalized;

    m_bufferDesc.emplace_back(offset,
                              buffer,
                              static_cast<std::uint16_t>(type),
                              static_cast<std::uint16_t>(location),
                              static_cast<std::uint8_t>(elemSize),
                              static_cast<std::uint8_t>(stride),
                              flags);

    return *this;
}

VertexObject&
VertexObject::draw()
{
    return draw(m_primitive, m_count, 0);
}

VertexObject&
VertexObject::draw(GLsizei count, GLint first)
{
    return draw(m_primitive, count, first);
}

VertexObject&
VertexObject::draw(VertexObject::Primitive primitive, GLsizei count, GLint first)
{
    if (count == 0)
        return *this;

    bind();

    if (isIndexed())
    {
        auto offset = PTR(first * (m_indexType == IndexType::UnsignedShort ? sizeof(GLushort) : sizeof(GLuint)));
        glDrawElements(GLenum(primitive), count, GLenum(m_indexType), offset);
    }
    else
    {
        glDrawArrays(GLenum(primitive), first, count);
    }

    unbind();

    return *this;
}

VertexObject&
VertexObject::drawInstances(GLsizei instanceCount)
{
    return drawInstances(instanceCount, m_primitive, m_count, 0);
}

VertexObject&
VertexObject::drawInstances(GLsizei instanceCount, GLsizei count, GLint first)
{
    return drawInstances(instanceCount, m_primitive, count, first);
}

VertexObject&
VertexObject::drawInstances(GLsizei instanceCount, VertexObject::Primitive primitive, GLsizei count, GLint first)
{
    if (count == 0)
        return *this;

    bind();

    if (isIndexed())
    {
        auto offset = PTR(first * (m_indexType == IndexType::UnsignedShort ? sizeof(GLushort) : sizeof(GLuint)));
        glDrawElementsInstanced(GLenum(primitive), count, GLenum(m_indexType), offset, instanceCount);
    }
    else
    {
        glDrawArraysInstanced(GLenum(primitive), first, count, instanceCount);
    }

    unbind();

    return *this;
}

VertexObject&
VertexObject::setIndexBuffer(const Buffer::SharedPtr &buffer, std::ptrdiff_t /*offset*/, VertexObject::IndexType type)
{
    if (buffer->targetHint() != Buffer::TargetHint::ElementArray)
        return *this;

    m_indexBuffer = buffer;
    m_indexType = type;

    return *this;
}

void
VertexObject::enableAttribArrays() const
{
    auto &binder = Binder::get();

    for (const auto &p : m_bufferDesc)
    {
        binder.bind(*p.buffer);

        glEnableVertexAttribArray(p.location);

        GLboolean normalized = util::is_set(p.flags, BufferFlags::Normalized) ? GL_TRUE : GL_FALSE;
        glVertexAttribPointer(p.location, p.elemSize, p.type, normalized, p.stride, PTR(p.offset));
        if (util::is_set(p.flags, BufferFlags::Instance))
            glVertexAttribDivisor(p.location, 1);
    }

    if (isIndexed())
        binder.bind(*m_indexBuffer);
}

void
VertexObject::bind()
{
    Binder::get().bind(*this);

    if (!m_initialized)
    {
        m_initialized = true;
        enableAttribArrays();
    }
}

void
VertexObject::unbind() const noexcept
{
    Binder::get().unbind(*this);
}

} // namespace celestia::gl
