#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_MAX_DIMENSIONS 2048
#include "glerminal-private.h"

#define GRID_SIZE_UNIFORM_NAME "grid_size"
#define SPRITES_UNIFORM_NAME "sprites"
#define LAYERS_UNIFORM_NAME "layers"
#define LAYER_COUNT_UNIFORM_NAME "layer_count"
#define ATLAS_WIDTH_UNIFORM_NAME "atlas_width"
#define CELL_SIZE_UNIFORM_NAME "cell_size"

namespace
{
	glerminal::glerminal* GLERMINAL_G = nullptr;

	constexpr float VBO_VERTICES[] =
	{
		// first triangle
		 0,  0, // top right
		 0, -1, // bottom right
		-1,  0, // top left

		// second triangle
		 0, -1, // bottom right
		-1, -1, // bottom left
		-1,  0 // top left
	};

	constexpr char VERTEX_SHADER_SOURCE[] =
		"#version 450 core\n"
		"layout (location = 0) in vec2 position;\n"
		"layout (location = 1) in vec2 offset;\n"
		"layout (location = 2) in int sprite;\n"
		"layout (location = 3) in vec4 color;\n"
		"layout (location = 4) in float scale;\n"
		"uniform float " CELL_SIZE_UNIFORM_NAME ";\n"
		"uniform vec4 " GRID_SIZE_UNIFORM_NAME ";\n"
		"uniform int " ATLAS_WIDTH_UNIFORM_NAME ";\n"
		"out VS_OUT {\n"
		"	flat int sprite;\n"
		"	flat int layer;\n"
		"	flat vec4 layer_color;\n"
		"	vec2 texcoord;\n"
		"} vs_out;\n"
		"void main()\n"
		"{\n"
		"	const int layer = int(floor(gl_InstanceID / " GRID_SIZE_UNIFORM_NAME ".y));\n"
		"	vec2 scaled_offset = 2 * offset * " GRID_SIZE_UNIFORM_NAME ".zw;\n"
		"	vs_out.sprite = sprite;\n"
		"	vs_out.layer = layer;\n"
		"	vs_out.layer_color = color;\n"
		"	vs_out.texcoord = vec2(sprite % " ATLAS_WIDTH_UNIFORM_NAME " + position.x + 1, (sprite / " ATLAS_WIDTH_UNIFORM_NAME ") - position.y) / vec2(" ATLAS_WIDTH_UNIFORM_NAME ") + vec2(-(2 * position.x + 1) * " GRID_SIZE_UNIFORM_NAME ".z / (2 * " CELL_SIZE_UNIFORM_NAME "), (2 * position.y + 1) * " GRID_SIZE_UNIFORM_NAME ".z / (2 * " CELL_SIZE_UNIFORM_NAME "));\n"
		"	vec2 cell_position = vec2(scale + (gl_InstanceID % int(" GRID_SIZE_UNIFORM_NAME ".y)) - " GRID_SIZE_UNIFORM_NAME ".x * floor((gl_InstanceID % int(" GRID_SIZE_UNIFORM_NAME ".y)) / " GRID_SIZE_UNIFORM_NAME ".x), -floor((gl_InstanceID % int(" GRID_SIZE_UNIFORM_NAME ".y)) / " GRID_SIZE_UNIFORM_NAME ".x));\n"
		"	vec2 temp = ((position + vec2(-0.5, 0.5)) * scale + cell_position + vec2(-0.5, 0.5)) * " GRID_SIZE_UNIFORM_NAME ".zw * 2 + vec2(-1, 1);\n"
		"	gl_Position = vec4(scaled_offset.x + temp.x, scaled_offset.y - temp.y, 0, 1);\n"
		"}";

