#include <psemek/app/application_base.hpp>
#include <psemek/app/default_application_factory.hpp>

#include <psemek/gfx/gl.hpp>
#include <psemek/gfx/painter.hpp>

#include <psemek/math/point.hpp>
#include <psemek/math/simplex.hpp>
#include <psemek/math/orthographic.hpp>
#include <psemek/math/rotation.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/math.hpp>
#include <psemek/math/distance.hpp>

#include <psemek/util/clock.hpp>
#include <psemek/util/unused.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/util/statistics.hpp>

#include <psemek/log/log.hpp>

#include <vector>

using namespace psemek;

namespace psemek::util
{

	template <typename T, std::size_t Max>
	struct fixed_vector
	{
		T * begin() { return data_; }
		T * end() { return data_ + size_; }

		T const * begin() const { return data_; }
		T const * end() const { return data_ + size_; }

		void push_back(T const & value)
		{
			assert(size_ < Max);
			data_[size_++] = value;
		}

	private:
		T data_[Max];
		std::size_t size_ = 0;
	};

}

struct stick_model
{
	std::vector<math::point<float, 2>> points;
	std::vector<math::vector<float, 2>> vels;
	std::vector<bool> movable;

	std::vector<math::segment<std::uint32_t>> sticks;
	std::vector<float> stick_length;
	std::vector<bool> stick_solid;

	void add_stick(std::size_t i, std::size_t j, bool solid = true);
};

void stick_model::add_stick(std::size_t i, std::size_t j, bool solid)
{
	sticks.push_back({i, j});
	stick_length.push_back(math::distance(points[i], points[j]));
	stick_solid.push_back(solid);
}

static const float ball_radius = 0.1f;

struct physics_demo_app
	: app::application_base
{
	physics_demo_app(options const &, context const &);
	~physics_demo_app();

	void on_event(app::resize_event const & event) override;

	void on_event(app::mouse_button_event const & event) override;

	void on_event(app::mouse_move_event const & event) override;

	void on_event(app::key_event const & event) override;

	void on_event(app::mouse_wheel_event const & event) override;

	void update() override;
	void present() override;

	math::point<float, 2> screen_to_world(math::point<float, 2> const & p) const;
	math::point<float, 2> world_to_screen(math::point<float, 2> const & p) const;

	util::clock<std::chrono::duration<float>> frame_clock;
	float update_time_left = 0.f;

	bool paused = false;

	stick_model model;

	gfx::painter painter;

	math::box<float, 2> view_region;

	std::optional<std::size_t> closest_point;
	std::optional<std::size_t> closest_stick;
	std::optional<math::vector<float, 2>> drag_delta;

	std::optional<std::size_t> new_spring_start;

	std::string text;

	util::statistics<float> physics_update_stats;
};

physics_demo_app::physics_demo_app(options const &, context const &)
{
	view_region[0] = {0.f, 0.f};
	view_region[1] = {-2.f, 5.f};

	int preset = 5;

	if (preset == 0)
	{
		int const N = 10;

		for (int i = 0; i <= N; ++i)
		{
			model.points.push_back({0.f, i * 0.5f});

			if (i > 0)
				model.add_stick(i - 1, i);
		}
	}
	else if (preset == 1)
	{
		int N = 10;

		for (int i = 0; i <= N; ++i)
		{
			float t = 1.f - (i * 1.f) / N;

			model.points.push_back({-(0.25f + 0.f * t), i * 0.5f});
			model.points.push_back({ (0.25f + 0.f * t), i * 0.5f});

			model.add_stick(2 * i + 0, 2 * i + 1);

			if (i > 0)
			{
				model.add_stick(2 * i - 2, 2 * i + 0);
				model.add_stick(2 * i - 1, 2 * i + 1);
				model.add_stick(2 * i - 2, 2 * i + 1);
				model.add_stick(2 * i - 1, 2 * i + 0);
			}
		}
	}
	else if (preset == 2)
	{
		int N = 12;

		float y = 1.f;

		model.points.push_back({0.f, y});

		for (int i = 0; i < N; ++i)
		{
			float a = (2.f * math::pi * i) / N;
			model.points.push_back({std::cos(a) * 0.5f, y + std::sin(a) * 0.5f});
		}

		for (int i = 0; i < N; ++i)
		{
			model.add_stick(0, i + 1);
			model.add_stick(i + 1, 1 + ((i + 1) % N));
		}
	}
	else if (preset == 3)
	{
		model.points.push_back({-5.f, -1.f});
		model.points.push_back({-4.f, -1.f});
		model.points.push_back({-5.f, 3.f});
		model.points.push_back({-4.f, 3.f});
		model.points.push_back({-4.5f, 2.8f});

		model.add_stick(0, 1);
		model.add_stick(0, 2);
		model.add_stick(0, 3);
		model.add_stick(1, 2);
		model.add_stick(1, 3);
		model.add_stick(2, 3);
		model.add_stick(2, 4);
		model.add_stick(3, 4);
	}

	model.vels.assign(model.points.size(), math::vector<float, 2>::zero());
	model.movable.assign(model.points.size(), true);
}

