#include <ui/ScrollWidget.hpp>
#include <context.hpp>


namespace rack {
namespace ui {


struct ScrollWidget::Internal {
	bool scrolling = false;
};


ScrollWidget::ScrollWidget() {
	internal = new Internal;

	container = new widget::Widget;
	addChild(container);

	horizontalScrollbar = new Scrollbar;
	horizontalScrollbar->vertical = false;
	horizontalScrollbar->hide();
	addChild(horizontalScrollbar);

	verticalScrollbar = new Scrollbar;
	verticalScrollbar->vertical = true;
	verticalScrollbar->hide();
	addChild(verticalScrollbar);
}


ScrollWidget::~ScrollWidget() {
	delete internal;
}


void ScrollWidget::scrollTo(math::Rect r) {
	math::Rect bound = math::Rect::fromMinMax(r.getBottomRight().minus(box.size), r.pos);
	offset = offset.clampSafe(bound);
}


math::Rect ScrollWidget::getContainerOffsetBound() {
	math::Rect r;
	r.pos = containerBox.pos;
	r.size = containerBox.size.minus(box.size);
	return r;
}


math::Vec ScrollWidget::getHandleOffset() {
	return offset.minus(containerBox.pos).div(getContainerOffsetBound().size);
}


math::Vec ScrollWidget::getHandleSize() {
	return box.size.div(containerBox.size);
}


bool ScrollWidget::isScrolling() {
	return internal->scrolling;
}


void ScrollWidget::draw(const DrawArgs& args) {
	nvgScissor(args.vg, RECT_ARGS(args.clipBox));
	Widget::draw(args);
	nvgResetScissor(args.vg);
}


void ScrollWidget::step() {
	Widget::step();

	// Set containerBox cache
	containerBox = container->getVisibleChildrenBoundingBox();

	// Clamp scroll offset
	math::Rect offsetBounds = getContainerOffsetBound();
	offset = offset.clamp(offsetBounds);

	// Update the container's position from the offset
	container->box.pos = offset.neg().round();

	// Make scrollbars visible only if there is a positive range to scroll.
	if (hideScrollbars) {
		horizontalScrollbar->setVisible(false);
		verticalScrollbar->setVisible(false);
	}
	else {
		horizontalScrollbar->setVisible(offsetBounds.size.x > 0.f);
		verticalScrollbar->setVisible(offsetBounds.size.y > 0.f);
	}

	// Reposition and resize scroll bars
	math::Vec inner = box.size.minus(math::Vec(verticalScrollbar->box.size.x, horizontalScrollbar->box.size.y));
	horizontalScrollbar->box.pos.y = inner.y;
	verticalScrollbar->box.pos.x = inner.x;
	horizontalScrollbar->box.size.x = verticalScrollbar->isVisible() ? inner.x : box.size.x;
	verticalScrollbar->box.size.y = horizontalScrollbar->isVisible() ? inner.y : box.size.y;
}


void ScrollWidget::onHover(const HoverEvent& e) {
	OpaqueWidget::onHover(e);

	if (!e.mouseDelta.isZero()) {
		internal->scrolling = false;
	}
}


void ScrollWidget::onButton(const ButtonEvent& e) {
	math::Rect offsetBound = getContainerOffsetBound();
	// Check if scrollable
	if (offsetBound.size.x > 0.f || offsetBound.size.y > 0.f) {
		// Handle Alt-click before children, since most widgets consume Alt-click without needing to.
		if (e.button == GLFW_MOUSE_BUTTON_LEFT && (e.mods & RACK_MOD_MASK) == GLFW_MOD_ALT) {
			e.consume(this);
			return;
		}
		// Might as well handle middle click before children as well.
		if (e.button == GLFW_MOUSE_BUTTON_MIDDLE) {
			e.consume(this);
			return;
		}
	}

	Widget::onButton(e);
}


void ScrollWidget::onDragStart(const DragStartEvent& e) {
	e.consume(this);
}


void ScrollWidget::onDragMove(const DragMoveEvent& e) {
	math::Vec offsetDelta = e.mouseDelta.div(getAbsoluteZoom());
	offset = offset.minus(offsetDelta);
}


void ScrollWidget::onHoverScroll(const HoverScrollEvent& e) {
	OpaqueWidget::onHoverScroll(e);
	if (e.isConsumed())
		return;

	// Check if scrollable
	math::Rect offsetBound = getContainerOffsetBound();
	if (offsetBound.size.x <= 0.f && offsetBound.size.y <= 0.f)
		return;

	math::Vec scrollDelta = e.scrollDelta;
	// Flip coordinates if shift is held
	// Mac (or GLFW?) already does this for us.
#if !defined ARCH_MAC
	int mods = APP->window->getMods();
	if ((mods & RACK_MOD_MASK) & GLFW_MOD_SHIFT)
		scrollDelta = scrollDelta.flip();
#endif

	offset = offset.minus(scrollDelta);
	e.consume(this);
	internal->scrolling = true;
}


void ScrollWidget::onHoverKey(const HoverKeyEvent& e) {
	OpaqueWidget::onHoverKey(e);
	if (e.isConsumed())
		return;

	// Check if scrollable
	math::Rect offsetBound = getContainerOffsetBound();
	if (offsetBound.size.x <= 0.f && offsetBound.size.y <= 0.f)
		return;

	if (e.action == GLFW_PRESS || e.action == GLFW_REPEAT) {
		if (e.isKeyCommand(GLFW_KEY_PAGE_UP)) {
			offset.y -= box.size.y * 0.5;
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_PAGE_UP, GLFW_MOD_SHIFT)) {
			offset.x -= box.size.x * 0.5;
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_PAGE_DOWN)) {
			offset.y += box.size.y * 0.5;
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_PAGE_DOWN, GLFW_MOD_SHIFT)) {
			offset.x += box.size.x * 0.5;
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_HOME)) {
			math::Rect containerBox = container->getVisibleChildrenBoundingBox();
			offset.y = containerBox.getTop();
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_HOME, GLFW_MOD_SHIFT)) {
			math::Rect containerBox = container->getVisibleChildrenBoundingBox();
			offset.x = containerBox.getLeft();
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_END)) {
			math::Rect containerBox = container->getVisibleChildrenBoundingBox();
			offset.y = containerBox.getBottom();
			e.consume(this);
		}
		if (e.isKeyCommand(GLFW_KEY_END, GLFW_MOD_SHIFT)) {
			math::Rect containerBox = container->getVisibleChildrenBoundingBox();
			offset.x = containerBox.getRight();
			e.consume(this);
		}
	}
}


} // namespace ui
} // namespace rack
