/******************************************************************************
*	File		-	vertex_buffer.cpp
*	Author		-	Joey Pollack
*	Date		-	2022/07/13 (y/m/d)
*	Mod Date	-	2022/07/13 (y/m/d)
*	Description	-	OpenGL vertex buffer object
******************************************************************************/

#include "vertex_buffer.h"
#include <glad/gl.h>

#include <utils/logger.h>

namespace lunarium
{    
    /////////////////////////////////////////////////////////////////////
    //				BUFFER LAYOUT
    /////////////////////////////////////////////////////////////////////
    void BufferLayout::PushVertexAttribute(VertexAttribute va)
    {
        mLayout.push_back(va);
    }

    void BufferLayout::ClearLayout()
    {
        mLayout.clear();
    }

    std::vector<VertexAttribute>& BufferLayout::GetLayout()
    {
        return mLayout;
    }


    int BufferLayout::GetNumAttributes() const
    {
        return mLayout.size();
    }

    int BufferLayout::GetLayoutSizeInBytes() const
    {
        int size = 0;
        for (auto va = mLayout.begin(); va != mLayout.end(); va++)
        {
            size += GetAttributeSizeInBytes(*va);
        }

        return size;
    }

    int BufferLayout::GetAttributeSizeInBytes(VertexAttribute va) const
    {
        int type_sizes[] = { 4, 8, 4, 8 };
        return type_sizes[va.Type] * va.NumValues;
    }

    void BufferLayout::ImplementLayout()
    {
        int type_sizes[] = { 4, 8, 4, 8 };
        GLenum gl_types[] =  { GL_INT, GL_INT, GL_FLOAT, GL_FLOAT };
        int stride = 0;
        std::vector<int> offsets;

        for (int i = 0; i < mLayout.size(); i++)
        {
            offsets.push_back(stride);
            stride += type_sizes[mLayout[i].Type] * mLayout[i].NumValues;
        }

        for (int i = 0; i < mLayout.size(); i++)
        {
            glVertexAttribPointer(i, mLayout[i].NumValues, gl_types[mLayout[i].Type], GL_FALSE, stride, (GLvoid*)offsets[i]);
            glEnableVertexAttribArray(i);
        }
    }


    /////////////////////////////////////////////////////////////////////
    //				VERTEX BUFFER
    /////////////////////////////////////////////////////////////////////
    VertexBuffer* VertexBuffer::Create(BufferLayout& VertexLayout, u32 num_vertices, const void* vertices, u32 num_indices, u32* indices)
    {
        return new VertexBuffer(VertexLayout, num_vertices, vertices, num_indices, indices);
    }

    void VertexBuffer::Destroy(VertexBuffer** ppBuf)
    {
        (*ppBuf)->Free();
        delete *ppBuf;
        *ppBuf = nullptr;
    }

