// zlib open source license
//
// Copyright (c) 2020 to 2022 David Forsgren Piuva
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
//    1. The origin of this software must not be misrepresented; you must not
//    claim that you wrote the original software. If you use this software
//    in a product, an acknowledgment in the product documentation would be
//    appreciated but is not required.
//
//    2. Altered source versions must be plainly marked as such, and must not be
//    misrepresented as being the original software.
//
//    3. This notice may not be removed or altered from any source
//    distribution.

#include "ScrollBarImpl.h"

using namespace dsr;

void ScrollBarImpl::loadTheme(VisualTheme theme, const ColorRgbI32 &color) {
	this->scalableImage_scrollTop = theme_getScalableImage(theme, this->vertical ? U"ScrollUp" : U"ScrollLeft");
	this->scalableImage_scrollBottom = theme_getScalableImage(theme, this->vertical ? U"ScrollDown" : U"ScrollRight");
	this->scalableImage_scrollKnob = theme_getScalableImage(theme, this->vertical ? U"VerticalScrollKnob" : U"HorizontalScrollKnob");
	this->scalableImage_scrollBackground = theme_getScalableImage(theme, this->vertical ? U"VerticalScrollList" : U"HorizontalScrollList");
	component_generateImage(theme, this->scalableImage_scrollTop,     this->scrollBarThickness,  this->scrollButtonLength, color.red, color.green, color.blue, 0)(this->scrollButtonTopImage_normal);
	component_generateImage(theme, this->scalableImage_scrollTop,     this->scrollBarThickness,  this->scrollButtonLength, color.red, color.green, color.blue, 1)(this->scrollButtonTopImage_pressed);	
	component_generateImage(theme, this->scalableImage_scrollBottom,  this->scrollBarThickness,  this->scrollButtonLength, color.red, color.green, color.blue, 0)(this->scrollButtonBottomImage_normal);
	component_generateImage(theme, this->scalableImage_scrollBottom,  this->scrollBarThickness,  this->scrollButtonLength, color.red, color.green, color.blue, 1)(this->scrollButtonBottomImage_pressed);
}

void ScrollBarImpl::updateScrollRange(const ScrollRange &range) {
	this->scrollRange = range;
}

void ScrollBarImpl::setValue(int64_t newValue) {
	this->value = newValue;
}

int64_t ScrollBarImpl::getValue() {
	return this->value;
}

void ScrollBarImpl::limitScrolling(const IRect &parentLocation, bool keepPinValueInRange, int64_t pinValue) {
	// Big enough list to need scrolling but big enough list-box to fit two buttons inside
	this->visible = this->scrollRange.minValue < this->scrollRange.maxValue && parentLocation.width() >= scrollBarThickness * 2 && parentLocation.height() >= this->scrollButtonLength * 3;
	if (keepPinValueInRange) {
		// Constrain to keep pinValue in range.
		int64_t maxScroll = pinValue;
		int64_t minScroll = pinValue + 1 - this->scrollRange.visibleItems;
		if (this->value > maxScroll) this->value = maxScroll;
		if (this->value < minScroll) this->value = minScroll;
	}
	// Constrain value to be within the inclusive minValue..maxValue interval, in case that pinValue is out of bound.
	if (this->value > scrollRange.maxValue) this->value = scrollRange.maxValue;
	if (this->value < scrollRange.minValue) this->value = scrollRange.minValue;
}

// Fill from the right side if vertical and bottom if horizontal.
static IRect getWallSide(int32_t parentWidth, int32_t parentHeight, int32_t thickness, bool vertical) {
	return vertical ? IRect(std::max(1, parentWidth - thickness), 0, thickness, parentHeight)
	                : IRect(0, std::max(1, parentHeight - thickness), parentWidth, thickness);
}

// Get the upper part if vertical and left part if horizontal.
static IRect getStartRect(const IRect &original, int32_t startLength, bool vertical) {
	return vertical ? IRect(original.left(), original.top(), original.width(), startLength)
	                : IRect(original.left(), original.top(), startLength, original.height());
}

// Get the bottom part if vertical and right part if horizontal.
static IRect getEndRect(const IRect &original, int32_t endLength, bool vertical) {
	return vertical ? IRect(original.left(), original.bottom() - endLength, original.width(), endLength)
	                : IRect(original.right() - endLength, original.top(), endLength, original.height());
}

