/******************************************************************************
*	File		-	renderer2D.h
*	Author		-	Joey Pollack
*	Date		-	2022/07/13 (y/m/d)
*	Mod Date	-	2022/07/13 (y/m/d)
*	Description	-	The main 2D render class
******************************************************************************/

#include "renderer2D.h"
#include "orthographic_camera.h"
#include "shaders/defaultShaders.h"

#include <utils/logger.h>
#include <utils/helpers.h>
#include <glad/gl.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <core/core.h>

namespace lunarium
{
    Renderer2D::Renderer2D()
        : mpCamera(nullptr)
    {

    }

    struct QuadTestData
    {
        Color color;
        BufferLayout mBufferLayout;
        VertexBuffer* mVertexBuffer;
        IndexBuffer* mIndexBuffer;
        Shader* mQuadShader;
    } mTestData;

    OpRes Renderer2D::Initialize()
    {
        mpCamera = nullptr;

        u32* indices = new u32[mQuadData.MaxIndices];
        // Need to adjust the set of indices because they reference verts deeper into the buffer
        // See Hazel for example of this
        int offset = 0;
        for (int i = 0; i < mQuadData.MaxIndices; i+=6)
        {
            for ( int j = 0; j < 6; j++)
            {
                indices[i + j] = mQuadData.indices[(i + j) % 6] + offset;
            }
            offset += 4;
        }

        // INIT QUAD DATA
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 3 }); // Position
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 2 }); // Tex_coords
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 4 }); // Color
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 1 });   // Texture Sampler Index
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 3 });   // translation
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 1 });   // angle
        mQuadData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 3 });   // Scale
        mQuadData.mVertexBuffer = VertexBuffer::Create(mQuadData.mBufferLayout, mQuadData.MaxVertices, nullptr, mQuadData.MaxIndices, indices);

        // RAW VERTEX BUFFER
        mQuadData.mRawVertexBuffer = new QuadData::Vertex[mQuadData.MaxVertices];
        mQuadData.mRawBufferIndex = 0;

        int layout_size = mQuadData.mBufferLayout.GetLayoutSizeInBytes();
        int struct_size = sizeof(QuadData::Vertex);
        if (struct_size != layout_size)
        {
            Logger::Error(LogCategory::GRAPHICS, 
                    "Quad::Vertex struct size does not match the vertex layout size! struct: %d, layout: %d", struct_size, layout_size);
        }

        //mQuadData.mIndexBuffer = new IndexBuffer(mQuadData.MaxIndices * sizeof(u32));

        std::string vert_source = File::ReadTextFile("quad.vert");
        std::string frag_source = File::ReadTextFile("quad.frag");
        mQuadData.mQuadShader = new Shader(vert_source.c_str(), nullptr, frag_source.c_str());
        if (!mQuadData.mQuadShader->IsValid())
        {
            return OpRes::Fail("Failed to create the quad shader");
        }
        
        // TODO: INIT SPRITE DATA


        // TODO: INIT DEBUG TEXTURE
        u8 data[4] = {255, 255, 255, 255};
        mpWhiteTexture = Texture::Create(data, 1, 1);
        mLoadedTextures.push_back(mpWhiteTexture);

        // DEBUG: INIT TEST DATA
    //     mTestData.mBufferLayout.PushVertexAttribute(VertexAttribute { VertexAttributeType::FLOAT32, 2}); // Position

    //     GLfloat vertices[6][2] = {
	// 				{ -0.5f,  0.5f, },
	// 				{ 0.5f, -0.5f,  },
	// 				{ -0.5f,  -0.5f, },

	// 				{ -0.5f, 0.5f, },
	// 				{  0.5f, 0.5f, },
	// 				{ 0.5f, -0.5f,  }
	// 			};
    //     u32 indices[6] = { 0, 1, 2, 3, 4, 5 };
        
    //    // mTestData.mIndexBuffer = new IndexBuffer(6, indices);
    //     mTestData.mVertexBuffer = VertexBuffer::Create(mTestData.mBufferLayout, 6, nullptr, 6, indices);
    //     std::string vert_source = File::ReadTextFile("test.vert");
    //     std::string frag_source = File::ReadTextFile("test.frag");
    //     mTestData.mQuadShader = new Shader(vert_source.c_str(), nullptr, frag_source.c_str());
    //     if (!mTestData.mQuadShader->IsValid())
    //     {
    //         return OpRes::Fail("Failed to create the test shader");
    //     }

        // glGenVertexArrays(1, &mTVAO);
		// glGenBuffers(1, &mTVBO);

		// glBindBuffer(GL_ARRAY_BUFFER, mTVBO);
		// glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 12, vertices, GL_STATIC_DRAW);

		// glBindVertexArray(mTVAO);
		// glEnableVertexAttribArray(0);
		// glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*)0);
		// glBindBuffer(GL_ARRAY_BUFFER, 0);
		// glBindVertexArray(0);

       return OpRes::OK();
    }
    
    void Renderer2D::Shutdown()
    {
        VertexBuffer::Destroy(&mQuadData.mVertexBuffer);
        //delete[] mQuadData.mIndexBuffer;
    }

    void Renderer2D::SetClearColor(Color color)
    {
        mClearColor = color;
        glClearColor(color.R, color.G, color.B, color.A);
    }

    Color Renderer2D::GetClearColor()
    {
        return mClearColor;
    }

    void Renderer2D::ResetFrameStats()
    {
        mFrameStats.DrawCalls = 0;
        mFrameStats.NumTris = 0;
    }

    Renderer2D::FrameStats Renderer2D::GetFrameStats() const
    {
        return mFrameStats;
    }

    void Renderer2D::BeginDraw(OrthographicCamera* pCamera)
    {
        mpCamera = pCamera;
        glViewport(0, 0, pCamera->GetViewport().Width, pCamera->GetViewport().Height);
        // int w, h;
        // Core::MainWindow().GetFramebufferSize(&w, &h);
        // glViewport(0, 0, w, h);
        glClear(GL_COLOR_BUFFER_BIT);
        //ResetFrameStats();
    }

    void Renderer2D::EndDraw()
    {
        Flush();
    }

    /////////////////////////////////////////////////////////////////////
    //				DRAW METHODS
    /////////////////////////////////////////////////////////////////////
    void Renderer2D::DrawQuad(Rectangle quad, Color color, Texture* texture, float angle)
    {
        int texture_slot = -1;
        if (texture)
        {
            for (int i = 0; i < mLoadedTextures.size(); i++)
            {
                if (texture == mLoadedTextures[i])
                {
                    texture_slot = i;
                    break;
                }
            }

            if (-1 == texture_slot)
            {
                mLoadedTextures.push_back(texture);
                texture_slot = mLoadedTextures.size() - 1;
            }
        }
        else
        {
            texture_slot = 0;
        }


        //unsigned char* vertices_wl = (unsigned char*)vertices;  // vertices write location pointer

        // glm::mat4 model(1.0f);
        // model = glm::translate(model, glm::vec3(quad.CenterPoint().x, quad.CenterPoint().y, 0.0f));
        // model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f));

        // glm::mat4 model = glm::mat4(1.0f);
		// model = glm::translate(model, glm::vec3(quad.X, quad.Y, 0.0f));
		// model = glm::rotate(model, angle, glm::vec3(0.0f, 0.0f, 1.0f));
		 // model = glm::scale(model, glm::vec3(quad.HalfWidth * 2, quad.HalfHeight * 2, 1.0f));

        if (mQuadData.mRawBufferIndex + 4 > mQuadData.MaxVertices)
        {
            Flush();
        }

        // FIRST
        QuadData::Vertex v1;
        int vert_size = sizeof(QuadData::Vertex);
        v1.pos = mQuadData.vert_pos[0];
        v1.tex_coord = mQuadData.vert_tex[0];
        v1.color = color;
        v1.tex_slot = texture_slot;
        v1.translation = glm::vec3(quad.CenterPoint().x, quad.CenterPoint().y, 0.0f);
        v1.angle = angle;
        v1.scale = glm::vec3(quad.HalfWidth * 2, quad.HalfHeight * 2, 1.0f);
        memcpy(&mQuadData.mRawVertexBuffer[mQuadData.mRawBufferIndex], &v1, vert_size);
        mQuadData.mRawBufferIndex += 1;

        // SECOND
        QuadData::Vertex v2;
        v2.pos =  mQuadData.vert_pos[1];
        v2.tex_coord = mQuadData.vert_tex[1];
        v2.color = color;
        v2.tex_slot = texture_slot;
        v2.translation = glm::vec3(quad.CenterPoint().x, quad.CenterPoint().y, 0.0f);
        v2.angle = angle;
        v2.scale = glm::vec3(quad.HalfWidth * 2, quad.HalfHeight * 2, 1.0f);
        memcpy(&mQuadData.mRawVertexBuffer[mQuadData.mRawBufferIndex], &v2, vert_size);
        mQuadData.mRawBufferIndex += 1;

        // THIRD
        QuadData::Vertex v3;
        v3.pos = mQuadData.vert_pos[2];
        v3.tex_coord = mQuadData.vert_tex[2];
        v3.color = color;
        v3.tex_slot = texture_slot;
        v3.translation = glm::vec3(quad.CenterPoint().x, quad.CenterPoint().y, 0.0f);
        v3.angle = angle;
        v3.scale = glm::vec3(quad.HalfWidth * 2, quad.HalfHeight * 2, 1.0f);
        memcpy(&mQuadData.mRawVertexBuffer[mQuadData.mRawBufferIndex], &v3, vert_size);
        mQuadData.mRawBufferIndex += 1;

        // FOURTH
        QuadData::Vertex v4;
        v4.pos = mQuadData.vert_pos[3];
        v4.tex_coord = mQuadData.vert_tex[3];
        v4.color = color;
        v4.tex_slot = texture_slot;
        v4.translation = glm::vec3(quad.CenterPoint().x, quad.CenterPoint().y, 0.0f);
        v4.angle = angle;
        v4.scale = glm::vec3(quad.HalfWidth * 2, quad.HalfHeight * 2, 1.0f);
        memcpy(&mQuadData.mRawVertexBuffer[mQuadData.mRawBufferIndex], &v4, vert_size);
        mQuadData.mRawBufferIndex += 1;

        

        // memcpy(mQuadData.mRawVertexBuffer + mQuadData.mRawBufferIndex, vertices, sizeof(QuadData::Vertex) * 4);
        // mQuadData.mRawBufferIndex += 4;

        // if (!mQuadData.mVertexBuffer->PushVertices(vertices, 4))
        // {
        //     //Logger::Info(LogCategory::GRAPHICS, "Quad VertexBuffer is full, flushing and retrying");
        //     Flush();
        //     DrawQuad(quad, color, texture);
        // }

        // INDICES
        // if (!mQuadData.mVertexBuffer->PushIndices(mQuadData.indices, 6))
        // {
        //     Logger::Error(LogCategory::GRAPHICS, "Quad IndexBuffer is full - This shouldn't happen because the VertexBuffer should fill up first!");
        //     return;
        // }

        mQuadData.mNumQuads++;
        mFrameStats.NumTris += 2;
        
    }


    void Renderer2D::DrawSprite(Texture& image, glm::vec2 position, Color color, float angle)
    {
        Logger::Warn(LogCategory::GRAPHICS, "Renderer2D::DrawSprite is not yet implemented");
		// float half_width = image.GetWidth() / 2;
		// float half_height = image.GetHeight() / 2;
        // DrawImage(image, Rectangle::MakeFromTopLeft(0, 0, image.GetWidth(), image.GetHeight()),
		//  	Rectangle(position.x + half_width, position.y + half_height, half_width, half_height), color, angle);
    }

    void Renderer2D::DrawSprite(Texture& image, Rectangle source, Rectangle destination, Color color, float angle)
    {
        Logger::Warn(LogCategory::GRAPHICS, "Renderer2D::DrawSprite is not yet implemented");
        // glm::mat4 id = glm::mat4(1.0f);
		// glm::vec3 pos = glm::vec3(destination.X, destination.Y, 0.0f);
		// glm::mat4 trans = glm::translate(id, pos);
		// trans = glm::rotate(trans, angle, glm::vec3(0.0f, 0.0f, 1.0f));
		// trans = glm::scale(trans, glm::vec3(destination.HalfWidth * 2, destination.HalfHeight * 2, 1.0f));


		// mImageShader.MakeActive();

		// float xScale = (source.HalfWidth * 2) / image.GetWidth();
		// float xOffset = source.left() / image.GetWidth();
		// float yScale = (source.HalfHeight * 2) / image.GetHeight();
		// float yOffset = source.top() / image.GetHeight();


		// // * -1.0f on yScale will flip the image vertically
		// mImageShader.SetUniformf("uvManip", { xScale, xOffset, yScale * -1.0f, yOffset});
		// mImageShader.SetUniformMatrix("model", 1, glm::value_ptr(trans));
		// mImageShader.SetUniformMatrix("projection", 1, glm::value_ptr(mProjection));
		// mImageShader.SetUniformf("spriteColor", { color.R, color.G, color.B, color.A });
		

        // glActiveTexture(GL_TEXTURE0);
        // glBindTexture(GL_TEXTURE_2D, image.GetGLTextureID());

        // glBindVertexArray(mImageVAO);
        // glDrawArrays(GL_TRIANGLES, 0, 6);
        // glBindVertexArray(0);
    }

    void Renderer2D::DrawString(const char* string, Rectangle boundingArea, Color color, float scale, int font)
    {
        Logger::Warn(LogCategory::GRAPHICS, "Renderer2D::DrawString is not yet implemented");
		// mText.DrawString(font, string, glm::vec2(boundingArea.left(), boundingArea.top()),
		// 					 glm::vec2(boundingArea.right(), boundingArea.bottom()), color, scale, mProjection);
    }


    /////////////////////////////////////////////////////////////////////
    //				HELPERS
    /////////////////////////////////////////////////////////////////////
    void Renderer2D::Flush()
    {
        if (mQuadData.mNumQuads > 0)
        {   
            if (!mQuadData.mVertexBuffer->PushVertices(mQuadData.mRawVertexBuffer, mQuadData.mNumQuads * 4))
            {
                Logger::Error(LogCategory::GRAPHICS, "Could not push verts into the quad buffer!");
                return;
            }

            //mQuadData.mIndexBuffer->Bind();
            mQuadData.mQuadShader->Use();
            for (int i = 0; i < mLoadedTextures.size(); i++)
            {
                mLoadedTextures[i]->Bind(i);
            }

            // Uniform umodel;
            // umodel.Type = UniformType::FMAT4;
            // umodel.Location = -1;
            // umodel.Name = "model";

            Uniform uprojview;
            uprojview.Type = UniformType::FMAT4;
            uprojview.Location = -1;
            uprojview.Name = "projview";

            //mQuadData.mQuadShader->SetUniform(umodel, (void*)glm::value_ptr(glm::mat4(1.0f))).LogIfFailed(LogCategory::GRAPHICS);
            mQuadData.mQuadShader->SetUniform(uprojview, (void*)glm::value_ptr(mpCamera->GetViewProjection())).LogIfFailed(LogCategory::GRAPHICS);

            //Uniform uni { "spriteColor", UniformType::F3, -1 };
            //Color c = ;
            //mQuadData.mQuadShader->SetUniform(uni, Color::Red().arr).LogIfFailed(LogCategory::GRAPHICS, "Failed to set uniform for the test shader!");

            mQuadData.mVertexBuffer->Bind();
            glDrawElements(GL_TRIANGLES, mQuadData.mNumQuads * 6,  GL_UNSIGNED_INT, nullptr);
            //glDrawArrays(GL_TRIANGLES, 0, mQuadData.mNumQuads * 2);
            mFrameStats.DrawCalls++;
        }
        // Reset drawing data
        for (int i = 0; i < mLoadedTextures.size(); i++)
        {
            mLoadedTextures[i]->Unbind();
        }

        mLoadedTextures.clear();
        mQuadData.mVertexBuffer->Clear();
        mQuadData.mNumQuads = 0;
        mQuadData.mRawBufferIndex = 0;

        // TODO: Add the debug texture back to the map
        mLoadedTextures.push_back(mpWhiteTexture);
    }
}