/*
 *  <Short Description>
 *  Copyright (C) 2023  Brett Terpstra
 *
 *  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 3 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, see <https://www.gnu.org/licenses/>.
 */
#include <blt/gfx/renderer/batch_2d_renderer.h>
#include <blt/gfx/renderer/shaders/2d_textured.vert>
#include <blt/gfx/renderer/shaders/2d_textured.frag>
#include <blt/gfx/renderer/shaders/2d_textured_circle.frag>
// https://stackoverflow.com/questions/60440682/drawing-a-line-in-modern-opengl

float square_vertices[20] = {
	// positions        // texture coords
	0.5f, 0.5f, 0.0f, 1.0f, 1.0f,   // top right
	0.5f, -0.5f, 0.0f, 1.0f, 0.0f,  // bottom right
	-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // bottom left
	-0.5f, 0.5f, 0.0f, 0.0f, 1.0f   // top left
};
const unsigned int square_indices[6] = {
	0, 1, 3, // first triangle
	1, 2, 3  // second triangle
};

float line_vertices[20] = {
	// positions        // texture coords
	0.5f, 0.5f, 0.0f, 1.0f, 1.0f,   // top right
	0.5f, -0.5f, 0.0f, 1.0f, 0.0f,  // bottom right
	-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // bottom left
	-0.5f, 0.5f, 0.0f, 0.0f, 1.0f   // top left
};

// 0, 1 (top right)
// 5, 6 (bottom right)
// 10, 11 (bottom left)
// 15, 16 (top left)

namespace blt::gfx
{
	curve2d_mesh_data_t::draw_t curve2d_mesh_data_t::to_vertex_array() const
	{
		const auto vertices = calculate_vertices();
		auto vao = std::make_unique<vertex_array_t>();

		vertex_buffer_t vertex_buffer;
		vertex_buffer.create();
		vertex_buffer.allocate(static_cast<long>(vertices.size() * sizeof(line_vertex_t)), vertices.data());

		const auto vb = vertex_array_t::make_vbo(vertex_buffer);
		vao->bindVBO(vb, 0, 3, GL_FLOAT, sizeof(line_vertex_t), 0);
		vao->bindVBO(vb, 1, 2, GL_FLOAT, sizeof(line_vertex_t), sizeof(vec3));

		return {std::move(vao), static_cast<i32>(vertices.size())};
	}

	size_t curve2d_mesh_data_t::populate_vertex_array(vertex_array_t& va) const
	{
		const auto vertices = calculate_vertices();
		va.getBuffer(0).update(static_cast<long>(vertices.size() * sizeof(line_vertex_t)), vertices.data());
		return vertices.size();
	}

	std::vector<curve2d_mesh_data_t::line_vertex_t> curve2d_mesh_data_t::calculate_vertices() const
	{
		std::vector<line_vertex_t> vertices;

		for (const auto [i, line] : enumerate(lines))
		{
			const float thickness = line.thickness;
			const vec2 dir = (line.p1 - line.p2).normalize();
			const vec2 right = {dir.y(), -dir.x()};
			const vec2 left = {-dir.y(), dir.x()};

			auto top_left = line.p2 + left * thickness;
			auto top_right = line.p2 + right * thickness;

			if (i == 0)
			{
				auto bottom_left = line.p1 + left * thickness;
				auto bottom_right = line.p1 + right * thickness;

				vertices.push_back({make_vec3(bottom_right), vec2{1, 0}});
				vertices.push_back({make_vec3(bottom_left), vec2{0, 0}});
			}

			if (i == lines.size() - 1)
			{
				vertices.push_back({make_vec3(top_right), vec2{1, 1}});
				vertices.push_back({make_vec3(top_left), vec2{0, 1}});
			} else
			{
				auto& next = lines[i + 1];
				vec2 next_dir = (next.p1 - next.p2).normalize();
				vec2 next_right = {next_dir.y(), -next_dir.x()};
				vec2 next_left = {-next_dir.y(), next_dir.x()};

				auto next_bottom_left = next.p1 + next_left * next.thickness;
				auto next_bottom_right = next.p1 + next_right * next.thickness;

				auto avg_top_left = (next_bottom_left + top_left) / 2.0f;
				auto avg_top_right = (next_bottom_right + top_right) / 2.0f;

				vertices.push_back({make_vec3(avg_top_right), vec2{1, 1}});
				vertices.push_back({make_vec3(avg_top_left), vec2{0, 1}});
			}
		}

		return vertices;
	}

