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

#include "chat_helpers/stickers_lottie.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "data/data_file_origin.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_document_media.h"
#include "data/data_streaming.h"
#include "data/data_peer_values.h"
#include "data/data_premium_limits.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "main/main_domain.h" // kMaxAccounts
#include "ui/chat/chat_theme.h"
#include "ui/chat/chat_style.h"
#include "ui/layers/generic_box.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/gradient.h"
#include "ui/text/text.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/gradient_round_button.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/boxes/confirm_box.h"
#include "ui/painter.h"
#include "settings/settings_premium.h"
#include "lottie/lottie_single_player.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/history_view_element.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h"
#include "window/window_session_controller.h"
#include "api/api_premium.h"
#include "apiwrap.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
#include "styles/style_settings.h"

#include <QSvgRenderer>

namespace {

constexpr auto kPremiumShift = 21. / 240;
constexpr auto kReactionsPerRow = 5;
constexpr auto kDisabledOpacity = 0.5;
constexpr auto kPreviewsCount = int(PremiumPreview::kCount);
constexpr auto kToggleStickerTimeout = 2 * crl::time(1000);
constexpr auto kStarOpacityOff = 0.1;
constexpr auto kStarOpacityOn = 1.;
constexpr auto kStarPeriod = 3 * crl::time(1000);

using Data::ReactionId;

struct Descriptor {
	PremiumPreview section = PremiumPreview::Stickers;
	DocumentData *requestedSticker = nullptr;
	bool fromSettings = false;
	Fn<void()> hiddenCallback;
	Fn<void(not_null<Ui::BoxContent*>)> shownCallback;
};

bool operator==(const Descriptor &a, const Descriptor &b) {
	return (a.section == b.section)
		&& (a.requestedSticker == b.requestedSticker)
		&& (a.fromSettings == b.fromSettings);
}

struct Preload {
	Descriptor descriptor;
	std::shared_ptr<Data::DocumentMedia> media;
	base::weak_ptr<Window::SessionController> controller;
};

[[nodiscard]] std::vector<Preload> &Preloads() {
	static auto result = std::vector<Preload>();
	return result;
}

void PreloadSticker(const std::shared_ptr<Data::DocumentMedia> &media) {
	const auto origin = media->owner()->stickerSetOrigin();
	media->automaticLoad(origin, nullptr);
	media->videoThumbnailWanted(origin);
}

[[nodiscard]] rpl::producer<QString> SectionTitle(PremiumPreview section) {
	switch (section) {
	case PremiumPreview::MoreUpload:
		return tr::lng_premium_summary_subtitle_more_upload();
	case PremiumPreview::FasterDownload:
		return tr::lng_premium_summary_subtitle_faster_download();
	case PremiumPreview::VoiceToText:
		return tr::lng_premium_summary_subtitle_voice_to_text();
	case PremiumPreview::NoAds:
		return tr::lng_premium_summary_subtitle_no_ads();
	case PremiumPreview::EmojiStatus:
		return tr::lng_premium_summary_subtitle_emoji_status();
	case PremiumPreview::InfiniteReactions:
		return tr::lng_premium_summary_subtitle_infinite_reactions();
	case PremiumPreview::Stickers:
		return tr::lng_premium_summary_subtitle_premium_stickers();
	case PremiumPreview::AnimatedEmoji:
		return tr::lng_premium_summary_subtitle_animated_emoji();
	case PremiumPreview::AdvancedChatManagement:
		return tr::lng_premium_summary_subtitle_advanced_chat_management();
	case PremiumPreview::ProfileBadge:
		return tr::lng_premium_summary_subtitle_profile_badge();
	case PremiumPreview::AnimatedUserpics:
		return tr::lng_premium_summary_subtitle_animated_userpics();
	}
	Unexpected("PremiumPreview in SectionTitle.");
}

[[nodiscard]] rpl::producer<QString> SectionAbout(PremiumPreview section) {
	switch (section) {
	case PremiumPreview::MoreUpload:
		return tr::lng_premium_summary_about_more_upload();
	case PremiumPreview::FasterDownload:
		return tr::lng_premium_summary_about_faster_download();
	case PremiumPreview::VoiceToText:
		return tr::lng_premium_summary_about_voice_to_text();
	case PremiumPreview::NoAds:
		return tr::lng_premium_summary_about_no_ads();
	case PremiumPreview::EmojiStatus:
		return tr::lng_premium_summary_about_emoji_status();
	case PremiumPreview::InfiniteReactions:
		return tr::lng_premium_summary_about_infinite_reactions();
	case PremiumPreview::Stickers:
		return tr::lng_premium_summary_about_premium_stickers();
	case PremiumPreview::AnimatedEmoji:
		return tr::lng_premium_summary_about_animated_emoji();
	case PremiumPreview::AdvancedChatManagement:
		return tr::lng_premium_summary_about_advanced_chat_management();
	case PremiumPreview::ProfileBadge:
		return tr::lng_premium_summary_about_profile_badge();
	case PremiumPreview::AnimatedUserpics:
		return tr::lng_premium_summary_about_animated_userpics();
	}
	Unexpected("PremiumPreview in SectionTitle.");
}

[[nodiscard]] object_ptr<Ui::RpWidget> ChatBackPreview(
		QWidget *parent,
		int height,
		const QImage &back) {
	auto result = object_ptr<Ui::FixedHeightWidget>(parent, height);
	const auto raw = result.data();

	raw->paintRequest(
	) | rpl::start_with_next([=] {
		auto p = QPainter(raw);
		p.drawImage(0, 0, back);
	}, raw->lifetime());

	return result;
}

[[nodiscard]] not_null<Ui::RpWidget*> StickerPreview(
		not_null<Ui::RpWidget*> parent,
		not_null<Window::SessionController*> controller,
		const std::shared_ptr<Data::DocumentMedia> &media,
		Fn<void()> readyCallback = nullptr) {
	using namespace HistoryView;

	PreloadSticker(media);

	const auto document = media->owner();
	const auto lottieSize = Sticker::Size(document);
	const auto effectSize = Sticker::PremiumEffectSize(document);
	const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
	result->show();

	parent->sizeValue(
	) | rpl::start_with_next([=](QSize size) {
		result->setGeometry(QRect(
			QPoint(
				(size.width() - effectSize.width()) / 2,
				(size.height() - effectSize.height()) / 2),
			effectSize));
	}, result->lifetime());
	auto &lifetime = result->lifetime();

	struct State {
		std::unique_ptr<Lottie::SinglePlayer> lottie;
		std::unique_ptr<Lottie::SinglePlayer> effect;
		std::unique_ptr<Ui::PathShiftGradient> pathGradient;
		bool readyInvoked = false;
	};
	const auto state = lifetime.make_state<State>();
	const auto createLottieIfReady = [=] {
		if (state->lottie) {
			return;
		}
		const auto document = media->owner();
		const auto sticker = document->sticker();
		if (!sticker || !sticker->isLottie() || !media->loaded()) {
			return;
		} else if (media->videoThumbnailContent().isEmpty()) {
			return;
		}

		const auto factor = style::DevicePixelRatio();
		state->lottie = ChatHelpers::LottiePlayerFromDocument(
			media.get(),
			nullptr,
			ChatHelpers::StickerLottieSize::MessageHistory,
			lottieSize * factor,
			Lottie::Quality::High);
		state->effect = document->session().emojiStickersPack().effectPlayer(
			document,
			media->videoThumbnailContent(),
			QString(),
			true);

		const auto update = [=] {
			if (!state->readyInvoked
				&& readyCallback
				&& state->lottie->ready()
				&& state->effect->ready()) {
				state->readyInvoked = true;
				readyCallback();
			}
			result->update();
		};
		auto &lifetime = result->lifetime();
		state->lottie->updates() | rpl::start_with_next(update, lifetime);
		state->effect->updates() | rpl::start_with_next(update, lifetime);
	};
	createLottieIfReady();
	if (!state->lottie || !state->effect) {
		controller->session().downloaderTaskFinished(
		) | rpl::take_while([=] {
			createLottieIfReady();
			return !state->lottie || !state->effect;
		}) | rpl::start(result->lifetime());
	}
	state->pathGradient = MakePathShiftGradient(
		controller->chatStyle(),
		[=] { result->update(); });

	result->paintRequest(
	) | rpl::start_with_next([=] {
		createLottieIfReady();

		auto p = QPainter(result);

		const auto left = effectSize.width()
			- int(lottieSize.width() * (1. + kPremiumShift));
		const auto top = (effectSize.height() - lottieSize.height()) / 2;
		const auto r = QRect(QPoint(left, top), lottieSize);
		if (!state->lottie
			|| !state->lottie->ready()
			|| !state->effect->ready()) {
			p.setBrush(controller->chatStyle()->msgServiceBg());
			ChatHelpers::PaintStickerThumbnailPath(
				p,
				media.get(),
				r,
				state->pathGradient.get());
			return;
		}

		const auto factor = style::DevicePixelRatio();
		const auto frame = state->lottie->frameInfo({ lottieSize * factor });
		const auto effect = state->effect->frameInfo(
			{ effectSize * factor });
		//const auto framesCount = !frame.image.isNull()
		//	? state->lottie->framesCount()
		//	: 1;
		//const auto effectsCount = !effect.image.isNull()
		//	? state->effect->framesCount()
		//	: 1;

		p.drawImage(r, frame.image);
		p.drawImage(
			QRect(QPoint(), effect.image.size() / factor),
			effect.image);

		if (!frame.image.isNull()/*
			&& ((frame.index % effectsCount) <= effect.index)*/) {
			state->lottie->markFrameShown();
		}
		if (!effect.image.isNull()/*
			&& ((effect.index % framesCount) <= frame.index)*/) {
			state->effect->markFrameShown();
		}
	}, lifetime);

	return result;
}

[[nodiscard]] not_null<Ui::RpWidget*> StickersPreview(
		not_null<Ui::RpWidget*> parent,
		not_null<Window::SessionController*> controller,
		Fn<void()> readyCallback) {
	const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
	result->show();

	parent->sizeValue(
	) | rpl::start_with_next([=](QSize size) {
		result->setGeometry(QRect(QPoint(), size));
	}, result->lifetime());
	auto &lifetime = result->lifetime();

	struct State {
		std::vector<std::shared_ptr<Data::DocumentMedia>> medias;
		Ui::RpWidget *previous = nullptr;
		Ui::RpWidget *current = nullptr;
		Ui::RpWidget *next = nullptr;
		Ui::Animations::Simple slide;
		base::Timer toggleTimer;
		bool toggleTimerPending = false;
		Fn<void()> singleReadyCallback;
		bool readyInvoked = false;
		bool timerFired = false;
		bool nextReady = false;
		int index = 0;
	};
	const auto premium = &controller->session().api().premium();
	const auto state = lifetime.make_state<State>();
	const auto create = [=](std::shared_ptr<Data::DocumentMedia> media) {
		const auto outer = Ui::CreateChild<Ui::RpWidget>(result);
		outer->show();

		result->sizeValue(
		) | rpl::start_with_next([=](QSize size) {
			outer->resize(size);
		}, outer->lifetime());

		[[maybe_unused]] const auto sticker = StickerPreview(
			outer,
			controller,
			media,
			state->singleReadyCallback);

		return outer;
	};
	const auto createNext = [=] {
		state->nextReady = false;
		state->next = create(state->medias[state->index]);
		state->next->move(0, state->current->height());
	};
	const auto check = [=] {
		if (!state->timerFired || !state->nextReady) {
			return;
		}
		const auto animationCallback = [=] {
			const auto top = int(base::SafeRound(state->slide.value(0.)));
			state->previous->move(0, top - state->current->height());
			state->current->move(0, top);
			if (!state->slide.animating()) {
				delete base::take(state->previous);
				state->timerFired = false;
				state->toggleTimer.callOnce(kToggleStickerTimeout);
			}
		};
		state->timerFired = false;
		++state->index;
		state->index %= state->medias.size();
		delete std::exchange(state->previous, state->current);
		state->current = state->next;
		createNext();
		state->slide.stop();
		state->slide.start(
			animationCallback,
			state->current->height(),
			0,
			st::premiumSlideDuration,
			anim::sineInOut);
	};
	state->toggleTimer.setCallback([=] {
		state->timerFired = true;
		check();
	});
	state->singleReadyCallback = [=] {
		if (!state->readyInvoked && readyCallback) {
			state->readyInvoked = true;
			readyCallback();
		}
		if (!state->next) {
			createNext();
			if (result->isHidden()) {
				state->toggleTimerPending = true;
			} else {
				state->toggleTimer.callOnce(kToggleStickerTimeout);
			}
		} else {
			state->nextReady = true;
			check();
		}
	};

	result->shownValue(
	) | rpl::filter([=](bool shown) {
		return shown && state->toggleTimerPending;
	}) | rpl::start_with_next([=] {
		state->toggleTimerPending = false;
		state->toggleTimer.callOnce(kToggleStickerTimeout);
	}, result->lifetime());

	const auto fill = [=] {
		const auto &list = premium->stickers();
		for (const auto &document : list) {
			state->medias.push_back(document->createMediaView());
		}
		if (!state->medias.empty()) {
			state->current = create(state->medias.front());
			state->index = 1 % state->medias.size();
			state->current->move(0, 0);
		}
	};

	fill();
	if (state->medias.empty()) {
		premium->stickersUpdated(
		) | rpl::take(1) | rpl::start_with_next(fill, lifetime);
	}

	return result;
}

struct VideoPreviewDocument {
	DocumentData *document = nullptr;
	RectPart align = RectPart::Bottom;
};

[[nodiscard]] bool VideoAlignToTop(PremiumPreview section) {
	return (section == PremiumPreview::MoreUpload)
		|| (section == PremiumPreview::NoAds)
		|| (section == PremiumPreview::AnimatedEmoji);
}

[[nodiscard]] DocumentData *LookupVideo(
		not_null<Main::Session*> session,
		PremiumPreview section) {
	const auto name = [&] {
		switch (section) {
		case PremiumPreview::MoreUpload: return "more_upload";
		case PremiumPreview::FasterDownload: return "faster_download";
		case PremiumPreview::VoiceToText: return "voice_to_text";
		case PremiumPreview::NoAds: return "no_ads";
		case PremiumPreview::AnimatedEmoji: return "animated_emoji";
		case PremiumPreview::AdvancedChatManagement:
			return "advanced_chat_management";
		case PremiumPreview::EmojiStatus: return "emoji_status";
		case PremiumPreview::InfiniteReactions: return "infinite_reactions";
		case PremiumPreview::ProfileBadge: return "profile_badge";
		case PremiumPreview::AnimatedUserpics: return "animated_userpics";
		}
		return "";
	}();
	const auto &videos = session->api().premium().videos();
	const auto i = videos.find(name);
	return (i != end(videos)) ? i->second.get() : nullptr;
}

[[nodiscard]] QPainterPath GenerateFrame(
		int left,
		int top,
		int width,
		int height,
		bool alignToBottom) {
	const auto radius = style::ConvertScaleExact(20.);
	const auto thickness = style::ConvertScaleExact(6.);
	const auto skip = thickness / 2.;
	auto path = QPainterPath();
	if (alignToBottom) {
		path.moveTo(left - skip, top + height);
		path.lineTo(left - skip, top - skip + radius);
		path.arcTo(
			left - skip,
			top - skip,
			radius * 2,
			radius * 2,
			180,
			-90);
		path.lineTo(left + width + skip - radius, top - skip);
		path.arcTo(
			left + width + skip - 2 * radius,
			top - skip,
			radius * 2,
			radius * 2,
			90,
			-90);
		path.lineTo(left + width + skip, top + height);
	} else {
		path.moveTo(left - skip, top);
		path.lineTo(left - skip, top + height + skip - radius);
		path.arcTo(
			left - skip,
			top + height + skip - 2 * radius,
			radius * 2,
			radius * 2,
			180,
			90);
		path.lineTo(left + width + skip - radius, top + height + skip);
		path.arcTo(
			left + width + skip - 2 * radius,
			top + height + skip - 2 * radius,
			radius * 2,
			radius * 2,
			270,
			90);
		path.lineTo(left + width + skip, top);
	}
	return path;
}

[[nodiscard]] not_null<Ui::RpWidget*> VideoPreview(
		not_null<Ui::RpWidget*> parent,
		not_null<Window::SessionController*> controller,
		not_null<DocumentData*> document,
		bool alignToBottom,
		Fn<void()> readyCallback) {
	const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
	result->show();

	parent->sizeValue(
	) | rpl::start_with_next([=](QSize size) {
		result->setGeometry(parent->rect());
	}, result->lifetime());
	auto &lifetime = result->lifetime();

	auto shared = document->owner().streaming().sharedDocument(
		document,
		Data::FileOriginPremiumPreviews());
	if (!shared) {
		return result;
	}

	struct State {
		State(
			std::shared_ptr<Media::Streaming::Document> shared,
			Fn<void()> waitingCallback)
		: instance(shared, std::move(waitingCallback))
		, star(u":/gui/icons/settings/star.svg"_q) {
		}
		QImage blurred;
		Media::Streaming::Instance instance;
		std::shared_ptr<Data::DocumentMedia> media;
		Ui::Animations::Basic loading;
		QPainterPath frame;
		QSvgRenderer star;
		bool readyInvoked = false;
	};
	const auto state = lifetime.make_state<State>(std::move(shared), [] {});
	state->media = document->createMediaView();
	if (const auto image = state->media->thumbnailInline()) {
		if (image->width() > 0) {
			const auto width = st::premiumVideoWidth;
			const auto height = std::max(
				int(base::SafeRound(
					float64(width) * image->height() / image->width())),
				1);
			using Option = Images::Option;
			const auto corners = alignToBottom
				? (Option::RoundSkipBottomLeft
					| Option::RoundSkipBottomRight)
				: (Option::RoundSkipTopLeft
					| Option::RoundSkipTopRight);
			state->blurred = Images::Prepare(
				image->original(),
				QSize(width, height) * style::DevicePixelRatio(),
				{ .options = (Option::Blur | Option::RoundLarge | corners) });
		}
	}
	const auto width = st::premiumVideoWidth;
	const auto height = state->blurred.height()
		? (state->blurred.height() / state->blurred.devicePixelRatio())
		: width;
	const auto left = (st::boxWideWidth - width) / 2;
	const auto top = alignToBottom ? (st::premiumPreviewHeight - height) : 0;
	state->frame = GenerateFrame(left, top, width, height, alignToBottom);
	const auto check = [=] {
		if (state->instance.playerLocked()) {
			return;
		} else if (state->instance.paused()) {
			state->instance.resume();
		}
		if (!state->instance.active() && !state->instance.failed()) {
			auto options = Media::Streaming::PlaybackOptions();
			options.waitForMarkAsShown = true;
			options.mode = ::Media::Streaming::Mode::Video;
			options.loop = true;
			state->instance.play(options);
		}
	};
	state->instance.player().updates(
	) | rpl::start_with_next_error([=](Media::Streaming::Update &&update) {
		if (v::is<Media::Streaming::Information>(update.data)
			|| v::is<Media::Streaming::UpdateVideo>(update.data)) {
			if (!state->readyInvoked && readyCallback) {
				state->readyInvoked = true;
				readyCallback();
			}
			result->update();
		}
	}, [=](::Media::Streaming::Error &&error) {
		result->update();
	}, state->instance.lifetime());

	state->loading.init([=] {
		if (!anim::Disabled()) {
			result->update();
		}
	});

	result->paintRequest(
	) | rpl::start_with_next([=] {
		auto p = QPainter(result);
		const auto paintFrame = [&](QColor color, float64 thickness) {
			auto hq = PainterHighQualityEnabler(p);
			auto pen = QPen(color);
			pen.setWidthF(style::ConvertScaleExact(thickness));
			p.setPen(pen);
			p.setBrush(Qt::NoBrush);
			p.drawPath(state->frame);
		};

		check();
		const auto ready = state->instance.player().ready()
			&& !state->instance.player().videoSize().isEmpty();
		const auto size = QSize(width, height) * style::DevicePixelRatio();

		using namespace Images;
		auto rounding = CornersMaskRef(
			Images::CornersMask(ImageRoundRadius::Large));
		if (alignToBottom) {
			rounding.p[kBottomLeft] = rounding.p[kBottomRight] = nullptr;
		} else {
			rounding.p[kTopLeft] = rounding.p[kTopRight] = nullptr;
		}
		const auto frame = !ready
			? state->blurred
			: state->instance.frame({
				.resize = size,
				.outer = size,
				.rounding = rounding,
			});
		paintFrame(QColor(0, 0, 0, 128), 12.);
		p.drawImage(QRect(left, top, width, height), frame);
		paintFrame(Qt::black, 6.6);
		if (ready) {
			state->loading.stop();
			state->instance.markFrameShown();
		} else {
			if (!state->loading.animating()) {
				state->loading.start();
			}
			const auto progress = anim::Disabled()
				? 1.
				: ((crl::now() % kStarPeriod) / float64(kStarPeriod));
			const auto ratio = anim::Disabled()
				? 1.
				: (1. + cos(progress * 2 * M_PI)) / 2.;
			const auto opacity = kStarOpacityOff
				+ (kStarOpacityOn - kStarOpacityOff) * ratio;
			p.setOpacity(opacity);

			const auto starSize = st::premiumVideoStarSize;
			state->star.render(&p, QRectF(
				QPointF(
					left + (width - starSize.width()) / 2.,
					top + (height - starSize.height()) / 2.),
				starSize));
		}
	}, lifetime);

	return result;
}

[[nodiscard]] not_null<Ui::RpWidget*> GenericPreview(
		not_null<Ui::RpWidget*> parent,
		not_null<Window::SessionController*> controller,
		PremiumPreview section,
		Fn<void()> readyCallback) {
	const auto result = Ui::CreateChild<Ui::RpWidget>(parent.get());
	result->show();

	parent->sizeValue(
	) | rpl::start_with_next([=](QSize size) {
		result->setGeometry(QRect(QPoint(), size));
	}, result->lifetime());
	auto &lifetime = result->lifetime();

	struct State {
		std::vector<std::shared_ptr<Data::DocumentMedia>> medias;
		Ui::RpWidget *single = nullptr;
	};
	const auto session = &controller->session();
	const auto state = lifetime.make_state<State>();
	const auto create = [=] {
		const auto document = LookupVideo(session, section);
		if (!document) {
			return;
		}
		state->single = VideoPreview(
			result,
			controller,
			document,
			!VideoAlignToTop(section),
			readyCallback);
	};
	create();
	if (!state->single) {
		session->api().premium().videosUpdated(
		) | rpl::take(1) | rpl::start_with_next(create, lifetime);
	}

	return result;
}

[[nodiscard]] not_null<Ui::RpWidget*> GenerateDefaultPreview(
		not_null<Ui::RpWidget*> parent,
		not_null<Window::SessionController*> controller,
		PremiumPreview section,
		Fn<void()> readyCallback) {
	switch (section) {
	case PremiumPreview::Stickers:
		return StickersPreview(parent, controller, readyCallback);
	default:
		return GenericPreview(parent, controller, section, readyCallback);
	}
}

[[nodiscard]] object_ptr<Ui::GradientButton> CreateGradientButton(
		QWidget *parent,
		QGradientStops stops) {
	return object_ptr<Ui::GradientButton>(parent, std::move(stops));
}

[[nodiscard]] object_ptr<Ui::GradientButton> CreatePremiumButton(
		QWidget *parent) {
	return CreateGradientButton(parent, Ui::Premium::ButtonGradientStops());
}

[[nodiscard]] object_ptr<Ui::RpWidget> CreateSwitch(
		not_null<Ui::RpWidget*> parent,
		not_null<rpl::variable<PremiumPreview>*> selected) {
	const auto padding = st::premiumDotPadding;
	const auto width = padding.left() + st::premiumDot + padding.right();
	const auto height = padding.top() + st::premiumDot + padding.bottom();
	const auto stops = Ui::Premium::ButtonGradientStops();
	auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);
	const auto raw = result.data();
	for (auto i = 0; i != kPreviewsCount; ++i) {
		const auto section = PremiumPreview(i);
		const auto button = Ui::CreateChild<Ui::AbstractButton>(raw);
		parent->widthValue(
		) | rpl::start_with_next([=](int outer) {
			const auto full = width * kPreviewsCount;
			const auto left = (outer - full) / 2 + (i * width);
			button->setGeometry(left, 0, width, height);
		}, button->lifetime());
		button->setClickedCallback([=] {
			*selected = section;
		});
		button->paintRequest(
		) | rpl::start_with_next([=] {
			auto p = QPainter(button);
			auto hq = PainterHighQualityEnabler(p);
			p.setBrush((selected->current() == section)
				? anim::gradient_color_at(
					stops,
					float64(i) / (kPreviewsCount - 1))
				: st::windowBgRipple->c);
			p.setPen(Qt::NoPen);
			p.drawEllipse(
				button->rect().marginsRemoved(st::premiumDotPadding));
		}, button->lifetime());
		selected->changes(
		) | rpl::start_with_next([=] {
			button->update();
		}, button->lifetime());
	}
	return result;
}

