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

#include "base/openssl_help.h"
#include "base/unixtime.h"
#include "iv/iv_data.h"
#include "lang/lang_keys.h"
#include "ui/image/image_prepare.h"
#include "ui/grouped_layout.h"
#include "styles/palette.h"
#include "styles/style_chat.h"

#include <QtCore/QSize>

namespace Iv {
namespace {

struct Attribute {
	QByteArray name;
	std::optional<QByteArray> value;
};
using Attributes = std::vector<Attribute>;

struct Photo {
	uint64 id = 0;
	int width = 0;
	int height = 0;
	QByteArray minithumbnail;
};

struct Document {
	uint64 id = 0;
	int width = 0;
	int height = 0;
	QByteArray minithumbnail;
};

template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
[[nodiscard]] QByteArray Number(T value) {
	return QByteArray::number(value);
}

template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
[[nodiscard]] QByteArray Percent(T value) {
	return Number(base::SafeRound(value * 10000.) / 100.);
};

[[nodiscard]] QByteArray Escape(QByteArray value) {
	auto result = QByteArray();
	result.reserve(value.size());
	for (const auto &ch : value) {
		switch (ch) {
		case '&': result.append("&amp;"); break;
		case '<': result.append("&lt;"); break;
		case '>': result.append("&gt;"); break;
		case '"': result.append("&quot;"); break;
		case '\'': result.append("&apos;"); break;
		default: result.append(ch); break;
		}
	}
	return result;
}

[[nodiscard]] QByteArray Date(TimeId date) {
	return Escape(langDateTimeFull(base::unixtime::parse(date)).toUtf8());
}

class Parser final {
public:
	Parser(const Source &source, const Options &options);

	[[nodiscard]] Prepared result();

private:
	void process(const Source &source);
	void process(const MTPPhoto &photo);
	void process(const MTPDocument &document);

	template <typename Inner>
	[[nodiscard]] QByteArray list(const MTPVector<Inner> &data);

	[[nodiscard]] QByteArray collage(
		const QVector<MTPPageBlock> &list,
		const std::vector<QSize> &dimensions,
		int offset = 0);
	[[nodiscard]] QByteArray slideshow(
		const QVector<MTPPageBlock> &list,
		QSize dimensions);

	[[nodiscard]] QByteArray block(const MTPDpageBlockUnsupported &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockTitle &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockSubtitle &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockAuthorDate &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockHeader &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockSubheader &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockParagraph &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockPreformatted &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockFooter &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockDivider &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockAnchor &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockList &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockBlockquote &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockPullquote &data);
	[[nodiscard]] QByteArray block(
		const MTPDpageBlockPhoto &data,
		const Ui::GroupMediaLayout &layout = {},
		QSize outer = {});
	[[nodiscard]] QByteArray block(
		const MTPDpageBlockVideo &data,
		const Ui::GroupMediaLayout &layout = {},
		QSize outer = {});
	[[nodiscard]] QByteArray block(const MTPDpageBlockCover &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockEmbed &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockEmbedPost &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockCollage &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockSlideshow &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockChannel &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockAudio &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockKicker &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockTable &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockOrderedList &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockDetails &data);
	[[nodiscard]] QByteArray block(
		const MTPDpageBlockRelatedArticles &data);
	[[nodiscard]] QByteArray block(const MTPDpageBlockMap &data);

	[[nodiscard]] QByteArray block(const MTPDpageRelatedArticle &data);

	[[nodiscard]] QByteArray block(const MTPDpageTableRow &data);
	[[nodiscard]] QByteArray block(const MTPDpageTableCell &data);

	[[nodiscard]] QByteArray block(const MTPDpageListItemText &data);
	[[nodiscard]] QByteArray block(const MTPDpageListItemBlocks &data);

	[[nodiscard]] QByteArray block(const MTPDpageListOrderedItemText &data);
	[[nodiscard]] QByteArray block(
		const MTPDpageListOrderedItemBlocks &data);

	[[nodiscard]] QByteArray wrap(const QByteArray &content, int views);

	[[nodiscard]] QByteArray tag(
		const QByteArray &name,
		const QByteArray &body = {});
	[[nodiscard]] QByteArray tag(
		const QByteArray &name,
		const Attributes &attributes,
		const QByteArray &body = {});
	[[nodiscard]] QByteArray utf(const MTPstring &text);
	[[nodiscard]] QByteArray utf(const tl::conditional<MTPstring> &text);
	[[nodiscard]] QByteArray rich(const MTPRichText &text);
	[[nodiscard]] QByteArray caption(const MTPPageCaption &caption);

	[[nodiscard]] Photo parse(const MTPPhoto &photo);
	[[nodiscard]] Document parse(const MTPDocument &document);
	[[nodiscard]] Geo parse(const MTPGeoPoint &geo);

	[[nodiscard]] Photo photoById(uint64 id);
	[[nodiscard]] Document documentById(uint64 id);

	[[nodiscard]] QByteArray photoFullUrl(const Photo &photo);
	[[nodiscard]] QByteArray documentFullUrl(const Document &document);
	[[nodiscard]] QByteArray embedUrl(const QByteArray &html);
	[[nodiscard]] QByteArray mapUrl(
		const Geo &geo,
		int width,
		int height,
		int zoom);
	[[nodiscard]] QByteArray resource(QByteArray id);

