#include "../external/glad/src/gl.c"

#include "assets.cpp"

// ::Globals::

PipelineCreateInfo g_Pipeline_Create_Info[] = {
	{ .vertex_shader = "shaders/pbr.vert", .fragment_shader = "shaders/pbr.frag" },
	//{ .vertex_shader = "shaders/gui.vert", .fragment_shader = "shaders/gui.frag" },
};

// ::Init::Impl::Start::

rIndexedBuffer yoder;

static b32
Init(Renderer *renderer)
{
	b32 success = true;

	renderer->perm_arena = ArenaCreate(MB(32));
	
	for (u64 i = 0; i < FRAME_OVERLAP; i += 1)
	{
		renderer->frame_arenas[i] = ArenaCreate(MB(16));
	}

	RGFW_setGLHint(RGFW_glMajor, 4);
	RGFW_setGLHint(RGFW_glMinor, 6);
	RGFW_setGLHint(RGFW_glProfile, RGFW_glCore);

	renderer->window = RGFW_createWindow("Video Game", RGFW_RECT(0, 0, 1280, 720), RGFW_windowCenter | RGFW_windowNoResize);
	if (renderer->window == NULL)
	{
		Printfln("Failed to create window");
		success = false;
	}

	if (success)
	{
		RGFW_window_makeCurrent(renderer->window);

		int glad_version = gladLoaderLoadGL();
		if (!glad_version)
		{
			Printfln("gladLoaderLoadGL failure");
			success = false;
		}
	}

#ifdef BUILD_DEBUG
	glEnable(GL_DEBUG_OUTPUT);
	glDebugMessageCallback(DebugCallback, NULL);
#endif

	for (u32 i = 0; i < PIPELINE_MAX; i += 1)
	{
		renderer->pipelines[i] = BuildPipeline(g_Pipeline_Create_Info + i);
	}

	glCreateBuffers(1, &renderer->mat_buf);
	apMatProps props = {0};
	glNamedBufferData(renderer->mat_buf, sizeof(apMatProps), &props, GL_STATIC_DRAW);

	InitModels(renderer);

	return success;
}

static void
InitModels(Renderer *renderer)
{
	Arena *arena = renderer->perm_arena;

	for (u64 i = 0; i < MODEL_ASSET_MAX; i += 1)
	{
		u64 hash = g_Model_Asset_Hashes[i];
		Asset asset = apLoadWithHash(hash);
		Assert(asset.model != NULL, "InitModels failure: Model loaded is NULL");

		apModel *a_model = asset.model;
		rModel *model = renderer->models + i;

		model->textures.length = a_model->textures.length;
		model->textures.data = (u32 *)ArenaAlloc(arena, sizeof(u32) * model->textures.length);

		for (u64 j = 0; j < model->textures.length; j += 1)
		{
			apTexData *t_data = a_model->textures.data + j;
			model->textures.data[j] = CreateTexture(t_data->bytes, t_data->w, t_data->h, t_data->ch);
			Assert(model->textures.data[j] != GL_NONE, "InitModels failure: texture data is GL_NONE");
		}

		apVertexArray v_arr = a_model->vertices;
		u32Array i_arr = a_model->indices;
		model->buffer = CreateIndexedBuffer(v_arr.data, v_arr.length, i_arr.data, i_arr.length);


		// TODO: placeholder/error material
		model->meshes.length = a_model->meshes.length;
		model->meshes.data = (rMesh *)ArenaAlloc(arena, sizeof(rMesh) * a_model->meshes.length);
		for (u64 i = 0; i < model->meshes.length; i += 1)
		{
			apMesh *a_mesh = a_model->meshes.data + i;
			rMesh *mesh = model->meshes.data + i;

			mesh->length = a_mesh->length;
			mesh->idx_offset = uintptr(a_mesh->start_idx * sizeof(u32));

			mesh->type = PPT_NONE;
			if (a_mesh->mat_idx != UINT32_MAX)
			{
				Printfln("mesh: %llu mat_idx: %llu", i, a_mesh->mat_idx);
				apMaterial *mat = a_model->materials.data + a_mesh->mat_idx;

				mesh->type |= mat->tex.albedo != UINT32_MAX ? PPT_ALBEDO_TEX : PPT_ALBEDO_VALUE;
				mesh->type |= mat->tex.ambient != UINT32_MAX ? PPT_AMBIENT_TEX : PPT_AMBIENT_VALUE;
				mesh->type |= mat->tex.specular != UINT32_MAX ? PPT_SPECULAR_TEX : PPT_SPECULAR_VALUE;

				mesh->values = mat->props;
				mesh->textures.albedo = mat->tex.albedo != UINT32_MAX ? model->textures.data[mat->tex.albedo] : 0;
				mesh->textures.ambient = mat->tex.ambient != UINT32_MAX ? model->textures.data[mat->tex.ambient] : 0;
				mesh->textures.specular = mat->tex.specular != UINT32_MAX ? model->textures.data[mat->tex.specular] : 0;
			}
		}
	}
}

