/*
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 "media/stories/media_stories_controller.h"

#include "base/power_save_blocker.h"
#include "base/qt_signal_producer.h"
#include "base/unixtime.h"
#include "boxes/peers/prepare_short_info_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/update_checker.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_user.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "media/stories/media_stories_caption_full_view.h"
#include "media/stories/media_stories_delegate.h"
#include "media/stories/media_stories_header.h"
#include "media/stories/media_stories_sibling.h"
#include "media/stories/media_stories_slider.h"
#include "media/stories/media_stories_reactions.h"
#include "media/stories/media_stories_recent_views.h"
#include "media/stories/media_stories_reply.h"
#include "media/stories/media_stories_share.h"
#include "media/stories/media_stories_stealth.h"
#include "media/stories/media_stories_view.h"
#include "media/audio/media_audio.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/report_box.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/round_rect.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h" // defaultReportBox
#include "styles/style_media_view.h"
#include "styles/style_boxes.h" // UserpicButton

#include <QtGui/QWindow>

namespace Media::Stories {
namespace {

constexpr auto kPhotoProgressInterval = crl::time(100);
constexpr auto kPhotoDuration = 5 * crl::time(1000);
constexpr auto kFullContentFade = 0.6;
constexpr auto kSiblingMultiplierDefault = 0.448;
constexpr auto kSiblingMultiplierMax = 0.72;
constexpr auto kSiblingOutsidePart = 0.24;
constexpr auto kSiblingUserpicSize = 0.3;
constexpr auto kInnerHeightMultiplier = 1.6;
constexpr auto kPreloadUsersCount = 3;
constexpr auto kPreloadStoriesCount = 5;
constexpr auto kPreloadNextMediaCount = 3;
constexpr auto kPreloadPreviousMediaCount = 1;
constexpr auto kMarkAsReadAfterSeconds = 0.2;
constexpr auto kMarkAsReadAfterProgress = 0.;

struct SameDayRange {
	int from = 0;
	int till = 0;
};
[[nodiscard]] SameDayRange ComputeSameDayRange(
		not_null<Data::Story*> story,
		const Data::StoriesIds &ids,
		int index) {
	Expects(index >= 0 && index < ids.list.size());

	auto result = SameDayRange{ .from = index, .till = index };
	const auto peerId = story->peer()->id;
	const auto stories = &story->owner().stories();
	const auto now = base::unixtime::parse(story->date());
	const auto b = begin(ids.list);
	for (auto i = b + index; i != b;) {
		if (const auto maybeStory = stories->lookup({ peerId, *--i })) {
			const auto day = base::unixtime::parse((*maybeStory)->date());
			if (day.date() != now.date()) {
				break;
			}
		}
		--result.from;
	}
	for (auto i = b + index + 1, e = end(ids.list); i != e; ++i) {
		if (const auto maybeStory = stories->lookup({ peerId, *i })) {
			const auto day = base::unixtime::parse((*maybeStory)->date());
			if (day.date() != now.date()) {
				break;
			}
		}
		++result.till;
	}
	return result;
}

[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {
	if (std::abs(angle) < 1.) {
		return point;
	}
	const auto alpha = angle / 180. * M_PI;
	const auto acos = cos(alpha);
	const auto asin = sin(alpha);
	point -= origin;
	return origin + QPoint(
		int(base::SafeRound(acos * point.x() - asin * point.y())),
		int(base::SafeRound(asin * point.x() + acos * point.y())));
}

} // namespace

class Controller::PhotoPlayback final {
public:
	explicit PhotoPlayback(not_null<Controller*> controller);

	[[nodiscard]] bool paused() const;
	void togglePaused(bool paused);

private:
	void callback();

	const not_null<Controller*> _controller;

	base::Timer _timer;
	crl::time _started = 0;
	crl::time _paused = 0;

};

class Controller::Unsupported final {
public:
	Unsupported(not_null<Controller*> controller, not_null<UserData*> user);

private:
	void setup(not_null<UserData*> user);

	const not_null<Controller*> _controller;
	std::unique_ptr<Ui::RpWidget> _bg;
	std::unique_ptr<Ui::FlatLabel> _text;
	std::unique_ptr<Ui::RoundButton> _button;
	Ui::RoundRect _bgRound;

};

Controller::PhotoPlayback::PhotoPlayback(not_null<Controller*> controller)
: _controller(controller)
, _timer([=] { callback(); })
, _started(crl::now())
, _paused(_started) {
}

bool Controller::PhotoPlayback::paused() const {
	return _paused != 0;
}

void Controller::PhotoPlayback::togglePaused(bool paused) {
	if (!_paused == !paused) {
		return;
	} else if (paused) {
		const auto now = crl::now();
		if (now - _started >= kPhotoDuration) {
			return;
		}
		_paused = now;
		_timer.cancel();
	} else {
		_started += crl::now() - _paused;
		_paused = 0;
		_timer.callEach(kPhotoProgressInterval);
	}
	callback();
}

void Controller::PhotoPlayback::callback() {
	const auto now = crl::now();
	const auto elapsed = now - _started;
	const auto finished = (now - _started >= kPhotoDuration);
	if (finished) {
		_timer.cancel();
	}
	using State = Player::State;
	const auto state = finished
		? State::StoppedAtEnd
		: _paused
		? State::Paused
		: State::Playing;
	_controller->updatePhotoPlayback({
		.state = state,
		.position = elapsed,
		.receivedTill = kPhotoDuration,
		.length = kPhotoDuration,
		.frequency = 1000,
	});
}

Controller::Unsupported::Unsupported(
	not_null<Controller*> controller,
	not_null<UserData*> user)
: _controller(controller)
, _bgRound(st::storiesRadius, st::storiesComposeBg) {
	setup(user);
}

void Controller::Unsupported::setup(not_null<UserData*> user) {
	const auto wrap = _controller->wrap();

	_bg = std::make_unique<Ui::RpWidget>(wrap);
	_bg->show();
	_bg->paintRequest() | rpl::start_with_next([=] {
		auto p = QPainter(_bg.get());
		_bgRound.paint(p, _bg->rect());
	}, _bg->lifetime());

	_controller->layoutValue(
	) | rpl::start_with_next([=](const Layout &layout) {
		_bg->setGeometry(layout.content);
	}, _bg->lifetime());

	_text = std::make_unique<Ui::FlatLabel>(
		wrap,
		tr::lng_stories_unsupported(),
		st::storiesUnsupportedLabel);
	_text->show();

	_button = std::make_unique<Ui::RoundButton>(
		wrap,
		tr::lng_update_telegram(),
		st::storiesUnsupportedUpdate);
	_button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
	_button->show();

	rpl::combine(
		_controller->layoutValue(),
		_text->sizeValue(),
		_button->sizeValue()
	) | rpl::start_with_next([=](
			const Layout &layout,
			QSize text,
			QSize button) {
		const auto wrap = layout.content;
		const auto totalHeight = st::storiesUnsupportedTop
			+ text.height()
			+ st::storiesUnsupportedSkip
			+ button.height();
		const auto top = (wrap.height() - totalHeight) / 2;
		_text->move(
			wrap.x() + (wrap.width() - text.width()) / 2,
			wrap.y() + top + st::storiesUnsupportedTop);
		_button->move(
			wrap.x() + (wrap.width() - button.width()) / 2,
			wrap.y() + top + totalHeight - button.height());
	}, _button->lifetime());

	_button->setClickedCallback([=] {
		Core::UpdateApplication();
	});
}

Controller::Controller(not_null<Delegate*> delegate)
: _delegate(delegate)
, _wrap(_delegate->storiesWrap())
, _header(std::make_unique<Header>(this))
, _slider(std::make_unique<Slider>(this))
, _replyArea(std::make_unique<ReplyArea>(this))
, _reactions(std::make_unique<Reactions>(this))
, _recentViews(std::make_unique<RecentViews>(this)) {
	initLayout();

	using namespace rpl::mappers;

	rpl::combine(
		_replyArea->activeValue(),
		_reactions->activeValue(),
		_1 || _2
	) | rpl::distinct_until_changed(
	) | rpl::start_with_next([=](bool active) {
		_replyActive = active;
		updateContentFaded();
	}, _lifetime);

	_reactions->setReplyFieldState(
		_replyArea->focusedValue(),
		_replyArea->hasSendTextValue());
	if (const auto like = _replyArea->likeAnimationTarget()) {
		_reactions->attachToReactionButton(like);
	}

	_reactions->chosen(
	) | rpl::start_with_next([=](Reactions::Chosen chosen) {
		reactionChosen(chosen.mode, chosen.reaction);
	}, _lifetime);

	_delegate->storiesLayerShown(
	) | rpl::start_with_next([=](bool shown) {
		if (_layerShown != shown) {
			_layerShown = shown;
			updatePlayingAllowed();
		}
	}, _lifetime);

	_header->tooltipShownValue(
	) | rpl::start_with_next([=](bool shown) {
		if (_tooltipShown != shown) {
			_tooltipShown = shown;
			updatePlayingAllowed();
		}
	}, _lifetime);

	const auto window = _wrap->window()->windowHandle();
	Assert(window != nullptr);
	base::qt_signal_producer(
		window,
		&QWindow::activeChanged
	) | rpl::start_with_next([=] {
		_windowActive = window->isActive();
		updatePlayingAllowed();
	}, _lifetime);
	_windowActive = window->isActive();

	_contentFadeAnimation.stop();
}

Controller::~Controller() {
	changeShown(nullptr);
}

void Controller::updateContentFaded() {
	const auto faded = _replyActive
		|| (_captionFullView && !_captionFullView->closing());
	if (_contentFaded == faded) {
		return;
	}
	_contentFaded = faded;
	_contentFadeAnimation.start(
		[=] { _delegate->storiesRepaint(); },
		_contentFaded ? 0. : 1.,
		_contentFaded ? 1. : 0.,
		st::fadeWrapDuration);
	updatePlayingAllowed();
}

void Controller::initLayout() {
	const auto headerHeight = st::storiesHeaderMargin.top()
		+ st::storiesHeaderPhoto.photoSize
		+ st::storiesHeaderMargin.bottom();
	const auto sliderHeight = st::storiesSliderMargin.top()
		+ st::storiesSliderWidth
		+ st::storiesSliderMargin.bottom();
	const auto outsideHeaderHeight = headerHeight
		+ sliderHeight
		+ st::storiesSliderOutsideSkip;
	const auto fieldMinHeight = st::storiesFieldMargin.top()
		+ st::storiesAttach.height
		+ st::storiesFieldMargin.bottom();
	const auto minHeightForOutsideHeader = st::storiesFieldMargin.bottom()
		+ outsideHeaderHeight
		+ st::storiesMaxSize.height()
		+ fieldMinHeight;

	_layout = _wrap->sizeValue(
	) | rpl::map([=](QSize size) {
		const auto topNotchSkip = _delegate->storiesTopNotchSkip();

		size = QSize(
			std::max(size.width(), st::mediaviewMinWidth),
			std::max(size.height(), st::mediaviewMinHeight));

		auto layout = Layout();
		layout.headerLayout = (size.height() >= minHeightForOutsideHeader)
			? HeaderLayout::Outside
			: HeaderLayout::Normal;

		const auto topSkip = topNotchSkip
			+ st::storiesFieldMargin.bottom()
			+ (layout.headerLayout == HeaderLayout::Outside
				? outsideHeaderHeight
				: 0);
		const auto bottomSkip = fieldMinHeight;
		const auto maxWidth = size.width() - 2 * st::storiesSideSkip;
		const auto availableHeight = size.height() - topSkip - bottomSkip;
		const auto maxContentHeight = std::min(
			availableHeight,
			st::storiesMaxSize.height());
		const auto nowWidth = maxContentHeight * st::storiesMaxSize.width()
			/ st::storiesMaxSize.height();
		const auto contentWidth = std::min(nowWidth, maxWidth);
		const auto contentHeight = (contentWidth < nowWidth)
			? (contentWidth * st::storiesMaxSize.height()
				/ st::storiesMaxSize.width())
			: maxContentHeight;
		const auto addedTopSkip = (availableHeight - contentHeight) / 2;
		layout.content = QRect(
			(size.width() - contentWidth) / 2,
			addedTopSkip + topSkip,
			contentWidth,
			contentHeight);

		const auto reactionsWidth = st::storiesReactionsWidth;
		layout.reactions = QRect(
			(size.width() - reactionsWidth) / 2,
			layout.content.y(),
			reactionsWidth,
			contentHeight);

		if (layout.headerLayout == HeaderLayout::Outside) {
			layout.header = QRect(
				layout.content.topLeft() - QPoint(0, outsideHeaderHeight),
				QSize(contentWidth, outsideHeaderHeight));
			layout.slider = QRect(
				layout.header.topLeft() + QPoint(0, headerHeight),
				QSize(contentWidth, sliderHeight));
		} else {
			layout.slider = QRect(
				layout.content.topLeft(),
				QSize(contentWidth, sliderHeight));
			layout.header = QRect(
				layout.slider.topLeft() + QPoint(0, sliderHeight),
				QSize(contentWidth, headerHeight));
		}
		layout.controlsWidth = std::max(
			layout.content.width(),
			st::storiesControlsMinWidth);
		layout.controlsBottomPosition = QPoint(
			(size.width() - layout.controlsWidth) / 2,
			(layout.content.y()
				+ layout.content.height()
				+ fieldMinHeight
				- st::storiesFieldMargin.bottom()));
		layout.views = QRect(
			layout.controlsBottomPosition - QPoint(0, fieldMinHeight),
			QSize(layout.controlsWidth, fieldMinHeight));
		layout.autocompleteRect = QRect(
			layout.controlsBottomPosition.x(),
			0,
			layout.controlsWidth,
			layout.controlsBottomPosition.y());

		const auto sidesAvailable = size.width() - layout.content.width();
		const auto widthForSiblings = sidesAvailable
			- 2 * st::storiesFieldMargin.bottom();
		const auto siblingWidthMax = widthForSiblings
			/ (2 * (1. - kSiblingOutsidePart));
		const auto siblingMultiplierMax = std::max(
			kSiblingMultiplierDefault,
			st::storiesSiblingWidthMin / float64(layout.content.width()));
		const auto siblingMultiplier = std::min({
			siblingMultiplierMax,
			kSiblingMultiplierMax,
			siblingWidthMax / layout.content.width(),
		});
		const auto siblingSize = layout.content.size() * siblingMultiplier;
		const auto siblingTop = (size.height() - siblingSize.height()) / 2;
		const auto outsideMax = int(base::SafeRound(
			siblingSize.width() * kSiblingOutsidePart));
		const auto leftAvailable = layout.content.x() - siblingSize.width();
		const auto xDesired = leftAvailable / 3;
		const auto xPossible = std::min(
			xDesired,
			(leftAvailable - st::storiesControlSize));
		const auto xLeft = std::max(xPossible, -outsideMax);
		const auto xRight = size.width() - siblingSize.width() - xLeft;
		const auto userpicSize = int(base::SafeRound(
			siblingSize.width() * kSiblingUserpicSize));
		const auto innerHeight = userpicSize * kInnerHeightMultiplier;
		const auto userpic = [&](QRect geometry) {
			return QRect(
				(geometry.width() - userpicSize) / 2,
				(geometry.height() - innerHeight) / 2,
				userpicSize,
				userpicSize
			).translated(geometry.topLeft());
		};
		const auto nameFontSize = std::max(
			(st::storiesMaxNameFontSize * contentHeight
				/ st::storiesMaxSize.height()),
			st::fsize);
		const auto nameBoundingRect = [&](QRect geometry, bool left) {
			const auto skipSmall = nameFontSize;
			const auto skipBig = skipSmall - std::min(xLeft, 0);
			return QRect(
				left ? skipBig : skipSmall,
				(geometry.height() - innerHeight) / 2,
				geometry.width() - skipSmall - skipBig,
				innerHeight
			).translated(geometry.topLeft());
		};
		const auto left = QRect({ xLeft, siblingTop }, siblingSize);
		const auto right = QRect({ xRight, siblingTop }, siblingSize);
		layout.siblingLeft = {
			.geometry = left,
			.userpic = userpic(left),
			.nameBoundingRect = nameBoundingRect(left, true),
			.nameFontSize = nameFontSize,
		};
		layout.siblingRight = {
			.geometry = right,
			.userpic = userpic(right),
			.nameBoundingRect = nameBoundingRect(right, false),
			.nameFontSize = nameFontSize,
		};
		if (!_locationAreas.empty()) {
			rebuildLocationAreas(layout);
		}
		return layout;
	});
}

void Controller::rebuildLocationAreas(const Layout &layout) const {
	Expects(_locations.size() == _locationAreas.size());

	const auto origin = layout.content.topLeft();
	const auto scale = layout.content.size();
	for (auto i = 0, count = int(_locations.size()); i != count; ++i) {
		auto &area = _locationAreas[i];
		const auto &general = _locations[i].area.geometry;
		area.geometry = QRect(
			int(base::SafeRound(general.x() * scale.width())),
			int(base::SafeRound(general.y() * scale.height())),
			int(base::SafeRound(general.width() * scale.width())),
			int(base::SafeRound(general.height() * scale.height()))
		).translated(origin);
	}
}

Data::Story *Controller::story() const {
	if (!_session) {
		return nullptr;
	}
	const auto maybeStory = _session->data().stories().lookup(_shown);
	return maybeStory ? maybeStory->get() : nullptr;
}

not_null<Ui::RpWidget*> Controller::wrap() const {
	return _wrap;
}

Layout Controller::layout() const {
	Expects(_layout.current().has_value());

	return *_layout.current();
}

rpl::producer<Layout> Controller::layoutValue() const {
	return _layout.value() | rpl::filter_optional();
}

ContentLayout Controller::contentLayout() const {
	const auto &current = _layout.current();
	Assert(current.has_value());

	return {
		.geometry = current->content,
		.fade = (_contentFadeAnimation.value(_contentFaded ? 1. : 0.)
			* kFullContentFade),
		.radius = st::storiesRadius,
		.headerOutside = (current->headerLayout == HeaderLayout::Outside),
	};
}

bool Controller::closeByClickAt(QPoint position) const {
	const auto &current = _layout.current();
	Assert(current.has_value());

	return (position.x() < current->content.x() - st::storiesControlSize)
		|| (position.x() > current->content.x() + current->content.width()
			+ st::storiesControlSize);
}

Data::FileOrigin Controller::fileOrigin() const {
	return Data::FileOriginStory(_shown.peer, _shown.story);
}

TextWithEntities Controller::captionText() const {
	return _captionText;
}

bool Controller::skipCaption() const {
	return _captionFullView != nullptr;
}

void Controller::toggleLiked() {
	_reactions->toggleLiked();
}

void Controller::reactionChosen(ReactionsMode mode, ChosenReaction chosen) {
	if (mode == ReactionsMode::Message) {
		_replyArea->sendReaction(chosen.id);
	} else if (const auto user = shownUser()) {
		user->owner().stories().sendReaction(_shown, chosen.id);
	}
	unfocusReply();
}

void Controller::showFullCaption() {
	if (_captionText.empty()) {
		return;
	}
	_captionFullView = std::make_unique<CaptionFullView>(this);
	updateContentFaded();
}

void Controller::captionClosing() {
	updateContentFaded();
}

void Controller::captionClosed() {
	if (!_captionFullView) {
		return;
	} else if (_captionFullView->focused()) {
		_wrap->setFocus();
	}
	_captionFullView = nullptr;
}

std::shared_ptr<ChatHelpers::Show> Controller::uiShow() const {
	return _delegate->storiesShow();
}

auto Controller::stickerOrEmojiChosen() const
-> rpl::producer<ChatHelpers::FileChosen> {
	return _delegate->storiesStickerOrEmojiChosen();
}

auto Controller::cachedReactionIconFactory() const
-> HistoryView::Reactions::CachedIconFactory & {
	return _delegate->storiesCachedReactionIconFactory();
}

void Controller::rebuildFromContext(
		not_null<UserData*> user,
		FullStoryId storyId) {
	using namespace Data;

	auto &stories = user->owner().stories();
	auto list = std::optional<StoriesList>();
	auto source = (const StoriesSource*)nullptr;
	const auto peerId = storyId.peer;
	const auto id = storyId.story;
	v::match(_context.data, [&](StoriesContextSingle) {
		hideSiblings();
	}, [&](StoriesContextPeer) {
		source = stories.source(peerId);
		hideSiblings();
	}, [&](StoriesContextSaved) {
		if (stories.savedCountKnown(peerId)) {
			if (const auto saved = stories.saved(peerId)) {
				const auto &ids = saved->list;
				const auto i = ids.find(id);
				if (i != end(ids)) {
					list = StoriesList{
						.user = user,
						.ids = *saved,
						.total = stories.savedCount(peerId),
					};
					_index = int(i - begin(ids));
					if (ids.size() < list->total
						&& (end(ids) - i) < kPreloadStoriesCount) {
						stories.savedLoadMore(peerId);
					}
				}
			}
		}
		hideSiblings();
	}, [&](StoriesContextArchive) {
		Expects(user->isSelf());

		if (stories.archiveCountKnown()) {
			const auto &archive = stories.archive();
			const auto &ids = archive.list;
			const auto i = ids.find(id);
			if (i != end(ids)) {
				list = StoriesList{
					.user = user,
					.ids = archive,
					.total = stories.archiveCount(),
				};
				_index = int(i - begin(ids));
				if (ids.size() < list->total
					&& (end(ids) - i) < kPreloadStoriesCount) {
					stories.archiveLoadMore();
				}
			}
		}
		hideSiblings();
	}, [&](StorySourcesList list) {
		source = stories.source(peerId);
		const auto &sources = stories.sources(list);
		const auto i = ranges::find(
			sources,
			storyId.peer,
			&StoriesSourceInfo::id);
		if (i != end(sources)) {
			if (_cachedSourcesList.empty()) {
				_showingUnreadSources = source && (source->readTill < id);
			}
			rebuildCachedSourcesList(sources, (i - begin(sources)));
			_cachedSourcesList[_cachedSourceIndex].shownId = storyId.story;
			showSiblings(&user->session());
			if (int(sources.end() - i) < kPreloadUsersCount) {
				stories.loadMore(list);
			}
		}
	});
	_sliderIndex = 0;
	_sliderCount = 0;
	if (list) {
		_source = std::nullopt;
		if (_list != list) {
			_list = std::move(list);
		}
		if (const auto maybe = user->owner().stories().lookup(storyId)) {
			const auto now = *maybe;
			const auto range = ComputeSameDayRange(now, _list->ids, _index);
			_sliderCount = range.till - range.from + 1;
			_sliderIndex = _index - range.from;
		}
	} else {
		if (source) {
			const auto i = source->ids.lower_bound(StoryIdDates{ id });
			if (i != end(source->ids) && i->id == id) {
				_index = int(i - begin(source->ids));
			} else {
				source = nullptr;
			}
		}
		if (!source) {
			_source = std::nullopt;
			_list = StoriesList{
				.user = user,
				.ids = { { id } },
				.total = 1,
			};
			_index = 0;
		} else {
			_list = std::nullopt;
			if (_source != *source) {
				_source = *source;
			}
		}
	}
	preloadNext();
	_slider->show({
		.index = _sliderCount ? _sliderIndex : _index,
		.total = _sliderCount ? _sliderCount : shownCount(),
	});
}

void Controller::preloadNext() {
	Expects(shown());

	auto ids = std::vector<FullStoryId>();
	ids.reserve(kPreloadPreviousMediaCount + kPreloadNextMediaCount);
	const auto user = shownUser();
	const auto count = shownCount();
	const auto till = std::min(_index + kPreloadNextMediaCount, count);
	for (auto i = _index + 1; i != till; ++i) {
		ids.push_back({ .peer = user->id, .story = shownId(i) });
	}
	const auto from = std::max(_index - kPreloadPreviousMediaCount, 0);
	for (auto i = _index; i != from;) {
		ids.push_back({ .peer = user->id, .story = shownId(--i) });
	}
	user->owner().stories().setPreloadingInViewer(std::move(ids));
}

void Controller::checkMoveByDelta() {
	const auto index = _index + _waitingForDelta;
	if (_waitingForDelta && shown() && index >= 0 && index < shownCount()) {
		subjumpTo(index);
	}
}

void Controller::show(
		not_null<Data::Story*> story,
		Data::StoriesContext context) {
	auto &stories = story->owner().stories();
	const auto storyId = story->fullId();
	const auto user = story->peer()->asUser();
	_context = context;
	_waitingForId = {};
	_waitingForDelta = 0;

	rebuildFromContext(user, storyId);
	_contextLifetime.destroy();
	const auto subscribeToSource = [&] {
		stories.sourceChanged() | rpl::filter(
			rpl::mappers::_1 == storyId.peer
		) | rpl::start_with_next([=] {
			rebuildFromContext(user, storyId);
		}, _contextLifetime);
	};
	v::match(_context.data, [&](Data::StoriesContextSingle) {
	}, [&](Data::StoriesContextPeer) {
		subscribeToSource();
	}, [&](Data::StoriesContextSaved) {
		stories.savedChanged() | rpl::filter(
			rpl::mappers::_1 == storyId.peer
		) | rpl::start_with_next([=] {
			rebuildFromContext(user, storyId);
			checkMoveByDelta();
		}, _contextLifetime);
	}, [&](Data::StoriesContextArchive) {
		stories.archiveChanged(
		) | rpl::start_with_next([=] {
			rebuildFromContext(user, storyId);
			checkMoveByDelta();
		}, _contextLifetime);
	}, [&](Data::StorySourcesList) {
		subscribeToSource();
	});

	const auto guard = gsl::finally([&] {
		_paused = false;
		_started = false;
		if (!story->document()) {
			_photoPlayback = std::make_unique<PhotoPlayback>(this);
		} else {
			_photoPlayback = nullptr;
		}
	});

	const auto unsupported = story->unsupported();
	if (!unsupported) {
		_unsupported = nullptr;
	} else {
		_unsupported = std::make_unique<Unsupported>(this, user);
		_header->raise();
		_slider->raise();
	}

	captionClosed();
	_captionText = story->caption();
	_contentFaded = false;
	_contentFadeAnimation.stop();
	const auto document = story->document();
	_header->show({
		.user = user,
		.date = story->date(),
		.fullIndex = _sliderCount ? _index : 0,
		.fullCount = _sliderCount ? shownCount() : 0,
		.privacy = story->privacy(),
		.edited = story->edited(),
		.video = (document != nullptr),
		.silent = (document && document->isSilentVideo()),
	});
	uiShow()->hideLayer(anim::type::instant);
	if (!changeShown(story)) {
		return;
	}
	_viewed = false;
	invalidate_weak_ptrs(&_viewsLoadGuard);
	_reactions->hide();
	if (_replyArea->focused()) {
		unfocusReply();
	}

	_replyArea->show({
		.user = unsupported ? nullptr : user,
		.id = story->id(),
	}, _reactions->likedValue());

	_recentViews->show({
		.list = story->recentViewers(),
		.reactions = story->reactions(),
		.total = story->views(),
		.valid = user->isSelf(),
	});

	stories.loadAround(storyId, context);

	updatePlayingAllowed();
	user->updateFull();
}

bool Controller::changeShown(Data::Story *story) {
	const auto id = story ? story->fullId() : FullStoryId();
	const auto session = story ? &story->session() : nullptr;
	const auto sessionChanged = (_session != session);
	if (_shown == id && !sessionChanged) {
		return false;
	}
	if (_shown) {
		Assert(_session != nullptr);
		_session->data().stories().unregisterPolling(
			_shown,
			Data::Stories::Polling::Viewer);
	}
	if (sessionChanged) {
		_sessionLifetime.destroy();
	}
	_shown = id;
	_session = session;
	if (sessionChanged) {
		subscribeToSession();
	}
	if (story) {
		story->owner().stories().registerPolling(
			story,
			Data::Stories::Polling::Viewer);
	}
	_reactions->showLikeFrom(story);

	const auto &locations = story
		? story->locations()
		: std::vector<Data::StoryLocation>();
	if (_locations != locations) {
		_locations = locations;
		_locationAreas.clear();
	}

	return true;
}

void Controller::subscribeToSession() {
	Expects(!_sessionLifetime);

	if (!_session) {
		return;
	}
	_session->changes().storyUpdates(
		Data::StoryUpdate::Flag::Destroyed
	) | rpl::start_with_next([=](Data::StoryUpdate update) {
		if (update.story->fullId() == _shown) {
			_delegate->storiesClose();
		}
	}, _sessionLifetime);
	_session->data().stories().itemsChanged(
	) | rpl::start_with_next([=](PeerId peerId) {
		if (_waitingForId.peer == peerId) {
			checkWaitingFor();
		}
	}, _sessionLifetime);
	_session->changes().storyUpdates(
		Data::StoryUpdate::Flag::Edited
		| Data::StoryUpdate::Flag::ViewsAdded
	) | rpl::filter([=](const Data::StoryUpdate &update) {
		return (update.story == this->story());
	}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
		if (update.flags & Data::StoryUpdate::Flag::Edited) {
			show(update.story, _context);
			_delegate->storiesRedisplay(update.story);
		} else {
			_recentViews->show({
				.list = update.story->recentViewers(),
				.reactions = update.story->reactions(),
				.total = update.story->views(),
				.valid = update.story->peer()->isSelf(),
			});
		}
	}, _sessionLifetime);
	_sessionLifetime.add([=] {
		_session->data().stories().setPreloadingInViewer({});
	});
}

PauseState Controller::pauseState() const {
	const auto inactive = !_windowActive
		|| _replyActive
		|| _layerShown
		|| _menuShown;
	const auto playing = !inactive && !_paused;
	return playing
		? PauseState::Playing
		: !inactive
		? PauseState::Paused
		: _paused
		? PauseState::InactivePaused
		: PauseState::Inactive;
}

float64 Controller::currentVolume() const {
	return Core::App().settings().videoVolume();
}

void Controller::toggleVolume() {
	_delegate->storiesVolumeToggle();
}

void Controller::changeVolume(float64 volume) {
	_delegate->storiesVolumeChanged(volume);
}

void Controller::volumeChangeFinished() {
	_delegate->storiesVolumeChangeFinished();
}

void Controller::updatePlayingAllowed() {
	if (!_shown) {
		return;
	}
	_header->updatePauseState();
	setPlayingAllowed(_started
		&& _windowActive
		&& !_paused
		&& !_replyActive
		&& (!_captionFullView || _captionFullView->closing())
		&& !_layerShown
		&& !_menuShown
		&& !_tooltipShown);
}

void Controller::setPlayingAllowed(bool allowed) {
	if (_photoPlayback) {
		_photoPlayback->togglePaused(!allowed);
	} else {
		_delegate->storiesTogglePaused(!allowed);
	}
}

void Controller::showSiblings(not_null<Main::Session*> session) {
	showSibling(
		_siblingLeft,
		session,
		(_cachedSourceIndex > 0
			? _cachedSourcesList[_cachedSourceIndex - 1]
			: CachedSource()));
	showSibling(
		_siblingRight,
		session,
		(_cachedSourceIndex + 1 < _cachedSourcesList.size()
			? _cachedSourcesList[_cachedSourceIndex + 1]
			: CachedSource()));
}

void Controller::hideSiblings() {
	_siblingLeft = nullptr;
	_siblingRight = nullptr;
}

void Controller::showSibling(
		std::unique_ptr<Sibling> &sibling,
		not_null<Main::Session*> session,
		CachedSource cached) {
	if (!cached) {
		sibling = nullptr;
		return;
	}
	const auto source = session->data().stories().source(cached.peerId);
	if (!source) {
		sibling = nullptr;
	} else if (!sibling || !sibling->shows(*source, cached.shownId)) {
		sibling = std::make_unique<Sibling>(this, *source, cached.shownId);
	}
}

void Controller::ready() {
	if (_started) {
		return;
	}
	_started = true;
	updatePlayingAllowed();
	_reactions->ready();
}

void Controller::updateVideoPlayback(const Player::TrackState &state) {
	updatePlayback(state);
}

void Controller::updatePhotoPlayback(const Player::TrackState &state) {
	updatePlayback(state);
}

void Controller::updatePlayback(const Player::TrackState &state) {
	_slider->updatePlayback(state);
	updatePowerSaveBlocker(state);
	maybeMarkAsRead(state);
	if (Player::IsStoppedAtEnd(state.state)) {
		if (!subjumpFor(1)) {
			_delegate->storiesClose();
		}
	}
}

ClickHandlerPtr Controller::lookupLocationHandler(QPoint point) const {
	const auto &layout = _layout.current();
	if (_locations.empty() || !layout) {
		return nullptr;
	} else if (_locationAreas.empty()) {
		_locationAreas = _locations | ranges::views::transform([](
				const Data::StoryLocation &location) {
			return LocationArea{
				.rotation = location.area.rotation,
				.handler = std::make_shared<LocationClickHandler>(
					location.point),
			};
		}) | ranges::to_vector;
		rebuildLocationAreas(*layout);
	}

	for (const auto &area : _locationAreas) {
		const auto center = area.geometry.center();
		const auto angle = -area.rotation;
		if (area.geometry.contains(Rotated(point, center, angle))) {
			return area.handler;
		}
	}
	return nullptr;
}

void Controller::maybeMarkAsRead(const Player::TrackState &state) {
	const auto length = state.length;
	const auto position = Player::IsStoppedAtEnd(state.state)
		? state.length
		: Player::IsStoppedOrStopping(state.state)
		? 0
		: state.position;
	if (position > state.frequency * kMarkAsReadAfterSeconds) {
		if (position > kMarkAsReadAfterProgress * length) {
			markAsRead();
		}
	}
}

void Controller::markAsRead() {
	Expects(shown());

	if (_viewed) {
		return;
	}
	_viewed = true;
	shownUser()->owner().stories().markAsRead(_shown, _started);
}

bool Controller::subjumpAvailable(int delta) const {
	const auto index = _index + delta;
	if (index < 0) {
		return _siblingLeft && _siblingLeft->shownId().valid();
	} else if (index >= shownCount()) {
		return _siblingRight && _siblingRight->shownId().valid();
	}
	return index >= 0 && index < shownCount();
}

bool Controller::subjumpFor(int delta) {
	if (delta > 0) {
		markAsRead();
	}
	const auto index = _index + delta;
	if (index < 0) {
		if (_siblingLeft && _siblingLeft->shownId().valid()) {
			return jumpFor(-1);
		} else if (!shown() || !shownCount()) {
			return false;
		}
		subjumpTo(0);
		return true;
	} else if (index >= shownCount()) {
		return _siblingRight
			&& _siblingRight->shownId().valid()
			&& jumpFor(1);
	} else {
		subjumpTo(index);
	}
	return true;
}

void Controller::subjumpTo(int index) {
	Expects(shown());
	Expects(index >= 0 && index < shownCount());

	const auto user = shownUser();
	const auto id = FullStoryId{
		.peer = user->id,
		.story = shownId(index),
	};
	auto &stories = user->owner().stories();
	if (!id.story) {
		const auto delta = index - _index;
		if (_waitingForDelta != delta) {
			_waitingForDelta = delta;
			_waitingForId = {};
			loadMoreToList();
		}
	} else if (stories.lookup(id)) {
		_delegate->storiesJumpTo(&user->session(), id, _context);
	} else if (_waitingForId != id) {
		_waitingForId = id;
		_waitingForDelta = 0;
		stories.loadAround(id, _context);
	}
}

void Controller::checkWaitingFor() {
	Expects(_waitingForId.valid());
	Expects(shown());

	const auto user = shownUser();
	auto &stories = user->owner().stories();
	const auto maybe = stories.lookup(_waitingForId);
	if (!maybe) {
		if (maybe.error() == Data::NoStory::Deleted) {
			_waitingForId = {};
		}
		return;
	}
	_delegate->storiesJumpTo(
		&user->session(),
		base::take(_waitingForId),
		_context);
}

bool Controller::jumpFor(int delta) {
	if (delta == -1) {
		if (const auto left = _siblingLeft.get()) {
			_delegate->storiesJumpTo(
				&left->peer()->session(),
				left->shownId(),
				_context);
			return true;
		}
	} else if (delta == 1) {
		if (shown() && _index + 1 >= shownCount()) {
			markAsRead();
		}
		if (const auto right = _siblingRight.get()) {
			_delegate->storiesJumpTo(
				&right->peer()->session(),
				right->shownId(),
				_context);
			return true;
		}
	}
	return false;
}

bool Controller::paused() const {
	return _paused;
}

void Controller::togglePaused(bool paused) {
	if (_paused != paused) {
		_paused = paused;
		updatePlayingAllowed();
	}
}

void Controller::contentPressed(bool pressed) {
	togglePaused(pressed);
	if (_captionFullView) {
		_captionFullView->close();
	}
	if (pressed) {
		_reactions->outsidePressed();
	}
}

void Controller::setMenuShown(bool shown) {
	if (_menuShown != shown) {
		_menuShown = shown;
		updatePlayingAllowed();
	}
}

void Controller::repaintSibling(not_null<Sibling*> sibling) {
	if (sibling == _siblingLeft.get() || sibling == _siblingRight.get()) {
		_delegate->storiesRepaint();
	}
}

SiblingView Controller::sibling(SiblingType type) const {
	const auto &pointer = (type == SiblingType::Left)
		? _siblingLeft
		: _siblingRight;
	if (const auto value = pointer.get()) {
		const auto over = _delegate->storiesSiblingOver(type);
		const auto layout = (type == SiblingType::Left)
			? _layout.current()->siblingLeft
			: _layout.current()->siblingRight;
		return value->view(layout, over);
	}
	return {};
}

const Data::StoryViews &Controller::views(int limit, bool initial) {
	invalidate_weak_ptrs(&_viewsLoadGuard);
	if (initial) {
		refreshViewsFromData();
	}
	if (_viewsSlice.total > _viewsSlice.list.size()
		&& _viewsSlice.list.size() < limit) {
		const auto done = viewsGotMoreCallback();
		const auto user = shownUser();
		auto &stories = user->owner().stories();
		stories.loadViewsSlice(_shown.story, _viewsSlice.nextOffset, done);
	}
	return _viewsSlice;
}

rpl::producer<> Controller::moreViewsLoaded() const {
	return _moreViewsLoaded.events();
}

Fn<void(Data::StoryViews)> Controller::viewsGotMoreCallback() {
	return crl::guard(&_viewsLoadGuard, [=](Data::StoryViews result) {
		if (_viewsSlice.list.empty()) {
			const auto user = shownUser();
			auto &stories = user->owner().stories();
			if (const auto maybeStory = stories.lookup(_shown)) {
				_viewsSlice = (*maybeStory)->viewsList();
			} else {
				_viewsSlice = {};
			}
		} else {
			_viewsSlice.list.insert(
				end(_viewsSlice.list),
				begin(result.list),
				end(result.list));
			_viewsSlice.total = result.nextOffset.isEmpty()
				? int(_viewsSlice.list.size())
				: std::max(result.total, int(_viewsSlice.list.size()));
			_viewsSlice.nextOffset = result.nextOffset;
		}
		_moreViewsLoaded.fire({});
	});
}

bool Controller::shown() const {
	return _source || _list;
}

UserData *Controller::shownUser() const {
	return _source
		? _source->user.get()
		: _list
		? _list->user.get()
		: nullptr;
}

int Controller::shownCount() const {
	return _source ? int(_source->ids.size()) : _list ? _list->total : 0;
}

StoryId Controller::shownId(int index) const {
	Expects(index >= 0 && index < shownCount());

	return _source
		? (_source->ids.begin() + index)->id
		: (index < int(_list->ids.list.size()))
		? *(_list->ids.list.begin() + index)
		: StoryId();
}

void Controller::loadMoreToList() {
	Expects(shown());

	using namespace Data;

	const auto user = shownUser();
	const auto peerId = _shown.peer;
	auto &stories = user->owner().stories();
	v::match(_context.data, [&](StoriesContextSaved) {
		stories.savedLoadMore(peerId);
	}, [&](StoriesContextArchive) {
		Expects(user->isSelf());

		stories.archiveLoadMore();
	}, [](const auto &) {
	});
}

void Controller::rebuildCachedSourcesList(
		const std::vector<Data::StoriesSourceInfo> &lists,
		int index) {
	Expects(index >= 0 && index < lists.size());

	const auto currentPeerId = lists[index].id;

	// Remove removed.
	_cachedSourcesList.erase(ranges::remove_if(_cachedSourcesList, [&](
			CachedSource source) {
		return !ranges::contains(
			lists,
			source.peerId,
			&Data::StoriesSourceInfo::id);
	}), end(_cachedSourcesList));

	// Find current, full rebuild if can't find.
	const auto i = ranges::find(
		_cachedSourcesList,
		currentPeerId,
		&CachedSource::peerId);
	if (i == end(_cachedSourcesList)) {
		_cachedSourcesList.clear();
	} else {
		_cachedSourceIndex = int(i - begin(_cachedSourcesList));
	}

	if (_cachedSourcesList.empty()) {
		// Full rebuild.
		const auto predicate = [&](const Data::StoriesSourceInfo &info) {
			return !_showingUnreadSources
				|| (info.unreadCount > 0)
				|| (info.id == currentPeerId);
		};
		const auto mapper = [](const Data::StoriesSourceInfo &info) {
			return CachedSource{ info.id };
		};
		_cachedSourcesList = lists
			| ranges::views::filter(predicate)
			| ranges::views::transform(mapper)
			| ranges::to_vector;
		_cachedSourceIndex = ranges::find(
			_cachedSourcesList,
			currentPeerId,
			&CachedSource::peerId
		) - begin(_cachedSourcesList);
	} else if (ranges::equal(
			lists,
			_cachedSourcesList,
			ranges::equal_to(),
			&Data::StoriesSourceInfo::id,
			&CachedSource::peerId)) {
		// No rebuild needed.
	} else {
		// All that go before the current push to front.
		for (auto before = index; before > 0;) {
			const auto &info = lists[--before];
			if (_showingUnreadSources && !info.unreadCount) {
				continue;
			} else if (!ranges::contains(
					_cachedSourcesList,
					info.id,
					&CachedSource::peerId)) {
				_cachedSourcesList.insert(
					begin(_cachedSourcesList),
					{ info.id });
				++_cachedSourceIndex;
			}
		}
		// All that go after the current push to back.
		for (auto after = index + 1, count = int(lists.size())
			; after != count
			; ++after) {
			const auto &info = lists[after];
			if (_showingUnreadSources && !info.unreadCount) {
				continue;
			} else if (!ranges::contains(
					_cachedSourcesList,
					info.id,
					&CachedSource::peerId)) {
				_cachedSourcesList.push_back({ info.id });
			}
		}
	}

	Ensures(_cachedSourcesList.size() <= lists.size());
	Ensures(_cachedSourceIndex >= 0
		&& _cachedSourceIndex < _cachedSourcesList.size());
}

void Controller::refreshViewsFromData() {
	Expects(shown());

	const auto user = shownUser();
	auto &stories = user->owner().stories();
	const auto maybeStory = stories.lookup(_shown);
	if (!maybeStory || !user->isSelf()) {
		_viewsSlice = {};
	} else {
		_viewsSlice = (*maybeStory)->viewsList();
	}
}

void Controller::unfocusReply() {
	_wrap->setFocus();
}

void Controller::shareRequested() {
	const auto show = _delegate->storiesShow();
	if (auto box = PrepareShareBox(show, _shown, true)) {
		show->show(std::move(box));
	}
}

void Controller::deleteRequested() {
	const auto story = this->story();
	if (!story) {
		return;
	}
	const auto id = story->fullId();
	const auto weak = base::make_weak(this);
	const auto owner = &story->owner();
	const auto confirmed = [=](Fn<void()> close) {
		if (const auto strong = weak.get()) {
			if (const auto story = strong->story()) {
				if (story->fullId() == id) {
					moveFromShown();
				}
			}
		}
		owner->stories().deleteList({ id });
		close();
	};
	uiShow()->show(Ui::MakeConfirmBox({
		.text = tr::lng_stories_delete_one_sure(),
		.confirmed = confirmed,
		.confirmText = tr::lng_selected_delete(),
		.labelStyle = &st::storiesBoxLabel,
	}));
}

void Controller::reportRequested() {
	ReportRequested(uiShow(), _shown, &st::storiesReportBox);
}

void Controller::togglePinnedRequested(bool pinned) {
	const auto story = this->story();
	if (!story || !story->peer()->isSelf()) {
		return;
	}
	if (!pinned && v::is<Data::StoriesContextSaved>(_context.data)) {
		moveFromShown();
	}
	story->owner().stories().togglePinnedList({ story->fullId() }, pinned);
	uiShow()->showToast(PrepareTogglePinnedToast(1, pinned));
}

void Controller::moveFromShown() {
	if (!subjumpFor(1)) {
		[[maybe_unused]] const auto jumped = subjumpFor(-1);
	}
}

bool Controller::ignoreWindowMove(QPoint position) const {
	return _replyArea->ignoreWindowMove(position)
		|| _header->ignoreWindowMove(position);
}

void Controller::tryProcessKeyInput(not_null<QKeyEvent*> e) {
	_replyArea->tryProcessKeyInput(e);
}

bool Controller::allowStealthMode() const {
	const auto story = this->story();
	return story
		&& !story->peer()->isSelf()
		&& story->peer()->session().premiumPossible();
}

void Controller::setupStealthMode() {
	SetupStealthMode(uiShow());
}

auto Controller::attachReactionsToMenu(
	not_null<Ui::PopupMenu*> menu,
	QPoint desiredPosition)
-> AttachStripResult {
	return _reactions->attachToMenu(menu, desiredPosition);
}

rpl::lifetime &Controller::lifetime() {
	return _lifetime;
}

void Controller::updatePowerSaveBlocker(const Player::TrackState &state) {
	const auto block = !Player::IsPausedOrPausing(state.state)
		&& !Player::IsStoppedOrStopping(state.state);
	base::UpdatePowerSaveBlocker(
		_powerSaveBlocker,
		block,
		base::PowerSaveBlockType::PreventDisplaySleep,
		[] { return u"Stories playback is active"_q; },
		[=] { return _wrap->window()->windowHandle(); });
}

Ui::Toast::Config PrepareTogglePinnedToast(int count, bool pinned) {
	return {
		.text = (pinned
			? (count == 1
				? tr::lng_stories_save_done(
					tr::now,
					Ui::Text::Bold)
				: tr::lng_stories_save_done_many(
					tr::now,
					lt_count,
					count,
					Ui::Text::Bold)).append(
						'\n').append(
							tr::lng_stories_save_done_about(tr::now))
			: (count == 1
				? tr::lng_stories_archive_done(
					tr::now,
					Ui::Text::WithEntities)
				: tr::lng_stories_archive_done_many(
					tr::now,
					lt_count,
					count,
					Ui::Text::WithEntities))),
		.st = &st::storiesActionToast,
		.duration = (pinned
			? Data::Stories::kPinnedToastDuration
			: Ui::Toast::kDefaultDuration),
	};
}

void ReportRequested(
		std::shared_ptr<Main::SessionShow> show,
		FullStoryId id,
		const style::ReportBox *stOverride) {
	const auto owner = &show->session().data();
	const auto st = stOverride ? stOverride : &st::defaultReportBox;
	show->show(Box(Ui::ReportReasonBox, *st, Ui::ReportSource::Story, [=](
			Ui::ReportReason reason) {
		const auto done = [=](const QString &text) {
			owner->stories().report(show, id, reason, text);
			show->hideLayer();
		};
		show->showBox(Box(Ui::ReportDetailsBox, *st, done));
	}));
}

object_ptr<Ui::BoxContent> PrepareShortInfoBox(not_null<PeerData*> peer) {
	const auto open = [=] {
		if (const auto window = Core::App().windowFor(peer)) {
			window->invokeForSessionController(
				&peer->session().account(),
				peer,
				[&](not_null<Window::SessionController*> controller) {
					Core::App().hideMediaView();
					controller->showPeerHistory(peer);
				});
		}
	};
	return ::PrepareShortInfoBox(
		peer,
		open,
		[] { return false; },
		&st::storiesShortInfoBox);
}

} // namespace Media::Stories