	// note: AMD_vertex_shader_layer support required
	constexpr char VERTEX_SHADER_ARB_SOURCE[] =
		"#version 450 core\n"
		"#extension GL_ARB_shader_viewport_layer_array : enable\n"
		"#extension GL_AMD_vertex_shader_layer : enable\n"
		"layout (location = 0) in vec2 position;\n"
		"layout (location = 1) in vec2 offset;\n"
		"layout (location = 2) in int sprite;\n"
		"layout (location = 3) in vec4 color;\n"
		"layout (location = 4) in float scale;\n"
		"uniform float " CELL_SIZE_UNIFORM_NAME ";\n"
		"uniform vec4 " GRID_SIZE_UNIFORM_NAME ";\n"
		"uniform int " ATLAS_WIDTH_UNIFORM_NAME ";\n"
		"out VS_OUT {\n"
		"	flat int sprite;\n"
		"	flat vec4 layer_color;\n"
		"	vec2 texcoord;\n"
		"} vs_out;\n"
		"void main()\n"
		"{\n"
		"	const int layer = int(floor(gl_InstanceID / " GRID_SIZE_UNIFORM_NAME ".y));\n"
		"	gl_Layer = layer;\n"
		"	vec2 scaled_offset = 2 * offset * " GRID_SIZE_UNIFORM_NAME ".zw;\n"
		"	vs_out.sprite = sprite;\n"
		"	vs_out.texcoord = vec2(sprite % " ATLAS_WIDTH_UNIFORM_NAME " + position.x + 1, (sprite / " ATLAS_WIDTH_UNIFORM_NAME ") - position.y) / vec2(" ATLAS_WIDTH_UNIFORM_NAME ") + vec2(-(2 * position.x + 1) * " GRID_SIZE_UNIFORM_NAME ".z / (2 * " CELL_SIZE_UNIFORM_NAME "), (2 * position.y + 1) * " GRID_SIZE_UNIFORM_NAME ".z / (2 * " CELL_SIZE_UNIFORM_NAME "));\n"
		"	vs_out.layer_color = color;\n"
		"	vec2 cell_position = vec2(scale + (gl_InstanceID % int(" GRID_SIZE_UNIFORM_NAME ".y)) - " GRID_SIZE_UNIFORM_NAME ".x * floor((gl_InstanceID % int(" GRID_SIZE_UNIFORM_NAME ".y)) / " GRID_SIZE_UNIFORM_NAME ".x), -floor((gl_InstanceID % int(" GRID_SIZE_UNIFORM_NAME ".y)) / " GRID_SIZE_UNIFORM_NAME ".x));\n"
		"	vec2 temp = ((position + vec2(-0.5, 0.5)) * scale + cell_position + vec2(-0.5, 0.5)) * " GRID_SIZE_UNIFORM_NAME ".zw * 2 + vec2(-1, 1);\n"
		"	gl_Position = vec4(scaled_offset.x + temp.x, scaled_offset.y - temp.y, 0, 1);\n"
		"}";

	constexpr const char* VERTEX_SHADER_SOURCE_PTR = VERTEX_SHADER_SOURCE;
	constexpr const char* VERTEX_SHADER_ARB_SOURCE_PTR = VERTEX_SHADER_ARB_SOURCE;

	constexpr char GEOMETRY_SHADER_SOURCE[] =
		"#version 450 core\n"
		"layout (triangles) in;\n"
		"layout (triangle_strip, max_vertices = 3) out;\n"
		"in VS_OUT {\n"
		"	flat int sprite;\n"
		"	flat int layer;\n"
		"	flat vec4 layer_color;\n"
		"	vec2 texcoord;\n"
		"} gs_in[];\n"
		"flat out int sprite;\n"
		"flat out vec4 layer_color;\n"
		"out vec2 texcoord;\n"
		"void main()\n"
		"{\n"
		"	gl_Layer = gs_in[0].layer;\n"
		"	gl_Position = gl_in[0].gl_Position;\n"
		"	sprite = gs_in[0].sprite;\n"
		"	layer_color = gs_in[0].layer_color;\n"
		"	texcoord = gs_in[0].texcoord;\n"
		"	EmitVertex();\n"
		"	gl_Layer = gs_in[1].layer;\n"
		"	gl_Position = gl_in[1].gl_Position;\n"
		"	sprite = gs_in[1].sprite;\n"
		"	layer_color = gs_in[1].layer_color;\n"
		"	texcoord = gs_in[1].texcoord;\n"
		"	EmitVertex();\n"
		"	gl_Layer = gs_in[2].layer;\n"
		"	gl_Position = gl_in[2].gl_Position;\n"
		"	sprite = gs_in[2].sprite;\n"
		"	layer_color = gs_in[2].layer_color;\n"
		"	texcoord = gs_in[2].texcoord;\n"
		"	EmitVertex();\n"
		"	EndPrimitive();\n"
		"}";

	constexpr const char* GEOMETRY_SHADER_SOURCE_PTR = GEOMETRY_SHADER_SOURCE;

	constexpr char FRAGMENT_SHADER_SOURCE[] =
		"#version 450 core\n"
		"flat in int sprite;\n"
		"flat vec4 layer_color;\n"
		"in vec2 texcoord;\n"
		"layout (binding = 2) uniform sampler2D " SPRITES_UNIFORM_NAME ";\n"
		"out vec4 FragColor;\n"
		"void main()\n"
		"{\n"
		"	FragColor = layer_color * texture(" SPRITES_UNIFORM_NAME ", texcoord);\n"
		"}";

	// note: AMD_vertex_shader_layer support required
	constexpr char FRAGMENT_SHADER_ARB_SOURCE[] =
		"#version 450 core\n"
		"in VS_OUT {\n"
		"	flat int sprite;\n"
		"	flat vec4 layer_color;\n"
		"	vec2 texcoord;\n"
		"} fs_in;\n"
		"layout (binding = 2) uniform sampler2D " SPRITES_UNIFORM_NAME ";\n"
		"out vec4 FragColor;\n"
		"void main()\n"
		"{\n"
		"	FragColor = fs_in.layer_color * texture(" SPRITES_UNIFORM_NAME ", fs_in.texcoord);\n"
		"}";

