#include <psemek/gfx/painter.hpp>
#include <psemek/gfx/program.hpp>
#include <psemek/gfx/mesh.hpp>
#include <psemek/gfx/texture.hpp>
#include <psemek/gfx/resource/font_9x12_png.hpp>
#include <psemek/cg/triangulation/ear_clipping.hpp>
#include <psemek/math/constants.hpp>
#include <psemek/io/memory_stream.hpp>
#include <psemek/util/enum.hpp>

static const char vertex_source[] =
R"(
uniform mat4 u_transform;

layout (location = 0) in vec4 in_position;
layout (location = 1) in vec4 in_color;

out vec4 color;

void main()
{
	gl_Position = u_transform * in_position;
	color = in_color;
}
)";

static const char fragment_source[] =
R"(
in vec4 color;

out vec4 out_color;

void main()
{
	out_color = color;
}
)";

static const char text_vertex_source[] =
R"(
uniform mat4 u_transform;
uniform uvec2 u_texture_size;

layout (location = 0) in vec4 in_position;
layout (location = 1) in vec4 in_color;
layout (location = 2) in vec2 in_texcoord;

out vec4 color;
out vec2 texcoord;

void main()
{
	gl_Position = u_transform * in_position;
	color = in_color;
	texcoord = in_texcoord / vec2(u_texture_size);
}
)";

static const char text_fragment_source[] =
R"(
uniform sampler2D u_texture;

in vec4 color;
in vec2 texcoord;

out vec4 out_color;

void main()
{
	out_color = color * texture(u_texture, texcoord);
}
)";

static const char texture_vertex_source[] =
R"(
uniform mat4 u_transform;

layout (location = 0) in vec4 in_position;
layout (location = 1) in vec2 in_texcoord;

out vec2 texcoord;

void main()
{
	gl_Position = u_transform * in_position;
	texcoord = in_texcoord;
}
)";

static const char texture_fragment_source[] =
R"(
uniform sampler2D u_texture;
uniform vec4 u_color;

in vec2 texcoord;

out vec4 out_color;

void main()
{
	vec4 color = texture(u_texture, texcoord);
	color.rgb = mix(color.rgb, u_color.rgb, u_color.a);
	out_color = color;
}
)";

namespace psemek::gfx
{

	static std::string shader_prefix = gl::sys::shader_prefix();

	struct painter::impl
	{
		struct vertex
		{
			math::point<float, 3> position;
			color_rgba color;
		};

		struct text_vertex
		{
			math::point<float, 3> position;
			color_rgba color;
			math::point<std::uint16_t, 2> texcoord;
		};

		struct texture_vertex
		{
			math::point<float, 3> position;
			math::point<std::uint16_t, 2> texcoord;
		};

		gfx::program program{shader_prefix + vertex_source, shader_prefix + fragment_source};
		gfx::program text_program{shader_prefix + text_vertex_source, shader_prefix + text_fragment_source};
		gfx::program texture_program{shader_prefix + texture_vertex_source, shader_prefix + texture_fragment_source};
		gfx::mesh mesh;
		gfx::mesh text_mesh;
		gfx::mesh texture_mesh;

		std::vector<vertex> vertices;
		std::vector<std::uint32_t> indices;

		std::vector<text_vertex> text_vertices;
		std::vector<std::uint32_t> text_indices;

		struct texture_render_data
		{
			std::vector<texture_vertex> vertices;
			std::vector<std::uint32_t> indices;
			texture_2d const * texture;
			color_rgba c;
		};

		std::vector<texture_render_data> textures;

		texture_2d font_texture;

		impl()
		{
			mesh.setup<math::vector<float, 3>, gfx::normalized<color_rgba>>();
			text_mesh.setup<math::point<float, 3>, gfx::normalized<color_rgba>, math::point<std::uint16_t, 2>>();
			texture_mesh.setup<math::point<float, 3>, gfx::normalized<math::point<std::uint16_t, 2>>>();

			font_texture.load(gfx::read_image<std::uint8_t>(io::memory_istream{resource::font_9x12_png.data}));

