/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/widgets/vertical_drum_picker.h"

#include "ui/effects/animation_value_f.h"
#include "ui/ui_utility.h"
#include "styles/style_basic.h"
#include "styles/style_widgets.h"

namespace Ui {
namespace {

constexpr auto kAlmostIndex = float64(.99);
constexpr auto kMinYScale = 0.2;

using PaintItemCallback = VerticalDrumPicker::PaintItemCallback;

} // namespace

PaintItemCallback VerticalDrumPicker::DefaultPaintCallback(
		const style::font &font,
		int itemHeight,
		Fn<void(QPainter&, QRectF, int)> paintContent) {
	return [=](
			QPainter &p,
			int index,
			float64 y,
			float64 distanceFromCenter,
			int outerWidth) {
		const auto r = QRectF(0, y, outerWidth, itemHeight);
		const auto progress = std::abs(distanceFromCenter);
		const auto revProgress = 1. - progress;
		p.save();
		p.translate(r.center());
		const auto yScale = kMinYScale
			+ (1. - kMinYScale) * anim::easeOutCubic(1., revProgress);
		p.scale(1., yScale);
		p.translate(-r.center());
		p.setOpacity(revProgress);
		p.setFont(font);
		p.setPen(st::defaultFlatLabel.textFg);
		paintContent(p, r, index);
		p.restore();
	};
}

PickerAnimation::PickerAnimation() = default;

void PickerAnimation::jumpToOffset(int offset) {
	_result.from = _result.current;
	_result.to += offset;
	_animation.stop();
	auto callback = [=](float64 value) {
		const auto was = _result.current;
		_result.current = anim::interpolateF(
			_result.from,
			_result.to,
			value);
		_updates.fire(_result.current - was);
	};
	if (anim::Disabled()) {
		auto value = float64(0.);
		const auto diff = _result.to - _result.from;
		const auto step = std::min(
			kAlmostIndex,
			1. / (std::max(1. - kAlmostIndex, std::abs(diff) + 1)));
		while (true) {
			value += step;
			if (value >= 1.) {
				callback(1.);
				break;
			} else {
				callback(value);
			}
		}
		return;
	}
	_animation.start(
		std::move(callback),
		0.,
		1.,
		st::fadeWrapDuration);
}

void PickerAnimation::setResult(float64 from, float64 current, float64 to) {
	_result = { from, current, to };
}

rpl::producer<PickerAnimation::Shift> PickerAnimation::updates() const {
	return _updates.events();
}

VerticalDrumPicker::VerticalDrumPicker(
	not_null<Ui::RpWidget*> parent,
	PaintItemCallback &&paintCallback,
	int itemsCount,
	int itemHeight,
	int startIndex,
	bool looped)
: RpWidget(parent)
, _itemsCount(itemsCount)
, _itemHeight(itemHeight)
, _paintCallback(std::move(paintCallback))
, _pendingStartIndex(startIndex)
, _loopData({ .looped = looped }) {
	Expects(_paintCallback != nullptr);

	sizeValue(
	) | rpl::on_next([=](const QSize &s) {
		_itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight);
		_itemsVisible.centerOffset = _itemsVisible.count / 2;
		if ((_pendingStartIndex >= 0) && _itemsVisible.count) {
			_index = normalizedIndex(_pendingStartIndex
				- _itemsVisible.centerOffset);
			_pendingStartIndex = -1;
		}

		if (!_loopData.looped) {
			_loopData.minIndex = -_itemsVisible.centerOffset;
			_loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset;
		}

		_changes.fire({});
	}, lifetime());

	paintRequest(
	) | rpl::on_next([=] {
		auto p = QPainter(this);

		const auto outerWidth = width();
		const auto centerY = height() / 2.;
		const auto shiftedY = _itemHeight * _shift;
		for (auto i = -1; i < (_itemsVisible.count + 1); i++) {
			const auto index = normalizedIndex(i + _index);
			if (!isIndexInRange(index)) {
				continue;
			}
			const auto y = (_itemHeight * i + shiftedY);
			_paintCallback(
				p,
				index,
				y,
				((y + _itemHeight / 2.) - centerY) / centerY,
				outerWidth);
		}
	}, lifetime());

	_animation.updates(
	) | rpl::on_next([=](PickerAnimation::Shift shift) {
		increaseShift(shift);
	}, lifetime());
}

