#include <psemek/ui/selector.hpp>
#include <psemek/ui/screen.hpp>
#include <psemek/ui/positioner.hpp>
#include <psemek/ui/event_interceptor.hpp>

#include <psemek/util/recursive.hpp>
#include <psemek/util/to_shared.hpp>

#include <psemek/math/contains.hpp>

#include <stdexcept>

namespace psemek::ui
{

	selector::selector()
		: children_range_(1, nullptr)
	{}

	selector::~selector()
	{
		release_children();
	}

	void selector::set_parent(element * parent)
	{
		if (!parent && mouseover_callback_)
			post([cb = mouseover_callback_]{ cb(std::nullopt); });

		element::set_parent(parent);
	}

	void selector::reshape(math::box<float, 2> const & box)
	{
		shape_.box = box;

		auto st = merged_own_style();

		math::interval<float> x_range = math::shrink<float>(box[0], (*st->inner_margin)[0]);

		float y = box[1].min;

		for (std::size_t i = 0; i < children_.size(); ++i)
		{
			auto c = children_[i].get();
			float y_start = y;
			y += (*st->inner_margin)[1];

			if (c)
			{
				auto sc = c->size_constraints();
				c->reshape({{x_range, {y, y + sc[1].min}}});
				y += sc[1].min;
			}

			y += (*st->inner_margin)[1];

			child_boxes_[i] = {{box[0], {y_start, y}}};

			y += y_extra() * *st->scale;
		}

		if (active_submenu_)
		{
			auto submenu = submenu_[*active_submenu_];
			auto sc = submenu->size_constraints();

			math::box<float, 2> box{{{0.f, sc[0].min}, {0.f, sc[1].min}}};
			box[0] += shape_.box[0].max;
			box[1] += active_submenu_y_;

			submenu->reshape(box);
		}
	}

	math::box<float, 2> selector::size_constraints() const
	{
		auto st = merged_own_style();

		auto x_range = math::interval<float>::full();

		float y_sum = 0.f;

		for (std::size_t i = 0; i < size(); ++i)
		{
			auto c = get(i);
			if (!c) continue;

			auto sc = c->size_constraints();

			if (submenu_[i])
				sc[0] += *st->scale * submenu_extra();

			x_range &= sc[0];

			y_sum += sc[1].min;
		}

		x_range += (*st->inner_margin)[0] * 2.f;

		y_sum += (*st->inner_margin)[1] * 2.f * size();
		if (!children_.empty())
			y_sum += *st->scale * y_extra() * static_cast<int>(size() - 1);

		return {{x_range, {y_sum, y_sum}}};
	}

	bool selector::on_event(mouse_move const & e)
	{
		auto const p = math::cast<float>(e.position);

		std::optional<std::size_t> new_selected;
		for (std::size_t i = 0; i < child_boxes_.size(); ++i)
		{
			if (math::contains(child_boxes_[i], p))
			{
				new_selected = i;
				break;
			}
		}

		if (new_selected && new_selected != selected_)
			on_submenu(*new_selected);

		if (new_selected != selected_ && mouseover_callback_)
			post([cb = mouseover_callback_, i = new_selected]{ cb(i); });

		selected_ = new_selected;

		return false;
	}

	bool selector::on_event(mouse_click const & e)
	{
		if (e.down && e.button == mouse_button::left && selected_)
		{
			if (!submenu_[*selected_] && callback_)
				post([cb = callback_, i = *selected_]{ cb(i); });
			return true;
		}

		return false;
	}

	std::size_t selector::size() const
	{
		return children_.size();
	}

	void selector::resize(std::size_t size)
	{
		children_.resize(size);
		submenu_.resize(size);
		child_boxes_.resize(size);
		children_range_.resize(size + 1);

		post_reshape();
	}

	std::size_t selector::add(std::shared_ptr<element> child)
	{
		children_.push_back(child);
		submenu_.push_back(nullptr);
		child_boxes_.emplace_back();
		children_range_.push_back(child.get());
		child->set_parent(this);

		post_reshape();

		return children_.size() - 1;
	}

	std::shared_ptr<element> selector::get(std::size_t index) const
	{
		return children_[index];
	}

	void selector::set_submenu(std::size_t index, std::shared_ptr<selector> submenu)
	{
		submenu_[index] = std::move(submenu);
	}

	std::shared_ptr<selector> selector::submenu(std::size_t index) const
	{
		return submenu_[index];
	}

