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

#include "boxes/premium_preview_box.h"
#include "data/components/sponsored_messages.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "menu/menu_sponsored.h"
#include "ui/effects/numbers_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/basic_click_handlers.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
#include "styles/style_chat.h"
#include "styles/style_media_view.h"

namespace Media::View {
namespace {

constexpr auto kStartDelayMin = crl::time(1000);
constexpr auto kDurationMin = 5 * crl::time(1000);

enum class Action {
	Close,
	PromotePremium,
	Pause,
	Unpause,
};

class Close final : public Ui::RippleButton {
public:
	Close(
		not_null<QWidget*> parent,
		const style::RippleAnimation &st,
		rpl::producer<crl::time> allowCloseAt);

	[[nodiscard]] rpl::producer<Action> actions() const;

private:
	QPoint prepareRippleStartPosition() const override;
	QImage prepareRippleMask() const override;
	void paintEvent(QPaintEvent *e) override;

	void updateProgress(crl::time now);

	rpl::event_stream<Action> _actions;

	Ui::NumbersAnimation _countdown;
	Ui::Animations::Basic _progress;
	base::Timer _noAnimationTimer;
	crl::time _allowCloseAt = 0;
	crl::time _startedAt = 0;
	crl::time _pausedAt = 0;
	int _secondsTill = 0;
	int _rippleSize = 0;
	QPoint _rippleOrigin;
	bool _allowClose = false;

};

Close::Close(
	not_null<QWidget*> parent,
	const style::RippleAnimation &st,
	rpl::producer<crl::time> allowCloseAt)
: RippleButton(parent, st)
, _countdown(st::mediaSponsoredCloseFont, [=] { update(); })
, _progress([=](crl::time now) { updateProgress(now); })
, _noAnimationTimer([=] { updateProgress(crl::now()); })
, _startedAt(crl::now()) {
	resize(st::mediaSponsoredCloseFull, st::mediaSponsoredCloseFull);

	const auto size = st::mediaSponsoredCloseRipple;
	const auto cut = int(base::SafeRound((width() - size) / 2.));
	_rippleSize = std::min(width() - 2 * cut, height() - 2 * cut);
	_rippleOrigin = QPoint(
		(width() - _rippleSize) / 2,
		(height() - _rippleSize) / 2);

	std::move(
		allowCloseAt
	) | rpl::on_next([=](crl::time at) {
		const auto now = crl::now();
		if (!at) {
			updateProgress(now);
			_pausedAt = now;
			_progress.stop();
		} else {
			if (_pausedAt) {
				_startedAt += now - base::take(_pausedAt);
			}
			_allowCloseAt = at;
			updateProgress(now);
			if (!anim::Disabled()) {
				_progress.start();
			} else if (!_allowClose) {
				_noAnimationTimer.callEach(crl::time(200));
			}
		}
	}, lifetime());
	updateProgress(_startedAt);

	setClickedCallback([=] {
		_actions.fire(_allowClose ? Action::Close : Action::PromotePremium);
	});
}

rpl::producer<Action> Close::actions() const {
	return _actions.events();
}

void Close::updateProgress(crl::time now) {
	update();
}

QPoint Close::prepareRippleStartPosition() const {
	return mapFromGlobal(QCursor::pos()) - _rippleOrigin;
}

QImage Close::prepareRippleMask() const {
	return Ui::RippleAnimation::EllipseMask({ _rippleSize, _rippleSize });
}

void Close::paintEvent(QPaintEvent *e) {
	auto p = QPainter(this);

	paintRipple(p, _rippleOrigin);

	const auto now = crl::now();
	if (!_pausedAt) {
		_allowClose = (now >= _allowCloseAt);
	}
	const auto msTill = _allowCloseAt - (_pausedAt ? _pausedAt : now);
	const auto msFull = _allowCloseAt - _startedAt;
	const auto secondsTill = (std::max(msTill, crl::time()) + 999) / 1000;
	const auto secondsFull = (std::max(msFull, crl::time()) + 999) / 1000;
	const auto allowCloseLeft = anim::Disabled()
		? (secondsFull ? (secondsTill / float64(secondsFull)) : 0)
		: std::max(msFull ? (msTill / float64(msFull)) : 0., 0.);
	const auto duration = crl::time(st::fadeWrapDuration);
	const auto allowedProgress = anim::Disabled()
		? (secondsTill ? 0. : 1.)
		: std::clamp(-msTill, crl::time(), duration) / float64(duration);

	if (_secondsTill != secondsTill) {
		const auto initial = !_secondsTill;
		_secondsTill = secondsTill;
		_countdown.setText(QString::number(_secondsTill), _secondsTill);
		if (initial) {
			_countdown.finishAnimating();
		}
	}

	auto pen = st::mediaviewTextLinkFg->p;
	if (allowedProgress < 1.) {
		if (allowedProgress > 0.) {
			p.setOpacity(1. - allowedProgress);
		}
		p.setPen(pen);

		const auto inner = QRect(
			(width() - st::mediaSponsoredCloseDiameter) / 2,
			(height() - st::mediaSponsoredCloseDiameter) / 2,
			st::mediaSponsoredCloseDiameter,
			st::mediaSponsoredCloseDiameter);
		_countdown.paint(
			p,
			inner.x() + (inner.width() - _countdown.countWidth()) / 2,
			(inner.y()
				+ (inner.height()
					- st::mediaSponsoredCloseFont->height) / 2),
			width());

		const auto skip = 0.23;
		const auto len = int(base::SafeRound(
			arc::kFullLength * (1. - skip) * allowCloseLeft));
		if (len > 0) {
			const auto from = arc::kFullLength / 4;
			auto hq = PainterHighQualityEnabler(p);
			pen.setWidthF(st::mediaSponsoredCloseStroke);
			pen.setCapStyle(Qt::RoundCap);
			p.setPen(pen);
			p.drawArc(inner, from, len);
		}

		p.setOpacity(1.);
	}

	const auto sizeFinal = st::mediaSponsoredCloseSize;
	const auto sizeSmall = st::mediaSponsoredCloseCorner;
	const auto twiceFinal = st::mediaSponsoredCloseTwice;
	const auto twiceSmall = st::mediaSponsoredCloseSmall;
	const auto size = sizeSmall + allowedProgress * (sizeFinal - sizeSmall);
	const auto twice = twiceSmall
		+ allowedProgress * (twiceFinal - twiceSmall);
	const auto leftFinal = (width() - size) / 2.;
	const auto leftSmall = (width() + st::mediaSponsoredCloseDiameter) / 2.
		- (st::mediaSponsoredCloseStroke / 2.)
		- sizeSmall;
	const auto topFinal = (height() - size) / 2.;
	const auto topSmall = (height() - st::mediaSponsoredCloseDiameter) / 2.;
	const auto left = leftSmall + allowedProgress * (leftFinal - leftSmall);
	const auto top = topSmall + allowedProgress * (topFinal - topSmall);

	auto hq = PainterHighQualityEnabler(p);
	pen.setWidthF(twice / 2.);
	p.setPen(pen);
	p.drawLine(QPointF(left, top), QPointF(left + size, top + size));
	p.drawLine(QPointF(left + size, top), QPointF(left, top + size));
}

[[nodiscard]] style::RoundButton PrepareAboutStyle() {
	static auto textBg = style::complex_color([] {
		auto result = st::mediaviewTextLinkFg->c;
		result.setAlphaF(result.alphaF() * 0.1);
		return result;
	});
	static auto textBgOver = style::complex_color([] {
		auto result = st::mediaviewTextLinkFg->c;
		result.setAlphaF(result.alphaF() * 0.15);
		return result;
	});
	static auto rippleColor = style::complex_color([] {
		auto result = st::mediaviewTextLinkFg->c;
		result.setAlphaF(result.alphaF() * 0.2);
		return result;
	});

	auto result = st::mediaSponsoredAbout;
	result.textFg = st::mediaviewTextLinkFg;
	result.textFgOver = st::mediaviewTextLinkFg;
	result.textBg = textBg.color();
	result.textBgOver = textBgOver.color();
	result.ripple.color = rippleColor.color();
	return result;
}

} // namespace

class PlaybackSponsored::Message final : public Ui::RpWidget {
public:
	Message(
		QWidget *parent,
		std::shared_ptr<ChatHelpers::Show> show,
		const Data::SponsoredMessage &data,
		rpl::producer<crl::time> allowCloseAt);