physics_demo_app::~physics_demo_app()
{
	log::info() << "Update: " << physics_update_stats;
}

void physics_demo_app::on_event(app::resize_event const & event)
{
	app::application_base::on_event(event);

	float aspect_ratio = static_cast<float>(event.size[0]) / event.size[1];

	float xc = view_region[0].center();
	float yl = view_region[1].length();

	view_region[0].min = xc - yl / 2.f * aspect_ratio;
	view_region[0].max = xc + yl / 2.f * aspect_ratio;
}

void physics_demo_app::on_event(app::mouse_button_event const & event)
{
	app::application_base::on_event(event);

	if (event.button == app::mouse_button::left && event.down && closest_point)
	{
		drag_delta = world_to_screen(model.points[*closest_point]) - math::cast<float>(state().mouse);
		model.vels[*closest_point] = math::vector<float, 2>::zero();
	}

	if (event.button == app::mouse_button::left && !event.down)
	{
		drag_delta = std::nullopt;
	}

	if (event.button == app::mouse_button::right && event.down)
	{
		if (new_spring_start)
		{
			new_spring_start = std::nullopt;
		}
		else if (closest_point)
		{
			model.movable[*closest_point] = !model.movable[*closest_point];
			model.vels[*closest_point] = math::vector<float, 2>::zero();
		}
	}
}

void physics_demo_app::on_event(app::mouse_move_event const & event)
{
	app::application_base::on_event(event);

	if (!drag_delta)
	{
		closest_point = std::nullopt;
		closest_stick = std::nullopt;

		{
			auto const m = math::cast<float>(state().mouse);

			std::size_t closest = 0;
			float distance = std::numeric_limits<float>::infinity();

			for (std::size_t i = 0; i < model.points.size(); ++i)
			{
				auto const d = math::distance(m, world_to_screen(model.points[i]));
				if (d < distance)
				{
					distance = d;
					closest = i;
				}
			}

			if (distance < 20.f)
			{
				closest_point = closest;
			}

			if (!closest_point)
			{
				std::size_t closest = 0;
				float distance = std::numeric_limits<float>::infinity();

				for (std::size_t i = 0; i < model.sticks.size(); ++i)
				{
					auto const d = math::distance(m, math::simplex{world_to_screen(model.points[model.sticks[i][0]]), world_to_screen(model.points[model.sticks[i][1]])});
					if (d < distance)
					{
						distance = d;
						closest = i;
					}
				}

				if (distance < 20.f)
				{
					closest_stick = closest;
				}
			}
		}
	}
}

