#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/scale.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/constants.hpp>
#include <psemek/math/intersection.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/util/to_string.hpp>

using namespace psemek;

struct map
{
	std::vector<math::box<float, 2>> platforms;
};

struct player
{
	math::vector<float, 2> size;
	math::point<float, 2> position;
	math::vector<float, 2> velocity;

	math::point<float, 2> ghost_position;
	math::vector<float, 2> ghost_velocity;

	bool grounded = false;

	math::box<float, 2> bbox() const
	{
		return math::expand(math::box<float, 2>::singleton(position), size);
	}
};

struct particle
{
	math::point<float, 2> position;
	math::vector<float, 2> velocity;
	float size;
	float lifetime;
	gfx::color_rgba color;
};

struct platformer_app
	: app::application_base
{
	platformer_app(options const &, context const &)
	{
		map_.platforms.push_back({{{-10.f, 10.f}, {-5.f, -4.f}}});

		map_.platforms.push_back({{{-5.f, -3.f}, {-2.5f, -2.f}}});
		map_.platforms.push_back({{{-1.f,  1.f}, {-2.f, -1.5f}}});
		map_.platforms.push_back({{{ 3.f,  5.f}, {-1.5f, -1.f}}});

		map_.platforms.push_back({{{-7.f, -6.f}, {-3.5f, -3.f}}});

		player_.size = {0.25f, 0.5f};
		player_.position = {0.f, 5.f};
		player_.velocity = {0.f, 0.f};
		player_.ghost_position = player_.position;
		player_.ghost_velocity = {0.f, 0.f};
	}

	void update() override
	{
		float const dt = frame_clock_.restart().count();

		float const gravity = -50.f;
		float const acceleration = 200.f;
		float const friction = 25.f;
		float const jump_speed = 15.f;

		math::vector const ghost_spring_force{200.f, 500.f};
		math::vector const ghost_spring_damping{10.f, 20.f};

		float const particle_grow_speed = 0.1f;
		float const move_particle_spawn_period = 1.f / 32.f;
		int const jump_particle_count = 16;
		int const land_particle_count = 32;

		player_.velocity[0] *= std::exp(- friction * dt);

		player_.velocity[1] += dt * gravity;

		player_.position += player_.velocity * dt;

		{
			auto v = (player_.position - player_.ghost_position);
			player_.ghost_velocity += math::pointwise_mult(v, ghost_spring_force) * dt;
			player_.ghost_velocity[0] *= std::exp(- ghost_spring_damping[0] * dt);
			player_.ghost_velocity[1] *= std::exp(- ghost_spring_damping[1] * dt);
		}
		player_.ghost_position += player_.ghost_velocity * dt;

		bool was_grounded = player_.grounded;
		auto velocity_before_collision = player_.velocity;

		player_.grounded = false;

		for (auto const & platform : map_.platforms)
		{
			auto const player_box = player_.bbox();
			auto const intersection = platform & player_box;
			if (intersection.empty()) continue;

			if (intersection[0].length() < intersection[1].length())
			{
				if (intersection[0].center() < player_.position[0])
					player_.position[0] += intersection[0].length();
				else
					player_.position[0] -= intersection[0].length();

				player_.velocity[0] = 0.f;
			}
			else
			{
				if (intersection[1].center() < player_.position[1])
				{
					player_.position[1] += intersection[1].length();
					player_.grounded = true;
				}
				else
					player_.position[1] -= intersection[1].length();

				player_.velocity[1] = 0.f;
			}
		}

		if (state().key_down.contains(app::keycode::A))
			player_.velocity[0] -= acceleration * dt;
		if (state().key_down.contains(app::keycode::D))
			player_.velocity[0] += acceleration * dt;

		if (player_.grounded && (state().key_down.contains(app::keycode::A) ^ state().key_down.contains(app::keycode::D)))
		{
			move_particle_spawn_timer_ += dt;
			if (move_particle_spawn_timer_ > move_particle_spawn_period)
			{
				move_particle_spawn_timer_ -= move_particle_spawn_period;
				float s = state().key_down.contains(app::keycode::A) ? 1.f : -1.f;

				auto position = player_.position + math::vector{s * player_.size[0], -player_.size[1]};
				auto velocity = math::direction(random::uniform(rng_, math::rad(60.f), math::rad(120.f)));
				auto size = random::uniform(rng_, 0.05f, 0.15f);
				auto lifetime = random::uniform(rng_, 0.25f, 0.5f);
				int c = random::uniform(rng_, 63, 191);
				gfx::color_rgba color{c, 0, c, 255};

				particles_.push_back({position, velocity, size, lifetime, color});
			}
		}

		if (player_.grounded && state().key_down.contains(app::keycode::W))
		{
			player_.velocity[1] += jump_speed;

			for (int i = 0; i < jump_particle_count; ++i)
			{
				float s = random::uniform(rng_, -1.f, 1.f);

				auto position = player_.position + math::vector{s * player_.size[0], -player_.size[1]};
				auto velocity = (random::uniform<bool>(rng_) ? 1.f : -1.f) * math::direction(random::uniform(rng_, math::rad(-30.f), math::rad(30.f))) * 3.f;
				auto size = random::uniform(rng_, 0.05f, 0.15f);
				auto lifetime = random::uniform(rng_, 0.25f, 0.5f);
				int c = random::uniform(rng_, 63, 191);
				gfx::color_rgba color{c, 0, c, 255};

				particles_.push_back({position, velocity, size, lifetime, color});
			}
		}

		if (!was_grounded && player_.grounded)
		{
			for (int i = 0; i < land_particle_count; ++i)
			{
				float s = random::uniform(rng_, -1.f, 1.f);

				auto position = player_.position + math::vector{s * player_.size[0], -player_.size[1]};
				auto velocity = math::direction(random::uniform(rng_, math::rad(0.f), math::rad(180.f))) * (-velocity_before_collision[1]) * 0.2f;
				auto size = random::uniform(rng_, 0.05f, 0.15f);
				auto lifetime = random::uniform(rng_, 0.25f, 0.5f);
				int c = random::uniform(rng_, 63, 191);
				gfx::color_rgba color{c, 0, c, 255};

				particles_.push_back({position, velocity, size, lifetime, color});
			}
		}

		std::vector<particle> alive_particles;
		for (auto & p : particles_)
		{
			p.lifetime -= dt;
			if (p.lifetime <= 0.f) continue;

			p.position += p.velocity * dt;
			p.size += particle_grow_speed * dt;
			alive_particles.push_back(p);
		}
		particles_ = std::move(alive_particles);
	}

	void present() override
	{
		gl::ClearColor(1.f, 1.f, 1.f, 0.f);
		gl::Clear(gl::COLOR_BUFFER_BIT);

		for (auto const & box : map_.platforms)
			painter_.rect(box, {0, 0, 0, 255});

		{
			math::point bottom = player_.position - math::vector{0.f, player_.size[1]};
			math::point top = player_.ghost_position + math::vector{0.f, player_.size[1]};

			auto d = math::vector{player_.size[0], 0.f};

			gfx::color_rgba color{255, 0, 0, 255};
			painter_.triangle(bottom - d, bottom + d, top - d, color);
			painter_.triangle(bottom + d, top - d, top + d, color);
		}

		for (auto const & p : particles_)
		{
			auto colorf = gfx::to_colorf(p.color);
			colorf[3] *= 1.f - std::exp(- 2.f * p.lifetime);
			painter_.circle(p.position, p.size, gfx::to_coloru8(colorf));
		}

		float const aspect_ratio = state().size[0] * 1.f / state().size[1];
		float const view_size = 5.f;
		math::box<float, 2> view_box{{{-view_size * aspect_ratio, view_size * aspect_ratio}, {-view_size, view_size}}};

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

private:
	map map_;
	player player_;
	std::vector<particle> particles_;
	float move_particle_spawn_timer_ = 0.f;

	random::generator rng_;

	util::clock<> frame_clock_;
	gfx::painter painter_;
};

namespace psemek::app
{

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

}
