/* ************************************************************************** */
/*                                                                            */
/*                                                        ::::::::            */
/*   mlx_init.c                                         :+:    :+:            */
/*                                                     +:+                    */
/*   By: W2Wizard <main@w2wizard.dev>                 +#+                     */
/*                                                   +#+                      */
/*   Created: 2021/12/28 00:24:30 by W2Wizard      #+#    #+#                 */
/*   Updated: 2023/06/08 18:16:19 by XEDGit        ########   odam.nl         */
/*                                                                            */
/* ************************************************************************** */

#include "MLX42/MLX42_Int.h"

//= Private =//

static void framebuffer_callback(GLFWwindow *window, int width, int height)
{
	(void)window;
	glViewport(0, 0, width, height);
}

static bool mlx_create_buffers(mlx_t* mlx)
{
	mlx_ctx_t* mlxctx = mlx->context;

	mlxctx->zdepth = 0;
	glActiveTexture(GL_TEXTURE0);
	glGenVertexArrays(1, &(mlxctx->vao));
	glGenBuffers(1, &(mlxctx->vbo));
	glBindVertexArray(mlxctx->vao);
	glBindBuffer(GL_ARRAY_BUFFER, mlxctx->vbo);

	// Vertex XYZ coordinates
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex_t), NULL);
	glEnableVertexAttribArray(0);

	// UV Coordinates
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)(sizeof(float) * 3));
	glEnableVertexAttribArray(1);

	// Texture index
	glVertexAttribIPointer(2, 1, GL_BYTE, sizeof(vertex_t), (void *)(sizeof(float) * 5));
	glEnableVertexAttribArray(2);

	glEnable(GL_BLEND);
	glEnable(GL_DEPTH_TEST);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture0"), 0);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture1"), 1);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture2"), 2);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture3"), 3);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture4"), 4);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture5"), 5);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture6"), 6);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture7"), 7);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture8"), 8);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture9"), 9);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture10"), 10);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture11"), 11);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture12"), 12);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture13"), 13);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture14"), 14);
	glUniform1i(glGetUniformLocation(mlxctx->shaderprogram, "Texture15"), 15);

	return (true);
}

/**
 * Compiles the given shader source code of a given shader type.
 * Returns shader object via param.
 *
 * @param code The shader source code.
 * @param Type GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER, ...
 * @return Non-zero on success, else 0.
 */
static uint32_t mlx_compile_shader(const char* code, int32_t type)
{
	GLuint shader;
	int32_t success;
	char infolog[512] = {0};

	if (!code || (shader = glCreateShader(type)) == 0)
		return (0);

	GLint len = strlen(code);
	glShaderSource(shader, 1, &code, &len);
	glCompileShader(shader);
	glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(shader, sizeof(infolog), NULL, infolog);
		fprintf(stderr, "%s", infolog);
		glDeleteShader(shader);
		return (0);
	}
	return (shader);
}