	[[nodiscard]] rpl::producer<Action> actions() const;

	void setFinalPosition(int x, int y);

	void fadeIn();
	void fadeOut(Fn<void()> hidden);

private:
	void paintEvent(QPaintEvent *e) override;
	void mouseMoveEvent(QMouseEvent *e) override;
	void mousePressEvent(QMouseEvent *e) override;
	void mouseReleaseEvent(QMouseEvent *e) override;

	int resizeGetHeight(int newWidth) override;

	void populate();
	void startFadeIn();
	void updateShown(Fn<void()> finished = nullptr);
	void startFade(Fn<void()> finished);

	const not_null<Main::Session*> _session;
	const std::shared_ptr<ChatHelpers::Show> _show;
	const Data::SponsoredMessage _data;

	style::RoundButton _aboutSt;
	std::unique_ptr<Ui::RoundButton> _about;
	std::unique_ptr<Close> _close;

	base::unique_qptr<Ui::PopupMenu> _menu;
	rpl::event_stream<Action> _actions;

	std::shared_ptr<Data::PhotoMedia> _photo;
	Ui::Text::String _title;
	Ui::Text::String _text;

	QPoint _finalPosition;
	int _left = 0;
	int _top = 0;
	int _titleHeight = 0;
	int _textHeight = 0;