static IRect getMiddleRect(const IRect &original, int32_t startCropping, int32_t endCropping, bool vertical) {
	return vertical ? IRect(original.left(), original.top() + startCropping, original.width(), std::max(1, original.height() - startCropping - endCropping))
	                : IRect(original.left() + startCropping, original.top(), std::max(1, original.width() - startCropping - endCropping), original.height());
}

static int32_t getStart(const IRect &rect, bool vertical) {
	return vertical ? rect.top() : rect.left();
}

static int32_t getEnd(const IRect &rect, bool vertical) {
	return vertical ? rect.bottom() : rect.right();
}

static int32_t getLength(const IRect &rect, bool vertical) {
	return vertical ? rect.height() : rect.width();
}

static int32_t getThickness(const IRect &rect, bool vertical) {
	return vertical ? rect.width() : rect.height();
}

IRect ScrollBarImpl::getScrollBarLocation(int32_t parentWidth, int32_t parentHeight) {
	IRect whole = getWallSide(parentWidth, parentHeight, this->scrollBarThickness, this->vertical);
	return getMiddleRect(whole, reservedStart, reservedEnd, this->vertical);
}

IRect ScrollBarImpl::getScrollRegion(const IRect &scrollBarLocation) {
	return getMiddleRect(scrollBarLocation, this->scrollButtonLength, this->scrollButtonLength, this->vertical);
}

IRect ScrollBarImpl::getDecreaseButton(const IRect &scrollBarLocation) {
	return getStartRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
}

IRect ScrollBarImpl::getIncreaseButton(const IRect &scrollBarLocation) {
	return getEndRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
}

IRect ScrollBarImpl::getKnobLocation(const IRect &scrollBarLocation) {
	// Eroded scroll-bar excluding buttons
	// The final knob is a sub-set of this region corresponding to the visibility
	IRect scrollRegion = getMiddleRect(scrollBarLocation, this->scrollButtonLength, this->scrollButtonLength, this->vertical);
	// The knob should represent the selected range within the total range.
	int64_t barLength = getLength(scrollRegion, this->vertical);
	int64_t barThickness = getThickness(scrollRegion, this->vertical);
	int64_t knobLength = (barLength * this->scrollRange.visibleItems) / (this->scrollRange.maxValue + this->scrollRange.visibleItems);
	if (knobLength < barThickness) {
		knobLength = barThickness;
	}
	// Visual range for center
	int64_t scrollStart = (this->vertical ? scrollRegion.top() : scrollRegion.left()) + knobLength / 2;
	int64_t scrollDistance = barLength - knobLength;
	int64_t knobStart = scrollStart + ((this->value * scrollDistance) / this->scrollRange.maxValue) - (knobLength / 2);
	return this->vertical ? IRect(scrollRegion.left(), knobStart, barThickness, knobLength)
	                      : IRect(knobStart, scrollRegion.top(), knobLength, barThickness);
}

void ScrollBarImpl::draw(OrderedImageRgbaU8 &target, VisualTheme &theme, const ColorRgbI32 &color) {
	if (this->visible) {
		int32_t parentWidth = image_getWidth(target);
		int32_t parentHeight = image_getHeight(target);
		IRect scrollBarLocation = this->getScrollBarLocation(parentWidth, parentHeight);
		IRect upper = getStartRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
		IRect lower = getEndRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
		IRect knob = this->getKnobLocation(scrollBarLocation);
		// Only redraw the knob image if its dimensions changed
		if (!image_exists(this->scrollKnobImage_normal)
		  || image_getWidth(this->scrollKnobImage_normal) != knob.width()
		  || image_getHeight(this->scrollKnobImage_normal) != knob.height()) {
			component_generateImage(theme, this->scalableImage_scrollKnob, knob.width(), knob.height(), color.red, color.green, color.blue, 0)(this->scrollKnobImage_normal);
			component_generateImage(theme, this->scalableImage_scrollKnob, knob.width(), knob.height(), color.red, color.green, color.blue, 1)(this->scrollKnobImage_pressed);
		}
		// Only redraw the scroll list if its dimenstions changed
		if (!image_exists(this->scrollBarImage)
		  || image_getWidth(this->scrollBarImage) != scrollBarLocation.width()
		  || image_getHeight(this->scrollBarImage) != scrollBarLocation.height()) {
			component_generateImage(theme, this->scalableImage_scrollBackground, scrollBarLocation.width(), scrollBarLocation.height(), color.red, color.green, color.blue, 0)(this->scrollBarImage);
		}
		// Draw the scroll-bar
		draw_alphaFilter(target, this->scrollBarImage, scrollBarLocation.left(), scrollBarLocation.top());
		draw_alphaFilter(target, this->holdingScrollBar ? this->scrollKnobImage_pressed : this->scrollKnobImage_normal, knob.left(), knob.top());
		draw_alphaFilter(target, this->pressScrollUp ? this->scrollButtonTopImage_pressed : this->scrollButtonTopImage_normal, upper.left(), upper.top());
		draw_alphaFilter(target, this->pressScrollDown ? this->scrollButtonBottomImage_pressed : this->scrollButtonBottomImage_normal, lower.left(), lower.top());
	}
}