	constexpr const char* FRAGMENT_SHADER_SOURCE_PTR = FRAGMENT_SHADER_SOURCE;
	constexpr const char* FRAGMENT_SHADER_ARB_SOURCE_PTR = FRAGMENT_SHADER_ARB_SOURCE;

	constexpr char SCREEN_VERTEX_SHADER_SOURCE[] =
		"#version 450 core\n"
		"layout (location = 0) in vec2 position;\n"
		"out vec2 texcoord;\n"
		"void main()\n"
		"{\n"
		"	gl_Position = vec4(position * 2 + 1, 0, 1);\n"
		"	texcoord = vec2(position.x + 1, -position.y);\n"
		"}";

	constexpr const char* SCREEN_VERTEX_SHADER_SOURCE_PTR = SCREEN_VERTEX_SHADER_SOURCE;

	constexpr char SCREEN_FRAGMENT_SHADER_SOURCE[] =
		"#version 450 core\n"
		"in vec2 texcoord;\n"
		"layout (binding = 1) uniform sampler2DArray " LAYERS_UNIFORM_NAME ";\n"
		"uniform int " LAYER_COUNT_UNIFORM_NAME ";\n"
		"out vec4 FragColor;\n"
		"void main()\n"
		"{\n"
		"	vec3 current_color = vec3(0);\n"
		"	for (int i = 0; i < " LAYER_COUNT_UNIFORM_NAME "; i++)\n"
		"	{\n"
		"		vec4 texsample = texture(" LAYERS_UNIFORM_NAME ", vec3(texcoord, i));\n"
		"		current_color = mix(current_color, texsample.rgb, texsample.a);\n"
		"	}\n"
		"	FragColor = vec4(current_color, 1);\n"
		"}";

	constexpr const char* SCREEN_FRAGMENT_SHADER_SOURCE_PTR = SCREEN_FRAGMENT_SHADER_SOURCE;
}

namespace glerminal
{
	glerminal::glerminal(glerminal_init_params params) :
		m_main(params.main),
		m_keypressed(params.keypress),
		m_keyreleased(params.keyrelease),
		m_mousemoved(params.moved),
		m_mousepressed(params.mousepress),
		m_mousereleased(params.mouserelease),
		m_cells{ },
		m_offsets{ },
		m_sprites{ },
		m_colors{ },
		m_scales{ }
#ifdef GLERMINAL_OPENGL_DEBUG_CONTEXT
		, m_log("log.txt")
#endif
	{
		if (GLERMINAL_G)
		{
			throw std::runtime_error("glerminal is already running.");
		}

		// unsure if this should be an error
		if (!params.init)
		{
			throw std::runtime_error("No init callback provided.");
		}

		if (!m_main)
		{
			throw std::runtime_error("No main callback provided.");
		}

		for (int i = 0; i < GRID_AREA_2 * LAYER_COUNT; i++)
		{
			m_colors[i * 4 + 0] = m_colors[i * 4 + 1] = m_colors[i * 4 + 2] = m_colors[i * 4 + 3] = 255;
			m_scales[i] = 1;
		}

		init_glfw();
		init_gl();
		init_audio();

		GLERMINAL_G = this;

		params.init();
	}

	glerminal::~glerminal()
	{
		deinit_audio();
		deinit_gl();
		deinit_glfw();

		GLERMINAL_G = nullptr;
	}

	void glerminal::run()
	{
		double last = glfwGetTime();
		while (!glfwWindowShouldClose(m_window))
		{
			glfwPollEvents();

			const double current = glfwGetTime();

			m_main(current - last);

			last = current;
		}
	}

	void glerminal::quit()
	{
		glfwSetWindowShouldClose(m_window, GLFW_TRUE);
	}

