/* ************************************************************************** */
/*                                                                            */
/*                                                        ::::::::            */
/*   mlx_images.c                                       :+:    :+:            */
/*                                                     +:+                    */
/*   By: W2Wizard <w2.wizzard@gmail.com>              +#+                     */
/*                                                   +#+                      */
/*   Created: 2022/01/21 15:34:45 by W2Wizard      #+#    #+#                 */
/*   Updated: 2022/07/28 18:26:59 by sbos          ########   odam.nl         */
/*                                                                            */
/* ************************************************************************** */

#include "MLX42/MLX42_Int.h"

//= Private =//

void mlx_flush_batch(mlx_ctx_t* mlx)
{
	if (mlx->batch_size <= 0)
		return;

	glBindBuffer(GL_ARRAY_BUFFER, mlx->vbo);
	glBufferData(GL_ARRAY_BUFFER, mlx->batch_size * sizeof(vertex_t), mlx->batch_vertices, GL_STATIC_DRAW);
	glDrawArrays(GL_TRIANGLES, 0, mlx->batch_size);

	mlx->batch_size = 0;
	memset(mlx->bound_textures, 0, sizeof(mlx->bound_textures));
}

static int8_t mlx_bind_texture(mlx_ctx_t* mlx, mlx_image_t* img)
{
	const GLint handle = (GLint)((mlx_image_ctx_t*)img->context)->texture;

	// Attempt to bind the texture, or obtain the index if it is already bound.
	for (int8_t i = 0; i < 16; i++)
	{
		if (mlx->bound_textures[i] == handle)
			return (i);

		if (mlx->bound_textures[i] == 0)
		{
			mlx->bound_textures[i] = handle;

			glActiveTexture(GL_TEXTURE0 + i);
			glBindTexture(GL_TEXTURE_2D, handle);
			return (i);
		}
	}

	// If no free slot was found, flush the batch and assign the texture to the first available slot
	mlx_flush_batch(mlx);

	mlx->bound_textures[0] = handle;
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, handle);
	return (0);
}

/**
 * Internal function to draw a single instance of an image
 * to the screen.
 */
void mlx_draw_instance(mlx_ctx_t* mlx, mlx_image_t* img, mlx_instance_t* instance)
{
	float w = (float) img->width;
	float h = (float) img->height;
	float x = (float) instance->x;
	float y = (float) instance->y;
	float z = (float) instance->z;
	int8_t tex = mlx_bind_texture(mlx, img);

	vertex_t vertices[6] = {
			(vertex_t){x, y, z, 0.f, 0.f, tex},
			(vertex_t){x + w, y + h, z, 1.f, 1.f, tex},
			(vertex_t){x + w, y, z, 1.f, 0.f, tex},
			(vertex_t){x, y, z, 0.f, 0.f, tex},
			(vertex_t){x, y + h, z, 0.f, 1.f, tex},
			(vertex_t){x + w, y + h, z, 1.f, 1.f, tex},
	};
	memmove(mlx->batch_vertices + mlx->batch_size, vertices, sizeof(vertices));
	mlx->batch_size += 6;

	if (mlx->batch_size >= MLX_BATCH_SIZE)
		mlx_flush_batch(mlx);
}

mlx_instance_t* mlx_grow_instances(mlx_image_t* img, bool* did_realloc)
{
	mlx_image_ctx_t* const ctx = img->context;
	if (img->count >= ctx->instances_capacity)
	{
		if (ctx->instances_capacity == 0)
			ctx->instances_capacity = img->count;
		else
			ctx->instances_capacity *= 2;
		*did_realloc = true;
		return realloc(img->instances, ctx->instances_capacity * sizeof(mlx_instance_t));
	}
	*did_realloc = false;
	return img->instances;
}

//= Public =//

void mlx_set_instance_depth(mlx_instance_t* instance, int32_t zdepth)
{
	MLX_NONNULL(instance);

	if (instance->z == zdepth)
		return;
	instance->z = zdepth;

	/**
	 * NOTE: The reason why we don't sort directly is that
	 * the user might call this function multiple times in a row and we don't
	 * want to sort for every change. Pre-loop wise that is.
	 */
	sort_queue = true;
}