	[[nodiscard]] std::vector<QSize> computeCollageDimensions(
		const QVector<MTPPageBlock> &items);
	[[nodiscard]] QSize computeSlideshowDimensions(
		const QVector<MTPPageBlock> &items);

	//const Options _options;
	const QByteArray _fileOriginPostfix;

	base::flat_set<QByteArray> _resources;

	Prepared _result;

	base::flat_map<uint64, Photo> _photosById;
	base::flat_map<uint64, Document> _documentsById;

};

[[nodiscard]] bool IsVoidElement(const QByteArray &name) {
	// Thanks https://developer.mozilla.org/en-US/docs/Glossary/Void_element
	static const auto voids = base::flat_set<QByteArray>{
		"area"_q,
		"base"_q,
		"br"_q,
		"col"_q,
		"embed"_q,
		"hr"_q,
		"img"_q,
		"input"_q,
		"link"_q,
		"meta"_q,
		"source"_q,
		"track"_q,
		"wbr"_q,
	};
	return voids.contains(name);
}

[[nodiscard]] QByteArray ArrowSvg(bool left) {
	const auto rotate = QByteArray(left ? "180" : "0");
	return R"(
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
	<path
		d="M14.9972363,18 L9.13865768,12.1414214 C9.06055283,12.0633165 9.06055283,11.9366835 9.13865768,11.8585786 L14.9972363,6 L14.9972363,6"
		transform="translate(11.997236, 12) scale(-1, -1) rotate()" + rotate + ") translate(-11.997236, -12)" + R"(">
	</path>
</svg>)";
}

Parser::Parser(const Source &source, const Options &options)
: /*_options(options)
, */_fileOriginPostfix('/' + Number(source.pageId)) {
	process(source);
	_result.pageId = source.pageId;
	_result.name = source.name;
	_result.rtl = source.page.data().is_rtl();

	const auto views = std::max(
		source.page.data().vviews().value_or_empty(),
		source.updatedCachedViews);
	const auto content = list(source.page.data().vblocks());
	_result.content = wrap(content, views);
}

Prepared Parser::result() {
	return _result;
}

void Parser::process(const Source &source) {
	const auto &data = source.page.data();
	for (const auto &photo : data.vphotos().v) {
		process(photo);
	}
	for (const auto &document : data.vdocuments().v) {
		process(document);
	}
	if (source.webpagePhoto) {
		process(*source.webpagePhoto);
	}
	if (source.webpageDocument) {
		process(*source.webpageDocument);
	}
}

void Parser::process(const MTPPhoto &photo) {
	_photosById.emplace(
		photo.match([](const auto &data) { return data.vid().v; }),
		parse(photo));
}

void Parser::process(const MTPDocument &document) {
	_documentsById.emplace(
		document.match([](const auto &data) { return data.vid().v; }),
		parse(document));
}

template <typename Inner>
QByteArray Parser::list(const MTPVector<Inner> &data) {
	auto result = QByteArrayList();
	result.reserve(data.v.size());
	for (const auto &item : data.v) {
		result.append(item.match([&](const auto &data) {
			return block(data);
		}));
	}
	return result.join(QByteArray());
}

QByteArray Parser::collage(
		const QVector<MTPPageBlock> &list,
		const std::vector<QSize> &dimensions,
		int offset) {
	Expects(list.size() == dimensions.size());

	constexpr auto kPerCollage = 10;
	const auto last = (offset + kPerCollage >= int(dimensions.size()));

	auto result = QByteArray();
	auto slice = ((offset > 0) || (dimensions.size() > kPerCollage))
		? (dimensions
			| ranges::views::drop(offset)
			| ranges::views::take(kPerCollage)
			| ranges::to_vector)
		: dimensions;
	const auto layout = Ui::LayoutMediaGroup(
		slice,
		st::historyGroupWidthMax,
		st::historyGroupWidthMin,
		st::historyGroupSkip);
	auto size = QSize();
	for (const auto &part : layout) {
		const auto &rect = part.geometry;
		size = QSize(
			std::max(size.width(), rect.x() + rect.width()),
			std::max(size.height(), rect.y() + rect.height()));
	}
	for (auto i = 0, count = int(layout.size()); i != count; ++i) {
		const auto &part = layout[i];
		list[offset + i].match([&](const MTPDpageBlockPhoto &data) {
			result += block(data, part, size);
		}, [&](const MTPDpageBlockVideo &data) {
			result += block(data, part, size);
		}, [](const auto &) {
			Unexpected("Block type in collage layout.");
		});
	}
	const auto aspectHeight = size.height() / float64(size.width());
	const auto aspectSkip = st::historyGroupSkip / float64(size.width());
	auto wrapped = tag("figure", {
		{ "class", "collage" },
		{
			"style",
			("padding-top: " + Percent(aspectHeight) + "%; "
				+ "margin-bottom: " + Percent(last ? 0 : aspectSkip) + "%;")
		},
	}, result);
	if (offset + kPerCollage < int(dimensions.size())) {
		wrapped += collage(list, dimensions, offset + kPerCollage);
	}
	return wrapped;
}

QByteArray Parser::slideshow(
		const QVector<MTPPageBlock> &list,
		QSize dimensions) {
	auto result = QByteArray();
	for (auto i = 0, count = int(list.size()); i != count; ++i) {
		list[i].match([&](const MTPDpageBlockPhoto &data) {
			result += block(data, {}, dimensions);
		}, [&](const MTPDpageBlockVideo &data) {
			result += block(data, {}, dimensions);
		}, [](const auto &) {
			Unexpected("Block type in collage layout.");
		});
	}

	auto inputs = QByteArrayList();
	for (auto i = 0; i != int(list.size()); ++i) {
		auto attributes = Attributes{
			{ "type", "radio" },
			{ "name", "s" },
			{ "value", Number(i) },
			{ "onchange", "return IV.slideshowSlide(this);" },
		};
		if (!i) {
			attributes.push_back({ "checked", std::nullopt });
		}
		inputs.append(tag("label", tag("input", attributes, tag("i"))));
	}
	const auto form = tag(
		"form",
		{ { "class", "slideshow-buttons" } },
		tag("fieldset", inputs.join(QByteArray())));
	const auto navigation = tag("a", {
		{ "class", "slideshow-prev" },
		{ "onclick", "IV.slideshowSlide(this, -1);" },
	}, ArrowSvg(true)) + tag("a", {
		{ "class", "slideshow-next" },
		{ "onclick", "IV.slideshowSlide(this, 1);" },
	}, ArrowSvg(false));
	auto wrapStyle = "padding-top: calc(min("
		+ Percent(dimensions.height() / float64(dimensions.width()))
		+ "%, 480px));";
	result = form + tag("figure", {
		{ "class", "slideshow" },
	}, result) + navigation;
	return tag("figure", {
		{ "class", "slideshow-wrap" },
		{ "style", wrapStyle },
	}, result);
}

QByteArray Parser::block(const MTPDpageBlockUnsupported &data) {
	return QByteArray();
}

QByteArray Parser::block(const MTPDpageBlockTitle &data) {
	return tag("h1", {
		{ "class", "title" },
		{ "dir", "auto" },
	}, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockSubtitle &data) {
	return tag("h2", {
		{ "class", "subtitle" },
		{ "dir", "auto" },
	}, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockAuthorDate &data) {
	auto inner = rich(data.vauthor());
	if (const auto date = data.vpublished_date().v) {
		inner += " \xE2\x80\xA2 " + tag("time", Date(date));
	}
	return tag("address", { { "dir", "auto" } }, inner);
}

QByteArray Parser::block(const MTPDpageBlockHeader &data) {
	return tag("h3", {
		{ "class", "header" },
		{ "dir", "auto" },
	}, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockSubheader &data) {
	return tag("h4", {
		{ "class", "subheader" },
		{ "dir", "auto" },
	}, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockParagraph &data) {
	return tag("p", { { "dir", "auto" } }, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockPreformatted &data) {
	auto list = Attributes{ { "dir", "auto" } };
	const auto language = utf(data.vlanguage());
	if (!language.isEmpty()) {
		list.push_back({ "data-language", language });
		list.push_back({ "class", "lang-" + language });
		_result.hasCode = true;
	}
	return tag("pre", list, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockFooter &data) {
	return tag("footer", {
		{ "class", "footer" },
		{ "dir", "auto" },
	}, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockDivider &data) {
	return tag("hr", Attributes{ { "class", "divider" } });
}

QByteArray Parser::block(const MTPDpageBlockAnchor &data) {
	return tag("a", { { "name", utf(data.vname()) } });
}

QByteArray Parser::block(const MTPDpageBlockList &data) {
	return tag("ul", list(data.vitems()));
}

QByteArray Parser::block(const MTPDpageBlockBlockquote &data) {
	const auto caption = rich(data.vcaption());
	const auto cite = caption.isEmpty()
		? QByteArray()
		: tag("cite", { { "dir", "auto" } }, caption);
	return tag("blockquote", {
		{ "dir", "auto" }
	}, rich(data.vtext()) + cite);
}

QByteArray Parser::block(const MTPDpageBlockPullquote &data) {
	const auto caption = rich(data.vcaption());
	const auto cite = caption.isEmpty()
		? QByteArray()
		: tag("cite", { { "dir", "auto" } }, caption);
	return tag("div", {
		{ "class", "pullquote" },
		{ "dir", "auto" },
	}, rich(data.vtext()) + cite);
}

QByteArray Parser::block(
		const MTPDpageBlockPhoto &data,
		const Ui::GroupMediaLayout &layout,
		QSize outer) {
	const auto collage = !layout.geometry.isEmpty();
	const auto slideshow = !collage && !outer.isEmpty();
	const auto photo = photoById(data.vphoto_id().v);
	if (!photo.id) {
		return "Photo not found.";
	}
	const auto src = photoFullUrl(photo);
	auto wrapStyle = QByteArray();
	if (collage) {
		const auto wcoef = 1. / outer.width();
		const auto hcoef = 1. / outer.height();
		wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
			+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
			+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
			+ "height: " + Percent(layout.geometry.height() * hcoef) + "%";
	} else if (!slideshow && photo.width) {
		wrapStyle += "max-width:" + Number(photo.width) + "px";
	}
	const auto dimension = collage
		? (layout.geometry.height() / float64(layout.geometry.width()))
		: (photo.width && photo.height)
		? (photo.height / float64(photo.width))
		: (3 / 4.);
	const auto paddingTop = collage
		? Percent(dimension) + "%"
		: "calc(min(480px, " + Percent(dimension) + "%))";
	const auto style = "background-image:url('" + src + "');"
		"padding-top: " + paddingTop + ";";
	auto inner = tag("div", {
		{ "class", "photo" },
		{ "style", style } });
	const auto minithumb = Images::ExpandInlineBytes(photo.minithumbnail);
	if (!minithumb.isEmpty()) {
		const auto image = Images::Read({ .content = minithumb });
		inner = tag("div", {
			{ "class", "photo-bg" },
			{ "style", "background-image:url('data:image/jpeg;base64,"
				+ minithumb.toBase64()
				+ "');" },
		}) + inner;
	}
	auto attributes = Attributes{
		{ "class", "photo-wrap" },
		{ "style", wrapStyle }
	};
	auto result = tag("div", attributes, inner);

	const auto href = data.vurl() ? utf(*data.vurl()) : photoFullUrl(photo);
	const auto id = Number(photo.id);
	result = tag("a", {
		{ "href", href },
		{ "oncontextmenu", data.vurl() ? QByteArray() : "return false;" },
		{ "data-context", data.vurl() ? QByteArray() : "viewer-photo" + id },
	}, result);
	if (!slideshow) {
		result += caption(data.vcaption());
		if (!collage) {
			result = tag("div", { { "class", "media-outer" } }, result);
		}
	}
	return result;
}

QByteArray Parser::block(
		const MTPDpageBlockVideo &data,
		const Ui::GroupMediaLayout &layout,
		QSize outer) {
	const auto collage = !layout.geometry.isEmpty();
	const auto slideshow = !collage && !outer.isEmpty();
	const auto collageSmall = collage
		&& (layout.geometry.width() < outer.width());
	const auto video = documentById(data.vvideo_id().v);
	if (!video.id) {
		return "Video not found.";
	}
	auto inner = tag("div", {
		{ "class", "video" },
		{ "data-src", documentFullUrl(video) },
		{ "data-autoplay", data.is_autoplay() ? "1" : "0" },
		{ "data-loop", data.is_loop() ? "1" : "0" },
		{ "data-small", collageSmall ? "1" : "0" },
	});
	const auto minithumb = Images::ExpandInlineBytes(video.minithumbnail);
	if (!minithumb.isEmpty()) {
		const auto image = Images::Read({ .content = minithumb });
		inner = tag("div", {
			{ "class", "video-bg" },
			{ "style", "background-image:url('data:image/jpeg;base64,"
				+ minithumb.toBase64()
				+ "');" },
		}) + inner;
	}
	auto wrapStyle = QByteArray();
	if (collage) {
		const auto wcoef = 1. / outer.width();
		const auto hcoef = 1. / outer.height();
		wrapStyle += "left: " + Percent(layout.geometry.x() * wcoef) + "%; "
			+ "top: " + Percent(layout.geometry.y() * hcoef) + "%; "
			+ "width: " + Percent(layout.geometry.width() * wcoef) + "%; "
			+ "height: " + Percent(layout.geometry.height() * hcoef) + "%; ";
	} else {
		const auto dimension = (video.width && video.height)
			? (video.height / float64(video.width))
			: (3 / 4.);
		wrapStyle += "padding-top: calc(min(480px, "
			+ Percent(dimension)
			+ "%));";
	}
	auto attributes = Attributes{
		{ "class", "video-wrap" },
		{ "style", wrapStyle },
	};
	auto result = tag("div", attributes, inner);
	if (data.is_autoplay() || collageSmall) {
		const auto id = Number(video.id);
		const auto href = resource("video" + id);
		result = tag("a", {
			{ "href", href },
			{ "oncontextmenu", "return false;" },
			{ "data-context", "viewer-video" + id },
		}, result);
	}
	if (!slideshow) {
		result += caption(data.vcaption());
		if (!collage) {
			result = tag("div", { { "class", "media-outer" } }, result);
		}
	}
	return result;
}

QByteArray Parser::block(const MTPDpageBlockCover &data) {
	return tag("figure", data.vcover().match([&](const auto &data) {
		return block(data);
	}));
}

QByteArray Parser::block(const MTPDpageBlockEmbed &data) {
	_result.hasEmbeds = true;
	auto eclass = data.is_full_width() ? QByteArray() : "nowide";
	auto width = QByteArray();
	auto height = QByteArray();
	auto iframeWidth = QByteArray();
	auto iframeHeight = QByteArray();
	const auto autosize = !data.vw();
	if (autosize) {
		iframeWidth = "100%";
		eclass = "nowide";
	} else if (data.is_full_width() || !data.vw()->v) {
		width = "100%";
		height = Number(data.vh()->v) + "px";
		iframeWidth = "100%";
		iframeHeight = height;
	} else {
		width = Number(data.vw()->v) + "px";
		height = Percent(data.vh()->v / float64(data.vw()->v)) + "%";
	}
	auto attributes = Attributes();
	if (autosize) {
		attributes.push_back({ "class", "autosize" });
	}
	attributes.push_back({ "width", iframeWidth });
	attributes.push_back({ "height", iframeHeight });
	if (const auto url = data.vurl()) {
		if (!autosize) {
			attributes.push_back({ "src", utf(url) });
		} else {
			attributes.push_back({ "srcdoc", utf(url) });
		}
	} else if (const auto html = data.vhtml()) {
		attributes.push_back({ "src", embedUrl(html->v) });
	}
	if (!data.is_allow_scrolling()) {
		attributes.push_back({ "scrolling", "no" });
	}
	attributes.push_back({ "frameborder", "0" });
	attributes.push_back({ "allowtransparency", "true" });
	attributes.push_back({ "allowfullscreen", "true" });
	auto result = tag("iframe", attributes);
	if (!autosize) {
		result = tag("div", {
			{ "class", "iframe-wrap" },
			{ "style", "width:" + width },
		}, tag("div", {
			{ "style", "padding-bottom: " + height },
		}, result));
	}
	result += caption(data.vcaption());
	return tag("figure", { { "class", eclass } }, result);
}

QByteArray Parser::block(const MTPDpageBlockEmbedPost &data) {
	auto result = QByteArray();
	if (!data.vblocks().v.isEmpty()) {
		auto address = QByteArray();
		const auto photo = photoById(data.vauthor_photo_id().v);
		if (photo.id) {
			const auto src = photoFullUrl(photo);
			address += tag(
				"figure",
				{ { "style", "background-image:url('" + src + "')" } });
		}
		address += tag(
			"a",
			{ { "rel", "author" }, { "onclick", "return false;" } },
			utf(data.vauthor()));
		if (const auto date = data.vdate().v) {
			const auto parsed = base::unixtime::parse(date);
			address += tag("time", Date(date));
		}
		const auto inner = tag("address", address) + list(data.vblocks());
		result = tag("blockquote", { { "class", "embed-post" } }, inner);
	} else {
		const auto url = utf(data.vurl());
		const auto inner = tag("strong", utf(data.vauthor()))
			+ tag(
				"small",
				tag("a", { { "href", url } }, url));
		result = tag("section", { { "class", "embed-post" } }, inner);
	}
	result += caption(data.vcaption());
	return tag("figure", result);
}

QByteArray Parser::block(const MTPDpageBlockCollage &data) {
	const auto &items = data.vitems().v;
	const auto dimensions = computeCollageDimensions(items);
	if (dimensions.empty()) {
		return tag(
			"figure",
			tag("figure", list(data.vitems())) + caption(data.vcaption()));
	}

	return tag(
		"figure",
		{ { "class", "collage-wrap" } },
		collage(items, dimensions) + caption(data.vcaption()));
}

QByteArray Parser::block(const MTPDpageBlockSlideshow &data) {
	const auto &items = data.vitems().v;
	const auto dimensions = computeSlideshowDimensions(items);
	if (dimensions.isEmpty()) {
		return list(data.vitems());
	}
	const auto result = slideshow(items, dimensions);
	return tag("figure", result + caption(data.vcaption()));
}

QByteArray Parser::block(const MTPDpageBlockChannel &data) {
	auto name = QByteArray();
	auto username = QByteArray();
	auto id = data.vchannel().match([](const auto &data) {
		return Number(data.vid().v);
	});
	data.vchannel().match([&](const MTPDchannel &data) {
		if (const auto has = data.vusername()) {
			username = utf(*has);
		}
		name = utf(data.vtitle());
	}, [&](const MTPDchat &data) {
		name = utf(data.vtitle());
	}, [](const auto &) {
	});
	auto result = tag(
		"div",
		{ { "class", "join" }, { "data-context", "join_link" + id } },
		tag("span")
	) + tag("h4", name);
	const auto link = username.isEmpty()
		? "javascript:alert('Channel Link');"
		: "https://t.me/" + username;
	result = tag(
		"a",
		{ { "href", link }, { "data-context", "channel" + id } },
		result);
	_result.channelIds.emplace(id);
	return tag("section", {
		{ "class", "channel joined" },
		{ "data-context", "channel" + id },
	}, result);
}

QByteArray Parser::block(const MTPDpageBlockAudio &data) {
	const auto audio = documentById(data.vaudio_id().v);
	if (!audio.id) {
		return "Audio not found.";
	}
	const auto src = documentFullUrl(audio);
	return tag("figure", tag("audio", {
		{ "src", src },
		{ "oncontextmenu", "return false;" },
		{ "controls", std::nullopt },
	}) + caption(data.vcaption()));
}

QByteArray Parser::block(const MTPDpageBlockKicker &data) {
	return tag("h5", {
		{ "class", "kicker" },
		{ "dir", "auto" },
	}, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageBlockTable &data) {
	auto classes = QByteArrayList();
	if (data.is_bordered()) {
		classes.push_back("bordered");
	}
	if (data.is_striped()) {
		classes.push_back("striped");
	}
	auto attibutes = Attributes();
	if (!classes.isEmpty()) {
		attibutes.push_back({ "class", classes.join(" ") });
	}
	auto title = rich(data.vtitle());
	if (!title.isEmpty()) {
		title = tag("caption", { { "dir", "auto" } }, title);
	}
	auto result = tag("table", attibutes, title + list(data.vrows()));
	result = tag("figure", { { "class", "table" } }, result);
	result = tag("figure", { { "class", "table-wrap" } }, result);
	return tag("figure", result);
}

QByteArray Parser::block(const MTPDpageBlockOrderedList &data) {
	return tag("ol", list(data.vitems()));
}

QByteArray Parser::block(const MTPDpageBlockDetails &data) {
	auto attributes = Attributes();
	if (data.is_open()) {
		attributes.push_back({ "open", std::nullopt });
	}
	return tag(
		"details",
		attributes,
		(tag("summary", { { "dir", "auto" } }, rich(data.vtitle()))
			+ list(data.vblocks())));
}

QByteArray Parser::block(const MTPDpageBlockRelatedArticles &data) {
	const auto result = list(data.varticles());
	if (result.isEmpty()) {
		return QByteArray();
	}
	auto title = rich(data.vtitle());
	if (!title.isEmpty()) {
		title = tag("h4", {
			{ "class", "related-title" },
			{ "dir", "auto" },
		}, title);
	}
	return tag("section", { { "class", "related" } }, title + result);
}

QByteArray Parser::block(const MTPDpageBlockMap &data) {
	const auto geo = parse(data.vgeo());
	if (!geo.access) {
		return "Map not found.";
	}
	const auto width = 650;
	const auto height = std::min(450, (data.vh().v * width / data.vw().v));
	return tag("figure", tag("img", {
		{ "src", mapUrl(geo, width, height, data.vzoom().v) },
	}) + caption(data.vcaption()));
}

QByteArray Parser::block(const MTPDpageRelatedArticle &data) {
	auto result = QByteArray();
	const auto photo = photoById(data.vphoto_id().value_or_empty());
	if (photo.id) {
		const auto src = photoFullUrl(photo);
		result += tag("i", {
			{ "class", "related-link-thumb" },
			{ "style", "background-image:url('" + src + "')" },
		});
	}
	const auto title = data.vtitle();
	const auto description = data.vdescription();
	const auto author = data.vauthor();
	const auto published = data.vpublished_date();
	if (title || description || author || published) {
		auto inner = QByteArray();
		if (title) {
			inner += tag(
				"span",
				{ { "class", "related-link-title" } },
				utf(*title));
		}
		if (description) {
			inner += tag(
				"span",
				{ { "class", "related-link-desc" } },
				utf(*description));
		}
		if (author || published) {
			inner += tag(
				"span",
				{ { "class", "related-link-source" } },
				((author ? utf(*author) : QByteArray())
					+ ((author && published) ? ", " : QByteArray())
					+ (published ? Date(published->v) : QByteArray())));
		}
		result += tag("span", {
			{ "class", "related-link-content" },
		}, inner);
	}
	const auto webpageId = data.vwebpage_id().v;
	const auto context = webpageId
		? ("webpage" + Number(webpageId))
		: QByteArray();
	return tag("a", {
		{ "class", "related-link" },
		{ "href", utf(data.vurl()) },
		{ "data-context", context },
	}, result);
}

QByteArray Parser::block(const MTPDpageTableRow &data) {
	return tag("tr", list(data.vcells()));
}

QByteArray Parser::block(const MTPDpageTableCell &data) {
	const auto text = data.vtext() ? rich(*data.vtext()) : QByteArray();
	auto style = QByteArray();
	if (data.is_align_right()) {
		style += "text-align:right;";
	} else if (data.is_align_center()) {
		style += "text-align:center;";
	} else {
		style += "text-align:left;";
	}
	if (data.is_valign_bottom()) {
		style += "vertical-align:bottom;";
	} else if (data.is_valign_middle()) {
		style += "vertical-align:middle;";
	} else {
		style += "vertical-align:top;";
	}
	auto attributes = Attributes{ { "style", style }, { "dir", "auto" } };
	if (const auto cs = data.vcolspan()) {
		attributes.push_back({ "colspan", Number(cs->v) });
	}
	if (const auto rs = data.vrowspan()) {
		attributes.push_back({ "rowspan", Number(rs->v) });
	}
	return tag(data.is_header() ? "th" : "td", attributes, text);
}

QByteArray Parser::block(const MTPDpageListItemText &data) {
	return tag("li", { { "dir", "auto" } }, rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageListItemBlocks &data) {
	return tag("li", list(data.vblocks()));
}

QByteArray Parser::block(const MTPDpageListOrderedItemText &data) {
	return tag(
		"li",
		{ { "value", utf(data.vnum()) }, { "dir", "auto" } },
		rich(data.vtext()));
}

QByteArray Parser::block(const MTPDpageListOrderedItemBlocks &data) {
	return tag(
		"li",
		{ { "value", utf(data.vnum()) } },
		list(data.vblocks()));
}

QByteArray Parser::utf(const MTPstring &text) {
	return Escape(text.v);
}

QByteArray Parser::utf(const tl::conditional<MTPstring> &text) {
	return text ? utf(*text) : QByteArray();
}

QByteArray Parser::wrap(const QByteArray &content, int views) {
	const auto sep = " \xE2\x80\xA2 ";
	const auto viewsText = views
		? (tr::lng_stories_views(tr::now, lt_count_decimal, views) + sep)
		: QString();
	return R"(
<div class="page-slide">
	<article>)"_q + content + R"(</article>
</div>
<div class="page-footer">
	<div class="content">
		)"_q
		+ viewsText.toUtf8()
		+ R"(<a class="wrong" data-context="report-iv">)"_q
		+ tr::lng_iv_wrong_layout(tr::now).toUtf8()
		+ R"(</a>
	</div>
</div>)"_q;
}

QByteArray Parser::tag(
		const QByteArray &name,
		const QByteArray &body) {
	return tag(name, {}, body);
}

QByteArray Parser::tag(
		const QByteArray &name,
		const Attributes &attributes,
		const QByteArray &body) {
	auto list = QByteArrayList();
	list.reserve(attributes.size());
	for (auto &[name, value] : attributes) {
		list.push_back(' ' + name + (value ? "=\"" + *value + "\"" : ""));
	}
	const auto serialized = list.join(QByteArray());
	return (IsVoidElement(name) && body.isEmpty())
		? ('<' + name + serialized + " />")
		: ('<' + name + serialized + '>' + body + "</" + name + '>');
}

QByteArray Parser::rich(const MTPRichText &text) {
	return text.match([&](const MTPDtextEmpty &data) {
		return QByteArray();
	}, [&](const MTPDtextPlain &data) {
		struct Replacement {
			QByteArray from;
			QByteArray to;
		};
		const auto replacements = std::vector<Replacement>{
			{ "\xE2\x81\xA6", "<span dir=\"ltr\">" },
			{ "\xE2\x81\xA7", "<span dir=\"rtl\">" },
			{ "\xE2\x81\xA8", "<span dir=\"auto\">" },
			{ "\xE2\x81\xA9", "</span>" },
		};
		auto text = utf(data.vtext());
		for (const auto &[from, to] : replacements) {
			text.replace(from, to);
		}
		return text;
	}, [&](const MTPDtextConcat &data) {
		const auto &list = data.vtexts().v;
		auto result = QByteArrayList();
		result.reserve(list.size());
		for (const auto &item : list) {
			result.append(rich(item));
		}
		return result.join(QByteArray());
	}, [&](const MTPDtextImage &data) {
		const auto image = documentById(data.vdocument_id().v);
		if (!image.id) {
			return "Image not found."_q;
		}
		auto attributes = Attributes{
			{ "class", "pic" },
			{ "src", documentFullUrl(image) },
		};
		if (const auto width = data.vw().v) {
			attributes.push_back({ "width", Number(width) });
		}
		if (const auto height = data.vh().v) {
			attributes.push_back({ "height", Number(height) });
		}
		return tag("img", attributes);
	}, [&](const MTPDtextBold &data) {
		return tag("b", rich(data.vtext()));
	}, [&](const MTPDtextItalic &data) {
		return tag("i", rich(data.vtext()));
	}, [&](const MTPDtextUnderline &data) {
		return tag("u", rich(data.vtext()));
	}, [&](const MTPDtextStrike &data) {
		return tag("s", rich(data.vtext()));
	}, [&](const MTPDtextFixed &data) {
		return tag("code", rich(data.vtext()));
	}, [&](const MTPDtextUrl &data) {
		const auto webpageId = data.vwebpage_id().v;
		const auto context = webpageId
			? ("webpage" + Number(webpageId))
			: QByteArray();
		return tag("a", {
			{ "href", utf(data.vurl()) },
			{ "class", webpageId ? "internal-iv-link" : "" },
			{ "data-context", context },
		}, rich(data.vtext()));
	}, [&](const MTPDtextEmail &data) {
		return tag("a", {
			{ "href", "mailto:" + utf(data.vemail()) },
		}, rich(data.vtext()));
	}, [&](const MTPDtextSubscript &data) {
		return tag("sub", rich(data.vtext()));
	}, [&](const MTPDtextSuperscript &data) {
		return tag("sup", rich(data.vtext()));
	}, [&](const MTPDtextMarked &data) {
		return tag("mark", rich(data.vtext()));
	}, [&](const MTPDtextPhone &data) {
		return tag("a", {
			{ "href", "tel:" + utf(data.vphone()) },
		}, rich(data.vtext()));
	}, [&](const MTPDtextAnchor &data) {
		const auto inner = rich(data.vtext());
		const auto name = utf(data.vname());
		return inner.isEmpty()
			? tag("a", { { "name", name } })
			: tag(
				"span",
				{ { "class", "reference" } },
				tag("a", { { "name", name } }) + inner);
	});
}

QByteArray Parser::caption(const MTPPageCaption &caption) {
	auto text = rich(caption.data().vtext());
	const auto credit = rich(caption.data().vcredit());
	if (!credit.isEmpty()) {
		text += tag("cite", { { "dir", "auto" } }, credit);
	} else if (text.isEmpty()) {
		return QByteArray();
	}
	return tag("figcaption", { { "dir", "auto" } }, text);
}

Photo Parser::parse(const MTPPhoto &photo) {
	auto result = Photo{
		.id = photo.match([&](const auto &d) { return d.vid().v; }),
	};
	auto sizes = base::flat_map<QByteArray, QSize>();
	photo.match([](const MTPDphotoEmpty &) {
	}, [&](const MTPDphoto &data) {
		for (const auto &size : data.vsizes().v) {
			size.match([&](const MTPDphotoSizeEmpty &data) {
			}, [&](const MTPDphotoSize &data) {
				sizes.emplace(
					data.vtype().v,
					QSize(data.vw().v, data.vh().v));
			}, [&](const MTPDphotoCachedSize &data) {
				sizes.emplace(
					data.vtype().v,
					QSize(data.vw().v, data.vh().v));
			}, [&](const MTPDphotoStrippedSize &data) {
				result.minithumbnail = data.vbytes().v;
			}, [&](const MTPDphotoSizeProgressive &data) {
				sizes.emplace(
					data.vtype().v,
					QSize(data.vw().v, data.vh().v));
			}, [&](const MTPDphotoPathSize &data) {
			});
		}
	});
	for (const auto attempt : { "y", "x", "w" }) {
		const auto i = sizes.find(QByteArray(attempt));
		if (i != end(sizes)) {
			result.width = i->second.width();
			result.height = i->second.height();
			break;
		}
	}
	return result;
}

Document Parser::parse(const MTPDocument &document) {
	auto result = Document{
		.id = document.match([&](const auto &d) { return d.vid().v; }),
	};
	document.match([](const MTPDdocumentEmpty &) {
	}, [&](const MTPDdocument &data) {
		for (const auto &attribute : data.vattributes().v) {
			attribute.match([&](const MTPDdocumentAttributeImageSize &data) {
				result.width = data.vw().v;
				result.height = data.vh().v;
			}, [&](const MTPDdocumentAttributeVideo &data) {
				result.width = data.vw().v;
				result.height = data.vh().v;
			}, [](const auto &) {});
		}
		if (const auto sizes = data.vthumbs()) {
			for (const auto &size : sizes->v) {
				size.match([&](const MTPDphotoStrippedSize &data) {
					result.minithumbnail = data.vbytes().v;
				}, [&](const auto &data) {
				});
			}

		}
	});
	return result;
}

Geo Parser::parse(const MTPGeoPoint &geo) {
	return geo.match([](const MTPDgeoPointEmpty &) {
		return Geo();
	}, [&](const MTPDgeoPoint &data) {
		return Geo{
			.lat = data.vlat().v,
			.lon = data.vlong().v,
			.access = data.vaccess_hash().v,
		};
	});
}

Photo Parser::photoById(uint64 id) {
	const auto i = _photosById.find(id);
	return (i != end(_photosById)) ? i->second : Photo();
}

Document Parser::documentById(uint64 id) {
	const auto i = _documentsById.find(id);
	return (i != end(_documentsById)) ? i->second : Document();
}

QByteArray Parser::photoFullUrl(const Photo &photo) {
	return resource("photo/" + Number(photo.id) + _fileOriginPostfix);
}

QByteArray Parser::documentFullUrl(const Document &document) {
	return resource("document/" + Number(document.id) + _fileOriginPostfix);
}

QByteArray Parser::embedUrl(const QByteArray &html) {
	auto binary = std::array<uchar, SHA256_DIGEST_LENGTH>{};
	SHA256(
		reinterpret_cast<const unsigned char*>(html.data()),
		html.size(),
		binary.data());
	const auto hex = [](uchar value) -> char {
		return (value >= 10) ? ('a' + (value - 10)) : ('0' + value);
	};
	auto result = QByteArray();
	result.reserve(binary.size() * 2);
	for (const auto byte : binary) {
		result.push_back(hex(byte / 16));
		result.push_back(hex(byte % 16));
	}
	result += ".html";
	_result.embeds.emplace(result, html);
	return resource("html/" + result);
}

QByteArray Parser::mapUrl(const Geo &geo, int width, int height, int zoom) {
	return resource("map/"
		+ GeoPointId(geo) + "&"
		+ Number(width) + ","
		+ Number(height) + "&"
		+ Number(zoom));
}

QByteArray Parser::resource(QByteArray id) {
	return '/' + id;
}

std::vector<QSize> Parser::computeCollageDimensions(
		const QVector<MTPPageBlock> &items) {
	if (items.size() < 2) {
		return {};
	}
	auto result = std::vector<QSize>(items.size());
	for (auto i = 0, count = int(items.size()); i != count; ++i) {
		items[i].match([&](const MTPDpageBlockPhoto &data) {
			const auto photo = photoById(data.vphoto_id().v);
			if (photo.id && photo.width > 0 && photo.height > 0) {
				result[i] = QSize(photo.width, photo.height);
			}
		}, [&](const MTPDpageBlockVideo &data) {
			const auto document = documentById(data.vvideo_id().v);
			if (document.id && document.width > 0 && document.height > 0) {
				result[i] = QSize(document.width, document.height);
			}
		}, [](const auto &) {});
		if (result[i].isEmpty()) {
			return {};
		}
	}
	return result;
}

QSize Parser::computeSlideshowDimensions(
		const QVector<MTPPageBlock> &items) {
	if (items.size() < 2) {
		return {};
	}
	auto result = QSize();
	for (const auto &item : items) {
		auto size = QSize();
		item.match([&](const MTPDpageBlockPhoto &data) {
			const auto photo = photoById(data.vphoto_id().v);
			if (photo.id && photo.width > 0 && photo.height > 0) {
				size = QSize(photo.width, photo.height);
			}
		}, [&](const MTPDpageBlockVideo &data) {
			const auto document = documentById(data.vvideo_id().v);
			if (document.id && document.width > 0 && document.height > 0) {
				size = QSize(document.width, document.height);
			}
		}, [](const auto &) {});
		if (size.isEmpty()) {
			return {};
		} else if (result.height() * size.width()
			< result.width() * size.height()) {
			result = size;
		}
	}
	return result;
}

} // namespace

Prepared Prepare(const Source &source, const Options &options) {
	auto parser = Parser(source, options);
	return parser.result();
}

} // namespace Iv
