/*
 * Copyright 2016 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#include <ox/std/assert.hpp>
#include <ox/std/istring.hpp>
#include <ox/std/trace.hpp>

#include "glutils/glutils.hpp"

namespace glutils {

void deleteBuffer(GLuint b) noexcept {
	glDeleteBuffers(1, &b);
}

void deleteFrameBuffer(GLuint b) noexcept {
	glDeleteFramebuffers(1, &b);
}

void deleteRenderBuffer(GLuint b) noexcept {
	glDeleteRenderbuffers(1, &b);
}

void deleteTexture(GLuint t) noexcept {
	glDeleteTextures(1, &t);
}

void deleteVertexArray(GLuint v) noexcept {
	glDeleteVertexArrays(1, &v);
}

void deleteProgram(GLuint p) noexcept {
	glDeleteProgram(p);
}

void deleteShader(GLuint s) noexcept {
	glDeleteShader(s);
}

template struct GLObject<deleteBuffer>;
template struct GLObject<deleteFrameBuffer>;
template struct GLObject<deleteRenderBuffer>;
template struct GLObject<deleteTexture, TextureBase>;
template struct GLObject<deleteVertexArray>;
template struct GLObject<deleteProgram>;
template struct GLObject<deleteShader>;

FrameBuffer const *FrameBufferBind::s_activeFb = nullptr;

FrameBufferBind::FrameBufferBind(FrameBuffer const &fb) noexcept: m_restoreFb(s_activeFb) {
	s_activeFb = &fb;
	glBindFramebuffer(GL_FRAMEBUFFER, fb);
	glViewport(0, 0, fb.width, fb.height);
}

FrameBufferBind::~FrameBufferBind() noexcept {
	s_activeFb = m_restoreFb;
	if (s_activeFb) {
		glBindFramebuffer(GL_FRAMEBUFFER, *s_activeFb);
		glViewport(0, 0, s_activeFb->width, s_activeFb->height);
	} else {
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
	}
}

void bind(FrameBuffer const &fb) noexcept {
	glBindFramebuffer(GL_FRAMEBUFFER, fb);
	glViewport(0, 0, fb.width, fb.height);
}


static ox::Result<GLShader> buildShader(
		GLuint const shaderType,
		GLchar const *src,
		ox::StringViewCR shaderName) noexcept {
	GLShader shader(glCreateShader(shaderType));
	glShaderSource(shader, 1, &src, nullptr);
	glCompileShader(shader);
	GLint status;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
	if (status != GL_TRUE) {
		ox::Vector<char> errMsg(ox::units::KB);
		glGetShaderInfoLog(shader, static_cast<GLsizei>(errMsg.size()), nullptr, errMsg.data());
		oxErrorf("shader compile error in {}: {}", shaderName, errMsg.data());
		return ox::Error(1, "shader compile error");
	}
	return shader;
}

ox::Result<GLProgram> buildShaderProgram(ProgramSource const &src) noexcept {
	OX_REQUIRE_M(program, buildShaderProgram(
			src.vertShader,
			src.fragShader,
			src.geomShader));
	setupShaderParams(program, src.shaderParams, src.rowLen);
	return std::move(program);
}

void setupShaderParams(
		GLProgram const &shader,
		ox::Vector<ShaderVarSet> const &vars,
		GLsizei vertexRowLen) noexcept {
	// setup vars
	for (size_t lenWritten = 0; auto const &v : vars) {
		auto const attr = static_cast<GLuint>(glGetAttribLocation(shader, v.name.c_str()));
		glEnableVertexAttribArray(attr);
		glVertexAttribPointer(
				attr, v.len, GL_FLOAT, GL_FALSE,
				vertexRowLen * static_cast<GLsizei>(sizeof(float)),
				std::bit_cast<void*>(uintptr_t{lenWritten * sizeof(float)}));
		lenWritten += static_cast<size_t>(v.len);
	}
}

void setupShaderParams(GLProgram const &shader, ox::Vector<ShaderVarSet> const &vars) noexcept {
	// get row len
	GLsizei vertexRowLen{};
	for (auto const &v : vars) {
		vertexRowLen += v.len;
	}
	setupShaderParams(shader, vars, vertexRowLen);
}

ox::Result<GLProgram> buildShaderProgram(
		ox::CStringView const &vert,
		ox::CStringView const &frag,
		ox::CStringView const &geo) noexcept {
	GLProgram prgm(glCreateProgram());
	OX_REQUIRE(vs, buildShader(GL_VERTEX_SHADER, vert.c_str(), "vshad"));
	glAttachShader(prgm, vs);
	if (geo.c_str() && geo.bytes() != 0) {
		OX_REQUIRE(gs, buildShader(GL_GEOMETRY_SHADER, geo.c_str(), "gshad"));
		glAttachShader(prgm, gs);
	}
	OX_REQUIRE(fs, buildShader(GL_FRAGMENT_SHADER, frag.c_str(), "fshad"));
	glAttachShader(prgm, fs);
	glLinkProgram(prgm);
	return prgm;
}

GLVertexArray generateVertexArrayObject() noexcept {
	GLVertexArray vao;
	glGenVertexArrays(1, &vao.id);
	return vao;
}

GLBuffer generateBuffer() noexcept {
	GLBuffer buff;
	glGenBuffers(1, &buff.id);
	return buff;
}

FrameBuffer generateFrameBuffer(int width, int height) noexcept {
	width = ox::max(1, width);
	height = ox::max(1, height);
	FrameBuffer fb;
	fb.width = width;
	fb.height = height;
	glGenFramebuffers(1, &fb.fbo.id);
	glBindFramebuffer(GL_FRAMEBUFFER, fb);
	// color texture
	glGenTextures(1, &fb.color.id);
	glBindTexture(GL_TEXTURE_2D, fb.color);
	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		GL_RGB,
		width,
		height,
		0,
		GL_RGB,
		GL_UNSIGNED_BYTE,
		nullptr);
	glGenerateMipmap(GL_TEXTURE_2D);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glFramebufferTexture2D(
		GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb.color, 0);
	// depth texture
	glGenRenderbuffers(1, &fb.depth.id);
	glBindRenderbuffer(GL_RENDERBUFFER, fb.depth);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
	glFramebufferRenderbuffer(
		GL_FRAMEBUFFER,
		GL_DEPTH_STENCIL_ATTACHMENT,
		GL_RENDERBUFFER,
		fb.depth);
	// verify FBO
	oxAssert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Frame Buffer is incomplete");
	// restore primary FB
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);
	return fb;
}

void resizeFrameBuffer(FrameBuffer &fb, int width, int height) noexcept {
	width = ox::max(1, width);
	height = ox::max(1, height);
	fb.width = width;
	fb.height = height;
	glBindFramebuffer(GL_FRAMEBUFFER, fb);
	// color texture
	glBindTexture(GL_TEXTURE_2D, fb.color);
	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		GL_RGB,
		width,
		height,
		0,
		GL_RGB,
		GL_UNSIGNED_BYTE,
		nullptr);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	// depth texture
	glBindRenderbuffer(GL_RENDERBUFFER, fb.depth);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
	// restore primary FB
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);
}

void resizeInitFrameBuffer(FrameBuffer &fb, int const width, int const height) noexcept {
	if (!fb) {
		fb = generateFrameBuffer(width, height);
		return;
	}
	resizeFrameBuffer(fb, width, height);
}

void resizeInitFrameBuffer(FrameBuffer &fb, ox::Size const &sz) noexcept {
	resizeInitFrameBuffer(fb, sz.width, sz.height);
}

void sendVbo(BufferSet const &bs) noexcept {
	auto const bufferSize = static_cast<GLsizeiptr>(sizeof(decltype(bs.vertices)::value_type) * bs.vertices.size());
	glBindBuffer(GL_ARRAY_BUFFER, bs.vbo);
	glBufferData(GL_ARRAY_BUFFER, bufferSize, bs.vertices.data(), GL_DYNAMIC_DRAW);
}

void sendEbo(BufferSet const &bs) noexcept {
	auto const bufferSize = static_cast<GLsizeiptr>(sizeof(decltype(bs.elements)::value_type) * bs.elements.size());
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bs.ebo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, bufferSize, bs.elements.data(), GL_STATIC_DRAW);
}

void clearScreen() noexcept {
	glClearColor(0, 0, 0, 1);
	glClear(GL_COLOR_BUFFER_BIT);
}

}
