#include <psemek/ui/label.hpp>

#include <psemek/math/contains.hpp>

#include <psemek/sdl2/cursor.hpp>

#include <stdexcept>
#include <cctype>
#include <unordered_map>

namespace psemek::ui
{

	label::label(std::string text)
	{
		set_text(std::move(text));
	}

	void label::set_text(std::string text)
	{
		if (selected_link_)
		{
			static auto arrow_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::arrow);
			sdl2::set_cursor(*arrow_cursor);
			selected_link_ = std::nullopt;
		}
		text_ = std::move(text);
		tagged_ = false;

		chunks_.clear();
		chunks_.push_back(text_chunk{{text_.data(), text_.size()}, text_style_, std::nullopt, std::nullopt});

		on_state_changed();
	}

	static std::unordered_set<std::string_view> const known_tags
	{
		"bold",
		"uline",
		"strike",
		"color",
		"link",
	};

	void check_attribute(std::string const & tag, std::optional<std::string> const & attribute)
	{
		if (tag == "bold" || tag == "uline" || tag == "strike")
		{
			if (attribute)
				throw std::runtime_error("tag [" + tag + "] doesn't support attributes");
		}
		else if (tag == "color")
		{
			if (!attribute)
				throw std::runtime_error("tag [color] requires an attribute");
			if (!gfx::parse_color(*attribute))
				throw std::runtime_error("failed to parse color \"" + std::string(*attribute) + "\"");
		}
		else if (tag == "link")
		{
			if (!attribute)
				throw std::runtime_error("tag [link] requires an attribute");
		}
	}

	void label::set_tagged_text(std::string text)
	{
		if (selected_link_)
		{
			static auto arrow_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::arrow);
			sdl2::set_cursor(*arrow_cursor);
			selected_link_ = std::nullopt;
		}
		text_ = std::move(text);
		tagged_ = true;
		selected_link_ = std::nullopt;

		chunks_.clear();

		auto parse_result = tagged_text::parse(text_);

		if (tag_mapper_)
		{
			std::vector<tagged_text::token> mapped;
			for (auto const & token : parse_result.tokens)
			{
				auto replace = tag_mapper_(token);
				if (replace)
					mapped.insert(mapped.end(), replace->begin(), replace->end());
				else
					mapped.push_back(token);
			}
			parse_result.tokens = std::move(mapped);
		}

		std::unordered_map<std::string, std::vector<std::optional<std::string>>> tags_stack;

		for (auto const & token : parse_result.tokens)
		{
			if (auto text = std::get_if<std::string>(&token))
			{
				text_chunk chunk;
				if (!tags_stack["bold"].empty())
					chunk.style.set(text_style_flag::bold);
				if (!tags_stack["uline"].empty())
					chunk.style.set(text_style_flag::underline);
				if (!tags_stack["strike"].empty())
					chunk.style.set(text_style_flag::strikethrough);
				if (!tags_stack["color"].empty())
					chunk.color = *gfx::parse_color(*tags_stack["color"].back());
				if (!tags_stack["link"].empty())
					chunk.link = tags_stack["link"].back();
				chunk.text = *text;
				chunks_.push_back(chunk);
			}
			else if (auto tag = std::get_if<tagged_text::opening_tag>(&token))
			{
				if (tag->type == "image")
				{
					if (!tag->attribute)
						throw std::runtime_error("tag [image] requires an attribute");
					image_chunk chunk;
					chunk.id = *tag->attribute;
					if (!tags_stack["color"].empty())
						chunk.color = *gfx::parse_color(*tags_stack["color"].back());
					if (!tags_stack["link"].empty())
						chunk.link = tags_stack["link"].back();
					chunks_.push_back(chunk);
				}
				else if (known_tags.contains(tag->type))
				{
					check_attribute(tag->type, tag->attribute);
					if (tag->type == "link" && !tags_stack["link"].empty())
						throw std::runtime_error("tag [link] cannot be nested");
					tags_stack[tag->type].push_back(tag->attribute);
				}
				else
					throw std::runtime_error("unknown tag [" + std::string(tag->type) + "]");
			}
			else if (auto tag = std::get_if<tagged_text::closing_tag>(&token))
			{
				if (!known_tags.contains(tag->type))
					throw std::runtime_error("unknown closing tag [" + std::string(tag->type) + "]");
				if (tags_stack[tag->type].empty())
					throw std::runtime_error("mismatched opening & closing tags for [" + std::string(tag->type) + "]");
				tags_stack[tag->type].pop_back();
			}
		}

		on_state_changed();
	}

	void label::set_text_style(ui::text_style style)
	{
		text_style_ = style;
		if (tagged_)
			set_tagged_text(std::move(text_));
		else
			set_text(std::move(text_));
	}

	void label::set_halign(halignment value)
	{
		halign_ = value;
		on_state_changed();
	}

	void label::set_valign(valignment value)
	{
		valign_ = value;
		on_state_changed();
	}

	void label::set_wrap(bool value)
	{
		wrap_ = value;
		on_state_changed();
	}

	void label::set_skip_spaces(bool value)
	{
		skip_spaces_ = value;
		on_state_changed();
	}

	image_provider * label::set_image_provider(struct image_provider * provider)
	{
		std::swap(provider, image_provider_);
		on_state_changed();
		return provider;
	}

	void label::set_tag_mapper(tagged_text::mapper mapper)
	{
		tag_mapper_ = std::move(mapper);
	}

	void label::on_link_mouseover(link_mouseover_callback callback)
	{
		link_mouseover_callback_ = std::move(callback);
	}

	void label::on_link_click(link_callback callback)
	{
		link_callback_ = std::move(callback);
	}

	void label::set_overflow(overflow_mode value)
	{
		overflow_ = value;
		on_state_changed();
	}

	void label::reshape(math::box<float, 2> const & bbox)
	{
		shape_.box = bbox;
		cached_state_.reset();
	}

	bool label::on_event(ui::mouse_move const & e)
	{
		if (cached_state_)
		{
			std::optional<std::string> new_selected_link;
			auto const p = math::cast<float>(e.position);
			for (auto const & b : cached_state_->link_bboxes)
			{
				if (math::contains(b.first, p))
				{
					new_selected_link = b.second;
					break;
				}
			}

			if (new_selected_link != selected_link_)
			{
				static auto arrow_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::arrow);
				static auto hand_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::hand);

				if (new_selected_link)
					sdl2::set_cursor(*hand_cursor);
				else
					sdl2::set_cursor(*arrow_cursor);

				if (link_mouseover_callback_)
					post([cb = link_mouseover_callback_, value = new_selected_link]{
						cb(value);
					});

				selected_link_ = new_selected_link;
				mouse_down_ = false;
				on_state_changed();
			}
		}

		return false;
	}

	bool label::on_event(ui::mouse_click const & e)
	{
		if (e.button == mouse_button::left)
		{
			if (e.down)
			{
				if (selected_link_)
				{
					if (!mouse_down_)
					{
						mouse_down_ = true;
						if (link_callback_)
							post([cb = link_callback_, text = *selected_link_]{ cb(text); });
						on_state_changed();
						return true;
					}
				}
			}
			else
			{
				if (mouse_down_)
				{
					mouse_down_ = false;
					on_state_changed();
				}
			}
		}

		return false;
	}

	math::box<float, 2> label::size_constraints() const
	{
		static float const inf = std::numeric_limits<float>::infinity();

		if (!cached_state_inf_)
			cached_state_inf_ = cached_state_for({{{0.f, inf}, {0.f, inf}}});

		return {{{cached_state_inf_->size[0], inf}, {cached_state_inf_->size[1], inf}}};
	}

	math::interval<float> label::width_constraints(float height) const
	{
		static float const inf = std::numeric_limits<float>::infinity();

		auto state = cached_state_for({{{0.f, inf}, {0.f, height}}});
		return {state.size[0], inf};
	}

	math::interval<float> label::height_constraints(float width) const
	{
		static float const inf = std::numeric_limits<float>::infinity();

		auto state = cached_state_for({{{0.f, width}, {0.f, inf}}});
		return {state.size[1], inf};
	}

	void label::style_updated()  const
	{
		element::style_updated();
		cached_state_.reset();
		cached_state_inf_.reset();
	}

	void label::own_style_updated() const
	{
		element::own_style_updated();
		cached_state_.reset();
		cached_state_inf_.reset();
	}

	void label::draw(painter & p) const
	{
		if (!cached_state_)
			update_cached_state();

		auto st = merged_own_style();
		if (!st) return;

		if (st->text_shadow_offset != math::vector{0, 0})
		{
			auto const offset = math::cast<float>(*st->text_shadow_offset);
			for (auto const & batch : cached_state_->batches)
			{
				if (!batch.text) continue;
				auto color = *st->shadow_color;
				color[3] = (color[3] * 1.f * batch.color[3]) / 255.f;
				for (auto const & image : batch.images)
				{
					if (cached_state_->font_type == fonts::font_type::bitmap)
						p.draw_image(image.position + offset, gfx::texture_view_2d{batch.texture, image.texcoords}, {color, painter::color_mode::multiply});
					else if (cached_state_->font_type == fonts::font_type::msdf)
						p.draw_msdf_glyph(image.position + offset, gfx::texture_view_2d{batch.texture, image.texcoords}, cached_state_->sdf_scale, {color});
				}
			}
		}

		for (auto const & batch : cached_state_->batches)
		{
			for (auto const & image : batch.images)
			{
				if (cached_state_->font_type == fonts::font_type::bitmap)
					p.draw_image(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, {batch.color, batch.text ? painter::color_mode::multiply : painter::color_mode::mix});
				else if (cached_state_->font_type == fonts::font_type::msdf)
					p.draw_msdf_glyph(image.position, gfx::texture_view_2d{batch.texture, image.texcoords}, cached_state_->sdf_scale, {batch.color});
			}
		}
	}

	void label::on_state_changed()
	{
		cached_state_.reset();
		cached_state_inf_.reset();
		post_reshape();
	}

	static bool is_newline(char32_t c)
	{
		return (c == '\n') || (c == '\r');
	}

	void label::update_cached_state() const
	{
		cached_state_ = cached_state_for(shape_.box);
	}

	label::cached_state label::cached_state_for(math::box<float, 2> const & bbox) const
	{
		auto state = cached_state{};

		if (chunks_.empty()) return state;

		auto st = merged_own_style();

		state.font_type = st->font->type();
		state.sdf_scale = st->font->sdf_scale();

		auto const font_height = st->font->size()[1] * (*st->text_scale);

		// Shape items (glyphs/images) chunk by chunk
		// All items within a chunk have
		// the exact same properties

		struct item
		{
			math::box<float, 2> position;
			std::optional<char32_t> character;
		};

		struct item_chunk
		{
			std::size_t end;
			ui::text_style style;
			gfx::color_rgba color;
			gfx::texture_view_2d image;
			std::optional<std::string> link;
		};

		std::vector<item> items;
		std::vector<item_chunk> item_chunks;

		{
			math::point<float, 2> pen{0.f, 0.f};
			fonts::shape_options opts;
			opts.scale = *st->text_scale;

			for (auto const & chunk : chunks_)
			{
				if (auto text = std::get_if<text_chunk>(&chunk))
				{
					auto text_style = *st->text_style | text->style;

					if (text->link)
					{
						if (selected_link_ && *text->link == *selected_link_)
						{
							if (mouse_down_)
								text_style |= *st->link_click_style;
							else
								text_style |= *st->link_hover_style;
						}
						else
							text_style |= *st->link_style;
					}

					gfx::color_rgba color;
					if (text->color)
						color = *text->color;
					else if (text->link)
					{
						if (selected_link_ && *text->link == *selected_link_)
						{
							if (mouse_down_)
								color = *st->link_click_color;
							else
								color = *st->link_hover_color;
						}
						else
							color = *st->link_color;
					}
					else
						color = *st->text_color;

					bool const bold = text_style.is_set(text_style_flag::bold);

					auto font = bold ? st->bold_font.get() : st->font.get();

					auto glyphs = font->shape(text->text, opts, pen);

					items.reserve(items.size() + glyphs.size());
					for (auto const & g : glyphs)
						items.push_back(item{g.position, g.character});

					item_chunks.push_back({items.size(), text_style, color, {}, text->link});
				}
				else if (auto image = std::get_if<image_chunk>(&chunk))
				{
					auto color = image->color ? *image->color : gfx::color_rgba{255, 255, 255, 0};

					if (!image_provider_)
						throw std::runtime_error("cannot use [image] tag without image provider");

					auto texture = image_provider_->get(image->id);

					float const scale = *st->text_scale;

					math::box<float, 2> position;
					position[0].min = pen[0];
					position[0].max = pen[0] + texture.width() * scale;
					position[1].min = pen[1] + font_height / 2.f - texture.height() * scale / 2.f;
					position[1].max = position[1].min + texture.height() * scale;

					pen[0] = position[0].max;

					items.push_back({position, {}});
					item_chunks.push_back({items.size(), {}, color, texture, image->link});
				}
			}
		}

		// Break items into lines

		struct line_range
		{
			std::size_t begin;
			std::size_t end;
		};

		std::vector<line_range> lines;

		{
			std::size_t current_item = 0;
			while (true)
			{
				if (skip_spaces_)
				{
					while (current_item < items.size() && items[current_item].character && std::isspace(*items[current_item].character))
						++current_item;
				}

				std::size_t line_begin = current_item;

				bool wrap_end = false;

				math::interval<float> x_range;
				while (current_item < items.size())
				{
					if (items[current_item].character && is_newline(*items[current_item].character))
					{
						break;
					}

					x_range |= items[current_item].position[0];

					if (x_range.length() > bbox[0].length() && wrap_)
					{
						wrap_end = true;
						break;
					}

					++current_item;
				}

				if (wrap_end && current_item == line_begin)
					break;

				if (wrap_end && current_item > line_begin)
				{
					std::size_t space_pos = current_item - 1;
					while (space_pos > line_begin && (!items[space_pos].character || !std::isspace(*items[space_pos].character)))
						--space_pos;

					if (space_pos > line_begin)
						current_item = space_pos;
				}

				lines.push_back({line_begin, current_item});

				if (current_item == items.size())
					break;

				++current_item;
			}
		}

		// Compute line bboxes

		std::vector<math::box<float, 2>> line_bbox(lines.size());
		for (std::size_t l = 0; l < lines.size(); ++l)
		{
			math::box<float, 2> bbox;
			// Glyphs might be smaller than total text height, so
			// hardcode font height as min line height
			bbox[1] = {0.f, 1.f * font_height};
			for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
				bbox[0] |= items[i].position[0];
			line_bbox[l] = bbox;
		}

		// Compute total text size

		math::vector<float, 2> text_size{0.f, 0.f};
		for (auto const & l : line_bbox)
		{
			text_size[0] = std::max(text_size[0], l[0].length());
			text_size[1] += l[1].length();
		}

		// TODO: handle text overflow

		// Position lines & items inside label bbox

		std::vector<math::vector<float, 2>> line_offset(lines.size());
		{
			float current_y;
			switch (valign_)
			{
			case valignment::top:
				current_y = bbox[1].min;
				break;
			case valignment::center:
				current_y = bbox[1].center() - text_size[1] / 2.f;
				break;
			case valignment::bottom:
				current_y = bbox[1].max - text_size[1];
				break;
			}

			for (std::size_t l = 0; l < lines.size(); ++l)
			{
				int spaces = 0;

				for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
					if (items[i].character && std::isspace(*items[i].character))
						++spaces;

				math::vector<float, 2> offset{0.f, current_y - line_bbox[l][1].min};
				float space_extra = 0.f;

				switch (halign_)
				{
				case halignment::left:
					offset[0] = bbox[0].min - line_bbox[l][0].min;
					break;
				case halignment::center:
					offset[0] = bbox[0].center() - line_bbox[l][0].length() / 2.f - line_bbox[l][0].min;
					break;
				case halignment::right:
					offset[0] = bbox[0].max - line_bbox[l][0].length() - line_bbox[l][0].min;
					break;
				case halignment::stretch:
					offset[0] = bbox[0].min - line_bbox[l][0].min;
					if ((l + 1 != lines.size()) && (lines[l].end >= items.size() || !items[lines[l].end].character || !is_newline(*items[lines[l].end].character)) && spaces > 0)
						space_extra = (bbox[0].length() - line_bbox[l][0].length()) / spaces;
					break;
				}

				line_offset[l] = offset;

				for (std::size_t i = lines[l].begin; i < lines[l].end; ++i)
				{
					items[i].position += offset;
					if (items[i].character && std::isspace(*items[i].character))
						offset[0] += space_extra;
				}

				current_y += line_bbox[l][1].length();
			}
		}

		// Convert chunks into image batches

		{
			std::size_t begin = 0;
			for (auto const & ch : item_chunks)
			{
				auto & batch = state.batches.emplace_back();

				if (ch.image)
				{
					batch.texture = ch.image.texture;
					batch.color = ch.color;
					batch.text = false;

					for (std::size_t i = begin; i < ch.end; ++i)
					{
						auto & g = items[i];

						g.position[0] += (std::round(g.position[0].min) - g.position[0].min);
						g.position[1] += (std::round(g.position[1].min) - g.position[1].min);

						auto & image = batch.images.emplace_back();
						image.position = g.position;
						image.texcoords = ch.image.part;
					}
				}
				else
				{
					auto font = ch.style.is_set(text_style_flag::bold) ? st->bold_font.get() : st->font.get();

					batch.texture = &font->atlas();
					batch.color = ch.color;
					batch.text = true;

					for (std::size_t i = begin; i < ch.end; ++i)
					{
						auto & g = items[i];
						auto tc = font->texcoords(*g.character);
						if (!tc) continue;

						g.position[0] += (std::round(g.position[0].min) - g.position[0].min);
						g.position[1] += (std::round(g.position[1].min) - g.position[1].min);

						auto & image = batch.images.emplace_back();
						image.position = g.position;
						image.texcoords = *tc;
					}
				}

				begin = ch.end;
			}
		}

		// Convert chunks into underline & strikethrough batches

		{
			float const line_width = 1.f * (*st->text_scale);

			auto get_batch = [&](gfx::color_rgba const & color) -> cached_state::batch & {
				auto texture = single_white_pixel_texture().get();
				if (state.batches.empty() || state.batches.back().texture != texture || state.batches.back().color != color)
				{
					auto & b = state.batches.emplace_back();
					b.texture = texture;
					b.color = color;
					b.text = true;
				}
				return state.batches.back();
			};

			std::size_t ch_begin = 0;
			std::size_t ch = 0;
			for (std::size_t l = 0; l < lines.size(); ++l)
			{
				float underline_y = std::round(line_offset[l][1] + font_height - line_width);
				float strikethrough_y = std::round(line_offset[l][1] + font_height / 2.f - line_width / 2.f);

				std::optional<float> last_underline_end;
				std::optional<float> last_strikethrough_end;

				for (; ch < item_chunks.size(); ++ch)
				{
					auto const & chunk = item_chunks[ch];

					bool const underline = chunk.style.is_set(text_style_flag::underline);
					bool const strikethrough = chunk.style.is_set(text_style_flag::strikethrough);

					std::size_t ibegin = std::max(ch_begin,  lines[l].begin);
					std::size_t iend   = std::min(chunk.end, lines[l].end);

					if (ibegin < iend)
					{
						math::interval<float> x_range;
						for (std::size_t i = ibegin; i < iend; ++i)
							x_range |= items[i].position[0];

						if (underline)
						{
							auto & image = get_batch(chunk.color).images.emplace_back();
							image.position[0] = x_range;
							if (last_underline_end)
								image.position[0] |= *last_underline_end;
							image.position[1] = {underline_y, underline_y + line_width};
							last_underline_end = image.position[0].max;
						}
						else
							last_underline_end = std::nullopt;

						if (strikethrough)
						{
							auto & image = get_batch(chunk.color).images.emplace_back();
							image.position[0] = x_range;
							if (last_strikethrough_end)
								image.position[0] |= *last_strikethrough_end;
							image.position[1] = {strikethrough_y, strikethrough_y + line_width};
							last_strikethrough_end = image.position[0].max;
						}
						else
							last_strikethrough_end = std::nullopt;
					}

					if (chunk.end > lines[l].end)
						break;

					ch_begin = chunk.end;
				}
			}
		}

		// Generate link bboxes
		{
			std::size_t begin = 0;
			for (auto const & ch : item_chunks)
			{
				if (ch.link)
				{
					math::box<float, 2> bbox;
					for (std::size_t i = begin; i < ch.end; ++i)
						bbox |= items[i].position;

					state.link_bboxes.push_back({bbox, *ch.link});
				}

				begin = ch.end;
			}
		}

		state.size = text_size;

		return state;
	}

	std::shared_ptr<gfx::texture_2d> label::single_white_pixel_texture() const
	{
		static std::weak_ptr<gfx::texture_2d> texture;

		if (!single_white_pixel_texture_)
		{
			std::shared_ptr<gfx::texture_2d> ptr;

			if (!(ptr = texture.lock()))
			{
				ptr = std::make_shared<gfx::texture_2d>();

				gfx::pixmap_rgba pm({1, 1}, {255, 255, 255, 255});
				ptr->load(pm);
				ptr->nearest_filter();
				texture = ptr;
			}

			single_white_pixel_texture_ = ptr;
		}

		return single_white_pixel_texture_;
	}

}
