#include <psemek/fonts/font_v2.hpp>

#include <psemek/gfx/pixmap.hpp>
#include <psemek/util/exception.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/util/unicode.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/log/log.hpp>

#include <psemek/io/file_stream.hpp>

#include <ft2build.h>
#include FT_FREETYPE_H

#include <iomanip>

namespace psemek::fonts
{

	namespace
	{

		void ft_check_result(FT_Error error, std::string const & message)
		{
			if (error != 0)
			{
				if (auto error_str = FT_Error_String(error))
					throw util::exception(message + error_str);
				else
					throw util::exception(message + util::to_string("0x", std::hex, error));
			}
		}

		struct ft_initializer
		{
			ft_initializer()
			{
				ft_check_result(FT_Init_FreeType(&library_), "Failed to initialize Freetype library: ");

				int major, minor, patch;
				FT_Library_Version(library_, &major, &minor, &patch);
				log::info() << "Initialized Freetype " << major << '.' << minor << '.' << patch;
			}

			~ft_initializer()
			{
				if (auto error = FT_Done_FreeType(library_); error != 0)
				{
					if (auto error_str = FT_Error_String(error))
						log::warning() << "Failed to deinitialize Freetype library: " << error_str;
					else
						log::warning() << "Failed to deinitialize Freetype library: 0x" << std::hex << error;
				}
			}

			FT_Library library() const
			{
				return library_;
			}

		private:
			FT_Library library_;
		};

		FT_Library ft_library()
		{
			static ft_initializer initializer;

			return initializer.library();
		}

		struct face_shared
		{
			wgpu::device device;

			struct face_data
			{
				util::blob data;
				FT_Face face;
			};

			std::vector<face_data> faces = {};

			~face_shared()
			{
				for (auto const & face : faces)
					FT_Done_Face(face.face);
			}
		};

		struct freetype_font
			: font
		{
			freetype_font(std::shared_ptr<face_shared> state, font_type type, int size)
				: state_(state)
				, type_(type)
				, size_(size)
			{
				for (auto const & face : state_->faces)
					ft_check_result(FT_Set_Char_Size(face.face, 0, size_ << 6, 0, 0), "Failed to select freetype face size: ");

				font::shape("x", {});
				xheight_ = -shaped_text_[0].position[1].min;
				shaped_text_.clear();
			}

			font_type type() const override
			{
				return type_;
			}

			std::string_view name() const override
			{
				return FT_Get_Postscript_Name(state_->faces[0].face);
			}

			math::vector<int, 2> size() const override
			{
				return {size_, size_};
			}

			int xheight() const override
			{
				return xheight_;
			}

			std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options, math::point<float, 2> & pen) override
			{
				return shape_impl(util::utf8_range(str), options, pen);
			}

			std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options, math::point<float, 2> & pen) override
			{
				return shape_impl(str, options, pen);
			}

		private:
			std::shared_ptr<face_shared> state_;
			font_type type_;
			int size_;
			int xheight_;

			static constexpr int page_size = 256;
			static constexpr int padding = 1;

			struct page
			{
				util::ndarray<std::uint8_t, 2> pixmap;

				int current_row_start = 0;
				int current_row_height = 0;
				int current_row_x = 0;

				wgpu::texture texture;
				std::unique_ptr<wgpu::texture_view> texture_view;
				bool needs_update = true;
			};

			std::vector<page> pages_;

			struct glyph_mapping_data
			{
				std::uint32_t font_index;
				std::uint32_t glyph_index;
			};

			struct glyph_data
			{
				int page;
				math::box<int, 2> part;
				math::vector<int, 2> offset;
				math::vector<int, 2> advance;
			};

			util::hash_map<char32_t, glyph_mapping_data> glyph_mapping_;
			util::hash_map<std::uint32_t, util::hash_map<std::uint32_t, glyph_data>> glyphs_;

			std::vector<shaped_glyph> shaped_text_;