int32_t mlx_image_to_window(mlx_t* mlx, mlx_image_t* img, int32_t x, int32_t y)
{
	MLX_NONNULL(mlx);
	MLX_NONNULL(img);

	// Allocate buffers...
	img->count++;
	bool did_realloc;
	mlx_instance_t* instances = mlx_grow_instances(img, &did_realloc);
	draw_queue_t* queue = calloc(1, sizeof(draw_queue_t));
	if (!instances || !queue)
	{
		if (did_realloc)
			free(instances);
		return (free(queue), mlx_error(MLX_MEMFAIL), -1);
	}

	// Set data...
	queue->image = img;
	int32_t index = queue->instanceid = img->count - 1;
	img->instances = instances;
	img->instances[index].x = x;
	img->instances[index].y = y;

	// NOTE: We keep updating the Z for the convenience of the user.
	// Always update Z depth to prevent overlapping images by default.
	img->instances[index].z = ((mlx_ctx_t*)mlx->context)->zdepth++;
	img->instances[index].enabled = true;

	// Add draw call...
	sort_queue = true;
	mlx_list_t* templst;
	if ((templst = mlx_lstnew(queue)))
	{
		mlx_lstadd_front(&((mlx_ctx_t*)mlx->context)->render_queue, templst);
		return (index);
	}
	return (mlx_freen(2, instances, queue), mlx_error(MLX_MEMFAIL), -1);
}

mlx_image_t* mlx_new_image(mlx_t* mlx, uint32_t width, uint32_t height)
{
	MLX_NONNULL(mlx);

	if (!width || !height || width > INT16_MAX || height > INT16_MAX)
		return ((void*)mlx_error(MLX_INVDIM));

	const mlx_ctx_t* mlxctx = mlx->context;
	mlx_image_t* newimg = calloc(1, sizeof(mlx_image_t));
	mlx_image_ctx_t* newctx = calloc(1, sizeof(mlx_image_ctx_t));
	if (!newimg || !newctx)
	{
		mlx_freen(2, newimg, newctx);
		return ((void *)mlx_error(MLX_MEMFAIL));
	}
	newimg->enabled = true;
	newimg->context = newctx;
	(*(uint32_t*)&newimg->width) = width;
	(*(uint32_t*)&newimg->height) = height;
	if (!(newimg->pixels = calloc(width * height, sizeof(int32_t))))
	{
		mlx_freen(2, newimg, newctx);
		return ((void *)mlx_error(MLX_MEMFAIL));
	}

	mlx_list_t* newentry;
	if (!(newentry = mlx_lstnew(newimg)))
	{
		mlx_freen(3, newimg->pixels, newimg->context, newimg);
		return ((void *)mlx_error(MLX_MEMFAIL));
	}

	// Generate OpenGL texture
	glGenTextures(1, &newctx->texture);
	glBindTexture(GL_TEXTURE_2D, newctx->texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	mlx_lstadd_front((mlx_list_t**)(&mlxctx->images), newentry);
	return (newimg);
}

void mlx_delete_image(mlx_t* mlx, mlx_image_t* image)
{
	MLX_NONNULL(mlx);
	MLX_NONNULL(image);

	mlx_ctx_t* mlxctx = mlx->context;

	// Delete all instances in the render queue
	mlx_list_t* quelst;
	while ((quelst = mlx_lstremove(&mlxctx->render_queue, image, &mlx_equal_inst)))
		mlx_freen(2, quelst->content, quelst);

	mlx_list_t* imglst;
	if ((imglst = mlx_lstremove(&mlxctx->images, image, &mlx_equal_image)))
	{
		glDeleteTextures(1, &((mlx_image_ctx_t*)image->context)->texture);
		mlx_freen(5, image->pixels, image->instances, image->context, imglst, image);
	}
}

bool mlx_resize_image(mlx_image_t* img, uint32_t nwidth, uint32_t nheight)
{
	MLX_NONNULL(img);

	if (!nwidth || !nheight || nwidth > INT16_MAX || nheight > INT16_MAX)
		return (mlx_error(MLX_INVDIM));
	if (nwidth != img->width || nheight != img->height)
	{
		uint8_t* tempbuff = realloc(img->pixels, (nwidth * nheight) * BPP);
		if (!tempbuff)
			return (mlx_error(MLX_MEMFAIL));
		img->pixels = tempbuff;
		(*(uint32_t*)&img->width) = nwidth;
		(*(uint32_t*)&img->height) = nheight;
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nwidth, nheight, 0, GL_RGBA, GL_UNSIGNED_BYTE, img->pixels);
	}
	return (true);
}
