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

#include "base/qt/qt_common_adapters.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_article_text.h"
#include "iv/markdown/iv_markdown_prepare_native_richtext.h"
#include "lang/lang_keys.h"
#include "spellcheck/spellcheck_highlight_syntax.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/layers/show.h"
#include "ui/text/text_extended_data.h"
#include "ui/ui_utility.h"
#include "ui/widgets/popup_menu.h"
#include "ui/color_contrast.h"
#include "ui/integration.h"

#include "styles/palette.h"
#include "styles/style_chat.h"
#include "styles/style_iv.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"

#include <QtCore/QElapsedTimer>
#include <QtCore/QPointer>
#include <QtGui/QClipboard>
#include <QtGui/QContextMenuEvent>
#include <QtGui/QCursor>
#include <QtGui/QGuiApplication>
#include <QtGui/QKeyEvent>
#include <QtGui/QKeySequence>
#include <QtGui/QMouseEvent>
#include <QtGui/QTouchEvent>
#include <QtGui/QWheelEvent>
#include <QtWidgets/QApplication>

#include <algorithm>
#include <cmath>
#include <utility>

namespace Iv::Markdown {
namespace {

void EnsureBlockquotePaintCache(
		std::unique_ptr<Ui::Text::QuotePaintCache> &cache,
		const style::color &color) {
	if (cache) {
		return;
	}
	cache = std::make_unique<Ui::Text::QuotePaintCache>();
	cache->bg = color->c;
	cache->bg.setAlpha(Ui::kDefaultBgOpacity * 255);
	cache->outlines[0] = color->c;
	cache->outlines[0].setAlpha(Ui::kDefaultOutline1Opacity * 255);
	cache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0);
	cache->header = color->c;
	cache->header.setAlpha(Ui::kDefaultOutline2Opacity * 255);
	cache->icon = color->c;
	cache->icon.setAlpha(Ui::kDefaultOutline3Opacity * 255);
}

[[nodiscard]] bool UseDarkPrePaintBackground() {
	const auto withBg = [](const QColor &color) {
		return Ui::CountContrast(st::windowBg->c, color);
	};
	return withBg({ 0, 0, 0 }) < withBg({ 255, 255, 255 });
}

void EnsurePrePaintCache(
		std::unique_ptr<Ui::Text::QuotePaintCache> &cache,
		const style::color &color) {
	if (cache) {
		return;
	}
	cache = std::make_unique<Ui::Text::QuotePaintCache>();
	if (UseDarkPrePaintBackground()) {
		cache->bg = QColor(0, 0, 0, 192);
	} else {
		cache->bg = color->c;
		cache->bg.setAlpha(Ui::kDefaultBgOpacity * 255);
	}
	cache->outlines[0] = color->c;
	cache->outlines[0].setAlpha(Ui::kDefaultOutline1Opacity * 255);
	cache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0);
	cache->header = color->c;
	cache->header.setAlpha(Ui::kDefaultOutline2Opacity * 255);
	cache->icon = color->c;
	cache->icon.setAlpha(Ui::kDefaultOutline3Opacity * 255);
}

[[nodiscard]] int CompareSelectionPositions(
		MarkdownArticleSelectionPosition a,
		MarkdownArticleSelectionPosition b) {
	if (a.segment != b.segment) {
		return (a.segment < b.segment) ? -1 : 1;
	}
	if (a.offset != b.offset) {
		return (a.offset < b.offset) ? -1 : 1;
	}
	return 0;
}

[[nodiscard]] MarkdownArticleSelection NormalizeSelection(
		MarkdownArticleSelection selection) {
	if (selection.empty()) {
		return {};
	}
	if (CompareSelectionPositions(selection.from, selection.to) > 0) {
		std::swap(selection.from, selection.to);
	}
	return selection;
}

[[nodiscard]] MarkdownArticleSelectionEndpoint MakeSelectionEndpoint(
		const MarkdownArticleHitTestResult &result) {
	return {
		.segment = result.segmentIndex,
		.direct = result.direct,
	};
}

[[nodiscard]] std::vector<Ui::Text::SpecialColor> HighlightColors(
		not_null<const Ui::ChatStyle*> style) {
	auto result = Ui::SyntaxHighlightColors(style);

	const auto &fg = style->lightButtonFg();
	const auto &bg = style->lightButtonBgOver();
	result.push_back({ &fg->p, &fg->p, &bg->b, &bg->b });

	Ensures(result.size() == kNativeIvLinkSpecialColorIndex);
	return result;
}

[[nodiscard]] std::unique_ptr<Ui::ChatTheme> CreateStandaloneChatTheme() {
	const auto palette = style::main_palette::get();
	return std::make_unique<Ui::ChatTheme>(Ui::ChatThemeDescriptor{
		.preparePalette = [=](style::palette &copy) {
			copy = *palette;
		},
		.backgroundData = {
			.colors = { palette->windowBg()->c },
		},
	});
}

[[nodiscard]] QPoint LocalPosition(QWheelEvent *e) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
	return e->position().toPoint();
#else // Qt >= 6.0
	return e->pos();
#endif // Qt >= 6.0
}

[[nodiscard]] QPoint ArticlePointFromWidget(QPoint point, double scale) {
	if (scale != 1.) {
		point = QPoint(
			int(std::floor(point.x() / scale)),
			int(std::floor(point.y() / scale)));
	}
	return point;
}

} // namespace

