/*
 * 86Box	A hypervisor and IBM PC system emulator that specializes in
 *		running old operating systems and software designed for IBM
 *		PC systems and compatibles from 1981 through fairly recent
 *		system designs based on the PCI bus.
 *
 *		This file is part of the 86Box distribution.
 *
 *		File parser for .glslp and .glsl shader files
 *		in the format of libretro.
 *
 * TODO:	Read .glslp files for multipass shaders and settings.
 *
 * Authors:	Teemu Korhonen
 *
 *		Copyright 2021 Teemu Korhonen
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
#include <glad/glad.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <86box/86box.h>
#include <86box/plat.h>
#include <86box/win_opengl_glslp.h>

 /**
  * @brief Default vertex shader.
 */
static const GLchar* vertex_shader = "#version 130\n\
in vec2 VertexCoord;\n\
in vec2 TexCoord;\n\
out vec2 tex;\n\
void main(){\n\
	gl_Position = vec4(VertexCoord, 0.0, 1.0);\n\
	tex = TexCoord;\n\
}\n";

/**
 * @brief Default fragment shader.
 */
static const GLchar* fragment_shader = "#version 130\n\
in vec2 tex;\n\
uniform sampler2D texsampler;\n\
out vec4 color;\n\
void main() {\n\
	color = texture(texsampler, tex);\n\
}\n";

/**
 * @brief OpenGL shader program build targets
*/
typedef enum
{
	OPENGL_BUILD_TARGET_VERTEX,
	OPENGL_BUILD_TARGET_FRAGMENT,
	OPENGL_BUILD_TARGET_LINK
} opengl_build_target_t;

/**
 * @brief Reads a whole file into a null terminated string.
 * @param Path Path to the file relative to executable path.
 * @return Pointer to the string or NULL on error. Remember to free() after use.
*/
static char* read_file_to_string(const char* path)
{
	FILE* file_handle = plat_fopen(path, "rb");

	if (file_handle != NULL)
	{
		/* get file size */
		fseek(file_handle, 0, SEEK_END);
		
		size_t file_size = (size_t)ftell(file_handle);
		
		fseek(file_handle, 0, SEEK_SET);

		/* read to buffer and close */
		char* content = (char*)malloc(sizeof(char) * (file_size + 1));

		if (!content)
			return NULL;

		size_t length = fread(content, sizeof(char), file_size, file_handle);

		fclose(file_handle);

		content[length] = 0;

		return content;
	}
	return NULL;
}

static int check_status(GLuint id, opengl_build_target_t build_target, const char* shader_path)
{
	GLint status = GL_FALSE;

	if (build_target != OPENGL_BUILD_TARGET_LINK)
		glGetShaderiv(id, GL_COMPILE_STATUS, &status);
	else
		glGetProgramiv(id, GL_LINK_STATUS, &status);

	if (status == GL_FALSE)
	{
		int info_log_length;

		if (build_target != OPENGL_BUILD_TARGET_LINK)
			glGetShaderiv(id, GL_INFO_LOG_LENGTH, &info_log_length);
		else
			glGetProgramiv(id, GL_INFO_LOG_LENGTH, &info_log_length);

		GLchar* info_log_text = (GLchar*)malloc(sizeof(GLchar) * info_log_length);

		if (build_target != OPENGL_BUILD_TARGET_LINK)
			glGetShaderInfoLog(id, info_log_length, NULL, info_log_text);
		else
			glGetProgramInfoLog(id, info_log_length, NULL, info_log_text);

		const char* reason = NULL;

		switch (build_target)
		{
		case OPENGL_BUILD_TARGET_VERTEX:
			reason = "compiling vertex shader";
			break;
		case OPENGL_BUILD_TARGET_FRAGMENT:
			reason = "compiling fragment shader";
			break;
		case OPENGL_BUILD_TARGET_LINK:
			reason = "linking shader program";
			break;
		}

		/* Shader compilation log can be lengthy, mark begin and end */
		const char* line = "--------------------";

		pclog("OpenGL: Error when %s in %s:\n%sBEGIN%s\n%s\n%s END %s\n", reason, shader_path, line, line, info_log_text, line, line);

		free(info_log_text);

		return 0;
	}

	return 1;
}

/**
 * @brief Compile custom shaders into a program.
 * @return Shader program identifier.
*/
GLuint load_custom_shaders(const char* path)
{
	char* shader = read_file_to_string(path);

	if (shader != NULL)
	{
		int success = 1;

		const char* vertex_sources[3] = { "#version 130\n", "#define VERTEX\n", shader };
		const char* fragment_sources[3] = { "#version 130\n", "#define FRAGMENT\n", shader };

		/* Check if the shader program defines version directive */
		char* version_start = strstr(shader, "#version");

		/*	If the shader program contains a version directive,
			it must be captured and placed as the first statement. */
		if (version_start != NULL)
		{
			/* Version directive found, search the line end */
			char* version_end = strchr(version_start, '\n');

			if (version_end != NULL)
			{
				char version[30] = "";

				size_t version_len = MIN(version_end - version_start + 1, 29);

				strncat(version, version_start, version_len);

				/* replace the default version directive */
				vertex_sources[0] = version;
				fragment_sources[0] = version;
			}

			/*	Comment out the original version directive
				as only one is allowed. */
			memset(version_start, '/', 2);
		}

		GLuint vertex_id = glCreateShader(GL_VERTEX_SHADER);
		GLuint fragment_id = glCreateShader(GL_FRAGMENT_SHADER);

		glShaderSource(vertex_id, 3, vertex_sources, NULL);
		glCompileShader(vertex_id);
		success *= check_status(vertex_id, OPENGL_BUILD_TARGET_VERTEX, path);

		glShaderSource(fragment_id, 3, fragment_sources, NULL);
		glCompileShader(fragment_id);
		success *= check_status(fragment_id, OPENGL_BUILD_TARGET_FRAGMENT, path);

		free(shader);

		GLuint prog_id = 0;

		if (success)
		{
			prog_id = glCreateProgram();

			glAttachShader(prog_id, vertex_id);
			glAttachShader(prog_id, fragment_id);
			glLinkProgram(prog_id);
			check_status(prog_id, OPENGL_BUILD_TARGET_LINK, path);

			glDetachShader(prog_id, vertex_id);
			glDetachShader(prog_id, fragment_id);
		}

		glDeleteShader(vertex_id);
		glDeleteShader(fragment_id);

		return prog_id;
	}
	return 0;
}

/**
 * @brief Compile default shaders into a program.
 * @return Shader program identifier.
*/
GLuint load_default_shaders()
{
	GLuint vertex_id = glCreateShader(GL_VERTEX_SHADER);
	GLuint fragment_id = glCreateShader(GL_FRAGMENT_SHADER);

	glShaderSource(vertex_id, 1, &vertex_shader, NULL);
	glCompileShader(vertex_id);

	glShaderSource(fragment_id, 1, &fragment_shader, NULL);
	glCompileShader(fragment_id);

	GLuint prog_id = glCreateProgram();

	glAttachShader(prog_id, vertex_id);
	glAttachShader(prog_id, fragment_id);

	glLinkProgram(prog_id);

	glDetachShader(prog_id, vertex_id);
	glDetachShader(prog_id, fragment_id);

	glDeleteShader(vertex_id);
	glDeleteShader(fragment_id);

	return prog_id;
}