void physics_demo_app::on_event(app::key_event const & event)
{
	app::application_base::on_event(event);

	if (event.down && event.key == app::keycode::SPACE)
	{
		paused = !paused;
	}
	else if (event.down && event.key == app::keycode::X)
	{
		if (closest_point)
		{
			model.points.erase(model.points.begin() + *closest_point);
			model.vels.erase(model.vels.begin() + *closest_point);
			model.movable.erase(model.movable.begin() + *closest_point);

			std::vector<math::segment<std::uint32_t>> new_sticks;
			std::vector<float> new_stick_length;
			std::vector<bool> new_stick_solid;

			for (std::size_t i = 0; i < model.sticks.size(); ++i)
			{
				if (model.sticks[i].points[0] == *closest_point) continue;
				if (model.sticks[i].points[1] == *closest_point) continue;

				auto s = model.sticks[i];
				if (s[0] > *closest_point) --s[0];
				if (s[1] > *closest_point) --s[1];

				new_sticks.push_back(s);
				new_stick_length.push_back(model.stick_length[i]);
				new_stick_solid.push_back(model.stick_solid[i]);
			}

			model.sticks = std::move(new_sticks);
			model.stick_length = std::move(new_stick_length);
			model.stick_solid = std::move(new_stick_solid);

			closest_point = std::nullopt;
		}

		if (closest_stick)
		{
			model.sticks.erase(model.sticks.begin() + *closest_stick);
			model.stick_length.erase(model.stick_length.begin() + *closest_stick);
			model.stick_solid.erase(model.stick_solid.begin() + *closest_stick);
			closest_stick = std::nullopt;
		}
	}
	else if (event.down && event.key == app::keycode::C)
	{
		if (new_spring_start && closest_point)
		{
			if (new_spring_start != closest_point)
			{
				model.add_stick(*new_spring_start, *closest_point);
				new_spring_start = std::nullopt;
			}
		}
		else if (new_spring_start)
		{
			model.points.push_back(screen_to_world(math::cast<float>(state().mouse)));
			model.vels.push_back(math::vector<float, 2>::zero());
			model.movable.push_back(false);
			model.add_stick(*new_spring_start, model.points.size() - 1);
			new_spring_start = model.points.size() - 1;
		}
		else if (closest_point)
		{
			new_spring_start = *closest_point;
			closest_point = std::nullopt;
		}
		else
		{
			model.points.push_back(screen_to_world(math::cast<float>(state().mouse)));
			model.vels.push_back(math::vector<float, 2>::zero());
			model.movable.push_back(false);
		}
	}
	else if (event.down && event.key == app::keycode::N)
	{
		model.points.push_back(screen_to_world(math::cast<float>(state().mouse)));
		model.vels.push_back(math::vector<float, 2>::zero());
		model.movable.push_back(true);
	}
	else if (event.down && event.key == app::keycode::F)
	{
		if (closest_stick)
		{
			model.stick_solid[*closest_stick] = !model.stick_solid[*closest_stick];
		}
	}
	else if (event.down && event.key == app::keycode::W)
	{
		std::uint32_t const base = model.points.size();

		int N = 12;

		auto o = screen_to_world(math::cast<float>(state().mouse));

		model.points.push_back(o);
		model.vels.push_back(math::vector<float, 2>::zero());
		model.movable.push_back(true);

		float r = 0.5f;

		for (int i = 0; i < N; ++i)
		{
			float a = (2.f * math::pi * i) / N;
			model.points.push_back({o[0] + std::cos(a) * r, o[1] + std::sin(a) * r});
			model.vels.push_back(math::vector<float, 2>::zero());
			model.movable.push_back(true);
		}

		for (int i = 0; i < N; ++i)
		{
			model.add_stick(base, base + i + 1, false);
			model.add_stick(base + i + 1, base + 1 + ((i + 1) % N));
		}
	}
	else if (event.down && event.key == app::keycode::B)
	{
		std::uint32_t const base = model.points.size();

		auto o = screen_to_world(math::cast<float>(state().mouse));

		float w = 0.25f;

		model.points.push_back({o[0] - w, o[1] - w});
		model.points.push_back({o[0] + w, o[1] - w});
		model.points.push_back({o[0] - w, o[1] + w});
		model.points.push_back({o[0] + w, o[1] + w});

		model.vels.push_back(math::vector<float, 2>::zero());
		model.vels.push_back(math::vector<float, 2>::zero());
		model.vels.push_back(math::vector<float, 2>::zero());
		model.vels.push_back(math::vector<float, 2>::zero());
		model.movable.push_back(true);
		model.movable.push_back(true);
		model.movable.push_back(true);
		model.movable.push_back(true);

		model.add_stick(base + 0, base + 1);
		model.add_stick(base + 0, base + 2);
		model.add_stick(base + 0, base + 3, false);
		model.add_stick(base + 1, base + 2, false);
		model.add_stick(base + 1, base + 3);
		model.add_stick(base + 2, base + 3);
	}
}

void physics_demo_app::on_event(app::mouse_wheel_event const & event)
{
	app::application_base::on_event(event);

	if (closest_stick)
	{
		auto & l = model.stick_length[*closest_stick];

		l += event.delta * ball_radius / 2.f;

		l = std::max(3.f * ball_radius, l);
	}
}

