#include "renderer/editor_camera.hpp"
#include "renderer/uniform_buffer.hpp"
#include <pch.hpp>

#include <renderer/render_command.hpp>
#include <renderer/vertex_array.hpp>
#include <renderer/renderer2d.hpp>
#include <renderer/texture.hpp>
#include <renderer/buffer.hpp>
#include <renderer/shader.hpp>

#include <glm/ext/quaternion_common.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/trigonometric.hpp>
#include <glm/fwd.hpp>
#include <cstdint>

namespace OpenEngine {
    struct QuadVertex
    {
        glm::vec3 position;
        glm::vec4 color;
        glm::vec2 tex_coord;
        float tex_index;
        float tiling_factor;
        int id = -1;
    };

    struct Renderer2DData
    {
        static const uint32_t max_quads = 20000;
        static const uint32_t max_vertices = max_quads * 4;
        static const uint32_t max_indices = max_quads * 6;
        static const uint32_t max_texture_slots = 32;

        Ref<VertexArray> vertex_array;
        Ref<VertexBuffer> vertex_buffer;
        Ref<Shader> texture_shader;
        Ref<Texture2D> white_texture;

        uint32_t quad_index_count = 0;
        QuadVertex* quad_vertex_base = nullptr;
        QuadVertex* quad_vertex_ptr = nullptr;

        std::array<Ref<Texture2D>, max_texture_slots> texture_slots; 
        uint32_t texture_slot_index = 1; // 0 is white texture

        glm::vec4 quad_vertex_positioning[4];

        Statistics stats;

        struct CameraData
        {
            glm::mat4 view_projection;
        };
        CameraData camera_buffer;
        Ref<UniformBuffer> camera_uniform_buffer;
    };

    static Renderer2DData renderer_data;

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

        renderer_data.vertex_array = VertexArray::Create();
        renderer_data.vertex_buffer = VertexBuffer::Create(renderer_data.max_vertices * sizeof(QuadVertex));

        BufferLayout layout = {
            { ShaderDataType::Float3, "a_Position" },
            { ShaderDataType::Float4, "a_Color" },
            { ShaderDataType::Float2, "a_TexCoords" },
            { ShaderDataType::Float, "a_TexIndex" },
            { ShaderDataType::Float, "a_TilingFactor" },
            { ShaderDataType::Int, "a_EntityID"}
        };

        renderer_data.vertex_buffer->SetLayout(layout);
        renderer_data.vertex_array->AddVertexBuffer(renderer_data.vertex_buffer);

        renderer_data.quad_vertex_base = new QuadVertex[renderer_data.max_vertices];

        uint32_t* quad_indices = new uint32_t[renderer_data.max_indices];

        uint32_t offset = 0;
        for (uint32_t i = 0; i < renderer_data.max_indices; i += 6) {
            quad_indices[i + 0] = offset + 0;
            quad_indices[i + 1] = offset + 1;
            quad_indices[i + 2] = offset + 2;

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

            offset += 4;
        }

        Ref<IndexBuffer> index_buffer = IndexBuffer::Create(quad_indices, renderer_data.max_indices);
        renderer_data.vertex_array->SetIndexBuffer(index_buffer);
        delete[] quad_indices;

        renderer_data.white_texture = Texture2D::Create(1, 1);
        uint32_t white_texture_data = 0xffffffff;
        renderer_data.white_texture->SetData(&white_texture_data, sizeof(uint32_t));

        int32_t samplers[renderer_data.max_texture_slots];
        for (uint32_t i = 0; i < renderer_data.max_texture_slots; i++)
            samplers[i] = i;

        renderer_data.texture_shader = Shader::Create("assets/shaders/texture.glsl");

        renderer_data.texture_slots[0] = renderer_data.white_texture;

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