MarkdownDocumentWidget::MarkdownDocumentWidget(QWidget *parent)
: Ui::RpWidget(parent)
, _theme(CreateStandaloneChatTheme())
, _style(std::make_unique<Ui::ChatStyle>(style::main_palette::get())) {
	_style->apply(_theme.get());
	_highlightColors = HighlightColors(_style.get());

	setMouseTracking(true);
	setAttribute(Qt::WA_AcceptTouchEvents);
	setFocusPolicy(Qt::StrongFocus);

	Spellchecker::HighlightReady(
	) | rpl::on_next([=](Spellchecker::HighlightProcessId processId) {
		if (_article && _article->highlightProcessDone(processId)) {
			update();
		}
	}, _highlightReadyLifetime);
}

MarkdownDocumentWidget::~MarkdownDocumentWidget() {
	if (_article && (_article->mediaBlockHost() == this)) {
		_article->setTextRepaintCallbacks(nullptr, nullptr);
		_article->setMediaBlockHost(nullptr);
	}
}

void MarkdownDocumentWidget::setLinkActivationCallback(
		std::function<void(const PreparedLink &, Qt::MouseButton)> callback) {
	_activateLink = std::move(callback);
}

void MarkdownDocumentWidget::setMediaActivationCallback(
		std::function<bool(const MediaActivation &, Qt::MouseButton)> callback) {
	_activateMedia = std::move(callback);
}

void MarkdownDocumentWidget::setClickHandlerContext(
		QVariant context,
		std::shared_ptr<QVariant> contextRef) {
	_clickHandlerContext = std::move(context);
	_clickHandlerContextRef = std::move(contextRef);
}

void MarkdownDocumentWidget::setArticle(
		std::shared_ptr<MarkdownArticle> article) {
	ClickHandler::clearActive(this);
	applyCursor(style::cur_default);
	auto previous = std::move(_article);
	if (previous && (previous->mediaBlockHost() == this)) {
		previous->setTextRepaintCallbacks(nullptr, nullptr);
		previous->setMediaBlockHost(nullptr);
	}
	if (previous && article && _articlePainted) {
		_retainedArticle = std::move(previous);
	} else if (!article) {
		_retainedArticle = nullptr;
	}
	_article = std::move(article);
	_articlePainted = false;
	if (_article) {
		const auto weak = QPointer<MarkdownDocumentWidget>(this);
		_article->setTextRepaintCallbacks(
			[=] {
				if (weak) {
					weak->requestRepaint(QRect());
				}
			},
			[=](QRect articleRect) {
				if (weak) {
					weak->requestRepaint(articleRect);
				}
			});
		_article->setMediaBlockHost(this);
	}
	_lastRelayoutMs = 0;
	resetTextPaintCaches();
	resetSelection();
	forceRelayoutCurrentWidth();
}

void MarkdownDocumentWidget::articleContentChanged() {
	ClickHandler::clearActive(this);
	applyCursor(style::cur_default);
	stopPressedPlaceholderRipple();
	clearSelection();
	_articlePainted = false;
	resetTextPaintCaches();
	forceRelayoutCurrentWidth();
}

void MarkdownDocumentWidget::setZoom(int value) {
	value = (value > 0) ? value : 100;
	if (_zoom == value) {
		return;
	}
	_zoom = value;
	clearSelection();
	forceRelayoutCurrentWidth();
}

void MarkdownDocumentWidget::refreshPalette() {
	ClickHandler::clearActive(this);
	applyCursor(style::cur_default);
	_theme = CreateStandaloneChatTheme();
	_style->apply(_theme.get());
	_highlightColors = HighlightColors(_style.get());
	resetTextPaintCaches();
	if (_article) {
		_article->invalidatePaletteCache();
	}
	update();
}

void MarkdownDocumentWidget::invalidateRasterCache() {
	ClickHandler::clearActive(this);
	applyCursor(style::cur_default);
	if (_article) {
		_article->invalidateRasterCache();
	}
	relayoutCurrentWidth(false);
	update();
}

int MarkdownDocumentWidget::maxWidth() const {
	return _article
		? std::max(int(std::ceil(_article->maxWidth() * zoomScale())), 1)
		: 1;
}

int MarkdownDocumentWidget::anchorTop(const QString &anchorId) const {
	const auto top = _article ? _article->anchorTop(anchorId) : -1;
	if (top < 0) {
		return -1;
	}
	return int(std::floor(top * zoomScale()));
}

bool MarkdownDocumentWidget::expandDetailsToAnchor(const QString &anchorId) {
	if (!_article) {
		return false;
	}
	const auto result = _article->expandDetailsToAnchor(anchorId);
	if (!result.found) {
		return false;
	}
	if (result.changed) {
		clearSelection();
		forceRelayoutCurrentWidth();
		updateHoverAtCursor();
	}
	return true;
}

bool MarkdownDocumentWidget::toggleDetails(const QString &anchorId) {
	if (!_article || !_article->toggleDetails(anchorId)) {
		return false;
	}
	clearSelection();
	forceRelayoutCurrentWidth();
	updateHoverAtCursor();
	return true;
}

int MarkdownDocumentWidget::lastRelayoutMs() const {
	return _lastRelayoutMs;
}

int MarkdownDocumentWidget::resizeGetHeight(int newWidth) {
	ClickHandler::clearActive(this);
	applyCursor(style::cur_default);
	clearSelection();
	if (!_article) {
		return 1;
	}
	const auto scale = zoomScale();
	const auto layoutWidth = std::max(int(std::floor(newWidth / scale)), 1);
	auto timer = QElapsedTimer();
	timer.start();
	const auto layoutHeight = _article->resizeGetHeight(layoutWidth);
	syncArticleVisibleTopBottom();
	_lastRelayoutMs = int(timer.elapsed());
	return std::max(int(std::ceil(layoutHeight * scale)), 1);
}