void PreviewBox(
		not_null<Ui::GenericBox*> box,
		not_null<Window::SessionController*> controller,
		const Descriptor &descriptor,
		const std::shared_ptr<Data::DocumentMedia> &media,
		const QImage &back) {
	const auto single = st::boxWideWidth;
	const auto size = QSize(single, st::premiumPreviewHeight);
	box->setWidth(size.width());
	box->setNoContentMargin(true);

	const auto outer = box->addRow(
		ChatBackPreview(box, size.height(), back),
		{});

	struct Hiding {
		not_null<Ui::RpWidget*> widget;
		int leftFrom = 0;
		int leftTill = 0;
	};
	struct State {
		int leftFrom = 0;
		Ui::RpWidget *content = nullptr;
		Ui::RpWidget *stickersPreload = nullptr;
		bool stickersPreloadReady = false;
		bool preloadScheduled = false;
		bool showFinished = false;
		Ui::Animations::Simple animation;
		Fn<void()> preload;
		std::vector<Hiding> hiding;
		rpl::variable<PremiumPreview> selected;
	};
	const auto state = outer->lifetime().make_state<State>();
	state->selected = descriptor.section;

	const auto move = [=](int delta) {
		using Type = PremiumPreview;
		const auto count = int(Type::kCount);
		const auto now = state->selected.current();
		state->selected = Type((int(now) + count + delta) % count);
	};

	const auto buttonsParent = box->verticalLayout().get();
	const auto close = Ui::CreateChild<Ui::IconButton>(
		buttonsParent,
		st::settingsPremiumTopBarClose);
	close->setClickedCallback([=] { box->closeBox(); });

	const auto left = Ui::CreateChild<Ui::IconButton>(
		buttonsParent,
		st::settingsPremiumMoveLeft);
	left->setClickedCallback([=] { move(-1); });

	const auto right = Ui::CreateChild<Ui::IconButton>(
		buttonsParent,
		st::settingsPremiumMoveRight);
	right->setClickedCallback([=] { move(1); });

	buttonsParent->widthValue(
	) | rpl::start_with_next([=](int width) {
		const auto outerHeight = st::premiumPreviewHeight;
		close->moveToRight(0, 0, width);
		left->moveToLeft(0, (outerHeight - left->height()) / 2, width);
		right->moveToRight(0, (outerHeight - right->height()) / 2, width);
	}, close->lifetime());

	state->preload = [=] {
		if (!state->showFinished) {
			state->preloadScheduled = true;
			return;
		}
		const auto now = state->selected.current();
		if (now != PremiumPreview::Stickers && !state->stickersPreload) {
			const auto ready = [=] {
				if (state->stickersPreload) {
					state->stickersPreloadReady = true;
				} else {
					state->preload();
				}
			};
			state->stickersPreload = GenerateDefaultPreview(
				outer,
				controller,
				PremiumPreview::Stickers,
				ready);
			state->stickersPreload->hide();
		}
	};

	switch (descriptor.section) {
	case PremiumPreview::Stickers:
		state->content = media
			? StickerPreview(outer, controller, media, state->preload)
			: StickersPreview(outer, controller, state->preload);
		break;
	default:
		state->content = GenericPreview(
			outer,
			controller,
			descriptor.section,
			state->preload);
		break;
	}

	state->selected.value(
	) | rpl::combine_previous(
	) | rpl::start_with_next([=](PremiumPreview was, PremiumPreview now) {
		const auto animationCallback = [=] {
			if (!state->animation.animating()) {
				for (const auto &hiding : base::take(state->hiding)) {
					delete hiding.widget;
				}
				state->leftFrom = 0;
				state->content->move(0, 0);
			} else {
				const auto progress = state->animation.value(1.);
				state->content->move(
					anim::interpolate(state->leftFrom, 0, progress),
					0);
				for (const auto &hiding : state->hiding) {
					hiding.widget->move(anim::interpolate(
						hiding.leftFrom,
						hiding.leftTill,
						progress), 0);
				}
			}
		};
		animationCallback();
		const auto toLeft = int(now) > int(was);
		auto start = state->content->x() + (toLeft ? single : -single);
		for (const auto &hiding : state->hiding) {
			const auto left = hiding.widget->x();
			if (toLeft && left + single > start) {
				start = left + single;
			} else if (!toLeft && left - single < start) {
				start = left - single;
			}
		}
		for (auto &hiding : state->hiding) {
			hiding.leftFrom = hiding.widget->x();
			hiding.leftTill = hiding.leftFrom - start;
		}
		state->hiding.push_back({
			.widget = state->content,
			.leftFrom = state->content->x(),
			.leftTill = state->content->x() - start,
		});
		state->leftFrom = start;
		if (now == PremiumPreview::Stickers && state->stickersPreload) {
			state->content = base::take(state->stickersPreload);
			state->content->show();
			if (base::take(state->stickersPreloadReady)) {
				state->preload();
			}
		} else {
			state->content = GenerateDefaultPreview(
				outer,
				controller,
				now,
				state->preload);
		}
		state->animation.stop();
		state->animation.start(
			animationCallback,
			0.,
			1.,
			st::premiumSlideDuration,
			anim::sineInOut);
	}, outer->lifetime());

	auto title = state->selected.value(
	) | rpl::map([=](PremiumPreview section) {
		return SectionTitle(section);
	}) | rpl::flatten_latest();

	auto text = state->selected.value(
	) | rpl::map([=](PremiumPreview section) {
		return SectionAbout(section);
	}) | rpl::flatten_latest();

	const auto padding = st::premiumPreviewAboutPadding;
	const auto available = size.width() - padding.left() - padding.right();
	auto titleLabel = object_ptr<Ui::FlatLabel>(
		box,
		std::move(title),
		st::premiumPreviewAboutTitle);
	titleLabel->resizeToWidth(available);
	box->addRow(
		object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
			box,
			std::move(titleLabel)),
		st::premiumPreviewAboutTitlePadding);
	auto textLabel = object_ptr<Ui::FlatLabel>(
		box,
		std::move(text),
		st::premiumPreviewAbout);
	textLabel->resizeToWidth(available);
	box->addRow(
		object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(box, std::move(textLabel)),
		padding);
	box->addRow(
		CreateSwitch(box->verticalLayout(), &state->selected),
		st::premiumDotsMargin);
	const auto showFinished = [=] {
		state->showFinished = true;
		if (base::take(state->preloadScheduled)) {
			state->preload();
		}
	};
	if (descriptor.fromSettings && controller->session().premium()) {
		box->setShowFinishedCallback(showFinished);
		box->addButton(tr::lng_close(), [=] { box->closeBox(); });
	} else {
		box->setStyle(st::premiumPreviewBox);
		const auto buttonPadding = st::premiumPreviewBox.buttonPadding;
		const auto width = size.width()
			- buttonPadding.left()
			- buttonPadding.right();
		const auto computeRef = [=] {
			return Settings::LookupPremiumRef(state->selected.current());
		};
		auto unlock = state->selected.value(
		) | rpl::map([=](PremiumPreview section) {
			return (section == PremiumPreview::InfiniteReactions)
				? tr::lng_premium_unlock_reactions()
				: (section == PremiumPreview::Stickers)
				? tr::lng_premium_unlock_stickers()
				: (section == PremiumPreview::AnimatedEmoji)
				? tr::lng_premium_unlock_emoji()
				: (section == PremiumPreview::EmojiStatus)
				? tr::lng_premium_unlock_status()
				: tr::lng_premium_more_about();
		}) | rpl::flatten_latest();
		auto button = descriptor.fromSettings
			? object_ptr<Ui::GradientButton>::fromRaw(
				Settings::CreateSubscribeButton({
					controller,
					box,
					computeRef,
				}))
			: CreateUnlockButton(box, std::move(unlock));
		button->resizeToWidth(width);
		if (!descriptor.fromSettings) {
			button->setClickedCallback([=] {
				Settings::ShowPremium(
					controller,
					Settings::LookupPremiumRef(state->selected.current()));
			});
		}
		box->setShowFinishedCallback([=, raw = button.data()]{
			showFinished();
			raw->startGlareAnimation();
		});
		box->addButton(std::move(button));
	}

	if (descriptor.fromSettings) {
		Data::AmPremiumValue(
			&controller->session()
		) | rpl::skip(1) | rpl::start_with_next([=] {
			box->closeBox();
		}, box->lifetime());
	}

	box->events(
	) | rpl::start_with_next([=](not_null<QEvent*> e) {
		if (e->type() == QEvent::KeyPress) {
			const auto key = static_cast<QKeyEvent*>(e.get())->key();
			if (key == Qt::Key_Left) {
				move(-1);
			} else if (key == Qt::Key_Right) {
				move(1);
			}
		}
	}, box->lifetime());

	if (const auto &hidden = descriptor.hiddenCallback) {
		box->boxClosing() | rpl::start_with_next(hidden, box->lifetime());
	}
}

