/*
 * This software is licensed under the terms of the MIT License.
 * See COPYING for further information.
 * ---
 * Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
 * Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
 */

#include "framebuffer.h"

#include "../glcommon/debug.h"
#include "gl33.h"
#include "texture.h"

GLuint r_attachment_to_gl_attachment[] = {
	[FRAMEBUFFER_ATTACH_DEPTH] = GL_DEPTH_ATTACHMENT,
	[FRAMEBUFFER_ATTACH_COLOR0] = GL_COLOR_ATTACHMENT0,
	[FRAMEBUFFER_ATTACH_COLOR1] = GL_COLOR_ATTACHMENT1,
	[FRAMEBUFFER_ATTACH_COLOR2] = GL_COLOR_ATTACHMENT2,
	[FRAMEBUFFER_ATTACH_COLOR3] = GL_COLOR_ATTACHMENT3,
};

static_assert(sizeof(r_attachment_to_gl_attachment)/sizeof(GLuint) == FRAMEBUFFER_MAX_ATTACHMENTS, "");

Framebuffer *gl33_framebuffer_create(void) {
	auto fb = ALLOC(Framebuffer);
	glGenFramebuffers(1, &fb->gl_fbo);
	snprintf(fb->debug_label, sizeof(fb->debug_label), "FBO #%i", fb->gl_fbo);

	for(int i = 0; i < FRAMEBUFFER_MAX_OUTPUTS; ++i) {
		fb->output_mapping[i] = FRAMEBUFFER_ATTACH_COLOR0 + i;
	}

	// According to the GL spec, the FBO is only actually created when it's first bound.
	// Let's make sure this happens as early as possible.
	Framebuffer *prev_fb = r_framebuffer_current();
	r_framebuffer(fb);
	gl33_sync_framebuffer();
	r_framebuffer(prev_fb);

	return fb;
}

attr_unused static inline char *attachment_str(FramebufferAttachment a) {
	#define A(x) case x: return #x;
	switch(a) {
		A(FRAMEBUFFER_ATTACH_COLOR0)
		A(FRAMEBUFFER_ATTACH_COLOR1)
		A(FRAMEBUFFER_ATTACH_COLOR2)
		A(FRAMEBUFFER_ATTACH_COLOR3)
		A(FRAMEBUFFER_ATTACH_DEPTH)
		default: UNREACHABLE;
	}
	#undef A
}

void gl33_framebuffer_attach(Framebuffer *framebuffer, Texture *tex, uint mipmap, FramebufferAttachment attachment) {
	assert(attachment >= 0 && attachment < FRAMEBUFFER_MAX_ATTACHMENTS);
	assert(!tex || mipmap < tex->params.mipmaps);
	assert(tex || mipmap == 0);

	GLuint gl_tex = tex ? tex->gl_handle : 0;
	Framebuffer *prev_fb = r_framebuffer_current();

	// gl33_sync_framebuffer will call gl33_framebuffer_prepare; we don't want it to touch draw buffers yet.
	framebuffer->draw_buffers_dirty = false;

	r_framebuffer(framebuffer);
	gl33_sync_framebuffer();
	glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, r_attachment_to_gl_attachment[attachment], GL_TEXTURE_2D, gl_tex, mipmap);
	r_framebuffer(prev_fb);

	framebuffer->attachments[attachment] = tex;
	framebuffer->attachment_mipmaps[attachment] = mipmap;

	// need to update draw buffers
	framebuffer->draw_buffers_dirty = true;

	IF_DEBUG(if(tex) {
		log_debug("%s %s = %s (%ux%u mip %u)", framebuffer->debug_label, attachment_str(attachment), tex->debug_label, tex->params.width, tex->params.height, mipmap);
	} else {
		log_debug("%s %s = NULL", framebuffer->debug_label, attachment_str(attachment));
	});
}

