#include <cstdint>
#include <pch.hpp>

#include <core.hpp>
#include <glad/glad.h>
#include <opengl/opengl_framebuffer.hpp>
#include <renderer/framebuffer.hpp>


namespace OpenEngine {

    static const uint32_t max_framebuffer_size = 8192;

    namespace Utils {

        static bool IsDepthFormat(FramebufferTextureFormat format)
        {
            switch (format) {
                case FramebufferTextureFormat::DEPTH24STENCIL8: return true;
            }

            return false;
        }

        static GLenum TextureTarget(bool multisampled)
        {
            return multisampled ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D;
        }

        static void CreateTexture(bool multisampled, uint32_t* out_id,
                uint32_t count)
        {
            glCreateTextures(TextureTarget(multisampled), count, out_id);
        }

        static void BindTexture(bool multisampled, uint32_t id)
        {
            glBindTexture(TextureTarget(multisampled), id);
        }

        static void AttachColorTexture(uint32_t id, int samples,
                GLenum internal_format, GLenum format,
                uint32_t width, uint32_t height, int index)
        {
            bool multisampled = samples > 1;
            if (multisampled) {
                glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples,
                        internal_format, width, height, GL_FALSE);
            } else {
                glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height,
                        0, format, GL_UNSIGNED_BYTE, nullptr);

                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            }

            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + index,
                    TextureTarget(multisampled), id, 0);
        }

        static void AttachDepthTexture(uint32_t id, int samples, GLenum format,
                GLenum attachment_type, uint32_t width, uint32_t height)
        {
            bool multisampled = samples > 1;
            if (multisampled) {
                glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples,
                        format, width, height, GL_FALSE);
            } else {
                glTexStorage2D(GL_TEXTURE_2D, 1, format, width, height);

                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            }

            glFramebufferTexture2D(GL_FRAMEBUFFER, attachment_type,
                    TextureTarget(multisampled), id, 0);
        }

    }

	OpenGLFramebuffer::OpenGLFramebuffer(const FramebufferSpecification& spec)
		: specs(spec)
	{
        for (auto format : specs.attachments.attachments) {
            if (!Utils::IsDepthFormat(format.texture_format))
                color_attachment_specs.emplace_back(format);
            else
                depth_attachment_specs = format.texture_format;
        }
		Invalidate();
	}

	OpenGLFramebuffer::~OpenGLFramebuffer()
	{
		glDeleteFramebuffers(1, &id);
        glDeleteTextures(color_attachment_ids.size(), color_attachment_ids.data());
        glDeleteTextures(1, &depth_attachment_id);
	}

	void OpenGLFramebuffer::Invalidate()
	{
        if (id) {
            glDeleteFramebuffers(1, &id);
            glDeleteTextures(color_attachment_ids.size(), color_attachment_ids.data());
            glDeleteTextures(1, &depth_attachment_id);
            
            color_attachment_ids.clear();
            depth_attachment_id = 0;
        }

		glCreateFramebuffers(1, &id);
		glBindFramebuffer(GL_FRAMEBUFFER, id);
		
        bool multisample = specs.samples > 1;

        // Attachments
        if (color_attachment_specs.size()) {
            color_attachment_ids.resize(color_attachment_specs.size());
            Utils::CreateTexture(multisample, color_attachment_ids.data(), color_attachment_ids.size());

            for (size_t i = 0; i < color_attachment_specs.size(); i++) {
                Utils::BindTexture(multisample, color_attachment_ids[i]);
                switch (color_attachment_specs[i].texture_format) {
                    case FramebufferTextureFormat::RGBA8:
                        Utils::AttachColorTexture(color_attachment_ids[i],
                                specs.samples, GL_RGBA8, GL_RGBA,
                                specs.width, specs.height, i);
                        break;
                    case FramebufferTextureFormat::RED_INTEGER:
                        Utils::AttachColorTexture(color_attachment_ids[i],
                                specs.samples, GL_R32I, GL_RED_INTEGER,
                                specs.width, specs.height, i);
                        break;
                }
            }
        }

        if (depth_attachment_specs.texture_format != FramebufferTextureFormat::None) {
            Utils::CreateTexture(multisample, &depth_attachment_id, 1);
            Utils::BindTexture(multisample, depth_attachment_id);

            switch (depth_attachment_specs.texture_format) {
                case OpenEngine::FramebufferTextureFormat::DEPTH24STENCIL8:
                    Utils::AttachDepthTexture(depth_attachment_id, specs.samples,
                            GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT,
                            specs.width, specs.height);
                    break;
            }
        }

        if (color_attachment_ids.size() > 1) {
            OE_CORE_ASSERT(color_attachment_ids.size() < 4, "Only 4 color attachments supported.");
            GLenum buffers[4] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 };
            glDrawBuffers(color_attachment_ids.size(), buffers);
        } else if (color_attachment_ids.empty()) {
            glDrawBuffer(GL_NONE);
        }

		OE_CORE_ASSERT(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Framebuffer is incomplete!");

		glBindFramebuffer(GL_FRAMEBUFFER, 0);
	}

	void OpenGLFramebuffer::Bind()
	{
		glBindFramebuffer(GL_FRAMEBUFFER, id);
        glViewport(0, 0, specs.width, specs.height);
	}

	void OpenGLFramebuffer::Unbind()
	{
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
	}

    void OpenGLFramebuffer::Resize(uint32_t width, uint32_t height)
    {
        if (width == 0 || height == 0 || width > max_framebuffer_size || height > max_framebuffer_size) {
            OE_CORE_WARN("Attempted to resize framebuffer to {} {}", width, height);
            return;
        }
        specs.width = width;
        specs.height = height;

        Invalidate();
    }

    int OpenGLFramebuffer::ReadPixel(uint32_t index, int x, int y)
    {
        OE_CORE_ASSERT(index < color_attachment_ids.size(), "Index is greater than attachment count.");

        glReadBuffer(GL_COLOR_ATTACHMENT0 + index);

        int pixel_data;
        glReadPixels(x, y, 1, 1, GL_RED_INTEGER, GL_INT, &pixel_data);

        return pixel_data;
    }
    void OpenGLFramebuffer::ClearBufferI(int index, int value)
    {
        glClearTexImage(color_attachment_ids[index], 0, GL_RED_INTEGER, GL_INT, &value);
    }
}