void MarkdownDocumentWidget::requestRepaint(QRect articleRect) {
	crl::on_main(this, [=] {
		if (!_article) {
			return;
		}
		if (articleRect.isEmpty()) {
			update();
		} else {
			update(articleRectToWidget(articleRect));
		}
	});
}

void MarkdownDocumentWidget::requestRelayout(QRect articleRect) {
	crl::on_main(this, [=] {
		if (!_article) {
			return;
		}
		_article->invalidateLayout();
		const auto previousHeight = height();
		const auto scale = zoomScale();
		const auto layoutWidth = std::max(int(std::floor(width() / scale)), 1);
		auto timer = QElapsedTimer();
		timer.start();
		const auto articleHeight = _article->resizeGetHeight(layoutWidth);
		syncArticleVisibleTopBottom();
		_lastRelayoutMs = int(timer.elapsed());
		const auto newHeight = std::max(int(std::ceil(articleHeight * scale)), 1);
		if (previousHeight != newHeight) {
			resize(width(), newHeight);
			update();
		} else if (articleRect.isEmpty()) {
			update();
		} else {
			update(articleRectToWidget(articleRect));
		}
		updateHoverAtCursor();
	});
}

void MarkdownDocumentWidget::setPlaceholderLoading(
		PreparedPlaceholderBlockId id) {
	if (_article) {
		_article->setPlaceholderLoading(id);
	}
}

void MarkdownDocumentWidget::clearPlaceholderLoading(
		PreparedPlaceholderBlockId id) {
	if (_article) {
		_article->clearPlaceholderLoading(id);
	}
}

void MarkdownDocumentWidget::clearAllPlaceholderLoading() {
	if (_article) {
		_article->clearAllPlaceholderLoading();
	}
}

void MarkdownDocumentWidget::addPlaceholderRipple(
		PreparedPlaceholderBlockId id,
		QPoint point) {
	if (_article) {
		_article->addPlaceholderRipple(id, point);
	}
}

void MarkdownDocumentWidget::stopPlaceholderRipple(
		PreparedPlaceholderBlockId id) {
	if (_article) {
		_article->stopPlaceholderRipple(id);
	}
}

void MarkdownDocumentWidget::paintEvent(QPaintEvent *e) {
	if (!_article) {
		return;
	}
	auto p = Painter(this);
	p.setTextPalette(st::inTextPalette);
	const auto scale = zoomScale();
	const auto clip = (scale == 1.)
		? e->rect()
		: QRect(
			int(std::floor(e->rect().x() / scale)),
			int(std::floor(e->rect().y() / scale)),
			int(std::ceil(e->rect().width() / scale)) + 1,
			int(std::ceil(e->rect().height() / scale)) + 1);
	auto context = textPaintContext(clip);
	if (scale == 1.) {
		_article->paint(p, context);
		_articlePainted = true;
		_retainedArticle = nullptr;
		return;
	}
	p.save();
	p.scale(scale, scale);
	_article->paint(p, context);
	p.restore();
	_articlePainted = true;
	_retainedArticle = nullptr;
}

void MarkdownDocumentWidget::visibleTopBottomUpdated(
		int visibleTop,
		int visibleBottom) {
	_visibleRange = Ui::VisibleRange{
		.top = visibleTop,
		.bottom = visibleBottom,
	};
	syncArticleVisibleTopBottom();
}

void MarkdownDocumentWidget::keyPressEvent(QKeyEvent *e) {
	if (e == QKeySequence::Copy && !selectionForCopy().empty()) {
		copySelectedText();
		return;
	}
	Ui::RpWidget::keyPressEvent(e);
}

void MarkdownDocumentWidget::contextMenuEvent(QContextMenuEvent *e) {
	const auto globalPoint = (e->reason() == QContextMenuEvent::Mouse)
		? e->globalPos()
		: QCursor::pos();
	const auto localPoint = (e->reason() == QContextMenuEvent::Mouse)
		? e->pos()
		: mapFromGlobal(globalPoint);
	const auto state = hitTest(
		localPoint,
		Ui::Text::StateRequest::Flag::LookupLink
			| Ui::Text::StateRequest::Flag::LookupSymbol);
	const auto selection = selectionForCopy();
	const auto uponSelection = !selection.empty()
		&& ((e->reason() != QContextMenuEvent::Mouse)
			|| selectionContains(selection, state));
	const auto contextText = uponSelection
		? TextForMimeData()
		: (_article ? _article->textForContext(state) : TextForMimeData());
	const auto link = state.preparedLink;

	_contextMenu = base::make_unique_q<Ui::PopupMenu>(
		this,
		st::popupMenuWithIcons);
	if (uponSelection) {
		_contextMenu->addAction(
			Ui::Integration::Instance().phraseContextCopySelected(),
			[=] { copySelectedText(); },
			&st::menuIconCopy);
	} else if (!contextText.empty()) {
		_contextMenu->addAction(
			tr::lng_context_copy_text(tr::now),
			[text = contextText, this] {
				TextUtilities::SetClipboardText(text);
				showToast(tr::lng_text_copied(tr::now));
			},
			&st::menuIconCopy);
	}

	if (link) {
		const auto handler = CreatePreparedLinkHandler(*link);
		const auto copyText = handler ? handler->copyToClipboardText() : QString();
		const auto copyLabel = handler
			? handler->copyToClipboardContextItemText()
			: QString();
		if (!copyText.isEmpty() && !copyLabel.isEmpty()) {
			_contextMenu->addAction(
				copyLabel,
				[text = copyText] {
					QGuiApplication::clipboard()->setText(text);
				},
				&st::menuIconCopy);
		}
		switch (link->kind) {
		case PreparedLinkKind::RejectedRelative:
		case PreparedLinkKind::ToggleDetails:
			break;
		case PreparedLinkKind::External:
		case PreparedLinkKind::InstantViewPage:
		case PreparedLinkKind::Anchor:
		case PreparedLinkKind::Footnote:
		case PreparedLinkKind::FootnoteBacklink:
		case PreparedLinkKind::LocalFile:
			_contextMenu->addAction(
				tr::lng_open_link(tr::now),
				[=, prepared = *link] {
					if (_activateLink) {
						_activateLink(prepared, Qt::LeftButton);
					}
				},
				&st::menuIconAddress);
			break;
		}
	}

	if (_contextMenu->empty()) {
		_contextMenu = nullptr;
		return;
	}
	_contextMenu->popup(globalPoint);
	e->accept();
}