FramebufferAttachmentQueryResult gl33_framebuffer_query_attachment(Framebuffer *framebuffer, FramebufferAttachment attachment) {
	assert(attachment >= 0 && attachment < FRAMEBUFFER_MAX_ATTACHMENTS);
	return (FramebufferAttachmentQueryResult) {
		.texture = framebuffer->attachments[attachment],
		.miplevel = framebuffer->attachment_mipmaps[attachment],
	};
}

void gl33_framebuffer_outputs(
	Framebuffer *framebuffer,
	FramebufferAttachment config[FRAMEBUFFER_MAX_OUTPUTS],
	uint8_t write_mask
) {
	if(write_mask == 0x00) {
		memcpy(config, framebuffer->output_mapping, sizeof(framebuffer->output_mapping));
		return;
	}

	for(int i = 0; i < FRAMEBUFFER_MAX_OUTPUTS; ++i) {
		if(write_mask & (1 << i) && config[i] != framebuffer->output_mapping[i]) {
			if(config[i] != FRAMEBUFFER_ATTACH_NONE) {
				assert(config[i] >= FRAMEBUFFER_ATTACH_COLOR0);
				assert(config[i] < FRAMEBUFFER_ATTACH_COLOR0 + FRAMEBUFFER_MAX_COLOR_ATTACHMENTS);
			}

			framebuffer->output_mapping[i] = config[i];
			framebuffer->draw_buffers_dirty = true;
		}
	}
}

void gl33_framebuffer_destroy(Framebuffer *framebuffer) {
	gl33_framebuffer_deleted(framebuffer);
	glDeleteFramebuffers(1, &framebuffer->gl_fbo);
	mem_free(framebuffer);
}

void gl33_framebuffer_taint(Framebuffer *framebuffer) {
	for(uint i = 0; i < FRAMEBUFFER_MAX_ATTACHMENTS; ++i) {
		if(framebuffer->attachments[i] != NULL) {
			gl33_texture_taint(framebuffer->attachments[i]);
		}
	}
}

void gl33_framebuffer_prepare(Framebuffer *framebuffer) {
	if(framebuffer->draw_buffers_dirty) {
		// NOTE: this framebuffer is guaranteed to be bound at this point

		if(glext.draw_buffers) {
			GLenum drawbufs[FRAMEBUFFER_MAX_OUTPUTS];

			for(int i = 0; i < FRAMEBUFFER_MAX_OUTPUTS; ++i) {
				FramebufferAttachment a = framebuffer->output_mapping[i];

				if(a == FRAMEBUFFER_ATTACH_NONE || framebuffer->attachments[a] == NULL) {
					drawbufs[i] = GL_NONE;
				} else {
					drawbufs[i] = GL_COLOR_ATTACHMENT0 + (a - FRAMEBUFFER_ATTACH_COLOR0);
				}
			}

			glDrawBuffers(FRAMEBUFFER_MAX_OUTPUTS, drawbufs);
		} else {
			// TODO: maybe simulate this by modifying framebuffer attachments?
		}

		framebuffer->draw_buffers_dirty = false;
	}
}

void gl33_framebuffer_set_debug_label(Framebuffer *fb, const char *label) {
	glcommon_set_debug_label(fb->debug_label, "FBO", GL_FRAMEBUFFER, fb->gl_fbo, label);
}

const char *gl33_framebuffer_get_debug_label(Framebuffer* fb) {
	return fb->debug_label;
}

static GLuint buffer_flags_to_gl(BufferKindFlags flags) {
	GLuint glflags = 0;

	if(flags & BUFFER_COLOR) {
		glflags |= GL_COLOR_BUFFER_BIT;
	}

	if(flags & BUFFER_DEPTH) {
		glflags |= GL_DEPTH_BUFFER_BIT;
	}

	return glflags;
}

