/*
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_prepare_native_blocks.h"
#include "base/unixtime.h"
#include "iv/iv_rich_page.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "lang/lang_keys.h"

#include <algorithm>
#include <limits>
#include <utility>

namespace Iv::Markdown {
namespace {

using RichPage = Iv::RichPage;
using RichPageBlock = Iv::RichPage::Block;
using RichPageBlockKind = Iv::RichPage::BlockKind;
using RichPageListItem = Iv::RichPage::ListItem;
using RichPageRelatedArticle = Iv::RichPage::RelatedArticle;
using RichPageTableCell = Iv::RichPage::TableCell;
using RichPageTableRow = Iv::RichPage::TableRow;

struct NativeIvDepthContext {
	int listDepth = 0;
	int quoteDepth = 0;
};

[[nodiscard]] int CappedNativeIvListDepth(int depth) {
	return std::min(depth, std::max(PrepareLimitsForIv().visualListDepth, 0));
}

[[nodiscard]] int CappedNativeIvQuoteDepth(int depth) {
	return std::min(depth, std::max(PrepareLimitsForIv().visualQuoteDepth, 0));
}

[[nodiscard]] bool PrepareCanonicalNativeIvBlocks(
	const std::vector<RichPageBlock> &blocks,
	std::vector<PreparedBlock> *result,
	NativeIvPrepareState *state,
	PreparedEditBlockContainerPath container,
	NativeIvDepthContext depthContext);

[[nodiscard]] QString NativeIvDateText(TimeId date) {
	return langDateTimeFull(base::unixtime::parse(date));
}

[[nodiscard]] int ScaleNativeIvFormulaCap(
		int cap,
		int textSize,
		int baseTextSize) {
	if (cap <= 0) {
		return 0;
	}
	const auto numerator = int64(cap) * std::max(textSize, 1);
	const auto denominator = std::max(baseTextSize, 1);
	return std::max(int((numerator + denominator - 1) / denominator), 1);
}

[[nodiscard]] NativeIvRichTextContext NativeIvRichTextContextForTextSize(
		int textSize,
		const MarkdownPrepareDimensions &dimensions) {
	return {
		.textSize = textSize,
		.renderWidthCap = ScaleNativeIvFormulaCap(
			dimensions.displayMathMaxRenderWidth,
			textSize,
			dimensions.displayMathTextSize),
		.renderHeightCap = ScaleNativeIvFormulaCap(
			dimensions.displayMathMaxRenderHeight,
			textSize,
			dimensions.displayMathTextSize),
	};
}

[[nodiscard]] int NativeIvFlowTextSize(
		PreparedBlockKind kind,
		int headingLevel,
		const MarkdownPrepareDimensions &dimensions) {
	if (kind == PreparedBlockKind::Heading
		&& headingLevel >= 1
		&& headingLevel <= int(dimensions.headingTextSizes.size())) {
		return dimensions.headingTextSizes[headingLevel - 1];
	}
	return dimensions.bodyTextSize;
}

[[nodiscard]] PreparedLink PrepareNativeIvRelatedArticleLink(
		QString url,
		uint64 webpageId,
		QStringView renderedText) {
	auto result = PreparedLink();
	if (webpageId) {
		result.kind = PreparedLinkKind::InstantViewPage;
		result.webpageId = webpageId;
		NormalizePreparedUrlLink(&result, url);
	} else {
		result = ClassifiedLink(0, url, nullptr);
		if (result.kind == PreparedLinkKind::RejectedRelative
			|| result.kind == PreparedLinkKind::LocalFile) {
			result.kind = PreparedLinkKind::External;
			NormalizePreparedUrlLink(&result, url);
		}
	}
	FinalizePreparedUrlLink(&result, renderedText);
	return result;
}

[[nodiscard]] QString NativeIvDetailsAnchorId(NativeIvPrepareState *state) {
	return u"details-"_q + QString::number(++state->nextGeneratedId);
}

void SortPreparedIvRichText(PreparedIvRichText *text) {
	SortEntities(&text->text);
}

[[nodiscard]] TextWithEntities StripOneTrailingNewline(TextWithEntities text) {
	auto removed = 0;
	if (text.text.endsWith(u"\r\n"_q)) {
		removed = 2;
	} else if (!text.text.isEmpty()) {
		const auto last = text.text.back();
		if ((last == QChar(u'\n')) || (last == QChar(u'\r'))) {
			removed = 1;
		}
	}
	if (!removed) {
		return text;
	}
	const auto newLength = text.text.size() - removed;
	text.text.chop(removed);
	for (auto &entity : text.entities) {
		entity.updateTextEnd(newLength);
	}
	text.entities.erase(
		std::remove_if(
			text.entities.begin(),
			text.entities.end(),
			[](const EntityInText &entity) {
				return (entity.length() <= 0);
			}),
		text.entities.end());
	return text;
}

[[nodiscard]] bool RichTextHasMeaningfulContent(
		const RichPage::RichText &text) {
	return !text.text.text.trimmed().isEmpty()
		|| !text.anchorId.isEmpty()
		|| !text.anchorIds.empty();
}

[[nodiscard]] PreparedBlock EmptyParagraphBlock() {
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::Paragraph;
	return block;
}

[[nodiscard]] PreparedBlock PrepareNativeIvEmbedPostFallbackParagraph(
		QString url) {
	auto block = EmptyParagraphBlock();
	if (url.isEmpty()) {
		return block;
	}
	auto link = PreparedLink();
	link.index = 1;
	link.kind = PreparedLinkKind::External;
	NormalizePreparedUrlLink(&link, url);
	FinalizePreparedUrlLink(&link, url);
	block.text.text = std::move(url);
	block.text.entities.push_back(EntityInText(
		EntityType::CustomUrl,
		0,
		block.text.text.size(),
		InternalLinkData(link.index)));
	block.links.push_back(std::move(link));
	return block;
}

[[nodiscard]] PreparedBlock PrepareRuleBlock() {
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::Rule;
	return block;
}

void WrapPreparedIvRichTextItalic(PreparedIvRichText *prepared) {
	if (!prepared || prepared->text.text.isEmpty()) {
		return;
	}
	prepared->text.entities.push_back(EntityInText(
		EntityType::Italic,
		0,
		prepared->text.text.size()));
}

void ApplyNativeIvEditPlaceholderText(PreparedBlock *block);

bool AppendPreparedQuoteParagraph(
		std::vector<PreparedBlock> *result,
		PreparedIvRichText prepared,
		bool pullquote,
		bool supplementary = false,
		bool allowEmpty = false,
		std::optional<PreparedEditLeafSource> editLeaf = std::nullopt) {
	if (pullquote) {
		WrapPreparedIvRichTextItalic(&prepared);
	}
	const auto count = result->size();
	if (!AppendPreparedIvRichBlock(
			result,
			PreparedBlockKind::Paragraph,
			0,
			std::move(prepared),
			QString(),
			allowEmpty,
			supplementary,
			std::nullopt,
			std::move(editLeaf))) {
		return false;
	}
	if (result->size() > count) {
		if (allowEmpty) {
			ApplyNativeIvEditPlaceholderText(&result->back());
		}
		if (pullquote) {
			result->back().flowAlignment = TableAlignment::Center;
			result->back().pullquote = true;
		}
	}
	return true;
}

[[nodiscard]] bool ParseOrderedNumber(
		const QString &value,
		int *result) {
	auto ok = false;
	const auto parsed = value.toInt(&ok);
	if (!ok) {
		return false;
	}
	*result = parsed;
	return true;
}

[[nodiscard]] int NextNativeIvOrderedNumber(const PreparedBlock &result) {
	return result.children.empty()
		? result.startNumber
		: (result.children.back().orderedNumber + 1);
}

using NativeIvTableOccupancyRow = std::vector<char>;
using NativeIvTableOccupancyGrid = std::vector<NativeIvTableOccupancyRow>;

[[nodiscard]] int NormalizeNativeIvTableSpan(int span) {
	return std::max(span, 1);
}

[[nodiscard]] int ClampNativeIvTableRowspan(
		int rawRowspan,
		int row,
		int rowCount) {
	if ((row < 0) || (row >= rowCount) || (rowCount <= 0)) {
		return 0;
	}
	const auto remainingRows = int64(rowCount) - row;
	return int(std::min<int64>(
		NormalizeNativeIvTableSpan(rawRowspan),
		remainingRows));
}

[[nodiscard]] int ClampNativeIvTableColspan(
		int rawColspan,
		int column,
		int maxColumns) {
	if ((column < 0) || (column >= maxColumns) || (maxColumns <= 0)) {
		return 0;
	}
	const auto remainingColumns = int64(maxColumns) - column;
	return int(std::min<int64>(
		NormalizeNativeIvTableSpan(rawColspan),
		remainingColumns));
}

[[nodiscard]] bool CanOccupyNativeIvTableSlots(
		const NativeIvTableOccupancyGrid &occupancy,
		int row,
		int column,
		int rowspan,
		int colspan) {
	if ((row < 0)
		|| (column < 0)
		|| (rowspan <= 0)
		|| (colspan <= 0)
		|| (row >= int(occupancy.size()))) {
		return false;
	}
	const auto rowLimit = int(std::min<int64>(
		int64(row) + rowspan,
		occupancy.size()));
	const auto columnLimit64 = int64(column) + colspan;
	if (columnLimit64 <= column) {
		return false;
	}
	const auto columnLimit = int(std::min<int64>(
		columnLimit64,
		std::numeric_limits<int>::max()));
	for (auto currentRow = row; currentRow < rowLimit; ++currentRow) {
		const auto &occupied = occupancy[currentRow];
		const auto occupiedLimit = std::min(columnLimit, int(occupied.size()));
		for (auto currentColumn = column;
			currentColumn < occupiedLimit;
			++currentColumn) {
			if (occupied[currentColumn]) {
				return false;
			}
		}
	}
	return true;
}

[[nodiscard]] int FirstAvailableNativeIvTableColumn(
		const NativeIvTableOccupancyGrid &occupancy,
		int row,
		int rowspan,
		int colspan,
		int maxColumns) {
	if ((row < 0)
		|| (row >= int(occupancy.size()))
		|| (rowspan <= 0)
		|| (colspan <= 0)
		|| (maxColumns <= 0)) {
		return -1;
	}
	for (auto column = 0; column < maxColumns; ++column) {
		const auto effectiveColspan = ClampNativeIvTableColspan(
			colspan,
			column,
			maxColumns);
		if (effectiveColspan <= 0) {
			continue;
		}
		if (CanOccupyNativeIvTableSlots(
				occupancy,
				row,
				column,
				rowspan,
				effectiveColspan)) {
			return column;
		}
	}
	return -1;
}

void MarkNativeIvTableSlots(
		NativeIvTableOccupancyGrid *occupancy,
		int row,
		int column,
		int rowspan,
		int colspan) {
	if ((row < 0)
		|| (column < 0)
		|| (rowspan <= 0)
		|| (colspan <= 0)
		|| (row >= int(occupancy->size()))) {
		return;
	}
	const auto rowLimit = int(std::min<int64>(
		int64(row) + rowspan,
		occupancy->size()));
	const auto columnLimit64 = int64(column) + colspan;
	if (columnLimit64 <= column) {
		return;
	}
	const auto columnLimit = int(std::min<int64>(
		columnLimit64,
		std::numeric_limits<int>::max()));
	for (auto currentRow = row; currentRow < rowLimit; ++currentRow) {
		auto &occupied = (*occupancy)[currentRow];
		if (columnLimit > int(occupied.size())) {
			occupied.resize(columnLimit, false);
		}
		for (auto currentColumn = column;
			currentColumn < columnLimit;
			++currentColumn) {
			occupied[currentColumn] = true;
		}
	}
}

[[nodiscard]] int NativeIvTableColumnCount(
		const NativeIvTableOccupancyGrid &occupancy) {
	auto result = 0;
	for (const auto &row : occupancy) {
		result = std::max(result, int(row.size()));
	}
	return result;
}

[[maybe_unused]] [[nodiscard]] PreparedEditBlockContainerPath BlockChildrenContainer(
		PreparedEditBlockPath block) {
	auto result = std::move(block.container);
	result.steps.push_back({
		.kind = PreparedEditBlockContainerKind::BlockChildren,
		.blockIndex = block.index,
	});
	return result;
}

[[maybe_unused]] [[nodiscard]] PreparedEditBlockContainerPath ListItemChildrenContainer(
		PreparedEditBlockPath block,
		int listItemIndex) {
	auto result = std::move(block.container);
	result.steps.push_back({
		.kind = PreparedEditBlockContainerKind::ListItemChildren,
		.blockIndex = block.index,
		.listItemIndex = listItemIndex,
	});
	return result;
}

[[nodiscard]] PreparedEditBlockSource BlockSource(
		PreparedEditBlockPath block) {
	return { .path = std::move(block) };
}

[[maybe_unused]] [[nodiscard]] PreparedEditListItemSource ListItemSource(
		PreparedEditBlockPath block,
		int listItemIndex) {
	return {
		.block = std::move(block),
		.listItemIndex = listItemIndex,
	};
}

[[maybe_unused]] [[nodiscard]] PreparedEditTableRowSource TableRowSource(
		PreparedEditBlockPath block,
		int tableRowIndex) {
	return {
		.block = std::move(block),
		.tableRowIndex = tableRowIndex,
	};
}

[[maybe_unused]] [[nodiscard]] PreparedEditTableCellSource TableCellSource(
		PreparedEditBlockPath block,
		int tableRowIndex,
		int tableCellIndex,
		int column,
		int colspan,
		int rowspan) {
	return {
		.block = std::move(block),
		.tableRowIndex = tableRowIndex,
		.tableCellIndex = tableCellIndex,
		.column = column,
		.colspan = colspan,
		.rowspan = rowspan,
	};
}

[[nodiscard]] PreparedEditLeafSource BlockTextLeafSource(
		PreparedEditBlockPath block) {
	return {
		.kind = PreparedEditLeafKind::BlockText,
		.block = std::move(block),
	};
}

[[nodiscard]] PreparedEditLeafSource BlockCaptionLeafSource(
		PreparedEditBlockPath block) {
	return {
		.kind = PreparedEditLeafKind::BlockCaption,
		.block = std::move(block),
	};
}

[[nodiscard]] PreparedEditLeafSource ListItemTextLeafSource(
		PreparedEditBlockPath block,
		int listItemIndex) {
	return {
		.kind = PreparedEditLeafKind::ListItemText,
		.block = std::move(block),
		.listItemIndex = listItemIndex,
	};
}

[[maybe_unused]] [[nodiscard]] PreparedEditLeafSource TableCellTextLeafSource(
		PreparedEditBlockPath block,
		int tableRowIndex,
		int tableCellIndex) {
	return {
		.kind = PreparedEditLeafKind::TableCellText,
		.block = std::move(block),
		.tableRowIndex = tableRowIndex,
		.tableCellIndex = tableCellIndex,
	};
}

[[nodiscard]] PreparedEditLeafSource MathFormulaLeafSource(
		PreparedEditBlockPath block) {
	return {
		.kind = PreparedEditLeafKind::MathFormula,
		.block = std::move(block),
	};
}

void ApplyBlockCaptionEditSource(
		PreparedBlock *block,
		PreparedEditBlockPath path) {
	block->editBlock = BlockSource(path);
	block->editLeaf = BlockCaptionLeafSource(path);
}

[[nodiscard]] QString NativeIvEditPlaceholderText(
		PreparedBlockKind kind,
		PreparedEditLeafKind leafKind) {
	switch (leafKind) {
	case PreparedEditLeafKind::BlockCaption:
		return u"Caption"_q;
	case PreparedEditLeafKind::TableCellText:
		return u"Cell"_q;
	case PreparedEditLeafKind::MathFormula:
		return u"x^2 + y^2"_q;
	case PreparedEditLeafKind::ListItemText:
		return u"Text"_q;
	case PreparedEditLeafKind::BlockText:
		if (kind == PreparedBlockKind::Table) {
			return u"Title"_q;
		}
		return (kind == PreparedBlockKind::Heading)
			|| (kind == PreparedBlockKind::Details)
			? u"Header"_q
			: u"Text"_q;
	}
	return QString();
}

void ApplyNativeIvEditPlaceholderText(PreparedBlock *block) {
	if (!block->editLeaf) {
		return;
	}
	block->editPlaceholderText = NativeIvEditPlaceholderText(
		block->kind,
		block->editLeaf->kind);
}

void ApplyNativeIvEditPlaceholderText(PreparedTableCell *cell) {
	if (!cell->editLeaf) {
		return;
	}
	cell->editPlaceholderText = NativeIvEditPlaceholderText(
		PreparedBlockKind::Table,
		cell->editLeaf->kind);
}

[[nodiscard]] const std::vector<RichPageBlock> *ResolveCanonicalNativeIvContainer(
		const RichPage &page,
		const PreparedEditBlockContainerPath &container) {
	auto blocks = &page.blocks;
	for (const auto &step : container.steps) {
		if (step.kind == PreparedEditBlockContainerKind::Root) {
			blocks = &page.blocks;
			continue;
		}
		if (step.blockIndex < 0 || step.blockIndex >= int(blocks->size())) {
			return nullptr;
		}
		const auto &block = (*blocks)[step.blockIndex];
		switch (step.kind) {
		case PreparedEditBlockContainerKind::BlockChildren:
			blocks = &block.blocks;
			break;
		case PreparedEditBlockContainerKind::ListItemChildren:
			if (step.listItemIndex < 0
				|| step.listItemIndex >= int(block.listItems.size())) {
				return nullptr;
			}
			blocks = &block.listItems[step.listItemIndex].blocks;
			break;
		case PreparedEditBlockContainerKind::Root:
			blocks = &page.blocks;
			break;
		}
	}
	return blocks;
}

[[nodiscard]] const RichPageBlock *ResolveCanonicalNativeIvBlock(
		const RichPage &page,
		const PreparedEditBlockPath &path) {
	const auto container = ResolveCanonicalNativeIvContainer(page, path.container);
	if (!container || path.index < 0 || path.index >= int(container->size())) {
		return nullptr;
	}
	return &(*container)[path.index];
}

[[nodiscard]] PreparedBlock *FindPreparedNativeIvBlockByPath(
		std::vector<PreparedBlock> *blocks,
		const PreparedEditBlockPath &path) {
	for (auto &block : *blocks) {
		if (block.editBlock && block.editBlock->path == path) {
			return &block;
		}
		if (const auto nested = FindPreparedNativeIvBlockByPath(
				&block.children,
				path)) {
			return nested;
		}
	}
	return nullptr;
}

[[nodiscard]] PreparedBlock *FindPreparedNativeIvLeafBlock(
		PreparedBlock *block,
		const PreparedEditLeafSource &source) {
	if (!block) {
		return nullptr;
	}
	if (block->editLeaf && (*block->editLeaf == source)) {
		return block;
	}
	for (auto &child : block->children) {
		if (const auto nested = FindPreparedNativeIvLeafBlock(&child, source)) {
			return nested;
		}
	}
	return nullptr;
}

[[nodiscard]] PreparedTableCell *FindPreparedNativeIvLeafCell(
		PreparedBlock *block,
		const PreparedEditLeafSource &source) {
	if (!block) {
		return nullptr;
	}
	for (auto &row : block->tableRows) {
		for (auto &cell : row.cells) {
			if (cell.editLeaf && (*cell.editLeaf == source)) {
				return &cell;
			}
		}
	}
	return nullptr;
}

void StripPreparedDetailsSummaryLinks(PreparedIvRichText *summary) {
	if (summary->links.empty()) {
		return;
	}
	const auto from = std::remove_if(
		summary->text.entities.begin(),
		summary->text.entities.end(),
		[](const EntityInText &entity) {
			return entity.type() == EntityType::CustomUrl;
		});
	summary->text.entities.erase(from, summary->text.entities.end());
	summary->links.clear();
}

void RefreshPreparedNativeIvBlockPlaceholder(
		PreparedBlock *block,
		NativeIvPrepareState *state) {
	block->editPlaceholderText = QString();
	if (state->editMode && block->editLeaf) {
		ApplyNativeIvEditPlaceholderText(block);
	}
}

void RefreshPreparedNativeIvCellPlaceholder(
		PreparedTableCell *cell,
		NativeIvPrepareState *state) {
	cell->editPlaceholderText = QString();
	if (state->editMode && cell->editLeaf) {
		ApplyNativeIvEditPlaceholderText(cell);
	}
}

void RefreshPreparedNativeIvMediaCaptionPlaceholder(
		PreparedBlock *block,
		NativeIvPrepareState *state) {
	block->editPlaceholderText = QString();
	if (state->editMode && block->editLeaf) {
		ApplyNativeIvEditPlaceholderText(block);
	}
}

void RefreshPreparedNativeIvPlaceholderCopyText(PreparedBlock *block) {
	if (block->kind != PreparedBlockKind::Placeholder) {
		return;
	}
	block->placeholder.copyText = block->text.text.isEmpty()
		? block->placeholder.label
		: (block->placeholder.label + u"\n"_q + block->text.text);
}

[[nodiscard]] bool PreparePreparedNativeIvRichText(
		const RichPage::RichText &text,
		PreparedIvRichText *prepared,
		NativeIvPrepareState *state,
		NativeIvRichTextContext context = {}) {
	return PrepareNativeIvRichText(text, prepared, nullptr, state, context);
}

[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvBlockText(
		PreparedBlock *preparedBlock,
		const RichPageBlock &canonicalBlock,
		const PreparedEditLeafSource &source,
		NativeIvPrepareState *state,
		NativeIvPreparedLeafFormulaRange *formulaRange) {
	formulaRange->from = state->nextFormulaIndex;
	formulaRange->till = state->nextFormulaIndex;
	switch (canonicalBlock.kind) {
	case RichPageBlockKind::Heading:
	case RichPageBlockKind::Paragraph:
	case RichPageBlockKind::Footer: {
		if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		const auto kind = (canonicalBlock.kind == RichPageBlockKind::Heading)
			? PreparedBlockKind::Heading
			: PreparedBlockKind::Paragraph;
		const auto context = NativeIvRichTextContextForTextSize(
			NativeIvFlowTextSize(
				kind,
				canonicalBlock.headingLevel,
				state->dimensions),
			state->dimensions);
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.text,
				&prepared,
				state,
				context)) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		SortPreparedIvRichText(&prepared);
		preparedBlock->text = std::move(prepared.text);
		preparedBlock->links = std::move(prepared.links);
		RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
	} break;
	case RichPageBlockKind::Code: {
		if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.text,
				&prepared,
				state,
				{ .dropClickHandlers = true })) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		SortPreparedIvRichText(&prepared);
		preparedBlock->text = StripOneTrailingNewline(std::move(prepared.text));
		preparedBlock->links = std::move(prepared.links);
		RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
	} break;
	case RichPageBlockKind::Quote: {
		const auto target = FindPreparedNativeIvLeafBlock(preparedBlock, source);
		if (!target) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.text,
				&prepared,
				state)) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		if (canonicalBlock.pullquote) {
			WrapPreparedIvRichTextItalic(&prepared);
		}
		SortPreparedIvRichText(&prepared);
		target->text = std::move(prepared.text);
		target->links = std::move(prepared.links);
		RefreshPreparedNativeIvBlockPlaceholder(target, state);
	} break;
	case RichPageBlockKind::Table: {
		if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.text,
				&prepared,
				state)) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		SortPreparedIvRichText(&prepared);
		preparedBlock->text = std::move(prepared.text);
		preparedBlock->links = std::move(prepared.links);
	} break;
	case RichPageBlockKind::Details: {
		if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.text,
				&prepared,
				state)) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		StripPreparedDetailsSummaryLinks(&prepared);
		SortPreparedIvRichText(&prepared);
		preparedBlock->text = std::move(prepared.text);
		preparedBlock->links = std::move(prepared.links);
		RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
	} break;
	case RichPageBlockKind::Math: {
		if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		if (canonicalBlock.formula.trimmed().isEmpty() && !state->editMode) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto formulaIndex = preparedBlock->formulaIndex;
		if (formulaIndex >= 0 && formulaIndex < int(state->result.formulas.size())) {
			auto slot = PreparedFormulaSlot();
			slot.trimmedTex = canonicalBlock.formula.trimmed();
			slot.kind = MathKind::Display;
			slot.textSize = state->dimensions.displayMathTextSize;
			slot.renderWidthCap = state->dimensions.displayMathMaxRenderWidth;
			slot.renderHeightCap = state->dimensions.displayMathMaxRenderHeight;
			slot.present = true;
			state->result.formulas[formulaIndex] = std::move(slot);
		} else {
			auto prepared = PreparedBlock();
			prepared.kind = PreparedBlockKind::DisplayMath;
			prepared.formulaTex = canonicalBlock.formula;
			prepared.mathKind = MathKind::Display;
			formulaIndex = state->rememberFormula(prepared);
		}
		preparedBlock->formulaTex = canonicalBlock.formula;
		preparedBlock->formulaIndex = formulaIndex;
		RefreshPreparedNativeIvBlockPlaceholder(preparedBlock, state);
		formulaRange->from = formulaIndex;
		formulaRange->till = formulaIndex + 1;
	} break;
	default:
		return NativeInstantViewLeafUpdateResult::Unsupported;
	}
	if (canonicalBlock.kind != RichPageBlockKind::Math) {
		formulaRange->till = state->nextFormulaIndex;
	}
	return NativeInstantViewLeafUpdateResult::Updated;
}

[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvBlockCaption(
		PreparedBlock *preparedBlock,
		const RichPageBlock &canonicalBlock,
		const PreparedEditLeafSource &source,
		NativeIvPrepareState *state,
		NativeIvPreparedLeafFormulaRange *formulaRange) {
	formulaRange->from = state->nextFormulaIndex;
	formulaRange->till = state->nextFormulaIndex;
	switch (canonicalBlock.kind) {
	case RichPageBlockKind::Quote: {
		const auto target = FindPreparedNativeIvLeafBlock(preparedBlock, source);
		if (!target) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.caption,
				&prepared,
				state)) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		if (canonicalBlock.pullquote) {
			WrapPreparedIvRichTextItalic(&prepared);
		}
		SortPreparedIvRichText(&prepared);
		target->text = std::move(prepared.text);
		target->links = std::move(prepared.links);
		RefreshPreparedNativeIvBlockPlaceholder(target, state);
	} break;
	case RichPageBlockKind::Photo:
	case RichPageBlockKind::Video:
	case RichPageBlockKind::Audio:
	case RichPageBlockKind::Map: {
		if (!preparedBlock->editLeaf || (*preparedBlock->editLeaf != source)) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		auto prepared = PreparedIvRichText();
		if (!PreparePreparedNativeIvRichText(
				canonicalBlock.caption,
				&prepared,
				state)) {
			return NativeInstantViewLeafUpdateResult::Failed;
		}
		SortPreparedIvRichText(&prepared);
		preparedBlock->text = std::move(prepared.text);
		preparedBlock->links = std::move(prepared.links);
		if (preparedBlock->kind == PreparedBlockKind::Photo) {
			preparedBlock->photo.caption = preparedBlock->text;
		} else if (preparedBlock->kind == PreparedBlockKind::Video) {
			preparedBlock->video.caption = preparedBlock->text;
		}
		RefreshPreparedNativeIvMediaCaptionPlaceholder(preparedBlock, state);
		RefreshPreparedNativeIvPlaceholderCopyText(preparedBlock);
	} break;
	default:
		return NativeInstantViewLeafUpdateResult::Unsupported;
	}
	formulaRange->till = state->nextFormulaIndex;
	return NativeInstantViewLeafUpdateResult::Updated;
}

[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvListItemText(
		PreparedBlock *preparedBlock,
		const RichPageListItem &canonicalItem,
		const PreparedEditLeafSource &source,
		NativeIvPrepareState *state,
		NativeIvPreparedLeafFormulaRange *formulaRange) {
	const auto target = FindPreparedNativeIvLeafBlock(preparedBlock, source);
	if (!target) {
		return NativeInstantViewLeafUpdateResult::Unsupported;
	}
	formulaRange->from = state->nextFormulaIndex;
	formulaRange->till = state->nextFormulaIndex;
	auto prepared = PreparedIvRichText();
	if (!PreparePreparedNativeIvRichText(canonicalItem.text, &prepared, state)) {
		return NativeInstantViewLeafUpdateResult::Failed;
	}
	SortPreparedIvRichText(&prepared);
	target->text = std::move(prepared.text);
	target->links = std::move(prepared.links);
	RefreshPreparedNativeIvBlockPlaceholder(target, state);
	formulaRange->till = state->nextFormulaIndex;
	return NativeInstantViewLeafUpdateResult::Updated;
}

[[nodiscard]] NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvTableCellText(
		PreparedBlock *preparedBlock,
		const RichPageTableCell &canonicalCell,
		const PreparedEditLeafSource &source,
		NativeIvPrepareState *state,
		NativeIvPreparedLeafFormulaRange *formulaRange) {
	const auto target = FindPreparedNativeIvLeafCell(preparedBlock, source);
	if (!target) {
		return NativeInstantViewLeafUpdateResult::Unsupported;
	}
	formulaRange->from = state->nextFormulaIndex;
	formulaRange->till = state->nextFormulaIndex;
	auto prepared = PreparedIvRichText();
	const auto context = NativeIvRichTextContextForTextSize(
		canonicalCell.header
			? state->dimensions.tableHeaderTextSize
			: state->dimensions.tableBodyTextSize,
		state->dimensions);
	if (!PreparePreparedNativeIvRichText(
			canonicalCell.text,
			&prepared,
			state,
			context)) {
		return NativeInstantViewLeafUpdateResult::Failed;
	}
	SortPreparedIvRichText(&prepared);
	target->text = std::move(prepared.text);
	target->links = std::move(prepared.links);
	RefreshPreparedNativeIvCellPlaceholder(target, state);
	formulaRange->till = state->nextFormulaIndex;
	return NativeInstantViewLeafUpdateResult::Updated;
}

using PrepareCanonicalMediaBlock = bool (*)(
	const RichPageBlock &data,
	std::vector<PreparedBlock> *result,
	NativeIvPrepareState *state);

[[nodiscard]] bool PrepareCanonicalNativeIvMediaBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path,
		PrepareCanonicalMediaBlock prepare) {
	const auto count = result->size();
	if (!prepare(data, result, state)) {
		return false;
	}
	if (result->size() > count) {
		ApplyBlockCaptionEditSource(&result->back(), std::move(path));
		if (state->editMode) {
			ApplyNativeIvEditPlaceholderText(&result->back());
		}
	}
	return true;
}

[[nodiscard]] bool PrepareCanonicalNativeIvGroupedMediaBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path) {
	const auto count = result->size();
	if (!PrepareNativeIvGroupedMediaBlock(data, result, state)) {
		return false;
	}
	if (result->size() > count) {
		result->back().editBlock = BlockSource(std::move(path));
	}
	return true;
}

void ClearPreparedEditSources(std::vector<PreparedBlock> *blocks) {
	for (auto &block : *blocks) {
		block.editBlock.reset();
		block.editListItem.reset();
		block.editLeaf.reset();
		for (auto &row : block.tableRows) {
			row.editRow.reset();
			for (auto &cell : row.cells) {
				cell.editCell.reset();
				cell.editLeaf.reset();
			}
		}
		ClearPreparedEditSources(&block.children);
	}
}

[[nodiscard]] bool PrepareNativeIvRichPlaceholderBlock(
		QString label,
		const RichPage::RichText &caption,
		QString anchorId,
		std::optional<EmbedRequest> embed,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state) {
	auto prepared = PreparedIvRichText();
	if (!PrepareNativeIvRichText(
			caption,
			&prepared,
			&anchorId,
			state)) {
		return state->result.failure.failed()
			? false
			: PrepareNativeIvPlainPlaceholderBlock(std::move(label), result);
	}
	SortPreparedIvRichText(&prepared);
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::Placeholder;
	block.text = std::move(prepared.text);
	block.links = std::move(prepared.links);
	block.anchorId = std::move(anchorId);
	block.anchorIds = std::move(prepared.anchorIds);
	block.supplementary = true;
	block.placeholder.label = std::move(label);
	block.placeholder.copyText = block.text.text.isEmpty()
		? block.placeholder.label
		: (block.placeholder.label + u"\n"_q + block.text.text);
	block.placeholder.embed = std::move(embed);
	if (block.placeholder.embed) {
		block.placeholder.id = { .value = uint64(++state->nextGeneratedId) };
	}
	result->push_back(std::move(block));
	return true;
}

[[nodiscard]] auto EmbedRequestFromCanonicalBlock(
		const RichPageBlock &block) -> std::optional<EmbedRequest> {
	auto request = EmbedRequest{
		.width = block.width,
		.height = block.height,
		.fullWidth = block.fullWidth,
		.fixedHeight = block.fixedHeight,
		.allowScrolling = block.allowScrolling,
	};
	if (!block.url.isEmpty()) {
		request.url = block.url;
	} else if (!block.html.isEmpty()) {
		request.html = block.html;
	} else {
		return std::nullopt;
	}
	return request;
}

[[nodiscard]] bool AppendNativeIvFlowBlock(
		std::vector<PreparedBlock> *result,
		PreparedBlockKind kind,
		int headingLevel,
		const RichPage::RichText &text,
		QString anchorId,
		std::optional<PreparedEditBlockPath> path,
		NativeIvPrepareState *state,
		bool allowEmpty = false) {
	auto prepared = PreparedIvRichText();
	const auto context = NativeIvRichTextContextForTextSize(
		NativeIvFlowTextSize(kind, headingLevel, state->dimensions),
		state->dimensions);
	if (!PrepareNativeIvRichText(
			text,
			&prepared,
			&anchorId,
			state,
			context)) {
		return false;
	}
	auto editBlock = std::optional<PreparedEditBlockSource>();
	auto editLeaf = std::optional<PreparedEditLeafSource>();
	if (path) {
		editBlock = BlockSource(*path);
		editLeaf = BlockTextLeafSource(*path);
	}
	const auto count = result->size();
	const auto appended = AppendPreparedIvRichBlock(
		result,
		kind,
		headingLevel,
		std::move(prepared),
		std::move(anchorId),
		allowEmpty || state->editMode,
		false,
		std::move(editBlock),
		std::move(editLeaf));
	if (appended && state->editMode && (result->size() > count)) {
		ApplyNativeIvEditPlaceholderText(&result->back());
	}
	return appended;
}

[[nodiscard]] bool PrepareCanonicalNativeIvQuoteBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path,
		NativeIvDepthContext depthContext) {
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::Quote;
	block.anchorId = data.anchorId;
	block.pullquote = data.pullquote;
	block.actualDepth = depthContext.quoteDepth;
	block.visualDepth = CappedNativeIvQuoteDepth(block.actualDepth);
	block.depthClamped = (block.actualDepth > block.visualDepth);
	block.editBlock = BlockSource(path);
	auto body = PreparedIvRichText();
	if (!PrepareNativeIvRichText(data.text, &body, &block.anchorId, state)) {
		return false;
	}
	if (data.blocks.empty() && !AppendPreparedQuoteParagraph(
			&block.children,
			std::move(body),
			data.pullquote,
			false,
			state->editMode,
			BlockTextLeafSource(path))) {
		return false;
	}
	auto childContext = depthContext;
	++childContext.quoteDepth;
	if (!data.blocks.empty() && !PrepareCanonicalNativeIvBlocks(
			data.blocks,
			&block.children,
			state,
			BlockChildrenContainer(path),
			childContext)) {
		return false;
	}
	auto cite = PreparedIvRichText();
	if (!PrepareNativeIvRichText(data.caption, &cite, &block.anchorId, state)) {
		return false;
	}
	if (!AppendPreparedQuoteParagraph(
			&block.children,
			std::move(cite),
			data.pullquote,
			true,
			state->editMode,
			BlockCaptionLeafSource(path))) {
		return false;
	}
	if (block.children.empty()) {
		block.children.push_back(EmptyParagraphBlock());
		if (data.pullquote) {
			block.children.back().flowAlignment = TableAlignment::Center;
			block.children.back().pullquote = true;
		}
	}
	result->push_back(std::move(block));
	return true;
}

[[nodiscard]] TaskState NativeIvTaskStateFromRichPage(
		RichPage::TaskState state) {
	switch (state) {
	case RichPage::TaskState::Unchecked:
		return TaskState::Unchecked;
	case RichPage::TaskState::Checked:
		return TaskState::Checked;
	case RichPage::TaskState::None:
		break;
	}
	return TaskState::None;
}

[[nodiscard]] bool PrepareCanonicalNativeIvList(
		const std::vector<RichPageListItem> &items,
		PreparedBlock *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path,
		NativeIvDepthContext depthContext) {
	result->actualDepth = depthContext.listDepth;
	result->visualDepth = CappedNativeIvListDepth(result->actualDepth);
	result->depthClamped = (result->actualDepth > result->visualDepth);
	result->editBlock = BlockSource(path);
	auto firstNumber = true;
	for (auto i = 0, count = int(items.size()); i != count; ++i) {
		const auto &item = items[i];
		auto block = PreparedBlock();
		block.kind = PreparedBlockKind::ListItem;
		block.listKind = result->listKind;
		block.listDelimiter = result->listDelimiter;
		block.taskState = NativeIvTaskStateFromRichPage(item.taskState);
		block.actualDepth = result->actualDepth;
		block.visualDepth = result->visualDepth;
		block.depthClamped = result->depthClamped;
		block.editListItem = ListItemSource(path, i);
		if (result->listKind == ListKind::Ordered) {
			auto orderedNumber = 0;
			if (!ParseOrderedNumber(item.number, &orderedNumber)) {
				orderedNumber = NextNativeIvOrderedNumber(*result);
			}
			block.orderedNumber = orderedNumber;
			if (firstNumber) {
				result->startNumber = block.orderedNumber;
				firstNumber = false;
			}
		}
		if (RichTextHasMeaningfulContent(item.text)) {
			auto prepared = PreparedIvRichText();
			auto anchorId = item.anchorId;
			if (!PrepareNativeIvRichText(
					item.text,
					&prepared,
					&anchorId,
					state)) {
				return false;
			}
			block.anchorId = std::move(anchorId);
			if (!AppendPreparedIvRichBlock(
					&block.children,
					PreparedBlockKind::Paragraph,
					0,
					std::move(prepared),
					QString(),
					state->editMode,
					false,
					std::nullopt,
					ListItemTextLeafSource(path, i))) {
				return false;
			}
			if (state->editMode && !block.children.empty()) {
				ApplyNativeIvEditPlaceholderText(&block.children.back());
			}
		}
		auto childContext = depthContext;
		++childContext.listDepth;
		if (!item.blocks.empty() && !PrepareCanonicalNativeIvBlocks(
				item.blocks,
				&block.children,
				state,
				ListItemChildrenContainer(path, i),
				childContext)) {
			return false;
		}
		if (block.children.empty()) {
			block.children.push_back(EmptyParagraphBlock());
			block.children.back().editLeaf = ListItemTextLeafSource(path, i);
			if (state->editMode) {
				ApplyNativeIvEditPlaceholderText(&block.children.back());
			}
		}
		result->children.push_back(std::move(block));
	}
	return true;
}

[[nodiscard]] TableAlignment NativeIvTableAlignment(
		const RichPageTableCell &cell) {
	switch (cell.alignment) {
	case RichPage::TableAlignment::Center:
		return TableAlignment::Center;
	case RichPage::TableAlignment::Right:
		return TableAlignment::Right;
	case RichPage::TableAlignment::Left:
		break;
	}
	return TableAlignment::Left;
}

[[nodiscard]] PreparedTableCellVerticalAlignment NativeIvTableVerticalAlignment(
		const RichPageTableCell &cell) {
	switch (cell.verticalAlignment) {
	case RichPage::TableVerticalAlignment::Middle:
		return PreparedTableCellVerticalAlignment::Middle;
	case RichPage::TableVerticalAlignment::Bottom:
		return PreparedTableCellVerticalAlignment::Bottom;
	case RichPage::TableVerticalAlignment::Top:
		break;
	}
	return PreparedTableCellVerticalAlignment::Top;
}

[[nodiscard]] bool PrepareCanonicalNativeIvTableBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path) {
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::Table;
	block.tableBordered = data.bordered;
	block.tableStriped = data.striped;
	block.editBlock = BlockSource(path);
	auto title = PreparedIvRichText();
	block.anchorId = data.anchorId;
	if (!PrepareNativeIvRichText(data.text, &title, &block.anchorId, state)) {
		return false;
	}
	SortPreparedIvRichText(&title);
	block.text = std::move(title.text);
	block.links = std::move(title.links);
	block.anchorIds = std::move(title.anchorIds);
	if (!block.text.text.isEmpty() || state->editMode) {
		block.editLeaf = BlockTextLeafSource(path);
	}
	if (state->editMode) {
		block.forceTextSegment = true;
		ApplyNativeIvEditPlaceholderText(&block);
	}

	const auto &limits = PrepareTableRenderLimitsForIv();
	const auto rowCount = std::min(int(data.tableRows.size()), limits.maxRows);
	auto occupancy = NativeIvTableOccupancyGrid(rowCount);
	block.tableRows.reserve(rowCount);
	auto occupiedSlotCountSoFar = int64(0);

	for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) {
		const auto &row = data.tableRows[rowIndex];
		auto preparedRow = PreparedTableRow();
		preparedRow.editRow = TableRowSource(path, rowIndex);
		preparedRow.cells.reserve(std::min(int(row.cells.size()), limits.maxColumns));
		for (auto cellIndex = 0, cellCount = int(row.cells.size());
				cellIndex != cellCount;
				++cellIndex) {
			const auto &cell = row.cells[cellIndex];
			auto preparedCell = PreparedTableCell();
			const auto normalizedColspan = NormalizeNativeIvTableSpan(cell.colspan);
			const auto rowspan = ClampNativeIvTableRowspan(
				cell.rowspan,
				rowIndex,
				rowCount);
			if (rowspan <= 0) {
				continue;
			}
			const auto column = FirstAvailableNativeIvTableColumn(
				occupancy,
				rowIndex,
				rowspan,
				normalizedColspan,
				limits.maxColumns);
			if (column < 0) {
				continue;
			}
			const auto colspan = ClampNativeIvTableColspan(
				normalizedColspan,
				column,
				limits.maxColumns);
			if (colspan <= 0) {
				continue;
			}
			const auto occupiedSlotGrowth = int64(rowspan) * colspan;
			if (occupiedSlotGrowth > limits.maxCells
				|| (occupiedSlotCountSoFar + occupiedSlotGrowth) > limits.maxCells) {
				continue;
			}
			preparedCell.column = column;
			preparedCell.alignment = NativeIvTableAlignment(cell);
			preparedCell.header = cell.header;
			preparedCell.verticalAlignment = NativeIvTableVerticalAlignment(cell);
			preparedCell.colspan = colspan;
			preparedCell.rowspan = rowspan;
			preparedCell.editCell = TableCellSource(
				path,
				rowIndex,
				cellIndex,
				column,
				colspan,
				rowspan);
			preparedCell.editLeaf = TableCellTextLeafSource(
				path,
				rowIndex,
				cellIndex);
			if (state->editMode) {
				ApplyNativeIvEditPlaceholderText(&preparedCell);
			}
			if (!cell.text.text.empty()) {
				auto rich = PreparedIvRichText();
				const auto context = NativeIvRichTextContextForTextSize(
					cell.header
						? state->dimensions.tableHeaderTextSize
						: state->dimensions.tableBodyTextSize,
					state->dimensions);
				if (!PrepareNativeIvRichText(
						cell.text,
						&rich,
						nullptr,
						state,
						context)) {
					return false;
				}
				SortPreparedIvRichText(&rich);
				preparedCell.text = std::move(rich.text);
				preparedCell.links = std::move(rich.links);
			}
			MarkNativeIvTableSlots(
				&occupancy,
				rowIndex,
				column,
				rowspan,
				colspan);
			occupiedSlotCountSoFar += occupiedSlotGrowth;
			preparedRow.cells.push_back(std::move(preparedCell));
		}
		preparedRow.header = !preparedRow.cells.empty()
			&& std::all_of(
				preparedRow.cells.begin(),
				preparedRow.cells.end(),
				[](const PreparedTableCell &cell) {
					return cell.header;
				});
		block.tableRows.push_back(std::move(preparedRow));
	}

	block.tableColumnCount = NativeIvTableColumnCount(occupancy);
	block.tableAlignments.resize(block.tableColumnCount, TableAlignment::Left);
	for (const auto &preparedRow : block.tableRows) {
		for (const auto &preparedCell : preparedRow.cells) {
			const auto from = std::max(preparedCell.column, 0);
			const auto to = std::min(
				preparedCell.column + preparedCell.colspan,
				block.tableColumnCount);
			for (auto column = from; column != to; ++column) {
				block.tableAlignments[column] = preparedCell.alignment;
			}
		}
	}
	result->push_back(std::move(block));
	return true;
}

[[nodiscard]] bool PrepareCanonicalNativeIvDetailsBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path,
		NativeIvDepthContext depthContext) {
	auto summary = PreparedIvRichText();
	auto anchorId = NativeIvDetailsAnchorId(state);
	if (!PrepareNativeIvRichText(
			data.text,
			&summary,
			&anchorId,
			state)) {
		return false;
	}
	if (!summary.links.empty()) {
		const auto from = std::remove_if(
			summary.text.entities.begin(),
			summary.text.entities.end(),
			[](const EntityInText &entity) {
				return entity.type() == EntityType::CustomUrl;
			});
		summary.text.entities.erase(from, summary.text.entities.end());
		summary.links.clear();
	}
	SortPreparedIvRichText(&summary);
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::Details;
	block.anchorId = std::move(anchorId);
	block.detailsOpen = data.open;
	block.collapsed = state->editMode ? false : !data.open;
	block.text = std::move(summary.text);
	block.links = std::move(summary.links);
	block.anchorIds = std::move(summary.anchorIds);
	block.editBlock = BlockSource(path);
	block.editLeaf = BlockTextLeafSource(path);
	if (state->editMode) {
		ApplyNativeIvEditPlaceholderText(&block);
	}
	return PrepareCanonicalNativeIvBlocks(
		data.blocks,
		&block.children,
		state,
		BlockChildrenContainer(path),
		depthContext)
		? (result->push_back(std::move(block)), true)
		: false;
}

[[nodiscard]] QString NativeIvRelatedArticleFooterText(
		const RichPageRelatedArticle &article) {
	if (article.publishedDate && !article.author.isEmpty()) {
		return article.author + u", "_q + NativeIvDateText(article.publishedDate);
	} else if (article.publishedDate) {
		return NativeIvDateText(article.publishedDate);
	}
	return article.author;
}

[[nodiscard]] bool PrepareCanonicalNativeIvRelatedArticlesBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state) {
	auto related = std::vector<PreparedBlock>();
	related.reserve(data.relatedArticles.size());
	for (const auto &article : data.relatedArticles) {
		auto prepared = PreparedBlock();
		prepared.kind = PreparedBlockKind::RelatedArticle;
		prepared.relatedArticle.title = article.title.trimmed();
		prepared.relatedArticle.description = article.description.trimmed();
		prepared.relatedArticle.footer = NativeIvRelatedArticleFooterText(article);
		prepared.relatedArticle.photoId = article.photoId;
		if (prepared.relatedArticle.title.isEmpty()
			&& prepared.relatedArticle.description.isEmpty()
			&& prepared.relatedArticle.footer.isEmpty()) {
			prepared.relatedArticle.title = article.url.trimmed();
		}
		const auto linkText = !prepared.relatedArticle.title.isEmpty()
			? prepared.relatedArticle.title
			: !prepared.relatedArticle.description.isEmpty()
			? prepared.relatedArticle.description
			: prepared.relatedArticle.footer;
		prepared.relatedArticle.link = PrepareNativeIvRelatedArticleLink(
			article.url,
			article.webpageId,
			linkText);
		const auto appendLine = [&](QString *copyText, const QString &line) {
			if (line.isEmpty()) {
				return;
			} else if (!copyText->isEmpty()) {
				copyText->append(QChar('\n'));
			}
			copyText->append(line);
		};
		appendLine(&prepared.relatedArticle.copyText, prepared.relatedArticle.title);
		appendLine(
			&prepared.relatedArticle.copyText,
			prepared.relatedArticle.description);
		appendLine(&prepared.relatedArticle.copyText, prepared.relatedArticle.footer);
		if (prepared.relatedArticle.copyText.isEmpty()) {
			prepared.relatedArticle.copyText = prepared.relatedArticle.link.target;
		}
		related.push_back(std::move(prepared));
	}
	if (related.empty()) {
		return true;
	}

	auto title = PreparedIvRichText();
	auto anchorId = data.anchorId;
	if (!PrepareNativeIvRichText(data.text, &title, &anchorId, state)) {
		return false;
	}
	SortPreparedIvRichText(&title);
	if (!title.text.text.isEmpty()) {
		if (!AppendPreparedIvRichBlock(
				result,
				PreparedBlockKind::Heading,
				4,
				std::move(title),
				std::move(anchorId),
				state->editMode)) {
			return false;
		}
	} else if (!anchorId.isEmpty() && related.front().anchorId.isEmpty()) {
		related.front().anchorId = std::move(anchorId);
	}
	result->insert(
		result->end(),
		std::make_move_iterator(related.begin()),
		std::make_move_iterator(related.end()));
	return true;
}

[[nodiscard]] bool PrepareCanonicalNativeIvEmbedPostBlock(
		const RichPageBlock &data,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		NativeIvDepthContext depthContext) {
	auto caption = PreparedIvRichText();
	auto block = PreparedBlock();
	block.kind = PreparedBlockKind::EmbedPost;
	block.embedPost.url = data.url.trimmed();
	block.embedPost.authorPhotoId = data.photoId;
	block.embedPost.author = data.author.trimmed();
	if (data.date) {
		block.embedPost.dateText = NativeIvDateText(data.date);
	}
	auto anchorId = data.anchorId;
	if (!PrepareNativeIvRichText(data.caption, &caption, &anchorId, state)) {
		return false;
	}
	SortPreparedIvRichText(&caption);
	block.text = std::move(caption.text);
	block.links = std::move(caption.links);
	block.anchorId = std::move(anchorId);
	block.anchorIds = std::move(caption.anchorIds);
	block.supplementary = true;
	if (data.blocks.empty()) {
		block.children.push_back(
			PrepareNativeIvEmbedPostFallbackParagraph(block.embedPost.url));
	} else if (!PrepareCanonicalNativeIvBlocks(
			data.blocks,
			&block.children,
			state,
			PreparedEditBlockContainerPath(),
			depthContext)) {
		return false;
	} else {
		ClearPreparedEditSources(&block.children);
	}
	result->push_back(std::move(block));
	return true;
}

[[nodiscard]] bool PrepareCanonicalNativeIvBlock(
		const RichPageBlock &block,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockPath path,
		NativeIvDepthContext depthContext) {
	if (state->blocked()) {
		return false;
	}
	switch (block.kind) {
	case RichPageBlockKind::Unsupported:
		return true;
	case RichPageBlockKind::Heading:
		return AppendNativeIvFlowBlock(
			result,
			PreparedBlockKind::Heading,
			block.headingLevel,
			block.text,
			block.anchorId,
			path,
			state);
	case RichPageBlockKind::Paragraph:
	case RichPageBlockKind::Footer:
		return AppendNativeIvFlowBlock(
			result,
			PreparedBlockKind::Paragraph,
			0,
			block.text,
			block.anchorId,
			path,
			state);
	case RichPageBlockKind::Thinking:
		return AppendNativeIvFlowBlock(
			result,
			PreparedBlockKind::Thinking,
			0,
			block.text,
			block.anchorId,
			std::nullopt,
			state);
	case RichPageBlockKind::AuthorDate: {
		auto prepared = PreparedIvRichText();
		auto anchorId = block.anchorId;
		if (!PrepareNativeIvRichText(
				block.text,
				&prepared,
				&anchorId,
				state)) {
			return false;
		}
		if (block.date) {
			if (!prepared.text.text.isEmpty()) {
				prepared.text.append(u" \u2022 "_q);
			}
			prepared.text.append(NativeIvDateText(block.date));
		}
		return AppendPreparedIvRichBlock(
			result,
			PreparedBlockKind::Paragraph,
			0,
			std::move(prepared),
			std::move(anchorId),
			false,
			true);
	}
	case RichPageBlockKind::Code: {
		auto prepared = PreparedIvRichText();
		auto anchorId = block.anchorId;
		if (!PrepareNativeIvRichText(
				block.text,
				&prepared,
				&anchorId,
				state,
				{ .dropClickHandlers = true })) {
			return false;
		}
		auto code = PreparedBlock();
		code.kind = PreparedBlockKind::CodeBlock;
		code.anchorId = std::move(anchorId);
		code.codeLanguage = block.language;
		SortPreparedIvRichText(&prepared);
		code.text = StripOneTrailingNewline(std::move(prepared.text));
		code.links = std::move(prepared.links);
		code.anchorIds = std::move(prepared.anchorIds);
		code.editBlock = BlockSource(path);
		code.editLeaf = BlockTextLeafSource(path);
		if (state->editMode) {
			ApplyNativeIvEditPlaceholderText(&code);
		}
		result->push_back(std::move(code));
		return true;
	}
	case RichPageBlockKind::Divider: {
		auto prepared = PrepareRuleBlock();
		prepared.editBlock = BlockSource(path);
		result->push_back(std::move(prepared));
		return true;
	}
	case RichPageBlockKind::Anchor: {
		auto prepared = PreparedIvRichText();
		auto editBlock = std::optional<PreparedEditBlockSource>();
		editBlock = BlockSource(path);
		return AppendPreparedIvRichBlock(
			result,
			PreparedBlockKind::Paragraph,
			0,
			std::move(prepared),
			block.anchorId,
			true,
			false,
			std::move(editBlock));
	}
	case RichPageBlockKind::List: {
		auto prepared = PreparedBlock();
		prepared.kind = PreparedBlockKind::List;
		prepared.listKind = (block.listKind == RichPage::ListKind::Ordered)
			? ListKind::Ordered
			: ListKind::Bullet;
		if (prepared.listKind == ListKind::Ordered) {
			prepared.listDelimiter = ListDelimiter::Period;
			prepared.startNumber = 1;
		}
		return PrepareCanonicalNativeIvList(
			block.listItems,
			&prepared,
			state,
			path,
			depthContext)
			? (result->push_back(std::move(prepared)), true)
			: false;
	}
	case RichPageBlockKind::Quote:
		return PrepareCanonicalNativeIvQuoteBlock(
			block,
			result,
			state,
			path,
			depthContext);
	case RichPageBlockKind::Photo:
		return PrepareCanonicalNativeIvMediaBlock(
			block,
			result,
			state,
			path,
			PrepareNativeIvPhotoBlock);
	case RichPageBlockKind::Video:
		return PrepareCanonicalNativeIvMediaBlock(
			block,
			result,
			state,
			path,
			PrepareNativeIvVideoBlock);
	case RichPageBlockKind::Embed:
		return PrepareNativeIvRichPlaceholderBlock(
			tr::lng_iv_click_to_view(tr::now),
			block.caption,
			block.anchorId,
			EmbedRequestFromCanonicalBlock(block),
			result,
			state);
	case RichPageBlockKind::EmbedPost:
		return PrepareCanonicalNativeIvEmbedPostBlock(
			block,
			result,
			state,
			depthContext);
	case RichPageBlockKind::GroupedMedia:
		return PrepareCanonicalNativeIvGroupedMediaBlock(
			block,
			result,
			state,
			path);
	case RichPageBlockKind::Channel:
		return PrepareNativeIvChannelBlock(block, result, state);
	case RichPageBlockKind::Audio:
		return PrepareCanonicalNativeIvMediaBlock(
			block,
			result,
			state,
			path,
			PrepareNativeIvAudioBlock);
	case RichPageBlockKind::Math:
		if (block.formula.trimmed().isEmpty() && !state->editMode) {
			return true;
		} else {
			auto prepared = PreparedBlock();
			prepared.kind = PreparedBlockKind::DisplayMath;
			prepared.formulaTex = block.formula;
			prepared.mathKind = MathKind::Display;
			prepared.editBlock = BlockSource(path);
			prepared.editLeaf = MathFormulaLeafSource(path);
			if (state->editMode) {
				ApplyNativeIvEditPlaceholderText(&prepared);
			}
			prepared.formulaIndex = state->rememberFormula(prepared);
			result->push_back(std::move(prepared));
			return true;
		}
	case RichPageBlockKind::Table:
		return PrepareCanonicalNativeIvTableBlock(block, result, state, path);
	case RichPageBlockKind::Details:
		return PrepareCanonicalNativeIvDetailsBlock(
			block,
			result,
			state,
			path,
			depthContext);
	case RichPageBlockKind::RelatedArticles:
		return PrepareCanonicalNativeIvRelatedArticlesBlock(block, result, state);
	case RichPageBlockKind::Map:
		return PrepareCanonicalNativeIvMediaBlock(
			block,
			result,
			state,
			path,
			PrepareNativeIvMapBlock);
	}
	return true;
}

[[nodiscard]] bool PrepareCanonicalNativeIvBlocks(
		const std::vector<RichPageBlock> &blocks,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state,
		PreparedEditBlockContainerPath container,
		NativeIvDepthContext depthContext) {
	for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
		const auto path = PreparedEditBlockPath{
			.container = container,
			.index = i,
		};
		if (!PrepareCanonicalNativeIvBlock(
				blocks[i],
				result,
				state,
				path,
				depthContext)) {
			if (state->result.failure.failed()) {
				return false;
			}
			(void)PrepareNativeIvPlainPlaceholderBlock(
				u"Unsupported Block"_q,
				result);
		}
	}
	return !state->result.failure.failed();
}

} // namespace

bool PrepareNativeIvBlocks(
		const Iv::RichPage &page,
		std::vector<PreparedBlock> *result,
		NativeIvPrepareState *state) {
	return PrepareCanonicalNativeIvBlocks(
		page.blocks,
		result,
		state,
		PreparedEditBlockContainerPath(),
		NativeIvDepthContext());
}

NativeInstantViewLeafUpdateResult UpdatePreparedNativeIvLeaf(
		std::vector<PreparedBlock> *blocks,
		const Iv::RichPage &page,
		const PreparedEditLeafSource &source,
		NativeIvPrepareState *state,
		NativeIvPreparedLeafFormulaRange *formulaRange) {
	if (!blocks || !state || !formulaRange) {
		return NativeInstantViewLeafUpdateResult::Failed;
	}
	const auto canonicalBlock = ResolveCanonicalNativeIvBlock(page, source.block);
	const auto preparedBlock = FindPreparedNativeIvBlockByPath(blocks, source.block);
	if (!canonicalBlock || !preparedBlock) {
		return NativeInstantViewLeafUpdateResult::Unsupported;
	}
	switch (source.kind) {
	case PreparedEditLeafKind::BlockText:
	case PreparedEditLeafKind::MathFormula:
	case PreparedEditLeafKind::BlockCaption:
		break;
	case PreparedEditLeafKind::ListItemText:
		if (source.listItemIndex < 0
			|| source.listItemIndex >= int(canonicalBlock->listItems.size())) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		break;
	case PreparedEditLeafKind::TableCellText:
		if (source.tableRowIndex < 0
			|| source.tableRowIndex >= int(canonicalBlock->tableRows.size())) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		if (source.tableCellIndex < 0
			|| source.tableCellIndex
				>= int(canonicalBlock->tableRows[source.tableRowIndex].cells.size())) {
			return NativeInstantViewLeafUpdateResult::Unsupported;
		}
		break;
	}
	switch (source.kind) {
	case PreparedEditLeafKind::BlockText:
	case PreparedEditLeafKind::MathFormula:
		return UpdatePreparedNativeIvBlockText(
			preparedBlock,
			*canonicalBlock,
			source,
			state,
			formulaRange);
	case PreparedEditLeafKind::BlockCaption:
		return UpdatePreparedNativeIvBlockCaption(
			preparedBlock,
			*canonicalBlock,
			source,
			state,
			formulaRange);
	case PreparedEditLeafKind::ListItemText:
		return UpdatePreparedNativeIvListItemText(
			preparedBlock,
			canonicalBlock->listItems[source.listItemIndex],
			source,
			state,
			formulaRange);
	case PreparedEditLeafKind::TableCellText:
		return UpdatePreparedNativeIvTableCellText(
			preparedBlock,
			canonicalBlock->tableRows[source.tableRowIndex]
				.cells[source.tableCellIndex],
			source,
			state,
			formulaRange);
	}
	return NativeInstantViewLeafUpdateResult::Unsupported;
}

} // namespace Iv::Markdown