	void selector::clear()
	{
		release_children();
		children_.clear();
		submenu_.clear();
		child_boxes_.clear();
		children_range_.assign(1, nullptr);
		post_reshape();
	}

	math::interval<float> selector::y_range(std::size_t index) const
	{
		return child_boxes_[index][1];
	}

	std::optional<std::size_t> selector::selected() const
	{
		if (active_submenu_)
			return active_submenu_;
		if (selected_)
			return selected_;
		return std::nullopt;
	}

	void selector::on_selected(callback_type callback)
	{
		callback_ = std::move(callback);
	}

	void selector::on_mouseover(std::function<void(std::optional<std::size_t>)> callback)
	{
		mouseover_callback_ = std::move(callback);
	}

	selector::callback_type selector::on_selected() const
	{
		return callback_;
	}

	void selector::on_submenu(std::size_t index)
	{
		release_submenu();

		auto submenu = submenu_[index].get();
		if (submenu)
		{
			children_range_[0] = submenu;
			active_submenu_ = index;
			submenu->set_parent(this);
			active_submenu_y_ = child_boxes_[index][1].min;
			post_reshape();
		}
	}

	void selector::release_submenu()
	{
		if (active_submenu_)
		{
			auto submenu = submenu_[*active_submenu_].get();
			submenu->release_submenu();
			submenu->set_parent(nullptr);
			active_submenu_ = std::nullopt;
			children_range_[0] = nullptr;
		}
	}

	namespace
	{

		struct opaque_event_interceptor
			: event_interceptor
		{
			bool transparent() const override { return false; }
		};

	}

	bool spawn_selector(element * root, std::shared_ptr<selector> selector, math::point<float, 2> const & position, std::function<void()> on_canceled)
	{
		ui::screen * screen = find_last_parent_of_type<struct screen>(root);
		if (!screen)
			return false;

		auto event_interceptor = std::make_shared<opaque_event_interceptor>();
		auto positioner = std::make_shared<struct positioner>();
		positioner->set_child(selector);
		event_interceptor->set_child(positioner);

		auto loop = screen->loop();

		auto close = [selector_root = event_interceptor.get(), selector = selector.get(), on_canceled]{
			if (on_canceled)
				on_canceled();

			auto p = dynamic_cast<struct screen *>(selector_root->parent());
			if (p)
				p->remove_child(selector_root);
		};

		std::vector<std::pair<ui::selector *, ui::selector::callback_type>> old_callbacks;

		util::recursive([&](auto && self, struct selector * target) -> void{
			old_callbacks.push_back({target, target->on_selected()});
			for (std::size_t i = 0; i < target->size(); ++i)
				if (auto s = target->submenu(i))
					self(s.get());
		})(selector.get());

		auto revert_callbacks = [old_callbacks = util::to_shared(std::move(old_callbacks))]{
			for (auto & p : *old_callbacks)
				p.first->on_selected(std::move(p.second));
		};

		auto cancel = [close, revert_callbacks, selector]{
			close();
			revert_callbacks();
		};

		auto patch_callback_recursive = util::recursive([&](auto && self, struct selector * target) -> void {
			target->on_selected([target, close, cb = target->on_selected()](std::size_t index){
				[[maybe_unused]] auto shared_target = target->shared_from_this();
				close();
				if (cb)
					cb(index);
				target->on_selected(cb);
			});

			for (std::size_t i = 0; i < target->size(); ++i)
				if (auto s = target->submenu(i))
					self(s.get());
		});
		patch_callback_recursive(selector.get());

		event_interceptor->on_mouse_click([loop, cancel](ui::mouse_click const & e) -> bool {
			if (e.down && (e.button == ui::mouse_button::right || e.button == ui::mouse_button::left))
			{
				loop->post(cancel);
				return true;
			}

			return false;
		});

		event_interceptor->on_key_press([loop, cancel](key_press const & e) -> bool {
			if (e.down && e.key == SDLK_ESCAPE)
			{
				loop->post(cancel);
				return true;
			}

			return false;
		});

		event_interceptor->on_mouse_move([](mouse_move const &){ return true; });

		send_fake_mouse_move_event(event_interceptor.get(), true);

		positioner->set_position(position, positioner::x_align::left, positioner::y_align::top);
		screen->add_child(event_interceptor, screen::x_policy::fill, screen::y_policy::fill);
		return true;
	}

}