bool ScrollBarImpl::pressScrollBar(const IRect &parentLocation, int64_t localCoordinate) {
	int64_t oldValue = this->value;
	IRect scrollBarLocation = this->getScrollBarLocation(parentLocation.width(), parentLocation.height());
	IRect scrollRegion = this->getScrollRegion(scrollBarLocation);
	IRect knobLocation = this->getKnobLocation(scrollBarLocation);
	int64_t knobLength = getLength(knobLocation, this->vertical);
	int64_t minimumAt = getStart(scrollRegion, this->vertical) + knobLength / 2;
	int64_t maximumAt = getEnd(scrollRegion, this->vertical) - knobLength / 2;
	int64_t pixelRange = maximumAt - minimumAt;
	int64_t valueRange = this->scrollRange.maxValue - this->scrollRange.minValue;
	this->value = this->scrollRange.minValue;
	if (pixelRange > 0) {
		this->value += ((localCoordinate - minimumAt) * valueRange + pixelRange / 2) / pixelRange;
	}
	this->limitScrolling(parentLocation);
	// Avoid expensive redrawing if the index did not change
	return this->value != oldValue;
}

bool ScrollBarImpl::receiveMouseEvent(const IRect &parentLocation, const MouseEvent& event) {
	if (!this->visible) {
		return false;
	}
	bool intercepted = false;
	IVector2D localPosition = event.position - parentLocation.upperLeft();
	IRect scrollBarLocation = this->getScrollBarLocation(parentLocation.width(), parentLocation.height());
	IRect cursorLocation = IRect(localPosition.x, localPosition.y, 1, 1);
	int64_t usedCoordinate = (this->vertical ? localPosition.y : localPosition.x);
	if (event.mouseEventType == MouseEventType::MouseDown) {
		if (IRect::touches(scrollBarLocation, cursorLocation)) {
			IRect upperLocation = getStartRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
			IRect lowerLocation = getEndRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
			intercepted = true;
			if (IRect::touches(upperLocation, cursorLocation)) {
				// Upper scroll button
				this->pressScrollUp = true;
				this->value--;
			} else if (IRect::touches(lowerLocation, cursorLocation)) {
				// Lower scroll button
				this->pressScrollDown = true;
				this->value++;
			} else {
				// Start scrolling with the mouse using the relative height on the scroll bar.
				IRect knobLocation = this->getKnobLocation(scrollBarLocation);
				int64_t halfKnobLength = getLength(knobLocation, this->vertical) / 2;
				this->knobHoldOffset = usedCoordinate - (getStart(knobLocation, this->vertical) + halfKnobLength);
				if (this->knobHoldOffset < -halfKnobLength || this->knobHoldOffset > halfKnobLength) {
					// If pressing outside of the knob, pull it directly to the pressed location before pulling from the center.
					this->knobHoldOffset = 0;
					this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
				}
				this->holdingScrollBar = true;
			}
		}
		this->limitScrolling(parentLocation);
	} else if (event.mouseEventType == MouseEventType::MouseUp) {
		this->pressScrollUp = false;
		this->pressScrollDown = false;
		this->holdingScrollBar = false;
		intercepted = true;
	} else if (event.mouseEventType == MouseEventType::Scroll) {
		if (this->vertical) {
			if (event.key == MouseKeyEnum::ScrollUp) {
				this->value--;
			} else if (event.key == MouseKeyEnum::ScrollDown) {
				this->value++;
			}
			this->limitScrolling(parentLocation);
		}
		this->holdingScrollBar = false;
		intercepted = true;
	} else if (event.mouseEventType == MouseEventType::MouseMove) {
		if (this->holdingScrollBar) {
			intercepted = this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
		}
	}
	return intercepted;
}
