#include <psemek/ui/controller.hpp>

#include <psemek/ui/painter_impl.hpp>
#include <psemek/ui/box_shape.hpp>
#include <psemek/gfx/gl.hpp>
#include <psemek/util/recursive.hpp>
#include <psemek/math/camera.hpp>
#include <psemek/math/contains.hpp>

namespace psemek::ui
{

	namespace
	{

		struct root_proxy
			: element
		{
			children_range children() const override
			{
				return children_range{&root_raw, &root_raw + 1};
			}

			struct shape const & shape() const override
			{
				return shape_;
			}

			using element::reshape;

			void reshape(math::box<float, 2> const & bbox) override
			{
				shape_.box = bbox;
				if (root)
					root->reshape(bbox);
				if (on_reshape)
					post(on_reshape);
			}

			math::box<float, 2> size_constraints() const override
			{
				return root->size_constraints();
			}

			bool transparent() const override
			{
				return true;
			}

			void draw(painter &) const override {}

			std::shared_ptr<element> root;
			element * root_raw = nullptr;
			box_shape shape_;
			std::function<void()> on_reshape;
		};

	}

	struct controller::impl
	{
		async::event_loop * loop;
		painter_impl painter;
		std::shared_ptr<root_proxy> root;
		std::weak_ptr<element> focused;
		std::optional<math::point<int, 2>> mouse;
		float hint_delay = 0.f;
		float hint_timer = 0.f;
		util::function<void(element *)> on_hint;
		std::optional<std::weak_ptr<element>> hinted_element = std::nullopt;
		std::optional<std::string> hint_called = std::nullopt;
		util::function<void(element *, struct painter &)> post_draw_effect;

		impl(async::event_loop * loop);

		template <typename E>
		element * event(E const & e, bool use_event_bbox = false);

		element * hinted() const;

		void update(float dt);
	};

	controller::impl::impl(async::event_loop * loop)
		: loop(loop)
		, root{std::make_shared<root_proxy>()}
	{
		root->set_loop(loop);
		root->on_reshape = [this]{
			if (mouse)
				event(mouse_move{*mouse}, true);
		};
	}

	template <typename E>
	element * controller::impl::event(E const & e, bool use_event_bbox)
	{
		auto visitor = util::recursive([&](auto && self, element * elem) -> element * {
			if (elem->hidden()) return nullptr;

			if (!use_event_bbox || math::contains(elem->events_bbox(), math::cast<float>(*mouse)))
			{
				auto cs = elem->children();
				for (auto it = cs.rbegin(); it != cs.rend(); ++it)
					if (*it)
						if (auto p = self(*it))
							return p;
			}

			if (elem->enabled() && elem->on_event(e)) return elem;
			return nullptr;
		});

		if (auto p = focused.lock())
		{
			if (p->focused())
			{
				auto result = p->on_event(e);
				if (!p->focused())
					focused.reset();
				if (result)
					return p.get();
			}
			else
				focused.reset();
		}

		if (root)
			if (auto result = visitor(root.get()))
			{
				if (!focused.lock() && result->focused())
					focused = result->weak_from_this();
			}

		return nullptr;
	}

	element * controller::impl::hinted() const
	{
		if (!mouse)
			return nullptr;

		auto m = math::cast<float>(*mouse);
		auto visitor = util::recursive([&](auto && self, element * elem) -> element * {
			if (elem->hidden()) return nullptr;

			auto cs = elem->children();
			for (auto it = cs.rbegin(); it != cs.rend(); ++it)
				if (*it)
					if (auto p = self(*it))
						return p;

			if (elem->enabled() && elem->shape().contains(m) && !elem->transparent())
				return elem;

			return nullptr;
		});

		if (root)
			if (auto result = visitor(root.get()); result && result->hint())
				return result;

		return nullptr;
	}

	void controller::impl::update(float dt)
	{
		auto visitor = util::recursive([dt](auto && self, element * elem) -> void {
			if (elem->enabled()) elem->update(dt);
			for (auto c : elem->children())
				if (c) self(c);
		});

		if (root)
			visitor(root.get());
	}

	controller::controller(async::event_loop * loop)
		: pimpl_{make_impl(loop)}
	{}