	f32 curve2d_mesh_data_t::length() const
	{
		f32 length = 0;
		for (const auto& line : lines)
			length += (line.p2 - line.p1).magnitude();
		return length;
	}

	curve2d_t::curve2d_t(const vec2 p0, const vec2 p1): m_p0(p0), m_p2(p1)
	{
		const auto dir = (p1 - p0) / 2;
		m_p1 = p0 + dir;
	}

	curve2d_t::curve2d_t(const vec2 p0, const vec2 p1, const vec2 p2): m_p0(p0), m_p1(p1), m_p2(p2)
	{}

	curve2d_t::curve2d_t(const vec2 p0, const vec2 p1, const vec2 p2, const vec2 p3): m_p0(p0), m_p1(p1), m_p2(p2), m_p3(p3)
	{}

	vec2 curve2d_t::get_point(const float t) const
	{
		const auto t_inv = 1.0f - t;
		const auto t_inv_sq = t_inv * t_inv;
		const auto t_sq = t * t;
		if (m_p3)
		{
			const auto t_inv_cub = t_inv_sq * t_inv;
			const auto t_cub = t_sq * t;
			return t_inv_cub * m_p0 + 3 * t_inv_sq * t * m_p1 + 3 * t_inv * t_sq * m_p2 + t_cub * *m_p3;
		}
		return t_inv_sq * m_p0 + 2 * t_inv * t * m_p1 + t_sq * m_p2;
	}

	std::vector<line2d_t> curve2d_t::to_lines(const i32 segments, const float thickness) const
	{
		std::vector<line2d_t> lines;
		float t = 0;
		const float diff = 1.0f / static_cast<float>(segments);

		for (i32 i = 0; i < segments; ++i)
		{
			auto begin = get_point(t);
			t += diff;
			auto end = get_point(t);

			lines.emplace_back(begin, end, thickness);
		}

		return lines;
	}

	curve2d_mesh_data_t curve2d_t::to_mesh(const i32 segments, const float thickness) const
	{
		curve2d_mesh_data_t mesh_data;
		mesh_data.lines = to_lines(segments, thickness);
		return mesh_data;
	}

	f32 curve2d_t::length(const i32 segments) const
	{
		f32 length = 0;
		float t = 0;
		const float diff = 1.0f / static_cast<float>(segments);

		for (i32 i = 0; i < segments; ++i)
		{
			auto begin = get_point(t);
			t += diff;
			auto end = get_point(t);

			length += (end - begin).magnitude();
		}
		return length;
	}

	f32 curve2d_t::length_fast() const
	{
		const auto d1 = (m_p1 - m_p0).magnitude();
		const auto d2 = (m_p2 - m_p1).magnitude();
		if (m_p3)
		{
			const auto d3 = (*m_p3 - m_p2).magnitude();
			return d1 + d2 + d3;
		}
		return d1 + d2;
	}

	void batch_renderer_2d::create()
	{
		{
			vertex_buffer_t vertices_vbo;
			element_buffer_t indices_vbo;

			vertices_vbo.create();
			indices_vbo.create();

			vertices_vbo.allocate(sizeof(square_vertices), square_vertices);
			indices_vbo.allocate(sizeof(square_indices), square_indices);

			square_vao = std::make_unique<vertex_array_t>();
			auto vb = vertex_array_t::make_vbo(vertices_vbo);
			square_vao->bindVBO(vb, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
			square_vao->bindVBO(vb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
			square_vao->bindElement(indices_vbo);
		}
		{
			vertex_buffer_t vertices_vbo;
			element_buffer_t indices_vbo;

			vertices_vbo.create();
			indices_vbo.create();

			vertices_vbo.allocate(sizeof(line_vertices), line_vertices, GL_DYNAMIC_DRAW);
			indices_vbo.allocate(sizeof(square_indices), square_indices, GL_DYNAMIC_DRAW);

			line_vao = std::make_unique<vertex_array_t>();
			const auto vb = vertex_array_t::make_vbo(vertices_vbo);
			line_vao->bindVBO(vb, 0, 3, GL_FLOAT, 5 * sizeof(float), 0);
			line_vao->bindVBO(vb, 1, 2, GL_FLOAT, 5 * sizeof(float), 3 * sizeof(float));
			line_vao->bindElement(indices_vbo);
		}
		{
			curve_vao = std::make_unique<vertex_array_t>();

			vertex_buffer_t vertex_buffer;
			vertex_buffer.create();
			vertex_buffer.allocate(0, GL_DYNAMIC_DRAW);

			const auto vb = vertex_array_t::make_vbo(vertex_buffer);
			curve_vao->bindVBO(vb, 0, 3, GL_FLOAT, sizeof(curve2d_mesh_data_t::line_vertex_t), 0);
			curve_vao->bindVBO(vb, 1, 2, GL_FLOAT, sizeof(curve2d_mesh_data_t::line_vertex_t), sizeof(vec3));
		}

		square_shader = shader_t::make_unique(shader_2d_textured_vert, shader_2d_textured_frag);
		square_shader->bindAttribute(0, "vertex");
		square_shader->bindAttribute(1, "uv_in");

		point_shader = shader_t::make_unique(shader_2d_textured_vert, shader_2d_textured_cirlce_frag);
		point_shader->bindAttribute(0, "vertex");
		point_shader->bindAttribute(1, "uv_in");

		engine->create();
	}

	void batch_renderer_2d::drawRectangle(const rectangle2d_t& rectangle, const std::string_view texture, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_rectangles, render_info_t::make_info(texture), {-z_index, rectangle});
	}

	void batch_renderer_2d::drawRectangle(const rectangle2d_t& rectangle, const vec4& color, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_rectangles, render_info_t::make_info(color), {-z_index, rectangle});
	}