void MarkdownDocumentWidget::wheelEvent(QWheelEvent *e) {
	const auto phase = e->phase();
	if (phase == Qt::NoScrollPhase) {
		_horizontalScrollLock = std::nullopt;
	} else if (phase == Qt::ScrollBegin) {
		_horizontalScrollLock = std::nullopt;
	}
	if (!_article) {
		e->ignore();
		return;
	}
	const auto delta = Ui::ScrollDeltaF(e);
	const auto horizontal = (std::abs(delta.x()) > std::abs(delta.y()));
	if (phase != Qt::NoScrollPhase
		&& phase != Qt::ScrollBegin
		&& !_horizontalScrollLock) {
		_horizontalScrollLock = horizontal ? Qt::Horizontal : Qt::Vertical;
	}
	const auto local = ArticlePointFromWidget(LocalPosition(e), zoomScale());
	if (!_article->horizontalScrollHit(local).scrollable) {
		e->ignore();
		return;
	}
	if (horizontal) {
		if (_horizontalScrollLock == Qt::Vertical) {
			e->ignore();
			return;
		}
		(void)_article->consumeHorizontalScroll(
			local,
			int(std::round(delta.x())));
		e->accept();
		return;
	}
	if (_horizontalScrollLock == Qt::Horizontal) {
		e->accept();
	} else {
		e->ignore();
	}
}

void MarkdownDocumentWidget::touchEvent(QTouchEvent *e) {
	if (e->type() == QEvent::TouchCancel) {
		_pendingTouchHorizontalScrollPoint = std::nullopt;
		if (!_activeTouchHorizontalScroll) {
			return;
		}
		_activeTouchHorizontalScroll = false;
		if (_article) {
			_article->endHorizontalScroll();
		}
		e->accept();
		return;
	}
	if (!_article || e->touchPoints().isEmpty()) {
		return;
	}
	const auto point = mapFromGlobal(
		e->touchPoints().cbegin()->screenPos().toPoint());
	const auto local = ArticlePointFromWidget(point, zoomScale());
	switch (e->type()) {
	case QEvent::TouchBegin: {
		_pendingTouchHorizontalScrollPoint = std::nullopt;
		const auto hit = _article->horizontalScrollHit(local);
		_activeTouchHorizontalScroll = hit.overScrollbar
			&& _article->beginHorizontalScroll(local, false);
		if (!_activeTouchHorizontalScroll && hit.overViewport) {
			_pendingTouchHorizontalScrollPoint = local;
		}
		if (_activeTouchHorizontalScroll) {
			e->accept();
		}
	} break;
	case QEvent::TouchUpdate:
		if (_activeTouchHorizontalScroll) {
			(void)_article->updateHorizontalScroll(local);
			e->accept();
		} else if (_pendingTouchHorizontalScrollPoint) {
			const auto delta = local - *_pendingTouchHorizontalScrollPoint;
			if (delta.manhattanLength() < QApplication::startDragDistance()) {
				break;
			}
			const auto horizontal = (std::abs(delta.x()) > std::abs(delta.y()));
			if (!horizontal) {
				_pendingTouchHorizontalScrollPoint = std::nullopt;
				break;
			}
			_activeTouchHorizontalScroll = _article->beginHorizontalScroll(
				*_pendingTouchHorizontalScrollPoint,
				true);
			_pendingTouchHorizontalScrollPoint = std::nullopt;
			if (_activeTouchHorizontalScroll) {
				(void)_article->updateHorizontalScroll(local);
				e->accept();
			}
		}
		break;
	case QEvent::TouchEnd:
		_pendingTouchHorizontalScrollPoint = std::nullopt;
		if (_activeTouchHorizontalScroll) {
			_activeTouchHorizontalScroll = false;
			_article->endHorizontalScroll();
			e->accept();
		}
		break;
	default:
		break;
	}
}

void MarkdownDocumentWidget::mouseMoveEvent(QMouseEvent *e) {
	if (_activeHorizontalScrollDrag) {
		if (_article) {
			(void)_article->updateHorizontalScroll(
				ArticlePointFromWidget(e->pos(), zoomScale()));
		}
		e->accept();
		return;
	}
	dragActionUpdate(e->pos());
}

void MarkdownDocumentWidget::mousePressEvent(QMouseEvent *e) {
	if (e->button() == Qt::LeftButton) {
		if (_article && _article->beginHorizontalScroll(
				ArticlePointFromWidget(e->pos(), zoomScale()),
				false)) {
			_activeHorizontalScrollDrag = true;
			e->accept();
			return;
		}
		dragActionStart(e->pos(), e->button());
		return;
	}
	updateHover(hitTest(
		e->pos(),
		Ui::Text::StateRequest::Flag::LookupLink
			| Ui::Text::StateRequest::Flag::LookupSymbol));
	if (e->button() == Qt::MiddleButton) {
		ClickHandler::pressed();
	}
}

