#include <psemek/ui/scroller.hpp>

#include <psemek/math/contains.hpp>

namespace psemek::ui
{

	void scroller::set_preferred_direction(direction d)
	{
		preferred_direction_ = d;
	}

	bool scroller::set_horizontal_scroll(bool enabled)
	{
		std::swap(horizontal_, enabled);
		return enabled;
	}

	bool scroller::set_vertical_scroll(bool enabled)
	{
		std::swap(vertical_, enabled);
		return enabled;
	}

	math::box<float, 2> scroller::events_bbox() const
	{
		auto st = merged_own_style();
		auto result = shape_.box;

		if (horizontal_scroll())
			result[1].max -= width() * *st->scale;
		if (vertical_scroll())
			result[0].max -= width() * *st->scale;

		return result;
	}

	bool scroller::on_event(mouse_move const & e)
	{
		mouse_ = e.position;

		auto pos = math::cast<float>(e.position);

		if (horizontal_scroll())
		{
			auto box = horizontal_box();
			bool over = math::contains(box, pos);

			switch (horizontal_state_)
			{
			case state_t::normal:
				if (over)
					horizontal_state_ = state_t::mouseover;
				break;
			case state_t::mouseover:
				if (!over)
					horizontal_state_ = state_t::normal;
				break;
			case state_t::mousedown:
				{
					if (!child_)
						return false;

					float c = math::unlerp<float>(box[0], e.position[0]);
					set_position(direction::horizontal, math::clamp(c, {0.f, 1.f}), true);
				}
				break;
			}
		}

		if (vertical_scroll())
		{
			auto box = vertical_box();
			bool over = math::contains(box, pos);

			switch (vertical_state_)
			{
			case state_t::normal:
				if (over)
					vertical_state_ = state_t::mouseover;
				break;
			case state_t::mouseover:
				if (!over)
					vertical_state_ = state_t::normal;
				break;
			case state_t::mousedown:
				{
					if (!child_)
						return false;

					float c = math::unlerp<float>(box[1], e.position[1]);
					c = math::clamp(c, {0.f, 1.f});
					set_position(direction::vertical, math::clamp(c, {0.f, 1.f}), true);
				}
				break;
			}
		}

		return false;
	}

	bool scroller::on_event(mouse_click const & e)
	{
		if (e.button != mouse_button::left)
			return false;

		bool result = false;

		if (horizontal_scroll())
		{
			switch (horizontal_state_)
			{
			case state_t::normal:
				break;
			case state_t::mouseover:
				if (e.down)
				{
					horizontal_state_ = state_t::mousedown;
					result = true;
				}
				break;
			case state_t::mousedown:
				if (!e.down)
				{
					horizontal_state_ = state_t::mouseover;
					result = true;
				}
				break;
			}
		}

		if (vertical_scroll())
		{
			switch (vertical_state_)
			{
			case state_t::normal:
				break;
			case state_t::mouseover:
				if (e.down)
				{
					vertical_state_ = state_t::mousedown;
					result = true;
				}
				break;
			case state_t::mousedown:
				if (!e.down)
				{
					vertical_state_ = state_t::mouseover;
					result = true;
				}
				break;
			}
		}

		return result;
	}

	bool scroller::on_event(mouse_wheel const & e)
	{
		if (mouse_ && math::contains(shape_.box, math::cast<float>(*mouse_)))
		{
			math::vector<float, 2> delta = {0.f, 0.f};

			auto pos = math::cast<float>(*mouse_);

			auto vcontains = math::contains(vertical_box(), pos);
			auto hcontains = math::contains(horizontal_box(), pos);

			if (vertical_scroll() && horizontal_scroll())
			{
				if (vcontains)
					delta[1] = e.delta;
				else if (hcontains)
					delta[0] = e.delta;
				else if (preferred_direction() == direction::vertical)
					delta[1] = e.delta;
				else
					delta[0] = e.delta;
			}
			else if (vertical_scroll())
			{
				delta[1] = e.delta;
			}
			else if (horizontal_scroll())
			{
				delta[0] = e.delta;
			}

			auto old = shift_tgt_;
			shift_tgt_ += delta * 50.f;
			clamp_shift();
			on_scroll(shift_tgt_ - old);
			post_reshape();
			return true;
		}
		return false;
	}

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

		auto st = merged_own_style();