static PipelineHandle 
BuildPipeline(PipelineCreateInfo *create_info)
{
	auto vertex_shader = CreateShader(GL_VERTEX_SHADER);
	Assert(BuildShader(&vertex_shader, create_info->vertex_shader), "BuildShader vertex failure");

	auto fragment_shader = CreateShader(GL_FRAGMENT_SHADER);
	Assert(BuildShader(&fragment_shader, create_info->fragment_shader), "BuildShader fragment failure");

	auto pipeline = CreatePipeline();
	glAttachShader(pipeline, vertex_shader);
	glAttachShader(pipeline, fragment_shader);
	glLinkProgram(pipeline);

	int success = 0;
	glGetProgramiv(pipeline, GL_LINK_STATUS, &success);
	if (!success)
	{
		char info_log[512];
		glGetProgramInfoLog(pipeline, 512, NULL, info_log);
		Assert(false, "Failed to build pipeline");
	}

	return pipeline;
}

static b32 
BuildShader(ShaderHandle *handle, char *asset)
{
	b32 success = true;

	Asset data = apLoad(asset);
	Assert(data.bytes != NULL && asset, "Shader data is NULL");

	glShaderBinary(1, handle, GL_SHADER_BINARY_FORMAT_SPIR_V, data.bytes, data.len);
	glSpecializeShader(*handle, "main", 0, 0, 0);

	int compiled = 0;
	glGetShaderiv(*handle, GL_COMPILE_STATUS, &compiled);
	if (!compiled)
	{
		char info_log[512];
		glGetShaderInfoLog(*handle, 512, NULL, info_log);
		Printfln("Unable to compile shader: %s", info_log);
		success = false;
	}

	return success;
}

static PipelineHandle 
CreatePipeline()
{
	PipelineHandle pipeline = glCreateProgram();
	return pipeline;
}

static VertexBuffer 
CreateVertexBuffer()
{
	VertexBuffer buf;
	glGenBuffers(1, &buf);

	return buf;
}

static IndexBuffer 
CreateIndexBuffer()
{
	IndexBuffer buf;
	glGenBuffers(1, &buf);

	return buf;
}

static VertexArray
CreateVertexArray()
{
	VertexArray array;
	glGenVertexArrays(1, &array);

	return array;
}

static ShaderHandle 
CreateShader(int type)
{
	return (ShaderHandle)glCreateShader(type);
}

// ::Init::Impl::End::


// ::Draw::Impl::Start::

static void
rDrawModel(Renderer *renderer, rModel *model)
{
	glUseProgram(renderer->pipelines[PIPELINE_PBR]);

	for (u64 i = 0; i < model->meshes.length; i += 1)
	{
		rMesh *mesh = model->meshes.data + i;

		u32 albedo = mesh->textures.albedo != UINT32_MAX ? mesh->textures.albedo : 0;
		glBindTextureUnit(0, albedo);

		u32 ambient = mesh->textures.ambient != UINT32_MAX ? mesh->textures.ambient : 0;
		glBindTextureUnit(1, ambient);

		u32 specular = mesh->textures.specular != UINT32_MAX ? mesh->textures.ambient : 0;
		glBindTextureUnit(2, specular);

		glNamedBufferSubData(renderer->mat_buf, 0, sizeof(apMatProps), &mesh->values);

		glBindBufferBase(GL_UNIFORM_BUFFER, 0, renderer->mat_buf);

		glBindVertexArray(model->buffer.vao);
		glDrawElements(GL_TRIANGLES, mesh->length, GL_UNSIGNED_INT, (void *)(model->buffer.ioffset + mesh->idx_offset));
	}
}