void MarkdownDocumentWidget::mouseReleaseEvent(QMouseEvent *e) {
	const auto weak = base::make_weak(this);
	if (_activeHorizontalScrollDrag && e->button() == Qt::LeftButton) {
		if (_article) {
			(void)_article->updateHorizontalScroll(
				ArticlePointFromWidget(e->pos(), zoomScale()));
		}
		_activeHorizontalScrollDrag = false;
		if (_article) {
			_article->endHorizontalScroll();
		}
		if (weak && rect().contains(e->pos())) {
			updateHover(hitTest(
				e->pos(),
				Ui::Text::StateRequest::Flag::LookupLink
					| Ui::Text::StateRequest::Flag::LookupSymbol));
		} else if (weak) {
			ClickHandler::clearActive(this);
			applyCursor(style::cur_default);
		}
		e->accept();
		return;
	}
	dragActionFinish(e->pos(), e->button());
	if (weak && !rect().contains(e->pos())) {
		ClickHandler::clearActive(this);
		applyCursor(style::cur_default);
	}
}

void MarkdownDocumentWidget::mouseDoubleClickEvent(QMouseEvent *e) {
	if (_article && _article->horizontalScrollHit(
			ArticlePointFromWidget(e->pos(), zoomScale())).overScrollbar) {
		return;
	}
	dragActionStart(e->pos(), e->button());
	if (_dragAction != Selecting || _selectionType != TextSelectType::Letters) {
		return;
	}
	const auto state = hitTest(
		e->pos(),
		Ui::Text::StateRequest::Flag::LookupLink
			| Ui::Text::StateRequest::Flag::LookupSymbol);
	if (!_article
		|| !_article->segmentIsText(state.segmentIndex)
		|| !state.direct
		|| !state.state.uponSymbol) {
		return;
	}
	_dragSegment = state.segmentIndex;
	_dragSymbol = selectionOffsetFromHit(state);
	_selectionType = TextSelectType::Words;
	_selection = selectionFromHit(state);
	_savedSelection = {};
	_selectionEndpoints = {
		.from = MakeSelectionEndpoint(state),
		.to = MakeSelectionEndpoint(state),
	};
	_savedSelectionEndpoints = {};
	if (_selection.from.segment == _dragSegment
		&& _selection.to.segment == _dragSegment) {
		_dragExpandedSelection = TextSelection(
			uint16(_selection.from.offset),
			uint16(_selection.to.offset));
	}
	setFocus();
	updateHover(state);
	update();
}

void MarkdownDocumentWidget::focusOutEvent(QFocusEvent *e) {
	stopPressedPlaceholderRipple();
	if (!_selection.empty()) {
		_savedSelection = _selection;
		_savedSelectionEndpoints = _selectionEndpoints;
		_selection = {};
		_selectionEndpoints = {};
		update();
	}
	ClickHandler::clearActive(this);
	applyCursor(style::cur_default);
	Ui::RpWidget::focusOutEvent(e);
}

void MarkdownDocumentWidget::focusInEvent(QFocusEvent *e) {
	if (!_savedSelection.empty()) {
		_selection = _savedSelection;
		_selectionEndpoints = _savedSelectionEndpoints;
		_savedSelection = {};
		_savedSelectionEndpoints = {};
		update();
	}
	Ui::RpWidget::focusInEvent(e);
}

bool MarkdownDocumentWidget::eventHook(QEvent *e) {
	if (e->type() == QEvent::TouchBegin
		|| e->type() == QEvent::TouchUpdate
		|| e->type() == QEvent::TouchEnd
		|| e->type() == QEvent::TouchCancel) {
		auto *ev = static_cast<QTouchEvent*>(e);
		if (ev->device()->type() == base::TouchDevice::TouchScreen) {
			const auto active = _activeTouchHorizontalScroll;
			touchEvent(ev);
			if (active || _activeTouchHorizontalScroll) {
				return true;
			}
		}
	}
	return Ui::RpWidget::eventHook(e);
}

void MarkdownDocumentWidget::leaveEventHook(QEvent *e) {
	ClickHandler::clearActive(this);
	applyCursor((_dragAction == Selecting)
		? style::cur_text
		: style::cur_default);
	Ui::RpWidget::leaveEventHook(e);
}

void MarkdownDocumentWidget::clickHandlerActiveChanged(
		const ClickHandlerPtr &,
		bool) {
	update();
}

void MarkdownDocumentWidget::clickHandlerPressedChanged(
		const ClickHandlerPtr &,
		bool) {
	update();
}

ClickHandlerPtr MarkdownDocumentWidget::linkAt(QPoint point) const {
	return hitTest(
		point,
		Ui::Text::StateRequest::Flag::LookupLink
			| Ui::Text::StateRequest::Flag::LookupSymbol).state.link;
}

MarkdownArticleHitTestResult MarkdownDocumentWidget::hitTest(
		QPoint point,
		Ui::Text::StateRequest::Flags flags) const {
	if (!_article) {
		return {};
	}
	return _article->hitTest(
		ArticlePointFromWidget(point, zoomScale()),
		flags);
}

MarkdownArticleSelection MarkdownDocumentWidget::selectionForCopy() const {
	return !_selection.empty()
		? _selection
		: _contextMenu
		? _savedSelection
		: MarkdownArticleSelection();
}

MarkdownArticleSelectionEndpoints MarkdownDocumentWidget::selectionEndpointsForCopy() const {
	return !_selection.empty()
		? _selectionEndpoints
		: _contextMenu
		? _savedSelectionEndpoints
		: MarkdownArticleSelectionEndpoints();
}