void physics_demo_app::update()
{
	if (paused)
	{
		frame_clock.restart();
		return;
	}

	float const dt = 0.001f;

	float const spring_constant = 10000.f;
	float const spring_damping_constant = 200.f;
	float const ground_friction = 100.f;
	float const ground_mu = 0.3f;
	float const ground_bounce = 0.5f;
	float const ball_friction = 0.f;
	float const ball_bounce = 0.5f;
	float const stick_friction = 500.f;
	float const stick_bounce = 0.9f;

	math::vector<float, 2> const gravity {0.f, -10.f};

	update_time_left += frame_clock.restart().count();

	int update_steps = 0;

	while (update_time_left >= dt)
	{
		++update_steps;
		if (update_steps == 32)
		{
			log::warning() << "Can't keep up with physics, skipping " << std::round(update_time_left / dt) << " updates";
			update_time_left = 0.f;
			break;
		}

		util::clock<> timer;

		update_time_left -= dt;

		float const cell_size = ball_radius * 2.f;

		std::vector<util::fixed_vector<std::array<int, 2>, 8>> ball_cells(model.points.size());

		util::ndarray<util::fixed_vector<std::uint32_t, 16>, 2> cells({std::ceil(view_region[0].length() / cell_size), std::ceil(view_region[1].length() / cell_size)});

		float dx = view_region[0].length() / cells.width();
		float dy = view_region[1].length() / cells.height();

		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			int x = std::floor(cells.width() * (model.points[i][0] - view_region[0].min) / view_region[0].length());
			x = math::clamp<int>(x, {0, cells.width() - 1});

			int y = std::floor(cells.height() * (model.points[i][1] - view_region[1].min) / view_region[1].length());
			y = math::clamp<int>(y, {0, cells.height() - 1});

			float cx = view_region[0].min + dx * x;
			float cy = view_region[1].min + dy * y;

			if (x > 0 && model.points[i][0] - ball_radius <= cx)
			{
				if (y > 0 && model.points[i][1] - ball_radius <= cy)
				{
					cells(x - 1, y - 1).push_back(i);
					ball_cells[i].push_back({x - 1, y - 1});
				}

				cells(x - 1, y).push_back(i);
				ball_cells[i].push_back({x - 1, y});

				if (y + 1 < cells.height() && model.points[i][1] + ball_radius >= cy + dy)
				{
					cells(x - 1, y + 1).push_back(i);
					ball_cells[i].push_back({x - 1, y + 1});
				}
			}

			if (y > 0 && model.points[i][1] - ball_radius <= cy)
			{
				cells(x, y - 1).push_back(i);
				ball_cells[i].push_back({x, y - 1});
			}

			cells(x, y).push_back(i);
			ball_cells[i].push_back({x, y});

			if (y + 1 < cells.height() && model.points[i][1] + ball_radius >= cy + dy)
			{
				cells(x, y + 1).push_back(i);
				ball_cells[i].push_back({x, y + 1});
			}

			if (x + 1 < cells.width() && model.points[i][0] + ball_radius >= cx + dx)
			{
				if (y > 0 && model.points[i][1] - ball_radius <= cy)
				{
					cells(x + 1, y - 1).push_back(i);
					ball_cells[i].push_back({x + 1, y - 1});
				}

				cells(x + 1, y).push_back(i);
				ball_cells[i].push_back({x + 1, y});

				if (y + 1 < cells.height() && model.points[i][1] + ball_radius >= cy + dy)
				{
					cells(x + 1, y + 1).push_back(i);
					ball_cells[i].push_back({x + 1, y + 1});
				}
			}
		}

		std::optional<std::size_t> fixed;

		if (closest_point && drag_delta)
		{
			auto target = screen_to_world(math::cast<float>(state().mouse) + *drag_delta);
			auto & point = model.points[*closest_point];

			auto & vel = model.vels[*closest_point];

			vel = vel * 0.9f + (target - point) * 0.1f / dt;

			point = target;

			fixed = closest_point;
		}

		std::vector<math::vector<float, 2>> force(model.points.size(), math::vector<float, 2>::zero());

		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			force[i] += gravity;
		}

		for (std::size_t i = 0; i < model.sticks.size(); ++i)
		{
			auto const & s = model.sticks[i];
			auto d = model.points[s[1]] - model.points[s[0]];
			auto l = math::length(d);
			auto n = d / l;

			auto f = - spring_constant * (l - model.stick_length[i]) * n / 2.f;

			force[s[0]] -= f;
			force[s[1]] += f;
		}

		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			if (fixed && i == *fixed) continue;

			if (!model.movable[i]) continue;

			model.vels[i] += dt * force[i];
		}

		std::vector<math::vector<float, 2>> impulse(model.points.size(), math::vector<float, 2>::zero());
		std::vector<math::vector<float, 2>> offset(model.points.size(), math::vector<float, 2>::zero());

		for (std::size_t i = 0; i < model.sticks.size(); ++i)
		{
			auto const & s = model.sticks[i];

			auto rel = model.vels[s[1]] - model.vels[s[0]];
			auto n = math::normalized(model.points[s[1]] - model.points[s[0]]);

			rel = n * math::dot(rel, n);

			impulse[s[0]] += rel / 2.f * spring_damping_constant * dt;
			impulse[s[1]] -= rel / 2.f * spring_damping_constant * dt;
		}

		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			bool collision = false;
			math::vector<float, 2> n;
			float d;

			if (model.points[i][0] < view_region[0].min + ball_radius && model.vels[i][0] < 0.f)
			{
				collision = true;
				d = view_region[0].min + ball_radius - model.points[i][0];
				n = {1.f, 0.f};
			}

			if (model.points[i][0] > view_region[0].max - ball_radius && model.vels[i][0] > 0.f)
			{
				collision = true;
				d = ball_radius + model.points[i][0] - view_region[0].max;
				n = {-1.f, 0.f};
			}

			if (model.points[i][1] < view_region[1].min + ball_radius && model.vels[i][1] < 0.f)
			{
				collision = true;
				d = view_region[1].min + ball_radius - model.points[i][1];
				n = {0.f, 1.f};
			}

			if (model.points[i][1] > view_region[1].max - ball_radius && model.vels[i][1] > 0.f)
			{
				collision = true;
				d = ball_radius + model.points[i][1] - view_region[1].max;
				n = {0.f, -1.f};
			}

			if (collision)
			{
				offset[i] += n * d;
				(void)d;

				auto nv = n * math::dot(n, model.vels[i]);
				auto tv = model.vels[i] - nv;

				auto jn = - (1.f + ground_bounce) * nv;

				auto jt = - tv * ground_friction * dt;

				unused(ground_mu);

//				if (math::length(jt) > ground_mu * math::length(jn))
//				{
//					jt = math::normalized(jt) * ground_mu * math::length(jn);
//				}

				impulse[i] += jn + jt;
			}
		}

		std::vector<std::uint32_t> checked;
		checked.reserve(16);
		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			checked.clear();

