// 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.

#pragma once

#include <cstdint>
#include <memory>
#include <vector>
#include <celengine/glsupport.h>

#include "buffer.h"

namespace celestia::gl
{

/**
 * @brief VertexObject
 *
 * Wraps an Vertex Array Object if provided by OpenGL or implemnts its functions.
 */

class VertexObject
{
public:
    /**
     * Vertex data type.
     *
     * @see @ref addVertexBuffer()
     */
    enum class DataType : GLenum
    {
        Byte            = GL_BYTE,
        UnsignedByte    = GL_UNSIGNED_BYTE,
        Short           = GL_SHORT,
        UnsignedShort   = GL_UNSIGNED_SHORT,
        Int             = GL_INT,
        UnsignedInt     = GL_UNSIGNED_INT,
        Half            = GL_HALF_FLOAT,
        Float           = GL_FLOAT,
    };

    /**
     * Index types. Unsigned bytes are not supported as OpenGL ES and WebGL don't provide them.
     *
     * @see @ref setIndexBuffer() @ref isIndexed()
     */
    enum class IndexType : GLenum
    {
        UnsignedShort   = GL_UNSIGNED_SHORT,
        UnsignedInt     = GL_UNSIGNED_INT,
    };

    /**
     * Primitive type to draw.
     *
     * @see @ref draw() @ref draw(Primitive, int, int)
     */
    enum class Primitive : GLenum
    {
        Points          = GL_POINTS,
        Lines           = GL_LINES,
        LineStrip       = GL_LINE_STRIP,
        LineLoop        = GL_LINE_LOOP,
        Triangles       = GL_TRIANGLES,
        TriangleStrip   = GL_TRIANGLE_STRIP,
        TriangleFan     = GL_TRIANGLE_FAN,
    };

    /**
     * @brief Construct a new VertexObject object.
     *
     * Create a C++ object but don't create OpenGL objects.
     */
    VertexObject();

    /**
     * @brief Construct a new VertexObject object.
     *
     * Create C++ and OpenGL objects.
     *
     * @param primitive Primitive type to draw.
     *
     * @see @ref Primitive
     */
    explicit VertexObject(Primitive primitive);

    //! Copying is prohibited.
    VertexObject(const VertexObject&) = delete;

    //! Move constructor.
    VertexObject(VertexObject&&) noexcept;

    //! Destructor.
    ~VertexObject();

    //! Copying is prohibited.
    VertexObject& operator=(const VertexObject&) = delete;

    //! Move operator.
    VertexObject& operator=(VertexObject&&) noexcept;

    /**
     * @brief Return an OpenGL identificator.
     *
     * Return an OpenGL identificator of an underlying object if supported or 0 otherwise.
     */
    GLuint id() const;

    /**
     * @brief Render VertexObject.
     *
     * Render VertexObject using a primitive set by constructor or setPrimitive.
     *
     * @return Reference to self.
     *
     * @see @ref Primitive @ref VertexObject(Primitive)
     */
    VertexObject& draw();

    /**
     * @brief Render VertexObject.
     *
     * Render VertexObject using a default primitive.
     *
     * @param count Number of vertices to draw.
     * @param first First vertex to draw.
     * @return Reference to self.
     *
     * @see @ref Primitive @ref VertexObject(Primitive)
     */
    VertexObject& draw(GLsizei count, GLint first = 0);

    /**
     * @brief Render VertexObject.
     *
     * Render VertexObject using a primitive provided.
     *
     * @param primitive Primitive.
     * @param count Number of vertices to draw.
     * @param first First vertex to draw.
     * @return Reference to self.
     *
     * @see @ref Primitive @ref VertexObject(Primitive)
     */
    VertexObject& draw(Primitive primitive, GLsizei count, GLint first = 0);

    /**
     * @brief Render VertexObject with the given instance count
     *
     * Render VertexObject using a primitive set by constructor or setPrimitive.
     *
     * @param instanceCount Number of instances to draw.
     * @return Reference to self.
     *
     * @see @ref Primitive @ref VertexObject(Primitive)
     */
    VertexObject& drawInstances(GLsizei instanceCount);

    /**
     * @brief Render VertexObject with the given instance count.
     *
     * Render VertexObject using a default primitive.
     *
     * @param instanceCount Number of instances to draw.
     * @param count Number of vertices to draw.
     * @param first First vertex to draw.
     * @return Reference to self.
     *
     * @see @ref Primitive @ref VertexObject(Primitive)
     */
    VertexObject& drawInstances(GLsizei instanceCount, GLsizei count, GLint first = 0);

    /**
     * @brief Render VertexObject with the given instance count.
     *
     * Render VertexObject using a primitive provided.
     *
     * @param instanceCount Number of instances to draw.
     * @param primitive Primitive.
     * @param count Number of vertices to draw.
     * @param first First vertex to draw.
     * @return Reference to self.
     *
     * @see @ref Primitive @ref VertexObject(Primitive)
     */
    VertexObject& drawInstances(GLsizei instanceCount, Primitive primitive, GLsizei count, GLint first = 0);

