/*
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/middle_click_autoscroll.h"

#include "styles/style_chat.h"
#include "styles/style_widgets.h"
#include "ui/effects/animation_value.h"
#include "ui/rect.h"

namespace Ui {
namespace {

constexpr auto kDelay = 15;
constexpr auto kHoldToToggleThreshold = crl::time(220);

[[nodiscard]] int Deadzone() {
	return std::max(1, int(std::lround(style::ConvertFloatScale(6.))));
}

[[nodiscard]] QImage PaintCursorImage(
		int size,
		int factor,
		bool drawOuter,
		bool drawTopArrow,
		bool drawBottomArrow,
		const QColor &inner,
		const QColor &indicator,
		const QColor &topArrow,
		const QColor &bottomArrow) {
	auto image = QImage(
		QSize(size, size) * factor,
		QImage::Format_ARGB32_Premultiplied);
	image.setDevicePixelRatio(factor);
	image.fill(Qt::transparent);
	auto p = QPainter(&image);
	p.setRenderHint(QPainter::Antialiasing);
	p.setPen(Qt::NoPen);

	const auto outerRect = QRectF(QPointF(0.5, 0.5), Size(size - 1.));
	if (drawOuter) {
		const auto outerStroke = std::max(1., style::ConvertFloatScale(1.5));
		auto pen = QPen(indicator);
		pen.setWidthF(outerStroke);
		p.setPen(pen);
		p.setBrush(inner);
		p.drawEllipse(outerRect.marginsRemoved(Margins(outerStroke * 0.5)));
	}
	p.setPen(Qt::NoPen);
	const auto centerSkip = style::ConvertFloatScale(10.5);
	const auto centerRect = outerRect - Margins(centerSkip);
	p.setBrush(indicator);
	p.drawEllipse(centerRect);

	const auto middle = size * 0.5;
	const auto arrowGap = style::ConvertFloatScale(5.);
	const auto headHalfWidth = style::ConvertFloatScale(4.5);
	const auto headHeight = style::ConvertFloatScale(3.5);
	const auto stroke = std::max(1., style::ConvertFloatScale(2.));
	const auto drawArrow = [&](bool up, const QColor &color) {
		auto pen = QPen(color);
		pen.setWidthF(stroke);
		pen.setCapStyle(Qt::RoundCap);
		pen.setJoinStyle(Qt::RoundJoin);
		p.setPen(pen);
		const auto dir = up ? -1. : 1.;
		const auto tipY = middle + dir * (arrowGap + headHeight);
		const auto neckY = tipY - dir * headHeight;
		p.drawLine(
			QPointF(middle, tipY),
			QPointF(middle - headHalfWidth, neckY));
		p.drawLine(
			QPointF(middle, tipY),
			QPointF(middle + headHalfWidth, neckY));
	};
	if (drawTopArrow) {
		drawArrow(true, topArrow);
	}
	if (drawBottomArrow) {
		drawArrow(false, bottomArrow);
	}
	return image;
}

} // namespace

MiddleClickAutoscroll::MiddleClickAutoscroll(
	Fn<void(int)> scrollBy,
	Fn<void(const QCursor &)> applyCursor,
	Fn<void()> restoreCursor,
	Fn<bool()> shouldContinue)
: _scrollBy(std::move(scrollBy))
, _applyCursor(std::move(applyCursor))
, _restoreCursor(std::move(restoreCursor))
, _shouldContinue(std::move(shouldContinue))
, _timer([=] { onTimer(); }) {
}

void MiddleClickAutoscroll::toggleOrBeginHold(const QPoint &globalPosition) {
	if (_active) {
		stop();
		return;
	}
	_middlePressed = true;
	_middlePressedAt = crl::now();
	start(globalPosition);
}

bool MiddleClickAutoscroll::finishHold(Qt::MouseButton button) {
	if (!_middlePressed || (button != Qt::MiddleButton)) {
		return false;
	}
	_middlePressed = false;
	if ((crl::now() - _middlePressedAt) >= kHoldToToggleThreshold) {
		stop();
	}
	return true;
}

void MiddleClickAutoscroll::start(const QPoint &globalPosition) {
	if (_active) {
		stop();
	}
	_active = true;
	_startPosition = globalPosition;
	_time = crl::now();
	updateCursor(QCursor::pos().y() - _startPosition.y());
	_timer.callEach(kDelay);
}

void MiddleClickAutoscroll::stop() {
	if (!_active) {
		return;
	}
	_middlePressed = false;
	_active = false;
	_timer.cancel();
	if (_restoreCursor) {
		_restoreCursor();
	}
}

void MiddleClickAutoscroll::updateCursor(int delta) {
	if (!_applyCursor) {
		return;
	}
	const auto mode = (std::abs(delta) <= Deadzone())
		? CursorMode::Neutral
		: (delta < 0)
		? CursorMode::Up
		: CursorMode::Down;
	_applyCursor(makeCursor(mode));
}

QCursor MiddleClickAutoscroll::makeCursor(CursorMode mode) {
	struct CachedCursor {
		CursorMode mode = CursorMode::Neutral;
		int size = 0;
		int factor = 0;
		QColor inner;
		QColor indicator;
		QCursor cursor;
	};
	const auto size = std::max(
		1,
		int(std::lround(style::ConvertFloatScale(27.))));
	const auto factor = style::DevicePixelRatio();
	const auto inner = anim::with_alpha(st::windowBg->c, (240. / 255.));
	const auto active = anim::with_alpha(st::windowFg->c, (210. / 255.));
	const auto passive = anim::with_alpha(
		st::windowSubTextFg->c,
		(210. / 255.));
	const auto neutral = (mode == CursorMode::Neutral);
	const auto indicator = neutral ? passive : active;
	static auto cache = std::vector<CachedCursor>();
	if (const auto i = ranges::find_if(
			cache,
			[&](const CachedCursor &entry) {
				return (entry.mode == mode)
					&& (entry.size == size)
					&& (entry.factor == factor)
					&& (entry.inner == inner)
					&& (entry.indicator == indicator);
			});
		i != cache.end()) {
		return i->cursor;
	}
	const auto image = PaintCursorImage(
		size,
		factor,
		neutral,
		mode != CursorMode::Down,
		mode != CursorMode::Up,
		inner,
		indicator,
		indicator,
		indicator);
	const auto hot = int(std::floor(size * 0.5));
	auto cursor = QCursor(QPixmap::fromImage(image), hot, hot);
	cache.push_back(CachedCursor{
		.mode = mode,
		.size = size,
		.factor = factor,
		.inner = inner,
		.indicator = indicator,
		.cursor = cursor,
	});
	return cursor;
}

void MiddleClickAutoscroll::onTimer() {
	if (!_active) {
		return;
	} else if (_shouldContinue && !_shouldContinue()) {
		stop();
		return;
	}
	const auto now = crl::now();
	const auto elapsed = std::max(now - _time, crl::time(1));
	_time = now;
	const auto delta = QCursor::pos().y() - _startPosition.y();
	updateCursor(delta);
	const auto absolute = std::abs(delta) - Deadzone();
	if (absolute <= 0) {
		return;
	}
	const auto speed = std::min(
		absolute * float64(st::middleClickAutoscrollSpeedScale),
		float64(st::middleClickAutoscrollMaxSpeed));
	auto scroll = int(std::lround((speed * elapsed) / 1000.));
	if (scroll <= 0) {
		scroll = 1;
	}
	if (delta < 0) {
		scroll = -scroll;
	}
	_scrollBy(scroll);
}

} // namespace Ui