	QImage _cache;
	Ui::Animations::Simple _showAnimation;
	bool _shown = false;
	bool _over = false;
	bool _pressed = false;

	rpl::lifetime _photoLifetime;

};

PlaybackSponsored::Message::Message(
	QWidget *parent,
	std::shared_ptr<ChatHelpers::Show> show,
	const Data::SponsoredMessage &data,
	rpl::producer<crl::time> allowCloseAt)
: RpWidget(parent)
, _session(&data.history->session())
, _show(std::move(show))
, _data(data)
, _aboutSt(PrepareAboutStyle())
, _about(std::make_unique<Ui::RoundButton>(
	this,
	tr::lng_search_sponsored_button(),
	_aboutSt))
, _close(
	std::make_unique<Close>(
		this,
		_aboutSt.ripple,
		std::move(allowCloseAt))) {
	setMouseTracking(true);
	populate();
	hide();
}

rpl::producer<Action> PlaybackSponsored::Message::actions() const {
	return rpl::merge(_actions.events(), _close->actions());
}

void PlaybackSponsored::Message::setFinalPosition(int x, int y) {
	_finalPosition = { x, y };
	if (_shown) {
		updateShown();
	}
}

void PlaybackSponsored::Message::fadeIn() {
	_shown = true;
	if (!_photo || _photo->loaded()) {
		startFadeIn();
		return;
	}
	_photo->owner()->session().downloaderTaskFinished(
	) | rpl::filter([=] {
		return _photo->loaded();
	}) | rpl::on_next([=] {
		_photoLifetime.destroy();
		startFadeIn();
	}, _photoLifetime);
}

void PlaybackSponsored::Message::startFadeIn() {
	if (!_shown) {
		return;
	}
	startFade([=] {
		_session->sponsoredMessages().view(_data.randomId);
	});
	show();
}

void PlaybackSponsored::Message::fadeOut(Fn<void()> hidden) {
	if (!_shown) {
		if (const auto onstack = hidden) {
			onstack();
		}
		return;
	}
	_shown = false;
	startFade(std::move(hidden));
}

void PlaybackSponsored::Message::startFade(Fn<void()> finished) {
	_cache = Ui::GrabWidgetToImage(this);
	_about->hide();
	_close->hide();
	const auto from = _shown ? 0. : 1.;
	const auto till = _shown ? 1. : 0.;
	_showAnimation.start([=] {
		updateShown(finished);
	}, from, till, st::fadeWrapDuration);
}

void PlaybackSponsored::Message::updateShown(Fn<void()> finished) {
	const auto shown = _showAnimation.value(_shown ? 1. : 0.);
	const auto shift = anim::interpolate(st::mediaSponsoredShift, 0, shown);
	move(_finalPosition.x(), _finalPosition.y() + shift);
	update();
	if (!_showAnimation.animating()) {
		_cache = QImage();
		_close->show();
		_about->show();
		if (const auto onstack = finished) {
			onstack();
		}
	}
}

void PlaybackSponsored::Message::paintEvent(QPaintEvent *e) {
	auto p = QPainter(this);

	const auto shown = _showAnimation.value(_shown ? 1. : 0.);
	if (!_cache.isNull()) {
		p.setOpacity(shown);
		p.drawImage(0, 0, _cache);
		return;
	}

	Ui::FillRoundRect(
		p,
		rect(),
		st::mediaviewSaveMsgBg,
		Ui::MediaviewSaveCorners);

	const auto &padding = st::mediaSponsoredPadding;
	if (_photo) {
		if (const auto image = _photo->image(Data::PhotoSize::Large)) {
			const auto size = st::mediaSponsoredThumb;
			const auto x = padding.left();
			const auto y = (height() - size) / 2;
			p.drawPixmap(
				x,
				y,
				image->pixSingle(
					size,
					size,
					{ .options = Images::Option::RoundCircle }));
		}
	}

	p.setPen(st::mediaviewControlFg);

	_title.draw(p, {
		.position = { _left, _top },
		.availableWidth = _about->x() - _left,
		.palette = &st::mediaviewTextPalette,
	});

	_text.draw(p, {
		.position = { _left, _top + _titleHeight },
		.availableWidth = _close->x() - _left,
		.palette = &st::mediaviewTextPalette,
	});
}

void PlaybackSponsored::Message::mouseMoveEvent(QMouseEvent *e) {
	const auto &padding = st::mediaSponsoredPadding;
	const auto point = e->pos();
	const auto about = _about->geometry();
	const auto close = _close->geometry();
	const auto over = !about.marginsAdded(padding).contains(point)
		&& !close.marginsAdded(padding).contains(point);
	if (_over != over) {
		_over = over;
		setCursor(_over ? style::cur_pointer : style::cur_default);
	}
}

void PlaybackSponsored::Message::mousePressEvent(QMouseEvent *e) {
	if (_over) {
		_pressed = true;
	}
}

void PlaybackSponsored::Message::mouseReleaseEvent(QMouseEvent *e) {
	if (base::take(_pressed) && _over) {
		_session->sponsoredMessages().clicked(_data.randomId, false, false);
		UrlClickHandler::Open(_data.link);
	}
}

int PlaybackSponsored::Message::resizeGetHeight(int newWidth) {
	const auto &padding = st::mediaSponsoredPadding;
	const auto userpic = st::mediaSponsoredThumb;
	_left = padding.left() + (_photo ? (userpic + padding.left()) : 0);
	const auto innerWidth = newWidth - _left - _close->width();
	const auto titleWidth = innerWidth - _about->width() - padding.right();
	_titleHeight = _title.countHeight(titleWidth);
	_textHeight = _text.countHeight(innerWidth);

	const auto use = std::max(_titleHeight + _textHeight, userpic);

	const auto height = padding.top() + use + padding.bottom();
	_top = padding.top() + (use - _titleHeight - _textHeight) / 2;

	_about->move(
		_left + std::min(titleWidth, _title.maxWidth()) + padding.right(),
		_top);
	_close->move(
		newWidth - _close->width(),
		(height - _close->height()) / 2);

	return height;
}

void PlaybackSponsored::Message::populate() {
	const auto &from = _data.from;
	const auto photo = from.photoId
		? _data.history->owner().photo(from.photoId).get()
		: nullptr;
	if (photo) {
		_photo = photo->createMediaView();
		photo->load({}, LoadFromCloudOrLocal, true);
	}
	_title = Ui::Text::String(
		st::semiboldTextStyle,
		from.title,
		kDefaultTextOptions,
		st::msgMinWidth);
	_text = Ui::Text::String(
		st::defaultTextStyle,
		_data.textWithEntities,
		kMarkupTextOptions,
		st::msgMinWidth);

	_about->setClickedCallback([=] {
		_menu = nullptr;
		const auto parent = parentWidget();
		_menu = base::make_unique_q<Ui::PopupMenu>(
			parent,
			st::mediaviewPopupMenu);
		const auto raw = _menu.get();
		const auto addAction = Ui::Menu::CreateAddActionCallback(raw);
		Menu::FillSponsored(
			addAction,
			_show,
			Menu::SponsoredPhrases::Channel,
			_session->sponsoredMessages().lookupDetails(_data),
			_session->sponsoredMessages().createReportCallback(
				_data.randomId,
				crl::guard(this, [=] { _actions.fire(Action::Close); })),
			{ .dark = true });
		_actions.fire(Action::Pause);
		Ui::Connect(raw, &QObject::destroyed, this, [=] {
			_actions.fire(Action::Unpause);
		});
		raw->popup(QCursor::pos());
	});
}

PlaybackSponsored::PlaybackSponsored(
	not_null<Ui::RpWidget*> controls,
	std::shared_ptr<ChatHelpers::Show> show,
	not_null<HistoryItem*> item)
: _parent(controls->parentWidget())
, _session(&item->history()->session())
, _show(std::move(show))
, _itemId(item->fullId())
, _controlsGeometry(controls->geometryValue())
, _timer([=] { update(); }) {
	_session->sponsoredMessages().requestForVideo(item, crl::guard(this, [=](
			Data::SponsoredForVideo data) {
		if (data.list.empty()) {
			return;
		}
		_data = std::move(data);
		if (_data->state.initial()
			|| (_data->state.itemIndex > _data->list.size())
			|| (_data->state.itemIndex == _data->list.size()
				&& _data->state.leftTillShow <= 0)) {
			_data->state.itemIndex = 0;
			_data->state.leftTillShow = std::max(
				_data->startDelay,
				kStartDelayMin);
		}
		update();
	}));
}

PlaybackSponsored::~PlaybackSponsored() {
	saveState();
}

void PlaybackSponsored::start() {
	_started = true;
	if (!_paused) {
		_start = crl::now();
		update();
	}
}

void PlaybackSponsored::setPaused(bool paused) {
	setPausedOutside(paused);
}

void PlaybackSponsored::updatePaused() {
	const auto paused = _pausedInside || _pausedOutside;
	if (_paused == paused) {
		return;
	} else if (_started && paused) {
		update();
	}
	_paused = paused;
	if (!_started) {
		return;
	} else if (_paused) {
		_start = 0;
		_timer.cancel();
		_allowCloseAt = 0;
	} else {
		_start = crl::now();
		update();
	}
}

void PlaybackSponsored::setPausedInside(bool paused) {
	if (_pausedInside == paused) {
		return;
	}
	_pausedInside = paused;
	updatePaused();
}

void PlaybackSponsored::setPausedOutside(bool paused) {
	if (_pausedOutside == paused) {
		return;
	}
	_pausedOutside = paused;
	updatePaused();
}

void PlaybackSponsored::finish() {
	_timer.cancel();
	if (_data) {
		saveState();
		_data = std::nullopt;
	}
}

void PlaybackSponsored::update() {
	if (!_data || !_start) {
		return;
	}

	const auto [now, state] = computeState();
	const auto message = (_data->state.itemIndex < _data->list.size())
		? &_data->list[state.itemIndex]
		: nullptr;
	const auto duration = message
		? std::max(
			message->durationMin + kDurationMin,
			message->durationMax)
		: crl::time(0);
	if (_data->state.leftTillShow > 0 && state.leftTillShow <= 0) {
		_data->state.leftTillShow = 0;
		if (duration) {
			_allowCloseAt = now + message->durationMin;
			show(*message);

			_start = now;
			_timer.callOnce(duration);
			saveState();
		} else {
			finish();
		}
	} else if (_data->state.leftTillShow <= 0
		&& state.leftTillShow <= -duration) {
		hide(now);
	} else {
		if (state.leftTillShow <= 0 && duration) {
			_allowCloseAt = now + state.leftTillShow + message->durationMin;
			if (!_widget) {
				show(*message);
			}
		}
		_data->state = state;
		_timer.callOnce((state.leftTillShow > 0)
			? state.leftTillShow
			: (state.leftTillShow + duration));
	}
}


void PlaybackSponsored::show(const Data::SponsoredMessage &data) {
	_widget = std::make_unique<Message>(
		_parent,
		_show,
		data,
		_allowCloseAt.value());
	const auto raw = _widget.get();

	_controlsGeometry.value() | rpl::on_next([=](QRect controls) {
		raw->resizeToWidth(controls.width());
		raw->setFinalPosition(
			controls.x(),
			controls.y() - st::mediaSponsoredSkip - raw->height());
	}, raw->lifetime());

	raw->actions() | rpl::on_next([=](Action action) {
		switch (action) {
		case Action::Close: hide(crl::now()); break;
		case Action::PromotePremium: showPremiumPromo(); break;
		case Action::Pause: setPausedInside(true); break;
		case Action::Unpause: setPausedInside(false); break;
		}
	}, raw->lifetime());

	raw->fadeIn();
}

void PlaybackSponsored::showPremiumPromo() {
	ShowPremiumPreviewBox(_show, PremiumFeature::NoAds);
}

void PlaybackSponsored::hide(crl::time now) {
	Expects(_widget != nullptr);

	_widget->fadeOut([this, raw = _widget.get()] {
		if (_widget.get() == raw) {
			_widget = nullptr;
		}
	});

	++_data->state.itemIndex;
	_data->state.leftTillShow = std::max(
		_data->betweenDelay,
		kStartDelayMin);
	_start = now;
	_timer.callOnce(_data->state.leftTillShow);
	saveState();
}

void PlaybackSponsored::saveState() {
	_session->sponsoredMessages().updateForVideo(
		_itemId,
		computeState().data);
}

PlaybackSponsored::State PlaybackSponsored::computeState() const {
	auto result = State{ crl::now() };
	if (!_data) {
		return result;
	}
	result.data = _data->state;
	if (!_start) {
		return result;
	}
	const auto elapsed = result.now - _start;
	result.data.leftTillShow -= elapsed;
	return result;
}

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

bool PlaybackSponsored::Has(HistoryItem *item) {
	return item
		&& item->history()->session().sponsoredMessages().canHaveFor(item);
}

} // namespace Media::View