    /**
     * @brief Set the primitive.
     *
     * Set the default primitive type to be used by draw().
     *
     * @param primitive New primitive type.
     * @return Reference to self.
     *
     * @see @ref Primitive @ref draw()
     */
    VertexObject& setPrimitive(Primitive primitive);

    /**
     * @brief Return VertexObject's assigned primitive type.
     *
     * @return Primitive
     */
    Primitive primitive() const;

    /**
     * @brief Set the count.
     *
     *  Set the default vertex/index count to be used by draw().
     *
     * @param count New vertices count.
     * @return Reference to self.
     *
     * @see @ref draw()
     */
    VertexObject& setCount(GLsizei count);

    /**
     * @brief Return vertex/index count.
     *
     * @return GLsizei
     */
    GLsizei count() const;

    /**
     * Whether the VertexObject is indexed.
     *
     * @see @ref setIndexBuffer(const Buffer &, std::ptrdiff_t, IndexType)
     */
    bool isIndexed() const;

    /**
     * @brief Define an array of generic vertex attribute data.
     *
     * Define an array of generic vertex attribute data. See documentation for glVertexAttribPointer
     * OpenGL method for more information.
     *
     * @param buffer Buffer with vertex data.
     * @param location Index of the generic vertex attribute.
     * @param elemSize Number of components per generic vertex attribute. Must be 1, 2, 3, or 4.
     * @param type Data type of each component.
     * @param normalized Whether fixed-point data values should be normalized (true) or
     * converted directly as fixed-point values (false).
     * @param stride Offset in bytes between consecutive generic vertex attributes. If stride is 0,
     * the generic vertex attributes are understood to be tightly packed in the array.
     * @param offset Offset of the first component of the first generic vertex attribute in the array.
     * @return Reference to self.
     *
     * @see @ref DataType
     */
    VertexObject& addVertexBuffer(const Buffer::SharedPtr&,
                                  int location,
                                  int elemSize,
                                  DataType type,
                                  bool normalized = false,
                                  int stride = 0,
                                  std::ptrdiff_t offset = 0);

    /**
     * @brief Define an array of generic instance attribute data.
     *
     * Define an array of generic vertex attribute data. See documentation for the glVertexAttribPointer
     * and glVertexAttribDivisor OpenGL methods for more information.
     *
     * @param buffer Buffer with vertex data.
     * @param location Index of the generic vertex attribute.
     * @param elemSize Number of components per generic vertex attribute. Must be 1, 2, 3, or 4.
     * @param type Data type of each component.
     * @param normalized Whether fixed-point data values should be normalized (true) or
     * converted directly as fixed-point values (false).
     * @param stride Offset in bytes between consecutive generic vertex attributes. If stride is 0,
     * the generic vertex attributes are understood to be tightly packed in the array.
     * @param offset Offset of the first component of the first generic vertex attribute in the array.
     * @return Reference to self.
     *
     * @see @ref DataType
     */
    VertexObject& addInstanceBuffer(const Buffer::SharedPtr&,
                                    int location,
                                    int elemSize,
                                    DataType type,
                                    bool normalized = false,
                                    int stride = 0,
                                    std::ptrdiff_t offset = 0);

    /**
     * @brief Add index buffer and become its owner.
     *
     * @param buffer Buffer with index data.
     * @param offset Unused.
     * @param type Index type.
     *
     * @see @ref IndexType
     */
    VertexObject& setIndexBuffer(const Buffer::SharedPtr&, std::ptrdiff_t offset, IndexType type);

private:
    //! Reset object to initial state
    void clear();
    //! Destroy underlying OpenGL resources
    void destroy() noexcept;
    //! Enable attribute arrays and bind index and vertex buffers
    void enableAttribArrays() const;
    //! Bind the current VertexObject
    void bind();
    //! Unbind the current VertexObject
    void unbind() const noexcept;

    struct BufferDesc;

    //! VAO arrays description
    std::vector<BufferDesc> m_bufferDesc;

    //! Index buffer
    Buffer::SharedPtr m_indexBuffer;

    //! Index type
    IndexType m_indexType{ IndexType::UnsignedShort };

    //! GL primitive to draw
    Primitive m_primitive{ Primitive::Triangles };

    //! GL primitive count
    GLsizei m_count{ 0 };

    //! VAO Id (OpenGL name)
    GLuint m_id{ 0 };

    /* additional variables to track the state */
    bool m_initialized{ false };
};

inline VertexObject::Primitive
VertexObject::primitive() const
{
    return m_primitive;
}

inline int
VertexObject::count() const
{
    return m_count;
}

inline bool
VertexObject::isIndexed() const
{
    return static_cast<bool>(m_indexBuffer);
}

inline GLuint
VertexObject::id() const
{
    return m_id;
}

} // namespace celestia::gl