bool MarkdownDocumentWidget::selectionContains(
		MarkdownArticleSelection selection,
		const MarkdownArticleHitTestResult &result) const {
	const auto endpoints = selectionEndpointsForCopy();
	return _article
		? _article->selectionContains(
			selection,
			&endpoints,
			result)
		: false;
}

int MarkdownDocumentWidget::selectionOffsetFromHit(
		const MarkdownArticleHitTestResult &result) const {
	return _article
		? _article->selectionOffsetFromHit(result, _selectionType)
		: 0;
}

MarkdownArticleSelection MarkdownDocumentWidget::selectionFromHit(
		const MarkdownArticleHitTestResult &result) const {
	if (!_article || _dragSegment < 0 || !result.valid()) {
		return {};
	}
	auto first = _dragSymbol;
	auto second = selectionOffsetFromHit(result);
	if (_selectionType != TextSelectType::Letters
		&& !_dragExpandedSelection.empty()
		&& result.segmentIndex != _dragSegment) {
		first = (CompareSelectionPositions(
			MarkdownArticleSelectionPosition{ result.segmentIndex, second },
			MarkdownArticleSelectionPosition{ _dragSegment, _dragSymbol }) < 0)
			? _dragExpandedSelection.to
			: _dragExpandedSelection.from;
	}
	if (result.segmentIndex == _dragSegment
		&& _article->segmentIsText(_dragSegment)) {
		const auto adjusted = _article->adjustSelection(
			_dragSegment,
			TextSelection(
				uint16(std::min(first, second)),
				uint16(std::max(first, second))),
			_selectionType);
		return {
			{ _dragSegment, adjusted.from },
			{ _dragSegment, adjusted.to },
		};
	}
	return NormalizeSelection({
		{ _dragSegment, first },
		{ result.segmentIndex, second },
	});
}

TextForMimeData MarkdownDocumentWidget::getSelectedText() const {
	const auto endpoints = selectionEndpointsForCopy();
	return _article
		? _article->textForSelection(
			selectionForCopy(),
			&endpoints)
		: TextForMimeData();
}

QVariant MarkdownDocumentWidget::clickHandlerContext() const {
	return _clickHandlerContextRef
		? *_clickHandlerContextRef
		: _clickHandlerContext;
}

QVariant MarkdownDocumentWidget::viewerToastClickHandlerContext() const {
	const auto context = clickHandlerContext().value<ClickHandlerContext>();
	if (!context.show) {
		return clickHandlerContext();
	}
	auto sanitized = context;
	sanitized.sessionWindow = base::weak_ptr<Window::SessionController>();
	return QVariant::fromValue(sanitized);
}

void MarkdownDocumentWidget::showToast(const QString &text) const {
	const auto context = clickHandlerContext().value<ClickHandlerContext>();
	if (context.show) {
		context.show->showToast(text);
	}
}

void MarkdownDocumentWidget::copySelectedText() {
	if (const auto text = getSelectedText(); !text.empty()) {
		TextUtilities::SetClipboardText(text);
		showToast(tr::lng_text_copied(tr::now));
	}
}

void MarkdownDocumentWidget::copyCodeBlock(
		const MarkdownArticleHitTestResult &state) {
	if (!_article) {
		return;
	}
	auto text = _article->textForContext(state);
	if (text.empty()) {
		return;
	}
	if (!text.rich.text.endsWith('\n')) {
		text.rich.text.append('\n');
	}
	if (!text.expanded.endsWith('\n')) {
		text.expanded.append('\n');
	}
	if (Ui::Integration::Instance().copyPreOnClick(
			viewerToastClickHandlerContext())) {
		TextUtilities::SetClipboardText(std::move(text));
	}
}

void MarkdownDocumentWidget::syncArticleVisibleTopBottom() {
	if (!_article) {
		return;
	}
	const auto scale = zoomScale();
	_article->setVisibleTopBottom(
		int(std::floor(_visibleRange.top / scale)),
		int(std::ceil(_visibleRange.bottom / scale)));
}

void MarkdownDocumentWidget::relayoutCurrentWidth(bool clearSelection) {
	if (clearSelection) {
		this->clearSelection();
	}
	if (!_article) {
		_lastRelayoutMs = 0;
		return;
	}
	const auto scale = zoomScale();
	const auto layoutWidth = std::max(int(std::floor(width() / scale)), 1);
	auto timer = QElapsedTimer();
	timer.start();
	const auto articleHeight = _article->resizeGetHeight(layoutWidth);
	syncArticleVisibleTopBottom();
	(void)articleHeight;
	_lastRelayoutMs = int(timer.elapsed());
}

void MarkdownDocumentWidget::forceRelayoutCurrentWidth() {
	resizeToWidth(width());
	update();
}