	void glerminal::flush()
	{
		glNamedBufferSubData(m_sprites_instance_vbo, 0, sizeof(m_cells), m_cells);
		glNamedBufferSubData(m_offsets_instance_vbo, 0, sizeof(m_offsets), m_offsets);
		update_sprites();
		update_colors();
		update_scales();

		glUseProgram(m_program);
		glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
		glBindVertexArray(m_vao);
		glClear(GL_COLOR_BUFFER_BIT);
		glDrawArraysInstanced(GL_TRIANGLES, 0, 6, GRID_AREA_2 * LAYER_COUNT);

		glUseProgram(m_screen_program);
		glBindFramebuffer(GL_FRAMEBUFFER, m_screen_framebuffer);
		glBindVertexArray(m_screen_vao);
		glClear(GL_COLOR_BUFFER_BIT);
		glDrawArrays(GL_TRIANGLES, 0, 6);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glClear(GL_COLOR_BUFFER_BIT);
		glBlitNamedFramebuffer(m_screen_framebuffer, 0, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
		glfwSwapBuffers(m_window);
	}

	void glerminal::set(int x, int y, int layer, unsigned short sprite)
	{
		if (x >= 0 && x < GRID_WIDTH + 2 && y >= 0 && y < GRID_HEIGHT + 2 && layer >= 0 && layer < LAYER_COUNT)
		{
			m_cells[x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2] = sprite;
		}
	}

	unsigned short glerminal::get(int x, int y, int layer) const
	{
		if (x >= 0 && x < GRID_WIDTH + 2 && y >= 0 && y < GRID_HEIGHT + 2 && layer >= 0 && layer < LAYER_COUNT)
		{
			return m_cells[x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2];
		}
		else
		{
			return 0;
		}
	}

	void glerminal::offset(int x, int y, int layer, float x_offset, float y_offset)
	{
		if (x >= 0 && x < GRID_WIDTH + 2 && y >= 0 && y < GRID_HEIGHT + 2 && layer >= 0 && layer < LAYER_COUNT)
		{
			m_offsets[2 * (x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2) + 0] = x_offset;
			m_offsets[2 * (x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2) + 1] = y_offset;
		}
	}

	void glerminal::color(int x, int y, int layer, unsigned int color)
	{
		if (x >= 0 && x < GRID_WIDTH + 2 && y >= 0 && y < GRID_HEIGHT + 2 && layer >= 0 && layer < LAYER_COUNT)
		{
			m_colors[4 * (x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2) + 0] = (color >>  0) & 0xFF;
			m_colors[4 * (x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2) + 1] = (color >>  8) & 0xFF;
			m_colors[4 * (x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2) + 2] = (color >> 16) & 0xFF;
			m_colors[4 * (x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2) + 3] = (color >> 24) & 0xFF;
		}
	}

	void glerminal::scale(int x, int y, int layer, float scale)
	{
		if (x >= 0 && x < GRID_WIDTH + 2 && y >= 0 && y < GRID_HEIGHT + 2 && layer >= 0 && layer < LAYER_COUNT)
		{
			m_scales[x + y * (GRID_WIDTH + 2) + layer * GRID_AREA_2] = scale;
		}
	}

	void glerminal::load_atlas(unsigned char w, unsigned char h, const unsigned int* data)
	{
		for (int src_atlas_row = 0; src_atlas_row < h; src_atlas_row++)
		{
			for (int src_atlas_col = 0; src_atlas_col < w; src_atlas_col++)
			{
				for (int sprite_row = 0; sprite_row < CELL_SIZE; sprite_row++)
				{
					const unsigned int sprite_index = src_atlas_col + src_atlas_row * w;
					const unsigned int dst_atlas_row = sprite_index / MAX_SPRITES_ROW;
					const unsigned int dst_atlas_col = sprite_index % MAX_SPRITES_ROW;

					// offset from base address in source atlas layout
					const unsigned int src_offset = CELL_SIZE * (src_atlas_col + sprite_row * w + src_atlas_row * w * CELL_SIZE);

					const unsigned int dst_column = dst_atlas_col * (CELL_SIZE + 2) + 1;
					const unsigned int dst_row = MAX_SPRITES_ROW * (CELL_SIZE + 2) * ((sprite_row + 1) + dst_atlas_row * (CELL_SIZE + 2));
					// offset from base address in glerminal atlas layout
					const unsigned int dst_offset = dst_column + dst_row;

					memcpy(m_sprites + dst_offset, data + src_offset, CELL_SIZE * sizeof(unsigned int));
				}
			}
		}
	}

	bool glerminal::load_sound(const char* name)
	{
		if (m_sounds.find(name) == m_sounds.end())
		{
			ma_sound& ref = m_sounds[name];
			const ma_result result = ma_sound_init_from_file(
				&m_audio_engine,
				name,
				MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_NO_SPATIALIZATION,
				nullptr,
				nullptr,
				&ref
			);

			if (result != MA_SUCCESS)
			{
				return false;
			}
		}

		return true;
	}

	void glerminal::play_sound(const char* name)
	{
		load_sound(name);
		const ma_result result = ma_engine_play_sound(&m_audio_engine, name, nullptr);
		if (result != MA_SUCCESS)
		{

		}
	}

	void glerminal::init_glfw()
	{
		glfwInit();

		glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
		glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
		glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
		// not resizable for now.
		// 
		// need to think about how to handle resizing to ensure
		// that the window stays an integer number of "tiles" large
		glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
#ifdef GLERMINAL_OPENGL_DEBUG_CONTEXT
		glfwWindowHint(GLFW_CONTEXT_DEBUG, GLFW_TRUE);
#endif

		// non-adjustable size for the same reason as above
		m_window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "glerminal", nullptr, nullptr);

		if (!m_window)
		{
			throw std::runtime_error("Failed to create glerminal window.");
		}

		glfwSetWindowUserPointer(m_window, this);
		glfwSetKeyCallback(m_window, glfw_key_handler);
		glfwSetCursorPosCallback(m_window, glfw_mousemoved_handler);
		glfwSetMouseButtonCallback(m_window, glfw_mousepress_handler);
	}
	
