#include <psemek/ui/edit.hpp>

#include <psemek/util/to_string.hpp>
#include <psemek/util/unicode.hpp>

#include <psemek/sdl2/cursor.hpp>

namespace psemek::ui
{

	bool edit::set_text(std::string_view text, bool signal)
	{
		return set_text(util::from_utf8(std::string(text)), signal);
	}

	bool edit::set_text(std::u32string text, bool signal)
	{
		if (validator_ && !validator_(text))
			return false;

		text_ = std::move(text);
		caret_ = std::min(caret_, text_.size());
		on_state_changed();
		post_text_changed();
		if (signal)
			post_text_entered();

		return true;
	}

	void edit::set_font(font_type f)
	{
		if (font_ != f)
		{
			font_ = f;
			on_state_changed();
		}
	}

	void edit::set_halign(halignment value)
	{
		if (halign_ != value)
		{
			halign_ = value;
			on_state_changed();
		}
	}

	void edit::set_valign(valignment value)
	{
		if (valign_ != value)
		{
			valign_ = value;
			on_state_changed();
		}
	}

	bool edit::set_validator(validator_type validator)
	{
		if (validator && !validator(text_))
			return false;
		validator_ = std::move(validator);
		return true;
	}

	edit::validator_type edit::numeric(int max_length)
	{
		return [max_length](std::u32string_view const str) -> bool {
			auto begin = str.begin();
			auto end = str.end();
			if (begin != end && *begin == '-')
				++begin;

			return std::all_of(begin, end, [](char32_t c){ return '0' <= c && c <= '9'; }) && (end - begin) <= max_length;
		};
	}

	edit::validator_type edit::numeric_nonnegative(int max_length)
	{
		return [max_length](std::u32string_view const str) -> bool {
			return std::all_of(str.begin(), str.end(), [](char32_t c){ return '0' <= c && c <= '9'; }) && str.length() <= max_length;
		};
	}

	void edit::on_text_entered(callback_type callback, bool signal)
	{
		on_text_entered_ = std::move(callback);
		if (signal)
			post_text_entered();
	}

	void edit::on_start_input(std::function<void()> callback)
	{
		on_start_input_ = std::move(callback);
	}

	void edit::on_text_changed(callback_type callback)
	{
		on_text_changed_ = std::move(callback);
	}

	bool edit::on_event(mouse_move const & e)
	{
		static auto beam_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::beam);
		static auto arrow_cursor = sdl2::get_default_cursor(sdl2::default_cursor_type::arrow);

		auto m = math::cast<float>(e.position);
		mouse_x_ = m[0];

		bool new_mouseover = shape_.contains(m);
		if (!mouseover_ && new_mouseover)
			sdl2::set_cursor(*beam_cursor);
		else if (mouseover_ && !new_mouseover)
			sdl2::set_cursor(*arrow_cursor);

