#ifndef SHADER_H
#define SHADER_H

#include <GL/glew.h>

enum shader_err {
	SHADER_ERR_OK,
	SHADER_ERR_FAIL_CREATE,
	SHADER_ERR_FAIL_CREATE_PROG,
	SHADER_ERR_FAIL_COMPILE,
	SHADER_ERR_FAIL_READ_FILE,
	SHADER_ERR_FAIL_LINK_PROG
};

struct ret_uint32_t_err {
	uint32_t f1;
	enum shader_err f2;
};

struct ret_uint32_t_err shader_compile_source(const char *src, GLenum type);
struct ret_uint32_t_err shader_compile_file(const char *filepath, GLenum type);
uint32_t shader_compile_file_or_exit(const char *filepath, GLenum type);

struct ret_uint32_t_err program_link(uint32_t vert_shader,
				     uint32_t frag_shader);
uint32_t program_link_or_exit(uint32_t vert_shader, uint32_t frag_shader);

const char *shader_type_to_cstr(GLenum type);

#if defined(SHADER_IMP) || defined(IMP)

#include <stdio.h>

#include "unwrap.h"
#include "file.h"

struct ret_uint32_t_err
shader_compile_source(const char *src, GLenum type)
{
	enum shader_err err;
	uint32_t shdr = glCreateShader(type);
	if ( shdr == 0 ) {
		err = SHADER_ERR_FAIL_CREATE;
		goto err;
	}
	glShaderSource(shdr, 1, &src, NULL);
	glCompileShader(shdr);

	int32_t comp_status = GL_FALSE;
	glGetShaderiv(shdr, GL_COMPILE_STATUS, &comp_status);

	if ( comp_status == GL_FALSE ) {
		err = SHADER_ERR_FAIL_COMPILE;
		int32_t log_length = 1024;
		glGetShaderiv(shdr, GL_INFO_LOG_LENGTH, &log_length);
		log_length += 10;
		char *log = calloc(sizeof(char), (size_t)log_length);
		glGetShaderInfoLog(shdr, log_length, NULL, log);
		fprintf(stderr, "FAILED COMPILE %s : %s\n",
			shader_type_to_cstr(type), log);
		goto err;
	}

	return (struct ret_uint32_t_err) {
		.f1 = shdr,
		.f2 = SHADER_ERR_OK
	};

err: ;
	return (struct ret_uint32_t_err) {
		.f1 = 0,
		.f2 = err
	};
}

struct ret_uint32_t_err
shader_compile_file(const char *filepath, GLenum type)
{
	char *src;
	enum file_err err;

	src = (char *) file_read_all(filepath, NULL, &err);
	if ( err != FILE_ERR_OK ) {
		return (struct ret_uint32_t_err) {
			.f1 = 0,
			.f2 = SHADER_ERR_FAIL_READ_FILE,
		};
	}

	struct ret_uint32_t_err ret = shader_compile_source(src, type);
	free(src);
	return ret;
}

uint32_t
shader_compile_file_or_exit(const char *filepath, GLenum type)
{
	enum shader_err err;
	uint32_t shader;
	RET_UNWRAP2(shader, err, struct ret_uint32_t_err,
		    shader_compile_file(filepath, type));
	if ( err != SHADER_ERR_OK ) {
		fprintf(stderr, "Failed to compile shader %s\n", filepath);
		exit(EXIT_FAILURE);
	}
	return shader;
}

struct ret_uint32_t_err
program_link(uint32_t vert_shader, uint32_t frag_shader)
{
	enum shader_err err;
	uint32_t prog = glCreateProgram();
	if ( prog == 0 ) {
		err = SHADER_ERR_FAIL_CREATE_PROG;
		goto err;
	}

	glAttachShader(prog, vert_shader);
	glAttachShader(prog, frag_shader);

	glLinkProgram(prog);

	int32_t link_status = GL_FALSE;
	glGetProgramiv(prog, GL_LINK_STATUS, &link_status);

	if ( link_status != GL_TRUE ) {
		err = SHADER_ERR_FAIL_LINK_PROG;
		int32_t log_length = 1024;
		glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_length);
		log_length += 10;
		char *log = calloc(sizeof(char), (size_t)log_length);
		glGetProgramInfoLog(prog, log_length, NULL, log);
		fprintf(stderr, "FAILED LINK PROG: %s\n", log);
		goto err;
	}

	glDeleteShader(vert_shader);
	glDeleteShader(frag_shader);

	return (struct ret_uint32_t_err) {
		.f1 = prog,
		.f2 = SHADER_ERR_OK,
	};

err: ;
	return (struct ret_uint32_t_err) {
		.f1 = 0,
		.f2 = err,
	};
}

uint32_t
program_link_or_exit(uint32_t vert_shader, uint32_t frag_shader)
{
	enum shader_err err;
	uint32_t prog;
	RET_UNWRAP2(prog, err, struct ret_uint32_t_err,
		    program_link(vert_shader, frag_shader));
	if ( err != SHADER_ERR_OK ) {
		fprintf(stderr, "Failed to link program\n");
		exit(EXIT_FAILURE);
	}
	return prog;
}


const char *
shader_type_to_cstr(GLenum type)
{
	switch ( type ) {
	case GL_VERTEX_SHADER: return "GL_VERTEX_SHADER"; break;
	case GL_FRAGMENT_SHADER: return "GL_FRAGMENT_SHADER"; break;
	}
	return "(Unknown Shader)";
}

#endif /* defined(SHADER_IMP) || defined(IMP) */

#endif