void Show(
		not_null<Window::SessionController*> controller,
		const Descriptor &descriptor,
		const std::shared_ptr<Data::DocumentMedia> &media,
		QImage back) {
	const auto box = controller->show(
		Box(PreviewBox, controller, descriptor, media, back));
	if (descriptor.shownCallback) {
		descriptor.shownCallback(box);
	}
}

void Show(not_null<Window::SessionController*> controller, QImage back) {
	auto &list = Preloads();
	for (auto i = begin(list); i != end(list);) {
		const auto already = i->controller.get();
		if (!already) {
			i = list.erase(i);
		} else if (already == controller) {
			Show(controller, i->descriptor, i->media, back);
			i = list.erase(i);
			return;
		} else {
			++i;
		}
	}
}

void Show(
		not_null<Window::SessionController*> controller,
		Descriptor &&descriptor) {
	if (!controller->session().premiumPossible()) {
		const auto box = controller->show(Box(PremiumUnavailableBox));
		if (descriptor.shownCallback) {
			descriptor.shownCallback(box);
		}
		return;
	}
	auto &list = Preloads();
	for (auto i = begin(list); i != end(list);) {
		const auto already = i->controller.get();
		if (!already) {
			i = list.erase(i);
		} else if (already == controller) {
			if (i->descriptor == descriptor) {
				return;
			}
			i->descriptor = descriptor;
			i->media = descriptor.requestedSticker
				? descriptor.requestedSticker->createMediaView()
				: nullptr;
			if (const auto &media = i->media) {
				PreloadSticker(media);
			}
			return;
		} else {
			++i;
		}
	}

	const auto weak = base::make_weak(controller);
	list.push_back({
		.descriptor = descriptor,
		.media = (descriptor.requestedSticker
			? descriptor.requestedSticker->createMediaView()
			: nullptr),
		.controller = weak,
	});
	if (const auto &media = list.back().media) {
		PreloadSticker(media);
	}

	const auto fill = QSize(st::boxWideWidth, st::boxWideWidth);
	const auto stops = Ui::Premium::LimitGradientStops();
	crl::async([=] {
		const auto factor = style::DevicePixelRatio();
		auto cropped = QImage(
			fill * factor,
			QImage::Format_ARGB32_Premultiplied);
		cropped.setDevicePixelRatio(factor);
		auto p = QPainter(&cropped);
		auto gradient = QLinearGradient(0, fill.height(), fill.width(), 0);
		gradient.setStops(stops);
		p.fillRect(QRect(QPoint(), fill), gradient);
		p.end();

		const auto result = Images::Round(
			std::move(cropped),
			Images::CornersMask(st::boxRadius),
			RectPart::TopLeft | RectPart::TopRight);
		crl::on_main([=] {
			if (const auto strong = weak.get()) {
				Show(strong, result);
			}
		});
	});
}

} // namespace