void VerticalDrumPicker::increaseShift(float64 by) {
	{
		// Guard input.
		if (by >= 1.) {
			by = kAlmostIndex;
		} else if (by <= -1.) {
			by = -kAlmostIndex;
		}
	}

	auto shift = _shift;
	auto index = _index;
	shift += by;
	if (shift >= 1.) {
		shift -= 1.;
		index--;
		index = normalizedIndex(index);
	} else if (shift <= -1.) {
		shift += 1.;
		index++;
		index = normalizedIndex(index);
	}
	if (_loopData.minIndex == _loopData.maxIndex) {
		_shift = 0.;
	} else if (!_loopData.looped && (index <= _loopData.minIndex)) {
		_shift = std::min(0., shift);
		_index = _loopData.minIndex;
	} else if (!_loopData.looped && (index >= _loopData.maxIndex)) {
		_shift = std::max(0., shift);
		_index = _loopData.maxIndex;
	} else {
		_shift = shift;
		_index = index;
	}
	_changes.fire({});
	update();
}

void VerticalDrumPicker::handleWheelEvent(not_null<QWheelEvent*> e) {
	const auto direction = Ui::WheelDirection(e);
	if (direction) {
		_animation.jumpToOffset(direction);
	} else {
		if (const auto delta = e->pixelDelta().y(); delta) {
			increaseShift(delta / float64(_itemHeight));
		} else if (e->phase() == Qt::ScrollEnd) {
			animationDataFromIndex();
			_animation.jumpToOffset(0);
		} else {
			constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);

			_touch.verticalDelta += e->angleDelta().y();
			while (std::abs(_touch.verticalDelta) >= step) {
				if (_touch.verticalDelta < 0) {
					_touch.verticalDelta += step;
					_animation.jumpToOffset(1);
				} else {
					_touch.verticalDelta -= step;
					_animation.jumpToOffset(-1);
				}
			}
		}
	}
}

void VerticalDrumPicker::handleKeyEvent(not_null<QKeyEvent*> e) {
	if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Up) {
		_animation.jumpToOffset(1);
	} else if (e->key() == Qt::Key_PageUp && !e->isAutoRepeat()) {
		_animation.jumpToOffset(_itemsVisible.count);
	} else if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Down) {
		_animation.jumpToOffset(-1);
	} else if (e->key() == Qt::Key_PageDown && !e->isAutoRepeat()) {
		_animation.jumpToOffset(-_itemsVisible.count);
	}
}

void VerticalDrumPicker::handleMouseEvent(not_null<QMouseEvent*> e) {
	if (e->type() == QEvent::MouseButtonPress) {
		_mouse.pressed = true;
		_mouse.lastPositionY = e->pos().y();
	} else if (e->type() == QEvent::MouseMove) {
		if (_mouse.pressed) {
			const auto was = _mouse.lastPositionY;
			_mouse.lastPositionY = e->pos().y();
			const auto diff = _mouse.lastPositionY - was;
			increaseShift(float64(diff) / _itemHeight);
			_mouse.clickDisabled = true;
		}
	} else if (e->type() == QEvent::MouseButtonRelease) {
		if (_mouse.clickDisabled) {
			animationDataFromIndex();
			_animation.jumpToOffset(0);
		} else {
			_mouse.lastPositionY = e->pos().y();
			const auto toOffset = _itemsVisible.centerOffset
				- (_mouse.lastPositionY / _itemHeight);
			_animation.jumpToOffset(toOffset);
		}
		_mouse = {};
	}
}


void VerticalDrumPicker::wheelEvent(QWheelEvent *e) {
	handleWheelEvent(e);
}

void VerticalDrumPicker::mousePressEvent(QMouseEvent *e) {
	handleMouseEvent(e);
}

void VerticalDrumPicker::mouseMoveEvent(QMouseEvent *e) {
	handleMouseEvent(e);
}

void VerticalDrumPicker::mouseReleaseEvent(QMouseEvent *e) {
	handleMouseEvent(e);
}

void VerticalDrumPicker::keyPressEvent(QKeyEvent *e) {
	handleKeyEvent(e);
}

void VerticalDrumPicker::animationDataFromIndex() {
	_animation.setResult(
		_index,
		_index + _shift,
		std::round(_index + _shift));
}

bool VerticalDrumPicker::isIndexInRange(int index) const {
	return (index >= 0) && (index < _itemsCount);
}

int VerticalDrumPicker::normalizedIndex(int index) const {
	if (!_loopData.looped) {
		return index;
	}
	if (index < 0) {
		index += _itemsCount;
	} else if (index >= _itemsCount) {
		index -= _itemsCount;
	}
	return index;
}

int VerticalDrumPicker::index() const {
	return normalizedIndex(_index + _itemsVisible.centerOffset);
}

rpl::producer<int> VerticalDrumPicker::changes() const {
	return _changes.events() | rpl::map([=] { return index(); });
}

rpl::producer<int> VerticalDrumPicker::value() const {
	return rpl::single(index())
		| rpl::then(changes())
		| rpl::distinct_until_changed();
}

} // namespace Ui