	void glerminal::log(GLenum type, GLuint id, GLenum severity, const char* message) const
	{
#ifdef GLERMINAL_OPENGL_DEBUG_CONTEXT
		glDebugMessageInsert(GL_DEBUG_SOURCE_THIRD_PARTY, type, id, severity, -1, message);
#endif
	}

	void glerminal::init_gl()
	{
		glfwMakeContextCurrent(m_window);

		if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress)))
		{
			throw std::runtime_error("Failed to initialize GLAD.");
		}

#ifdef GLERMINAL_OPENGL_DEBUG_CONTEXT
		glEnable(GL_DEBUG_OUTPUT);
		glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
		glDebugMessageCallback([](GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam)
			{
				const char* source_str = "[UNKNOWN]";
				switch (source)
				{
				case GL_DEBUG_SOURCE_API:
					source_str = "[API]";
					break;

				case GL_DEBUG_SOURCE_WINDOW_SYSTEM:
					source_str = "[WINDOW]";
					break;

				case GL_DEBUG_SOURCE_SHADER_COMPILER:
					source_str = "[SHADER]";
					break;

				case GL_DEBUG_SOURCE_THIRD_PARTY:
					source_str = "[GLERMINAL]";
					break;

				case GL_DEBUG_SOURCE_APPLICATION:
					source_str = "[APPLICATION]";
					break;
				}

				const char* type_str = "[UNKNOWN]";
				switch (type)
				{
				case GL_DEBUG_TYPE_ERROR:
					type_str = "[ERROR]";
					break;

				case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
					type_str = "[DEPRECATED]";
					break;

				case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
					type_str = "[UNDEFINED]";
					break;

				case GL_DEBUG_TYPE_PORTABILITY:
					type_str = "[PORTABILITY]";
					break;

				case GL_DEBUG_TYPE_PERFORMANCE:
					type_str = "[PERFORMANCE]";
					break;
				}

				static_cast<const glerminal*>(userParam)->m_log << source_str << type_str << ' ' << std::string(message, length) << std::endl;

			}, this);