static void
DrawIndexedBuffer(rIndexedBuffer *buffer)
{
	glBindVertexArray(buffer->vao);
	glDrawElements(GL_TRIANGLES, buffer->icount, GL_UNSIGNED_INT, (void *)buffer->ioffset);
}

// ::Draw::Impl::End::


// ::Buffers::Impl::Start::

static rIndexedBuffer
CreateIndexedBuffer(rawptr vert_data, u32 vcount, rawptr idx_data, u32 icount)
{
	i32 alignment = GL_NONE;
	glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &alignment);

	u32 vao = GL_NONE;
	u32 buffer = GL_NONE;

	isize vert_len = isize(vcount * sizeof(apVertex));
	isize idx_len = isize(icount * sizeof(u32));

	isize vert_len_aligned = Align(vert_len, alignment);
	isize idx_len_aligned = Align(idx_len, alignment);

#if BUILD_DEBUG
	Assert((vert_len_aligned % alignment) == 0, "Buffers are not aligned");
	Assert((idx_len_aligned % alignment) == 0, "Buffers are not aligned");
#endif

	isize vert_offset = 0;
	isize idx_offset = vert_len_aligned;

	glCreateBuffers(1, &buffer);
	glNamedBufferStorage(buffer, idx_len_aligned + vert_len_aligned, NULL, GL_DYNAMIC_STORAGE_BIT);

	glNamedBufferSubData(buffer, vert_offset, vert_len, vert_data);
	glNamedBufferSubData(buffer, idx_offset, idx_len, idx_data);

	glCreateVertexArrays(1, &vao);

	constexpr u32 pos_size = SizeOfMember(apVertex, pos) / sizeof(f32);
	SetVertexAttrib(vao, 0, pos_size, GL_FLOAT, GL_FALSE, offsetof(apVertex, pos));

	constexpr u32 normal_size = SizeOfMember(apVertex, normal) / sizeof(f32);
	SetVertexAttrib(vao, 1, normal_size, GL_FLOAT, GL_FALSE, offsetof(apVertex, normal));

	constexpr u32 col_size = SizeOfMember(apVertex, color) / sizeof(u8);
	SetVertexAttrib(vao, 2, col_size, GL_UNSIGNED_BYTE, GL_FALSE, offsetof(apVertex, color));

	constexpr u32 uv_size = SizeOfMember(apVertex, uv) / sizeof(f32);
	SetVertexAttrib(vao, 3, uv_size, GL_FLOAT, GL_FALSE, offsetof(apVertex, uv));

	glVertexArrayVertexBuffer(vao, 0, buffer, vert_offset, sizeof(apVertex));
	glVertexArrayElementBuffer(vao, buffer);

	rIndexedBuffer indexed_buffer = {0};
	indexed_buffer.ioffset = idx_offset;
	indexed_buffer.icount = icount;
	indexed_buffer.vao = vao;
	indexed_buffer.buffer = buffer;

	return indexed_buffer;
}

static void
SetVertexAttrib(u32 vao, u32 idx, u32 count, i32 type, b32 normalized, u32 offset)
{
	glEnableVertexArrayAttrib(vao, idx);
	glVertexArrayAttribBinding(vao, idx, 0);
	glVertexArrayAttribFormat(vao, idx, count, type, normalized, offset);
}

// ::Buffers::Impl::End::



// ::Textures::Impl::Start::