	async::event_loop * controller::loop() const
	{
		return impl().loop;
	}

	controller::~controller() = default;

	std::shared_ptr<element> controller::set_root(std::shared_ptr<element> r)
	{
		auto old = std::move(impl().root->root);
		if (old)
			old->set_parent(nullptr);

		impl().root->root = std::move(r);
		impl().root->root_raw = impl().root->root.get();
		if (impl().root->root)
			impl().root->root->set_parent(impl().root.get());

		impl().root->reshape();

		return old;
	}

	element * controller::root()
	{
		return impl().root->root.get();
	}

	void controller::set_hint_delay(float seconds)
	{
		impl().hint_delay = seconds;
	}

	void controller::on_hint(util::function<void(element *)> callback)
	{
		impl().on_hint = std::move(callback);
	}

	void controller::set_post_draw_effect(util::function<void(element *, painter &)> effect)
	{
		impl().post_draw_effect = std::move(effect);
	}

	void controller::reshape(math::box<float, 2> const & shape)
	{
		impl().root->reshape(shape);
	}

	bool controller::event(mouse_move const & e)
	{
		impl().mouse = e.position;
		auto result = impl().event(e, true);

		if (impl().hinted_element)
		{
			auto p = impl().hinted_element->lock();
			if (!p)
			{
				if (impl().hint_called && impl().on_hint)
					impl().on_hint(nullptr);
				impl().hinted_element = std::nullopt;
				impl().hint_timer = 0.f;
				impl().hint_called = std::nullopt;
			}
		}

		auto new_hinted_element = impl().hinted();

		std::shared_ptr<element> old_hinted_element;
		if (impl().hinted_element)
			old_hinted_element = impl().hinted_element->lock();

		if (new_hinted_element != old_hinted_element.get())
		{
			if (impl().hint_called && impl().on_hint)
				impl().on_hint(nullptr);
			impl().hint_timer = 0.f;
			impl().hint_called = std::nullopt;

			if (new_hinted_element)
				impl().hinted_element = new_hinted_element->weak_from_this();
			else
				impl().hinted_element = std::nullopt;
		}

		return result;
	}

	bool controller::event(mouse_click const & e)
	{
		return impl().event(e, static_cast<bool>(impl().mouse));
	}

	bool controller::event(mouse_wheel const & e)
	{
		return impl().event(e, static_cast<bool>(impl().mouse));
	}

	bool controller::event(key_press const & e)
	{
		return impl().event(e);
	}

	bool controller::event(text_input const & e)
	{
		return impl().event(e);
	}

	void controller::update(float dt)
	{
		impl().update(dt);

		std::shared_ptr<element> p;

		if (impl().hinted_element)
		{
			p = impl().hinted_element->lock();

			if (!p)
			{
				if (impl().hint_called && impl().on_hint)
					impl().on_hint(nullptr);
				impl().hinted_element = std::nullopt;
				impl().hint_timer = 0.f;
				impl().hint_called = std::nullopt;
			}
		}

		if (p && (!impl().hint_called || *impl().hint_called != *p->hint()))
		{
			impl().hint_timer += dt;
			if (impl().hint_timer >= impl().hint_delay)
			{
				if (impl().on_hint)
					impl().on_hint(p.get());
				impl().hint_called = *p->hint();
			}
		}
	}

	void controller::render(gfx::render_target const & rt)
	{
		impl().painter.start_frame(math::cast<float>(rt.viewport));

		auto visitor = util::recursive([&](auto && self, element * elem) -> void {
			if (elem->hidden()) return;
			bool const visible = !(impl().painter.current_bbox() & elem->shape().bbox()).empty();
			if (visible)
				elem->draw(impl().painter);
			for (auto c : elem->children())
				if (c) self(c);
			if (visible)
			{
				elem->post_draw(impl().painter);
				if (impl().post_draw_effect)
					impl().post_draw_effect(elem, impl().painter);
			}
		});

		visitor(impl().root.get());

		rt.bind();

		gl::Enable(gl::DEPTH_TEST);
		gl::DepthFunc(gl::LEQUAL);

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

		gl::Disable(gl::CULL_FACE);

		impl().painter.render(math::window_camera{rt.viewport[0].length(), rt.viewport[1].length()}.transform());
	}

}