//			for (std::size_t j = i + 1; j < model.points.size(); ++j)
			for (auto c : ball_cells[i]) for (auto j : cells(c[0], c[1]))
			{
				if (i >= j) continue;

				if (std::find(checked.begin(), checked.end(), j) != checked.end()) continue;

				checked.push_back(j);

				auto r = math::distance(model.points[i], model.points[j]);

				if (r < ball_radius * 2.f)
				{
					auto n = (model.points[j] - model.points[i]) / r;
					auto rel = model.vels[j] - model.vels[i];

					auto d = math::dot(rel, n);

					if (d < 0.f)
					{
						auto nv = n * d;
						auto tv = rel - nv;

						impulse[i] += nv * (1.f + ball_bounce) / 2.f;
						impulse[j] -= nv * (1.f + ball_bounce) / 2.f;

						impulse[i] += tv * ball_friction * dt / 2.f;
						impulse[j] -= tv * ball_friction * dt / 2.f;

						offset[i] -= n * (2.f * ball_radius - r) / 2.f / 2.f;
						offset[j] += n * (2.f * ball_radius - r) / 2.f / 2.f;
					}
				}
			}
		}

		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			for (std::size_t j = 0; j < model.sticks.size(); ++j)
			{
				if (!model.stick_solid[j]) continue;

				auto const & s = model.sticks[j];

				if (i == s[0] || i == s[1]) continue;

				auto d = math::normalized(model.points[s[1]] - model.points[s[0]]);

				auto t = math::dot(d, model.points[i] - model.points[s[0]]) / math::distance(model.points[s[1]], model.points[s[0]]);

				if (t < 0.f || t > 1.f) continue;

				auto v = math::lerp(model.vels[s[0]], model.vels[s[1]], t);

				auto rel = model.vels[i] - v;

				auto n = math::ort(d);

				float r = math::dot(n, model.points[i] - model.points[s[0]]);

				if (math::dot(rel, n) * r >= 0.f) continue;

				auto nv = n * math::dot(rel, n);
				auto tv = rel - nv;

				unused(stick_friction);
				unused(tv);

				if (std::abs(r) >= ball_radius) continue;

				math::vector<float, 2> off = (r < 0.f ? -1.f : 1.f) * n * (ball_radius - std::abs(r));
				unused(off);

				math::vector<float, 2> imp = - nv * (1.f + stick_bounce) - tv * stick_friction * dt;

				offset[i] += off * 2.f / 3.f;
				offset[s[0]] -= off / 3.f * (1.f - t);
				offset[s[1]] -= off / 3.f * t;

				impulse[i] += imp * 2.f / 3.f;
				impulse[s[0]] -= imp / 3.f * (1.f - t);
				impulse[s[1]] -= imp / 3.f * t;
			}
		}

		for (std::size_t i = 0; i < model.points.size(); ++i)
		{
			if (fixed && i == *fixed) continue;

			if (!model.movable[i]) continue;

			model.vels[i] += impulse[i];
			model.points[i] += dt * model.vels[i] + offset[i];
		}

		physics_update_stats.push(timer.count());
	}
}