			template <typename String>
			std::vector<shaped_glyph> const & shape_impl(String const & string, shape_options const & options, math::point<float, 2> & pen)
			{
				(void)options; // TODO: support options

				shaped_text_.clear();

				bool need_update_pages = false;

				for (char32_t ch : string)
				{
					glyph_mapping_data mapping{0, 0};

					if (auto it = glyph_mapping_.find(ch); it != glyph_mapping_.end())
					{
						mapping = it->second;
					}
					else
					{
						int glyph_index = 0;
						for (int font_index = 0; font_index < state_->faces.size(); ++font_index)
						{
							glyph_index = FT_Get_Char_Index(state_->faces[font_index].face, ch);
							if (glyph_index != 0)
							{
								mapping = {font_index, glyph_index};
								break;
							}
						}

						if (glyph_index == 0)
						{
							mapping.font_index = 0;
							mapping.glyph_index = FT_Get_Char_Index(state_->faces[0].face, '?');
						}

						glyph_mapping_[ch] = mapping;
					}

					auto face = state_->faces[mapping.font_index].face;

					glyph_data * data = nullptr;

					auto & glyphs = glyphs_[mapping.font_index];

					if (auto it = glyphs.find(mapping.glyph_index); it != glyphs.end())
					{
						data = &(it->second);
					}
					else
					{
						data = &glyphs[mapping.glyph_index];

						FT_Load_Glyph(face, mapping.glyph_index, type_ == font_type::bitmap ? FT_LOAD_TARGET_LIGHT : FT_LOAD_DEFAULT);
						FT_Render_Glyph(face->glyph, type_ == font_type::bitmap ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_SDF);

						if (face->glyph->bitmap.width + 2 * padding > page_size || face->glyph->bitmap.rows + 2 * padding > page_size)
						{
							data->page = 0;
							data->part = {{{0, 0}, {0, 0}}};
							data->offset = {0, 0};
							data->advance = {size_, 0};
							log::warning() << "Glyph with ID " << mapping.glyph_index << " (U+" << std::hex << (int)ch << ") is larger than font atlas page: " << face->glyph->bitmap.width << "x" << face->glyph->bitmap.rows;
						}
						else
						{
							data->offset[0] = face->glyph->bitmap_left;
							data->offset[1] = - face->glyph->bitmap_top;
							data->advance[0] = face->glyph->advance.x >> 6;
							data->advance[1] = face->glyph->advance.y >> 6;

							bool need_new_page = false;

							if (pages_.empty())
							{
								need_new_page = true;
							}
							else
							{
								if (pages_.back().current_row_x + face->glyph->bitmap.width + 2 * padding > page_size)
								{
									pages_.back().current_row_start += pages_.back().current_row_height;
									pages_.back().current_row_height = 0;
									pages_.back().current_row_x = 0;
								}

								if (pages_.back().current_row_start + face->glyph->bitmap.rows + 2 * padding > page_size)
								{
									need_new_page = true;
								}
							}

							if (need_new_page)
							{
								pages_.emplace_back();
								pages_.back().pixmap.resize({page_size, page_size}, 0);

								pages_.back().texture = state_->device.create_texture({
									.usage = wgpu::texture::usage::copy_dst | wgpu::texture::usage::texture_binding,
									.dimension = wgpu::texture::dimension::_2d,
									.size = {page_size, page_size, 1},
									.format = wgpu::texture::format::r8unorm,
								});

								pages_.back().texture_view = std::make_unique<wgpu::texture_view>(pages_.back().texture.create_view(wgpu::texture_view::descriptor{
									.format = wgpu::texture::format::r8unorm,
									.dimension = wgpu::texture_view::dimension::_2d,
								}));
							}

							data->page = pages_.size() - 1;
							data->part[0].min = pages_.back().current_row_x + padding;
							data->part[0].max = data->part[0].min + face->glyph->bitmap.width;
							data->part[1].min = pages_.back().current_row_start + padding;
							data->part[1].max = data->part[1].min + face->glyph->bitmap.rows;

							for (int y = 0; y < face->glyph->bitmap.rows; ++y)
							{
								auto src_begin = face->glyph->bitmap.buffer + y * face->glyph->bitmap.pitch;
								auto src_end = src_begin + face->glyph->bitmap.width;
								auto dst_begin = pages_.back().pixmap.data() + (pages_.back().current_row_start + y + padding) * page_size + pages_.back().current_row_x + padding;

								std::copy(src_begin, src_end, dst_begin);
							}

							pages_.back().current_row_x += face->glyph->bitmap.width + 2 * padding;
							math::make_max<int>(pages_.back().current_row_height, face->glyph->bitmap.rows + 2 * padding);

							pages_.back().needs_update = true;
							need_update_pages = true;
						}
					}

					auto & result = shaped_text_.emplace_back();
					result.texture = pages_[data->page].texture_view.get();
					result.position[0].min = pen[0] + data->offset[0];
					result.position[0].max = result.position[0].min + data->part[0].length();
					result.position[1].min = pen[1] + data->offset[1];
					result.position[1].max = result.position[1].min + data->part[1].length();
					result.texcoords[0].min = static_cast<float>(data->part[0].min) / page_size;
					result.texcoords[0].max = static_cast<float>(data->part[0].max) / page_size;
					result.texcoords[1].min = static_cast<float>(data->part[1].min) / page_size;
					result.texcoords[1].max = static_cast<float>(data->part[1].max) / page_size;

					pen += math::cast<float>(data->advance);
				}

				if (need_update_pages)
				{
					for (auto & page : pages_)
					{
						if (!page.needs_update) continue;
						page.needs_update = false;

						state_->device.get_queue().write_texture({.texture = page.texture}, {(char *)page.pixmap.data(), page.pixmap.size()}, {.bytes_per_row = page_size, .rows_per_image = page_size}, {page_size, page_size, 1});
					}
				}

				return shaped_text_;
			}
		};

		struct freetype_font_builder
			: font_builder
		{
			freetype_font_builder(std::shared_ptr<face_shared> state)
				: state_(state)
			{}

			std::string_view name() const override
			{
				return FT_Get_Postscript_Name(state_->faces[0].face);
			}

			std::unique_ptr<font> create(font_type type, int size) override
			{
				if (type == font_type::msdf)
					throw util::exception("MSDF fonts are not supported by Freetype");
				return std::make_unique<freetype_font>(state_, type, size);
			}

		private:
			std::shared_ptr<face_shared> state_;
		};

	}

	std::unique_ptr<font_builder> load_freetype_font(wgpu::device device, std::filesystem::path const & path)
	{
		return load_combined_freetype_font(device, {path});
	}

	std::unique_ptr<font_builder> load_combined_freetype_font(wgpu::device device, std::vector<std::filesystem::path> const & paths)
	{
		auto result = std::make_shared<face_shared>(device);

		for (auto const & path : paths)
		{
			auto font_data = io::read_full(io::file_istream{path});

			FT_Face face = nullptr;
			ft_check_result(FT_New_Memory_Face(ft_library(), reinterpret_cast<FT_Byte const *>(font_data.data()), font_data.size(), 0, &face), util::to_string("Failed to load font ", path, ": "));

			result->faces.push_back({std::move(font_data), face});

			log::debug() << "Loaded font " << FT_Get_Postscript_Name(face) << " (" << path << ")";
		}

		return std::make_unique<freetype_font_builder>(std::move(result));
	}

}