    VertexBuffer::VertexBuffer(BufferLayout& VertexLayout, u32 num_vertices, const void* vertices, u32 num_indices, u32* indices)
        : mLayout(VertexLayout), mSize(0), mIndex(0), mEBO(0), mEBOSize(num_indices * sizeof(u32)), mEBOIndex(0), mNumIndices(0), mNumVertices(0),
          mIsStatic(false), mCPUBuffer(nullptr), mCPUIdxBuffer(nullptr), mPrimType(LayoutType::TRIANGLE)
    {
        mIsStatic = vertices != nullptr;

        // Calculate buffer size
        mSize = VertexLayout.GetLayoutSizeInBytes() * num_vertices;
        mCPUBuffer = new u8[mSize];

        glGenVertexArrays(1, &mVAO);
        glGenBuffers(1, &mVBO);

        if (mEBOSize > 0)
        {
            glGenBuffers(1, &mEBO);
            mCPUIdxBuffer = new u32[num_indices];
        }

        
        Bind();
		BindVBO();
        if (vertices)
        {
            glBufferData(GL_ARRAY_BUFFER, mSize, vertices, GL_STATIC_DRAW);
            mNumVertices = num_vertices;
        }
        else
        {
            glBufferData(GL_ARRAY_BUFFER, mSize, nullptr, GL_DYNAMIC_DRAW);
        }

        int err = glGetError();
        if (err > 0)
        {
            Logger::Error(LogCategory::GRAPHICS, "Failed to set vertex buffer data, error code: %d", err);
        }

        if (mEBOSize > 0)
        {
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEBO);
            u32 mode = indices ? GL_STATIC_DRAW : GL_DYNAMIC_DRAW;
            mEBOIsStatic = indices ? true : false;
            mNumIndices = indices ? num_indices : 0;
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, mEBOSize, indices, mode);
        }

        err = glGetError();
        if (err > 0)
        {
            Logger::Error(LogCategory::GRAPHICS, "Failed to create EBO, error code: %d", err);
        }

        mLayout.ImplementLayout();

        err = glGetError();
        if (err > 0)
        {
            Logger::Error(LogCategory::GRAPHICS, "Failed to implement vertex layout: error code: %d", err);
        }
        Unbind();
		UnbindVBO();
    }

    
    void VertexBuffer::Clear()
    {
        if (mIsStatic)
        {
            Logger::Warn(LogCategory::GRAPHICS, "Attemping to clear a static VertexBuffer");
        }

        mIndex = 0;
        mNumVertices = 0;
        

        if (mNumIndices > 0 && !mEBOIsStatic)
        {
            mNumIndices = 0;
            mEBOIndex = 0;

        }
    }

    
    void VertexBuffer::SetType(LayoutType primitive_type)
    {
        mPrimType = primitive_type;
    }

    void VertexBuffer::Bind()
    {
        glBindVertexArray(mVAO);
    }

    void VertexBuffer::Unbind()
    {
        glBindVertexArray(0);
    }

    
    void VertexBuffer::BindVBO()
    {
        glBindBuffer(GL_ARRAY_BUFFER, mVBO);
    }

    void VertexBuffer::UnbindVBO()
    {
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    
    bool VertexBuffer::WillFit(u32 num_vertices)
    {
        int data_size = mLayout.GetLayoutSizeInBytes() * num_vertices;
        return mIndex + data_size <= mSize;
    }

    
    bool VertexBuffer::IdxWillFit(u32 num_indices)
    {
        int data_size = sizeof(u32) * num_indices;
        return (mEBOIndex + data_size <= mEBOSize);
    }

    u32 VertexBuffer::GetNumVerts() const
    {
        return mNumVertices;
    }

    bool VertexBuffer::PushVertices(const void* vertices, u32 num_verts)
    {
        if (mIsStatic)
        {
            Logger::Warn(LogCategory::GRAPHICS, "Cannot push vertices into a static buffer!");
            return false;
        }

        int data_size = mLayout.GetLayoutSizeInBytes() * num_verts;

        // TODO: BE CAREFUL! mSize is the size in bytes, not number of vertices.
        if (mIndex + data_size > mSize)
        {
            // Vertices do not fit into the buffer
           // Logger::Warn(LogCategory::GRAPHICS, "Cannot push vertices into buffer - not enough space left! Space left: %d, data size: %d", mSize - mIndex, data_size);
            return false;
        }

        // Add verts to the cpu buffer
        memcpy(mCPUBuffer + mIndex, vertices, data_size);

        mNumVertices += num_verts;
        mIndex += data_size;
        return true;
    }

    
    bool VertexBuffer::PushIndices(const u32* indices, u32 num_indices)
    {
        if (mEBOIsStatic)
        {
            Logger::Warn(LogCategory::GRAPHICS, "Cannot push indices into a static buffer!");
            return false;
        }

        int data_size = sizeof(u32) * num_indices;
        if (mEBOIndex + data_size > mEBOSize)
        {
            // Indices do not fit into the buffer
            Logger::Warn(LogCategory::GRAPHICS, "Cannot push indices into buffer - not enough space left! Space left: %d, data size: %d", mEBOSize - mEBOIndex, data_size);
            return false;
        }

        memcpy(((u8*)mCPUIdxBuffer) + mEBOIndex, indices, data_size);

        // Bind();
		// glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, mEBOIndex, data_size, indices);
        // Unbind();
        // int error = glGetError();
        // if (error > 0)
        // {
        //     Logger::Error(LogCategory::GRAPHICS, "OGL ERROR!");
        // }

        mNumIndices += num_indices;
        mEBOIndex += data_size;
        return true;
    }

    
    void VertexBuffer::DrawArray(u32 num_elements)
    {

        if (!mIsStatic)
        {
            BindVBO();
		    glBufferSubData(GL_ARRAY_BUFFER, 0, mIndex, mCPUBuffer);    // mIndex also happens to be the size in bytes of the cpu buffer
            UnbindVBO();
        }

        GLenum primitive = (mPrimType == LayoutType::TRIANGLE) ? GL_TRIANGLES : GL_LINES;

        Bind();
        if (mNumIndices > 0)
        {
            if (!mEBOIsStatic)
            {
		        glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, mEBOIndex, mCPUIdxBuffer);
            }
            
            num_elements = (num_elements > 0) ? num_elements : mNumIndices;
            glDrawElements(primitive, num_elements, GL_UNSIGNED_INT, nullptr);
        }
        else
        {
            num_elements = (num_elements > 0) ? num_elements : mNumVertices;
            glDrawArrays(primitive, 0, num_elements);
        }



        mIndex = 0;
        mEBOIndex = 0;

        if (!mIsStatic)
        {
            mNumVertices = 0;
        }

        if (!mEBOIsStatic)
        {
            mNumIndices = 0;
        }
    }

    /////////////////////////////////////////////////////////////////////
    //				HELPER METHODS
    /////////////////////////////////////////////////////////////////////
    // void VertexBuffer::InitLayout()
    // {
        
        // switch (mLayoutType)
        // {
        //     case LayoutType::LINE:
        //     {
        //         Logger::Error(LogCategory::GRAPHICS, "Vertex layout type LINE not supported yet!");
        //     }
        //     break;

        //     case LayoutType::QUAD:
        //     {
        //         // position
		//         glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 10 * sizeof(GLfloat), (GLvoid*)0);
        //         glEnableVertexAttribArray(0);

        //         // Texture coords
		//         glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 10 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(float)));
        //         glEnableVertexAttribArray(1);

        //         // Color
		//         glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 10 * sizeof(GLfloat), (GLvoid*)(5 * sizeof(float)));
        //         glEnableVertexAttribArray(2);

        //         // Texture sampler index
		//         glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 10 * sizeof(GLfloat), (GLvoid*)(9 * sizeof(float)));
        //         glEnableVertexAttribArray(2);

        //         // Texture ID - SHADERS SHOULD IGNORE THIS
		//         glVertexAttribPointer(4, 1, GL_INT, GL_FALSE, 10 * 8, (GLvoid*)(10 * sizeof(float)));
        //         glEnableVertexAttribArray(2);

        //     }
        //     break;

        //     case LayoutType::SPRITE:
        //     {
        //         Logger::Error(LogCategory::GRAPHICS, "Vertex layout type SPRITE not supported yet!");
        //     }
        //     break;

        //     default:
        //         Logger::Warn(LogCategory::GRAPHICS, "UNKNOWN Vertex Layout Type: %d", mLayoutType);

        // }
    //}

    void VertexBuffer::Free()
    {
        glDeleteVertexArrays(1, &mVAO);
        glDeleteBuffers(1, &mVBO);
    }

}