void physics_demo_app::present()
{
	gl::Viewport(0, 0, state().size[0], state().size[1]);

	gl::ClearColor(0.8f, 0.8f, 0.9f, 0.f);
	gl::Clear(gl::COLOR_BUFFER_BIT);

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

	for (std::size_t i = 0; i < model.sticks.size(); ++i)
	{
		painter.line(model.points[model.sticks[i][0]], model.points[model.sticks[i][1]], model.stick_solid[i] ? ball_radius / 2.f : ball_radius / 4.f,
			model.stick_solid[i] ? gfx::color_rgba{0, 0, 0, 255} : gfx::color_rgba{63, 63, 63, 255});
	}

	if (new_spring_start)
	{
		painter.line(model.points[*new_spring_start], screen_to_world(math::cast<float>(state().mouse)), ball_radius / 2.f, gfx::red);
	}

	if (closest_stick)
	{
		painter.line(model.points[model.sticks[*closest_stick][0]], model.points[model.sticks[*closest_stick][1]], ball_radius / 2.f, gfx::red);
	}

	for (std::size_t i = 0; i < model.points.size(); ++i)
	{
		painter.circle(model.points[i], ball_radius, model.movable[i] ? gfx::black : gfx::blue);
	}

	if (closest_point)
	{
		painter.circle(model.points[*closest_point], ball_radius, gfx::red);
	}

	painter.render(math::orthographic<float, 3>(math::box<float, 3>{{view_region[0], view_region[1], {-1.f, 1.f}}}).homogeneous_matrix());

	text = util::to_string("Balls: ", model.points.size(), "\n", text);

	{
		gfx::painter::text_options opts;
		opts.x = gfx::painter::x_align::left;
		opts.y = gfx::painter::y_align::top;
		opts.c = gfx::black;
		opts.f = gfx::painter::font::font_9x12;
		opts.scale = {2.f, 2.f};

		float y = 10.f;
		for (std::size_t i = 0;;)
		{
			std::size_t j = text.find_first_of('\n', i);

			if (j > i)
			{
				painter.text({10.f, y}, std::string_view(text).substr(i, j == std::string::npos ? std::string::npos : j - i), opts);
			}

			if (j == std::string::npos) break;

			i = j + 1;
			y += 24.f;
		}
	}

	text.clear();

	painter.render(math::window_camera{state().size[0], state().size[1]}.transform());
}

math::point<float, 2> physics_demo_app::screen_to_world(math::point<float, 2> const & p) const
{
	return math::orthographic<float, 2>(view_region).inverse()(math::point{2.f * p[0] / state().size[0] - 1.f, 1.f - 2.f * p[1] / state().size[1]});
}

math::point<float, 2> physics_demo_app::world_to_screen(math::point<float, 2> const & p) const
{
	auto q = math::orthographic<float, 2>(view_region)(p);

	return {(q[0] + 1.f) / 2.f * state().size[0], (1.f - q[1]) / 2.f * state().size[1]};
}

namespace psemek::app
{

	std::unique_ptr<application::factory> make_application_factory()
	{
		return default_application_factory<physics_demo_app>({.name = "Physics example"});
	}

}