			font_texture.bind();
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_SWIZZLE_G, gl::RED);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_SWIZZLE_B, gl::RED);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_SWIZZLE_A, gl::RED);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE);
		}
	};

	painter::painter()
		: pimpl_{make_impl()}
	{}

	painter::~painter() = default;

	void painter::triangle(math::point<float, 2> const & p0, math::point<float, 2> const & p1, math::point<float, 2> const & p2, color const & c)
	{
		triangle(p0, p1, p2, c, c, c);
	}

	void painter::triangle(math::point<float, 2> const & p0, math::point<float, 2> const & p1, math::point<float, 2> const & p2, color const & c0, color const & c1, color const & c2)
	{
		std::uint32_t const base = impl().vertices.size();

		impl().vertices.push_back({{p0[0], p0[1], 0.f}, c0});
		impl().vertices.push_back({{p1[0], p1[1], 0.f}, c1});
		impl().vertices.push_back({{p2[0], p2[1], 0.f}, c2});

		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 1);
		impl().indices.push_back(base + 2);
	}

	void painter::rect(math::box<float, 2> const & box, color const & c)
	{
		std::uint32_t const base = impl().vertices.size();

		impl().vertices.push_back({{box[0].min, box[1].min, 0.f}, c});
		impl().vertices.push_back({{box[0].max, box[1].min, 0.f}, c});
		impl().vertices.push_back({{box[0].min, box[1].max, 0.f}, c});
		impl().vertices.push_back({{box[0].max, box[1].max, 0.f}, c});

		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 1);
		impl().indices.push_back(base + 3);
		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 3);
		impl().indices.push_back(base + 2);
	}

	void painter::circle(math::point<float, 2> const & p, float r, color const & c, int quality)
	{
		circle(p, r, c, c, quality);
	}

	void painter::circle(math::point<float, 2> const & p, float r, color const & c0, color const & c1, int quality)
	{
		std::uint32_t const base = impl().vertices.size();

		impl().vertices.push_back({{p[0], p[1], 0.f}, c0});
		for (int i = 0; i < quality; ++i)
		{
			float const a = (math::pi * 2.f * i) / quality;
			impl().vertices.push_back({{p[0] + r * std::cos(a), p[1] + r * std::sin(a), 0.f}, c1});
		}

		for (int i = 0; i < quality; ++i)
		{
			impl().indices.push_back(base);
			impl().indices.push_back(base + 1 + i);
			impl().indices.push_back(base + 1 + ((i + 1) % quality));
		}
	}

	void painter::line(math::point<float, 2> const & p0, math::point<float, 2> const & p1, float width, color const & c, bool smooth)
	{
		line(p0, p1, width, width, c, c, smooth);
	}

	void painter::line(math::point<float, 2> const & p0, math::point<float, 2> const & p1, float width0, float width1, color const & c0, color const & c1, bool smooth)
	{
		std::uint32_t const base = impl().vertices.size();
		float const r0 = width0 / 2.f;
		float const r1 = width1 / 2.f;

		auto const d = math::normalized(p1 - p0);
		math::vector<float, 2> const o { -d[1], d[0] };

		impl().vertices.push_back({{p0[0] + r0 * o[0], p0[1] + r0 * o[1], 0.f}, c0});
		impl().vertices.push_back({{p0[0] - r0 * o[0], p0[1] - r0 * o[1], 0.f}, c0});
		impl().vertices.push_back({{p1[0] + r1 * o[0], p1[1] + r1 * o[1], 0.f}, c1});
		impl().vertices.push_back({{p1[0] - r1 * o[0], p1[1] - r1 * o[1], 0.f}, c1});

		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 1);
		impl().indices.push_back(base + 3);
		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 3);
		impl().indices.push_back(base + 2);

		if (smooth)
		{
			circle(p0, r0, c0);
			circle(p1, r1, c1);
		}
	}

	void painter::besier(math::point<float, 2> const & p0, math::point<float, 2> const & p1, math::point<float, 2> const & p2, float width, color const & c, int quality, bool smooth)
	{
		auto at = [&](float t)
		{
			return p0 + (2.f * t) * (p1 - p0) + (t * t) * ((p2 - p1) + (p0 - p1));
		};

		for (int i = 0; i < quality; ++i)
		{
			float t0 = (i * 1.f) / quality;
			float t1 = ((i + 1) * 1.f) / quality;

			line(at(t0), at(t1), width, c, smooth);
		}
	}

	void painter::polygon(util::span<math::point<float, 2> const> points, color const & c)
	{
		auto dcel = cg::ear_clipping<std::uint32_t>(points.begin(), points.end());

		for (std::uint32_t i = 0; i < dcel.faces.size(); ++i)
		{
			auto e = dcel.face(i).edge();
			auto p0 = points[e.origin().index()]; e = e.next();
			auto p1 = points[e.origin().index()]; e = e.next();
			auto p2 = points[e.origin().index()];

			triangle(p0, p1, p2, c);
		}
	}

	math::vector<float, 2> painter::text_size(std::string_view str, font f)
	{
		// TODO: multiline text
		math::vector<float, 2> font_size;
		switch (f)
		{
		case font::font_9x12:
			font_size = {9.f, 12.f};
			break;
		default:
			throw util::unknown_enum_value_exception(f);
		}

		int max_line_width = 0;
		int line_count = 0;

		int last_line_start = 0;
		for (int i = 0; i < str.size(); ++i)
		{
			if (str[i] == '\n')
			{
				math::make_max(max_line_width, i - last_line_start);
				last_line_start = i;
				++line_count;
			}
		}

		if (last_line_start + 1 != str.size())
		{
			math::make_max(max_line_width, static_cast<int>(str.size()) - last_line_start);
			++line_count;
		}

		return {font_size[0] * max_line_width, font_size[1] * line_count};
	}

	void painter::text(math::point<float, 2> const & p, std::string_view str, text_options const & opts)
	{
		text3d(math::point{p[0], p[1], 0.f}, str, opts, math::matrix<float, 3, 3>::identity());
	}

	void painter::texture(gfx::texture_2d const & texture, math::box<float, 2> const & box, color const & c)
	{
		impl::texture_render_data data;
		data.texture = &texture;
		data.c = c;

		data.indices = {0, 1, 2, 2, 1, 3};

		std::uint16_t lo = 0;
		std::uint16_t hi = 65535;

		data.vertices.push_back({{box[0].min, box[1].min, 0.f}, {lo, lo}});
		data.vertices.push_back({{box[0].max, box[1].min, 0.f}, {hi, lo}});
		data.vertices.push_back({{box[0].min, box[1].max, 0.f}, {lo, hi}});
		data.vertices.push_back({{box[0].max, box[1].max, 0.f}, {hi, hi}});

		impl().textures.push_back(data);
	}

	void painter::axes(math::point<float, 3> const & p, float length, float width)
	{
		math::vector<float, 3> const d[3]
		{
			{ 1.f, 0.f, 0.f },
			{ 0.f, 1.f, 0.f },
			{ 0.f, 0.f, 1.f },
		};

		color const c[3]
		{
			{ 255, 0, 0, 255 },
			{ 0, 255, 0, 255 },
			{ 0, 0, 255, 255 },
		};

		for (int i = 0; i < 3; ++i)
		{
			int j = (i + 1) % 3;
			int k = (i + 2) % 3;

			std::uint32_t const base = impl().vertices.size();

			impl().vertices.push_back({p + d[i] * length, c[i]});
			impl().vertices.push_back({p + d[j] * width, c[i]});
			impl().vertices.push_back({p + d[k] * width, c[i]});
			impl().vertices.push_back({p - d[j] * width, c[i]});
			impl().vertices.push_back({p - d[k] * width, c[i]});

			impl().indices.push_back(base + 0);
			impl().indices.push_back(base + 1);
			impl().indices.push_back(base + 2);
			impl().indices.push_back(base + 0);
			impl().indices.push_back(base + 2);
			impl().indices.push_back(base + 3);
			impl().indices.push_back(base + 0);
			impl().indices.push_back(base + 3);
			impl().indices.push_back(base + 4);
			impl().indices.push_back(base + 0);
			impl().indices.push_back(base + 4);
			impl().indices.push_back(base + 1);
		}

		std::uint32_t const base = impl().vertices.size();

		color const white {255, 255, 255, 255};

		impl().vertices.push_back({p - d[0] * width, white});
		impl().vertices.push_back({p - d[1] * width, white});
		impl().vertices.push_back({p - d[2] * width, white});

		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 2);
		impl().indices.push_back(base + 1);
	}

	void painter::sphere(math::point<float, 3> const & p, float radius, color const & c, int quality)
	{
		std::uint32_t const base = impl().vertices.size();

		impl().vertices.push_back({p - math::vector{0.f, 0.f, radius}, c});
		impl().vertices.push_back({p + math::vector{0.f, 0.f, radius}, c});

		auto const ray = [](float ax, float ay)
		{
			return math::vector{std::cos(ax) * std::cos(ay), std::sin(ax) * std::cos(ay), std::sin(ay)};
		};

		for (int y = - quality + 1; y < quality; ++y)
		{
			for (int x = 0; x < quality * 4; ++x)
			{
				float ax = (x * math::pi) / quality / 2.f;
				float ay = (y * math::pi) / quality / 2.f;

				impl().vertices.push_back({p + radius * ray(ax, ay), c});
			}
		}

		auto const index = [quality, base](int x, int y)
		{
			if (y == -quality)
				return base;
			if (y == quality)
				return base + 1;

			return static_cast<std::uint32_t>(2 + (y + quality - 1) * 4 * quality + x);
		};

		for (int x = 0; x < quality * 4; ++x)
		{
			int xx = (x + 1) % (quality * 4);

			impl().indices.push_back(index(x, - quality + 1));
			impl().indices.push_back(index(x, - quality));
			impl().indices.push_back(index(xx, - quality + 1));
		}

		for (int y = - quality + 1; y + 1 < quality; ++y)
		{
			for (int x = 0; x < quality * 4; ++x)
			{
				int yy = y + 1;
				int xx = (x + 1) % (quality * 4);

				impl().indices.push_back(index(x, y));
				impl().indices.push_back(index(xx, y));
				impl().indices.push_back(index(xx, yy));

				impl().indices.push_back(index(x, y));
				impl().indices.push_back(index(xx, yy));
				impl().indices.push_back(index(x, yy));
			}
		}

		for (int x = 0; x < quality * 4; ++x)
		{
			int xx = (x + 1) % (quality * 4);

			impl().indices.push_back(index(x, quality - 1));
			impl().indices.push_back(index(xx, quality - 1));
			impl().indices.push_back(index(x, quality));
		}
	}

	void painter::line3d(math::point<float, 3> const & p0, math::point<float, 3> const & p1, float width, color const & c)
	{
		std::uint32_t const base = impl().vertices.size();
		float const r = width / 2.f;

		auto const d = math::normalized(p1 - p0);
		math::vector<float, 3> const o = math::normalized(math::vector{ -d[1], d[0], 0.f });

		impl().vertices.push_back({{p0[0] + r * o[0], p0[1] + r * o[1], p0[2]}, c});
		impl().vertices.push_back({{p0[0] - r * o[0], p0[1] - r * o[1], p0[2]}, c});
		impl().vertices.push_back({{p1[0] + r * o[0], p1[1] + r * o[1], p1[2]}, c});
		impl().vertices.push_back({{p1[0] - r * o[0], p1[1] - r * o[1], p1[2]}, c});

		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 1);
		impl().indices.push_back(base + 3);
		impl().indices.push_back(base + 0);
		impl().indices.push_back(base + 3);
		impl().indices.push_back(base + 2);
	}

	void painter::text3d(math::point<float, 3> const & p, std::string_view str, text_options const & opts, math::matrix<float, 3, 3> const & t)
	{
		auto const size = math::pointwise_mult(text_size(str, opts.f), opts.scale);

		math::vector<float, 3> origin { 0.f, 0.f, 0.f };

		switch (opts.x)
		{
		case x_align::left:
			break;
		case x_align::center:
			origin[0] -= size[0] / 2.f;
			break;
		case x_align::right:
			origin[0] -= size[0];
			break;
		default:
			throw util::unknown_enum_value_exception(opts.x);
		}

		switch (opts.y)
		{
		case y_align::top:
			break;
		case y_align::center:
			origin[1] -= size[1] / 2.f;
			break;
		case y_align::bottom:
			origin[1] -= size[1];
			break;
		default:
			throw util::unknown_enum_value_exception(opts.y);
		}

		auto pen = origin;

		math::vector<float, 3> const sx = {9.f * opts.scale[0], 0.f, 0.f};
		math::vector<float, 3> const sy = {0.f, 12.f * opts.scale[1], 0.f};

		auto to_texcoord = [](int tx, int ty, int ix, int iy)
		{
			return math::point<std::uint16_t, 2>{ tx * 11 + (ix == 0 ? 1 : 10), ty * 14 + (iy == 0 ? 1 : 13) };
		};

		for (char c : str)
		{
			if (c == '\n')
			{
				pen[0] = origin[0];
				pen += sy;
				continue;
			}

			// Guard against unsigned char
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtype-limits"
#endif
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare"
#endif

			if ((c < 32) || (c >= 128)) c = '?';
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#ifdef __clang__
#pragma clang diagnostic pop
#endif

			int ty = (c - 32) / 16;
			int tx = (c - 32) % 16;

			std::uint32_t const base = impl().text_vertices.size();

			impl().text_vertices.push_back({p + t * pen, opts.c, to_texcoord(tx, ty, 0, 0)});
			impl().text_vertices.push_back({p + t * (pen + sx), opts.c, to_texcoord(tx, ty, 1, 0)});
			impl().text_vertices.push_back({p + t * (pen + sy), opts.c, to_texcoord(tx, ty, 0, 1)});
			impl().text_vertices.push_back({p + t * (pen + sx + sy), opts.c, to_texcoord(tx, ty, 1, 1)});

			impl().text_indices.push_back(base + 0);
			impl().text_indices.push_back(base + 1);
			impl().text_indices.push_back(base + 3);
			impl().text_indices.push_back(base + 0);
			impl().text_indices.push_back(base + 3);
			impl().text_indices.push_back(base + 2);

			pen += sx;
		}
	}

	void painter::render(math::matrix<float, 4, 4> const & transform)
	{
		gl::ActiveTexture(gl::TEXTURE0);

		gl::Disable(gl::CULL_FACE);

		gl::Enable(gl::BLEND);
		gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);

		gl::Disable(gl::DEPTH_TEST);

		impl().mesh.load(impl().vertices, impl().indices, gl::TRIANGLES, gl::STREAM_DRAW);
		impl().vertices.clear();
		impl().indices.clear();

		impl().text_mesh.load(impl().text_vertices, impl().text_indices, gl::TRIANGLES, gl::STREAM_DRAW);
		impl().text_vertices.clear();
		impl().text_indices.clear();

		impl().program.bind();
		impl().program["u_transform"] = transform;
		impl().mesh.draw();

		impl().text_program.bind();
		impl().text_program["u_transform"] = transform;
		impl().text_program["u_texture"] = 0;
		impl().text_program["u_texture_size"] = impl().font_texture.size();
		impl().font_texture.bind();
		impl().text_mesh.draw();

		impl().texture_program.bind();
		impl().texture_program["u_transform"] = transform;
		impl().texture_program["u_texture"] = 0;
		for (auto const & data : impl().textures)
		{
			impl().texture_program["u_color"] = to_colorf(data.c);
			impl().texture_mesh.load(data.vertices, data.indices, gl::TRIANGLES, gl::STREAM_DRAW);
			data.texture->bind();
			impl().texture_mesh.draw();
		}
		impl().textures.clear();
	}

}