		if (child_)
		{
			auto child_constraints = child_->size_constraints();

			auto child_bbox = bbox;

			if (horizontal_scroll())
				child_bbox[1].max -= width() * *st->scale;
			if (vertical_scroll())
				child_bbox[0].max -= width() * *st->scale;

			if (width_first())
			{
				child_constraints[1] = child_->height_constraints(child_bbox[0].length());
				child_bbox[1].max = child_bbox[1].min + child_constraints[1].min;
			}
			else
			{
				child_constraints[0] = child_->width_constraints(child_bbox[1].length());
				child_bbox[0].max = child_bbox[0].min + child_constraints[0].min;
			}

			{
				auto old_shift = shift_tgt_;
				clamp_shift();
				if (shift_tgt_[0] != old_shift[0])
					shift_[0] = shift_tgt_[0];
				if (shift_tgt_[1] != old_shift[1])
					shift_[1] = shift_tgt_[1];
			}

			if (horizontal_scroll())
				child_bbox[0] += shift_[0];

			if (vertical_scroll())
				child_bbox[1] += shift_[1];

			child_->reshape(child_bbox);
		}
	}

	math::box<float, 2> scroller::size_constraints() const
	{
		math::box<float, 2> result;
		result[0] = {0.f, std::numeric_limits<float>::infinity()};
		result[1] = {0.f, std::numeric_limits<float>::infinity()};

		if (child_)
		{
			auto child_constraints = child_->size_constraints();

			if (!horizontal_scroll())
				result[0] = child_constraints[0];

			if (!vertical_scroll())
				result[1] = child_constraints[1];
		}

		auto st = merged_own_style();

		if (horizontal_scroll())
			result[1] += width() * *st->scale;
		if (vertical_scroll())
			result[0] += width() * *st->scale;

		return result;
	}

	math::interval<float> scroller::width_constraints(float height) const
	{
		math::interval<float> result = {0.f, std::numeric_limits<float>::infinity()};

		auto st = merged_own_style();

		if (child_ && !horizontal_scroll())
			result = child_->width_constraints(height - width() * *st->scale);

		if (vertical_scroll())
			result += width() * *st->scale;

		return result;
	}

	math::interval<float> scroller::height_constraints(float width) const
	{
		math::interval<float> result = {0.f, std::numeric_limits<float>::infinity()};

		auto st = merged_own_style();

		if (child_ && !horizontal_scroll())
			result = child_->height_constraints(width - this->width() * *st->scale);

		if (horizontal_scroll())
			result += this->width() * *st->scale;

		return result;
	}

	float scroller::position(direction dir) const
	{
		if (!child_) return 0.f;

		auto child_box = child_->shape().bbox();
		auto st = merged_own_style();
		auto child_area = shape_.box.dimensions();

		if (horizontal_scroll())
			child_area[1] -= width() * *st->scale;
		if (vertical_scroll())
			child_area[0] -= width() * *st->scale;

		if (dir == direction::horizontal)
			return -shift_[0] / (child_box[0].length() - child_area[0]);
		else
			return -shift_[1] / (child_box[1].length() - child_area[1]);
	}

	void scroller::set_position(direction dir, float position, bool animate)
	{
		if (dir == direction::horizontal && !horizontal_scroll())
			return;
		if (dir == direction::vertical && !vertical_scroll())
			return;

		auto old_shift = shift_tgt_;

		int dim = (dir == direction::horizontal) ? 0 : 1;

		auto child_box = child_->shape().bbox();
		auto st = merged_own_style();
		auto child_area = shape_.box.dimensions();

		if (horizontal_scroll())
			child_area[1] -= width() * *st->scale;
		if (vertical_scroll())
			child_area[0] -= width() * *st->scale;

		shift_tgt_[dim] = position * (child_area[dim] - child_box[dim].length());
		clamp_shift();

		if (!animate)
			shift_[dim] = shift_tgt_[dim];

		on_scroll(shift_tgt_ - old_shift);
	}

	void scroller::update(float dt)
	{
		shift_ += (shift_tgt_ - shift_) * std::min(25.f * dt, 1.f);
		post_reshape();
	}

	void scroller::draw(painter & p) const
	{
		auto st = merged_own_style();
		float w = width() * *st->scale;

		auto box = shape_.box;
		if (horizontal_scroll())
			box[1].max -= w;
		if (vertical_scroll())
			box[0].max -= w;

		p.begin_stencil();
		p.draw_rect(box, {0, 0, 0, 255});
		p.commit_stencil();
	}

	void scroller::post_draw(painter & p) const
	{
		p.end_stencil();
	}

	void scroller::on_scroll(math::vector<float, 2> const &)
	{}

	float scroller::width() const
	{
		return 0;
	}

	math::box<float, 2> scroller::visible_range() const
	{
		if (!child_)
			return {{{0.f, 1.f}, {0.f, 1.f}}};

		math::box<float, 2> result;
		auto c = child_->size_constraints();
		result[0] = {- shift_[0] / c[0].min, (shape_.box[0].length() - shift_[0]) / c[0].min};
		result[1] = {- shift_[1] / c[1].min, (shape_.box[1].length() - shift_[1]) / c[1].min};

		result[0].max = std::min(result[0].max, 1.f);
		result[1].max = std::min(result[1].max, 1.f);

		return result;
	}

	math::box<float, 2> scroller::horizontal_box() const
	{
		auto box = shape_.box;

		float w = width() * *merged_own_style()->scale;

		box[1].min = box[1].max - w;
		if (vertical_scroll())
			box[0].max -= w;
		return box;
	}

	math::box<float, 2> scroller::vertical_box() const
	{
		auto box = shape_.box;

		float w = width() * *merged_own_style()->scale;

		box[0].min = box[0].max - w;
		if (horizontal_scroll())
			box[1].max -= w;
		return box;
	}

	void scroller::clamp_shift()
	{
		if (!child_)
			return;

		math::vector<float, 2> min = child_->shape().bbox().dimensions();

		auto st = merged_own_style();
		auto child_area = shape_.box.dimensions();

		if (horizontal_scroll())
			child_area[1] -= width() * *st->scale;
		if (vertical_scroll())
			child_area[0] -= width() * *st->scale;

		min[0] = child_area[0] - min[0];
		min[1] = child_area[1] - min[1];

		// Can't use clamp since the order of min & max matters
		shift_tgt_[0] = std::max(shift_tgt_[0], min[0]);
		shift_tgt_[0] = std::min(shift_tgt_[0], 0.f);

		shift_tgt_[1] = std::max(shift_tgt_[1], min[1]);
		shift_tgt_[1] = std::min(shift_tgt_[1], 0.f);
	}

}