#endif

		glDisable(GL_DEPTH_TEST);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

		// -- setup vertex data --
		// create vertex buffer object
		glGenBuffers(1, &m_vbo);
		glGenBuffers(1, &m_sprites_instance_vbo);
		glGenBuffers(1, &m_offsets_instance_vbo);
		glGenBuffers(1, &m_colors_instance_vbo);
		glGenBuffers(1, &m_scales_instance_vbo);

		// create vertex array object
		glGenVertexArrays(1, &m_vao);
		glBindVertexArray(m_vao);

		// set up static vertex attributes
		glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
		glBufferStorage(GL_ARRAY_BUFFER, sizeof(VBO_VERTICES), VBO_VERTICES, GL_NONE);
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(*VBO_VERTICES), reinterpret_cast<void*>(0));

		// set up instanced vertex attributes
		glBindBuffer(GL_ARRAY_BUFFER, m_offsets_instance_vbo);
		glBufferStorage(GL_ARRAY_BUFFER, sizeof(m_offsets), m_offsets, GL_DYNAMIC_STORAGE_BIT);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(*m_offsets), reinterpret_cast<void*>(0));
		glVertexAttribDivisor(1, 1);

		glBindBuffer(GL_ARRAY_BUFFER, m_sprites_instance_vbo);
		glBufferStorage(GL_ARRAY_BUFFER, sizeof(m_cells), m_cells, GL_DYNAMIC_STORAGE_BIT);
		glEnableVertexAttribArray(2);
		glVertexAttribIPointer(2, 1, GL_UNSIGNED_SHORT, 1 * sizeof(*m_cells), reinterpret_cast<void*>(0));
		glVertexAttribDivisor(2, 1);

		glBindBuffer(GL_ARRAY_BUFFER, m_colors_instance_vbo);
		glBufferStorage(GL_ARRAY_BUFFER, sizeof(m_colors), m_colors, GL_DYNAMIC_STORAGE_BIT);
		glEnableVertexAttribArray(3);
		glVertexAttribPointer(3, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 * sizeof(*m_colors), reinterpret_cast<void*>(0));
		glVertexAttribDivisor(3, 1);

		glBindBuffer(GL_ARRAY_BUFFER, m_scales_instance_vbo);
		glBufferStorage(GL_ARRAY_BUFFER, sizeof(m_scales), m_scales, GL_DYNAMIC_STORAGE_BIT);
		glEnableVertexAttribArray(4);
		glVertexAttribPointer(4, 1, GL_FLOAT, GL_FALSE, 1 * sizeof(*m_scales), reinterpret_cast<void*>(0));
		glVertexAttribDivisor(4, 1);

		// set up static vertex attributes
		glGenVertexArrays(1, &m_screen_vao);
		glBindVertexArray(m_screen_vao);

		glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(*VBO_VERTICES), reinterpret_cast<void*>(0));

		glBindVertexArray(0);

		// -- setup shader program --
		// test for features
		const bool vertex_shader_layer_supported =
			glfwExtensionSupported("GL_ARB_shader_viewport_layer_array") || glfwExtensionSupported("GL_AMD_vertex_shader_layer");

		// compile
		const unsigned int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vertex_shader, 1, &(vertex_shader_layer_supported ? VERTEX_SHADER_ARB_SOURCE_PTR : VERTEX_SHADER_SOURCE_PTR), nullptr);
		glCompileShader(vertex_shader);

		const unsigned int geometry_shader = glCreateShader(GL_GEOMETRY_SHADER);
		glShaderSource(geometry_shader, 1, &GEOMETRY_SHADER_SOURCE_PTR, nullptr);
		glCompileShader(geometry_shader);

		const unsigned int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(fragment_shader, 1, &(vertex_shader_layer_supported ? FRAGMENT_SHADER_ARB_SOURCE_PTR : FRAGMENT_SHADER_SOURCE_PTR), nullptr);
		glCompileShader(fragment_shader);

		constexpr int INFO_LOG_SIZE = 512;
		int success;
		char info_log[INFO_LOG_SIZE + 1] = {};

		// verify compile
		glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(vertex_shader, INFO_LOG_SIZE, nullptr, info_log);

			glDeleteShader(vertex_shader);
			glDeleteShader(geometry_shader);
			glDeleteShader(fragment_shader);

			log(GL_DEBUG_TYPE_ERROR, 0, GL_DEBUG_SEVERITY_HIGH, "Could not compile vertex shader.");
			log(GL_DEBUG_TYPE_ERROR, 0, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not compile vertex shader.");
		}

		glGetShaderiv(geometry_shader, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(geometry_shader, INFO_LOG_SIZE, nullptr, info_log);

			glDeleteShader(vertex_shader);
			glDeleteShader(geometry_shader);
			glDeleteShader(fragment_shader);

			log(GL_DEBUG_TYPE_ERROR, 1, GL_DEBUG_SEVERITY_HIGH, "Could not compile geometry shader.");
			log(GL_DEBUG_TYPE_ERROR, 1, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not compile geometry shader.");
		}

		glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(fragment_shader, INFO_LOG_SIZE, nullptr, info_log);

			glDeleteShader(vertex_shader);
			glDeleteShader(geometry_shader);
			glDeleteShader(fragment_shader);

			log(GL_DEBUG_TYPE_ERROR, 2, GL_DEBUG_SEVERITY_HIGH, "Could not compile fragment shader.");
			log(GL_DEBUG_TYPE_ERROR, 2, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not compile fragment shader.");
		}

		// link
		m_program = glCreateProgram();
		glAttachShader(m_program, vertex_shader);
		// only need geometry shader if AMD_vertex_shader_layer is not supported
		if (!vertex_shader_layer_supported)
		{
			glAttachShader(m_program, geometry_shader);
		}
		glAttachShader(m_program, fragment_shader);
		glLinkProgram(m_program);
		glDeleteShader(vertex_shader);
		glDeleteShader(geometry_shader);
		glDeleteShader(fragment_shader);

		// verify link
		glGetProgramiv(m_program, GL_LINK_STATUS, &success);
		if (!success)
		{
			glGetProgramInfoLog(m_program, INFO_LOG_SIZE, nullptr, info_log);

			glDeleteProgram(m_program);

			log(GL_DEBUG_TYPE_ERROR, 3, GL_DEBUG_SEVERITY_HIGH, "Could not link shader program.");
			log(GL_DEBUG_TYPE_ERROR, 3, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not link shader program.");
		}

		// setup uniforms
		glUseProgram(m_program);
		glUniform1f(glGetUniformLocation(m_program, CELL_SIZE_UNIFORM_NAME), CELL_SIZE);
		glUniform4f(glGetUniformLocation(m_program, GRID_SIZE_UNIFORM_NAME), GRID_WIDTH + 2, GRID_AREA_2, 1.0f / GRID_WIDTH, 1.0f / GRID_HEIGHT);
		glUniform1i(glGetUniformLocation(m_program, ATLAS_WIDTH_UNIFORM_NAME), MAX_SPRITES_ROW);

		// compile
		const unsigned int screen_vertex_shader = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(screen_vertex_shader, 1, &SCREEN_VERTEX_SHADER_SOURCE_PTR, nullptr);
		glCompileShader(screen_vertex_shader);

		const unsigned int screen_fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(screen_fragment_shader, 1, &SCREEN_FRAGMENT_SHADER_SOURCE_PTR, nullptr);
		glCompileShader(screen_fragment_shader);

		// verify compile
		glGetShaderiv(screen_vertex_shader, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(screen_vertex_shader, INFO_LOG_SIZE, 0, info_log);

			glDeleteShader(screen_vertex_shader);
			glDeleteShader(screen_fragment_shader);

			log(GL_DEBUG_TYPE_ERROR, 4, GL_DEBUG_SEVERITY_HIGH, "Could not compile screen vertex shader.");
			log(GL_DEBUG_TYPE_ERROR, 4, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not compile screen vertex shader.");
		}

		glGetShaderiv(screen_fragment_shader, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(screen_fragment_shader, INFO_LOG_SIZE, 0, info_log);

			glDeleteShader(screen_vertex_shader);
			glDeleteShader(screen_fragment_shader);

			log(GL_DEBUG_TYPE_ERROR, 5, GL_DEBUG_SEVERITY_HIGH, "Could not compile screen fragment shader.");
			log(GL_DEBUG_TYPE_ERROR, 5, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not compile screen fragment shader.");
		}

		// link
		m_screen_program = glCreateProgram();
		glAttachShader(m_screen_program, screen_vertex_shader);
		glAttachShader(m_screen_program, screen_fragment_shader);
		glLinkProgram(m_screen_program);
		glDeleteShader(screen_vertex_shader);
		glDeleteShader(screen_fragment_shader);

		// verify link
		glGetProgramiv(m_screen_program, GL_LINK_STATUS, &success);
		if (!success)
		{
			glDeleteProgram(m_screen_program);

			log(GL_DEBUG_TYPE_ERROR, 6, GL_DEBUG_SEVERITY_HIGH, "Could not link screen shader program.");
			log(GL_DEBUG_TYPE_ERROR, 6, GL_DEBUG_SEVERITY_HIGH, info_log);
			throw std::runtime_error("Could not link screen shader program.");
		}

		// setup uniforms for screen shader
		glUseProgram(m_screen_program);

		glUniform1i(glGetUniformLocation(m_screen_program, LAYER_COUNT_UNIFORM_NAME), LAYER_COUNT);

		// -- setup textures --
		glGenTextures(1, &m_sprites_texture);
		glBindTexture(GL_TEXTURE_2D, m_sprites_texture);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		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_BASE_LEVEL, 0);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);

		glTextureStorage2D(m_sprites_texture, 1, GL_RGBA8, MAX_SPRITES_ROW * (CELL_SIZE + 2), MAX_SPRITES_ROW * (CELL_SIZE + 2));

		update_sprites();

		glBindTextureUnit(2, m_sprites_texture);

		// -- setup framebuffer --
		glGenFramebuffers(1, &m_framebuffer);
		glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);

		glGenTextures(1, &m_framebuffer_backing_texture);
		glBindTexture(GL_TEXTURE_2D_ARRAY, m_framebuffer_backing_texture);

		glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

		glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 0);
		glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 0);

		glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, SCREEN_WIDTH, SCREEN_HEIGHT, LAYER_COUNT);
		glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_framebuffer_backing_texture, 0);

		glBindTextureUnit(1, m_framebuffer_backing_texture);

		// -- setup screen framebuffer --
		glGenFramebuffers(1, &m_screen_framebuffer);
		glBindFramebuffer(GL_FRAMEBUFFER, m_screen_framebuffer);

		glGenTextures(1, &m_screen_framebuffer_backing_texture);
		glBindTexture(GL_TEXTURE_2D, m_screen_framebuffer_backing_texture);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		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_BASE_LEVEL, 0);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);

		glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGB8, SCREEN_WIDTH, SCREEN_HEIGHT);
		glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_screen_framebuffer_backing_texture, 0);
	}

	void glerminal::init_audio()
	{
		const ma_result result = ma_engine_init(nullptr, &m_audio_engine);

		if (result != MA_SUCCESS)
		{
			throw std::runtime_error("Failed to initialize audio engine");
		}

		ma_engine_set_volume(&m_audio_engine, 1);
	}

	void glerminal::deinit_glfw()
	{
		glfwDestroyWindow(m_window);

		glfwTerminate();
	}

	void glerminal::deinit_gl()
	{
		glDeleteFramebuffers(1, &m_screen_framebuffer);
		glDeleteTextures(1, &m_screen_framebuffer_backing_texture);
		glDeleteFramebuffers(1, &m_framebuffer);
		glDeleteTextures(1, &m_framebuffer_backing_texture);
		glDeleteTextures(1, &m_sprites_texture);
		glDeleteVertexArrays(1, &m_vao);
		glDeleteVertexArrays(1, &m_screen_vao);
		glDeleteBuffers(1, &m_vbo);
		glDeleteBuffers(1, &m_sprites_instance_vbo);
		glDeleteBuffers(1, &m_offsets_instance_vbo);
		glDeleteBuffers(1, &m_colors_instance_vbo);
		glDeleteBuffers(1, &m_scales_instance_vbo);
		glDeleteProgram(m_program);
	}

	void glerminal::deinit_audio()
	{
		for (auto& elem : m_sounds)
		{
			ma_sound_uninit(&elem.second);
		}
		ma_engine_uninit(&m_audio_engine);
	}

	void glerminal::update_sprites()
	{
		glTextureSubImage2D(m_sprites_texture, 0, 0, 0, (CELL_SIZE + 2) * MAX_SPRITES_ROW, (CELL_SIZE + 2) * MAX_SPRITES_ROW, GL_RGBA, GL_UNSIGNED_BYTE, m_sprites);
	}

	void glerminal::update_colors()
	{
		glNamedBufferSubData(m_colors_instance_vbo, 0, sizeof(m_colors), m_colors);
	}

	void glerminal::update_scales()
	{
		glNamedBufferSubData(m_scales_instance_vbo, 0, sizeof(m_scales), m_scales);
	}

	void glerminal::glfw_key_handler(GLFWwindow* window, int key, int scancode, int action, int mods)
	{
		const glerminal* const self = static_cast<glerminal*>(glfwGetWindowUserPointer(window));

		if (self->m_keypressed && action == GLFW_PRESS)
		{
			self->m_keypressed(key);
		}

		if (self->m_keyreleased && action == GLFW_RELEASE)
		{
			self->m_keyreleased(key);
		}
	}

	void glerminal::glfw_mousemoved_handler(GLFWwindow *window, double x, double y)
	{
		const glerminal* const self = static_cast<glerminal*>(glfwGetWindowUserPointer(window));

		if (self->m_mousemoved) { self->m_mousemoved(x / (CELL_SIZE * CELL_SCALE), y / (CELL_SIZE * CELL_SCALE)); }
	}

	void glerminal::glfw_mousepress_handler(GLFWwindow *window, int button, int action, int mods)
	{
		const glerminal* const self = static_cast<glerminal*>(glfwGetWindowUserPointer(window));

		double x, y;
		glfwGetCursorPos(window, &x, &y);

		if (self->m_mousepressed && action == GLFW_PRESS)
		{
			self->m_mousepressed(button, x / (CELL_SIZE * CELL_SCALE), y / (CELL_SIZE * CELL_SCALE));
		}

		if (self->m_mousereleased && action == GLFW_RELEASE)
		{
			self->m_mousereleased(button, x / (CELL_SIZE * CELL_SCALE), y / (CELL_SIZE * CELL_SCALE));
		}
	}

}