void ShowStickerPreviewBox(
		not_null<Window::SessionController*> controller,
		not_null<DocumentData*> document) {
	Show(controller, Descriptor{
		.section = PremiumPreview::Stickers,
		.requestedSticker = document,
	});
}

void ShowPremiumPreviewBox(
		not_null<Window::SessionController*> controller,
		PremiumPreview section,
		Fn<void(not_null<Ui::BoxContent*>)> shown) {
	Show(controller, Descriptor{
		.section = section,
		.shownCallback = std::move(shown),
	});
}

void ShowPremiumPreviewToBuy(
		not_null<Window::SessionController*> controller,
		PremiumPreview section,
		Fn<void()> hiddenCallback) {
	Show(controller, Descriptor{
		.section = section,
		.fromSettings = true,
		.hiddenCallback = std::move(hiddenCallback),
	});
}

void PremiumUnavailableBox(not_null<Ui::GenericBox*> box) {
	Ui::ConfirmBox(box, {
		.text = tr::lng_premium_unavailable(
			tr::now,
			Ui::Text::RichLangValue),
		.inform = true,
	});
}

void DoubledLimitsPreviewBox(
		not_null<Ui::GenericBox*> box,
		not_null<Main::Session*> session) {
	const auto limits = Data::PremiumLimits(session);
	auto entries = std::vector<Ui::Premium::ListEntry>();
	{
		const auto premium = limits.channelsPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_channels(),
			tr::lng_premium_double_limits_about_channels(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.channelsDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.dialogsPinnedPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_pins(),
			tr::lng_premium_double_limits_about_pins(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.dialogsPinnedDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.channelsPublicPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_links(),
			tr::lng_premium_double_limits_about_links(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.channelsPublicDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.gifsPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_gifs(),
			tr::lng_premium_double_limits_about_gifs(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.gifsDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.stickersFavedPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_stickers(),
			tr::lng_premium_double_limits_about_stickers(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.stickersFavedDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.aboutLengthPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_bio(),
			tr::lng_premium_double_limits_about_bio(
				Ui::Text::RichLangValue),
			limits.aboutLengthDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.captionLengthPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_captions(),
			tr::lng_premium_double_limits_about_captions(
				Ui::Text::RichLangValue),
			limits.captionLengthDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.dialogFiltersPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_folders(),
			tr::lng_premium_double_limits_about_folders(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.dialogFiltersDefault(),
			premium,
		});
	}
	{
		const auto premium = limits.dialogFiltersChatsPremium();
		entries.push_back(Ui::Premium::ListEntry{
			tr::lng_premium_double_limits_subtitle_folder_chats(),
			tr::lng_premium_double_limits_about_folder_chats(
				lt_count,
				rpl::single(float64(premium)),
				Ui::Text::RichLangValue),
			limits.dialogFiltersChatsDefault(),
			premium,
		});
	}
	const auto nextMax = session->domain().maxAccounts() + 1;
	const auto till = (nextMax >= Main::Domain::kPremiumMaxAccounts)
		? QString::number(Main::Domain::kPremiumMaxAccounts)
		: (QString::number(nextMax) + QChar('+'));
	entries.push_back(Ui::Premium::ListEntry{
		tr::lng_premium_double_limits_subtitle_accounts(),
		tr::lng_premium_double_limits_about_accounts(
			lt_count,
			rpl::single(float64(Main::Domain::kPremiumMaxAccounts)),
			Ui::Text::RichLangValue),
		Main::Domain::kMaxAccounts,
		Main::Domain::kPremiumMaxAccounts,
		till,
	});
	Ui::Premium::ShowListBox(box, std::move(entries));
}

object_ptr<Ui::GradientButton> CreateUnlockButton(
		QWidget *parent,
		rpl::producer<QString> text) {
	auto result = CreatePremiumButton(parent);
	const auto &st = st::premiumPreviewBox.button;
	result->resize(result->width(), st.height);

	const auto label = Ui::CreateChild<Ui::FlatLabel>(
		result.data(),
		std::move(text),
		st::premiumPreviewButtonLabel);
	label->setAttribute(Qt::WA_TransparentForMouseEvents);
	rpl::combine(
		result->widthValue(),
		label->widthValue()
	) | rpl::start_with_next([=](int outer, int width) {
		label->moveToLeft(
			(outer - width) / 2,
			st::premiumPreviewBox.button.textTop,
			outer);
	}, label->lifetime());

	return result;
}
