/*
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 "iv/markdown/iv_markdown_controller.h"
#include "base/event_filter.h"
#include "base/weak_ptr.h"
#include "core/click_handler_types.h"
#include "core/credits_amount.h"
#include "core/file_utilities.h"
#include "iv/markdown/iv_markdown_parse.h"
#include "iv/markdown/iv_markdown_view.h"
#include "iv/iv_delegate_impl.h"
#include "iv/iv_zoom_controls.h"
#include "lang/lang_keys.h"
#include "ui/layers/layer_manager.h"
#include "ui/layers/show.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/rp_window.h"
#include "ui/wrap/fade_wrap.h"
#include "logs.h"

#include "styles/palette.h"
#include "styles/style_iv.h"
#include "styles/style_menu_icons.h"
#include "styles/style_window.h"

#include <QtCore/QElapsedTimer>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QUrl>
#include <QtGui/QKeyEvent>
#include <QtGui/QKeySequence>
#include <QtGui/QPainter>

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

namespace Iv::Markdown {
namespace {

constexpr auto kZoomStep = int(10);

[[nodiscard]] OpenOptions PrepareOpenOptions(
		OpenOptions options,
		not_null<Delegate*> delegate,
		const QString &title) {
	options.delegate = delegate;
	Q_UNUSED(title);
	return options;
}

[[nodiscard]] ViewerKind ResolveViewerKind(const OpenOptions &options) {
	return (options.viewerKind != ViewerKind::Auto)
		? options.viewerKind
		: options.sourcePath.isEmpty()
		? ViewerKind::InstantView
		: ViewerKind::LocalFile;
}

[[nodiscard]] QString SubtitleText(
		const OpenOptions &options,
		const QString &title) {
	if (!options.sourceName.trimmed().isEmpty()) {
		return options.sourceName.trimmed();
	}
	const auto host = QUrl(options.sourceUrl).host().trimmed();
	return !host.isEmpty() ? host : title.trimmed();
}

[[nodiscard]] QString OpenSourceLabel(ViewerKind kind) {
	return (kind == ViewerKind::InstantView)
		? tr::lng_iv_open_in_browser(tr::now)
		: tr::lng_markdown_preview_open_file(tr::now);
}

[[nodiscard]] const style::icon *OpenSourceIcon(ViewerKind kind) {
	return (kind == ViewerKind::InstantView)
		? &st::menuIconIpAddress
		: &st::menuIconFile;
}

[[nodiscard]] QVariant ExtendClickHandlerContext(
		QVariant context,
		const std::shared_ptr<Ui::Show> &show) {
	if (!show) {
		return context;
	} else if (!context.isValid()
		|| context.canConvert<ClickHandlerContext>()) {
		auto clickContext = context.isValid()
			? context.value<ClickHandlerContext>()
			: ClickHandlerContext();
		clickContext.show = show;
		return QVariant::fromValue(clickContext);
	}
	return context;
}

[[nodiscard]] std::shared_ptr<QVariant> ResolveClickHandlerContextRef(
		const std::shared_ptr<QVariant> &current,
		const OpenOptions &options) {
	return options.clickHandlerContextRef
		? options.clickHandlerContextRef
		: (current ? current : std::make_shared<QVariant>());
}

void ProcessZoomShortcut(not_null<Delegate*> delegate, QKeyEvent *event) {
	if (!(event->modifiers() & Qt::ControlModifier)) {
		return;
	}
	if (event->key() == Qt::Key_Plus || event->key() == Qt::Key_Equal) {
		event->accept();
		delegate->ivSetZoom(delegate->ivZoom() + kZoomStep);
	} else if (event->key() == Qt::Key_Minus) {
		event->accept();
		delegate->ivSetZoom(delegate->ivZoom() - kZoomStep);
	} else if (event->key() == Qt::Key_0) {
		event->accept();
		delegate->ivSetZoom(0);
	}
}

struct OpenTarget {
	QString path;
	QString fragment;
};

struct PageHistoryTarget {
	uint64 pageId = 0;
	QString sourceUrl;
	QString hash;
};

[[nodiscard]] bool IsReadableLocalFile(const QFileInfo &info) {
	return info.exists() && info.isFile() && info.isReadable();
}

struct ReadSource {
	QString path;
	QString name;
	QByteArray bytes;

	explicit operator bool() const {
		return !path.isEmpty();
	}
};
[[nodiscard]] ReadSource ReadLocalSource(
		const QString &path,
		const MarkdownParseLimits &limits) {
	const auto info = QFileInfo(path);
	auto name = info.fileName();
	if (!IsReadableLocalFile(info) || !LooksLikeMarkdownFile(name)) {
		return {};
	} else if (info.size() > limits.maxSourceBytes) {
		DEBUG_LOG(("Native Markdown IV: rejected local file too large: %1"
			).arg(path));
		return {};
	}
	auto file = QFile(path);
	if (!file.open(QIODevice::ReadOnly)) {
		return {};
	}
	auto data = file.read(limits.maxSourceBytes);
	if (file.error() != QFileDevice::NoError || !file.atEnd()) {
		DEBUG_LOG(("Native Markdown IV: could not read local file: %1"
			).arg(path));
		return {};
	}
	return {
		.path = info.absoluteFilePath(),
		.name = std::move(name),
		.bytes = std::move(data),
	};
}

[[nodiscard]] QString NormalizeFragmentId(QString fragment) {
	fragment = QString::fromUtf8(
		QByteArray::fromPercentEncoding(fragment.toUtf8()));
	fragment = fragment.trimmed().toLower();
	while (fragment.startsWith(QChar('#'))) {
		fragment.remove(0, 1);
	}
	return fragment;
}

[[nodiscard]] PageHistoryTarget ParsePageHistoryTarget(
		uint64 pageId,
		QString url) {
	const auto hash = url.indexOf(QChar('#'));
	return (hash < 0)
		? PageHistoryTarget{
			.pageId = pageId,
			.sourceUrl = std::move(url),
		}
		: PageHistoryTarget{
			.pageId = pageId,
			.sourceUrl = url.mid(0, hash),
			.hash = NormalizeFragmentId(url.mid(hash + 1)),
		};
}

[[nodiscard]] QString ComposePageHistoryUrl(const PageHistoryTarget &target) {
	return target.hash.isEmpty()
		? target.sourceUrl
		: (target.sourceUrl + u"#"_q + target.hash);
}

[[nodiscard]] OpenTarget ParseOpenTarget(QString path) {
	const auto direct = QFileInfo(path);
	if (direct.exists()) {
		return { path, QString() };
	}
	const auto hash = path.lastIndexOf(QChar('#'));
	if (hash <= 0) {
		return { path, QString() };
	}
	const auto candidate = path.mid(0, hash);
	if (candidate.isEmpty()) {
		return { path, QString() };
	}
	const auto info = QFileInfo(candidate);
	return info.exists()
		? OpenTarget{ candidate, NormalizeFragmentId(path.mid(hash + 1)) }
		: OpenTarget{ path, QString() };
}

[[nodiscard]] bool HasPreviewableContent(const MarkdownNode &node) {
	switch (node.kind) {
	case NodeKind::Document:
	case NodeKind::Unsupported:
		break;
	default:
		return true;
	}
	return std::any_of(
		node.children.begin(),
		node.children.end(),
		[](const MarkdownNode &child) {
			return HasPreviewableContent(child);
		});
}

[[nodiscard]] bool AcceptsPreview(const PreparedDocument &document) {
	return HasPreviewableContent(document.document)
		|| !document.formulas.empty();
}

void LogDocumentWarnings(
		const PreparedDocument &document,
		const QString &path) {
	for (const auto &warning : document.warnings) {
		DEBUG_LOG(("Native Markdown IV: warning (%1): %2"
			).arg(warning
			).arg(path));
	}
}

} // namespace

Controller::Controller(
	not_null<Delegate*> delegate,
	PreparedDocument document,
	QString title,
	OpenOptions options)
: _delegate(delegate)
, _document(std::make_shared<PreparedDocument>(std::move(document)))
, _title(std::move(title))
, _renderer(nullptr)
, _options(PrepareOpenOptions(std::move(options), delegate, _title)) {
	_clickHandlerContextRef = ResolveClickHandlerContextRef(
		_clickHandlerContextRef,
		_options);
	_options.clickHandlerContextRef = _clickHandlerContextRef;
	createWindow();
}

Controller::Controller(
	not_null<Delegate*> delegate,
	MarkdownArticleContent content,
	QString title,
	std::shared_ptr<MathRenderer> renderer,
	OpenOptions options)
: _delegate(delegate)
, _preparedContent(std::move(content))
, _title(std::move(title))
, _renderer(renderer ? std::move(renderer) : std::make_shared<MathRenderer>())
, _options(PrepareOpenOptions(std::move(options), delegate, _title)) {
	_clickHandlerContextRef = ResolveClickHandlerContextRef(
		_clickHandlerContextRef,
		_options);
	_options.clickHandlerContextRef = _clickHandlerContextRef;
	createWindow();
}

Controller::~Controller() = default;

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

void Controller::activate() {
	if (_window->isMinimized()) {
		_window->showNormal();
	} else if (_window->isHidden()) {
		_window->show();
	}
	_window->raise();
	_window->activateWindow();
	_window->setFocus();
	if (_preview) {
		_preview->setFocus();
	}
}

void Controller::update(
		MarkdownArticleContent content,
		QString title,
		OpenOptions options) {
	setContent(
		std::move(content),
		std::move(title),
		std::move(options),
		true);
}

void Controller::show(
		MarkdownArticleContent content,
		QString title,
		OpenOptions options) {
	setContent(
		std::move(content),
		std::move(title),
		std::move(options),
		false);
	activate();
}

void Controller::setContent(
		MarkdownArticleContent content,
		QString title,
		OpenOptions options,
		bool preserveScroll) {
	const auto scrollTop = (preserveScroll && _preview)
		? MarkdownPreviewScrollTop(_preview.get())
		: 0;
	if (preserveScroll) {
		saveCurrentHistoryScroll(scrollTop);
	}
	auto refreshed = PrepareOpenOptions(std::move(options), _delegate, title);
	if (preserveScroll && historyEnabled(refreshed)) {
		auto activeIndex = -1;
		if (_shownHistoryIndex >= 0
			&& _shownHistoryIndex < int(_history.size())) {
			activeIndex = _shownHistoryIndex;
		} else if (_historyIndex >= 0
			&& _historyIndex < int(_history.size())) {
			activeIndex = _historyIndex;
		}
		if (activeIndex >= 0
			&& sameHistoryPage(
				_history[activeIndex],
				refreshed.currentPageId,
				refreshed.sourceUrl)) {
			refreshed.initialFragment = _history[activeIndex].hash;
		}
	}
	if (preserveScroll
		&& _preview
		&& historyEnabled(refreshed)) {
		auto activeIndex = -1;
		if (_shownHistoryIndex >= 0
			&& _shownHistoryIndex < int(_history.size())) {
			activeIndex = _shownHistoryIndex;
		} else if (_historyIndex >= 0
			&& _historyIndex < int(_history.size())) {
			activeIndex = _historyIndex;
		}
		if (activeIndex >= 0
			&& sameHistoryLocation(
				_history[activeIndex],
				refreshed.currentPageId,
				refreshed.sourceUrl,
				refreshed.initialFragment)) {
			_title = std::move(title);
			if (_menu) {
				_menu = nullptr;
				_menuToggle->setForceRippled(false);
			}
			const auto updated = updateExistingPreview(
				std::move(content),
				std::move(refreshed),
				scrollTop);
			Assert(updated);
			if (updated) {
				refreshTitle();
				if (_window && _window->isActiveWindow() && _preview) {
					_preview->setFocus();
				}
				updateHistoryButtons();
			}
			return;
		}
	}
	if (historyEnabled(refreshed)
		&& _shownHistoryIndex >= 0
		&& _shownHistoryIndex < int(_history.size())
		&& !sameHistoryPage(
			_history[_shownHistoryIndex],
			refreshed.currentPageId,
			refreshed.sourceUrl)) {
		saveCurrentHistoryScroll();
	}
	if (historyEnabled(refreshed)) {
		auto index = findHistoryEntry(
			refreshed.currentPageId,
			refreshed.sourceUrl,
			refreshed.initialFragment);
		if (index < 0) {
			if (_historyIndex < 0) {
				_history.push_back({});
				index = 0;
			} else {
				_history.resize(_historyIndex + 1);
				index = int(_history.size());
				_history.push_back({});
			}
			_historyIndex = index;
		} else if (index != _historyIndex) {
			updateCurrentHistoryEntry(content, title, refreshed);
			updateHistoryButtons();
			return;
		}
	}
	_preparedContent = std::move(content);
	_title = std::move(title);
	_options = refreshed;
	_clickHandlerContextRef = ResolveClickHandlerContextRef(
		_clickHandlerContextRef,
		_options);
	_options.clickHandlerContextRef = _clickHandlerContextRef;
	if (_menu) {
		_menu = nullptr;
		_menuToggle->setForceRippled(false);
	}
	refreshTitle();
	createPreview();
	if (preserveScroll && _preview) {
		ScrollMarkdownPreviewToY(_preview.get(), scrollTop);
	}
	if (_window && _window->isActiveWindow() && _preview) {
		_preview->setFocus();
	}
	updateHistoryButtons();
}

bool Controller::updateExistingPreview(
		MarkdownArticleContent content,
		OpenOptions options,
		int scrollTop) {
	Expects(_preview != nullptr);

	_preparedContent = std::move(content);
	_options = std::move(options);
	_clickHandlerContextRef = ResolveClickHandlerContextRef(
		_clickHandlerContextRef,
		_options);
	_options.clickHandlerContextRef = _clickHandlerContextRef;
	auto previewOptions = _options;
	previewOptions.clickHandlerContextRef = _clickHandlerContextRef;
	previewOptions.clickHandlerContext = ExtendClickHandlerContext(
		std::move(previewOptions.clickHandlerContext),
		_show);
	if (previewOptions.clickHandlerContextRef) {
		*previewOptions.clickHandlerContextRef
			= previewOptions.clickHandlerContext;
	}
	if (historyEnabled(_options)) {
		updateCurrentHistoryEntry(*_preparedContent, _title, _options);
		const auto index = findHistoryEntry(
			_options.currentPageId,
			_options.sourceUrl,
			_options.initialFragment);
		Assert(index >= 0);
		if (index >= 0) {
			_historyIndex = index;
			_shownHistoryIndex = index;
		}
	} else {
		_historyIndex = -1;
		_shownHistoryIndex = -1;
	}
	previewOptions.initialFragment = QString();
	if (!UpdateMarkdownPreviewWidget(
			_preview.get(),
			std::move(*_preparedContent),
			previewOptions)) {
		return false;
	}
	_preparedContent.reset();
	ScrollMarkdownPreviewToY(_preview.get(), scrollTop);
	return true;
}

void Controller::updateOptions(OpenOptions options) {
	const auto initialFragment = options.initialFragment;
	auto refreshed = PrepareOpenOptions(std::move(options), _delegate, _title);
	if (refreshed.sourceName.isEmpty()) {
		refreshed.sourceName = _options.sourceName;
	}
	if (refreshed.sourcePath.isEmpty()) {
		refreshed.sourcePath = _options.sourcePath;
	}
	if (refreshed.sourceUrl.isEmpty()) {
		refreshed.sourceUrl = _options.sourceUrl;
	}
	if (!refreshed.ivWebviewStorageId) {
		refreshed.ivWebviewStorageId = _options.ivWebviewStorageId;
	}
	if (refreshed.viewerKind == ViewerKind::Auto) {
		refreshed.viewerKind = _options.viewerKind;
	}
	if (!refreshed.openSource) {
		refreshed.openSource = _options.openSource;
	}
	if (!refreshed.activateMedia) {
		refreshed.activateMedia = _options.activateMedia;
	}
	_options = std::move(refreshed);
	if (!_clickHandlerContextRef) {
		_clickHandlerContextRef = std::make_shared<QVariant>();
	}
	_options.clickHandlerContextRef = _clickHandlerContextRef;
	if (_clickHandlerContextRef) {
		*_clickHandlerContextRef = ExtendClickHandlerContext(
			_options.clickHandlerContext,
			_show);
	}
	if (_menu) {
		_menu = nullptr;
		_menuToggle->setForceRippled(false);
	}
	refreshTitle();
	if (!initialFragment.isEmpty() && _preview) {
		const auto scrolled = ScrollMarkdownPreviewToAnchor(
			_preview.get(),
			initialFragment);
		static_cast<void>(scrolled);
	}
	if (_window && _window->isActiveWindow() && _preview) {
		_preview->setFocus();
	}
}

bool Controller::active() const {
	return _window && _window->isActiveWindow();
}

void Controller::showJoinedTooltip() {
	if (_show) {
		_show->showToast(tr::lng_action_you_joined(tr::now));
	}
}

void Controller::minimize() {
	if (_window) {
		_window->setWindowState(_window->windowState() | Qt::WindowMinimized);
	}
}

void Controller::close() {
	_events.fire({ Event::Type::Close });
}

ViewerKind Controller::viewerKind() const {
	return ResolveViewerKind(_options);
}

QString Controller::subtitleText() const {
	return SubtitleText(_options, _title);
}

bool Controller::canOpenSource() const {
	if (_options.openSource) {
		return true;
	}
	return (viewerKind() == ViewerKind::InstantView)
		? !_options.sourceUrl.isEmpty()
		: !_options.sourcePath.isEmpty();
}

bool Controller::canShare() const {
	return static_cast<bool>(_options.share);
}

bool Controller::historyEnabled(const OpenOptions &options) const {
	return (ResolveViewerKind(options) == ViewerKind::InstantView)
		&& (options.currentPageId != 0);
}

bool Controller::sameHistoryPage(
		const HistoryEntry &entry,
		uint64 pageId,
		const QString &sourceUrl) const {
	return entry.pageId
		? (entry.pageId == pageId)
		: (!sourceUrl.isEmpty() && entry.sourceUrl == sourceUrl);
}

bool Controller::sameCurrentPage(uint64 pageId, const QString &sourceUrl) const {
	return (pageId != 0
		&& _options.currentPageId != 0
		&& pageId == _options.currentPageId)
		|| (!sourceUrl.isEmpty()
			&& !_options.sourceUrl.isEmpty()
			&& sourceUrl == _options.sourceUrl);
}

bool Controller::sameHistoryLocation(
		const HistoryEntry &entry,
		uint64 pageId,
		const QString &sourceUrl,
		const QString &hash) const {
	return sameHistoryPage(entry, pageId, sourceUrl)
		&& (entry.hash == hash);
}

int Controller::findHistoryEntry(
		uint64 pageId,
		const QString &sourceUrl,
		const QString &hash) const {
	if (_historyIndex >= 0
		&& _historyIndex < int(_history.size())
		&& sameHistoryLocation(
			_history[_historyIndex],
			pageId,
			sourceUrl,
			hash)) {
		return _historyIndex;
	}
	if (_shownHistoryIndex >= 0
		&& _shownHistoryIndex < int(_history.size())
		&& _shownHistoryIndex != _historyIndex
		&& sameHistoryLocation(
			_history[_shownHistoryIndex],
			pageId,
			sourceUrl,
			hash)) {
		return _shownHistoryIndex;
	}
	for (auto i = 0, count = int(_history.size()); i != count; ++i) {
		if (sameHistoryLocation(_history[i], pageId, sourceUrl, hash)) {
			return i;
		}
	}
	return -1;
}

void Controller::saveCurrentHistoryScroll(std::optional<int> scrollTop) {
	const auto index = (_shownHistoryIndex >= 0)
		? _shownHistoryIndex
		: _historyIndex;
	if (!_preview
		|| index < 0
		|| index >= int(_history.size())) {
		return;
	}
	_history[index].scrollTop = scrollTop.value_or(
		MarkdownPreviewScrollTop(_preview.get()));
}

void Controller::updateCurrentHistoryEntry(
		const MarkdownArticleContent &content,
		const QString &title,
		const OpenOptions &options) {
	if (!historyEnabled(options)) {
		return;
	}
	auto index = findHistoryEntry(
		options.currentPageId,
		options.sourceUrl,
		options.initialFragment);
	if (index < 0) {
		if (_historyIndex < 0) {
			_history.push_back({});
			index = 0;
			_historyIndex = 0;
		} else {
			_history.resize(_historyIndex + 1);
			index = int(_history.size());
			_history.push_back({});
			_historyIndex = index;
		}
	}
	auto &entry = _history[index];
	entry.pageId = options.currentPageId;
	entry.sourceUrl = options.sourceUrl;
	entry.hash = options.initialFragment;
	entry.title = title;
	entry.preparedContent = std::make_shared<MarkdownArticleContent>(content);
	entry.options = options;
}

void Controller::handleOpenPage(Event event) {
	if (!historyEnabled(_options)) {
		_events.fire(std::move(event));
		return;
	}
	const auto target = ParsePageHistoryTarget(event.webpageId, event.url);
	if (!target.pageId || target.sourceUrl.isEmpty()) {
		_events.fire(std::move(event));
		return;
	}
	const auto currentIndex = (_historyIndex >= 0
		&& _historyIndex < int(_history.size()))
		? _historyIndex
		: -1;
	const auto current = (currentIndex >= 0) ? &_history[currentIndex] : nullptr;
	const auto samePage = sameCurrentPage(target.pageId, target.sourceUrl)
		|| (current
			&& sameHistoryPage(*current, target.pageId, target.sourceUrl));
	if (samePage) {
		if (target.hash.isEmpty()) {
			if (_preview) {
				ScrollMarkdownPreviewToY(
					_preview.get(),
					0,
					MarkdownPreviewScrollMode::Animated);
			}
			return;
		}
		if (_preview
			&& ScrollMarkdownPreviewToAnchor(
				_preview.get(),
				target.hash,
				MarkdownPreviewScrollMode::Animated)) {
			return;
		}
		DEBUG_LOG(("Native Markdown IV: unresolved anchor: %1"
			).arg(target.hash));
		return;
	}
	saveCurrentHistoryScroll();
	auto targetIndex = -1;
	if ((_historyIndex + 1) < int(_history.size())
		&& sameHistoryLocation(
			_history[_historyIndex + 1],
			target.pageId,
			target.sourceUrl,
			target.hash)) {
		targetIndex = _historyIndex + 1;
	} else {
		auto options = _options;
		options.sourceUrl = target.sourceUrl;
		options.initialFragment = target.hash;
		options.currentPageId = target.pageId;
		_history.resize(_historyIndex + 1);
		_history.push_back({
			.pageId = target.pageId,
			.sourceUrl = target.sourceUrl,
			.hash = target.hash,
			.options = std::move(options),
		});
		targetIndex = int(_history.size()) - 1;
	}
	auto &entry = _history[targetIndex];
	entry.pageId = target.pageId;
	entry.sourceUrl = target.sourceUrl;
	entry.hash = target.hash;
	entry.options.sourceUrl = target.sourceUrl;
	entry.options.initialFragment = target.hash;
	entry.options.currentPageId = target.pageId;
	_historyIndex = targetIndex;
	updateHistoryButtons();
	if (!showHistoryEntry(targetIndex)) {
		event.url = ComposePageHistoryUrl(target);
		_events.fire(std::move(event));
	}
}

bool Controller::showHistoryEntry(int index) {
	if (index < 0 || index >= int(_history.size())) {
		return false;
	}
	const auto &entry = _history[index];
	if (!entry.hydrated()) {
		return false;
	}
	auto options = entry.options;
	options.sourceUrl = entry.sourceUrl;
	options.initialFragment = entry.hash;
	options.currentPageId = entry.pageId;
	setContent(*entry.preparedContent, entry.title, std::move(options), false);
	if (_preview) {
		ScrollMarkdownPreviewToY(_preview.get(), entry.scrollTop);
	}
	return true;
}

void Controller::stepHistory(int delta) {
	const auto index = _historyIndex + delta;
	if (index < 0 || index >= int(_history.size())) {
		return;
	}
	saveCurrentHistoryScroll();
	_historyIndex = index;
	updateHistoryButtons();
	if (!showHistoryEntry(index)) {
		const auto &entry = _history[index];
		_events.fire({
			.type = Event::Type::OpenPage,
			.webpageId = entry.pageId,
			.url = ComposePageHistoryUrl(PageHistoryTarget{
				.pageId = entry.pageId,
				.sourceUrl = entry.sourceUrl,
				.hash = entry.hash,
			}),
		});
	}
}

void Controller::updateHistoryButtons() {
	if (!_back || !_forward) {
		return;
	}
	const auto canGoBack = (_historyIndex > 0);
	const auto canGoForward = (_historyIndex >= 0)
		&& ((_historyIndex + 1) < int(_history.size()));
	const auto updateButton = [](
			Ui::FadeWrapScaled<Ui::IconButton> *button,
			bool enabled,
			const style::icon &disabledIcon) {
		Expects(button != nullptr);
		button->entity()->setDisabled(!enabled);
		button->entity()->setIconOverride(
			enabled ? nullptr : &disabledIcon,
			enabled ? nullptr : &disabledIcon);
		button->entity()->setPointerCursor(enabled);
		button->setAttribute(Qt::WA_TransparentForMouseEvents, !enabled);
		if (!enabled) {
			button->entity()->clearState();
		}
	};
	_back->toggle(canGoBack || canGoForward, anim::type::normal);
	_forward->toggle(canGoForward, anim::type::normal);
	updateButton(_back, canGoBack, st::ivBackIconDisabled);
	updateButton(_forward, canGoForward, st::ivForwardIconDisabled);
	if (_window) {
		updateTitleGeometry(_window->body()->width());
	}
}

void Controller::refreshTitle() {
	if (_window) {
		_window->setTitle(_title);
		_window->setWindowTitle(_title);
	}
	if (_subtitle) {
		_subtitle->setText(subtitleText());
		updateTitleGeometry(_window->body()->width());
	}
}

void Controller::updateTitleGeometry(int newWidth) const {
	_subtitleWrap->setGeometry(0, 0, newWidth, st::ivSubtitleHeight);
	const auto progressBack = _back
		? _subtitleBackShift.value(_back->toggled() ? 1. : 0.)
		: 0.;
	const auto progressForward = _forward
		? _subtitleForwardShift.value(_forward->toggled() ? 1. : 0.)
		: 0.;
	const auto backAdded = (_back
		? (_back->width() + st::ivSubtitleSkip - st::ivSubtitleLeft)
		: 0);
	const auto forwardAdded = _forward ? _forward->width() : 0;
	const auto left = int(st::ivSubtitleLeft
		+ anim::interpolate(0, backAdded, progressBack)
		+ anim::interpolate(0, forwardAdded, progressForward));
	_subtitle->resizeToWidth(newWidth - left - _menuToggle->width());
	_subtitle->moveToLeft(left, st::ivSubtitleTop);
	if (_back) {
		_back->moveToLeft(0, 0);
	}
	if (_forward) {
		_forward->moveToLeft(
			_back ? int(_back->width() * progressBack) : 0,
			0);
	}
	_menuToggle->moveToRight(0, 0);
	_titleShadow->resizeToWidth(newWidth);
	_titleShadow->move(0, st::ivSubtitleHeight);
}

void Controller::openSource() {
	if (_options.openSource) {
		_options.openSource();
	} else if (viewerKind() == ViewerKind::InstantView) {
		File::OpenUrl(_options.sourceUrl);
	} else {
		File::Launch(_options.sourcePath);
	}
}

void Controller::showMenu() {
	if (!_window || _menu) {
		return;
	}
	_menu = base::make_unique_q<Ui::PopupMenu>(
		_window.get(),
		st::popupMenuWithIcons);
	_menu->setDestroyedCallback(crl::guard(_window.get(), [
			this,
			menu = _menu.get()] {
		if (_menu == menu) {
			_menuToggle->setForceRippled(false);
		}
	}));
	_menuToggle->setForceRippled(true);

	const auto hasOpenSource = canOpenSource();
	if (hasOpenSource) {
		_menu->addAction(
			OpenSourceLabel(viewerKind()),
			crl::guard(_window.get(), [=] {
				openSource();
			}),
			OpenSourceIcon(viewerKind()));
	}

	if (canShare()) {
		_menu->addAction(
			tr::lng_iv_share(tr::now),
			crl::guard(_window.get(), [=, share = _options.share] {
				share(_show);
			}),
			&st::menuIconShare);
	}

	if (hasOpenSource || canShare()) {
		_menu->addSeparator();
	}
	_menu->addAction(CreateZoomMenuAction(_menu, _delegate));

	_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
	_menu->popup(_window->body()->mapToGlobal(
		QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
}

void Controller::createLayerManager() {
	if (!_window || _layerManager) {
		return;
	}
	_layerManager = std::make_unique<Ui::LayerManager>(
		not_null{ _window->body().get() });
	_layerManager->setHideByBackgroundClick(false);
	_show = _layerManager->uiShow();
}

void Controller::createPreview() {
	if (!_container) {
		return;
	}
	const auto parent = _container;
	auto options = _options;
	options.clickHandlerContextRef = _clickHandlerContextRef;
	options.clickHandlerContext = ExtendClickHandlerContext(
		std::move(options.clickHandlerContext),
		_show);
	if (options.clickHandlerContextRef) {
		*options.clickHandlerContextRef = options.clickHandlerContext;
	}
	const auto callback = [=](Event event) {
		switch (event.type) {
		case Event::Type::OpenPage:
			handleOpenPage(std::move(event));
			break;
		case Event::Type::Close:
		case Event::Type::Quit:
		case Event::Type::OpenFile:
		case Event::Type::Report:
			_events.fire(std::move(event));
			break;
		}
	};
	_preview = nullptr;
	const auto preparedForHistory = (_preparedContent && historyEnabled(options))
		? std::make_shared<MarkdownArticleContent>(*_preparedContent)
		: nullptr;
	_preview = _preparedContent
		? CreateMarkdownPreviewWidget(
			parent,
			std::move(*_preparedContent),
			_renderer,
			callback,
			options)
		: CreateMarkdownPreviewWidget(
			parent,
			*_document,
			callback,
			options);
	_preparedContent.reset();
	if (preparedForHistory) {
		updateCurrentHistoryEntry(*preparedForHistory, _title, options);
		_shownHistoryIndex = _historyIndex;
	} else {
		_shownHistoryIndex = -1;
	}
	_preview->setGeometry(parent->rect());
	parent->sizeValue() | rpl::on_next([=](QSize size) {
		_preview->resize(size);
	}, _preview->lifetime());

	_titleShadow->show(anim::type::instant);

	_preview->show();
	updateHistoryButtons();
}

void Controller::createWindow() {
	_window = std::make_unique<Ui::RpWindow>();
	const auto window = _window.get();
	_titleShadow.create(window->body().get());
	_subtitleWrap = std::make_unique<Ui::RpWidget>(window->body().get());
	_subtitle = std::make_unique<Ui::FlatLabel>(
		_subtitleWrap.get(),
		subtitleText(),
		st::ivSubtitle);
	_subtitle->setSelectable(true);
	_menuToggle.create(_subtitleWrap.get(), st::ivMenuToggle);
	_menuToggle->setClickedCallback([=] {
		showMenu();
	});
	_back.create(
		_subtitleWrap.get(),
		object_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivBack));
	_back->entity()->setClickedCallback([=] {
		stepHistory(-1);
	});
	_forward.create(
		_subtitleWrap.get(),
		object_ptr<Ui::IconButton>(_subtitleWrap.get(), st::ivForward));
	_forward->entity()->setClickedCallback([=] {
		stepHistory(1);
	});
	_back->toggledValue(
	) | rpl::on_next([=](bool toggled) {
		_subtitleBackShift.start(
			[=] { updateTitleGeometry(_window->body()->width()); },
			toggled ? 0. : 1.,
			toggled ? 1. : 0.,
			st::fadeWrapDuration);
	}, _back->lifetime());
	_back->hide(anim::type::instant);
	_forward->toggledValue(
	) | rpl::on_next([=](bool toggled) {
		_subtitleForwardShift.start(
			[=] { updateTitleGeometry(_window->body()->width()); },
			toggled ? 0. : 1.,
			toggled ? 1. : 0.,
			st::fadeWrapDuration);
	}, _forward->lifetime());
	_forward->hide(anim::type::instant);
	_subtitleBackShift.stop();
	_subtitleForwardShift.stop();
	_subtitleWrap->paintRequest() | rpl::on_next([=](QRect clip) {
		QPainter(_subtitleWrap.get()).fillRect(clip, st::windowBg);
	}, _subtitleWrap->lifetime());
	window->body()->widthValue() | rpl::on_next([=](int width) {
		updateTitleGeometry(width);
	}, _subtitle->lifetime());
	window->setTitle(_title);
	window->setWindowTitle(_title);
	window->setGeometry(_delegate->ivGeometry(window));
	window->setMinimumSize({ st::ivWidthMin, st::ivHeightMin });
	window->geometryValue(
	) | rpl::distinct_until_changed(
	) | rpl::skip(1) | rpl::on_next([=] {
		_delegate->ivSaveGeometry(window);
	}, window->lifetime());
	window->body()->paintRequest() | rpl::on_next([=](QRect clip) {
		QPainter(window->body().get()).fillRect(clip, st::windowBg);
	}, window->body()->lifetime());
	_container = Ui::CreateChild<Ui::RpWidget>(window->body().get());
	rpl::combine(
		window->body()->sizeValue(),
		_subtitleWrap->heightValue()
	) | rpl::on_next([=](QSize size, int titleHeight) {
		_container->setGeometry(QRect(QPoint(), size).marginsRemoved(
			{ 0, titleHeight, 0, 0 }));
	}, _container->lifetime());
	_container->paintRequest() | rpl::on_next([=](QRect clip) {
		QPainter(_container).fillRect(clip, st::windowBg);
	}, _container->lifetime());
	updateTitleGeometry(window->body()->width());

	createLayerManager();
	createPreview();
	_container->show();
	updateHistoryButtons();

	window->events() | rpl::on_next([=](not_null<QEvent*> e) {
		if (e->type() == QEvent::Close) {
			close();
		} else if (e->type() == QEvent::KeyPress) {
			const auto event = static_cast<QKeyEvent*>(e.get());
			if (event->key() == Qt::Key_Escape) {
				event->accept();
				close();
			}
		}
	}, window->lifetime());
	base::install_event_filter(window, qApp, [=](not_null<QEvent*> e) {
		if (e->type() != QEvent::ShortcutOverride || !window->isActiveWindow()) {
			return base::EventFilterResult::Continue;
		}
		const auto event = static_cast<QKeyEvent*>(e.get());
		const auto previousAccepted = event->isAccepted();
		ProcessZoomShortcut(_delegate, event);
		return event->isAccepted() && !previousAccepted
			? base::EventFilterResult::Cancel
			: base::EventFilterResult::Continue;
	});

	_titleShadow->raise();
	window->show();
}

std::unique_ptr<Controller> TryOpenLocalFile(
		not_null<Delegate*> delegate,
		const QString &path,
		OpenOptions options) {
	const auto &limits = ParseLimitsForIv();
	const auto target = ParseOpenTarget(path);

	const auto source = ReadLocalSource(target.path, limits);
	if (!source) {
		return nullptr;
	}
	const auto &bytes = source.bytes;
	const auto &fallbackTitle = source.name;

	const auto start = crl::now();
	auto validateResult = ValidateMarkdownSourceForIv(
		bytes,
		ParseOptions{ fallbackTitle });
	const auto validated = crl::now();
	if (!validateResult.ok) {
		DEBUG_LOG(("Native Markdown IV: "
			"source validation failure (%1, %2 ms): %3"
			).arg(validateResult.error
			).arg(validated - start
			).arg(target.path));
		return nullptr;
	}

	auto parseResult = ParseMarkdownForIv(std::move(validateResult.source));
	const auto parsed = crl::now();
	if (!parseResult.ok) {
		const auto &error = parseResult.error;
		if (error.startsWith(u"cmark-"_q)) {
			DEBUG_LOG(("Native Markdown IV: "
				"cmark parse failure (%1, %2 ms): %3"
				).arg(error
				).arg(parsed - validated
				).arg(target.path));
		} else {
			DEBUG_LOG(("Native Markdown IV: parse failure (%1, %2 ms): %3"
				).arg(error
				).arg(parsed - validated
				).arg(target.path));
		}
		return nullptr;
	} else if (!AcceptsPreview(parseResult.document)) {
		DEBUG_LOG(("Native Markdown IV: "
			"unsupported or empty document (%1 ms): %2"
			).arg(crl::now() - parsed
			).arg(target.path));
		return nullptr;
	}
	LogDocumentWarnings(parseResult.document, target.path);

	DEBUG_LOG(("Native Markdown IV: "
		"opening as native Markdown IV (%1 ms validate, %2 ms parse): %3"
		).arg(validated - start
		).arg(parsed - validated
		).arg(target.path));

	if (options.sourceName.isEmpty()) {
		options.sourceName = source.name;
	}
	options.sourcePath = std::move(source.path);
	options.initialFragment = std::move(target.fragment);
	if (options.viewerKind == ViewerKind::Auto) {
		options.viewerKind = ViewerKind::LocalFile;
	}
	return std::make_unique<Controller>(
		delegate,
		std::move(parseResult.document),
		(parseResult.document.title.trimmed().isEmpty()
			? fallbackTitle
			: parseResult.document.title.trimmed()),
		std::move(options));
}

} // namespace Iv::Markdown