void glerminal_run(glerminal_init_params params)
{
	try
	{
		glerminal::glerminal* g = new glerminal::glerminal(params);
		g->run();
		delete g;
	}
	catch (const std::runtime_error& e)
	{
		std::cout << e.what() << std::endl;
	}
}

void glerminal_quit()
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->quit();
}

void glerminal_flush()
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->flush();
}

void glerminal_set(int x, int y, int layer, unsigned short sprite)
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->set(x + 1, y + 1, layer, sprite);
}

unsigned short glerminal_get(int x, int y, int layer)
{
	if (!GLERMINAL_G) { return 0; }

	return GLERMINAL_G->get(x + 1, y + 1, layer);
}

void glerminal_offset(int x, int y, int layer, float x_offset, float y_offset)
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->offset(x + 1, y + 1, layer, x_offset, y_offset);
}

void glerminal_color(int x, int y, int layer, unsigned int color)
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->color(x + 1, y + 1, layer, color);
}

void glerminal_scale(int x, int y, int layer, float scale)
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->scale(x + 1, y + 1, layer, scale);
}

int glerminal_load_sprites_file(const char* filename)
{
	if (!GLERMINAL_G) { return false; }

	bool success = false;

	int w, h;
	stbi_uc* const buffer = stbi_load(filename, &w, &h, nullptr, 4);

	// verify atlas size is a multiple of CELL_SIZE in each dimension
	if (buffer && w % glerminal::CELL_SIZE == 0 && h % glerminal::CELL_SIZE == 0)
	{
		GLERMINAL_G->load_atlas(w / glerminal::CELL_SIZE, h / glerminal::CELL_SIZE, reinterpret_cast<unsigned int*>(buffer));

		success = true;
	}

	stbi_image_free(buffer);

	return success;
}

int glerminal_load_sprites_buffer(unsigned char width, unsigned char height, const unsigned int* buffer)
{
	if (!GLERMINAL_G) { return false; }

	// verify atlas size is a multiple of CELL_SIZE in each dimension
	if (width % glerminal::CELL_SIZE == 0 && height % glerminal::CELL_SIZE == 0)
	{
		GLERMINAL_G->load_atlas(width / glerminal::CELL_SIZE, height / glerminal::CELL_SIZE, buffer);

		return true;
	}
	else
	{
		return false;
	}
}

void glerminal_sound(const char* name)
{
	if (!GLERMINAL_G) { return; }

	GLERMINAL_G->play_sound(name);
}