void gl33_framebuffer_clear(Framebuffer *framebuffer, BufferKindFlags flags, const Color *colorval, float depthval) {
	GLuint glflags = buffer_flags_to_gl(flags);

	if(flags & BUFFER_COLOR) {
		assert(colorval != NULL);
		gl33_set_clear_color(colorval);
	}

	if(flags & BUFFER_DEPTH) {
		gl33_set_clear_depth(depthval);
	}

	r_flush_sprites();

	Framebuffer *fb_saved = r_framebuffer_current();
	r_framebuffer(framebuffer);
	gl33_sync_framebuffer();
	gl33_sync_scissor();
	glClear(glflags);
	r_framebuffer(fb_saved);
}

void gl33_framebuffer_copy(Framebuffer *dst, Framebuffer *src, BufferKindFlags flags) {
	GLuint glflags = buffer_flags_to_gl(flags);

	r_flush_sprites();

	IntExtent size = r_framebuffer_get_size(dst);
	GLint X0 = 0;
	GLint X1 = size.w;
	GLint Y0 = 0;
	GLint Y1 = size.h;

	Framebuffer *fb_saved = r_framebuffer_current();
	r_framebuffer(dst);
	gl33_sync_framebuffer();
	gl33_sync_scissor();
	// TODO track this?
	glBindFramebuffer(GL_READ_FRAMEBUFFER, src->gl_fbo);
	glBlitFramebuffer(X0, Y0, X1, Y1, X0, Y0, X1, Y1, glflags, GL_NEAREST);
	r_framebuffer(fb_saved);
}

IntExtent gl33_framebuffer_get_effective_size(Framebuffer *framebuffer) {
	// According to the OpenGL wiki:
	// "The effective size of the FBO is the intersection of all of the sizes of the bound images (ie: the smallest in each dimension)."

	// TODO: do it once in gl33_framebuffer_prepare and cache the result

	IntExtent fb_size = { 0, 0 };

	for(int i = 0; i < FRAMEBUFFER_MAX_ATTACHMENTS; ++i) {
		Texture *tex = framebuffer->attachments[i];

		if(tex != NULL) {
			uint mipmap = framebuffer->attachment_mipmaps[i];
			uint tex_w, tex_h;
			gl33_texture_get_size(tex, mipmap, &tex_w, &tex_h);
			IntExtent tex_size = { tex_w, tex_h };

			if(fb_size.w == 0 && fb_size.h == 0) {
				fb_size = tex_size;
			} else if(tex_size.w < fb_size.w) {
				fb_size.w = tex_size.w;
			} else if(tex_size.h < fb_size.h) {
				fb_size.h = tex_size.h;
			}
		}
	}

	return fb_size;
}

GLTextureFormatInfo *gl33_framebuffer_get_format(
	Framebuffer *framebuffer, FramebufferAttachment attachment
) {
	GLTextureFormatInfo *fmtinfo = NULL;

	if(framebuffer) {
		FramebufferAttachmentQueryResult aq = gl33_framebuffer_query_attachment(framebuffer, attachment);
		fmtinfo = NOT_NULL(aq.texture)->fmt_info;
	} else {
		assert(attachment == FRAMEBUFFER_ATTACH_NONE);

		GLenum ifmt = GL_RGBA8;  // FIXME how to query this?

		auto formats = glcommon_get_texture_formats();
		dynarray_foreach_elem(formats, auto fmt, {
			if(fmt->internal_format == ifmt) {
				fmtinfo = fmt;
				break;
			}
		});
	}

	return NOT_NULL(fmtinfo);
}

void gl33_framebuffer_bind_for_read(
	Framebuffer *framebuffer, FramebufferAttachment attachment
) {
	if(framebuffer) {
		assert(attachment >= 0 && attachment < FRAMEBUFFER_MAX_ATTACHMENTS);
		glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer->gl_fbo);
		glReadBuffer(r_attachment_to_gl_attachment[attachment]);
	} else {
		assert(attachment == FRAMEBUFFER_ATTACH_NONE);
		glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
		glReadBuffer(GL_BACK);
	}
}