void MarkdownDocumentWidget::updateHover(
		const MarkdownArticleHitTestResult &state) {
	const auto changed = ClickHandler::setActive(state.state.link, this);
	auto cursor = style::cur_default;
	if (_dragAction == NoDrag) {
		if (state.codeHeaderCopy
			|| state.state.link
			|| (state.preparedLink
				&& state.preparedLink->kind == PreparedLinkKind::ToggleDetails)
			|| state.mediaActivation.kind != MediaActivationKind::None) {
			cursor = style::cur_pointer;
		} else if (state.direct) {
			cursor = style::cur_text;
		}
	} else {
		if (_dragAction == Selecting) {
			const auto selection = selectionFromHit(state);
			const auto endpoints = MarkdownArticleSelectionEndpoints{
				.from = _selectionEndpoints.from.valid()
					? _selectionEndpoints.from
					: MarkdownArticleSelectionEndpoint{ _dragSegment, false },
				.to = MakeSelectionEndpoint(state),
			};
			const auto endpointsChanged
				= (_selectionEndpoints.from.segment != endpoints.from.segment)
				|| (_selectionEndpoints.from.direct != endpoints.from.direct)
				|| (_selectionEndpoints.to.segment != endpoints.to.segment)
				|| (_selectionEndpoints.to.direct != endpoints.to.direct);
			if (_selection != selection || endpointsChanged) {
				_selection = selection;
				_selectionEndpoints = endpoints;
				_savedSelection = {};
				_savedSelectionEndpoints = {};
				setFocus();
				update();
			} else {
				_selectionEndpoints = endpoints;
			}
			cursor = style::cur_text;
		} else if (ClickHandler::getPressed()) {
			cursor = style::cur_pointer;
		}
	}
	if (changed || cursor != _cursor) {
		applyCursor(cursor);
	}
}

void MarkdownDocumentWidget::updateHoverAtCursor() {
	const auto point = mapFromGlobal(QCursor::pos());
	if (rect().contains(point)) {
		updateHover(hitTest(
			point,
			Ui::Text::StateRequest::Flag::LookupLink
				| Ui::Text::StateRequest::Flag::LookupSymbol));
	} else {
		ClickHandler::clearActive(this);
		applyCursor(style::cur_default);
	}
}

void MarkdownDocumentWidget::resetSelection() {
	_selection = {};
	_savedSelection = {};
	_selectionEndpoints = {};
	_savedSelectionEndpoints = {};
	_selectionType = TextSelectType::Letters;
	_dragAction = NoDrag;
	_dragStartPosition = QPoint();
	_dragSegment = -1;
	_dragSymbol = 0;
	_dragExpandedSelection = {};
	_selectionClickPreparedLink = std::nullopt;
	_dragStartHadSelection = false;
}

void MarkdownDocumentWidget::clearSelection() {
	const auto hadSelection = !_selection.empty()
		|| !_savedSelection.empty()
		|| (_dragAction != NoDrag);
	resetSelection();
	if (hadSelection) {
		update();
	}
}

void MarkdownDocumentWidget::resetTextPaintCaches() {
	_prePaintCache = nullptr;
	_blockquotePaintCache = nullptr;
}

QRect MarkdownDocumentWidget::articleRectToWidget(QRect articleRect) const {
	if (articleRect.isEmpty()) {
		return rect();
	}
	const auto scale = zoomScale();
	const auto left = int(std::floor(articleRect.x() * scale));
	const auto top = int(std::floor(articleRect.y() * scale));
	const auto right = int(std::ceil(
		(articleRect.x() + articleRect.width()) * scale));
	const auto bottom = int(std::ceil(
		(articleRect.y() + articleRect.height()) * scale));
	return QRect(
		left,
		top,
		std::max(right - left, 1),
		std::max(bottom - top, 1));
}

Ui::Text::QuotePaintCache *MarkdownDocumentWidget::ensurePrePaintCache() {
	EnsurePrePaintCache(_prePaintCache, st::inTextPalette.monoFg);
	return _prePaintCache.get();
}

Ui::Text::QuotePaintCache *MarkdownDocumentWidget::ensureBlockquotePaintCache() {
	EnsureBlockquotePaintCache(
		_blockquotePaintCache,
		st::defaultMarkdown.quotePaintColors.blockquote);
	return _blockquotePaintCache.get();
}

MarkdownArticlePaintContext MarkdownDocumentWidget::textPaintContext(
		QRect clip) {
	const auto scale = zoomScale();
	const auto logicalRect = QRect(QPoint(), QSize(
		std::max(int(std::floor(width() / scale)), 1),
		std::max(int(std::floor(height() / scale)), 1)));
	auto context = MarkdownArticlePaintContext(_theme->preparePaintContext(
		_style.get(),
		logicalRect,
		logicalRect,
		clip,
		!window()->isActiveWindow()));
	context.caches = {
		.pre = ensurePrePaintCache(),
		.blockquote = ensureBlockquotePaintCache(),
		.colors = _highlightColors,
		.repaint = [=] {
			crl::on_main(this, [=] {
				update();
			});
		},
		.repaintRect = [=](QRect articleRect) {
			crl::on_main(this, [=] {
				if (articleRect.isEmpty()) {
					update();
				} else {
					update(articleRectToWidget(articleRect));
				}
			});
		},
	};
	context.selectionState.selection = _selection;
	context.selectionState.endpoints = &_selectionEndpoints;
	return context;
}

void MarkdownDocumentWidget::stopPressedPlaceholderRipple() {
	if (_pressedPlaceholderId) {
		stopPlaceholderRipple(_pressedPlaceholderId);
		_pressedPlaceholderId = {};
	}
}

