//
// Created by sfd on 25-5-17.
//

#include "Renderer2D.h"

#include <array>
#include <iostream>
#include <Hazel/Debug/Instrumentor.h>
#include "RendererCommand.h"
#include "Shader.h"
#include "glm/ext/matrix_transform.hpp"


namespace Hazel
{

// #define s_Data Renderer2DData::Get()

	struct QuadVertex
	{
		glm::vec3 Position;
		glm::vec4 Color;
		glm::vec2 TexCoord;
		float TexIndex;
		float TilingFactor;
		// TODO:
	};

	struct Renderer2DStorage
	{
		const uint32_t            MaxQuad         = 10000;
		const uint32_t            MaxVertices     = MaxQuad * 4;
		const uint32_t            MaxIndices      = MaxQuad * 6;
		static constexpr uint32_t MaxTextureSlots = 32;		//TODO: RenderCaps

		Ref<VertexArray>  QuadVertexArray;
		Ref<VertexBuffer> QuadVertexBuffer;
		Ref<Shader>       TextureShader;
		Ref<Texture2D>    WhiteTexture;

		uint32_t    QuadIndexCount       = 0;
		QuadVertex* QuadVertexBufferBase = nullptr;
		QuadVertex* QuadVertexBufferPtr  = nullptr;

		std::array<Ref<Texture2D>, MaxTextureSlots> TextureSlots;
		uint32_t TextureSlotIndex = 1;					// 0 use white texture

		glm::vec4 QuadVertexPosition[4];

		Renderer2D::Statistic Stats;
	};

	static Renderer2DStorage s_Data;

    void Renderer2D::Init()
    {
        HZ_PROFILE_FUNCTION();

    	// s_Data = new Renderer2DStorage();
		s_Data.QuadVertexArray = VertexArray::Create();

		//= VertexBuffer::Create(squareVertices, sizeof(squareVertices));
		s_Data.QuadVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(QuadVertex));
		s_Data.QuadVertexBuffer->SetLayout(
			{
				{ShaderDataType::Float3, "a_Postion"},
				{ShaderDataType::Float4, "a_Color"},
				{ShaderDataType::Float2, "a_TexCoord"},
				{ShaderDataType::Float, "a_TexIndex"},
				{ShaderDataType::Float, "a_TilingFactor"}
			});
		s_Data.QuadVertexArray->AddVertexBuffer(s_Data.QuadVertexBuffer);

		s_Data.QuadVertexBufferBase = new QuadVertex[s_Data.MaxVertices];

    	uint32_t* quadIndices = new uint32_t[s_Data.MaxIndices];

    	uint32_t offset = 0;
    	for (uint32_t i = 0; i < s_Data.MaxIndices; i += 6)
    	{
    		quadIndices[i + 0] = offset + 0;
    		quadIndices[i + 1] = offset + 1;
    		quadIndices[i + 2] = offset + 2;

    		quadIndices[i + 3] = offset + 2;
    		quadIndices[i + 4] = offset + 3;
    		quadIndices[i + 5] = offset + 0;

    		offset += 4;
    	}

		Ref<IndexBuffer> quadIB = IndexBuffer::Create(quadIndices, s_Data.MaxIndices);
		s_Data.QuadVertexArray->SetIndexBuffer(quadIB);
    	delete[] quadIndices;


    	s_Data.WhiteTexture = Texture2D::Create(1, 1);
    	uint32_t whiteTextureData =  0xffffffff;
    	s_Data.WhiteTexture->SetData(&whiteTextureData, sizeof(uint32_t));


    	int32_t samplers[s_Data.MaxTextureSlots];
    	for (uint32_t i = 0; i < s_Data.MaxTextureSlots; i ++)
    	{
    		samplers[i] = i;
    	}

		s_Data.TextureShader = Shader::Create("assets/shaders/Texture.glsl");
    	s_Data.TextureShader->Bind();
    	s_Data.TextureShader->SetIntv("u_Textures", samplers, s_Data.MaxTextureSlots);

    	// set all texture slot to 0
    	s_Data.TextureSlots[0] = s_Data.WhiteTexture;
    	/*
		memset(s_Data.TextureSlots.data(), 0, s_Data.TextureSlots.size() * sizeof(uint32_t));
		or
    	for (uint32_t i = 0; i < s_Data.TextureSlots.size(); i ++)
    	{
    		s_Data.TextureSlots[i] = 0;
    	}
	    */

