#include <psemek/app/application_base.hpp>
#include <psemek/app/default_application_factory.hpp>
#include <psemek/gfx/mesh.hpp>
#include <psemek/gfx/program.hpp>
#include <psemek/gfx/texture.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/math.hpp>
#include <psemek/math/rotation.hpp>
#include <psemek/math/scale.hpp>
#include <psemek/math/translation.hpp>
#include <psemek/math/easing.hpp>
#include <psemek/math/gradient.hpp>
#include <psemek/random/device.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform_sphere.hpp>
#include <psemek/pcg/perlin.hpp>
#include <psemek/util/clock.hpp>

using namespace psemek;

static char const vertex_source[] =
R"(#version 330

uniform mat4 u_transform;
uniform vec3 u_camera_pos;

layout (location = 0) in vec4 in_position;

layout (location = 1) in vec3 in_instance_pos;
layout (location = 2) in vec3 in_instance_z;
layout (location = 3) in float in_instance_size;
layout (location = 4) in vec2 in_instance_noise_offset;

out vec2 texcoord;
out vec2 noise_offset;
out float z;

void main()
{
	vec3 instance_y = u_camera_pos - in_instance_pos;
	instance_y -= dot(in_instance_z, instance_y) * in_instance_z;
	instance_y = -normalize(instance_y);
	vec3 instance_x = cross(instance_y, in_instance_z);

	mat4 transform;
	transform[0] = vec4(instance_x, 0.0);
	transform[1] = vec4(instance_y, 0.0);
	transform[2] = vec4(in_instance_z, 0.0);
	transform[3] = vec4(in_instance_pos, 1.0);

	gl_Position = u_transform * (transform * in_position);

	texcoord = in_position.xz * 0.5 + vec2(0.5, 0.5);
	noise_offset = in_instance_noise_offset;
	z = in_position.z;
}
)";

static char const fragment_source[] =
R"(#version 330

uniform sampler2D u_texture;
uniform sampler2D u_noise_texture;

uniform float u_time;

in vec2 texcoord;
in vec2 noise_offset;
in float z;

out vec4 out_color;

void main()
{
	vec2 tc = texcoord + (2.0 * texture(u_noise_texture, texcoord + noise_offset * u_time).xy - vec2(1.0, 1.0)) * 0.1 * z;
	out_color = texture(u_texture, tc);
}
)";

struct candle_renderer
{
	candle_renderer();

	void add(math::point<float, 3> const & pos, math::vector<float, 3> const & dir, float size);

	void render(math::camera const & camera, float time);

private:
	struct candle
	{
		math::point<float, 3> pos;
		math::vector<float, 3> dir;
		float size;
		math::vector<float, 2> noise_offset;
	};

	std::vector<candle> candles_;
	bool instances_need_update_ = false;

	gfx::program program_{vertex_source, fragment_source};

	gfx::mesh mesh_;

	gfx::texture_2d texture_;
	gfx::texture_2d noise_texture_;

	void update_instances();
};

candle_renderer::candle_renderer()
{
	std::vector<math::point<float, 3>> vertices;
	vertices.push_back({-1.f, 0.f, -1.f});
	vertices.push_back({ 1.f, 0.f, -1.f});
	vertices.push_back({ 1.f, 0.f,  1.f});
	vertices.push_back({-1.f, 0.f, -1.f});
	vertices.push_back({ 1.f, 0.f,  1.f});
	vertices.push_back({-1.f, 0.f,  1.f});

	mesh_.setup<math::point<float, 3>, gfx::instanced<math::point<float, 3>>, gfx::instanced<math::vector<float, 3>>, gfx::instanced<float>, gfx::instanced<math::vector<float, 2>>>();
	mesh_.load(vertices, gl::TRIANGLES);

	math::vector<float, 4> c0 {1.f, 0.99f, 0.98f, 1.f};
	math::vector<float, 4> c1 {1.f, 0.4f, 0.f, 0.75f};
	math::vector<float, 4> c2 {c1[0], c1[1], c1[2], 0.f};

	math::gradient<float> gy{
		std::make_pair(0.f, 0.f),
		math::easing_type::quadratic_in,
		std::make_pair(1.f, 2.f),
	};

	math::gradient<float, math::vector<float, 4>> gc
	{
		std::make_pair(0.1f, c0),
		math::easing_type::linear,
		std::pair{0.8f, c1},
		math::easing_type::cubic,
		std::pair{1.2f, c2}
	};

	{
		gfx::pixmap_rgba pm({512, 512}, {0, 0, 0, 0});
		for (auto idx : pm.indices())
		{
			float x = 2.f * (idx[0] + 0.5f) / pm.width() - 1.f;
			float y = 2.f * (idx[1] + 0.5f) / pm.height() - 1.f;

			float d = 10.f;

			float size = 0.1f;

			if (y >= 0)
			{
				d = std::abs(x) / size + gy(y);
			}
			else
			{
				d = std::sqrt(x * x + y * y) / size;
			}

			pm(idx) = gfx::to_coloru8(gc(d));
		}
		texture_.load(pm);
		texture_.linear_filter();
		texture_.anisotropy();
		texture_.generate_mipmap();
		texture_.clamp();
	}

	{
		random::generator rng;
		random::uniform_sphere_vector_distribution<float, 2> d;

		util::ndarray<math::vector<float, 2>, 2> grad({16, 16});

		for (auto & v : grad) v = d(rng);
		pcg::perlin<float, 2> perlinx(grad, pcg::seamless);

		for (auto & v : grad) v = d(rng);
		pcg::perlin<float, 2> perliny(grad, pcg::seamless);

		gfx::basic_pixmap<math::vector<std::uint8_t, 2>> pm({512, 512});
		for (auto idx : pm.indices())
		{
			float x = (0.5f + idx[0]) / pm.width();
			float y = (0.5f + idx[1]) / pm.height();

			pm(idx) = gfx::to_coloru8(math::vector{perlinx({x, y}), perliny({x, y})});
		}
		noise_texture_.load(pm);
		noise_texture_.linear_filter();
		noise_texture_.anisotropy();
		noise_texture_.generate_mipmap();
		noise_texture_.repeat();
	}
}