static u32 
CreateTexture(rawptr data, u32 width, u32 height, u32 channels)
{
	u32 tex;
	glCreateTextures(GL_TEXTURE_2D, 1, &tex);

	i32 mipmap = Mini32(5, i32(log2f(f32(Maxi32(width, height)))));

	u32 format, copy_format;
	switch (channels) 
	{
	case 1:
		{
			format = GL_R8;
			copy_format = GL_RED;
		} break;
	case 2:
		{
			format = GL_RG8;
			copy_format = GL_RG;
		} break;
	case 3:
		{
			format = GL_RGB8;
			copy_format = GL_RGB;
		} break;
	case 4:
		{
			format = GL_RGBA8;
			copy_format = GL_RGBA;
		} break;
	default:
		{
			Assert(false, "CreateTexture failure: unsupported channel number passed in");
		} break;
	}

	glTextureStorage2D(tex, mipmap, format, width, height);
	glTextureSubImage2D(tex, 0, 0, 0, width, height, copy_format, GL_UNSIGNED_BYTE, data);

	glTextureParameteri(tex, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
	glTextureParameteri(tex, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTextureParameterf(tex, GL_TEXTURE_BASE_LEVEL, 0);
	glTextureParameteri(tex, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTextureParameteri(tex, GL_TEXTURE_WRAP_T, GL_REPEAT);

	glGenerateTextureMipmap(tex);

	return tex;
}

// ::Textures::Impl::End::

static b32
RunCycle(Renderer *renderer)
{
	while (RGFW_window_checkEvent(renderer->window));

	glUseProgram(renderer->pipelines[PIPELINE_PBR]);

	glClearColor(0.1f, 0.8f, 0.4f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	rDrawModel(renderer, renderer->models + 2);

	RGFW_window_swapBuffers(renderer->window);

	return !RGFW_window_shouldClose(renderer->window);
}



// ::Debug::Impl::Start::

void 
DebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, GLchar const* message, void const* user_param)
{
	return;
	if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return;

	const char *source_str, *type_str, *severity_str; 

	switch (source)
	{
		case GL_DEBUG_SOURCE_API: 
			{
				source_str = "API";
			} break;
		case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
			{
				source_str = "WINDOW SYSTEM";
			} break;
		case GL_DEBUG_SOURCE_SHADER_COMPILER: 
			{
				source_str = "SHADER COMPILER";
			} break;
		case GL_DEBUG_SOURCE_THIRD_PARTY: 
			{
				source_str = "THIRD PARTY";
			} break;
		case GL_DEBUG_SOURCE_APPLICATION:
			{
				source_str = "APPLICATION";
			} break;
		case GL_DEBUG_SOURCE_OTHER: 
			{
				source_str = "OTHER";
			} break;
		default:
			{
				source_str = "UNKNOWN";
			} break;
	}

	switch (type)
	{
		case GL_DEBUG_TYPE_ERROR:
			{
				type_str = "ERROR";
			} break;
		case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
			{
				type_str = "DEPRECATED BEHAVIOR";
			} break;
		case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
			{
				type_str = "UNDEFINED BEHAVIOR";
			} break;
		case GL_DEBUG_TYPE_PORTABILITY:
			{
				type_str = "PORTABILITY";
			} break;
		case GL_DEBUG_TYPE_PERFORMANCE:
			{
				type_str = "PERFORMANCE";
			} break;
		case GL_DEBUG_TYPE_MARKER:
			{
				type_str = "MARKER";
			} break;
		case GL_DEBUG_TYPE_OTHER:
			{
				type_str = "OTHER";
			} break;
		default:
			{
				type_str = "UNKNOWN";
			} break;
	}

	switch (severity)
	{
		case GL_DEBUG_SEVERITY_NOTIFICATION:
			{
				severity_str = "NOTIFICATION";
			} break;
		case GL_DEBUG_SEVERITY_LOW:
			{
				severity_str = "LOW";
			} break;
		case GL_DEBUG_SEVERITY_MEDIUM:
			{
				severity_str = "MEDIUM";
			} break;
		case GL_DEBUG_SEVERITY_HIGH:
			{
				severity_str = "HIGH";
			} break;
		default:
			{
				severity_str = "UNKNOWN";
			} break;
	}

	Printfln("Source: [%s] \nType: [%s] \nSeverity: [%s] \n%s", source_str, type_str, severity_str, message);
}

// ::Debug::Impl::End::