    	s_Data.QuadVertexPosition[0] = {-0.5f, -0.5f, 0.0f, 1.0f};
    	s_Data.QuadVertexPosition[1] = {0.5f, -0.5f, 0.0f, 1.0f};
    	s_Data.QuadVertexPosition[2] = {0.5f, 0.5f, 0.0f, 1.0f};
    	s_Data.QuadVertexPosition[3] = {-0.5f, 0.5f, 0.0f, 1.0f};
    }

    void Renderer2D::BeginScene(const Camera& camera, const glm::mat4& tranform)
    {
        HZ_PROFILE_FUNCTION();

    	const glm::mat4 viewProjection = camera.GetProjection() * glm::inverse(tranform);


    	s_Data.TextureShader->Bind();
        s_Data.TextureShader->SetMat4("u_ViewProjection", viewProjection);

    	s_Data.QuadIndexCount = 0;
    	s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;

    	s_Data.TextureSlotIndex = 1;
    }

    void Renderer2D::BeginScene(const OrthographicCamera& camera)
    {
        HZ_PROFILE_FUNCTION();
    	s_Data.TextureShader->Bind();
        s_Data.TextureShader->SetMat4("u_ViewProjection", camera.GetViewProjectionMatrix());

    	s_Data.QuadIndexCount = 0;
    	s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;

    	s_Data.TextureSlotIndex = 1;
    }

    void Renderer2D::EndScene()
    {
	    HZ_PROFILE_FUNCTION();

    	const uint32_t datasize = reinterpret_cast<uint8_t*>(s_Data.QuadVertexBufferPtr) - reinterpret_cast<uint8_t*>(s_Data.QuadVertexBufferBase);
    	s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, datasize);
		Flush();
    }

    void Renderer2D::Flush()
    {
    	for (uint32_t i = 0; i < s_Data.TextureSlotIndex; i ++ )
    	{
    		s_Data.TextureSlots[i]->Bind(i);
    	}
		RendererCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);

    	s_Data.Stats.DrawCalls++;
    }

    void Renderer2D::Shutdown()
    {
        HZ_PROFILE_FUNCTION();
    }

    void Renderer2D::ResetStats()
    {
    	memset(&s_Data.Stats, 0, sizeof(Statistic));
    }

    Renderer2D::Statistic Renderer2D::GetStats()
    {
    	return s_Data.Stats;
    }

    void Renderer2D::FlushAndReset()
    {
    	EndScene();

    	s_Data.QuadIndexCount = 0;
    	s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;

    	s_Data.TextureSlotIndex = 1;
    }


    void Renderer2D::DrawQuad(const glm::vec2& position, const glm::vec2& size, const glm::vec4& color)
    {
    	DrawQuad({position.x, position.y, 0.0f}, size, color);
    }

    void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2& size, const glm::vec4& color)
    {
        HZ_PROFILE_FUNCTION();
    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}
    	const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

    	DrawQuad(transform, color);
    }

    void Renderer2D::DrawQuad(const glm::vec2& position, const glm::vec2& size, const Ref<Texture2D>& texture, const float tilingFactor, const glm::vec4& tintColor)
    {
    	DrawQuad({position.x, position.y, 0.0f}, size, texture, tilingFactor, tintColor);
    }

    void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2& size, const Ref<Texture2D>& texture, const float tilingFactor, const glm::vec4& tintColor)
    {
        HZ_PROFILE_FUNCTION();
    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

    	const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

    	DrawQuad(transform, texture, tilingFactor, tintColor);
    }

    void Renderer2D::DrawQuad(const glm::vec2& position, const glm::vec2& size, const Ref<SubTexture2D>& subTexture, const float tilingFactor, const glm::vec4& tintColor)
    {
    	DrawQuad({position.x, position.y, 0.0f}, size, subTexture, tilingFactor, tintColor);
    }

    void Renderer2D::DrawQuad(const glm::vec3& position, const glm::vec2& size, const Ref<SubTexture2D>& subTexture, const float tilingFactor, const glm::vec4& tintColor)
    {
        HZ_PROFILE_FUNCTION();
    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

    	const glm::vec2* texCoords = subTexture->GetTexCoords();
    	const Ref<Texture2D> texture = subTexture->GetTexture();

    	float textureIndex = 0.0f;

    	for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
    	{
    		if (*s_Data.TextureSlots[i].get() == *texture.get())
    		{
    			textureIndex = (float)i;
    			break;
    		}

    	}

    	if (textureIndex == 0.0f)
    	{
    		textureIndex =	(float)s_Data.TextureSlotIndex;
    		s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
    		s_Data.TextureSlotIndex++;
    	}

    	const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

    	for (unsigned int i = 0; i < 4; i++)
	    {
		    s_Data.QuadVertexBufferPtr->Position     = transform * s_Data.QuadVertexPosition[i];
		    s_Data.QuadVertexBufferPtr->Color        = tintColor;
		    s_Data.QuadVertexBufferPtr->TexCoord     = texCoords[i];
		    s_Data.QuadVertexBufferPtr->TexIndex     = textureIndex;
		    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
		    s_Data.QuadVertexBufferPtr++;
	    }
    	s_Data.QuadIndexCount += 6;

    	s_Data.Stats.QuadCount ++;
    }

    void Renderer2D::DrawQuad(const glm::mat4& transform, const glm::vec4& Color)
    {
        HZ_PROFILE_FUNCTION();
    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

    	glm::vec2 texCoords[] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};
    	const float textureIndex = 0.0f;
    	const float tilingFactor = 1.0f;

    	for (unsigned int i = 0; i < 4; i++)
	    {
		    s_Data.QuadVertexBufferPtr->Position     = transform * s_Data.QuadVertexPosition[i];
		    s_Data.QuadVertexBufferPtr->Color        = Color;
		    s_Data.QuadVertexBufferPtr->TexCoord     = texCoords[i];
		    s_Data.QuadVertexBufferPtr->TexIndex     = textureIndex;
		    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
		    s_Data.QuadVertexBufferPtr++;
	    }
    	s_Data.QuadIndexCount += 6;

    	s_Data.Stats.QuadCount ++;
    }

    void Renderer2D::DrawQuad(const glm::mat4& transform, const Ref<Texture2D>& texture, float tilingFactor,
	    const glm::vec4& tintColor)
    {
        HZ_PROFILE_FUNCTION();
    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

    	glm::vec2 texCoords[] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};

    	float textureIndex = 0.0f;

    	for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
    	{
    		if (*s_Data.TextureSlots[i].get() == *texture.get())
    		{
    			textureIndex = (float)i;
    			break;
    		}

    	}

    	if (textureIndex == 0.0f)
    	{
    		textureIndex =	(float)s_Data.TextureSlotIndex;
    		s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
    		s_Data.TextureSlotIndex++;
    	}


    	for (unsigned int i = 0; i < 4; i++)
	    {
		    s_Data.QuadVertexBufferPtr->Position     = transform * s_Data.QuadVertexPosition[i];
		    s_Data.QuadVertexBufferPtr->Color        = tintColor;
		    s_Data.QuadVertexBufferPtr->TexCoord     = texCoords[i];
		    s_Data.QuadVertexBufferPtr->TexIndex     = textureIndex;
		    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
		    s_Data.QuadVertexBufferPtr++;
	    }
    	s_Data.QuadIndexCount += 6;

    	s_Data.Stats.QuadCount ++;
    }


    void Renderer2D::DrawRotateQuad(const glm::vec2& position, const glm::vec2& size, const float rotation,
                                    const glm::vec4& color)
    {
    	DrawRotateQuad({position.x, position.y, 0.0f}, size, rotation, color);
    }

    void Renderer2D::DrawRotateQuad(const glm::vec3& position, const glm::vec2& size, const float rotation,
	    const glm::vec4& color)
    {
        HZ_PROFILE_FUNCTION();


    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

        constexpr float     texIndex     = 0.0f;
        constexpr float tilingFactor = 1.0f;
    	glm::vec2 texCoords[] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};

    	const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::rotate(glm::mat4(1.0f), rotation, {0.0f, 0.0f, 1.0f}) * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

    	for (unsigned int i = 0; i < 4; i++)
	    {
		    s_Data.QuadVertexBufferPtr->Position     = transform * s_Data.QuadVertexPosition[i];
		    s_Data.QuadVertexBufferPtr->Color        = color;
		    s_Data.QuadVertexBufferPtr->TexCoord     = texCoords[i];
		    s_Data.QuadVertexBufferPtr->TexIndex     = texIndex;
		    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
		    s_Data.QuadVertexBufferPtr++;
	    }
    	s_Data.QuadIndexCount += 6;

    	s_Data.Stats.QuadCount ++;
    }

    void Renderer2D::DrawRotateQuad(const glm::vec2& position, const glm::vec2& size, const float rotation,
	    const Ref<Texture2D>& texture, const float tilingFactor, const glm::vec4& tintColor)
    {
    	DrawRotateQuad({position.x, position.y, 0.0f}, size, rotation, texture, tilingFactor, tintColor);
    }

    void Renderer2D::DrawRotateQuad(const glm::vec3& position, const glm::vec2& size, const float rotation,
	    const Ref<Texture2D>& texture, const float tilingFactor, const glm::vec4& tintColor)
    {
        HZ_PROFILE_FUNCTION();

    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

    	float textureIndex = 0.0f;
    	glm::vec2 texCoords[] = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};

    	for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
    	{
    		if (*s_Data.TextureSlots[i].get() == *texture.get())
    		{
    			textureIndex = (float)i;
    			break;
    		}
    	}

    	if (textureIndex == 0.0f)
    	{
    		textureIndex =	(float)s_Data.TextureSlotIndex;
    		s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
    		s_Data.TextureSlotIndex++;
    	}

    	const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::rotate(glm::mat4(1.0f), rotation, {0.0f, 0.0f, 1.0f}) * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

    	for (unsigned int i = 0; i < 4; i++)
	    {
		    s_Data.QuadVertexBufferPtr->Position     = transform * s_Data.QuadVertexPosition[i];
		    s_Data.QuadVertexBufferPtr->Color        = tintColor;
		    s_Data.QuadVertexBufferPtr->TexCoord     = texCoords[i];
		    s_Data.QuadVertexBufferPtr->TexIndex     = textureIndex;
		    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
		    s_Data.QuadVertexBufferPtr++;
	    }
    	s_Data.QuadIndexCount += 6;

    	s_Data.Stats.QuadCount ++;
    }

    void Renderer2D::DrawRotateQuad(const glm::vec2& position, const glm::vec2& size, const float rotation,
	    const Ref<SubTexture2D>& subTexture, const float tilingFactor, const glm::vec4& tintColor)
    {
    	DrawRotateQuad({position.x, position.y, 0.0f}, size, rotation, subTexture, tilingFactor, tintColor);
    }

    void Renderer2D::DrawRotateQuad(const glm::vec3& position, const glm::vec2& size, const float rotation,
	    const Ref<SubTexture2D>& subTexture, const float tilingFactor, const glm::vec4& tintColor)
    {
        HZ_PROFILE_FUNCTION();

    	if (s_Data.QuadIndexCount >= s_Data.MaxIndices)
    	{
			FlushAndReset();
    	}

    	float textureIndex = 0.0f;
		const glm::vec2* texCoords = subTexture->GetTexCoords();
    	const Ref<Texture2D>& texture = subTexture->GetTexture();

    	for (uint32_t i = 1; i < s_Data.TextureSlotIndex; i++)
    	{
    		if (*s_Data.TextureSlots[i].get() == *texture.get())
    		{
    			textureIndex = (float)i;
    			break;
    		}
    	}

    	if (textureIndex == 0.0f)
    	{
    		textureIndex =	(float)s_Data.TextureSlotIndex;
    		s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
    		s_Data.TextureSlotIndex++;
    	}

    	const glm::mat4 transform = glm::translate(glm::mat4(1.0f), position) * glm::rotate(glm::mat4(1.0f), rotation, {0.0f, 0.0f, 1.0f}) * glm::scale(glm::mat4(1.0f), { size.x, size.y, 1.0f });

    	for (unsigned int i = 0; i < 4; i++)
	    {
		    s_Data.QuadVertexBufferPtr->Position     = transform * s_Data.QuadVertexPosition[i];
		    s_Data.QuadVertexBufferPtr->Color        = tintColor;
		    s_Data.QuadVertexBufferPtr->TexCoord     = texCoords[i];
		    s_Data.QuadVertexBufferPtr->TexIndex     = textureIndex;
		    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
		    s_Data.QuadVertexBufferPtr++;
	    }
    	s_Data.QuadIndexCount += 6;

    	s_Data.Stats.QuadCount ++;
    }
}
