#include <psemek/app/application_base.hpp>
#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/painter.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/contains.hpp>
#include <psemek/cg/kdtree.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/random/uniform_box.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/util/ndarray.hpp>
#include <psemek/log/log.hpp>

#include <format>

using namespace psemek;

struct particle
{
	math::point<float, 2> position;
	math::vector<float, 2> velocity;
	float mass;
	float radius;
	int type;
};

struct particle_life_2d_app
	: app::application_base
{
	particle_life_2d_app(options const &, context const &)
	{
		float S = 5.f;
		float W = 160.f * S;
		float H =  90.f * S;
		area_ = {{{-W, W}, {-H, H}}};

		types_ = 10;

		auto seed = random::device{}();

		// Explosions
		// types_ = 6;
		// seed = 3940378904;

		// Cells
		// types_ = 8;
		// seed = 3071915692;

		// Cells v2
		// types_ = 6;
		// seed = 3337663839;

		// Cool cells!
		types_ = 10;
		seed = 388834085;

		// One cell, use central gravity
		// types_ = 10;
		// seed = 175098945;

		// Mitochondria cell
		// types_ = 10;
		// seed = 1763331406;

		// Just fun
		// types_ = 10;
		// seed = 1676103531;

		// Another cell
		// types_ = 10;
		// seed = 2396049785;

		log::info() << "Seed: " << seed;

		random::generator rng{seed, 0};

		random::uniform_distribution<int> dcolor(63, 255);
		random::uniform_distribution<int> dtype(0, types_ - 1);
		random::uniform_box_point_distribution<float, 2> darea(area_);

		for (int i = 0; i < types_; ++i)
			colors_.push_back({dcolor(rng), dcolor(rng), dcolor(rng), 255});

		for (int i = 0; i < 4 * 1024; ++i)
		{
			particles_.push_back({
				.position = darea(rng),
				.velocity = {0.f, 0.f},
				.mass = 1.f,
				.radius = 1.f,
				.type = dtype(rng),
				// .type = random::uniform_from(rng, {4, 6}),
			});
		}

		auto dforce = [](auto & rng)
		{
			return random::uniform(rng, 20.f, 100.f) * (random::uniform<bool>(rng) ? 1.f : -1.f);
		};

		random::uniform_distribution<float> ddistance(3.f, 20.f);

		force_constants_.resize({types_, types_});
		force_distance_.resize({types_, types_});
		collision_distance_.resize({types_, types_});

		for (int i = 0; i < types_; ++i)
			for (int j = 0; j < types_; ++j)
				force_constants_(i, j) = dforce(rng);

		for (int i = 0; i < types_; ++i)
			for (int j = 0; j < types_; ++j)
				force_distance_(i, j) = ddistance(rng);

		for (int i = 0; i < types_; ++i)
			for (int j = 0; j < types_; ++j)
				collision_distance_(i, j) = random::uniform(rng, 0.125f, 0.75f) * force_distance_(i, j);

		// for (int i = 0; i < types_; ++i)
		// 	for (int j = 0; j < types_; ++j)
		// 		force_constants_(i, j) = -100.f * ((i == j) ? 1.f : 2.f);

		max_force_distance_.resize(types_, 2.f);
		for (int i = 0; i < types_; ++i)
			for (int j = 0; j < types_; ++j)
				math::make_max(max_force_distance_[i], force_distance_(i, j));
	}

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

		scale_target_ *= std::pow(1.25f, -event.delta);
	}

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

		if (event.down && event.key == app::keycode::SPACE)
			paused_ ^= true;
	}

	void update() override
	{
		float const dt = 0.02f;

		scale_ += (scale_target_ - scale_) * (- std::expm1(- 20.f * dt));

		auto visible_area = area_;
		visible_area = math::expand(visible_area, 0.5f * (scale_ - 1.f) * visible_area.dimensions());

		float window_aspect_ratio = state().size[0] * 1.f / state().size[1];
		float area_aspect_ratio = visible_area[0].length() / visible_area[1].length();

		if (area_aspect_ratio < window_aspect_ratio)
		{
			view_area_[1] = visible_area[1];
			view_area_[0] = math::expand(math::interval<float>::singleton(visible_area[0].center()), visible_area[1].length() * window_aspect_ratio * 0.5f);
		}
		else
		{
			view_area_[0] = visible_area[0];
			view_area_[1] = math::expand(math::interval<float>::singleton(visible_area[1].center()), visible_area[0].length() / window_aspect_ratio * 0.5f);
		}

		if (!paused_)
		for (int step = 0; step < 5; ++step)
		{
			cg::kdtree<float, 2, int> kdtree;

			for (int i = 0; i < particles_.size(); ++i)
				kdtree.insert({particles_[i].position, i});

			float const collision_strength = 200.f;

			mouseover_particle_ = std::nullopt;
			if (state().mouse_button_down.contains(app::mouse_button::left))
			{
				math::point<float, 2> m;
				m[0] = math::lerp(view_area_[0], state().mouse[0] * 1.f / state().size[0]);
				m[1] = math::lerp(view_area_[1], 1.f - state().mouse[1] * 1.f / state().size[1]);
				mouseover_particle_ = kdtree.closest(m).data;
			}

			#pragma omp parallel for
			for (int i = 0; i < particles_.size(); ++i)
			{
				// ======== kd-tree based algorithm ========

				auto & pi = particles_[i];
				auto f = math::vector{0.f, 0.f};

				kdtree.closer_than_map(pi.position, max_force_distance_[pi.type], [&](auto const & value){
					auto j = value.data;

					if (i == j)
						return;

					auto const & pj = particles_[j];

					auto r = pj.position - pi.position;
					float l = math::length(r);
					auto n = r / l;

					// ==== 1/R forces ====
					// float lg = std::max(0.5f, l);
					// auto g = n / (lg * lg);

					// if (l < force_distance_(pi.type, pj.type))
					// 	f += force_constants_(pi.type, pj.type) * g;

					// float collision_distance = pi.radius + pj.radius;

					// if (l < collision_distance)
					// {
					// 	float d = std::max(0.25f, l / collision_distance);

					// 	f -= collision_strength * n * (1.f / (d * d) - 1.f);
					// }

					// ==== Linear forces ====
					if (l < force_distance_(pi.type, pj.type))
						f += force_constants_(pi.type, pj.type) * n * (1.f - l / force_distance_(pi.type, pj.type));

					if (l < collision_distance_(pi.type, pj.type))
						f -= 10.f * std::abs(force_constants_(pi.type, pj.type)) * n * (1.f - l / collision_distance_(pi.type, pj.type));

					(void)collision_strength;
				});

				pi.velocity += f * dt / pi.mass;






				// ======== Naive algorithm v2 ========

				// auto & pi = particles_[i];
				// auto f = math::vector{0.f, 0.f};

				// for (int j = 0; j < particles_.size(); ++j)
				// {
				// 	if (i == j)
				// 		continue;

				// 	auto const & pj = particles_[j];

				// 	auto r = pj.position - pi.position;
				// 	float l = math::length(r);
				// 	auto n = r / l;
				// 	auto g = n / (l * l);

				// 	if (l < force_distance_(pi.type, pj.type))
				// 		f += force_constants_(pi.type, pj.type) * g;

				// 	if (l < pi.radius + pj.radius)
				// 	{
				// 		float d = l / (pi.radius + pj.radius);

				// 		f -= collision_strength * n * (1.f / (d * d) - 1.f);
				// 	}
				// }

				// pi.velocity += f * dt / pi.mass;





				// ======== Naive algorithm ========

				// for (int j = i + 1; j < particles_.size(); ++j)
				// {
				// 	auto & pi = particles_[i];
				// 	auto & pj = particles_[j];

				// 	auto fi = math::vector{0.f, 0.f};
				// 	auto fj = math::vector{0.f, 0.f};

				// 	auto r = pj.position - pi.position;
				// 	float l = math::length(r);
				// 	auto n = r / l;
				// 	auto g = n / (l * l);

				// 	if (l < force_distance_(pi.type, pj.type))
				// 		fi += force_constants_(pi.type, pj.type) * g;

				// 	if (l < force_distance_(pj.type, pi.type))
				// 		fj -= force_constants_(pj.type, pi.type) * g;

				// 	if (l < pi.radius + pj.radius)
				// 	{
				// 		float d = l / (pi.radius + pj.radius);

				// 		auto f = collision_strength * n * (1.f / (d * d) - 1.f);
				// 		fi -= f;
				// 		fj += f;
				// 	}

				// 	pi.velocity += fi * dt / pi.mass;
				// 	pj.velocity += fj * dt / pj.mass;
				// }
			}

			float const friction = 10.f;
			float const friction_factor = std::exp(- friction * dt);
			float const wall_distance = 0.f;
			float const wall_force = 10.f;
			float const gravity = 0.f;
			float const center_gravity = 0.f;
			float const line_gravity = 0.f;
			float const circle_gravity = 0.f;
			float const circle_radius = 100.f;
			bool const circle_wall = false;
			float const circle_wall_radius = 100.f;

			for (auto & p : particles_)
			{
				auto r = p.position - p.position.zero();

				p.velocity[1] -= gravity * dt;
				p.velocity[1] -= line_gravity * p.position[1] * dt;
				p.velocity -= center_gravity * r * dt;
				p.velocity += circle_gravity * r / math::length(r) * (circle_radius - math::length(r)) * dt;
				p.velocity *= friction_factor;
				p.position += p.velocity * dt;

				for (int d : {0, 1})
				{
					// if (p.position[d] < area_[d].min + p.radius)
					// {
					// 	p.position[d] = area_[d].min + p.radius;
					// 	p.velocity[d] *= -1.f;
					// }

					// if (p.position[d] > area_[d].max - p.radius)
					// {
					// 	p.position[d] = area_[d].max - p.radius;
					// 	p.velocity[d] *= -1.f;
					// }

					if (circle_wall)
					{
						auto r = p.position - p.position.zero();
						auto l = math::length(r);
						if (l > circle_wall_radius)
						{
							p.velocity -= wall_force * r / l * (l - circle_wall_radius) * dt;
						}
					}
					else
					{
						if (p.position[d] < area_[d].min + wall_distance)
						{
							p.velocity[d] += wall_force * (area_[d].min + wall_distance - p.position[d]) * dt;
						}

						if (p.position[d] > area_[d].max - wall_distance)
						{
							p.velocity[d] -= wall_force * (p.position[d] - (area_[d].max - wall_distance)) * dt;
						}
					}
				}
			}
		}
	}

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

		gl::ClearColor(0.02f, 0.02f, 0.02f, 1.f);
		gl::Clear(gl::COLOR_BUFFER_BIT);

		for (auto const & p : particles_)
		{
			auto c0 = colors_[p.type];
			c0[3] = 15;

			auto c1 = colors_[p.type];
			c1[3] = 0;

			painter_.circle(p.position, p.radius * 8.f, c0, c1);
		}

		for (auto const & p : particles_)
			painter_.circle(p.position, p.radius, colors_[p.type]);

		painter_.render(math::orthographic_camera{view_area_}.transform());

		if (mouseover_particle_)
		{
			int type = particles_[*mouseover_particle_].type;
			painter_.text({state().size[0] / 2.f, state().size[1] - 20.f}, std::format("Species #{}", type), {
				.scale = {2.f, 2.f},
				.y = gfx::painter::y_align::bottom,
				.c = colors_[type],
			});
		}

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

private:
	gfx::painter painter_;

	util::clock<std::chrono::duration<float>, std::chrono::high_resolution_clock> clock_;

	math::box<float, 2> area_;
	math::box<float, 2> view_area_;
	float scale_ = 1.f;
	float scale_target_ = 1.f;

	int types_;
	std::vector<gfx::color_rgba> colors_;
	util::ndarray<float, 2> force_constants_;
	util::ndarray<float, 2> force_distance_;
	util::ndarray<float, 2> collision_distance_;
	std::vector<float> max_force_distance_;

	std::vector<particle> particles_;

	std::optional<int> mouseover_particle_;

	bool paused_ = true;
};

namespace psemek::app
{

	std::unique_ptr<application::factory> make_application_factory()
	{
		return default_application_factory<particle_life_2d_app>({.name = "Particle Life 2D"});
	}

}