		mouseover_ = new_mouseover;
		return false;
	}

	bool edit::on_event(mouse_click const & e)
	{
		if (e.button == mouse_button::left && e.down)
		{
			if (mouseover_)
			{
				if (!editing_)
				{
					editing_ = true;
					old_text_ = text_;
					start_text_input();
					reset_caret();
					post_start_input();
				}

				if (cached_state_ && mouse_x_)
				{
					float x_offset = 0.f;

					switch (halign_)
					{
					case halignment::left:
						x_offset = text_box_[0].min;
						break;
					case halignment::center:
						x_offset = text_box_[0].center() - cached_state_->size[0] * 0.5f;
						break;
					case halignment::right:
						x_offset = text_box_[0].max - cached_state_->size[0];
						break;
					}

					auto it = std::lower_bound(cached_state_->images.begin(), cached_state_->images.end(), *mouse_x_, [&](auto const & i, float x){
						return i.position[0].center() + x_offset < x;
					});

					caret_ = it - cached_state_->images.begin();
					caret_blink_timer_ = 0.f;
					caret_visible_ = true;
				}

				return true;
			}
			else
			{
				if (editing_)
				{
					editing_ = false;
					stop_text_input();
					reset_caret();
					post_text_entered();
				}
			}
		}

		if (e.button == mouse_button::right && e.down)
		{
			if (editing_)
			{
				editing_ = false;
				text_ = old_text_;
				cached_state_.reset();
				stop_text_input();
				reset_caret();
				caret_ = std::min(caret_, text_.size());
				post_text_entered();
				return true;
			}
		}

		return false;
	}

	bool edit::on_event(key_press const & e)
	{
		if (e.key == SDLK_LCTRL)
			ctrl_down_ = e.down;

		if (e.down && editing_)
		{
			if (e.key == SDLK_LEFT)
			{
				if (ctrl_down_)
				{
					std::size_t start = caret_;
					while (start > 0 && std::isspace(text_[start - 1])) --start;
					while (start > 0 && !std::isspace(text_[start - 1])) --start;
					caret_ = start;
					reset_caret();
				}
				else
				{
					if (caret_ > 0)
					{
						--caret_;
						reset_caret();
					}
				}
			}
			else if (e.key == SDLK_RIGHT)
			{
				if (ctrl_down_)
				{
					std::size_t end = caret_;
					while (end < text_.size() && !std::isspace(text_[end])) ++end;
					while (end < text_.size() && std::isspace(text_[end])) ++end;
					caret_ = end;
					reset_caret();
				}
				else
				{
					if (caret_ < text_.size())
					{
						++caret_;
						reset_caret();
					}
				}
			}
			else if (e.key == SDLK_HOME)
			{
				caret_ = 0;
				reset_caret();
			}
			else if (e.key == SDLK_END)
			{
				caret_ = text_.size();
				reset_caret();
			}
			else if (e.key == SDLK_RETURN)
			{
				if (editing_)
				{
					editing_ = false;
					stop_text_input();
					reset_caret();
					post_text_entered();
				}
			}
			else if (e.key == SDLK_ESCAPE)
			{
				if (editing_)
				{
					editing_ = false;
					text_ = old_text_;
					cached_state_.reset();
					stop_text_input();
					reset_caret();
					caret_ = std::min(caret_, text_.size());
					post_text_changed();
					post_text_entered();
				}
			}
			else if (e.key == SDLK_BACKSPACE)
			{
				if (ctrl_down_)
				{
					std::size_t start = caret_;
					while (start > 0 && std::isspace(text_[start - 1])) --start;
					while (start > 0 && !std::isspace(text_[start - 1])) --start;

					auto new_text = text_;
					new_text.erase(start, caret_ - start);
					if (set_text(std::move(new_text), false))
					{
						caret_ = start;
						reset_caret();
					}
				}
				else
				{
					if (caret_ > 0)
					{
						--caret_;
						auto new_text = text_;
						new_text.erase(new_text.begin() + caret_);
						if (!set_text(std::move(new_text), false))
							++caret_;
						reset_caret();
					}
				}
			}
			else if (e.key == SDLK_DELETE)
			{
				if (ctrl_down_)
				{
					std::size_t end = caret_;
					while (end < text_.size() && !std::isspace(text_[end])) ++end;
					while (end < text_.size() && std::isspace(text_[end])) ++end;

					auto new_text = text_;
					new_text.erase(caret_, end - caret_);
					if (set_text(std::move(new_text), false))
					{
						reset_caret();
					}
				}
				else
				{
					if (caret_ < text_.size())
					{
						auto new_text = text_;
						new_text.erase(new_text.begin() + caret_);
						set_text(std::move(new_text), false);
						reset_caret();
					}
				}
			}

			return true;
		}

		return false;
	}

	bool edit::on_event(text_input const & e)
	{
		if (editing_)
		{
			auto new_text = text_;
			auto range = util::utf8_range(e.text);
			new_text.insert(new_text.begin() + caret_, range.begin(), range.end());
			if (set_text(std::move(new_text), false))
				caret_ += range.size();
			reset_caret();
			return true;
		}

		return false;
	}

	bool edit::focused() const
	{
		return editing_;
	}

	bool edit::editing() const
	{
		return editing_;
	}

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

	math::box<float, 2> edit::size_constraints() const
	{
		// TODO: min x-size
		static float const inf = std::numeric_limits<float>::infinity();

		auto st = merged_own_style();
		return {{{0.f, inf}, {1.f * st->font->size()[1] * (*st->scale), inf}}};
	}

	void edit::update(float dt)
	{
		if (editing_)
		{
			caret_blink_timer_ += dt;
			if (caret_blink_timer_ >= caret_blink_period_)
			{
				caret_blink_timer_ -= caret_blink_period_;
				caret_visible_ = !caret_visible_;
			}
		}
	}

	void edit::draw(painter & p) const
	{
		auto st = merged_own_style();

		if (!cached_state_)
		{
			auto font = (font_ == font_type::bold) ? st->bold_font.get() : st->font.get();

			cached_state state;
			state.texture = &font->atlas();

			fonts::shape_options options;
			options.scale = *st->text_scale;
			auto glyphs = font->shape(text_, options);

			math::box<float, 2> bbox;
			for (auto const & g : glyphs)
				bbox |= g.position;

			state.size[0] = glyphs.empty() ? 0.f : bbox[0].length();
			state.size[1] = font->size()[1] * (*st->text_scale);

			for (auto const & g : glyphs)
			{
				auto tc = font->texcoords(g.character);
				if (!tc) continue;
				state.images.push_back({*tc, g.position});
			}

			cached_state_ = std::move(state);
		}

		math::vector<float, 2> offset{0.f, 0.f};

		switch (halign_)
		{
		case halignment::left:
			offset[0] = text_box_[0].min;
			break;
		case halignment::center:
			offset[0] = text_box_[0].center() - cached_state_->size[0] * 0.5f;
			break;
		case halignment::right:
			offset[0] = text_box_[0].max - cached_state_->size[0];
			break;
		}

		switch (valign_)
		{
		case valignment::top:
			offset[1] = text_box_[1].min;
			break;
		case valignment::center:
			offset[1] = text_box_[1].center() - cached_state_->size[1] * 0.5f;
			break;
		case valignment::bottom:
			offset[1] = text_box_[1].max - cached_state_->size[1];
			break;
		}

		offset[0] = std::round(offset[0]);
		offset[1] = std::round(offset[1]);

		p.begin_stencil();
		p.draw_rect(text_box_, {0, 0, 0, 255});
		p.commit_stencil();

		if (*st->text_shadow_offset != math::vector{0, 0} && (*st->shadow_color)[3] != 0)
		{
			auto shoffset = offset + math::cast<float>(*st->text_shadow_offset);
			for (auto const & i : cached_state_->images)
				p.draw_image(i.position + shoffset, {cached_state_->texture, i.texcoords}, {*st->shadow_color, painter::color_mode::multiply});
		}

		for (auto const & i : cached_state_->images)
			p.draw_image(i.position + offset, {cached_state_->texture, i.texcoords}, {*st->text_color, painter::color_mode::multiply});

		p.end_stencil();

		if (caret_visible_)
		{
			float x;
			if (cached_state_->images.empty())
				x = 0.f;
			else if (caret_ == 0)
				x = cached_state_->images.front().position[0].min;
			else if (caret_ >= cached_state_->images.size())
				x = cached_state_->images.back().position[0].max;
			else
				x = (cached_state_->images[caret_ - 1].position[0].max + cached_state_->images[caret_].position[0].min) / 2.f;

			x += offset[0];

			float y = offset[1];

			p.draw_rect({{{x - (*st->text_scale) * 0.5f, x + (*st->text_scale) * 0.5f}, {y, y + cached_state_->size[1]}}}, *st->text_color);
		}
	}

	void edit::set_text_shape(math::box<float, 2> const & box)
	{
		text_box_ = box;
	}

	void edit::on_state_changed()
	{
		cached_state_ = std::nullopt;
		post_reshape();
		reset_caret();
	}

	void edit::reset_caret()
	{
		caret_visible_ = editing_;
		caret_blink_timer_ = 0.f;
	}

	void edit::post_text_entered() const
	{
		if (on_text_entered_)
		{
			post([weak_self = weak_from_this(), cb = on_text_entered_]{
				if (auto self = weak_self.lock())
					cb(static_cast<edit const *>(self.get())->text_);
			});
		}
	}

	void edit::post_start_input() const
	{
		if (on_start_input_)
			post(on_start_input_);
	}

	void edit::post_text_changed() const
	{
		if (on_text_changed_)
		{
			post([weak_self = weak_from_this(), cb = on_text_changed_]{
				if (auto self = weak_self.lock())
					cb(static_cast<edit const *>(self.get())->text_);
			});
		}
	}

}