        renderer_data.camera_uniform_buffer = UniformBuffer::Create(sizeof(Renderer2DData), 0);
    }

    void Renderer2D::Shutdown()
    {
        renderer_data.white_texture.reset();
        renderer_data.vertex_array.reset();
        renderer_data.vertex_buffer.reset();
        renderer_data.texture_shader.reset();

        delete[] renderer_data.quad_vertex_base;

        for (uint32_t i = 0; i < renderer_data.texture_slot_index; i++)
            renderer_data.texture_slots[i].reset();

        OE_PROFILE_FUNCTION();
    }

    void Renderer2D::BeginScene(const OrthographicCamera& camera)
    {
        OE_PROFILE_FUNCTION();

        renderer_data.camera_buffer.view_projection = camera.GetViewProjectionMatrix();
        renderer_data.camera_uniform_buffer->SetData(&renderer_data.camera_buffer, sizeof(Renderer2DData::CameraData));

        StartBatch();
    }

    void Renderer2D::BeginScene(const Camera& camera, const glm::mat4& transform)
    {
        OE_PROFILE_FUNCTION();

        renderer_data.camera_buffer.view_projection = camera.GetProjection() * glm::inverse(transform);
		renderer_data.camera_uniform_buffer->SetData(&renderer_data.camera_buffer, sizeof(Renderer2DData::CameraData));

        StartBatch();
    }

    void Renderer2D::BeginScene(const EditorCamera& camera)
    {
        OE_PROFILE_FUNCTION();

        renderer_data.camera_buffer.view_projection = camera.GetViewProjection();
		renderer_data.camera_uniform_buffer->SetData(&renderer_data.camera_buffer, sizeof(Renderer2DData::CameraData));

        StartBatch();
    }

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

        uint32_t data_size = (uint8_t*)renderer_data.quad_vertex_ptr - (uint8_t*)renderer_data.quad_vertex_base;
        renderer_data.vertex_buffer->SetData(renderer_data.quad_vertex_base, data_size);

        Flush();
    }

    void Renderer2D::Flush()
    {
        if (renderer_data.quad_index_count == 0)
            return;

        renderer_data.vertex_array->Bind();

        for (int i = 0; i < renderer_data.texture_slot_index; i++)
            renderer_data.texture_slots[i]->Bind(i);

        renderer_data.texture_shader->Bind();
        RenderCommand::DrawIndexed(renderer_data.vertex_array, renderer_data.quad_index_count);
        
        renderer_data.stats.draw_calls++;
    }

    void Renderer2D::DrawQuad(const Transform& transform_data,
            const Ref<Texture2D>& texture, int entity_id, float tiling_factor)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        constexpr glm::vec2 texture_coords[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f }};

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        constexpr glm::vec4 color = glm::vec4(1.0f);

        glm::vec3 position = transform_data.position;
        glm::vec3 size = transform_data.size;

        float texture_index = 0;
        for (uint32_t i = 1; i < renderer_data.texture_slot_index; i++) {
            if (*renderer_data.texture_slots[i].get() == *texture.get()) {
                texture_index = (float)i;
                break;
            }
        }

        if (texture_index == 0) {
            if (renderer_data.texture_slot_index >= Renderer2DData::max_texture_slots)
                FlushAndReset();

            texture_index = (float)renderer_data.texture_slot_index;
            renderer_data.texture_slots[renderer_data.texture_slot_index] = texture;
            renderer_data.texture_slot_index++;
        }

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

        DrawQuad(transform, texture, entity_id);
    }

    void Renderer2D::DrawQuad(const Transform& transform_data,
            const Ref<Texture2D>& texture, const glm::vec4& color,
            int entity_id, float tiling_factor)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        constexpr glm::vec2 texture_coords[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f }};

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        glm::vec3 position = transform_data.position;
        glm::vec3 size = transform_data.size;

        float texture_index = 0;
        for (uint32_t i = 1; i < renderer_data.texture_slot_index; i++) {
            if (*renderer_data.texture_slots[i].get() == *texture.get()) {
                texture_index = (float)i;
                break;
            }
        }

        if (texture_index == 0) {
            if (renderer_data.texture_slot_index >= Renderer2DData::max_texture_slots)
                FlushAndReset();

            texture_index = (float)renderer_data.texture_slot_index;
            renderer_data.texture_slots[renderer_data.texture_slot_index] = texture;
            renderer_data.texture_slot_index++;
        }

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

    void Renderer2D::DrawQuad(const Transform& transform_data,
            const Ref<Subtexture2D>& subtexture, int entity_id,
            float tiling_factor)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        const glm::vec2* texture_coords = subtexture->GetCoords();
        const Ref<Texture2D>& texture = subtexture->GetTexture();

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        constexpr glm::vec4 color = glm::vec4(1.0f);

        glm::vec3 position = transform_data.position;
        glm::vec3 size = transform_data.size;

        float texture_index = 0;
        for (uint32_t i = 1; i < renderer_data.texture_slot_index; i++) {
            if (*renderer_data.texture_slots[i].get() == *texture.get()) {
                texture_index = (float)i;
                break;
            }
        }

        if (texture_index == 0) {
            if (renderer_data.texture_slot_index >= Renderer2DData::max_texture_slots)
                FlushAndReset();

            texture_index = (float)renderer_data.texture_slot_index;
            renderer_data.texture_slots[renderer_data.texture_slot_index] = texture;
            renderer_data.texture_slot_index++;
        }

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

        DrawQuad(transform, subtexture, entity_id);
    }

    void Renderer2D::DrawQuad(const Transform& transform_data,
            const glm::vec4& color, int entity_id)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        constexpr glm::vec2 texture_coords[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f }};

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        glm::vec3 position = transform_data.position;
        glm::vec3 size = transform_data.size;

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


        DrawQuad(transform, color, entity_id);
    }

    void Renderer2D::DrawQuad(const glm::mat4& transform,
            const Ref<Texture2D>& texture, int entity_id, float tiling_factor)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        constexpr glm::vec2 texture_coords[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f }};

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        constexpr glm::vec4 color = glm::vec4(1.0f);

        float texture_index = 0;
        for (uint32_t i = 1; i < renderer_data.texture_slot_index; i++) {
            if (*renderer_data.texture_slots[i].get() == *texture.get()) {
                texture_index = (float)i;
                break;
            }
        }

        if (texture_index == 0) {
            if (renderer_data.texture_slot_index >= Renderer2DData::max_texture_slots)
                FlushAndReset();

            texture_index = (float)renderer_data.texture_slot_index;
            renderer_data.texture_slots[renderer_data.texture_slot_index] = texture;
            renderer_data.texture_slot_index++;
        }

        for (size_t i = 0; i < quad_vertex_count; i++) {
            renderer_data.quad_vertex_ptr->position = transform * renderer_data.quad_vertex_positioning[i];
            renderer_data.quad_vertex_ptr->color = color;
            renderer_data.quad_vertex_ptr->tex_coord = texture_coords[i];
            renderer_data.quad_vertex_ptr->tex_index = texture_index;
            renderer_data.quad_vertex_ptr->tiling_factor = tiling_factor;
            renderer_data.quad_vertex_ptr->id = entity_id;
            renderer_data.quad_vertex_ptr++;
        }

        renderer_data.quad_index_count += 6;
        renderer_data.stats.quad_count++;
    }

    void Renderer2D::DrawQuad(const glm::mat4& transform,
            const Ref<Texture2D>& texture, const glm::vec4& color,
            int entity_id, float tiling_factor)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        constexpr glm::vec2 texture_coords[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f }};

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        float texture_index = 0;
        for (uint32_t i = 1; i < renderer_data.texture_slot_index; i++) {
            if (*renderer_data.texture_slots[i].get() == *texture.get()) {
                texture_index = (float)i;
                break;
            }
        }

        if (texture_index == 0) {
            if (renderer_data.texture_slot_index >= Renderer2DData::max_texture_slots)
                FlushAndReset();

            texture_index = (float)renderer_data.texture_slot_index;
            renderer_data.texture_slots[renderer_data.texture_slot_index] = texture;
            renderer_data.texture_slot_index++;
        }

        for (size_t i = 0; i < quad_vertex_count; i++) {
            renderer_data.quad_vertex_ptr->position = transform * renderer_data.quad_vertex_positioning[i];
            renderer_data.quad_vertex_ptr->color = color;
            renderer_data.quad_vertex_ptr->tex_coord = texture_coords[i];
            renderer_data.quad_vertex_ptr->tex_index = texture_index;
            renderer_data.quad_vertex_ptr->tiling_factor = tiling_factor;
            renderer_data.quad_vertex_ptr->id = entity_id;
            renderer_data.quad_vertex_ptr++;
        }

        renderer_data.quad_index_count += 6;
        renderer_data.stats.quad_count++;
    }

    void Renderer2D::DrawQuad(const glm::mat4& transform,
            const Ref<Subtexture2D>& subtexture, int entity_id,
            float tiling_factor)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        const glm::vec2* texture_coords = subtexture->GetCoords();
        const Ref<Texture2D>& texture = subtexture->GetTexture();

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        constexpr glm::vec4 color = glm::vec4(1.0f);

        float texture_index = 0;
        for (uint32_t i = 1; i < renderer_data.texture_slot_index; i++) {
            if (*renderer_data.texture_slots[i].get() == *texture.get()) {
                texture_index = (float)i;
                break;
            }
        }

        if (texture_index == 0) {
            if (renderer_data.texture_slot_index >= Renderer2DData::max_texture_slots)
                FlushAndReset();

            texture_index = (float)renderer_data.texture_slot_index;
            renderer_data.texture_slots[renderer_data.texture_slot_index] = texture;
            renderer_data.texture_slot_index++;
        }

        for (size_t i = 0; i < quad_vertex_count; i++) {
            renderer_data.quad_vertex_ptr->position = transform * renderer_data.quad_vertex_positioning[i];
            renderer_data.quad_vertex_ptr->color = color;
            renderer_data.quad_vertex_ptr->tex_coord = texture_coords[i];
            renderer_data.quad_vertex_ptr->tex_index = texture_index;
            renderer_data.quad_vertex_ptr->tiling_factor = tiling_factor;
            renderer_data.quad_vertex_ptr->id = entity_id;
            renderer_data.quad_vertex_ptr++;
        }

        renderer_data.quad_index_count += 6;
        renderer_data.stats.quad_count++;
    }

    void Renderer2D::DrawQuad(const glm::mat4& transform,
            const glm::vec4& color, int entity_id)
    {
        OE_PROFILE_FUNCTION();

        constexpr size_t quad_vertex_count = 4;
        constexpr glm::vec2 texture_coords[] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f }};

        if (renderer_data.quad_index_count >= Renderer2DData::max_indices)
            FlushAndReset();

        for (size_t i = 0; i < quad_vertex_count; i++) {
            renderer_data.quad_vertex_ptr->position = transform * renderer_data.quad_vertex_positioning[i];
            renderer_data.quad_vertex_ptr->color = color;
            renderer_data.quad_vertex_ptr->tex_coord = texture_coords[i];
            renderer_data.quad_vertex_ptr->tex_index = 0;
            renderer_data.quad_vertex_ptr->tiling_factor = 1.0f;
            renderer_data.quad_vertex_ptr->id = entity_id;
            renderer_data.quad_vertex_ptr++;
        }

        renderer_data.quad_index_count += 6;
        renderer_data.stats.quad_count++;
    }


    void Renderer2D::ResetStats()
    {
        memset(&renderer_data.stats, 0, sizeof(Statistics));
    }

    const Statistics& Renderer2D::GetStats()
    {
        return renderer_data.stats;
    }

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

        renderer_data.quad_index_count = 0;
        renderer_data.quad_vertex_ptr = renderer_data.quad_vertex_base;

        renderer_data.texture_slot_index = 1; 
    }

    void Renderer2D::StartBatch()
	{
		renderer_data.quad_index_count = 0;
		renderer_data.quad_vertex_ptr = renderer_data.quad_vertex_base;

		renderer_data.texture_slot_index = 1;
	}

    void Renderer2D::NextBatch()
	{
		Flush();
		StartBatch();
	}
}
