/*
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 "history/view/media/history_view_photo.h"

#include "boxes/send_credits_box.h"
#include "history/history_item_components.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_media_spoiler.h"
#include "lang/lang_keys.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_utility.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "ui/image/image.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_utilities.h"
#include "ui/grouped_layout.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_streaming.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_file_click_handler.h"
#include "data/data_file_origin.h"
#include "data/data_auto_download.h"
#include "data/data_web_page.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "styles/style_chat.h"

namespace HistoryView {
namespace {

constexpr auto kStoryWidth = 720;
constexpr auto kStoryHeight = 1280;

using Data::PhotoSize;

[[nodiscard]] bool IsHostedInstantViewMedia(not_null<const Element*> parent) {
	return parent->Get<InstantViewMediaRuntime>() != nullptr;
}

[[nodiscard]] double HostedInstantViewMediaPixelScale(
		not_null<const Element*> parent) {
	const auto runtime = parent->Get<InstantViewMediaRuntime>();
	return runtime ? runtime->mediaPixelScale : 1.;
}

[[nodiscard]] QSize ScaledInstantViewMediaSize(QSize size, double scale) {
	return (scale == 1.)
		? size
		: QSize(
			std::max(qRound(size.width() * scale), 1),
			std::max(qRound(size.height() * scale), 1));
}

[[nodiscard]] QSize HostedInstantViewForcedSize(
		not_null<const Element*> parent,
		not_null<const Media*> media) {
	const auto runtime = parent->Get<InstantViewMediaRuntime>();
	return (runtime && runtime->forcedFor == media)
		? runtime->forcedSize
		: QSize();
}

[[nodiscard]] QSize PhotoDesiredMediaSize(
		QSize dimensions,
		bool hostedInstantView) {
	return hostedInstantView
		? NonEmptySize(style::ConvertScale(dimensions))
		: CountDesiredMediaSize(dimensions);
}

} // namespace

struct Photo::Streamed {
	explicit Streamed(std::shared_ptr<::Media::Streaming::Document> shared);
	::Media::Streaming::Instance instance;
	::Media::Streaming::FrameRequest frozenRequest;
	QImage frozenFrame;
	std::array<QImage, 4> roundingCorners;
	QImage roundingMask;
};

Photo::Streamed::Streamed(
	std::shared_ptr<::Media::Streaming::Document> shared)
: instance(std::move(shared), nullptr) {
}

Photo::Photo(
	not_null<Element*> parent,
	not_null<HistoryItem*> realParent,
	not_null<PhotoData*> photo,
	bool spoiler)
: File(parent, realParent)
, _data(photo)
, _storyId(realParent->media()
	? realParent->media()->storyId()
	: FullStoryId())
, _spoiler((spoiler || realParent->isMediaSensitive())
	? std::make_unique<MediaSpoiler>()
	: nullptr)
, _sensitiveSpoiler(realParent->isMediaSensitive() ? 1 : 0) {
	create(realParent->fullId());
}

Photo::Photo(
	not_null<Element*> parent,
	not_null<PeerData*> chat,
	not_null<PhotoData*> photo,
	int width)
: File(parent, parent->data())
, _data(photo)
, _serviceWidth(width) {
	create(parent->data()->fullId(), chat);
}

Photo::~Photo() {
	if (_streamed || _dataMedia) {
		if (_streamed) {
			_data->owner().streaming().keepAlive(_data);
			stopAnimation();
		}
		if (_dataMedia) {
			_data->owner().keepAlive(base::take(_dataMedia));
			_parent->checkHeavyPart();
		}
	}
	togglePollingStory(false);
}

void Photo::create(FullMsgId contextId, PeerData *chat) {
	setLinks(
		std::make_shared<PhotoOpenClickHandler>(
			_data,
			crl::guard(this, [=](FullMsgId id) { showPhoto(id); }),
			contextId),
		std::make_shared<PhotoSaveClickHandler>(_data, contextId, chat),
		std::make_shared<PhotoCancelClickHandler>(
			_data,
			crl::guard(this, [=](FullMsgId id) {
				_parent->delegate()->elementCancelUpload(id);
			}),
			contextId));
	if ((_dataMedia = _data->activeMediaView())) {
		dataMediaCreated();
	} else if (_data->inlineThumbnailBytes().isEmpty()
		&& (_data->hasExact(PhotoSize::Small)
			|| _data->hasExact(PhotoSize::Thumbnail))) {
		_data->load(PhotoSize::Small, contextId);
	}
	if (_spoiler) {
		createSpoilerLink(_spoiler.get());
	}
	_purchasedPriceTag = hasPurchasedTag() ? 1 : 0;
}

void Photo::ensureDataMediaCreated() const {
	if (_dataMedia) {
		return;
	}
	_dataMedia = _data->createMediaView();
	dataMediaCreated();
}

void Photo::dataMediaCreated() const {
	Expects(_dataMedia != nullptr);

	if (_data->inlineThumbnailBytes().isEmpty()
		&& !_dataMedia->image(PhotoSize::Large)
		&& !_dataMedia->image(PhotoSize::Thumbnail)
		&& !_data->extendedMediaPreview()) {
		_dataMedia->wanted(PhotoSize::Small, _realParent->fullId());
	}
	history()->owner().registerHeavyViewPart(_parent);
	togglePollingStory(true);
}

bool Photo::hasHeavyPart() const {
	return (_spoiler && _spoiler->animation) || _streamed || _dataMedia;
}

void Photo::unloadHeavyPart() {
	stopAnimation();
	_dataMedia = nullptr;
	if (_spoiler) {
		_spoiler->background = _spoiler->cornerCache = QImage();
		_spoiler->animation = nullptr;
	}
	_imageCache = QImage();
	togglePollingStory(false);
}

bool Photo::enforceBubbleWidth() const {
	return true;
}

void Photo::togglePollingStory(bool enabled) const {
	const auto pollingStory = (enabled ? 1 : 0);
	if (!_storyId || _pollingStory == pollingStory) {
		return;
	}
	const auto polling = Data::Stories::Polling::Chat;
	if (!enabled) {
		_data->owner().stories().unregisterPolling(_storyId, polling);
	} else if (
			!_data->owner().stories().registerPolling(_storyId, polling)) {
		return;
	}
	_pollingStory = pollingStory;
}

QSize Photo::countOptimalSize() {
	if (_serviceWidth > 0) {
		return { int(_serviceWidth), int(_serviceWidth) };
	}
	if (const auto forced = HostedInstantViewForcedSize(_parent, this)
		; !forced.isEmpty()) {
		return forced;
	}
	const auto hostedInstantView = IsHostedInstantViewMedia(_parent);
	const auto dimensions = photoSize();
	const auto scaled = PhotoDesiredMediaSize(dimensions, hostedInstantView);
	const auto maxMediaWidth = hostedInstantView
		? std::max(scaled.width(), st::maxMediaSize)
		: st::maxMediaSize;
	const auto minWidth = std::clamp(
		_parent->minWidthForMedia(),
		(_parent->hasBubble()
			? st::historyPhotoBubbleMinWidth
			: st::minPhotoSize),
		maxMediaWidth);
	const auto maxActualWidth = qMax(scaled.width(), minWidth);
	auto maxWidth = qMax(maxActualWidth, scaled.height());
	auto minHeight = qMax(scaled.height(), st::minPhotoSize);
	if (_parent->hasBubble()) {
		const auto botTop = _parent->Get<FakeBotAboutTop>();
		const auto captionMaxWidth = _parent->textualMaxWidth();
		if (botTop || !_parent->data()->isFakeAboutView()) {
			const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth);
			maxWidth = qMin(qMax(maxWidth, maxWithCaption), st::msgMaxWidth);
			minHeight = adjustHeightForLessCrop(
				dimensions,
				{ maxWidth, minHeight });
		}
	}
	return { maxWidth, minHeight };
}

QSize Photo::countCurrentSize(int newWidth) {
	if (_serviceWidth) {
		return { int(_serviceWidth), int(_serviceWidth) };
	}
	if (const auto forced = HostedInstantViewForcedSize(_parent, this)
		; !forced.isEmpty()) {
		return forced;
	}
	const auto hostedInstantView = IsHostedInstantViewMedia(_parent);
	const auto thumbMaxWidth = hostedInstantView
		? std::max(newWidth, 1)
		: qMin(newWidth, st::maxMediaSize);
	const auto minWidth = std::clamp(
		_parent->minWidthForMedia(),
		qMin(thumbMaxWidth, _parent->hasBubble()
			? st::historyPhotoBubbleMinWidth
			: st::minPhotoSize),
		thumbMaxWidth);
	const auto dimensions = photoSize();
	const auto desired = PhotoDesiredMediaSize(dimensions, hostedInstantView);
	auto pix = _data->extendedMediaVideoDuration()
		? CountMediaSize(
			desired,
			newWidth)
		: CountPhotoMediaSize(
			desired,
			newWidth,
			hostedInstantView ? newWidth : maxWidth());
	newWidth = qMax(pix.width(), minWidth);
	auto newHeight = qMax(pix.height(), st::minPhotoSize);
	if (_parent->hasBubble()) {
		auto captionMaxWidth = _parent->textualMaxWidth();
		const auto botTop = _parent->Get<FakeBotAboutTop>();
		if (botTop) {
			accumulate_max(captionMaxWidth, botTop->maxWidth);
		}
		if (botTop || !_parent->data()->isFakeAboutView()) {
			const auto maxWithCaption = qMin(
				st::msgMaxWidth,
				captionMaxWidth);
			newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
			newHeight = adjustHeightForLessCrop(
				dimensions,
				{ newWidth, newHeight });
		}
	}
	if (newWidth >= maxWidth()) {
		newHeight = qMin(newHeight, minHeight());
	}
	const auto enlargeInner = st::historyPageEnlargeSize;
	const auto enlargeOuter = 2 * st::historyPageEnlargeSkip + enlargeInner;
	const auto showEnlarge = (_parent->media() != this)
		&& _parent->data()->media()
		&& !_parent->data()->isSponsored()
		&& _parent->data()->media()->webpage()
		&& _parent->data()->media()->webpage()->suggestEnlargePhoto()
		&& (newWidth >= enlargeOuter)
		&& (newHeight >= enlargeOuter);
	_showEnlarge = showEnlarge ? 1 : 0;
	return { newWidth, newHeight };
}

int Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const {
	if (dimensions.isEmpty()
		|| !::Media::Streaming::FrameResizeMayExpand(current, dimensions)) {
		return current.height();
	}
	return qMax(
		current.height(),
		current.width() * dimensions.height() / dimensions.width());
}

void Photo::draw(Painter &p, const PaintContext &context) const {
	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
		return;
	} else if (_storyId && _data->isNull()) {
		return;
	}

	ensureDataMediaCreated();
	auto peerId = _parent->data()->from() ? _parent->data()->from()->id : PeerId(0);
	auto user = history()->session().data().peerLoaded(_parent->data()->from() ? _parent->data()->from()->id : PeerId(0));
	if (!blockExist(peerId.value) || (!GetEnhancedBool("blocked_user_spoiler_mode") && user && !user->isBlocked())) {
		_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
	}
	const auto st = context.st;
	const auto sti = context.imageStyle();
	const auto preview = _data->extendedMediaPreview();
	const auto loaded = preview || _dataMedia->loaded();
	const auto displayLoading = !preview && _data->displayLoading();

	const auto hostedInstantView = IsHostedInstantViewMedia(_parent);
	auto inWebPage = (_parent->media() != this);
	auto paintx = 0, painty = 0, paintw = width(), painth = height();
	auto bubble = _parent->hasBubble();

	if (displayLoading) {
		ensureAnimation();
		if (!_animation->radial.animating()) {
			_animation->radial.start(_dataMedia->progress());
		}
	}
	const auto radial = isRadialAnimation();

	auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
	if (_serviceWidth > 0) {
		paintUserpicFrame(p, context, rthumb.topLeft());
	} else {
		const auto rounding = hostedInstantView
			? std::optional<Ui::BubbleRounding>(Ui::BubbleRounding())
			: inWebPage
			? std::optional<Ui::BubbleRounding>()
			: adjustedBubbleRounding();
		if (!bubble && !hostedInstantView) {
			Assert(rounding.has_value());
			fillImageShadow(p, rthumb, *rounding, context);
		}
		const auto revealed = _spoiler
			? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)
			: 1.;
		if (revealed < 1.) {
			validateSpoilerImageCache(rthumb.size(), rounding);
		}
		if (revealed > 0.) {
			validateImageCache(rthumb.size(), rounding);
			p.drawImage(rthumb, _imageCache);
		}
		if (revealed < 1.) {
			p.setOpacity(1. - revealed);
			p.drawImage(rthumb, _spoiler->background);
			fillImageSpoiler(p, _spoiler.get(), rthumb, context);
			p.setOpacity(1.);
		}
		if (context.selected()) {
			fillImageOverlay(p, rthumb, rounding, context);
		}
	}

	const auto showEnlarge = false;
	const auto paintInCenter = !_sensitiveSpoiler
		&& (radial || (!loaded && !_data->loading()));
	if (paintInCenter || showEnlarge) {
		p.setPen(Qt::NoPen);
		if (context.selected()) {
			p.setBrush(st->msgDateImgBgSelected());
		} else if (showEnlarge) {
			const auto over = ClickHandler::showAsActive(_openl);
			p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
		} else if (isThumbAnimation()) {
			const auto over = _animation->a_thumbOver.value(1.);
			p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));
		} else {
			const auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
			p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
		}
	}
	if (paintInCenter) {
		const auto radialOpacity = (radial && loaded && !_data->uploading())
			? _animation->radial.opacity() :
			1.;
		const auto innerSize = st::msgFileLayout.thumbSize;
		QRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize);

		p.setOpacity(radialOpacity * p.opacity());

		{
			PainterHighQualityEnabler hq(p);
			p.drawEllipse(inner);
		}

		p.setOpacity(radialOpacity);
		const auto &icon = (radial || _data->loading())
			? sti->historyFileThumbCancel
			: sti->historyFileThumbDownload;
		icon.paintInCenter(p, inner);
		p.setOpacity(1);
		if (radial) {
			QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
			_animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg);
		}
	} else if (_sensitiveSpoiler || preview) {
		drawSpoilerTag(p, rthumb, context, [&] {
			return spoilerTagBackground();
		});
	}
	if (showEnlarge) {
		auto hq = PainterHighQualityEnabler(p);
		const auto rect = enlargeRect();
		const auto radius = st::historyPageEnlargeRadius;
		p.drawRoundedRect(rect, radius, radius);
		sti->historyPageEnlarge.paintInCenter(p, rect);
	}
	if (_purchasedPriceTag) {
		auto geometry = rthumb;
		if (showEnlarge) {
			const auto rect = enlargeRect();
			geometry.setY(rect.y() + rect.height());
		}
		drawPurchasedTag(p, geometry, context);
	}

	// date
	if (!inWebPage && (!bubble || isBubbleBottom())) {
		auto fullRight = paintx + paintw;
		auto fullBottom = painty + painth;
		if (needInfoDisplay()) {
			_parent->drawInfo(
				p,
				context,
				fullRight,
				fullBottom,
				2 * paintx + paintw,
				InfoDisplayType::Image);
		}
		if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
			auto fastShareLeft = _parent->hasRightLayout()
				? (paintx - size->width() - st::historyFastShareLeft)
				: (fullRight + st::historyFastShareLeft);
			auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
			_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);
		}
	}
}

void Photo::drawSpoilerTag(
		Painter &p,
		QRect rthumb,
		const PaintContext &context,
		Fn<QImage()> generateBackground) const {
	Media::drawSpoilerTag(
		p,
		_spoiler.get(),
		_spoilerTag,
		rthumb,
		context,
		std::move(generateBackground));
}

void Photo::validateUserpicImageCache(QSize size, bool forum) const {
	const auto forumValue = forum ? 1 : 0;
	const auto large = _dataMedia->image(PhotoSize::Large);
	const auto ratio = style::DevicePixelRatio();
	const auto blurredValue = large ? 0 : 1;
	if (_imageCache.size() == (size * ratio)
		&& _imageCacheForum == forumValue
		&& _imageCacheBlurred == blurredValue) {
		return;
	}
	auto original = [&] {
		if (large) {
			return large->original();
		} else if (const auto thumbnail = _dataMedia->image(
				PhotoSize::Thumbnail)) {
			return thumbnail->original();
		} else if (const auto small = _dataMedia->image(
				PhotoSize::Small)) {
			return small->original();
		} else if (const auto blurred = _dataMedia->thumbnailInline()) {
			return blurred->original();
		} else {
			return Image::Empty()->original();
		}
	}();
	auto args = Images::PrepareArgs();
	if (blurredValue) {
		args = args.blurred();
	}
	original = Images::Prepare(std::move(original), size * ratio, args);
	if (forumValue) {
		original = Images::Round(
			std::move(original),
			Images::CornersMask(std::min(size.width(), size.height())
				* Ui::ForumUserpicRadiusMultiplier()));
	} else {
		original = Images::Circle(std::move(original));
	}
	_imageCache = std::move(original);
	_imageCacheForum = forumValue;
	_imageCacheBlurred = blurredValue;
}

void Photo::validateImageCache(
		QSize outer,
		std::optional<Ui::BubbleRounding> rounding) const {
	const auto large = _dataMedia->image(PhotoSize::Large);
	const auto ratio = style::DevicePixelRatio();
	const auto scaled = ScaledInstantViewMediaSize(
		outer,
		HostedInstantViewMediaPixelScale(_parent));
	const auto blurredValue = large ? 0 : 1;
	if (_imageCache.size() == (scaled * ratio)
		&& _imageCacheRounding == rounding
		&& _imageCacheBlurred == blurredValue) {
		return;
	}
	_imageCache = Images::Round(
		prepareImageCache(scaled),
		MediaRoundingMask(rounding));
	_imageCacheRounding = rounding;
	_imageCacheBlurred = blurredValue;
}

void Photo::validateSpoilerImageCache(
		QSize outer,
		std::optional<Ui::BubbleRounding> rounding) const {
	Expects(_spoiler != nullptr);

	const auto ratio = style::DevicePixelRatio();
	const auto scaled = ScaledInstantViewMediaSize(
		outer,
		HostedInstantViewMediaPixelScale(_parent));
	if (_spoiler->background.size() == (scaled * ratio)
		&& _spoiler->backgroundRounding == rounding) {
		return;
	}
	_spoiler->background = Images::Round(
		prepareImageCacheWithLarge(scaled, nullptr),
		MediaRoundingMask(rounding));
	_spoiler->backgroundRounding = rounding;
}

QImage Photo::prepareImageCache(QSize outer) const {
	return prepareImageCacheWithLarge(
		outer,
		_dataMedia->image(PhotoSize::Large));
}

QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const {
	using Size = PhotoSize;
	auto blurred = (Image*)nullptr;
	if (const auto embedded = _dataMedia->thumbnailInline()) {
		blurred = embedded;
	} else if (const auto thumbnail = _dataMedia->image(Size::Thumbnail)) {
		blurred = thumbnail;
	} else if (const auto small = _dataMedia->image(Size::Small)) {
		blurred = small;
		} else {
			blurred = large;
	}
	const auto resize = large
		? ::Media::Streaming::DecideFrameResize(outer, large->size())
		: ::Media::Streaming::ExpandDecision();
	return PrepareWithBlurredBackground(outer, resize, large, blurred);
}

void Photo::paintUserpicFrame(
	Painter &p,
	QPoint photoPosition,
	bool markFrameShown) const {
	const auto autoplay = _data->videoCanBePlayed()
		&& videoAutoplayEnabled();
	const auto startPlay = autoplay && !_streamed;
	if (startPlay) {
		const_cast<Photo*>(this)->playAnimation(true);
	} else {
		checkStreamedIsStarted();
	}

	const auto size = QSize(width(), height());
	const auto rect = QRect(photoPosition, size);
	const auto forum = _parent->data()->history()->isForum();

	if (_streamed
		&& _streamed->instance.player().ready()
		&& !_streamed->instance.player().videoSize().isEmpty()) {
		const auto ratio = style::DevicePixelRatio();
		auto request = ::Media::Streaming::FrameRequest();
		request.outer = request.resize = size * ratio;
		if (forum) {
			const auto radius = int(std::min(size.width(), size.height())
				* Ui::ForumUserpicRadiusMultiplier());
			if (_streamed->roundingCorners[0].width() != radius * ratio) {
				_streamed->roundingCorners = Images::CornersMask(radius);
			}
			request.rounding = Images::CornersMaskRef(
				_streamed->roundingCorners);
		} else {
			if (_streamed->roundingMask.size() != request.outer) {
				_streamed->roundingMask = Images::EllipseMask(size);
			}
			request.mask = _streamed->roundingMask;
		}
		if (_streamed->instance.playerLocked()) {
			if (_streamed->frozenFrame.isNull()
				|| _streamed->frozenRequest != request) {
				_streamed->frozenRequest = request;
				_streamed->frozenFrame = _streamed->instance.frame(request);
			}
			p.drawImage(rect, _streamed->frozenFrame);
		} else {
			_streamed->frozenFrame = QImage();
			p.drawImage(rect, _streamed->instance.frame(request));
			if (markFrameShown) {
				_streamed->instance.markFrameShown();
			}
		}
		return;
	}
	validateUserpicImageCache(size, forum);
	p.drawImage(rect, _imageCache);
}

void Photo::paintUserpicFrame(
	Painter &p,
	const PaintContext &context,
	QPoint photoPosition) const {
	paintUserpicFrame(p, photoPosition, !context.paused);

	if (_data->videoCanBePlayed() && !_streamed) {
		const auto st = context.st;
		const auto sti = context.imageStyle();
		const auto innerSize = st::msgFileLayout.thumbSize;
		auto inner = QRect(
			photoPosition.x() + (width() - innerSize) / 2,
			photoPosition.y() + (height() - innerSize) / 2,
			innerSize,
			innerSize);
		p.setPen(Qt::NoPen);
		if (context.selected()) {
			p.setBrush(st->msgDateImgBgSelected());
		} else {
			const auto over = ClickHandler::showAsActive(_openl);
			p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
		}
		{
			PainterHighQualityEnabler hq(p);
			p.drawEllipse(inner);
		}
		sti->historyFileThumbPlay.paintInCenter(p, inner);
	}
}

QSize Photo::photoSize() const {
	if (_storyId) {
		return { kStoryWidth, kStoryHeight };
	} else if (_parent->data()->isFakeAboutView()
		&& !_parent->Get<FakeBotAboutTop>()) {
		return { st::managedBotImageWidth, st::managedBotImageHeight };
	}
	return QSize(_data->width(), _data->height());
}

QRect Photo::enlargeRect() const {
	const auto skip = st::historyPageEnlargeSkip;
	const auto enlargeInner = st::historyPageEnlargeSize;
	const auto enlargeOuter = 2 * skip + enlargeInner;
	return {
		width() - enlargeOuter + skip,
		skip,
		enlargeInner,
		enlargeInner,
	};
}

ClickHandlerPtr Photo::spoilerTagLink() const {
	return Media::spoilerTagLink(_spoiler.get(), _spoilerTag);
}

QImage Photo::spoilerTagBackground() const {
	return _spoiler ? _spoiler->background : QImage();
}

TextState Photo::textState(QPoint point, StateRequest request) const {
	auto result = TextState(_parent);

	if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) {
		return result;
	} else if (_storyId && _data->isNull()) {
		return result;
	}
	auto paintx = 0, painty = 0, paintw = width(), painth = height();
	auto bubble = _parent->hasBubble();

	if (QRect(paintx, painty, paintw, painth).contains(point)) {
		ensureDataMediaCreated();
		result.link = (_spoiler && !_spoiler->revealed)
			? ((_data->extendedMediaPreview() || _sensitiveSpoiler)
				? spoilerTagLink()
				: _spoiler->link)
			: _data->uploading()
			? _cancell
			: _dataMedia->loaded()
			? _openl
			: _data->loading()
			? _cancell
			: _savel;
		if (_showEnlarge
			&& result.link == _openl
			&& enlargeRect().contains(point)) {
			result.cursor = CursorState::Enlarge;
		}
	}
	if (_parent->media() == this && (!_parent->hasBubble() || isBubbleBottom())) {
		auto fullRight = paintx + paintw;
		auto fullBottom = painty + painth;
		const auto bottomInfoResult = _parent->bottomInfoTextState(
			fullRight,
			fullBottom,
			point,
			InfoDisplayType::Image);
		if (bottomInfoResult.link
			|| bottomInfoResult.cursor != CursorState::None
			|| bottomInfoResult.customTooltip) {
			return bottomInfoResult;
		}
		if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
			auto fastShareLeft = _parent->hasRightLayout()
				? (paintx - size->width() - st::historyFastShareLeft)
				: (fullRight + st::historyFastShareLeft);
			auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
			if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {
				result.link = _parent->rightActionLink(point
					- QPoint(fastShareLeft, fastShareTop));
			}
		}
	}
	return result;
}

QSize Photo::sizeForGroupingOptimal(int maxWidth, bool last) const {
	const auto size = photoSize();
	return { std::max(size.width(), 1), std::max(size.height(), 1)};
}

QSize Photo::sizeForGrouping(int width) const {
	return sizeForGroupingOptimal(width, false);
}

void Photo::drawGrouped(
		Painter &p,
		const PaintContext &context,
		const QRect &geometry,
		RectParts sides,
		Ui::BubbleRounding rounding,
		float64 highlightOpacity,
		not_null<uint64*> cacheKey,
		not_null<QPixmap*> cache) const {
	ensureDataMediaCreated();

	auto peerId = _parent->data()->from() ? _parent->data()->from()->id : PeerId(0);
	auto user = history()->session().data().peerLoaded(_parent->data()->from() ? _parent->data()->from()->id : PeerId(0));
	if (!blockExist(peerId.value) || (!GetEnhancedBool("blocked_user_spoiler_mode") && user && !user->isBlocked())) {
		_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
	}

	const auto st = context.st;
	const auto sti = context.imageStyle();
	const auto preview = _data->extendedMediaPreview();
	const auto loaded = preview || _dataMedia->loaded();
	const auto displayLoading = !preview && _data->displayLoading();

	if (displayLoading) {
		ensureAnimation();
		if (!_animation->radial.animating()) {
			_animation->radial.start(_dataMedia->progress());
		}
	}
	const auto radial = isRadialAnimation();

	const auto revealed = _spoiler
		? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)
		: 1.;
	if (revealed < 1.) {
		validateSpoilerImageCache(geometry.size(), rounding);
	}
	if (revealed > 0.) {
		validateGroupedCache(geometry, rounding, cacheKey, cache);
		p.drawPixmap(geometry, *cache);
	}
	if (revealed < 1.) {
		p.setOpacity(1. - revealed);
		p.drawImage(geometry, _spoiler->background);
		fillImageSpoiler(p, _spoiler.get(), geometry, context);
		p.setOpacity(1.);
	}

	const auto overlayOpacity = context.selected()
		? (1. - highlightOpacity)
		: highlightOpacity;
	if (overlayOpacity > 0.) {
		p.setOpacity(overlayOpacity);
		fillImageOverlay(p, geometry, rounding, context);
		if (!context.selected()) {
			fillImageOverlay(p, geometry, rounding, context);
		}
		p.setOpacity(1.);
	}

	const auto paintInCenter = !_sensitiveSpoiler
		&& (radial
			|| (!loaded && !_data->loading())
			|| _data->waitingForAlbum());
	if (paintInCenter) {
		const auto radialOpacity = radial
			? _animation->radial.opacity()
			: 1.;
		const auto backOpacity = (loaded && !_data->uploading())
			? radialOpacity
			: 1.;
		const auto radialSize = st::historyGroupRadialSize;
		const auto inner = QRect(
			geometry.x() + (geometry.width() - radialSize) / 2,
			geometry.y() + (geometry.height() - radialSize) / 2,
			radialSize,
			radialSize);
		p.setPen(Qt::NoPen);
		if (context.selected()) {
			p.setBrush(st->msgDateImgBgSelected());
		} else if (isThumbAnimation()) {
			auto over = _animation->a_thumbOver.value(1.);
			p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over));
		} else {
			auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
			p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg());
		}

		p.setOpacity(backOpacity * p.opacity());

		{
			PainterHighQualityEnabler hq(p);
			p.drawEllipse(inner);
		}

		const auto &icon = _data->waitingForAlbum()
			? sti->historyFileThumbWaiting
			: (radial || _data->loading())
			? sti->historyFileThumbCancel
			: sti->historyFileThumbDownload;
		const auto previous = _data->waitingForAlbum()
			? &sti->historyFileThumbCancel
			: nullptr;
		p.setOpacity(backOpacity);
		if (previous && radialOpacity > 0. && radialOpacity < 1.) {
			PaintInterpolatedIcon(p, icon, *previous, radialOpacity, inner);
		} else {
			icon.paintInCenter(p, inner);
		}
		p.setOpacity(1);
		if (radial) {
			const auto line = st::historyGroupRadialLine;
			const auto rinner = inner.marginsRemoved({ line, line, line, line });
			_animation->radial.draw(p, rinner, line, sti->historyFileThumbRadialFg);
		}
	}
}

TextState Photo::getStateGrouped(
		const QRect &geometry,
		RectParts sides,
		QPoint point,
		StateRequest request) const {
	if (!geometry.contains(point)) {
		return {};
	}
	ensureDataMediaCreated();
	auto link = (_spoiler && !_spoiler->revealed)
		? ((_data->extendedMediaPreview() || _sensitiveSpoiler)
			? spoilerTagLink()
			: _spoiler->link)
		: _data->uploading()
		? _cancell
		: _dataMedia->loaded()
		? _openl
		: _data->loading()
		? _cancell
		: _savel;
	return TextState(_parent, std::move(link));
}

float64 Photo::dataProgress() const {
	ensureDataMediaCreated();
	return _dataMedia->progress();
}

bool Photo::dataFinished() const {
	return !_data->loading()
		&& (!_data->uploading() || _data->waitingForAlbum());
}

bool Photo::dataLoaded() const {
	ensureDataMediaCreated();
	return _dataMedia->loaded();
}

bool Photo::needInfoDisplay() const {
	if (_parent->data()->isFakeAboutView()) {
		return false;
	}
	return _parent->data()->isSending()
		|| _parent->data()->hasFailed()
		|| _parent->isUnderCursor()
		|| (_parent->delegate()->elementContext() == Context::ChatPreview)
		|| _parent->isLastAndSelfMessage();
}

void Photo::validateGroupedCache(
		const QRect &geometry,
		Ui::BubbleRounding rounding,
		not_null<uint64*> cacheKey,
		not_null<QPixmap*> cache) const {
	using Option = Images::Option;

	ensureDataMediaCreated();

	const auto preview = _data->extendedMediaPreview();
	const auto loaded = preview || _dataMedia->loaded();
	const auto loadLevel = loaded
		? 2
		: (_dataMedia->thumbnailInline()
			|| _dataMedia->image(PhotoSize::Small)
			|| _dataMedia->image(PhotoSize::Thumbnail))
		? 1
		: 0;
	const auto scaled = ScaledInstantViewMediaSize(
		geometry.size(),
		HostedInstantViewMediaPixelScale(_parent));
	const auto width = scaled.width();
	const auto height = scaled.height();
	const auto options = (loaded ? Option() : Option::Blur);
	const auto key = (uint64(width) << 48)
		| (uint64(height) << 32)
		| (uint64(options) << 16)
		| (uint64(rounding.key()) << 8)
		| (uint64(loadLevel));
	if (*cacheKey == key) {
		return;
	}

	const auto unscaled = photoSize();
	const auto originalWidth = style::ConvertScale(unscaled.width());
	const auto originalHeight = style::ConvertScale(unscaled.height());
	const auto pixSize = Ui::GetImageScaleSizeForGeometry(
		{ originalWidth, originalHeight },
		{ width, height });
	const auto ratio = style::DevicePixelRatio();
	const auto image = _dataMedia->image(PhotoSize::Large)
		? _dataMedia->image(PhotoSize::Large)
		: _dataMedia->image(PhotoSize::Thumbnail)
		? _dataMedia->image(PhotoSize::Thumbnail)
		: _dataMedia->image(PhotoSize::Small)
		? _dataMedia->image(PhotoSize::Small)
		: _dataMedia->thumbnailInline()
		? _dataMedia->thumbnailInline()
		: Image::BlankMedia().get();

	*cacheKey = key;
	auto prepared = Images::Prepare(
		image->original(),
		pixSize * ratio,
		{ .options = options, .outer = { width, height } });
	auto rounded = Images::Round(
		std::move(prepared),
		MediaRoundingMask(rounding));
	*cache = Ui::PixmapFromImage(std::move(rounded));
}

bool Photo::createStreamingObjects() {
	using namespace ::Media::Streaming;

	setStreamed(std::make_unique<Streamed>(
		history()->owner().streaming().sharedDocument(
			_data,
			_realParent->fullId())));
	_streamed->instance.player().updates(
	) | rpl::on_next_error([=](Update &&update) {
		handleStreamingUpdate(std::move(update));
	}, [=](Error &&error) {
		handleStreamingError(std::move(error));
	}, _streamed->instance.lifetime());
	if (_streamed->instance.ready()) {
		streamingReady(base::duplicate(_streamed->instance.info()));
	}
	if (!_streamed->instance.valid()) {
		stopAnimation();
		return false;
	}
	checkStreamedIsStarted();
	return true;
}

void Photo::setStreamed(std::unique_ptr<Streamed> value) {
	const auto removed = (_streamed && !value);
	const auto set = (!_streamed && value);
	_streamed = std::move(value);
	if (set) {
		history()->owner().registerHeavyViewPart(_parent);
		togglePollingStory(true);
	} else if (removed) {
		_parent->checkHeavyPart();
	}
}

void Photo::handleStreamingUpdate(::Media::Streaming::Update &&update) {
	using namespace ::Media::Streaming;

	v::match(update.data, [&](Information &update) {
		streamingReady(std::move(update));
	}, [](PreloadedVideo) {
	}, [&](UpdateVideo) {
		repaintStreamedContent();
	}, [](PreloadedAudio) {
	}, [](UpdateAudio) {
	}, [](WaitingForData) {
	}, [](SpeedEstimate) {
	}, [](MutedByOther) {
	}, [](Finished) {
	});
}

void Photo::handleStreamingError(::Media::Streaming::Error &&error) {
	_data->setVideoPlaybackFailed();
	stopAnimation();
}

void Photo::repaintStreamedContent() {
	if (_streamed && !_streamed->frozenFrame.isNull()) {
		return;
	} else if (_parent->delegate()->elementAnimationsPaused()) {
		return;
	}
	repaint();
}

void Photo::streamingReady(::Media::Streaming::Information &&info) {
	repaint();
}

void Photo::checkAnimation() {
	if (_streamed && !videoAutoplayEnabled()) {
		stopAnimation();
	}
}

void Photo::stopAnimation() {
	setStreamed(nullptr);
}

void Photo::playAnimation(bool autoplay) {
	ensureDataMediaCreated();
	if (_streamed && autoplay) {
		return;
	} else if (_streamed && videoAutoplayEnabled()) {
		showPhoto(_parent->data()->fullId());
		return;
	}
	if (_streamed) {
		stopAnimation();
	} else if (_data->videoCanBePlayed()) {
		if (!videoAutoplayEnabled()) {
			history()->owner().checkPlayingAnimations();
		}
		if (!createStreamingObjects()) {
			_data->setVideoPlaybackFailed();
			return;
		}
	}
}

void Photo::checkStreamedIsStarted() const {
	if (!_streamed) {
		return;
	} else if (_streamed->instance.paused()) {
		_streamed->instance.resume();
	}
	if (_streamed
		&& !_streamed->instance.active()
		&& !_streamed->instance.failed()) {
		const auto position = _data->videoStartPosition();
		auto options = ::Media::Streaming::PlaybackOptions();
		options.position = position;
		options.mode = ::Media::Streaming::Mode::Video;
		options.loop = true;
		_streamed->instance.play(options);
	}
}

bool Photo::videoAutoplayEnabled() const {
	return Data::AutoDownload::ShouldAutoPlay(
		_data->session().settings().autoDownload(),
		_realParent->history()->peer,
		_data);
}

void Photo::hideSpoilers() {
	if (_spoiler) {
		_spoiler->revealed = false;
	}
}

bool Photo::needsBubble() const {
	if (_storyId) {
		return true;
	}
	const auto item = _parent->data();
	return !item->isService()
		&& (item->repliesAreComments()
			|| item->externalReply()
			|| item->viaBot()
			|| !item->emptyText()
			|| _parent->displayReply()
			|| _parent->displayForwardedFrom()
			|| _parent->displayFromName()
			|| _parent->displayedTopicButton());
}

QPoint Photo::resolveCustomInfoRightBottom() const {
	const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
	const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
	return QPoint(width() - skipx, height() - skipy);
}

bool Photo::isReadyForOpen() const {
	ensureDataMediaCreated();
	return _dataMedia->loaded();
}

void Photo::showPhoto(FullMsgId id) {
	_parent->delegate()->elementOpenPhoto(_data, id);
}

} // namespace HistoryView
