/*
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 "dialogs/ui/dialogs_stories_list.h"

#include "base/event_filter.h"
#include "base/qt_signal_producer.h"
#include "lang/lang_keys.h"
#include "ui/effects/outline_segments.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/tooltip.h"
#include "ui/painter.h"
#include "styles/style_dialogs.h"

#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
#include <QtGui/QPainter>

#include "base/debug_log.h"

namespace Dialogs::Stories {
namespace {

constexpr auto kSmallThumbsShown = 3;
constexpr auto kPreloadPages = 2;
constexpr auto kExpandAfterRatio = 0.72;
constexpr auto kCollapseAfterRatio = 0.68;
constexpr auto kFrictionRatio = 0.15;
constexpr auto kExpandCatchUpDuration = crl::time(200);
constexpr auto kMaxTooltipNames = 3;
constexpr auto kStoriesTooltipHideBgOpacity = 0.2;

[[nodiscard]] int AvailableNameWidth(const style::DialogsStoriesList &st) {
	const auto &full = st.full;
	const auto &font = full.nameStyle.font;
	const auto skip = font->spacew;
	return full.photoLeft * 2 + full.photo - 2 * skip;
}

[[nodiscard]] object_ptr<Ui::RpWidget> MakeTooltipContent(
		not_null<QWidget*> parent,
		rpl::producer<TextWithEntities> text,
		Fn<void()> hide) {
	const auto size = st::dialogsStoriesTooltipHide.width;
	const auto skip = st::defaultImportantTooltip.padding.right();
	auto result = object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
		parent,
		Ui::MakeNiceTooltipLabel(
			parent,
			std::move(text),
			st::dialogsStoriesTooltipMaxWidth,
			st::dialogsStoriesTooltipLabel),
		(st::defaultImportantTooltip.padding
			+ QMargins(0, 0, skip + size, 0)));
	const auto button = Ui::CreateChild<Ui::IconButton>(
		result.data(),
		st::dialogsStoriesTooltipHide);
	result->sizeValue(
	) | rpl::start_with_next([=](QSize size) {
		button->resize(button->width(), size.height());
		button->moveToRight(0, 0, size.width());
	}, button->lifetime());
	button->setClickedCallback(std::move(hide));
	return result;
}

} // namespace

struct List::Layout {
	int itemsCount = 0;
	QPointF geometryShift;
	float64 expandedRatio = 0.;
	float64 expandRatio = 0.;
	float64 ratio = 0.;
	float64 segmentsSpinProgress = 0.;
	float64 thumbnailLeft = 0.;
	float64 photoLeft = 0.;
	float64 left = 0.;
	float64 single = 0.;
	int smallSkip = 0;
	int leftFull = 0;
	int leftSmall = 0;
	int singleFull = 0;
	int singleSmall = 0;
	int startIndexSmall = 0;
	int endIndexSmall = 0;
	int startIndexFull = 0;
	int endIndexFull = 0;
};

List::List(
	not_null<QWidget*> parent,
	const style::DialogsStoriesList &st,
	rpl::producer<Content> content)
: RpWidget(parent)
, _st(st) {
	setCursor(style::cur_default);

	std::move(content) | rpl::start_with_next([=](Content &&content) {
		showContent(std::move(content));
	}, lifetime());

	setMouseTracking(true);
	resize(0, _data.empty() ? 0 : st.full.height);
}

List::~List() = default;

void List::showContent(Content &&content) {
	if (_content == content) {
		return;
	}
	if (content.elements.empty()) {
		_data = {};
		_empty = true;
		return;
	}
	const auto wasCount = int(_data.items.size());
	_content = std::move(content);
	auto items = base::take(_data.items);
	_data.items.reserve(_content.elements.size());
	for (const auto &element : _content.elements) {
		const auto id = element.id;
		const auto i = ranges::find(items, id, [](const Item &item) {
			return item.element.id;
		});
		if (i != end(items)) {
			_data.items.push_back(std::move(*i));
			auto &item = _data.items.back();
			if (item.element.thumbnail != element.thumbnail) {
				item.element.thumbnail = element.thumbnail;
				item.subscribed = false;
			}
			if (item.element.name != element.name) {
				item.element.name = element.name;
				item.nameCache = QImage();
			}
			item.element.count = element.count;
			item.element.unreadCount = element.unreadCount;
		} else {
			_data.items.emplace_back(Item{ .element = element });
		}
	}
	_lastCollapsedGeometry = {};
	if (int(_data.items.size()) != wasCount) {
		updateGeometry();
	}
	updateScrollMax();
	update();
	if (!wasCount) {
		_empty = false;
	}
	_tooltipText = computeTooltipText();
	updateTooltipGeometry();
}

void List::updateScrollMax() {
	const auto &full = _st.full;
	const auto singleFull = full.photoLeft * 2 + full.photo;
	const auto widthFull = full.left + int(_data.items.size()) * singleFull;
	_scrollLeftMax = std::max(widthFull - width(), 0);
	_scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax);
	checkLoadMore();
	update();
}

rpl::producer<uint64> List::clicks() const {
	return _clicks.events();
}

rpl::producer<ShowMenuRequest> List::showMenuRequests() const {
	return _showMenuRequests.events();
}

rpl::producer<bool> List::toggleExpandedRequests() const {
	return _toggleExpandedRequests.events();
}

rpl::producer<> List::entered() const {
	return _entered.events();
}

rpl::producer<> List::loadMoreRequests() const {
	return _loadMoreRequests.events();
}

rpl::producer<not_null<QWheelEvent*>> List::verticalScrollEvents() const {
	return _verticalScrollEvents.events();
}

void List::requestExpanded(bool expanded) {
	if (_expanded != expanded) {
		_expanded = expanded;
		const auto from = _expanded ? 0. : 1.;
		const auto till = _expanded ? 2. : 0.;
		const auto duration = (_expanded ? 2 : 1) * st::slideWrapDuration;
		if (!isHidden() && _expanded) {
			toggleTooltip(false);
		}
		_expandedAnimation.start([=] {
			checkForFullState();
			update();
			_collapsedGeometryChanged.fire({});
			if (!isHidden() && !_expandedAnimation.animating()) {
				toggleTooltip(false);
			}
		}, from, till, duration, anim::sineInOut);
	}
	_toggleExpandedRequests.fire_copy(_expanded);
}

void List::enterEventHook(QEnterEvent *e) {
	_entered.fire({});
}

void List::resizeEvent(QResizeEvent *e) {
	updateScrollMax();
}

void List::updateExpanding() {
	updateExpanding(
		_lastExpandedHeight * _expandCatchUpAnimation.value(1.),
		_st.full.height);
}

void List::updateExpanding(int expandingHeight, int expandedHeight) {
	Expects(!expandingHeight || expandedHeight > 0);

	const auto ratio = !expandingHeight
		? 0.
		: (float64(expandingHeight) / expandedHeight);
	if (_lastRatio == ratio) {
		return;
	}
	const auto expanding = (ratio > _lastRatio);
	_lastRatio = ratio;
	const auto change = _expanded
		? (!expanding && ratio < kCollapseAfterRatio)
		: (expanding && ratio > kExpandAfterRatio);
	if (change) {
		requestExpanded(!_expanded);
	}
	updateTooltipGeometry();
}

List::Layout List::computeLayout() {
	return computeLayout(_expandedAnimation.value(_expanded ? 2. : 0.));
}

List::Layout List::computeLayout(float64 expanded) const {
	const auto segmentsSpinProgress = expanded / 2.;
	expanded = std::min(expanded, 1.);

	const auto &st = _st.small;
	const auto &full = _st.full;
	const auto expandedRatio = _lastRatio;
	const auto collapsedRatio = expandedRatio * kFrictionRatio;
	const auto ratio = expandedRatio * expanded
		+ collapsedRatio * (1. - expanded);
	const auto expandRatio = (ratio >= kCollapseAfterRatio)
		? 1.
		: (ratio <= kExpandAfterRatio * kFrictionRatio)
		? 0.
		: ((ratio - (kExpandAfterRatio * kFrictionRatio))
			/ (kCollapseAfterRatio - (kExpandAfterRatio * kFrictionRatio)));

	const auto lerp = [&](float64 a, float64 b) {
		return a + (b - a) * ratio;
	};
	const auto widthFull = width();
	const auto itemsCount = int(_data.items.size());
	const auto leftFullMin = full.left;
	const auto singleFullMin = full.photoLeft * 2 + full.photo;
	const auto totalFull = leftFullMin + singleFullMin * itemsCount;
	const auto skipSide = (totalFull < widthFull)
		? (widthFull - totalFull) / (itemsCount + 1)
		: 0;
	const auto skipBetween = (totalFull < widthFull && itemsCount > 1)
		? (widthFull - totalFull - 2 * skipSide) / (itemsCount - 1)
		: skipSide;
	const auto singleFull = singleFullMin + skipBetween;
	const auto smallSkip = (itemsCount > 1
		&& _data.items[0].element.skipSmall)
		? 1
		: 0;
	const auto smallCount = std::min(
		kSmallThumbsShown,
		itemsCount - smallSkip);
	const auto leftSmall = st.left - (smallSkip ? st.shift : 0);
	const auto leftFull = full.left - _scrollLeft + skipSide;
	const auto startIndexFull = std::max(-leftFull, 0) / singleFull;
	const auto cellLeftFull = leftFull + (startIndexFull * singleFull);
	const auto endIndexFull = std::min(
		(width() - leftFull + singleFull - 1) / singleFull,
		itemsCount);
	const auto startIndexSmall = std::min(startIndexFull, smallSkip);
	const auto endIndexSmall = smallSkip + smallCount;
	const auto cellLeftSmall = leftSmall + (startIndexSmall * st.shift);
	const auto thumbnailLeftFull = cellLeftFull + full.photoLeft;
	const auto thumbnailLeftSmall = cellLeftSmall + st.photoLeft;
	const auto thumbnailLeft = lerp(thumbnailLeftSmall, thumbnailLeftFull);
	const auto photoLeft = lerp(st.photoLeft, full.photoLeft);
	return Layout{
		.itemsCount = itemsCount,
		.geometryShift = QPointF(
			(_state == State::Changing
				? (lerp(_changingGeometryFrom.x(), _geometryFull.x()) - x())
				: 0.),
			(_state == State::Changing
				? (lerp(_changingGeometryFrom.y(), _geometryFull.y()) - y())
				: 0.)),
		.expandedRatio = expandedRatio,
		.expandRatio = expandRatio,
		.ratio = ratio,
		.segmentsSpinProgress = segmentsSpinProgress,
		.thumbnailLeft = thumbnailLeft,
		.photoLeft = photoLeft,
		.left = thumbnailLeft - photoLeft,
		.single = lerp(st.shift, singleFull),
		.smallSkip = smallSkip,
		.leftFull = leftFull,
		.leftSmall = leftSmall,
		.singleFull = singleFull,
		.singleSmall = st.shift,
		.startIndexSmall = startIndexSmall,
		.endIndexSmall = endIndexSmall,
		.startIndexFull = startIndexFull,
		.endIndexFull = endIndexFull,
	};
}

void List::paintEvent(QPaintEvent *e) {
	const auto &st = _st.small;
	const auto &full = _st.full;
	const auto layout = computeLayout();
	const auto ratio = layout.ratio;
	const auto expandRatio = layout.expandRatio;
	const auto lerp = [&](float64 a, float64 b) {
		return a + (b - a) * ratio;
	};
	const auto elerp = [&](float64 a, float64 b) {
		return a + (b - a) * expandRatio;
	};
	const auto line = elerp(st.lineTwice, full.lineTwice) / 2.;
	const auto photo = lerp(st.photo, full.photo);
	const auto layered = layout.single < (photo + 4 * line);
	auto p = QPainter(this);
	if (layered) {
		ensureLayer();
		auto q = QPainter(&_layer);
		paint(q, layout, photo, line, true);
		q.end();
		p.drawImage(0, 0, _layer);
	} else {
		paint(p, layout, photo, line, false);
	}
}

void List::ensureLayer() {
	const auto ratio = style::DevicePixelRatio();
	const auto layer = size() * ratio;
	if (_layer.size() != layer) {
		_layer = QImage(layer, QImage::Format_ARGB32_Premultiplied);
		_layer.setDevicePixelRatio(ratio);
	}
	_layer.fill(Qt::transparent);
}

void List::paint(
		QPainter &p,
		const Layout &layout,
		float64 photo,
		float64 line,
		bool layered) {
	const auto &st = _st.small;
	const auto &full = _st.full;
	const auto expandRatio = layout.expandRatio;
	const auto elerp = [&](float64 a, float64 b) {
		return a + (b - a) * expandRatio;
	};
	const auto lineRead = elerp(st.lineReadTwice, full.lineReadTwice) / 2.;
	const auto photoTopSmall = st.photoTop;
	const auto photoTop = photoTopSmall
		+ (full.photoTop - photoTopSmall) * layout.expandedRatio;
	const auto nameScale = _lastRatio;
	const auto nameTop = full.nameTop
		+ (photoTop + photo - full.photoTop - full.photo);
	const auto nameWidth = nameScale * AvailableNameWidth(_st);
	const auto nameHeight = nameScale * full.nameStyle.font->height;
	const auto nameLeft = layout.photoLeft + (photo - nameWidth) / 2.;
	const auto readUserpicOpacity = elerp(_st.readOpacity, 1.);
	const auto readUserpicAppearingOpacity = elerp(_st.readOpacity, 0.);
	if (_state == State::Changing) {
		p.translate(layout.geometryShift);
	}

	const auto drawSmall = (expandRatio < 1.);
	const auto drawFull = (expandRatio > 0.);
	auto hq = PainterHighQualityEnabler(p);

	const auto count = std::max(
		layout.endIndexFull - layout.startIndexFull,
		layout.endIndexSmall - layout.startIndexSmall);

	struct Single {
		float64 x = 0.;
		int indexSmall = 0;
		Item *itemSmall = nullptr;
		int indexFull = 0;
		Item *itemFull = nullptr;
		float64 photoTop = 0.;

		explicit operator bool() const {
			return itemSmall || itemFull;
		}
	};
	const auto lookup = [&](int index) {
		const auto indexSmall = layout.startIndexSmall + index;
		const auto indexFull = layout.startIndexFull + index;
		const auto ySmall = photoTopSmall
			+ ((photoTop - photoTopSmall)
				* (kSmallThumbsShown - indexSmall + layout.smallSkip) / 0.5);
		const auto y = elerp(ySmall, photoTop);

		const auto small = (drawSmall
			&& indexSmall < layout.endIndexSmall
			&& indexSmall >= layout.smallSkip)
			? &_data.items[indexSmall]
			: nullptr;
		const auto full = (drawFull && indexFull < layout.endIndexFull)
			? &_data.items[indexFull]
			: nullptr;
		const auto x = layout.left + layout.single * index;
		return Single{ x, indexSmall, small, indexFull, full, y };
	};
	const auto hasUnread = [&](const Single &single) {
		return (single.itemSmall && single.itemSmall->element.unreadCount)
			|| (single.itemFull && single.itemFull->element.unreadCount);
	};
	const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) {
		auto nextGradientPainted = false;
		auto skippedPainted = false;
		const auto first = layout.smallSkip - layout.startIndexSmall;
		for (auto i = count; i != first;) {
			--i;
			const auto next = (i > 0) ? lookup(i - 1) : Single();
			const auto gradientPainted = nextGradientPainted;
			nextGradientPainted = false;
			if (const auto current = lookup(i)) {
				if (i == first && next && !skippedPainted) {
					skippedPainted = true;
					paintGradient(next);
					paintOther(next);
				}
				if (!gradientPainted) {
					paintGradient(current);
				}
				if (i > first && hasUnread(current) && next) {
					if (current.itemSmall || !next.itemSmall) {
						if (i - 1 == first
							&& first > 0
							&& !skippedPainted) {
							if (const auto skipped = lookup(i - 2)) {
								skippedPainted = true;
								paintGradient(skipped);
								paintOther(skipped);
							}
						}
						nextGradientPainted = true;
						paintGradient(next);
					}
				}
				paintOther(current);
			}
		}
	};
	auto gradient = Ui::UnreadStoryOutlineGradient();
	enumerate([&](Single single) {
		// Name.
		if (const auto full = single.itemFull) {
			validateName(full);
			if (expandRatio > 0.) {
				p.setOpacity(expandRatio);
				p.drawImage(QRectF(
					single.x + nameLeft,
					nameTop,
					nameWidth,
					nameHeight
				), full->nameCache);
			}
		}

		// Unread gradient.
		const auto x = single.x;
		const auto userpic = QRectF(
			x + layout.photoLeft,
			single.photoTop,
			photo,
			photo);
		const auto small = single.itemSmall;
		const auto itemFull = single.itemFull;
		const auto smallUnread = (small && small->element.unreadCount);
		const auto fullUnreadCount = itemFull
			? itemFull->element.unreadCount
			: 0;
		const auto unreadOpacity = (smallUnread && fullUnreadCount)
			? 1.
			: smallUnread
			? (1. - expandRatio)
			: fullUnreadCount
			? expandRatio
			: 0.;
		if (unreadOpacity > 0.) {
			p.setOpacity(unreadOpacity);
			const auto outerAdd = 1.5 * line;
			const auto outer = userpic.marginsAdded(
				{ outerAdd, outerAdd, outerAdd, outerAdd });
			gradient.setStart(userpic.topRight());
			gradient.setFinalStop(userpic.bottomLeft());
			if (!fullUnreadCount) {
				p.setPen(QPen(gradient, line));
				p.setBrush(Qt::NoBrush);
				p.drawEllipse(outer);
			} else {
				validateSegments(itemFull, gradient, line, true);
				Ui::PaintOutlineSegments(
					p,
					outer,
					itemFull->segments,
					layout.segmentsSpinProgress);
			}
		}
		p.setOpacity(1.);
	}, [&](Single single) {
		Expects(single.itemSmall || single.itemFull);

		const auto x = single.x;
		const auto userpic = QRectF(
			x + layout.photoLeft,
			single.photoTop,
			photo,
			photo);
		const auto small = single.itemSmall;
		const auto itemFull = single.itemFull;
		const auto smallUnread = small && small->element.unreadCount;
		const auto fullUnreadCount = itemFull
			? itemFull->element.unreadCount
			: 0;
		const auto fullCount = itemFull ? itemFull->element.count : 0;

		// White circle with possible read gray line.
		const auto hasReadLine = (itemFull && fullUnreadCount < fullCount);
		p.setOpacity((small && itemFull)
			? 1.
			: small
			? (1. - expandRatio)
			: expandRatio);
		const auto add = line + (hasReadLine ? (lineRead / 2.) : 0.);
		const auto rect = userpic.marginsAdded({ add, add, add, add });
		if (layered) {
			p.setCompositionMode(QPainter::CompositionMode_Source);
			p.setPen(Qt::NoPen);
			p.setBrush(st::transparent);
			p.drawEllipse(rect);
			p.setCompositionMode(QPainter::CompositionMode_SourceOver);
		}
		if (hasReadLine) {
			if (small && !small->element.unreadCount) {
				p.setOpacity(expandRatio);
			}
			validateSegments(
				itemFull,
				st::dialogsUnreadBgMuted->b,
				lineRead,
				false);
			Ui::PaintOutlineSegments(
				p,
				rect,
				itemFull->segments,
				layout.segmentsSpinProgress);
		}

		// Userpic.
		if (itemFull == small) {
			p.setOpacity(smallUnread ? 1. : readUserpicOpacity);
			validateThumbnail(itemFull);
			const auto size = full.photo;
			p.drawImage(userpic, itemFull->element.thumbnail->image(size));
		} else {
			if (small) {
				p.setOpacity(smallUnread
					? (itemFull ? 1. : (1. - expandRatio))
					: (itemFull
						? _st.readOpacity
						: readUserpicAppearingOpacity));
				validateThumbnail(small);
				const auto size = (expandRatio > 0.)
					? full.photo
					: st.photo;
				p.drawImage(userpic, small->element.thumbnail->image(size));
			}
			if (itemFull) {
				p.setOpacity(expandRatio);
				validateThumbnail(itemFull);
				const auto size = full.photo;
				p.drawImage(
					userpic,
					itemFull->element.thumbnail->image(size));
			}
		}
		p.setOpacity(1.);
	});
}

void List::validateThumbnail(not_null<Item*> item) {
	if (!item->subscribed) {
		item->subscribed = true;
		//const auto id = item.element.id;
		item->element.thumbnail->subscribeToUpdates([=] {
			update();
		});
	}
}

void List::validateSegments(
		not_null<Item*> item,
		const QBrush &brush,
		float64 line,
		bool forUnread) {
	const auto count = item->element.count;
	const auto unread = item->element.unreadCount;
	if (int(item->segments.size()) != count) {
		item->segments.resize(count);
	}
	auto i = 0;
	if (forUnread) {
		for (; i != count - unread; ++i) {
			item->segments[i].width = 0.;
		}
		for (; i != count; ++i) {
			item->segments[i].brush = brush;
			item->segments[i].width = line;
		}
	} else {
		for (; i != count - unread; ++i) {
			item->segments[i].brush = brush;
			item->segments[i].width = line;
		}
		for (; i != count; ++i) {
			item->segments[i].width = 0.;
		}
	}
}

void List::validateName(not_null<Item*> item) {
	const auto &element = item->element;
	const auto &color = (element.unreadCount || element.skipSmall)
		? st::dialogsNameFg
		: st::windowSubTextFg;
	if (!item->nameCache.isNull() && item->nameCacheColor == color->c) {
		return;
	}
	const auto &full = _st.full;
	const auto &font = full.nameStyle.font;
	const auto available = AvailableNameWidth(_st);
	const auto my = element.skipSmall
		? tr::lng_stories_my_name(tr::now)
		: QString();
	const auto use = (my.isEmpty()
		|| full.nameStyle.font->width(my) > available)
		? element.name
		: my;
	const auto text = Ui::Text::String(full.nameStyle, use);
	const auto ratio = style::DevicePixelRatio();
	item->nameCacheColor = color->c;
	item->nameCache = QImage(
		QSize(available, font->height) * ratio,
		QImage::Format_ARGB32_Premultiplied);
	item->nameCache.setDevicePixelRatio(ratio);
	item->nameCache.fill(Qt::transparent);
	auto p = Painter(&item->nameCache);
	p.setPen(color);
	text.drawElided(p, 0, 0, available, 1, style::al_top);
}

void List::wheelEvent(QWheelEvent *e) {
	const auto phase = e->phase();
	const auto fullDelta = e->pixelDelta().isNull()
		? e->angleDelta()
		: e->pixelDelta();
	if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {
		_scrollingLock = Qt::Orientation();
		if (fullDelta.isNull()) {
			return;
		}
	}
	const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());
	if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {
		_scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;
	}
	if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {
		_verticalScrollEvents.fire(e);
		return;
	} else if (_state == State::Small) {
		e->ignore();
		return;
	}
	const auto delta = vertical
		? fullDelta.y()
		: ((style::RightToLeft() ? -1 : 1) * fullDelta.x());

	const auto now = _scrollLeft;
	const auto used = now - delta;
	const auto next = std::clamp(used, 0, _scrollLeftMax);
	if (next != now) {
		requestExpanded(true);
		_scrollLeft = next;
		updateSelected();
		checkLoadMore();
		update();
	}
	e->accept();
}

void List::mousePressEvent(QMouseEvent *e) {
	if (e->button() != Qt::LeftButton) {
		return;
	} else if (_state == State::Small) {
		requestExpanded(true);
		if (const auto onstack = _tooltipHide) {
			onstack();
		}
		return;
	} else if (_state != State::Full) {
		return;
	}
	_lastMousePosition = e->globalPos();
	updateSelected();

	_mouseDownPosition = _lastMousePosition;
	_pressed = _selected;
}

void List::mouseMoveEvent(QMouseEvent *e) {
	_lastMousePosition = e->globalPos();
	updateSelected();

	if (!_dragging && _mouseDownPosition && _state == State::Full) {
		if ((_lastMousePosition - *_mouseDownPosition).manhattanLength()
			>= QApplication::startDragDistance()) {
			_dragging = true;
			_startDraggingLeft = _scrollLeft;
		}
	}
	checkDragging();
}

void List::checkDragging() {
	if (_dragging) {
		const auto sign = (style::RightToLeft() ? -1 : 1);
		const auto newLeft = std::clamp(
			(sign * (_mouseDownPosition->x() - _lastMousePosition.x())
				+ _startDraggingLeft),
			0,
			_scrollLeftMax);
		if (newLeft != _scrollLeft) {
			_scrollLeft = newLeft;
			checkLoadMore();
			update();
		}
	}
}

void List::checkLoadMore() {
	if (_scrollLeftMax - _scrollLeft < width() * kPreloadPages) {
		_loadMoreRequests.fire({});
	}
}

void List::mouseReleaseEvent(QMouseEvent *e) {
	_lastMousePosition = e->globalPos();
	const auto guard = gsl::finally([&] {
		_mouseDownPosition = std::nullopt;
	});

	const auto pressed = std::exchange(_pressed, -1);
	if (finishDragging()) {
		return;
	}
	updateSelected();
	if (_selected == pressed) {
		if (!_expanded) {
			requestExpanded(true);
		} else if (_selected < _data.items.size()) {
			_clicks.fire_copy(_data.items[_selected].element.id);
		}
	}
}

void List::setExpandedHeight(int height, bool momentum) {
	height = std::clamp(height, 0, _st.full.height);
	if (_lastExpandedHeight == height) {
		return;
	} else if (momentum && _expandIgnored) {
		return;
	} else if (momentum && height > 0 && !_lastExpandedHeight) {
		_expandIgnored = true;
		return;
	} else if (!momentum && _expandIgnored && height > 0) {
		_expandIgnored = false;
		_expandCatchUpAnimation.start([=] {
			updateExpanding();
			update();
			checkForFullState();
		}, 0., 1., kExpandCatchUpDuration);
	} else if (!height && _expandCatchUpAnimation.animating()) {
		_expandCatchUpAnimation.stop();
	}
	_lastExpandedHeight = height;
	updateExpanding();
	if (!checkForFullState()) {
		setState(!height ? State::Small : State::Changing);
	}
	update();
}

bool List::checkForFullState() {
	if (_expandCatchUpAnimation.animating()
		|| _expandedAnimation.animating()
		|| _lastExpandedHeight < _st.full.height) {
		return false;
	}
	setState(State::Full);
	return true;
}

void List::setLayoutConstraints(
		QPoint positionSmall,
		style::align alignSmall,
		QRect geometryFull) {
	if (_positionSmall == positionSmall
		&& _alignSmall == alignSmall
		&& _geometryFull == geometryFull) {
		return;
	}
	_positionSmall = positionSmall;
	_alignSmall = alignSmall;
	_geometryFull = geometryFull;
	_lastCollapsedGeometry = {};
	updateGeometry();
	update();
}

TextWithEntities List::computeTooltipText() const {
	const auto &list = _data.items;
	if (list.empty()) {
		return {};
	} else if (list.size() == 1 && list.front().element.skipSmall) {
		return { tr::lng_stories_click_to_view_mine(tr::now) };
	}
	auto names = QStringList();
	for (const auto &item : list) {
		if (item.element.skipSmall) {
			continue;
		}
		names.append(item.element.name);
		if (names.size() >= kMaxTooltipNames) {
			break;
		}
	}
	auto sequence = Ui::Text::Bold(names.front());
	if (names.size() > 1) {
		for (auto i = 1; i + 1 != names.size(); ++i) {
			sequence = tr::lng_stories_click_to_view_and_one(
				tr::now,
				lt_accumulated,
				sequence,
				lt_user,
				Ui::Text::Bold(names[i]),
				Ui::Text::WithEntities);
		}
		sequence = tr::lng_stories_click_to_view_and_last(
			tr::now,
			lt_accumulated,
			sequence,
			lt_user,
			Ui::Text::Bold(names.back()),
			Ui::Text::WithEntities);
	}
	return tr::lng_stories_click_to_view(
		tr::now,
		lt_users,
		sequence,
		Ui::Text::WithEntities);
}

void List::setShowTooltip(
		not_null<QWidget*> tooltipParent,
		rpl::producer<bool> shown,
		Fn<void()> hide) {
	_tooltip = nullptr;
	_tooltipHide = std::move(hide);
	_tooltipNotHidden = std::move(shown);
	_tooltipText = computeTooltipText();
	const auto notEmpty = [](const TextWithEntities &text) {
		return !text.empty();
	};
	_tooltip = std::make_unique<Ui::ImportantTooltip>(
		tooltipParent,
		MakeTooltipContent(
			tooltipParent,
			_tooltipText.value() | rpl::filter(notEmpty),
			_tooltipHide),
		st::dialogsStoriesTooltip);
	const auto tooltip = _tooltip.get();
	const auto weak = QPointer<QWidget>(tooltip);
	tooltip->toggleFast(false);
	updateTooltipGeometry();

	const auto handle = tooltipParent->window()->windowHandle();
	auto windowActive = rpl::single(
		handle->isActive()
	) | rpl::then(base::qt_signal_producer(
		handle,
		&QWindow::activeChanged
	) | rpl::map([=] {
		return handle->isActive();
	})) | rpl::distinct_until_changed();

	{
		const auto recompute = [=] {
			updateTooltipGeometry();
			tooltip->raise();
		};
		using namespace base;
		using Event = not_null<QEvent*>;
		install_event_filter(tooltip, tooltipParent, [=](Event e) {
			if (e->type() == QEvent::ChildAdded) {
				recompute();
			}
			return EventFilterResult::Continue;
		});
	}

	rpl::combine(
		_tooltipNotHidden.value(),
		_tooltipText.value() | rpl::map(
			notEmpty
		) | rpl::distinct_until_changed(),
		std::move(windowActive)
	) | rpl::start_with_next([=](bool, bool, bool active) {
		_tooltipWindowActive = active;
		if (!isHidden()) {
			toggleTooltip(false);
		}
	}, tooltip->lifetime());

	shownValue(
	) | rpl::skip(1) | rpl::start_with_next([=](bool shown) {
		toggleTooltip(true);
	}, tooltip->lifetime());
}

void List::raiseTooltip() {
	if (_tooltip) {
		_tooltip->raise();
	}
}

void List::toggleTooltip(bool fast) {
	const auto shown = !_expanded
		&& !_expandedAnimation.animating()
		&& !isHidden()
		&& _tooltipNotHidden.current()
		&& !_tooltipText.current().empty()
		&& window()->windowHandle()->isActive();
	if (_tooltip) {
		if (fast) {
			_tooltip->toggleFast(shown);
		} else {
			_tooltip->toggleAnimated(shown);
		}
	}
	if (shown) {
		updateTooltipGeometry();
	}
}

void List::updateTooltipGeometry() {
	if (!_tooltip || _expanded || _expandedAnimation.animating()) {
		return;
	}
	const auto collapsed = collapsedGeometryCurrent();
	const auto geometry = Ui::MapFrom(
		_tooltip->parentWidget(),
		parentWidget(),
		QRect(
			collapsed.geometry.x(),
			collapsed.geometry.y(),
			int(std::ceil(collapsed.singleWidth)),
			collapsed.geometry.height()));
	const auto weak = QPointer<QWidget>(_tooltip.get());
	const auto countPosition = [=](QSize size) {
		const auto left = geometry.x()
			+ (geometry.width() - size.width()) / 2;
		const auto right = _tooltip->parentWidget()->width()
			- st::dialogsStoriesTooltip.padding.right();
		return QPoint(
			std::max(std::min(left, right - size.width()), 0),
			geometry.y() + geometry.height());
	};
	_tooltip->pointAt(geometry, RectPart::Bottom, countPosition);
}

List::CollapsedGeometry List::collapsedGeometryCurrent() const {
	const auto expanded = _expandedAnimation.value(_expanded ? 2. : 0.);
	if (expanded >= 1.) {
		const auto single = 2 * _st.full.photoLeft + _st.full.photo;
		return { QRect(), 1., float64(single) };
	} else if (_lastCollapsedRatio == _lastRatio
		&& _lastCollapsedGeometry.expanded == expanded
		&& !_lastCollapsedGeometry.geometry.isEmpty()) {
		return _lastCollapsedGeometry;
	}
	const auto layout = computeLayout(0.);
	const auto small = countSmallGeometry();
	const auto index = layout.smallSkip - layout.startIndexSmall;
	const auto shift = x() + layout.geometryShift.x();
	const auto left = int(base::SafeRound(
		shift + layout.left + layout.single * index));
	const auto width = small.x() + small.width() - left;
	const auto photoTopSmall = _st.small.photoTop;
	const auto photoTop = photoTopSmall
		+ (_st.full.photoTop - photoTopSmall) * layout.expandedRatio;
	const auto ySmall = photoTopSmall
		+ ((photoTop - photoTopSmall) * kSmallThumbsShown / 0.5);
	const auto photo = _st.small.photo
		+ (_st.full.photo - _st.small.photo) * layout.ratio;
	const auto top = y() + layout.geometryShift.y();
	_lastCollapsedRatio = _lastRatio;
	_lastCollapsedGeometry = {
		QRect(left, top, width, ySmall + photo + _st.full.photoTop),
		expanded,
		layout.photoLeft * 2 + photo,
	};
	return _lastCollapsedGeometry;
}

rpl::producer<> List::collapsedGeometryChanged() const {
	return _collapsedGeometryChanged.events();
}

void List::updateGeometry() {
	switch (_state) {
	case State::Small: setGeometry(countSmallGeometry()); break;
	case State::Changing: {
		_changingGeometryFrom = countSmallGeometry();
		setGeometry(_geometryFull.united(_changingGeometryFrom));
	} break;
	case State::Full: setGeometry(_geometryFull);
	}
	updateTooltipGeometry();
	update();
}

QRect List::countSmallGeometry() const {
	const auto &st = _st.small;
	const auto layout = computeLayout(0.);
	const auto count = layout.endIndexSmall
		- std::max(layout.startIndexSmall, layout.smallSkip);
	const auto width = st.left
		+ st.photoLeft
		+ st.photo + (count - 1) * st.shift
		+ st.photoLeft
		+ st.left;
	const auto left = ((_alignSmall & Qt::AlignRight) == Qt::AlignRight)
		? (_positionSmall.x() - width)
		: ((_alignSmall & Qt::AlignCenter) == Qt::AlignCenter)
		? (_positionSmall.x() - (width / 2))
		: _positionSmall.x();
	return QRect(
		left,
		_positionSmall.y(),
		width,
		st.photoTop + st.photo + st.photoTop);
}

void List::setState(State state) {
	if (_state == state) {
		return;
	}
	_state = state;
	updateGeometry();
}

void List::contextMenuEvent(QContextMenuEvent *e) {
	_menu = nullptr;

	if (e->reason() == QContextMenuEvent::Mouse) {
		_lastMousePosition = e->globalPos();
		updateSelected();
	}
	if (_selected < 0 || _data.empty() || !_expanded) {
		return;
	}
	_menu = base::make_unique_q<Ui::PopupMenu>(
		this,
		st::popupMenuWithIcons);
	_showMenuRequests.fire({
		_data.items[_selected].element.id,
		Ui::Menu::CreateAddActionCallback(_menu),
	});
	if (_menu->empty()) {
		_menu = nullptr;
		return;
	}
	const auto updateAfterMenuDestroyed = [=] {
		const auto globalPosition = QCursor::pos();
		if (rect().contains(mapFromGlobal(globalPosition))) {
			_lastMousePosition = globalPosition;
			updateSelected();
		}
	};
	QObject::connect(
		_menu.get(),
		&QObject::destroyed,
		crl::guard(&_menuGuard, updateAfterMenuDestroyed));
	_menu->popup(e->globalPos());
	e->accept();
}

bool List::finishDragging() {
	if (!_dragging) {
		return false;
	}
	checkDragging();
	_dragging = false;
	updateSelected();
	return true;
}

void List::updateSelected() {
	if (_pressed >= 0) {
		return;
	}
	const auto &st = _st.small;
	const auto p = mapFromGlobal(_lastMousePosition);
	const auto layout = computeLayout();
	const auto firstRightFull = layout.leftFull
		+ (layout.startIndexFull + 1) * layout.singleFull;
	const auto secondLeftFull = firstRightFull;
	const auto firstRightSmall = layout.leftSmall
		+ st.photoLeft
		+ st.photo;
	const auto secondLeftSmall = layout.smallSkip
		? (layout.leftSmall + st.photoLeft + st.shift)
		: firstRightSmall;
	const auto lastRightAddFull = 0;
	const auto lastRightAddSmall = st.photoLeft;
	const auto lerp = [&](float64 a, float64 b) {
		return a + (b - a) * layout.ratio;
	};
	const auto firstRight = lerp(firstRightSmall, firstRightFull);
	const auto secondLeft = lerp(secondLeftSmall, secondLeftFull);
	const auto lastRightAdd = lerp(lastRightAddSmall, lastRightAddFull);
	const auto activateFull = (layout.ratio >= 0.5);
	const auto startIndex = activateFull
		? layout.startIndexFull
		: layout.startIndexSmall;
	const auto endIndex = activateFull
		? layout.endIndexFull
		: layout.endIndexSmall;
	const auto x = p.x();
	const auto infiniteIndex = (x < secondLeft)
		? 0
		: int(
			std::floor((std::max(x - firstRight, 0.)) / layout.single) + 1);
	const auto index = (endIndex == startIndex)
		? -1
		: (infiniteIndex == endIndex - startIndex
			&& x < firstRight
				+ (endIndex - startIndex - 1) * layout.single
				+ lastRightAdd)
		? (infiniteIndex - 1) // Last small part should still be clickable.
		: (startIndex + infiniteIndex >= endIndex)
		? (_st.fullClickable ? (endIndex - 1) : -1)
		: infiniteIndex;
	const auto selected = (index < 0
		|| startIndex + index >= layout.itemsCount)
		? -1
		: (startIndex + index);
	if (_selected != selected) {
		const auto over = (selected >= 0);
		if (over != (_selected >= 0)) {
			setCursor(over ? style::cur_pointer : style::cur_default);
		}
		_selected = selected;
	}
}

} // namespace Dialogs::Stories