static bool mlx_init_render(mlx_t* mlx)
{
    uint32_t vshader = 0;
    uint32_t fshader = 0;
    char infolog[512] = {0};
    mlx_ctx_t* mlxctx = mlx->context;

    glfwMakeContextCurrent(mlx->window);
    glfwSetFramebufferSizeCallback(mlx->window, framebuffer_callback);
    glfwSetWindowUserPointer(mlx->window, mlx);
    glfwSwapInterval(MLX_SWAP_INTERVAL);

    // Load all OpenGL function pointers
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
        return (mlx_error(MLX_GLADFAIL));
    if (!(vshader = mlx_compile_shader(vert_shader, GL_VERTEX_SHADER)))
        return (mlx_error(MLX_VERTFAIL));
    if (!(fshader = mlx_compile_shader(frag_shader, GL_FRAGMENT_SHADER)))
        return (mlx_error(MLX_FRAGFAIL));;
    if (!(mlxctx->shaderprogram = glCreateProgram()))
    {
        glDeleteShader(fshader);
        glDeleteShader(vshader);
        return (mlx_error(MLX_SHDRFAIL));
    }
    glAttachShader(mlxctx->shaderprogram, vshader);
    glAttachShader(mlxctx->shaderprogram, fshader);
    glLinkProgram(mlxctx->shaderprogram);

    int32_t success;
    glGetProgramiv(mlxctx->shaderprogram, GL_LINK_STATUS, &success);
    if (!success)
    {
        glGetProgramInfoLog(mlxctx->shaderprogram, sizeof(infolog), NULL, infolog);
        fprintf(stderr, "%s", infolog);
        glDeleteProgram(mlxctx->shaderprogram);
        glDeleteShader(vshader);
        glDeleteShader(fshader);
        return (mlx_error(MLX_SHDRFAIL));
    }

    // Detach shaders after linking but before deleting them
    glDetachShader(mlxctx->shaderprogram, vshader);
    glDetachShader(mlxctx->shaderprogram, fshader);

    // Delete shaders
    glDeleteShader(vshader);
    glDeleteShader(fshader);
    glUseProgram(mlxctx->shaderprogram);
    for (size_t i = 0; i < 16; i++)
        mlxctx->bound_textures[i] = 0;
    return (true);
}

//= Public =//

// NOTE: https://www.glfw.org/docs/3.3/group__window.html

// Default settings
int32_t mlx_settings[MLX_SETTINGS_MAX] = {false, false, false, true, false};
mlx_errno_t mlx_errno = MLX_SUCCESS;
bool sort_queue = false;

mlx_t* mlx_init(int32_t width, int32_t height, const char* title, bool resize)
{
	MLX_NONNULL(title);
	MLX_ASSERT(width > 0, "Window width must be positive");
	MLX_ASSERT(height > 0, "Window height must be positive");

	bool init;
	mlx_t* mlx;
	if (!(init = glfwInit()))
		return ((void*)mlx_error(MLX_GLFWFAIL));
	if (!(mlx = calloc(1, sizeof(mlx_t))))
		return ((void*)mlx_error(MLX_MEMFAIL));
	if (!(mlx->context = calloc(1, sizeof(mlx_ctx_t))))
		return (free(mlx), (void*)mlx_error(MLX_MEMFAIL));

	mlx_ctx_t* const mlxctx = mlx->context;
    mlx->window = NULL;
	mlx->width = width;
	mlx->height = height;
	mlxctx->initialWidth = width;
	mlxctx->initialHeight = height;

	// NOTE(W2): For emscripten, this value will be ignored anyway.
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef EMSCRIPTEN
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
	glfwWindowHint(GLFW_DECORATED, GLFW_TRUE);
	glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE);
	glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
#else
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_MAXIMIZED, mlx_settings[MLX_MAXIMIZED]);
	glfwWindowHint(GLFW_DECORATED, mlx_settings[MLX_DECORATED]);
	glfwWindowHint(GLFW_VISIBLE, !mlx_settings[MLX_HEADLESS]);
#endif
#ifdef __APPLE__
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
	glfwWindowHint(GLFW_RESIZABLE, resize ? GLFW_TRUE : GLFW_FALSE);
	if (!(mlx->window = glfwCreateWindow(width, height, title, mlx_settings[MLX_FULLSCREEN] ? glfwGetPrimaryMonitor() : NULL, NULL)))
		return (glfwTerminate(), (void*)mlx_error(MLX_WINFAIL));
	if (!mlx_init_render(mlx) || !mlx_create_buffers(mlx))
		return (mlx_terminate(mlx), NULL);
    glfwMakeContextCurrent(mlx->window);
	return (mlx);
}

void mlx_set_setting(mlx_settings_t setting, int32_t value)
{
	MLX_ASSERT(setting >= 0 && setting < MLX_SETTINGS_MAX, "Invalid settings value");
	mlx_settings[setting] = value;
}