void candle_renderer::add(math::point<float, 3> const & pos, math::vector<float, 3> const & dir, float size)
{
	random::generator rng{random::device{}};
	random::uniform_sphere_vector_distribution<float, 2> d;
	math::vector<float, 2> noise_offset;
	while (true)
	{
		noise_offset = d(rng);
		if (std::abs(noise_offset[0]) < 0.5f)
			break;
	}

	if (noise_offset[1] > 0.f)
		noise_offset[1] *= -1.f;

	candles_.push_back({pos, dir, size, noise_offset * 0.5f});
	instances_need_update_ = true;
}

void candle_renderer::render(math::camera const & camera, float time)
{
	if (instances_need_update_) update_instances();

	program_.bind();
	program_["u_transform"] = camera.transform();
	program_["u_camera_pos"] = camera.position();
	program_["u_texture"] = 0;
	program_["u_noise_texture"] = 1;
	program_["u_time"] = time;
	gl::ActiveTexture(gl::TEXTURE0);
	texture_.bind();
	gl::ActiveTexture(gl::TEXTURE1);
	noise_texture_.bind();
	mesh_.draw();
}

void candle_renderer::update_instances()
{
	struct instance
	{
		math::point<float, 3> pos;
		math::vector<float, 3> dir;
		float size;
		math::vector<float, 2> noise_offset;
	};

	std::vector<instance> instances;
	instances.reserve(candles_.size());

	for (candle const & c: candles_)
	{
		instances.push_back({c.pos, c.dir, c.size, c.noise_offset});
	}

	mesh_.load_instance(instances);
}

struct fire_app
	: app::application_base
{
	math::spherical_camera camera;

	candle_renderer candles;

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

	fire_app(options const &, context const &)
	{
		camera.fov_y = math::rad(45.f);
		camera.near_clip = 0.01f;
		camera.far_clip = 1000.f;
		camera.target = {0.f, 0.f, 0.f};
		camera.elevation_angle = 0.f;
		camera.azimuthal_angle = 0.f;
		camera.distance = 4.f;

		candles.add({0.f, 0.f, 0.f}, {0.f, 0.f, 1.f}, 1.f);
	}

	void on_event(app::resize_event const & event) override
	{
		app::application_base::on_event(event);
		camera.set_fov(camera.fov_y, (1.f * event.size[0]) / event.size[1]);
	}

	void on_event(app::mouse_move_event const & event) override
	{
		auto const old_mouse = state().mouse;

		app::application_base::on_event(event);

		if (state().mouse_button_down.contains(app::mouse_button::middle))
		{
			auto const delta = event.position - old_mouse;

			camera.azimuthal_angle -= delta[0] * 0.01f;
			camera.elevation_angle += delta[1] * 0.01f;
		}
	}

	void on_event(app::mouse_wheel_event const & event) override
	{
		app::application_base::on_event(event);
		camera.distance *= std::pow(0.8f, event.delta);
	}

	void update() override
	{
		if (!state().key_down.contains(app::keycode::SPACE))
			time += clock.restart().count();
	}

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

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

		candles.render(camera, time);
	}

};

namespace psemek::app
{

	std::unique_ptr<application::factory> make_application_factory()
	{
		return default_application_factory<fire_app>({.name = "Fire example", .multisampling = 4});
	}

}