void MarkdownDocumentWidget::dragActionStart(
		QPoint point,
		Qt::MouseButton button) {
	stopPressedPlaceholderRipple();
	const auto state = hitTest(
		point,
		Ui::Text::StateRequest::Flag::LookupLink
			| Ui::Text::StateRequest::Flag::LookupSymbol);
	updateHover(state);
	if (button != Qt::LeftButton) {
		return;
	}
	if (state.codeHeaderCopy) {
		clearSelection();
		_dragStartPosition = point;
		_selectionClickPreparedLink = std::nullopt;
		_dragAction = PrepareDrag;
		return;
	}
	if (state.mediaActivation.kind == MediaActivationKind::Embed
		&& state.mediaActivation.placeholderId) {
		_pressedPlaceholderId = state.mediaActivation.placeholderId;
		addPlaceholderRipple(
			state.mediaActivation.placeholderId,
			state.placeholderLocalPoint);
	}
	_dragStartPosition = point;
	_dragStartHadSelection = !selectionForCopy().empty();
	_selectionClickPreparedLink = (state.preparedLink
		&& state.preparedLink->kind == PreparedLinkKind::ToggleDetails)
		? state.preparedLink
		: std::nullopt;
	ClickHandler::pressed();
	_dragAction = NoDrag;
	_dragExpandedSelection = {};
	_dragSegment = -1;
	_dragSymbol = 0;
	if (ClickHandler::getPressed()) {
		_dragStartPosition = point;
		_dragAction = PrepareDrag;
		return;
	}
	if (!state.valid()) {
		clearSelection();
		return;
	}
	_dragSegment = state.segmentIndex;
	_dragSymbol = selectionOffsetFromHit(state);
	_selection = {
		{ _dragSegment, _dragSymbol },
		{ _dragSegment, _dragSymbol },
	};
	_savedSelection = {};
	_selectionEndpoints = {
		.from = MakeSelectionEndpoint(state),
		.to = MakeSelectionEndpoint(state),
	};
	_savedSelectionEndpoints = {};
	_dragAction = Selecting;
	update();
}

MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionUpdate(QPoint point) {
	const auto state = hitTest(
		point,
		Ui::Text::StateRequest::Flag::LookupLink
			| Ui::Text::StateRequest::Flag::LookupSymbol);
	if (_dragAction == PrepareDrag
		&& (point - _dragStartPosition).manhattanLength()
			>= QApplication::startDragDistance()) {
		_dragAction = Dragging;
	}
	updateHover(state);
	return state;
}

MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
		QPoint point,
		Qt::MouseButton button) {
	const auto state = dragActionUpdate(point);
	stopPressedPlaceholderRipple();
	auto activated = ClickHandler::unpressed();
	const auto dragStartHadSelection = _dragStartHadSelection;
	const auto wasClick = (_dragAction == NoDrag)
		|| (_dragAction == PrepareDrag);
	const auto toggleFromDetailsClick = !dragStartHadSelection
		&& _selection.empty()
		&& _selectionClickPreparedLink
		&& (point - _dragStartPosition).manhattanLength()
			< QApplication::startDragDistance()
		&& state.preparedLink
		&& state.preparedLink->kind == PreparedLinkKind::ToggleDetails
		&& state.preparedLink->target == _selectionClickPreparedLink->target;
	if (_dragAction == Dragging
		|| (_dragAction == Selecting && !_selection.empty())) {
		activated = nullptr;
	} else if (_dragAction == PrepareDrag && button != Qt::RightButton) {
		clearSelection();
	}
	const auto preparedToggle = toggleFromDetailsClick
		? state.preparedLink
		: std::nullopt;
	_dragStartHadSelection = false;
	_dragAction = NoDrag;
	_selectionType = TextSelectType::Letters;
	_dragExpandedSelection = {};
	updateHover(state);
	if (state.codeHeaderCopy
		&& (button == Qt::LeftButton || button == Qt::MiddleButton)
		&& wasClick) {
		copyCodeBlock(state);
		return state;
	}
	if (activated
		&& (button == Qt::LeftButton || button == Qt::MiddleButton)) {
		if (state.mediaActivation.kind != MediaActivationKind::None
			&& _activateMedia
			&& _activateMedia(state.mediaActivation, button)) {
			return state;
		}
		if (state.preparedLink && _activateLink) {
			if (state.preparedLink->kind == PreparedLinkKind::ToggleDetails
				&& dragStartHadSelection) {
				return state;
			}
			_activateLink(*state.preparedLink, button);
		} else {
			auto clickHandlerContext = this->clickHandlerContext();
			const auto monospace = std::dynamic_pointer_cast<MonospaceClickHandler>(
				activated);
			const auto pre = dynamic_cast<Ui::Text::PreClickHandler*>(
				activated.get());
			if (monospace || pre) {
				clickHandlerContext = viewerToastClickHandlerContext();
			}
			if (monospace) {
				const auto context = clickHandlerContext.value<ClickHandlerContext>();
				if (context.show) {
					const auto handled = Ui::Integration::Instance().copyPreOnClick(
						clickHandlerContext);
					static_cast<void>(handled);
				}
			}
			auto context = ClickContext();
			context.button = button;
			context.other = std::move(clickHandlerContext);
			ActivateClickHandler(window(), activated, context);
		}
	} else if (preparedToggle
		&& (button == Qt::LeftButton || button == Qt::MiddleButton)
		&& _activateLink) {
		clearSelection();
		_activateLink(*preparedToggle, button);
		return state;
	} else if ((button == Qt::LeftButton || button == Qt::MiddleButton)
		&& state.mediaActivation.kind != MediaActivationKind::None
		&& _activateMedia
		&& _activateMedia(state.mediaActivation, button)) {
		return state;
	}
	if (QGuiApplication::clipboard()->supportsSelection()
		&& !_selection.empty()) {
		if (const auto text = getSelectedText(); !text.empty()) {
			TextUtilities::SetClipboardText(text, QClipboard::Selection);
		}
	}
	return state;
}

void MarkdownDocumentWidget::applyCursor(style::cursor cursor) {
	if (_cursor != cursor) {
		_cursor = cursor;
		setCursor(_cursor);
	}
}

double MarkdownDocumentWidget::zoomScale() const {
	return std::max(_zoom, 1) / 100.;
}

} // namespace Iv::Markdown