	void batch_renderer_2d::drawRectangle(const rectangle2d_t& rectangle, const render_info_t& draw_info, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_rectangles, draw_info, {-z_index, rectangle});
	}

	void batch_renderer_2d::drawLine(const line2d_t& line, const std::string_view texture, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_lines, render_info_t::make_info(texture), {-z_index, line});
	}

	void batch_renderer_2d::drawLine(const line2d_t& line, const vec4& color, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_lines, render_info_t::make_info(color), {-z_index, line});
	}

	void batch_renderer_2d::drawLine(const line2d_t& line, const render_info_t& draw_info, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_lines, draw_info, {-z_index, line});
	}

	void batch_renderer_2d::drawPoint(const point2d_t& point, const std::string_view texture, const f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_points, render_info_t::make_info(texture), {-z_index, point});
	}

	void batch_renderer_2d::drawPoint(const point2d_t& point, const vec4& color, f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_points, render_info_t::make_info(color), {-z_index, point});
	}

	void batch_renderer_2d::drawPoint(const point2d_t& point, const render_info_t& draw_info, f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_points, draw_info, {-z_index, point});
	}

	void batch_renderer_2d::drawCurve(const curve2d_mesh_data_t& curve, std::string_view texture, f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_curves, render_info_t::make_info(texture), {-z_index, curve});
	}

	void batch_renderer_2d::drawCurve(const curve2d_mesh_data_t& curve, const vec4& color, f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_curves, render_info_t::make_info(color), {-z_index, curve});
	}

	void batch_renderer_2d::drawCurve(const curve2d_mesh_data_t& curve, const render_info_t& draw_info, f32 z_index)
	{
		update_z_index(z_index);
		insert_obj(draw.complex_curves, draw_info, {-z_index, curve});
	}

	void batch_renderer_2d::cleanup()
	{
		engine->cleanup();
		engine = nullptr;
		square_vao = nullptr;
		line_vao = nullptr;
		curve_vao = nullptr;
		square_shader = nullptr;
		point_shader = nullptr;
	}

	void batch_renderer_2d::render(i32, i32, const bool transparency, const bool postprocessing)
	{
		//draw_buffer.bind();
		//draw_buffer.updateBuffersStorage(width, height);
		//glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		if (transparency)
		{
			glEnable(GL_BLEND);
			glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		}
		glEnable(GL_DEPTH_TEST);
		glActiveTexture(GL_TEXTURE0);

		if (engine && postprocessing)
			engine->bind();
		draw_objects();

		glDisable(GL_DEPTH_TEST);
		if (transparency)
			glDisable(GL_BLEND);

		if (engine && postprocessing)
			engine->render();
		render_reset();
	}

	void batch_renderer_2d::draw_objects()
	{
		pre_reset();
		const f32 denominator = 1.0f / (draw.z_max - draw.z_min);

		glClear(GL_STENCIL_BUFFER_BIT);
		draw_points(denominator);
		draw_rectangles(denominator);
		draw_lines(denominator);
		draw_curves(denominator);
		post_reset();
	}

	void batch_renderer_2d::render_reset()
	{
		draw.z_min = std::numeric_limits<f32>::max();
		draw.z_max = std::numeric_limits<f32>::min();
	}

	void batch_renderer_2d::pre_reset()
	{
		draw.draw_count = 0;
	}

	void batch_renderer_2d::post_reset()
	{}

	void batch_renderer_2d::draw_points(const f32 denominator)
	{
		point_shader->bind();
		square_vao->bind();
		for (auto& [texture, objects] : draw.complex_points)
		{
			// resource manager handles the check for empty string
			if (auto val = resources.get(texture))
				val.value()->bind();
			for (auto& [render_info, object] : objects)
			{
				auto& [z_index, point] = object;

				mat4x4 model;
				model.translate(point.pos.x(), point.pos.y(), 0.0f);
				model.scale(point.scale, point.scale, 1.0);
				point_shader->setVec4("color", render_info.color);
				point_shader->setVec4("use_texture", render_info.blend);

				//point_shader->setVec4("outline_color", render_info.outline_color);
				point_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);
				point_shader->setMatrix("model", model);

				glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
				draw.draw_count++;
			}
			objects.clear();
		}
	}

	void batch_renderer_2d::draw_lines(const f32 denominator)
	{
		mat4x4 empty_model;
		square_shader->setMatrix("model", empty_model);
		line_vao->bind();
		auto& buf = line_vao->getBuffer(0);
		buf.bind();
		for (auto& [texture, objects] : draw.complex_lines)
		{
			// resource manager handles the check for empty string
			if (auto val = resources.get(texture))
				val.value()->bind();
			for (auto& [render_info, object] : objects)
			{
				auto& [z_index, line] = object;

				float thickness = line.thickness;
				square_shader->setVec4("color", render_info.color);
				square_shader->setVec4("use_texture", render_info.blend);

				//square_shader->setVec4("outline_color", render_info.outline_color);
				square_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);

				// 0, 1 (top right)
				// 5, 6 (bottom right)
				// 10, 11 (bottom left)
				// 15, 16 (top left)
				vec2 dir = (line.p1 - line.p2).normalize();
				vec2 right = {dir.y(), -dir.x()};
				vec2 left = {-dir.y(), dir.x()};

				auto bottom_left = line.p1 + left * thickness;
				auto bottom_right = line.p1 + right * thickness;

				auto top_left = line.p2 + left * thickness;
				auto top_right = line.p2 + right * thickness;

				line_vertices[0] = top_right.x();
				line_vertices[1] = top_right.y();

				line_vertices[5] = bottom_right.x();
				line_vertices[6] = bottom_right.y();

				line_vertices[10] = bottom_left.x();
				line_vertices[11] = bottom_left.y();

				line_vertices[15] = top_left.x();
				line_vertices[16] = top_left.y();

				buf.update(sizeof(line_vertices), line_vertices);

				glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
				draw.draw_count++;
			}
			objects.clear();
		}
	}

	void batch_renderer_2d::draw_curves(const f32 denominator)
	{
		curve_vao->bind();
		mat4x4 empty_model;
		square_shader->setMatrix("model", empty_model);
		for (auto& [texture, objects] : draw.complex_curves)
		{
			if (auto val = resources.get(texture))
				val.value()->bind();
			for (auto& [render_info, object] : objects)
			{
				auto& [z_index, curve] = object;
				square_shader->setVec4("color", render_info.color);
				square_shader->setVec4("use_texture", render_info.blend);
				square_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);

				const auto count = curve.populate_vertex_array(*curve_vao);
				glDrawArrays(GL_TRIANGLE_STRIP, 0, static_cast<i32>(count));

				draw.draw_count++;
			}
			objects.clear();
		}
	}

	void batch_renderer_2d::draw_rectangles(const f32 denominator)
	{
		square_shader->bind();
		square_vao->bind();
		for (auto& [texture, objects] : draw.complex_rectangles)
		{
			// resource manager handles the check for empty string
			if (auto val = resources.get(texture))
				val.value()->bind();
			for (auto& [render_info, object] : objects)
			{
				auto& [z_index, rect] = object;

				mat4x4 model;
				model.translate(rect.pos);
				model.scale(rect.size);
				square_shader->setVec4("color", render_info.color);
				square_shader->setVec4("use_texture", render_info.blend);
				if (rect.rotation != 0)
					model.rotateZ(toRadians(rect.rotation));

				//square_shader->setVec4("outline_color", render_info.outline_color);
				square_shader->setFloat("z_index", (z_index - draw.z_min) * denominator);
				square_shader->setMatrix("model", model);
				glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
				draw.draw_count++;
			}
			objects.clear();
		}
	}
}
