#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/cg/bbox.hpp>
#include <psemek/cg/triangulation/ear_clipping.hpp>
#include <psemek/cg/triangulation/monotone.hpp>
#include <psemek/cg/triangulation/delaunay.hpp>
#include <psemek/log/log.hpp>
#include <psemek/prof/profiler.hpp>
#include <psemek/util/clock.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/math/homogeneous.hpp>
#include <psemek/math/swizzle.hpp>
#include <psemek/random/generator.hpp>
#include <psemek/random/uniform.hpp>

#include <fstream>

using namespace psemek;

struct triangulation_app
	: app::application_base
{
	triangulation_app(options const &, context const &)
	{
		std::ifstream in(PSEMEK_EXAMPLES_DIR "/turkey");

		while (true)
		{
			math::point<float, 2> p;
			in >> p[0] >> p[1];
			if (!in)
				break;
			points_.push_back(p);
		}

		std::reverse(points_.begin(), points_.end());

		for (std::size_t i = 0; i < points_.size();)
		{
			std::size_t j = (i + 1) % points_.size();
			if (points_[i] == points_[j])
			{
				points_.erase(points_.begin() + i);
			}
			else
			{
				++i;
			}
		}

		log::info() << points_.size() << " input points";

		bbox_ = cg::bbox(points_.begin(), points_.end());
		camera_center_ = bbox_.center();
		camera_size_ = std::max(bbox_[0].length(), bbox_[1].length()) * 1.125f;
		camera_size_tgt_ = camera_size_;

		{
			prof::profiler prof("triangulate");

//			auto dcel = cg::ear_clipping<std::uint16_t>(math::fast, points_.begin(), points_.end());
			auto dcel = cg::monotone_triangulation<std::uint16_t>(math::fast, points_.begin(), points_.end());
			edges_ = cg::edge_mesh(dcel);
			triangles_ = cg::triangle_mesh(dcel);
		}

		prof::dump();

		log::info() << edges_.size() << " edges";
		log::info() << triangles_.size() << " triangles";
		log::info() << "Euler chi: " << (points_.size() - edges_.size() + triangles_.size());

		closest_points_.resize(points_.size());
		std::iota(closest_points_.begin(), closest_points_.end(), std::uint16_t{0});
	}

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

		if (event.down && event.button == app::mouse_button::left)
			drag_start_ = state().mouse;

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

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

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

		if (drag_start_)
		{
			auto delta = state().mouse - *drag_start_;
			delta[1] *= -1;
			camera_center_ -= math::cast<float>(delta) * camera_size_ / (1.f * state().size[1]);
			drag_start_ = state().mouse;
		}

		camera_size_ += (camera_size_tgt_ - camera_size_) * (1.f - std::exp(- 20.f * dt));
	}

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

		float aspect_ratio = (state().size[0] * 1.f) / state().size[1];
		math::box<float, 2> view_bbox = math::expand(math::box<float, 2>::singleton(camera_center_), math::vector{camera_size_ * 0.5f * aspect_ratio, camera_size_ * 0.5f});

		float line_width = 4.f * camera_size_ / state().size[1];

		auto edge = [this, line_width](auto i0, auto i1, gfx::color_rgba const & color)
		{
			painter_.line(points_[i0], points_[i1], line_width, color, true);
		};

		random::generator rng;

		for (auto const & t : triangles_)
		{
			gfx::color_rgba c;
			c[0] = random::uniform<std::uint8_t>(rng, {0, 255});
			c[1] = random::uniform<std::uint8_t>(rng, {0, 255});
			c[2] = random::uniform<std::uint8_t>(rng, {0, 255});
			c[3] = 255;

			painter_.triangle(points_[t[0]], points_[t[1]], points_[t[2]], c);
		}

//		for (auto const & e : edges_)
//			edge(e[0], e[1], gfx::black);

		for (std::size_t i = 0; i < points_.size(); ++i)
			edge(i, (i + 1) % points_.size(), gfx::black);

		auto camera_transform = math::orthographic_camera{view_bbox}.transform();

		painter_.render(camera_transform);

		{
			math::point<float, 2> m_world;
			m_world[0] = math::lerp(view_bbox[0], state().mouse[0] * 1.f / state().size[0]);
			m_world[1] = math::lerp(view_bbox[1], 1.f - state().mouse[1] * 1.f / state().size[1]);

			auto compare = [&](auto i, auto j){
				return math::distance(points_[i], m_world) < math::distance(points_[j], m_world);
			};

			std::size_t const n_closest = 8;

			std::partial_sort(closest_points_.begin(), closest_points_.begin() + n_closest, closest_points_.end(), compare);

			float max_distance = camera_size_ / 32.f;

			for (std::size_t j = 0; j < n_closest; ++j)
			{
				auto i = closest_points_[j];

				if (math::distance(points_[i], m_world) > max_distance)
					break;

				gfx::painter::text_options opts;
				opts.c = {0, 0, 0, 255};
				opts.x = gfx::painter::x_align::left;
				opts.y = gfx::painter::y_align::bottom;
				opts.scale = {2.f, 2.f};
				auto p = math::swizzle<0, 1>(math::as_point(camera_transform * math::homogeneous(math::swizzle<0, 1, -1>(points_[i]))));
				p[0] = std::round((p[0] * 0.5f + 0.5f) * state().size[0]);
				p[1] = std::round((0.5f - p[1] * 0.5f) * state().size[1]);
				painter_.text(p, util::to_string(i), opts);
			}
		}

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

private:
	std::vector<math::point<float, 2>> points_;
	std::vector<math::segment<std::uint16_t>> edges_;
	std::vector<math::triangle<std::uint16_t>> triangles_;
	math::box<float, 2> bbox_;
	math::point<float, 2> camera_center_;
	float camera_size_;
	float camera_size_tgt_;

	std::optional<math::point<int, 2>> drag_start_;
	std::vector<std::uint16_t> closest_points_;

	gfx::painter painter_;

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

namespace psemek::app
{

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

}
