// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/text/text.h"

#include "ui/basic_click_handlers.h"
#include "ui/text/text_block.h"
#include "ui/text/text_isolated_emoji.h"
#include "ui/emoji_config.h"
#include "ui/integration.h"
#include "ui/round_rect.h"
#include "ui/image/image_prepare.h"
#include "ui/spoiler_click_handler.h"
#include "base/platform/base_platform_info.h"
#include "base/qt/qt_common_adapters.h"

#include <private/qfontengine_p.h>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <private/qharfbuzz_p.h>
#endif // Qt < 6.0.0

namespace Ui {
namespace Text {
namespace {

constexpr auto kStringLinkIndexShift = uint16(0x8000);
constexpr auto kMaxDiacAfterSymbol = 2;
constexpr auto kSelectedSpoilerOpacity = 0.5;

inline bool IsMono(int32 flags) {
	return (flags & TextBlockFPre) || (flags & TextBlockFCode);
}

Qt::LayoutDirection StringDirection(
		const QString &str,
		int from,
		int to) {
	auto p = reinterpret_cast<const ushort*>(str.unicode()) + from;
	const auto end = p + (to - from);
	while (p < end) {
		uint ucs4 = *p;
		if (QChar::isHighSurrogate(ucs4) && p < end - 1) {
			ushort low = p[1];
			if (QChar::isLowSurrogate(low)) {
				ucs4 = QChar::surrogateToUcs4(ucs4, low);
				++p;
			}
		}
		switch (QChar::direction(ucs4)) {
		case QChar::DirL:
			return Qt::LeftToRight;
		case QChar::DirR:
		case QChar::DirAL:
			return Qt::RightToLeft;
		default:
			break;
		}
		++p;
	}
	return Qt::LayoutDirectionAuto;
}

TextWithEntities PrepareRichFromRich(
		const TextWithEntities &text,
		const TextParseOptions &options) {
	auto result = text;
	const auto &preparsed = text.entities;
	const bool parseLinks = (options.flags & TextParseLinks);
	const bool parsePlainLinks = (options.flags & TextParsePlainLinks);
	if (!preparsed.isEmpty() && (parseLinks || parsePlainLinks)) {
		bool parseMentions = (options.flags & TextParseMentions);
		bool parseHashtags = (options.flags & TextParseHashtags);
		bool parseBotCommands = (options.flags & TextParseBotCommands);
		bool parseMarkdown = (options.flags & TextParseMarkdown);
		if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) {
			int32 i = 0, l = preparsed.size();
			result.entities.clear();
			result.entities.reserve(l);
			for (; i < l; ++i) {
				auto type = preparsed.at(i).type();
				if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) ||
					(type == EntityType::Hashtag && !parseHashtags) ||
					(type == EntityType::Cashtag && !parseHashtags) ||
					(type == EntityType::PlainLink
						&& !parsePlainLinks
						&& !parseMarkdown) ||
					(!parseLinks
						&& (type == EntityType::Url
							|| type == EntityType::CustomUrl)) ||
					(type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities
					(!parseMarkdown && (type == EntityType::Bold
						|| type == EntityType::Semibold
						|| type == EntityType::Italic
						|| type == EntityType::Underline
						|| type == EntityType::StrikeOut
						|| type == EntityType::Code
						|| type == EntityType::Pre))) {
					continue;
				}
				result.entities.push_back(preparsed.at(i));
			}
		}
	}
	return result;
}

QFixed ComputeStopAfter(
		const TextParseOptions &options,
		const style::TextStyle &st) {
	return (options.maxw > 0 && options.maxh > 0)
		? ((options.maxh / st.font->height) + 1) * options.maxw
		: QFIXED_MAX;
}

// Open Sans tilde fix.
bool ComputeCheckTilde(const style::TextStyle &st) {
	const auto &font = st.font;
	return (font->size() * style::DevicePixelRatio() == 13)
		&& (font->flags() == 0)
		&& (font->f.family() == qstr("DAOpenSansRegular"));
}

bool IsParagraphSeparator(QChar ch) {
	switch (ch.unicode()) {
	case QChar::LineFeed:
		return true;
	default:
		break;
	}
	return false;
}

bool IsBad(QChar ch) {
	return (ch == 0)
		|| (ch >= 8232 && ch < 8237)
		|| (ch >= 65024 && ch < 65040 && ch != 65039)
		|| (ch >= 127 && ch < 160 && ch != 156)

		// qt harfbuzz crash see https://github.com/telegramdesktop/tdesktop/issues/4551
		|| (Platform::IsMac() && ch == 6158);
}

} // namespace
} // namespace Text
} // namespace Ui

const TextParseOptions kDefaultTextOptions = {
	TextParseLinks | TextParseMultiline, // flags
	0, // maxw
	0, // maxh
	Qt::LayoutDirectionAuto, // dir
};

const TextParseOptions kMarkupTextOptions = {
	TextParseLinks | TextParseMultiline | TextParseMarkdown, // flags
	0, // maxw
	0, // maxh
	Qt::LayoutDirectionAuto, // dir
};

const TextParseOptions kPlainTextOptions = {
	TextParseMultiline, // flags
	0, // maxw
	0, // maxh
	Qt::LayoutDirectionAuto, // dir
};

namespace Ui {
namespace Text {

class Parser {
public:
	Parser(
		not_null<String*> string,
		const TextWithEntities &textWithEntities,
		const TextParseOptions &options,
		const std::any &context);

private:
	struct ReadyToken {
	};

	class StartedEntity {
	public:
		enum class Type {
			Flags,
			Link,
			IndexedLink,
			Spoiler,
		};

		explicit StartedEntity(TextBlockFlags flags);
		explicit StartedEntity(uint16 index, Type type);

		std::optional<TextBlockFlags> flags() const;
		std::optional<uint16> lnkIndex() const;
		std::optional<uint16> spoilerIndex() const;

	private:
		const int _value = 0;
		const Type _type;

	};

	Parser(
		not_null<String*> string,
		TextWithEntities &&source,
		const TextParseOptions &options,
		const std::any &context,
		ReadyToken);

	void trimSourceRange();
	void blockCreated();
	void createBlock(int32 skipBack = 0);
	void createSkipBlock(int32 w, int32 h);
	void createNewlineBlock();

	// Returns true if at least one entity was parsed in the current position.
	bool checkEntities();
	void parseCurrentChar();
	void parseEmojiFromCurrent();
	void finalize(const TextParseOptions &options);

	void finishEntities();
	void skipPassedEntities();
	void skipBadEntities();

	bool isInvalidEntity(const EntityInText &entity) const;
	bool isLinkEntity(const EntityInText &entity) const;

	bool processCustomIndex(uint16 index);

	void parse(const TextParseOptions &options);
	void computeLinkText(
		const QString &linkData,
		QString *outLinkText,
		EntityLinkShown *outShown);

	const not_null<String*> _t;
	const TextWithEntities _source;
	const std::any &_context;
	const QChar * const _start = nullptr;
	const QChar *_end = nullptr; // mutable, because we trim by decrementing.
	const QChar *_ptr = nullptr;
	const EntitiesInText::const_iterator _entitiesEnd;
	EntitiesInText::const_iterator _waitingEntity;
	const bool _multiline = false;

	const QFixed _stopAfterWidth; // summary width of all added words
	const bool _checkTilde = false; // do we need a special text block for tilde symbol

	std::vector<uint16> _linksIndexes;

	std::vector<EntityLinkData> _links;
	std::vector<EntityLinkData> _spoilers;
	std::vector<EntityLinkData> _monos;
	base::flat_map<
		const QChar*,
		std::vector<StartedEntity>> _startedEntities;

	uint16 _maxLnkIndex = 0;
	uint16 _maxShiftedLnkIndex = 0;

	// current state
	int32 _flags = 0;
	uint16 _lnkIndex = 0;
	uint16 _spoilerIndex = 0;
	uint16 _monoIndex = 0;
	EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero
	int32 _blockStart = 0; // offset in result, from which current parsed block is started
	int32 _diacs = 0; // diac chars skipped without good char
	QFixed _sumWidth;
	bool _sumFinished = false;
	bool _newlineAwaited = false;

	// current char data
	QChar _ch; // current char (low surrogate, if current char is surrogate pair)
	int32 _emojiLookback = 0; // how far behind the current ptr to look for current emoji
	bool _lastSkipped = false; // did we skip current char

};

Parser::StartedEntity::StartedEntity(TextBlockFlags flags)
: _value(flags)
, _type(Type::Flags) {
	Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
}

Parser::StartedEntity::StartedEntity(uint16 index, Type type)
: _value(index)
, _type(type) {
	Expects((_type == Type::Link)
		? (_value >= kStringLinkIndexShift)
		: (_value < kStringLinkIndexShift));
}

std::optional<TextBlockFlags> Parser::StartedEntity::flags() const {
	if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) {
		return TextBlockFlags(_value);
	}
	return std::nullopt;
}

std::optional<uint16> Parser::StartedEntity::lnkIndex() const {
	if ((_value < int(kStringLinkIndexShift) && (_type == Type::IndexedLink))
		|| (_value >= int(kStringLinkIndexShift) && (_type == Type::Link))) {
		return uint16(_value);
	}
	return std::nullopt;
}

std::optional<uint16> Parser::StartedEntity::spoilerIndex() const {
	if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) {
		return uint16(_value);
	}
	return std::nullopt;
}

Parser::Parser(
	not_null<String*> string,
	const TextWithEntities &textWithEntities,
	const TextParseOptions &options,
	const std::any &context)
: Parser(
	string,
	PrepareRichFromRich(textWithEntities, options),
	options,
	context,
	ReadyToken()) {
}

Parser::Parser(
	not_null<String*> string,
	TextWithEntities &&source,
	const TextParseOptions &options,
	const std::any &context,
	ReadyToken)
: _t(string)
, _source(std::move(source))
, _context(context)
, _start(_source.text.constData())
, _end(_start + _source.text.size())
, _ptr(_start)
, _entitiesEnd(_source.entities.end())
, _waitingEntity(_source.entities.begin())
, _multiline(options.flags & TextParseMultiline)
, _stopAfterWidth(ComputeStopAfter(options, *_t->_st))
, _checkTilde(ComputeCheckTilde(*_t->_st)) {
	parse(options);
}

void Parser::blockCreated() {
	_sumWidth += _t->_blocks.back()->f_width();
	if (_sumWidth.floor().toInt() > _stopAfterWidth) {
		_sumFinished = true;
	}
}

void Parser::createBlock(int32 skipBack) {
	if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) {
		_maxLnkIndex = _lnkIndex;
	}
	if (_lnkIndex > kStringLinkIndexShift) {
		_maxShiftedLnkIndex = std::max(
			uint16(_lnkIndex - kStringLinkIndexShift),
			_maxShiftedLnkIndex);
	}

	int32 len = int32(_t->_text.size()) + skipBack - _blockStart;
	if (len > 0) {
		bool newline = !_emoji && (len == 1 && _t->_text.at(_blockStart) == QChar::LineFeed);
		if (_newlineAwaited) {
			_newlineAwaited = false;
			if (!newline) {
				_t->_text.insert(_blockStart, QChar::LineFeed);
				createBlock(skipBack - len);
			}
		}
		_lastSkipped = false;
		const auto lnkIndex = _monoIndex ? _monoIndex : _lnkIndex;
		if (_emoji) {
			_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex, _emoji));
			_emoji = nullptr;
			_lastSkipped = true;
		} else if (newline) {
			_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, lnkIndex, _spoilerIndex));
		} else {
			_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, lnkIndex, _spoilerIndex));
		}
		_blockStart += len;
		blockCreated();
	}
}

void Parser::createSkipBlock(int32 w, int32 h) {
	createBlock();
	_t->_text.push_back('_');
	_t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _monoIndex ? _monoIndex : _lnkIndex, _spoilerIndex));
	blockCreated();
}

void Parser::createNewlineBlock() {
	createBlock();
	_t->_text.push_back(QChar::LineFeed);
	createBlock();
}

void Parser::finishEntities() {
	while (!_startedEntities.empty()
		&& (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) {
		auto list = std::move(_startedEntities.begin()->second);
		_startedEntities.erase(_startedEntities.begin());

		while (!list.empty()) {
			if (const auto flags = list.back().flags()) {
				if (_flags & (*flags)) {
					createBlock();
					_flags &= ~(*flags);
					if (((*flags) & TextBlockFPre)
						&& !_t->_blocks.empty()
						&& _t->_blocks.back()->type() != TextBlockTNewline) {
						_newlineAwaited = true;
					}
					if (IsMono(*flags)) {
						_monoIndex = 0;
					}
				}
			} else if (const auto lnkIndex = list.back().lnkIndex()) {
				if (_lnkIndex == *lnkIndex) {
					createBlock();
					_lnkIndex = 0;
				}
			} else if (const auto spoilerIndex = list.back().spoilerIndex()) {
				if (_spoilerIndex == *spoilerIndex && (_spoilerIndex != 0)) {
					createBlock();
					_spoilerIndex = 0;
				}
			}
			list.pop_back();
		}
	}
}

// Returns true if at least one entity was parsed in the current position.
bool Parser::checkEntities() {
	finishEntities();
	skipPassedEntities();
	if (_waitingEntity == _entitiesEnd
		|| _ptr < _start + _waitingEntity->offset()) {
		return false;
	}

	auto flags = TextBlockFlags();
	auto link = EntityLinkData();
	auto monoIndex = 0;
	const auto entityType = _waitingEntity->type();
	const auto entityLength = _waitingEntity->length();
	const auto entityBegin = _start + _waitingEntity->offset();
	const auto entityEnd = entityBegin + entityLength;
	const auto pushSimpleUrl = [&](EntityType type) {
		link.type = type;
		link.data = QString(entityBegin, entityLength);
		if (type == EntityType::Url) {
			computeLinkText(link.data, &link.text, &link.shown);
		} else {
			link.text = link.data;
		}
	};
	const auto pushComplexUrl = [&] {
		link.type = entityType;
		link.data = _waitingEntity->data();
		link.text = QString(entityBegin, entityLength);
	};
	if (entityType == EntityType::Bold) {
		flags = TextBlockFBold;
	} else if (entityType == EntityType::Semibold) {
		flags = TextBlockFSemibold;
	} else if (entityType == EntityType::Italic) {
		flags = TextBlockFItalic;
	} else if (entityType == EntityType::Underline) {
		flags = TextBlockFUnderline;
	} else if (entityType == EntityType::PlainLink) {
		flags = TextBlockFPlainLink;
	} else if (entityType == EntityType::StrikeOut) {
		flags = TextBlockFStrikeOut;
	} else if ((entityType == EntityType::Code) // #TODO entities
		|| (entityType == EntityType::Pre)) {
		if (entityType == EntityType::Code) {
			flags = TextBlockFCode;
		} else {
			flags = TextBlockFPre;
			createBlock();
			if (!_t->_blocks.empty()
				&& _t->_blocks.back()->type() != TextBlockTNewline) {
				createNewlineBlock();
			}
		}
		const auto text = QString(entityBegin, entityLength);

		// It is better to trim the text to identify "Sample\n" as inline.
		const auto trimmed = text.trimmed();
		const auto isSingleLine = !trimmed.isEmpty()
			&& ranges::none_of(trimmed, IsNewline);

		// TODO: remove trimming.
		if (isSingleLine && (entityType == EntityType::Code)) {
			_monos.push_back({ .text = text, .type = entityType });
			monoIndex = _monos.size();
		}
	} else if (entityType == EntityType::Url
		|| entityType == EntityType::Email
		|| entityType == EntityType::Mention
		|| entityType == EntityType::Hashtag
		|| entityType == EntityType::Cashtag
		|| entityType == EntityType::BotCommand) {
		pushSimpleUrl(entityType);
	} else if (entityType == EntityType::CustomUrl) {
		const auto url = _waitingEntity->data();
		const auto text = QString(entityBegin, entityLength);
		if (url == text) {
			pushSimpleUrl(EntityType::Url);
		} else {
			pushComplexUrl();
		}
	} else if (entityType == EntityType::MentionName) {
		pushComplexUrl();
	}

	using Type = StartedEntity::Type;

	if (link.type != EntityType::Invalid) {
		createBlock();

		_links.push_back(link);
		const auto tempIndex = _links.size();
		const auto useCustom = processCustomIndex(tempIndex);
		_lnkIndex = tempIndex + (useCustom ? 0 : kStringLinkIndexShift);
		_startedEntities[entityEnd].emplace_back(
			_lnkIndex,
			useCustom ? Type::IndexedLink : Type::Link);
	} else if (flags) {
		if (!(_flags & flags)) {
			createBlock();
			_flags |= flags;
			_startedEntities[entityEnd].emplace_back(flags);
			_monoIndex = monoIndex;
		}
	} else if (entityType == EntityType::Spoiler) {
		createBlock();

		_spoilers.push_back(EntityLinkData{
			.data = QString::number(_spoilers.size() + 1),
			.type = entityType,
		});
		_spoilerIndex = _spoilers.size();

		_startedEntities[entityEnd].emplace_back(
			_spoilerIndex,
			Type::Spoiler);
	}

	++_waitingEntity;
	skipBadEntities();
	return true;
}

bool Parser::processCustomIndex(uint16 index) {
	auto &url = _links[index - 1].data;
	if (url.isEmpty()) {
		return false;
	}
	if (url.startsWith("internal:index")) {
		const auto customIndex = uint16(url.back().unicode());
		// if (customIndex != index) {
			url = QString();
			_linksIndexes.push_back(customIndex);
			return true;
		// }
	}
	return false;
}

void Parser::skipPassedEntities() {
	while (_waitingEntity != _entitiesEnd
		&& _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) {
		++_waitingEntity;
	}
}

void Parser::skipBadEntities() {
	if (_links.size() >= 0x7FFF) {
		while (_waitingEntity != _entitiesEnd
			&& (isLinkEntity(*_waitingEntity)
				|| isInvalidEntity(*_waitingEntity))) {
			++_waitingEntity;
		}
	} else {
		while (_waitingEntity != _entitiesEnd && isInvalidEntity(*_waitingEntity)) {
			++_waitingEntity;
		}
	}
}

void Parser::parseCurrentChar() {
	_ch = ((_ptr < _end) ? *_ptr : 0);
	_emojiLookback = 0;
	const auto isNewLine = _multiline && IsNewline(_ch);
	const auto isSpace = IsSpace(_ch);
	const auto isDiac = IsDiac(_ch);
	const auto isTilde = _checkTilde && (_ch == '~');
	const auto skip = [&] {
		if (IsBad(_ch) || _ch.isLowSurrogate()) {
			return true;
		} else if (_ch == 0xFE0F && Platform::IsMac()) {
			// Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :(
			return true;
		} else if (isDiac) {
			if (_lastSkipped || _emoji || ++_diacs > kMaxDiacAfterSymbol) {
				return true;
			}
		} else if (_ch.isHighSurrogate()) {
			if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) {
				return true;
			}
			const auto ucs4 = QChar::surrogateToUcs4(_ch, *(_ptr + 1));
			if (ucs4 >= 0xE0000) {
				// Unicode tags are skipped.
				// Only place they work is in some flag emoji,
				// but in that case they were already parsed as emoji before.
				//
				// For unknown reason in some unknown cases strings with such
				// symbols lead to crashes on some Linux distributions, see
				// https://github.com/telegramdesktop/tdesktop/issues/7005
				//
				// At least one crashing text was starting that way:
				//
				// 0xd83d 0xdcda 0xdb40 0xdc69 0xdb40 0xdc64 0xdb40 0xdc6a
				// 0xdb40 0xdc77 0xdb40 0xdc7f 0x32 ... simple text here ...
				//
				// or in codepoints:
				//
				// 0x1f4da 0xe0069 0xe0064 0xe006a 0xe0077 0xe007f 0x32 ...
				return true;
			}
		}
		return false;
	}();

	if (_ch.isHighSurrogate() && !skip) {
		_t->_text.push_back(_ch);
		++_ptr;
		_ch = *_ptr;
		_emojiLookback = 1;
	}

	_lastSkipped = skip;
	if (skip) {
		_ch = 0;
	} else {
		if (isTilde) { // tilde fix in OpenSans
			if (!(_flags & TextBlockFTilde)) {
				createBlock(-_emojiLookback);
				_flags |= TextBlockFTilde;
			}
		} else {
			if (_flags & TextBlockFTilde) {
				createBlock(-_emojiLookback);
				_flags &= ~TextBlockFTilde;
			}
		}
		if (isNewLine) {
			createNewlineBlock();
		} else if (isSpace) {
			_t->_text.push_back(QChar::Space);
		} else {
			if (_emoji) {
				createBlock(-_emojiLookback);
			}
			_t->_text.push_back(_ch);
		}
		if (!isDiac) _diacs = 0;
	}
}

void Parser::parseEmojiFromCurrent() {
	int len = 0;
	auto e = Emoji::Find(_ptr - _emojiLookback, _end, &len);
	if (!e) return;

	for (int l = len - _emojiLookback - 1; l > 0; --l) {
		_t->_text.push_back(*++_ptr);
	}
	if (e->hasPostfix()) {
		Assert(!_t->_text.isEmpty());
		const auto last = _t->_text[_t->_text.size() - 1];
		if (last.unicode() != Emoji::kPostfix) {
			_t->_text.push_back(QChar(Emoji::kPostfix));
			++len;
		}
	}

	createBlock(-len);
	_emoji = e;
}

bool Parser::isInvalidEntity(const EntityInText &entity) const {
	const auto length = entity.length();
	return (_start + entity.offset() + length > _end) || (length <= 0);
}

bool Parser::isLinkEntity(const EntityInText &entity) const {
	const auto type = entity.type();
	const auto urls = {
		EntityType::Url,
		EntityType::CustomUrl,
		EntityType::Email,
		EntityType::Hashtag,
		EntityType::Cashtag,
		EntityType::Mention,
		EntityType::MentionName,
		EntityType::BotCommand
	};
	return ranges::find(urls, type) != std::end(urls);
}

void Parser::parse(const TextParseOptions &options) {
	skipBadEntities();
	trimSourceRange();

	_t->_text.resize(0);
	_t->_text.reserve(_end - _ptr);

	for (; _ptr <= _end; ++_ptr) {
		while (checkEntities()) {
		}
		parseCurrentChar();
		parseEmojiFromCurrent();

		if (_sumFinished || _t->_text.size() >= 0x8000) {
			break; // 32k max
		}
	}
	createBlock();
	finalize(options);
}

void Parser::trimSourceRange() {
	const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset(
		_source.entities,
		_end - _start);

	while (_ptr != _end && IsTrimmed(*_ptr) && _ptr != _start + firstMonospaceOffset) {
		++_ptr;
	}
	while (_ptr != _end && IsTrimmed(*(_end - 1))) {
		--_end;
	}
}

// void Parser::checkForElidedSkipBlock() {
// 	if (!_sumFinished || !_rich) {
// 		return;
// 	}
// 	// We could've skipped the final skip block command.
// 	for (; _ptr < _end; ++_ptr) {
// 		if (*_ptr == TextCommand && readSkipBlockCommand()) {
// 			break;
// 		}
// 	}
// }

void Parser::finalize(const TextParseOptions &options) {
	_t->_links.resize(_maxLnkIndex + _maxShiftedLnkIndex);
	auto counterCustomIndex = uint16(0);
	auto currentIndex = uint16(0); // Current the latest index of _t->_links.
	struct {
		uint16 mono = 0;
		uint16 lnk = 0;
	} lastHandlerIndex;
	const auto avoidIntersectionsWithCustom = [&] {
		while (ranges::contains(_linksIndexes, currentIndex)) {
			currentIndex++;
		}
	};
	for (auto &block : _t->_blocks) {
		const auto spoilerIndex = block->spoilerIndex();
		if (spoilerIndex && (_t->_spoilers.size() < spoilerIndex)) {
			_t->_spoilers.resize(spoilerIndex);
			const auto handler = (options.flags & TextParseLinks)
				? std::make_shared<SpoilerClickHandler>()
				: nullptr;
			_t->setSpoiler(spoilerIndex, std::move(handler));
		}
		const auto shiftedIndex = block->lnkIndex();
		auto useCustomIndex = false;
		if (shiftedIndex <= kStringLinkIndexShift) {
			if (IsMono(block->flags()) && shiftedIndex) {
				const auto monoIndex = shiftedIndex;

				if (lastHandlerIndex.mono == monoIndex) {
					block->setLnkIndex(currentIndex);
					continue; // Optimization.
				} else {
					currentIndex++;
				}
				avoidIntersectionsWithCustom();
				block->setLnkIndex(currentIndex);
				const auto handler = Integration::Instance().createLinkHandler(
					_monos[monoIndex - 1],
					_context);
				_t->_links.resize(currentIndex);
				if (handler) {
					_t->setLink(currentIndex, handler);
				}
				lastHandlerIndex.mono = monoIndex;
				continue;
			} else if (shiftedIndex) {
				useCustomIndex = true;
			} else {
				continue;
			}
		}
		const auto usedIndex = [&] {
			return useCustomIndex
				? _linksIndexes[counterCustomIndex - 1]
				: currentIndex;
		};
		const auto realIndex = useCustomIndex
			? shiftedIndex
			: (shiftedIndex - kStringLinkIndexShift);
		if (lastHandlerIndex.lnk == realIndex) {
			block->setLnkIndex(usedIndex());
			continue; // Optimization.
		} else {
			(useCustomIndex ? counterCustomIndex : currentIndex)++;
		}
		if (!useCustomIndex) {
			avoidIntersectionsWithCustom();
		}
		block->setLnkIndex(usedIndex());

		_t->_links.resize(std::max(usedIndex(), uint16(_t->_links.size())));
		const auto handler = Integration::Instance().createLinkHandler(
			_links[realIndex - 1],
			_context);
		if (handler) {
			_t->setLink(usedIndex(), handler);
		}
		lastHandlerIndex.lnk = realIndex;
	}
	_t->_links.squeeze();
	_t->_spoilers.squeeze();
	_t->_blocks.squeeze();
	_t->_text.squeeze();
}

void Parser::computeLinkText(
		const QString &linkData,
		QString *outLinkText,
		EntityLinkShown *outShown) {
	auto url = QUrl(linkData);
	auto good = QUrl(url.isValid()
		? url.toEncoded()
		: QByteArray());
	auto readable = good.isValid()
		? good.toDisplayString()
		: linkData;
	*outLinkText = _t->_st->font->elided(readable, st::linkCropLimit);
	*outShown = (*outLinkText == readable)
		? EntityLinkShown::Full
		: EntityLinkShown::Partial;
}

namespace {

// COPIED FROM qtextengine.cpp AND MODIFIED

struct BidiStatus {
	BidiStatus() {
		eor = QChar::DirON;
		lastStrong = QChar::DirON;
		last = QChar:: DirON;
		dir = QChar::DirON;
	}
	QChar::Direction eor;
	QChar::Direction lastStrong;
	QChar::Direction last;
	QChar::Direction dir;
};

enum { _MaxBidiLevel = 61 };
enum { _MaxItemLength = 4096 };

struct BidiControl {
	inline BidiControl(bool rtl)
		: base(rtl ? 1 : 0), level(rtl ? 1 : 0) {}

	inline void embed(bool rtl, bool o = false) {
		unsigned int toAdd = 1;
		if((level%2 != 0) == rtl ) {
			++toAdd;
		}
		if (level + toAdd <= _MaxBidiLevel) {
			ctx[cCtx].level = level;
			ctx[cCtx].override = override;
			cCtx++;
			override = o;
			level += toAdd;
		}
	}
	inline bool canPop() const { return cCtx != 0; }
	inline void pdf() {
		Q_ASSERT(cCtx);
		--cCtx;
		level = ctx[cCtx].level;
		override = ctx[cCtx].override;
	}

	inline QChar::Direction basicDirection() const {
		return (base ? QChar::DirR : QChar:: DirL);
	}
	inline unsigned int baseLevel() const {
		return base;
	}
	inline QChar::Direction direction() const {
		return ((level%2) ? QChar::DirR : QChar:: DirL);
	}

	struct {
		unsigned int level = 0;
		bool override = false;
	} ctx[_MaxBidiLevel];
	unsigned int cCtx = 0;
	const unsigned int base;
	unsigned int level;
	bool override = false;
};

static void eAppendItems(QScriptAnalysis *analysis, int &start, int &stop, const BidiControl &control, QChar::Direction dir) {
	if (start > stop)
		return;

	int level = control.level;

	if(dir != QChar::DirON && !control.override) {
		// add level of run (cases I1 & I2)
		if(level % 2) {
			if(dir == QChar::DirL || dir == QChar::DirAN || dir == QChar::DirEN)
				level++;
		} else {
			if(dir == QChar::DirR)
				level++;
			else if(dir == QChar::DirAN || dir == QChar::DirEN)
				level += 2;
		}
	}

	QScriptAnalysis *s = analysis + start;
	const QScriptAnalysis *e = analysis + stop;
	while (s <= e) {
		s->bidiLevel = level;
		++s;
	}
	++stop;
	start = stop;
}

inline int32 countBlockHeight(const AbstractBlock *b, const style::TextStyle *st) {
	return (b->type() == TextBlockTSkip) ? static_cast<const SkipBlock*>(b)->height() : (st->lineHeight > st->font->height) ? st->lineHeight : st->font->height;
}

} // namespace

class Renderer {
public:
	Renderer(Painter *p, const String *t)
	: _p(p)
	, _t(t)
	, _originalPen(p ? p->pen() : QPen()) {
	}

	~Renderer() {
		restoreAfterElided();
		if (_p) {
			_p->setPen(_originalPen);
		}
	}

	void draw(int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection = { 0, 0 }, bool fullWidthSelection = true) {
		if (_t->isEmpty()) return;

		_blocksSize = _t->_blocks.size();
		if (_p) {
			_p->setFont(_t->_st->font);
			_textPalette = &_p->textPalette();
			_originalPenSelected = (_textPalette->selectFg->c.alphaF() == 0) ? _originalPen : _textPalette->selectFg->p;
		}

		_x = left;
		_y = top;
		_yFrom = yFrom + top;
		_yTo = (yTo < 0) ? -1 : (yTo + top);
		_selection = selection;
		_fullWidthSelection = fullWidthSelection;
		_wLeft = _w = w;
		if (_elideLast) {
			_yToElide = _yTo;
			if (_elideRemoveFromEnd > 0 && !_t->_blocks.empty()) {
				int firstBlockHeight = countBlockHeight(_t->_blocks.front().get(), _t->_st);
				if (_y + firstBlockHeight >= _yToElide) {
					_wLeft -= _elideRemoveFromEnd;
				}
			}
		}
		_str = _t->_text.unicode();

		if (_p) {
			auto clip = _p->hasClipping() ? _p->clipBoundingRect() : QRect();
			if (clip.width() > 0 || clip.height() > 0) {
				if (_yFrom < clip.y()) _yFrom = clip.y();
				if (_yTo < 0 || _yTo > clip.y() + clip.height()) _yTo = clip.y() + clip.height();
			}
		}

		_align = align;

		_parDirection = _t->_startDir;
		if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection();
		if ((*_t->_blocks.cbegin())->type() != TextBlockTNewline) {
			initNextParagraph(_t->_blocks.cbegin());
		}

		_lineStart = 0;
		_lineStartBlock = 0;

		_lineHeight = 0;
		_fontHeight = _t->_st->font->height;
		auto last_rBearing = QFixed(0);
		_last_rPadding = QFixed(0);

		auto blockIndex = 0;
		bool longWordLine = true;
		auto e = _t->_blocks.cend();
		for (auto i = _t->_blocks.cbegin(); i != e; ++i, ++blockIndex) {
			auto b = i->get();
			auto _btype = b->type();
			auto blockHeight = countBlockHeight(b, _t->_st);

			if (_btype == TextBlockTNewline) {
				if (!_lineHeight) _lineHeight = blockHeight;
				if (!drawLine((*i)->from(), i, e)) {
					return;
				}

				_y += _lineHeight;
				_lineHeight = 0;
				_lineStart = _t->countBlockEnd(i, e);
				_lineStartBlock = blockIndex + 1;

				last_rBearing = b->f_rbearing();
				_last_rPadding = b->f_rpadding();
				_wLeft = _w - (b->f_width() - last_rBearing);
				if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) {
					_wLeft -= _elideRemoveFromEnd;
				}

				_parDirection = static_cast<const NewlineBlock*>(b)->nextDirection();
				if (_parDirection == Qt::LayoutDirectionAuto) _parDirection = style::LayoutDirection();
				initNextParagraph(i + 1);

				longWordLine = true;
				continue;
			}

			auto b__f_rbearing = b->f_rbearing();
			auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + b->f_width() - b__f_rbearing);
			if (newWidthLeft >= 0) {
				last_rBearing = b__f_rbearing;
				_last_rPadding = b->f_rpadding();
				_wLeft = newWidthLeft;

				_lineHeight = qMax(_lineHeight, blockHeight);

				longWordLine = false;
				continue;
			}

			if (_btype == TextBlockTText) {
				auto t = static_cast<const TextBlock*>(b);
				if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line
					_last_rPadding += b->f_rpadding();

					_lineHeight = qMax(_lineHeight, blockHeight);

					longWordLine = false;
					continue;
				}

				auto f_wLeft = _wLeft; // vars for saving state of the last word start
				auto f_lineHeight = _lineHeight; // f points to the last word-start element of t->_words
				for (auto j = t->_words.cbegin(), en = t->_words.cend(), f = j; j != en; ++j) {
					auto wordEndsHere = (j->f_width() >= 0);
					auto j_width = wordEndsHere ? j->f_width() : -j->f_width();

					auto newWidthLeft = _wLeft - last_rBearing - (_last_rPadding + j_width - j->f_rbearing());
					if (newWidthLeft >= 0) {
						last_rBearing = j->f_rbearing();
						_last_rPadding = j->f_rpadding();
						_wLeft = newWidthLeft;

						_lineHeight = qMax(_lineHeight, blockHeight);

						if (wordEndsHere) {
							longWordLine = false;
						}
						if (wordEndsHere || longWordLine) {
							f = j + 1;
							f_wLeft = _wLeft;
							f_lineHeight = _lineHeight;
						}
						continue;
					}

					auto elidedLineHeight = qMax(_lineHeight, blockHeight);
					auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide);
					if (elidedLine) {
						_lineHeight = elidedLineHeight;
					} else if (f != j && !_breakEverywhere) {
						// word did not fit completely, so we roll back the state to the beginning of this long word
						j = f;
						_wLeft = f_wLeft;
						_lineHeight = f_lineHeight;
						j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width();
					}
					if (!drawLine(elidedLine ? ((j + 1 == en) ? _t->countBlockEnd(i, e) : (j + 1)->from()) : j->from(), i, e)) {
						return;
					}
					_y += _lineHeight;
					_lineHeight = qMax(0, blockHeight);
					_lineStart = j->from();
					_lineStartBlock = blockIndex;

					last_rBearing = j->f_rbearing();
					_last_rPadding = j->f_rpadding();
					_wLeft = _w - (j_width - last_rBearing);
					if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) {
						_wLeft -= _elideRemoveFromEnd;
					}

					longWordLine = !wordEndsHere;
					f = j + 1;
					f_wLeft = _wLeft;
					f_lineHeight = _lineHeight;
				}
				continue;
			}

			auto elidedLineHeight = qMax(_lineHeight, blockHeight);
			auto elidedLine = _elideLast && (_y + elidedLineHeight >= _yToElide);
			if (elidedLine) {
				_lineHeight = elidedLineHeight;
			}
			if (!drawLine(elidedLine ? _t->countBlockEnd(i, e) : b->from(), i, e)) {
				return;
			}
			_y += _lineHeight;
			_lineHeight = qMax(0, blockHeight);
			_lineStart = b->from();
			_lineStartBlock = blockIndex;

			last_rBearing = b__f_rbearing;
			_last_rPadding = b->f_rpadding();
			_wLeft = _w - (b->f_width() - last_rBearing);
			if (_elideLast && _elideRemoveFromEnd > 0 && (_y + blockHeight >= _yToElide)) {
				_wLeft -= _elideRemoveFromEnd;
			}

			longWordLine = true;
			continue;
		}
		if (_lineStart < _t->_text.size()) {
			if (!drawLine(_t->_text.size(), e, e)) return;
		}
		if (!_p && _lookupSymbol) {
			_lookupResult.symbol = _t->_text.size();
			_lookupResult.afterSymbol = false;
		}
	}

	void drawElided(int32 left, int32 top, int32 w, style::align align, int32 lines, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) {
		if (lines <= 0 || _t->isNull()) return;

		if (yTo < 0 || (lines - 1) * _t->_st->font->height < yTo) {
			yTo = lines * _t->_st->font->height;
			_elideLast = true;
			_elideRemoveFromEnd = removeFromEnd;
		}
		_breakEverywhere = breakEverywhere;
		draw(left, top, w, align, yFrom, yTo, selection);
	}

	StateResult getState(QPoint point, int w, StateRequest request) {
		if (!_t->isNull() && point.y() >= 0) {
			_lookupRequest = request;
			_lookupX = point.x();
			_lookupY = point.y();

			_breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere);
			_lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol);
			_lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink);
			if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) {
				draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1);
			}
		}
		return _lookupResult;
	}

	StateResult getStateElided(QPoint point, int w, StateRequestElided request) {
		if (!_t->isNull() && point.y() >= 0 && request.lines > 0) {
			_lookupRequest = request;
			_lookupX = point.x();
			_lookupY = point.y();

			_breakEverywhere = (_lookupRequest.flags & StateRequest::Flag::BreakEverywhere);
			_lookupSymbol = (_lookupRequest.flags & StateRequest::Flag::LookupSymbol);
			_lookupLink = (_lookupRequest.flags & StateRequest::Flag::LookupLink);
			if (_lookupSymbol || (_lookupX >= 0 && _lookupX < w)) {
				int yTo = _lookupY + 1;
				if (yTo < 0 || (request.lines - 1) * _t->_st->font->height < yTo) {
					yTo = request.lines * _t->_st->font->height;
					_elideLast = true;
					_elideRemoveFromEnd = request.removeFromEnd;
				}
				draw(0, 0, w, _lookupRequest.align, _lookupY, _lookupY + 1);
			}
		}
		return _lookupResult;
	}

private:
	void initNextParagraph(String::TextBlocks::const_iterator i) {
		_parStartBlock = i;
		const auto e = _t->_blocks.cend();
		if (i == e) {
			_parStart = _t->_text.size();
			_parLength = 0;
		} else {
			_parStart = (*i)->from();
			for (; i != e; ++i) {
				if ((*i)->type() == TextBlockTNewline) {
					break;
				}
			}
			_parLength = ((i == e) ? _t->_text.size() : (*i)->from()) - _parStart;
		}
		_parAnalysis.resize(0);
	}

	void initParagraphBidi() {
		if (!_parLength || !_parAnalysis.isEmpty()) return;

		String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1;

		bool ignore = false;
		bool rtl = (_parDirection == Qt::RightToLeft);
		if (!ignore && !rtl) {
			ignore = true;
			const ushort *start = reinterpret_cast<const ushort*>(_str) + _parStart;
			const ushort *curr = start;
			const ushort *end = start + _parLength;
			while (curr < end) {
				while (n != e && (*n)->from() <= _parStart + (curr - start)) {
					i = n;
					++n;
				}
				if ((*i)->type() != TextBlockTEmoji && *curr >= 0x590) {
					ignore = false;
					break;
				}
				++curr;
			}
		}

		_parAnalysis.resize(_parLength);
		QScriptAnalysis *analysis = _parAnalysis.data();

		BidiControl control(rtl);

		_parHasBidi = false;
		if (ignore) {
			memset(analysis, 0, _parLength * sizeof(QScriptAnalysis));
			if (rtl) {
				for (int i = 0; i < _parLength; ++i)
					analysis[i].bidiLevel = 1;
				_parHasBidi = true;
			}
		} else {
			_parHasBidi = eBidiItemize(analysis, control);
		}
	}

	bool drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_end) {
		_yDelta = (_lineHeight - _fontHeight) / 2;
		if (_yTo >= 0 && (_y + _yDelta >= _yTo || _y >= _yTo)) return false;
		if (_y + _yDelta + _fontHeight <= _yFrom) {
			if (_lookupSymbol) {
				_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
				_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
			}
			return true;
		}

		// Trimming pending spaces, because they sometimes don't fit on the line.
		// They also are not counted in the line width, they're in the right padding.
		// Line width is a sum of block / word widths and paddings between them, without trailing one.
		auto trimmedLineEnd = _lineEnd;
		for (; trimmedLineEnd > _lineStart; --trimmedLineEnd) {
			auto ch = _t->_text[trimmedLineEnd - 1];
			if (ch != QChar::Space && ch != QChar::LineFeed) {
				break;
			}
		}

		auto _endBlock = (_endBlockIter == _end) ? nullptr : _endBlockIter->get();
		auto elidedLine = _elideLast && (_y + _lineHeight >= _yToElide);
		if (elidedLine) {
			// If we decided to draw the last line elided only because of the skip block
			// that did not fit on this line, we just draw the line till the very end.
			// Skip block is ignored in the elided lines, instead "removeFromEnd" is used.
			if (_endBlock && _endBlock->type() == TextBlockTSkip) {
				_endBlock = nullptr;
			}
			if (!_endBlock) {
				elidedLine = false;
			}
		}

		auto blockIndex = _lineStartBlock;
		auto currentBlock = _t->_blocks[blockIndex].get();
		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;

		const auto extendLeft = (currentBlock->from() < _lineStart)
			? qMin(_lineStart - currentBlock->from(), 2)
			: 0;
		_localFrom = _lineStart - extendLeft;
		const auto extendedLineEnd = (_endBlock && _endBlock->from() < trimmedLineEnd && !elidedLine)
			? qMin(uint16(trimmedLineEnd + 2), _t->countBlockEnd(_endBlockIter, _end))
			: trimmedLineEnd;

		auto lineText = _t->_text.mid(_localFrom, extendedLineEnd - _localFrom);
		auto lineStart = extendLeft;
		auto lineLength = trimmedLineEnd - _lineStart;

		if (elidedLine) {
			initParagraphBidi();
			prepareElidedLine(lineText, lineStart, lineLength, _endBlock);
		}

		auto x = _x;
		if (_align & Qt::AlignHCenter) {
			x += (_wLeft / 2).toInt();
		} else if (((_align & Qt::AlignLeft) && _parDirection == Qt::RightToLeft) || ((_align & Qt::AlignRight) && _parDirection == Qt::LeftToRight)) {
			x += _wLeft;
		}

		if (!_p) {
			if (_lookupX < x) {
				if (_lookupSymbol) {
					if (_parDirection == Qt::RightToLeft) {
						_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
						_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
//						_lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false;
					} else {
						_lookupResult.symbol = _lineStart;
						_lookupResult.afterSymbol = false;
//						_lookupResult.uponSymbol = ((_lookupX >= _x) && (_lineStart > 0)) ? true : false;
					}
				}
				if (_lookupLink) {
					_lookupResult.link = nullptr;
				}
				_lookupResult.uponSymbol = false;
				return false;
			} else if (_lookupX >= x + (_w - _wLeft)) {
				if (_parDirection == Qt::RightToLeft) {
					_lookupResult.symbol = _lineStart;
					_lookupResult.afterSymbol = false;
//					_lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineStart > 0)) ? true : false;
				} else {
					_lookupResult.symbol = (_lineEnd > _lineStart) ? (_lineEnd - 1) : _lineStart;
					_lookupResult.afterSymbol = (_lineEnd > _lineStart) ? true : false;
//					_lookupResult.uponSymbol = ((_lookupX < _x + _w) && (_lineEnd < _t->_text.size()) && (!_endBlock || _endBlock->type() != TextBlockTSkip)) ? true : false;
				}
				if (_lookupLink) {
					_lookupResult.link = nullptr;
				}
				_lookupResult.uponSymbol = false;
				return false;
			}
		}

		if (_fullWidthSelection) {
			const auto selectFromStart = (_selection.to > _lineStart)
				&& (_lineStart > 0)
				&& (_selection.from <= _lineStart);
			const auto selectTillEnd = (_selection.to > trimmedLineEnd)
				&& (trimmedLineEnd < _t->_text.size())
				&& (_selection.from <= trimmedLineEnd)
				&& (!_endBlock || _endBlock->type() != TextBlockTSkip);

			if ((selectFromStart && _parDirection == Qt::LeftToRight)
				|| (selectTillEnd && _parDirection == Qt::RightToLeft)) {
				if (x > _x) {
					fillSelectRange(_x, x);
				}
			}
			if ((selectTillEnd && _parDirection == Qt::LeftToRight)
				|| (selectFromStart && _parDirection == Qt::RightToLeft)) {
				if (x < _x + _wLeft) {
					fillSelectRange(x + _w - _wLeft, _x + _w);
				}
			}
		}
		if (trimmedLineEnd == _lineStart && !elidedLine) {
			return true;
		}

		if (!elidedLine) initParagraphBidi(); // if was not inited

		_f = _t->_st->font;
		QStackTextEngine engine(lineText, _f->f);
		engine.option.setTextDirection(_parDirection);
		_e = &engine;

		eItemize();

		QScriptLine line;
		line.from = lineStart;
		line.length = lineLength;
		eShapeLine(line);

		int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1);
		int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0;
		if (!nItems) {
			return true;
		}

		int skipIndex = -1;
		QVarLengthArray<int> visualOrder(nItems);
		QVarLengthArray<uchar> levels(nItems);
		for (int i = 0; i < nItems; ++i) {
			auto &si = engine.layoutData->items[firstItem + i];
			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
				currentBlock = nextBlock;
				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
			}
			auto _type = currentBlock->type();
			if (_type == TextBlockTSkip) {
				levels[i] = si.analysis.bidiLevel = 0;
				skipIndex = i;
			} else {
				levels[i] = si.analysis.bidiLevel;
			}
			if (si.analysis.flags == QScriptAnalysis::Object) {
				if (_type == TextBlockTEmoji || _type == TextBlockTSkip) {
					si.width = currentBlock->f_width() + (nextBlock == _endBlock && (!nextBlock || nextBlock->from() >= trimmedLineEnd) ? 0 : currentBlock->f_rpadding());
				}
			}
		}
		QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data());
		if (style::RightToLeft() && skipIndex == nItems - 1) {
			for (int32 i = nItems; i > 1;) {
				--i;
				visualOrder[i] = visualOrder[i - 1];
			}
			visualOrder[0] = skipIndex;
		}

		blockIndex = _lineStartBlock;
		currentBlock = _t->_blocks[blockIndex].get();
		nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;

		int32 textY = _y + _yDelta + _t->_st->font->ascent, emojiY = (_t->_st->font->height - st::emojiSize) / 2;

		applyBlockProperties(currentBlock);
		for (int i = 0; i < nItems; ++i) {
			int item = firstItem + visualOrder[i];
			const QScriptItem &si = engine.layoutData->items.at(item);
			bool rtl = (si.analysis.bidiLevel % 2);

			while (blockIndex > _lineStartBlock + 1 && _t->_blocks[blockIndex - 1]->from() > _localFrom + si.position) {
				nextBlock = currentBlock;
				currentBlock = _t->_blocks[--blockIndex - 1].get();
				applyBlockProperties(currentBlock);
			}
			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
				currentBlock = nextBlock;
				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
				applyBlockProperties(currentBlock);
			}
			if (si.analysis.flags >= QScriptAnalysis::TabOrObject) {
				TextBlockType _type = currentBlock->type();
				if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest
					if (_lookupLink) {
						if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
							const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex());
							const auto resultLink = (spoilerLink || !currentBlock->lnkIndex())
								? spoilerLink
								: _t->_links.at(currentBlock->lnkIndex() - 1);
							if (resultLink) {
								_lookupResult.link = resultLink;
							}
						}
					}
					if (_type != TextBlockTSkip) {
						_lookupResult.uponSymbol = true;
					}
					if (_lookupSymbol) {
						if (_type == TextBlockTSkip) {
							if (_parDirection == Qt::RightToLeft) {
								_lookupResult.symbol = _lineStart;
								_lookupResult.afterSymbol = false;
							} else {
								_lookupResult.symbol = (trimmedLineEnd > _lineStart) ? (trimmedLineEnd - 1) : _lineStart;
								_lookupResult.afterSymbol = (trimmedLineEnd > _lineStart) ? true : false;
							}
							return false;
						}

						// Emoji with spaces after symbol lookup
						auto chFrom = _str + currentBlock->from();
						auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from());
						auto spacesWidth = (si.width - currentBlock->f_width());
						auto spacesCount = 0;
						while (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space) {
							++spacesCount;
							--chTo;
						}
						if (spacesCount > 0) { // Check if we're over a space.
							if (rtl) {
								if (_lookupX < x + spacesWidth) {
									_lookupResult.symbol = (chTo - _str); // up to a space, included, rtl
									_lookupResult.afterSymbol = (_lookupX < x + (spacesWidth / 2)) ? true : false;
									return false;
								}
							} else if (_lookupX >= x + si.width - spacesWidth) {
								_lookupResult.symbol = (chTo - _str); // up to a space, inclided, ltr
								_lookupResult.afterSymbol = (_lookupX >= x + si.width - spacesWidth + (spacesWidth / 2)) ? true : false;
								return false;
							}
						}
						if (_lookupX < x + (rtl ? (si.width - currentBlock->f_width()) : 0) + (currentBlock->f_width() / 2)) {
							_lookupResult.symbol = ((rtl && chTo > chFrom) ? (chTo - 1) : chFrom) - _str;
							_lookupResult.afterSymbol = (rtl && chTo > chFrom) ? true : false;
						} else {
							_lookupResult.symbol = ((rtl || chTo <= chFrom) ? chFrom : (chTo - 1)) - _str;
							_lookupResult.afterSymbol = (rtl || chTo <= chFrom) ? false : true;
						}
					}
					return false;
				} else if (_p && _type == TextBlockTEmoji) {
					auto glyphX = x;
					auto spacesWidth = (si.width - currentBlock->f_width());
					if (rtl) {
						glyphX += spacesWidth;
					}
					struct {
						QFixed from;
						QFixed to;
					} fillSelect;
					struct {
						QFixed from;
						QFixed width;
					} fillSpoiler;
					if (_background.selectActiveBlock) {
						fillSelect = { x, x + si.width };
					} else if (_localFrom + si.position < _selection.to) {
						auto chFrom = _str + currentBlock->from();
						auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from());
						if (_localFrom + si.position >= _selection.from) { // could be without space
							if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) {
								fillSelect = { x, x + si.width };
							} else { // or with space
								fillSelect = { glyphX, glyphX + currentBlock->f_width() };
							}
						} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) {
							if (rtl) { // rtl space only
								fillSelect = { x, glyphX };
							} else { // ltr space only
								fillSelect = { x + currentBlock->f_width(), x + si.width };
							}
						}
					}
					const auto hasSpoiler = _background.color &&
						(_background.inFront || _background.startMs);
					if (hasSpoiler) {
						fillSpoiler = { x, si.width };
					}
					const auto spoilerOpacity = hasSpoiler
						? fillSpoilerOpacity()
						: 0.;
					const auto hasSelect = fillSelect.to != QFixed();
					if (hasSelect) {
						fillSelectRange(fillSelect.from, fillSelect.to);
					}
					const auto opacity = _p->opacity();
					if (spoilerOpacity < 1.) {
						if (hasSpoiler) {
							_p->setOpacity(opacity * (1. - spoilerOpacity));
						}
						Emoji::Draw(
							*_p,
							static_cast<const EmojiBlock*>(currentBlock)->_emoji,
							Emoji::GetSizeNormal(),
							(glyphX + st::emojiPadding).toInt(),
							_y + _yDelta + emojiY);
					}
					if (hasSpoiler) {
						_p->setOpacity(opacity * spoilerOpacity);
						fillSpoilerRange(
							fillSpoiler.from,
							fillSpoiler.width,
							blockIndex,
							currentBlock->from(),
							(nextBlock ? nextBlock->from() : _t->_text.size()));
						_p->setOpacity(opacity);
					}
//				} else if (_p && currentBlock->type() == TextBlockSkip) { // debug
//					_p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast<SkipBlock*>(currentBlock)->height()), QColor(0, 0, 0, 32));
				}
				x += si.width;
				continue;
			}

			unsigned short *logClusters = engine.logClusters(&si);
			QGlyphLayout glyphs = engine.shapedGlyphs(&si);

			int itemStart = qMax(line.from, si.position), itemEnd;
			int itemLength = engine.length(item);
			int glyphsStart = logClusters[itemStart - si.position], glyphsEnd;
			if (line.from + line.length < si.position + itemLength) {
				itemEnd = line.from + line.length;
				glyphsEnd = logClusters[itemEnd - si.position];
			} else {
				itemEnd = si.position + itemLength;
				glyphsEnd = si.num_glyphs;
			}

			QFixed itemWidth = 0;
			for (int g = glyphsStart; g < glyphsEnd; ++g)
				itemWidth += glyphs.effectiveAdvance(g);

			if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest
				if (_lookupLink) {
					if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
						const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex());
						const auto resultLink = (spoilerLink || !currentBlock->lnkIndex())
							? spoilerLink
							: _t->_links.at(currentBlock->lnkIndex() - 1);
						if (resultLink) {
							_lookupResult.link = resultLink;
						}
					}
				}
				_lookupResult.uponSymbol = true;
				if (_lookupSymbol) {
					QFixed tmpx = rtl ? (x + itemWidth) : x;
					for (int ch = 0, g, itemL = itemEnd - itemStart; ch < itemL;) {
						g = logClusters[itemStart - si.position + ch];
						QFixed gwidth = glyphs.effectiveAdvance(g);
						// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes
						int ch2 = ch + 1;
						while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) {
							++ch2;
						}
						for (int charsCount = (ch2 - ch); ch < ch2; ++ch) {
							QFixed shift1 = QFixed(2 * (charsCount - (ch2 - ch)) + 2) * gwidth / QFixed(2 * charsCount),
								shift2 = QFixed(2 * (charsCount - (ch2 - ch)) + 1) * gwidth / QFixed(2 * charsCount);
							if ((rtl && _lookupX >= tmpx - shift1) ||
								(!rtl && _lookupX < tmpx + shift1)) {
								_lookupResult.symbol = _localFrom + itemStart + ch;
								if ((rtl && _lookupX >= tmpx - shift2) ||
									(!rtl && _lookupX < tmpx + shift2)) {
									_lookupResult.afterSymbol = false;
								} else {
									_lookupResult.afterSymbol = true;
								}
								return false;
							}
						}
						if (rtl) {
							tmpx -= gwidth;
						} else {
							tmpx += gwidth;
						}
					}
					if (itemEnd > itemStart) {
						_lookupResult.symbol = _localFrom + itemEnd - 1;
						_lookupResult.afterSymbol = true;
					} else {
						_lookupResult.symbol = _localFrom + itemStart;
						_lookupResult.afterSymbol = false;
					}
				}
				return false;
			} else if (_p) {
				QTextCharFormat format;
				QTextItemInt gf(glyphs.mid(glyphsStart, glyphsEnd - glyphsStart),
								&_e->fnt, engine.layoutData->string.unicode() + itemStart,
								itemEnd - itemStart, engine.fontEngine(si), format);
				gf.logClusters = logClusters + itemStart - si.position;
				gf.width = itemWidth;
				gf.justified = false;
				gf.initWithScriptItem(si);

				auto hasSelected = false;
				auto hasNotSelected = true;
				auto selectedRect = QRect();
				if (_background.selectActiveBlock) {
					fillSelectRange(x, x + itemWidth);
				} else if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) {
					hasSelected = true;
					auto selX = x;
					auto selWidth = itemWidth;
					if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) {
						hasNotSelected = false;
					} else {
						selWidth = 0;
						int itemL = itemEnd - itemStart;
						int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart);
						if (selStart < 0) selStart = 0;
						if (selEnd > itemL) selEnd = itemL;
						for (int ch = 0, g; ch < selEnd;) {
							g = logClusters[itemStart - si.position + ch];
							QFixed gwidth = glyphs.effectiveAdvance(g);
							// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes
							int ch2 = ch + 1;
							while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) {
								++ch2;
							}
							if (ch2 <= selStart) {
								selX += gwidth;
							} else if (ch >= selStart && ch2 <= selEnd) {
								selWidth += gwidth;
							} else {
								int sStart = ch, sEnd = ch2;
								if (ch < selStart) {
									sStart = selStart;
									selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch);
								}
								if (ch2 >= selEnd) {
									sEnd = selEnd;
									selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
									break;
								}
								selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
							}
							ch = ch2;
						}
					}
					if (rtl) selX = x + itemWidth - (selX - x) - selWidth;
					selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight);
					fillSelectRange(selX, selX + selWidth);
				}
				const auto hasSpoiler = (_background.inFront || _background.startMs);
				const auto spoilerOpacity = hasSpoiler
					? fillSpoilerOpacity()
					: 0.;
				const auto opacity = _p->opacity();
				const auto isElidedBlock = (!rtl)
					&& (_indexOfElidedBlock == blockIndex);
				if ((spoilerOpacity < 1.) || isElidedBlock) {
					if (hasSpoiler && !isElidedBlock) {
						_p->setOpacity(opacity * (1. - spoilerOpacity));
					}
					if (Q_UNLIKELY(hasSelected)) {
						if (Q_UNLIKELY(hasNotSelected)) {
							// There is a bug in retina QPainter clipping stack.
							// You can see glitches in rendering in such text:
							// aA
							// Aa
							// Where selection is both 'A'-s.
							// I can't debug it right now, this is a workaround.
#ifdef Q_OS_MAC
							_p->save();
#endif // Q_OS_MAC
							const auto clippingEnabled = _p->hasClipping();
							const auto clippingRegion = _p->clipRegion();
							_p->setClipRect(selectedRect, Qt::IntersectClip);
							_p->setPen(*_currentPenSelected);
							_p->drawTextItem(QPointF(x.toReal(), textY), gf);
							const auto externalClipping = clippingEnabled
								? clippingRegion
								: QRegion(QRect(
									(_x - _w).toInt(),
									_y - _lineHeight,
									(_x + 2 * _w).toInt(),
									_y + 2 * _lineHeight));
							_p->setClipRegion(externalClipping - selectedRect);
							_p->setPen(*_currentPen);
							_p->drawTextItem(QPointF(x.toReal(), textY), gf);
#ifdef Q_OS_MAC
							_p->restore();
#else // Q_OS_MAC
							if (clippingEnabled) {
								_p->setClipRegion(clippingRegion);
							} else {
								_p->setClipping(false);
							}
#endif // Q_OS_MAC
						} else {
							_p->setPen(*_currentPenSelected);
							_p->drawTextItem(QPointF(x.toReal(), textY), gf);
						}
					} else {
						_p->setPen(*_currentPen);
						_p->drawTextItem(QPointF(x.toReal(), textY), gf);
					}
				}

				if (hasSpoiler) {
					_p->setOpacity(opacity * spoilerOpacity);
					fillSpoilerRange(
						x,
						itemWidth,
						blockIndex,
						_localFrom + itemStart,
						_localFrom + itemEnd);
					_p->setOpacity(opacity);
				}
			}

			x += itemWidth;
		}
		return true;
	}
	void fillSelectRange(QFixed from, QFixed to) {
		auto left = from.toInt();
		auto width = to.toInt() - left;
		_p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg);
	}

	float fillSpoilerOpacity() {
		if (!_background.startMs) {
			return 1.;
		}
		const auto progress = float64(crl::now() - _background.startMs)
			/ st::fadeWrapDuration;
		if ((progress > 1.) && _background.spoilerIndex) {
			const auto link = _t->_spoilers.at(_background.spoilerIndex - 1);
			if (link) {
				link->setStartMs(0);
			}
		}
		return (1. - std::min(progress, 1.));
	}

	void fillSpoilerRange(
			QFixed x,
			QFixed width,
			int currentBlockIndex,
			int positionFrom,
			int positionTill) {
		if (!_background.color) {
			return;
		}
		const auto elideOffset = (_indexOfElidedBlock == currentBlockIndex)
			? (_elideRemoveFromEnd + _f->elidew)
			: 0;

		const auto parts = [&] {
			const auto blockIndex = currentBlockIndex - 1;
			const auto block = _t->_blocks[blockIndex].get();
			const auto nextBlock = (blockIndex + 1 < _t->_blocks.size())
				? _t->_blocks[blockIndex + 1].get()
				: nullptr;
			const auto blockEnd = nextBlock ? nextBlock->from() : _t->_text.size();
			const auto now = block->spoilerIndex();
			const auto was = (positionFrom > block->from())
				? now
				: (blockIndex > 0)
				? _t->_blocks[blockIndex - 1]->spoilerIndex()
				: 0;
			const auto will = elideOffset
				? 0
				: (positionTill < blockEnd)
				? now
				: nextBlock
				? nextBlock->spoilerIndex()
				: 0;
			return RectPart::None
				| ((now != was) ? (RectPart::FullLeft) : RectPart::None)
				| ((now != will) ? (RectPart::FullRight) : RectPart::None);
		}();
		const auto hasLeft = (parts & RectPart::Left) != 0;
		const auto hasRight = (parts & RectPart::Right) != 0;

		const auto &cache = _background.inFront
			? _t->_spoilerCache
			: _t->_spoilerShownCache;
		const auto cornerWidth = cache.corners[0].width()
			/ style::DevicePixelRatio();
		const auto useWidth = ((x + width).toInt() - x.toInt()) - elideOffset;
		const auto rect = QRect(
			x.toInt(),
			_y + _yDelta,
			std::max(
				useWidth,
				(hasRight ? cornerWidth : 0) + (hasLeft ? cornerWidth : 0)),
			_fontHeight);
		if (!rect.isValid()) {
			return;
		}

		if (parts != RectPart::None) {
			DrawRoundedRect(
				*_p,
				rect,
				*_background.color,
				cache.corners,
				parts);
		}
		_p->fillRect(
			rect.left() + (hasLeft ? cornerWidth : 0),
			rect.top(),
			rect.width()
				- (hasRight ? cornerWidth : 0)
				- (hasLeft ? cornerWidth : 0),
			rect.height(),
			*_background.color);
	}

	void elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) {
		if (_elideSavedBlock) {
			restoreAfterElided();
		}

		_elideSavedIndex = blockIndex;
		auto mutableText = const_cast<String*>(_t);
		_elideSavedBlock = std::move(mutableText->_blocks[blockIndex]);
		mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex(), (*_elideSavedBlock)->spoilerIndex());
		_blocksSize = blockIndex + 1;
		_endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr);
	}

	void setElideBidi(int32 elideStart, int32 elideLen) {
		int32 newParLength = elideStart + elideLen - _parStart;
		if (newParLength > _parAnalysis.size()) {
			_parAnalysis.resize(newParLength);
		}
		for (int32 i = elideLen; i > 0; --i) {
			_parAnalysis[newParLength - i].bidiLevel = (_parDirection == Qt::RightToLeft) ? 1 : 0;
		}
	}

	void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat = 0) {
		_f = _t->_st->font;
		QStackTextEngine engine(lineText, _f->f);
		engine.option.setTextDirection(_parDirection);
		_e = &engine;

		eItemize();

		auto blockIndex = _lineStartBlock;
		auto currentBlock = _t->_blocks[blockIndex].get();
		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;

		QScriptLine line;
		line.from = lineStart;
		line.length = lineLength;
		eShapeLine(line);

		auto elideWidth = _f->elidew;
		_wLeft = _w - elideWidth - _elideRemoveFromEnd;

		int firstItem = engine.findItem(line.from), lastItem = engine.findItem(line.from + line.length - 1);
		int nItems = (firstItem >= 0 && lastItem >= firstItem) ? (lastItem - firstItem + 1) : 0, i;

		for (i = 0; i < nItems; ++i) {
			QScriptItem &si(engine.layoutData->items[firstItem + i]);
			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
				currentBlock = nextBlock;
				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
			}
			TextBlockType _type = currentBlock->type();
			if (si.analysis.flags == QScriptAnalysis::Object) {
				if (_type == TextBlockTEmoji || _type == TextBlockTSkip) {
					si.width = currentBlock->f_width() + currentBlock->f_rpadding();
				}
			}
			if (_type == TextBlockTEmoji || _type == TextBlockTSkip || _type == TextBlockTNewline) {
				if (_wLeft < si.width) {
					lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis;
					lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart;
					_selection.to = qMin(_selection.to, currentBlock->from());
					_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
					setElideBidi(currentBlock->from(), kQEllipsis.size());
					elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth);
					return;
				}
				_wLeft -= si.width;
			} else if (_type == TextBlockTText) {
				unsigned short *logClusters = engine.logClusters(&si);
				QGlyphLayout glyphs = engine.shapedGlyphs(&si);

				int itemStart = qMax(line.from, si.position), itemEnd;
				int itemLength = engine.length(firstItem + i);
				int glyphsStart = logClusters[itemStart - si.position], glyphsEnd;
				if (line.from + line.length < si.position + itemLength) {
					itemEnd = line.from + line.length;
					glyphsEnd = logClusters[itemEnd - si.position];
				} else {
					itemEnd = si.position + itemLength;
					glyphsEnd = si.num_glyphs;
				}

				for (auto g = glyphsStart; g < glyphsEnd; ++g) {
					auto adv = glyphs.effectiveAdvance(g);
					if (_wLeft < adv) {
						auto pos = itemStart;
						while (pos < itemEnd && logClusters[pos - si.position] < g) {
							++pos;
						}

						if (lineText.size() <= pos || repeat > 3) {
							lineText += kQEllipsis;
							lineLength = _localFrom + pos + kQEllipsis.size() - _lineStart;
							_selection.to = qMin(_selection.to, uint16(_localFrom + pos));
							_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
							setElideBidi(_localFrom + pos, kQEllipsis.size());
							_blocksSize = blockIndex;
							_endBlock = nextBlock;
						} else {
							lineText = lineText.mid(0, pos);
							lineLength = _localFrom + pos - _lineStart;
							_blocksSize = blockIndex;
							_endBlock = nextBlock;
							prepareElidedLine(lineText, lineStart, lineLength, _endBlock, repeat + 1);
						}
						return;
					} else {
						_wLeft -= adv;
					}
				}
			}
		}

		int32 elideStart = _localFrom + lineText.size();
		_selection.to = qMin(_selection.to, uint16(elideStart));
		_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
		setElideBidi(elideStart, kQEllipsis.size());

		lineText += kQEllipsis;
		lineLength += kQEllipsis.size();

		if (!repeat) {
			for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) {
			}
			if (blockIndex < _blocksSize) {
				elideSaveBlock(blockIndex, _endBlock, elideStart, elideWidth);
			}
		}
	}

	void restoreAfterElided() {
		if (_elideSavedBlock) {
			const_cast<String*>(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock);
		}
	}

	// COPIED FROM qtextengine.cpp AND MODIFIED
	void eShapeLine(const QScriptLine &line) {
		int item = _e->findItem(line.from);
		if (item == -1)
			return;

		auto end = _e->findItem(line.from + line.length - 1, item);
		auto blockIndex = _lineStartBlock;
		auto currentBlock = _t->_blocks[blockIndex].get();
		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
		eSetFont(currentBlock);
		for (; item <= end; ++item) {
			QScriptItem &si = _e->layoutData->items[item];
			while (nextBlock && nextBlock->from() <= _localFrom + si.position) {
				currentBlock = nextBlock;
				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
				eSetFont(currentBlock);
			}
			_e->shape(item);
		}
	}

	style::font applyFlags(int32 flags, const style::font &f) {
		if (!flags) {
			return f;
		}
		auto result = f;
		if (IsMono(flags)) {
			result = result->monospace();
		} else {
			if (flags & TextBlockFBold) {
				result = result->bold();
			} else if (flags & TextBlockFSemibold) {
				result = result->semibold();
			}
			if (flags & TextBlockFItalic) result = result->italic();
			if (flags & TextBlockFUnderline) result = result->underline();
			if (flags & TextBlockFStrikeOut) result = result->strikeout();
			if (flags & TextBlockFTilde) { // tilde fix in OpenSans
				result = result->semibold();
			}
		}
		return result;
	}

	void eSetFont(const AbstractBlock *block) {
		const auto flags = block->flags();
		const auto usedFont = [&] {
			if (const auto index = block->lnkIndex()) {
				const auto active = ClickHandler::showAsActive(
					_t->_links.at(index - 1)
				) || (_textPalette && _textPalette->linkAlwaysActive > 0);
				return active
					? _t->_st->linkFontOver
					: _t->_st->linkFont;
			}
			return _t->_st->font;
		}();
		const auto newFont = applyFlags(flags, usedFont);
		if (newFont != _f) {
			_f = (newFont->family() == _t->_st->font->family())
				? applyFlags(flags | newFont->flags(), _t->_st->font)
				: newFont;
			_e->fnt = _f->f;
			_e->resetFontEngineCache();
		}
	}

	void eItemize() {
		_e->validate();
		if (_e->layoutData->items.size())
			return;

		int length = _e->layoutData->string.length();
		if (!length)
			return;

		const ushort *string = reinterpret_cast<const ushort*>(_e->layoutData->string.unicode());

		auto blockIndex = _lineStartBlock;
		auto currentBlock = _t->_blocks[blockIndex].get();
		auto nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;

		_e->layoutData->hasBidi = _parHasBidi;
		auto analysis = _parAnalysis.data() + (_localFrom - _parStart);

		{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
			QUnicodeTools::ScriptItemArray scriptItems;
			QUnicodeTools::initScripts(_e->layoutData->string, &scriptItems);
			for (int i = 0; i < scriptItems.length(); ++i) {
				const auto &item = scriptItems.at(i);
				int end = i < scriptItems.length() - 1 ? scriptItems.at(i + 1).position : length;
				for (int j = item.position; j < end; ++j)
					analysis[j].script = item.script;
			}
#else // Qt >= 6.0.0
			QVarLengthArray<uchar> scripts(length);
			QUnicodeTools::initScripts(string, length, scripts.data());
			for (int i = 0; i < length; ++i)
				analysis[i].script = scripts.at(i);
#endif // Qt < 6.0.0
		}

		blockIndex = _lineStartBlock;
		currentBlock = _t->_blocks[blockIndex].get();
		nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;

		auto start = string;
		auto end = start + length;
		while (start < end) {
			while (nextBlock && nextBlock->from() <= _localFrom + (start - string)) {
				currentBlock = nextBlock;
				nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
			}
			auto _type = currentBlock->type();
			if (_type == TextBlockTEmoji || _type == TextBlockTSkip) {
				analysis->script = QChar::Script_Common;
				analysis->flags = QScriptAnalysis::Object;
			} else {
				analysis->flags = QScriptAnalysis::None;
			}
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
			analysis->script = hbscript_to_script(script_to_hbscript(analysis->script)); // retain the old behavior
#endif // Qt < 6.0.0
			++start;
			++analysis;
		}

		{
			auto i_string = &_e->layoutData->string;
			auto i_analysis = _parAnalysis.data() + (_localFrom - _parStart);
			auto i_items = &_e->layoutData->items;

			blockIndex = _lineStartBlock;
			currentBlock = _t->_blocks[blockIndex].get();
			nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
			auto startBlock = currentBlock;

			if (!length) {
				return;
			}
			auto start = 0;
			auto end = start + length;
			for (int i = start + 1; i < end; ++i) {
				while (nextBlock && nextBlock->from() <= _localFrom + i) {
					currentBlock = nextBlock;
					nextBlock = (++blockIndex < _blocksSize) ? _t->_blocks[blockIndex].get() : nullptr;
				}
				// According to the unicode spec we should be treating characters in the Common script
				// (punctuation, spaces, etc) as being the same script as the surrounding text for the
				// purpose of splitting up text. This is important because, for example, a fullstop
				// (0x2E) can be used to indicate an abbreviation and so must be treated as part of a
				// word.  Thus it must be passed along with the word in languages that have to calculate
				// word breaks.  For example the thai word "ครม." has no word breaks but the word "ครม"
				// does.
				// Unfortuntely because we split up the strings for both wordwrapping and for setting
				// the font and because Japanese and Chinese are also aliases of the script "Common",
				// doing this would break too many things.  So instead we only pass the full stop
				// along, and nothing else.
				if (currentBlock == startBlock
					&& i_analysis[i].bidiLevel == i_analysis[start].bidiLevel
					&& i_analysis[i].flags == i_analysis[start].flags
					&& (i_analysis[i].script == i_analysis[start].script || i_string->at(i) == QLatin1Char('.'))
//					&& i_analysis[i].flags < QScriptAnalysis::SpaceTabOrObject // only emojis are objects here, no tabs
					&& i - start < _MaxItemLength)
					continue;
				i_items->append(QScriptItem(start, i_analysis[start]));
				start = i;
				startBlock = currentBlock;
			}
			i_items->append(QScriptItem(start, i_analysis[start]));
		}
	}

	QChar::Direction eSkipBoundryNeutrals(QScriptAnalysis *analysis,
											const ushort *unicode,
											int &sor, int &eor, BidiControl &control,
											String::TextBlocks::const_iterator i) {
		String::TextBlocks::const_iterator e = _t->_blocks.cend(), n = i + 1;

		QChar::Direction dir = control.basicDirection();
		int level = sor > 0 ? analysis[sor - 1].bidiLevel : control.level;
		while (sor <= _parLength) {
			while (i != _parStartBlock && (*i)->from() > _parStart + sor) {
				n = i;
				--i;
			}
			while (n != e && (*n)->from() <= _parStart + sor) {
				i = n;
				++n;
			}

			TextBlockType _itype = (*i)->type();
			if (eor == _parLength)
				dir = control.basicDirection();
			else if (_itype == TextBlockTEmoji)
				dir = QChar::DirCS;
			else if (_itype == TextBlockTSkip)
				dir = QChar::DirCS;
			else
				dir = QChar::direction(unicode[sor]);
			// Keep skipping DirBN as if it doesn't exist
			if (dir != QChar::DirBN)
				break;
			analysis[sor++].bidiLevel = level;
		}

		eor = sor;

		return dir;
	}

	// creates the next QScript items.
	bool eBidiItemize(QScriptAnalysis *analysis, BidiControl &control) {
		bool rightToLeft = (control.basicDirection() == 1);
		bool hasBidi = rightToLeft;

		int sor = 0;
		int eor = -1;

		const ushort *unicode = reinterpret_cast<const ushort*>(_t->_text.unicode()) + _parStart;
		int current = 0;

		QChar::Direction dir = rightToLeft ? QChar::DirR : QChar::DirL;
		BidiStatus status;

		String::TextBlocks::const_iterator i = _parStartBlock, e = _t->_blocks.cend(), n = i + 1;

		QChar::Direction sdir;
		TextBlockType _stype = (*_parStartBlock)->type();
		if (_stype == TextBlockTEmoji)
			sdir = QChar::DirCS;
		else if (_stype == TextBlockTSkip)
			sdir = QChar::DirCS;
		else
			sdir = QChar::direction(*unicode);
		if (sdir != QChar::DirL && sdir != QChar::DirR && sdir != QChar::DirEN && sdir != QChar::DirAN)
			sdir = QChar::DirON;
		else
			dir = QChar::DirON;

		status.eor = sdir;
		status.lastStrong = rightToLeft ? QChar::DirR : QChar::DirL;
		status.last = status.lastStrong;
		status.dir = sdir;

		while (current <= _parLength) {
			while (n != e && (*n)->from() <= _parStart + current) {
				i = n;
				++n;
			}

			QChar::Direction dirCurrent;
			TextBlockType _itype = (*i)->type();
			if (current == (int)_parLength)
				dirCurrent = control.basicDirection();
			else if (_itype == TextBlockTEmoji)
				dirCurrent = QChar::DirCS;
			else if (_itype == TextBlockTSkip)
				dirCurrent = QChar::DirCS;
			else
				dirCurrent = QChar::direction(unicode[current]);

			switch (dirCurrent) {

				// embedding and overrides (X1-X9 in the BiDi specs)
			case QChar::DirRLE:
			case QChar::DirRLO:
			case QChar::DirLRE:
			case QChar::DirLRO:
				{
					bool rtl = (dirCurrent == QChar::DirRLE || dirCurrent == QChar::DirRLO);
					hasBidi |= rtl;
					bool override = (dirCurrent == QChar::DirLRO || dirCurrent == QChar::DirRLO);

					unsigned int level = control.level+1;
					if ((level%2 != 0) == rtl) ++level;
					if (level < _MaxBidiLevel) {
						eor = current-1;
						eAppendItems(analysis, sor, eor, control, dir);
						eor = current;
						control.embed(rtl, override);
						QChar::Direction edir = (rtl ? QChar::DirR : QChar::DirL);
						dir = status.eor = edir;
						status.lastStrong = edir;
					}
					break;
				}
			case QChar::DirPDF:
				{
					if (control.canPop()) {
						if (dir != control.direction()) {
							eor = current-1;
							eAppendItems(analysis, sor, eor, control, dir);
							dir = control.direction();
						}
						eor = current;
						eAppendItems(analysis, sor, eor, control, dir);
						control.pdf();
						dir = QChar::DirON; status.eor = QChar::DirON;
						status.last = control.direction();
						if (control.override)
							dir = control.direction();
						else
							dir = QChar::DirON;
						status.lastStrong = control.direction();
					}
					break;
				}

				// strong types
			case QChar::DirL:
				if(dir == QChar::DirON)
					dir = QChar::DirL;
				switch(status.last)
					{
					case QChar::DirL:
						eor = current; status.eor = QChar::DirL; break;
					case QChar::DirR:
					case QChar::DirAL:
					case QChar::DirEN:
					case QChar::DirAN:
						if (eor >= 0) {
							eAppendItems(analysis, sor, eor, control, dir);
							status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i);
						} else {
							eor = current; status.eor = dir;
						}
						break;
					case QChar::DirES:
					case QChar::DirET:
					case QChar::DirCS:
					case QChar::DirBN:
					case QChar::DirB:
					case QChar::DirS:
					case QChar::DirWS:
					case QChar::DirON:
						if(dir != QChar::DirL) {
							//last stuff takes embedding dir
							if(control.direction() == QChar::DirR) {
								if(status.eor != QChar::DirR) {
									// AN or EN
									eAppendItems(analysis, sor, eor, control, dir);
									status.eor = QChar::DirON;
									dir = QChar::DirR;
								}
								eor = current - 1;
								eAppendItems(analysis, sor, eor, control, dir);
								status.eor = dir = eSkipBoundryNeutrals(analysis, unicode, sor, eor, control, i);
							} else {
								if(status.eor != QChar::DirL) {
									eAppendItems(analysis, sor, eor, control, dir);
									status.eor = QChar::DirON;
									dir = QChar::DirL;
								} else {
									eor = current; status.eor = QChar::DirL; break;
								}
							}
						} else {
							eor = current; status.eor = QChar::DirL;
						}
					default:
						break;
					}
				status.lastStrong = QChar::DirL;
				break;
			case QChar::DirAL:
			case QChar::DirR:
				hasBidi = true;
				if(dir == QChar::DirON) dir = QChar::DirR;
				switch(status.last)
					{
					case QChar::DirL:
					case QChar::DirEN:
					case QChar::DirAN:
						if (eor >= 0)
							eAppendItems(analysis, sor, eor, control, dir);
						// fall through
					case QChar::DirR:
					case QChar::DirAL:
						dir = QChar::DirR; eor = current; status.eor = QChar::DirR; break;
					case QChar::DirES:
					case QChar::DirET:
					case QChar::DirCS:
					case QChar::DirBN:
					case QChar::DirB:
					case QChar::DirS:
					case QChar::DirWS:
					case QChar::DirON:
						if(status.eor != QChar::DirR && status.eor != QChar::DirAL) {
							//last stuff takes embedding dir
							if(control.direction() == QChar::DirR
							   || status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) {
								eAppendItems(analysis, sor, eor, control, dir);
								dir = QChar::DirR; status.eor = QChar::DirON;
								eor = current;
							} else {
								eor = current - 1;
								eAppendItems(analysis, sor, eor, control, dir);
								dir = QChar::DirR; status.eor = QChar::DirON;
							}
						} else {
							eor = current; status.eor = QChar::DirR;
						}
					default:
						break;
					}
				status.lastStrong = dirCurrent;
				break;

				// weak types:

			case QChar::DirNSM:
				if (eor == current-1)
					eor = current;
				break;
			case QChar::DirEN:
				// if last strong was AL change EN to AN
				if(status.lastStrong != QChar::DirAL) {
					if(dir == QChar::DirON) {
						if(status.lastStrong == QChar::DirL)
							dir = QChar::DirL;
						else
							dir = QChar::DirEN;
					}
					switch(status.last)
						{
						case QChar::DirET:
							if (status.lastStrong == QChar::DirR || status.lastStrong == QChar::DirAL) {
								eAppendItems(analysis, sor, eor, control, dir);
								status.eor = QChar::DirON;
								dir = QChar::DirAN;
							}
							[[fallthrough]];
						case QChar::DirEN:
						case QChar::DirL:
							eor = current;
							status.eor = dirCurrent;
							break;
						case QChar::DirR:
						case QChar::DirAL:
						case QChar::DirAN:
							if (eor >= 0)
								eAppendItems(analysis, sor, eor, control, dir);
							else
								eor = current;
							status.eor = QChar::DirEN;
							dir = QChar::DirAN;
							break;
						case QChar::DirES:
						case QChar::DirCS:
							if(status.eor == QChar::DirEN || dir == QChar::DirAN) {
								eor = current; break;
							}
							[[fallthrough]];
						case QChar::DirBN:
						case QChar::DirB:
						case QChar::DirS:
						case QChar::DirWS:
						case QChar::DirON:
							if(status.eor == QChar::DirR) {
								// neutrals go to R
								eor = current - 1;
								eAppendItems(analysis, sor, eor, control, dir);
								status.eor = QChar::DirEN;
								dir = QChar::DirAN;
							}
							else if(status.eor == QChar::DirL ||
									 (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) {
								eor = current; status.eor = dirCurrent;
							} else {
								// numbers on both sides, neutrals get right to left direction
								if(dir != QChar::DirL) {
									eAppendItems(analysis, sor, eor, control, dir);
									status.eor = QChar::DirON;
									eor = current - 1;
									dir = QChar::DirR;
									eAppendItems(analysis, sor, eor, control, dir);
									status.eor = QChar::DirON;
									dir = QChar::DirAN;
								} else {
									eor = current; status.eor = dirCurrent;
								}
							}
							[[fallthrough]];
						default:
							break;
						}
					break;
				}
				[[fallthrough]];
			case QChar::DirAN:
				hasBidi = true;
				dirCurrent = QChar::DirAN;
				if(dir == QChar::DirON) dir = QChar::DirAN;
				switch(status.last)
					{
					case QChar::DirL:
					case QChar::DirAN:
						eor = current; status.eor = QChar::DirAN;
						break;
					case QChar::DirR:
					case QChar::DirAL:
					case QChar::DirEN:
						if (eor >= 0){
							eAppendItems(analysis, sor, eor, control, dir);
						} else {
							eor = current;
						}
						dir = QChar::DirAN; status.eor = QChar::DirAN;
						break;
					case QChar::DirCS:
						if(status.eor == QChar::DirAN) {
							eor = current; break;
						}
						[[fallthrough]];
					case QChar::DirES:
					case QChar::DirET:
					case QChar::DirBN:
					case QChar::DirB:
					case QChar::DirS:
					case QChar::DirWS:
					case QChar::DirON:
						if(status.eor == QChar::DirR) {
							// neutrals go to R
							eor = current - 1;
							eAppendItems(analysis, sor, eor, control, dir);
							status.eor = QChar::DirAN;
							dir = QChar::DirAN;
						} else if(status.eor == QChar::DirL ||
								   (status.eor == QChar::DirEN && status.lastStrong == QChar::DirL)) {
							eor = current; status.eor = dirCurrent;
						} else {
							// numbers on both sides, neutrals get right to left direction
							if(dir != QChar::DirL) {
								eAppendItems(analysis, sor, eor, control, dir);
								status.eor = QChar::DirON;
								eor = current - 1;
								dir = QChar::DirR;
								eAppendItems(analysis, sor, eor, control, dir);
								status.eor = QChar::DirAN;
								dir = QChar::DirAN;
							} else {
								eor = current; status.eor = dirCurrent;
							}
						}
						[[fallthrough]];
					default:
						break;
					}
				break;
			case QChar::DirES:
			case QChar::DirCS:
				break;
			case QChar::DirET:
				if(status.last == QChar::DirEN) {
					dirCurrent = QChar::DirEN;
					eor = current; status.eor = dirCurrent;
				}
				break;

				// boundary neutrals should be ignored
			case QChar::DirBN:
				break;
				// neutrals
			case QChar::DirB:
				// ### what do we do with newline and paragraph separators that come to here?
				break;
			case QChar::DirS:
				// ### implement rule L1
				break;
			case QChar::DirWS:
			case QChar::DirON:
				break;
			default:
				break;
			}

			if(current >= (int)_parLength) break;

			// set status.last as needed.
			switch(dirCurrent) {
			case QChar::DirET:
			case QChar::DirES:
			case QChar::DirCS:
			case QChar::DirS:
			case QChar::DirWS:
			case QChar::DirON:
				switch(status.last)
				{
				case QChar::DirL:
				case QChar::DirR:
				case QChar::DirAL:
				case QChar::DirEN:
				case QChar::DirAN:
					status.last = dirCurrent;
					break;
				default:
					status.last = QChar::DirON;
				}
				break;
			case QChar::DirNSM:
			case QChar::DirBN:
				// ignore these
				break;
			case QChar::DirLRO:
			case QChar::DirLRE:
				status.last = QChar::DirL;
				break;
			case QChar::DirRLO:
			case QChar::DirRLE:
				status.last = QChar::DirR;
				break;
			case QChar::DirEN:
				if (status.last == QChar::DirL) {
					status.last = QChar::DirL;
					break;
				}
				[[fallthrough]];
			default:
				status.last = dirCurrent;
			}

			++current;
		}

		eor = current - 1; // remove dummy char

		if (sor <= eor)
			eAppendItems(analysis, sor, eor, control, dir);

		return hasBidi;
	}

private:
	void applyBlockProperties(const AbstractBlock *block) {
		eSetFont(block);
		if (_p) {
			const auto isMono = IsMono(block->flags());
			_background = {};
			if (block->spoilerIndex()) {
				const auto handler
					= _t->_spoilers.at(block->spoilerIndex() - 1);
				const auto inBack = (handler && handler->shown());
				_background.inFront = !inBack;
				_background.color = inBack
					? &_textPalette->spoilerActiveBg
					: &_textPalette->spoilerBg;
				_background.startMs = handler ? handler->startMs() : 0;
				_background.spoilerIndex = block->spoilerIndex();

				const auto &cache = _background.inFront
					? _t->_spoilerCache
					: _t->_spoilerShownCache;
				if (cache.color != (*_background.color)->c) {
					auto mutableText = const_cast<String*>(_t);
					auto &mutableCache = _background.inFront
						? mutableText->_spoilerCache
						: mutableText->_spoilerShownCache;
					mutableCache.corners = Images::PrepareCorners(
						ImageRoundRadius::Small,
						*_background.color);
					mutableCache.color = (*_background.color)->c;
				}
			}
			if (isMono && block->lnkIndex() && !_background.inFront) {
				_background.selectActiveBlock = ClickHandler::showAsPressed(
					_t->_links.at(block->lnkIndex() - 1));
			}

			if (isMono) {
				_currentPen = &_textPalette->monoFg->p;
				_currentPenSelected = &_textPalette->selectMonoFg->p;
			} else if (block->lnkIndex()
				|| (block->flags() & TextBlockFPlainLink)) {
				_currentPen = &_textPalette->linkFg->p;
				_currentPenSelected = &_textPalette->selectLinkFg->p;
			} else {
				_currentPen = &_originalPen;
				_currentPenSelected = &_originalPenSelected;
			}
		}
	}

	Painter *_p = nullptr;
	const style::TextPalette *_textPalette = nullptr;
	const String *_t = nullptr;
	bool _elideLast = false;
	bool _breakEverywhere = false;
	int _elideRemoveFromEnd = 0;
	style::align _align = style::al_topleft;
	const QPen _originalPen;
	QPen _originalPenSelected;
	const QPen *_currentPen = nullptr;
	const QPen *_currentPenSelected = nullptr;
	struct {
		const style::color *color = nullptr;
		bool inFront = false;
		crl::time startMs = 0;
		uint16 spoilerIndex = 0;

		bool selectActiveBlock = false; // For monospace.
	} _background;
	int _yFrom = 0;
	int _yTo = 0;
	int _yToElide = 0;
	TextSelection _selection = { 0, 0 };
	bool _fullWidthSelection = true;
	const QChar *_str = nullptr;

	int _indexOfElidedBlock = -1; // For spoilers.

	// current paragraph data
	String::TextBlocks::const_iterator _parStartBlock;
	Qt::LayoutDirection _parDirection;
	int _parStart = 0;
	int _parLength = 0;
	bool _parHasBidi = false;
	QVarLengthArray<QScriptAnalysis, 4096> _parAnalysis;

	// current line data
	QTextEngine *_e = nullptr;
	style::font _f;
	QFixed _x, _w, _wLeft, _last_rPadding;
	int32 _y, _yDelta, _lineHeight, _fontHeight;

	// elided hack support
	int _blocksSize = 0;
	int _elideSavedIndex = 0;
	std::optional<Block> _elideSavedBlock;

	int _lineStart = 0;
	int _localFrom = 0;
	int _lineStartBlock = 0;

	// link and symbol resolve
	QFixed _lookupX = 0;
	int _lookupY = 0;
	bool _lookupSymbol = false;
	bool _lookupLink = false;
	StateRequest _lookupRequest;
	StateResult _lookupResult;

};

String::String(int32 minResizeWidth) : _minResizeWidth(minResizeWidth) {
}

String::String(const style::TextStyle &st, const QString &text, const TextParseOptions &options, int32 minResizeWidth)
: _minResizeWidth(minResizeWidth) {
	setText(st, text, options);
}

void String::setText(const style::TextStyle &st, const QString &text, const TextParseOptions &options) {
	_st = &st;
	clear();
	{
		Parser parser(this, { text }, options, {});
	}
	recountNaturalSize(true, options.dir);
}

void String::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) {
	NewlineBlock *lastNewline = 0;

	_maxWidth = _minHeight = 0;
	int32 lineHeight = 0;
	int32 lastNewlineStart = 0;
	QFixed _width = 0, last_rBearing = 0, last_rPadding = 0;
	for (auto &block : _blocks) {
		auto b = block.get();
		auto _btype = b->type();
		auto blockHeight = countBlockHeight(b, _st);
		if (_btype == TextBlockTNewline) {
			if (!lineHeight) lineHeight = blockHeight;
			if (initial) {
				Qt::LayoutDirection dir = optionsDir;
				if (dir == Qt::LayoutDirectionAuto) {
					dir = StringDirection(_text, lastNewlineStart, b->from());
				}
				if (lastNewline) {
					lastNewline->_nextDir = dir;
				} else {
					_startDir = dir;
				}
			}
			lastNewlineStart = b->from();
			lastNewline = &block.unsafe<NewlineBlock>();

			_minHeight += lineHeight;
			lineHeight = 0;
			last_rBearing = b->f_rbearing();
			last_rPadding = b->f_rpadding();

			accumulate_max(_maxWidth, _width);
			_width = (b->f_width() - last_rBearing);
			continue;
		}

		auto b__f_rbearing = b->f_rbearing(); // cache

		// We need to accumulate max width after each block, because
		// some blocks have width less than -1 * previous right bearing.
		// In that cases the _width gets _smaller_ after moving to the next block.
		//
		// But when we layout block and we're sure that _maxWidth is enough
		// for all the blocks to fit on their line we check each block, even the
		// intermediate one with a large negative right bearing.
		accumulate_max(_maxWidth, _width);

		_width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing);
		lineHeight = qMax(lineHeight, blockHeight);

		last_rBearing = b__f_rbearing;
		last_rPadding = b->f_rpadding();
		continue;
	}
	if (initial) {
		Qt::LayoutDirection dir = optionsDir;
		if (dir == Qt::LayoutDirectionAuto) {
			dir = StringDirection(_text, lastNewlineStart, _text.size());
		}
		if (lastNewline) {
			lastNewline->_nextDir = dir;
		} else {
			_startDir = dir;
		}
	}
	if (_width > 0) {
		if (!lineHeight) lineHeight = countBlockHeight(_blocks.back().get(), _st);
		_minHeight += lineHeight;
		accumulate_max(_maxWidth, _width);
	}
}

int String::countMaxMonospaceWidth() const {
	auto result = QFixed();
	auto paragraphWidth = QFixed();
	auto fullMonospace = true;
	QFixed _width = 0, last_rBearing = 0, last_rPadding = 0;
	for (auto &block : _blocks) {
		auto b = block.get();
		auto _btype = b->type();
		if (_btype == TextBlockTNewline) {
			last_rBearing = b->f_rbearing();
			last_rPadding = b->f_rpadding();

			if (fullMonospace) {
				accumulate_max(paragraphWidth, _width);
				accumulate_max(result, paragraphWidth);
				paragraphWidth = 0;
			} else {
				fullMonospace = true;
			}
			_width = (b->f_width() - last_rBearing);
			continue;
		}
		if (!(b->flags() & (TextBlockFPre | TextBlockFCode))
			&& (b->type() != TextBlockTSkip)) {
			fullMonospace = false;
		}
		auto b__f_rbearing = b->f_rbearing(); // cache

		// We need to accumulate max width after each block, because
		// some blocks have width less than -1 * previous right bearing.
		// In that cases the _width gets _smaller_ after moving to the next block.
		//
		// But when we layout block and we're sure that _maxWidth is enough
		// for all the blocks to fit on their line we check each block, even the
		// intermediate one with a large negative right bearing.
		if (fullMonospace) {
			accumulate_max(paragraphWidth, _width);
		}
		_width += last_rBearing + (last_rPadding + b->f_width() - b__f_rbearing);

		last_rBearing = b__f_rbearing;
		last_rPadding = b->f_rpadding();
		continue;
	}
	if (_width > 0 && fullMonospace) {
		accumulate_max(paragraphWidth, _width);
		accumulate_max(result, paragraphWidth);
	}
	return result.ceil().toInt();
}

void String::setMarkedText(const style::TextStyle &st, const TextWithEntities &textWithEntities, const TextParseOptions &options, const std::any &context) {
	_st = &st;
	clear();
	{
		// utf codes of the text display for emoji extraction
//		auto text = textWithEntities.text;
//		auto newText = QString();
//		newText.reserve(8 * text.size());
//		newText.append("\t{ ");
//		for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
//			if (*ch == TextCommand) {
//				break;
//			} else if (IsNewline(*ch)) {
//				newText.append("},").append(*ch).append("\t{ ");
//			} else {
//				if (ch->isHighSurrogate() || ch->isLowSurrogate()) {
//					if (ch->isHighSurrogate() && (ch + 1 != e) && ((ch + 1)->isLowSurrogate())) {
//						newText.append("0x").append(QString::number((uint32(ch->unicode()) << 16) | uint32((ch + 1)->unicode()), 16).toUpper()).append("U, ");
//						++ch;
//					} else {
//						newText.append("BADx").append(QString::number(ch->unicode(), 16).toUpper()).append("U, ");
//					}
//				} else {
//					newText.append("0x").append(QString::number(ch->unicode(), 16).toUpper()).append("U, ");
//				}
//			}
//		}
//		newText.append("},\n\n").append(text);
//		Parser parser(this, { newText, EntitiesInText() }, options, context);

		Parser parser(this, textWithEntities, options, context);
	}
	recountNaturalSize(true, options.dir);
}

void String::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) {
	if (!lnkIndex || lnkIndex > _links.size()) return;
	_links[lnkIndex - 1] = lnk;
}

void String::setSpoiler(
		uint16 lnkIndex,
		const std::shared_ptr<SpoilerClickHandler> &lnk) {
	if (!lnkIndex || lnkIndex > _spoilers.size()) {
		return;
	}
	_spoilers[lnkIndex - 1] = lnk;
}

void String::setSpoilerShown(uint16 lnkIndex, bool shown) {
	if (!lnkIndex
		|| (lnkIndex > _spoilers.size())
		|| !_spoilers[lnkIndex - 1]) {
		return;
	}
	_spoilers[lnkIndex - 1]->setShown(shown);
}

bool String::hasLinks() const {
	return !_links.isEmpty();
}

int String::spoilersCount() const {
	return _spoilers.size();
}

bool String::hasSkipBlock() const {
	return _blocks.empty() ? false : _blocks.back()->type() == TextBlockTSkip;
}

bool String::updateSkipBlock(int width, int height) {
	if (!_blocks.empty() && _blocks.back()->type() == TextBlockTSkip) {
		const auto block = static_cast<SkipBlock*>(_blocks.back().get());
		if (block->width() == width && block->height() == height) {
			return false;
		}
		_text.resize(block->from());
		_blocks.pop_back();
	}
	_text.push_back('_');
	_blocks.push_back(Block::Skip(
		_st->font,
		_text,
		_text.size() - 1,
		width,
		height,
		0,
		0));
	recountNaturalSize(false);
	return true;
}

bool String::removeSkipBlock() {
	if (_blocks.empty() || _blocks.back()->type() != TextBlockTSkip) {
		return false;
	}
	_text.resize(_blocks.back()->from());
	_blocks.pop_back();
	recountNaturalSize(false);
	return true;
}

int String::countWidth(int width, bool breakEverywhere) const {
	if (QFixed(width) >= _maxWidth) {
		return _maxWidth.ceil().toInt();
	}

	QFixed maxLineWidth = 0;
	enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) {
		if (lineWidth > maxLineWidth) {
			maxLineWidth = lineWidth;
		}
	});
	return maxLineWidth.ceil().toInt();
}

int String::countHeight(int width, bool breakEverywhere) const {
	if (QFixed(width) >= _maxWidth) {
		return _minHeight;
	}
	int result = 0;
	enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) {
		result += lineHeight;
	});
	return result;
}

void String::countLineWidths(int width, QVector<int> *lineWidths, bool breakEverywhere) const {
	enumerateLines(width, breakEverywhere, [&](QFixed lineWidth, int lineHeight) {
		lineWidths->push_back(lineWidth.ceil().toInt());
	});
}

template <typename Callback>
void String::enumerateLines(
		int w,
		bool breakEverywhere,
		Callback callback) const {
	QFixed width = w;
	if (width < _minResizeWidth) width = _minResizeWidth;

	int lineHeight = 0;
	QFixed widthLeft = width, last_rBearing = 0, last_rPadding = 0;
	bool longWordLine = true;
	for (auto &b : _blocks) {
		auto _btype = b->type();
		int blockHeight = countBlockHeight(b.get(), _st);

		if (_btype == TextBlockTNewline) {
			if (!lineHeight) lineHeight = blockHeight;
			callback(width - widthLeft, lineHeight);

			lineHeight = 0;
			last_rBearing = b->f_rbearing();
			last_rPadding = b->f_rpadding();
			widthLeft = width - (b->f_width() - last_rBearing);

			longWordLine = true;
			continue;
		}
		auto b__f_rbearing = b->f_rbearing();
		auto newWidthLeft = widthLeft - last_rBearing - (last_rPadding + b->f_width() - b__f_rbearing);
		if (newWidthLeft >= 0) {
			last_rBearing = b__f_rbearing;
			last_rPadding = b->f_rpadding();
			widthLeft = newWidthLeft;

			lineHeight = qMax(lineHeight, blockHeight);

			longWordLine = false;
			continue;
		}

		if (_btype == TextBlockTText) {
			const auto t = &b.unsafe<TextBlock>();
			if (t->_words.isEmpty()) { // no words in this block, spaces only => layout this block in the same line
				last_rPadding += b->f_rpadding();

				lineHeight = qMax(lineHeight, blockHeight);

				longWordLine = false;
				continue;
			}

			auto f_wLeft = widthLeft;
			int f_lineHeight = lineHeight;
			for (auto j = t->_words.cbegin(), e = t->_words.cend(), f = j; j != e; ++j) {
				bool wordEndsHere = (j->f_width() >= 0);
				auto j_width = wordEndsHere ? j->f_width() : -j->f_width();

				auto newWidthLeft = widthLeft - last_rBearing - (last_rPadding + j_width - j->f_rbearing());
				if (newWidthLeft >= 0) {
					last_rBearing = j->f_rbearing();
					last_rPadding = j->f_rpadding();
					widthLeft = newWidthLeft;

					lineHeight = qMax(lineHeight, blockHeight);

					if (wordEndsHere) {
						longWordLine = false;
					}
					if (wordEndsHere || longWordLine) {
						f_wLeft = widthLeft;
						f_lineHeight = lineHeight;
						f = j + 1;
					}
					continue;
				}

				if (f != j && !breakEverywhere) {
					j = f;
					widthLeft = f_wLeft;
					lineHeight = f_lineHeight;
					j_width = (j->f_width() >= 0) ? j->f_width() : -j->f_width();
				}

				callback(width - widthLeft, lineHeight);

				lineHeight = qMax(0, blockHeight);
				last_rBearing = j->f_rbearing();
				last_rPadding = j->f_rpadding();
				widthLeft = width - (j_width - last_rBearing);

				longWordLine = !wordEndsHere;
				f = j + 1;
				f_wLeft = widthLeft;
				f_lineHeight = lineHeight;
			}
			continue;
		}

		callback(width - widthLeft, lineHeight);

		lineHeight = qMax(0, blockHeight);
		last_rBearing = b__f_rbearing;
		last_rPadding = b->f_rpadding();
		widthLeft = width - (b->f_width() - last_rBearing);

		longWordLine = true;
		continue;
	}
	if (widthLeft < width) {
		callback(width - widthLeft, lineHeight);
	}
}

void String::draw(Painter &painter, int32 left, int32 top, int32 w, style::align align, int32 yFrom, int32 yTo, TextSelection selection, bool fullWidthSelection) const {
//	painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug
	Renderer p(&painter, this);
	p.draw(left, top, w, align, yFrom, yTo, selection, fullWidthSelection);
}

void String::drawElided(Painter &painter, int32 left, int32 top, int32 w, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const {
//	painter.fillRect(QRect(left, top, w, countHeight(w)), QColor(0, 0, 0, 32)); // debug
	Renderer p(&painter, this);
	p.drawElided(left, top, w, align, lines, yFrom, yTo, removeFromEnd, breakEverywhere, selection);
}

void String::drawLeft(Painter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const {
	draw(p, style::RightToLeft() ? (outerw - left - width) : left, top, width, align, yFrom, yTo, selection);
}

void String::drawLeftElided(Painter &p, int32 left, int32 top, int32 width, int32 outerw, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const {
	drawElided(p, style::RightToLeft() ? (outerw - left - width) : left, top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere, selection);
}

void String::drawRight(Painter &p, int32 right, int32 top, int32 width, int32 outerw, style::align align, int32 yFrom, int32 yTo, TextSelection selection) const {
	draw(p, style::RightToLeft() ? right : (outerw - right - width), top, width, align, yFrom, yTo, selection);
}

void String::drawRightElided(Painter &p, int32 right, int32 top, int32 width, int32 outerw, int32 lines, style::align align, int32 yFrom, int32 yTo, int32 removeFromEnd, bool breakEverywhere, TextSelection selection) const {
	drawElided(p, style::RightToLeft() ? right : (outerw - right - width), top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere, selection);
}

StateResult String::getState(QPoint point, int width, StateRequest request) const {
	return Renderer(nullptr, this).getState(point, width, request);
}

StateResult String::getStateLeft(QPoint point, int width, int outerw, StateRequest request) const {
	return getState(style::rtlpoint(point, outerw), width, request);
}

StateResult String::getStateElided(QPoint point, int width, StateRequestElided request) const {
	return Renderer(nullptr, this).getStateElided(point, width, request);
}

StateResult String::getStateElidedLeft(QPoint point, int width, int outerw, StateRequestElided request) const {
	return getStateElided(style::rtlpoint(point, outerw), width, request);
}

TextSelection String::adjustSelection(TextSelection selection, TextSelectType selectType) const {
	uint16 from = selection.from, to = selection.to;
	if (from < _text.size() && from <= to) {
		if (to > _text.size()) to = _text.size();
		if (selectType == TextSelectType::Paragraphs) {

			// Full selection of monospace entity.
			for (const auto &b : _blocks) {
				if (b->from() < from) {
					continue;
				}
				if (!IsMono(b->flags())) {
					break;
				}
				const auto &entities = toTextWithEntities().entities;
				const auto eIt = ranges::find_if(entities, [&](
						const EntityInText &e) {
					return (e.type() == EntityType::Pre
							|| e.type() == EntityType::Code)
						&& (from >= e.offset())
						&& ((e.offset() + e.length()) >= to);
				});
				if (eIt != entities.end()) {
					from = eIt->offset();
					to = eIt->offset() + eIt->length();
					while (to > 0 && IsSpace(_text.at(to - 1))) {
						--to;
					}
					if (to >= from) {
						return { from, to };
					}
				}
				break;
			}

			if (!IsParagraphSeparator(_text.at(from))) {
				while (from > 0 && !IsParagraphSeparator(_text.at(from - 1))) {
					--from;
				}
			}
			if (to < _text.size()) {
				if (IsParagraphSeparator(_text.at(to))) {
					++to;
				} else {
					while (to < _text.size() && !IsParagraphSeparator(_text.at(to))) {
						++to;
					}
				}
			}
		} else if (selectType == TextSelectType::Words) {
			if (!IsWordSeparator(_text.at(from))) {
				while (from > 0 && !IsWordSeparator(_text.at(from - 1))) {
					--from;
				}
			}
			if (to < _text.size()) {
				if (IsWordSeparator(_text.at(to))) {
					++to;
				} else {
					while (to < _text.size() && !IsWordSeparator(_text.at(to))) {
						++to;
					}
				}
			}
		}
	}
	return { from, to };
}

bool String::isEmpty() const {
	return _blocks.empty() || _blocks[0]->type() == TextBlockTSkip;
}

uint16 String::countBlockEnd(const TextBlocks::const_iterator &i, const TextBlocks::const_iterator &e) const {
	return (i + 1 == e) ? _text.size() : (*(i + 1))->from();
}

uint16 String::countBlockLength(const String::TextBlocks::const_iterator &i, const String::TextBlocks::const_iterator &e) const {
	return countBlockEnd(i, e) - (*i)->from();
}

template <typename AppendPartCallback, typename ClickHandlerStartCallback, typename ClickHandlerFinishCallback, typename FlagsChangeCallback>
void String::enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const {
	if (isEmpty() || selection.empty()) {
		return;
	}

	int lnkIndex = 0;
	uint16 lnkFrom = 0;

	int spoilerIndex = 0;
	uint16 spoilerFrom = 0;

	int32 flags = 0;
	for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) {
		int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex();
		int blockSpoilerIndex = (i == e) ? 0 : (*i)->spoilerIndex();
		uint16 blockFrom = (i == e) ? _text.size() : (*i)->from();
		int32 blockFlags = (i == e) ? 0 : (*i)->flags();

		if (IsMono(blockFlags)) {
			blockLnkIndex = 0;
		}
		if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links
			blockLnkIndex = 0;
		}
		if (blockLnkIndex != lnkIndex) {
			if (lnkIndex) {
				auto rangeFrom = qMax(selection.from, lnkFrom);
				auto rangeTo = qMin(selection.to, blockFrom);
				if (rangeTo > rangeFrom) { // handle click handler
					const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom);
					// Ignore links that are partially copied.
					const auto handler = (lnkFrom != rangeFrom || blockFrom != rangeTo)
						? nullptr
						: _links.at(lnkIndex - 1);
					const auto type = handler
						? handler->getTextEntity().type
						: EntityType::Invalid;
					clickHandlerFinishCallback(r, handler, type);
				}
			}
			lnkIndex = blockLnkIndex;
			if (lnkIndex) {
				lnkFrom = blockFrom;
				const auto handler = _links.at(lnkIndex - 1);
				clickHandlerStartCallback(handler
					? handler->getTextEntity().type
					: EntityType::Invalid);
			}
		}
		if (blockSpoilerIndex != spoilerIndex) {
			if (spoilerIndex) {
				auto rangeFrom = qMax(selection.from, spoilerFrom);
				auto rangeTo = qMin(selection.to, blockFrom);
				if (rangeTo > rangeFrom) { // handle click handler
					const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom);
					// Ignore links that are partially copied.
					const auto handler = (spoilerFrom != rangeFrom || blockFrom != rangeTo)
						? nullptr
						: _spoilers.at(spoilerIndex - 1);
					const auto type = EntityType::Spoiler;
					clickHandlerFinishCallback(r, handler, type);
				}
			}
			spoilerIndex = blockSpoilerIndex;
			if (spoilerIndex) {
				spoilerFrom = blockFrom;
				clickHandlerStartCallback(EntityType::Spoiler);
			}
		}

		const auto checkBlockFlags = (blockFrom >= selection.from)
			&& (blockFrom <= selection.to);
		if (checkBlockFlags && blockFlags != flags) {
			flagsChangeCallback(flags, blockFlags);
			flags = blockFlags;
		}
		if (i == e || (lnkIndex ? lnkFrom : blockFrom) >= selection.to) {
			break;
		}

		if ((*i)->type() == TextBlockTSkip) continue;

		auto rangeFrom = qMax(selection.from, blockFrom);
		auto rangeTo = qMin(selection.to, uint16(blockFrom + countBlockLength(i, e)));
		if (rangeTo > rangeFrom) {
			appendPartCallback(base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom));
		}
	}
}

QString String::toString(TextSelection selection) const {
	return toText(selection, false, false).rich.text;
}

TextWithEntities String::toTextWithEntities(TextSelection selection) const {
	return toText(selection, false, true).rich;
}

TextForMimeData String::toTextForMimeData(TextSelection selection) const {
	return toText(selection, true, true);
}

TextForMimeData String::toText(
		TextSelection selection,
		bool composeExpanded,
		bool composeEntities) const {
	struct MarkdownTagTracker {
		TextBlockFlags flag = TextBlockFlags();
		EntityType type = EntityType();
		int start = 0;
	};
	auto result = TextForMimeData();
	result.rich.text.reserve(_text.size());
	if (composeExpanded) {
		result.expanded.reserve(_text.size());
	}
	const auto insertEntity = [&](EntityInText &&entity) {
		auto i = result.rich.entities.end();
		while (i != result.rich.entities.begin()) {
			auto j = i;
			if ((--j)->offset() <= entity.offset()) {
				break;
			}
			i = j;
		}
		result.rich.entities.insert(i, std::move(entity));
	};
	auto linkStart = 0;
	auto spoilerStart = 0;
	auto markdownTrackers = composeEntities
		? std::vector<MarkdownTagTracker>{
			{ TextBlockFItalic, EntityType::Italic },
			{ TextBlockFBold, EntityType::Bold },
			{ TextBlockFSemibold, EntityType::Semibold },
			{ TextBlockFUnderline, EntityType::Underline },
			{ TextBlockFPlainLink, EntityType::PlainLink },
			{ TextBlockFStrikeOut, EntityType::StrikeOut },
			{ TextBlockFCode, EntityType::Code }, // #TODO entities
			{ TextBlockFPre, EntityType::Pre },
		} : std::vector<MarkdownTagTracker>();
	const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) {
		if (!composeEntities) {
			return;
		}
		for (auto &tracker : markdownTrackers) {
			const auto flag = tracker.flag;
			if ((oldFlags & flag) && !(newFlags & flag)) {
				insertEntity({
					tracker.type,
					tracker.start,
					int(result.rich.text.size()) - tracker.start });
			} else if ((newFlags & flag) && !(oldFlags & flag)) {
				tracker.start = result.rich.text.size();
			}
		}
	};
	const auto clickHandlerStartCallback = [&](EntityType type) {
		if (type == EntityType::Spoiler) {
			spoilerStart = result.rich.text.size();
		} else {
			linkStart = result.rich.text.size();
		}
	};
	const auto clickHandlerFinishCallback = [&](
			QStringView inText,
			const ClickHandlerPtr &handler,
			EntityType type) {
		if (type == EntityType::Spoiler) {
			insertEntity({
				type,
				spoilerStart,
				int(result.rich.text.size() - spoilerStart),
				QString(),
			});
			return;
		}
		if (!handler || (!composeExpanded && !composeEntities)) {
			return;
		}
		const auto entity = handler->getTextEntity();
		const auto plainUrl = (entity.type == EntityType::Url)
			|| (entity.type == EntityType::Email);
		const auto full = plainUrl
			? QStringView(entity.data).mid(0, entity.data.size())
			: inText;
		const auto customTextLink = (entity.type == EntityType::CustomUrl);
		const auto internalLink = customTextLink
			&& entity.data.startsWith(qstr("internal:"));
		if (composeExpanded) {
			const auto sameAsTextLink = customTextLink
				&& (entity.data
					== UrlClickHandler::EncodeForOpening(full.toString()));
			if (customTextLink && !internalLink && !sameAsTextLink) {
				const auto &url = entity.data;
				result.expanded.append(qstr(" (")).append(url).append(')');
			}
		}
		if (composeEntities && !internalLink) {
			insertEntity({
				entity.type,
				linkStart,
				int(result.rich.text.size() - linkStart),
				plainUrl ? QString() : entity.data });
		}
	};
	const auto appendPartCallback = [&](QStringView part) {
		result.rich.text += part;
		if (composeExpanded) {
			result.expanded += part;
		}
	};

	enumerateText(
		selection,
		appendPartCallback,
		clickHandlerStartCallback,
		clickHandlerFinishCallback,
		flagsChangeCallback);

	if (composeEntities) {
		const auto proj = [](const EntityInText &entity) {
			const auto type = entity.type();
			const auto isUrl = (type == EntityType::Url)
				|| (type == EntityType::CustomUrl)
				|| (type == EntityType::BotCommand)
				|| (type == EntityType::Mention)
				|| (type == EntityType::MentionName)
				|| (type == EntityType::Hashtag)
				|| (type == EntityType::Cashtag);
			return std::pair{ entity.offset(), isUrl ? 0 : 1 };
		};
		const auto pred = [&](const EntityInText &a, const EntityInText &b) {
			return proj(a) < proj(b);
		};
		std::sort(
			result.rich.entities.begin(),
			result.rich.entities.end(),
			pred);
	}

	return result;
}

IsolatedEmoji String::toIsolatedEmoji() const {
	auto result = IsolatedEmoji();
	const auto skip = (_blocks.empty()
		|| _blocks.back()->type() != TextBlockTSkip) ? 0 : 1;
	if ((_blocks.size() > kIsolatedEmojiLimit + skip)
		|| !_spoilers.empty()) {
		return IsolatedEmoji();
	}
	auto index = 0;
	for (const auto &block : _blocks) {
		const auto type = block->type();
		if (block->lnkIndex()) {
			return IsolatedEmoji();
		} else if (type == TextBlockTEmoji) {
			result.items[index++] = block.unsafe<EmojiBlock>()._emoji;
		} else if (type != TextBlockTSkip) {
			return IsolatedEmoji();
		}
	}
	return result;
}

void String::clear() {
	clearFields();
	_text.clear();
}

void String::clearFields() {
	_blocks.clear();
	_links.clear();
	_spoilers.clear();
	_maxWidth = _minHeight = 0;
	_startDir = Qt::LayoutDirectionAuto;
}

ClickHandlerPtr String::spoilerLink(uint16 spoilerIndex) const {
	if (spoilerIndex) {
		const auto &handler = _spoilers.at(spoilerIndex - 1);
		return (handler && !handler->shown()) ? handler : nullptr;
	}
	return nullptr;
}

bool IsWordSeparator(QChar ch) {
	switch (ch.unicode()) {
	case QChar::Space:
	case QChar::LineFeed:
	case '.':
	case ',':
	case '?':
	case '!':
	case '@':
	case '#':
	case '$':
	case ':':
	case ';':
	case '-':
	case '<':
	case '>':
	case '[':
	case ']':
	case '(':
	case ')':
	case '{':
	case '}':
	case '=':
	case '/':
	case '+':
	case '%':
	case '&':
	case '^':
	case '*':
	case '\'':
	case '"':
	case '`':
	case '~':
	case '|':
		return true;
	default:
		break;
	}
	return false;
}

bool IsAlmostLinkEnd(QChar ch) {
	switch (ch.unicode()) {
	case '?':
	case ',':
	case '.':
	case '"':
	case ':':
	case '!':
	case '\'':
		return true;
	default:
		break;
	}
	return false;
}

bool IsLinkEnd(QChar ch) {
	return IsBad(ch)
		|| IsSpace(ch)
		|| IsNewline(ch)
		|| ch.isLowSurrogate()
		|| ch.isHighSurrogate();
}

bool IsNewline(QChar ch) {
	return (ch == QChar::LineFeed)
		|| (ch == 156);
}

bool IsSpace(QChar ch) {
	return ch.isSpace()
		|| (ch < 32)
		|| (ch == QChar::ParagraphSeparator)
		|| (ch == QChar::LineSeparator)
		|| (ch == QChar::ObjectReplacementCharacter)
		|| (ch == QChar::CarriageReturn)
		|| (ch == QChar::Tabulation)
		|| (ch == QChar(8203)/*Zero width space.*/);
}

bool IsDiac(QChar ch) { // diac and variation selectors
	return (ch.category() == QChar::Mark_NonSpacing)
		|| (ch == 1652)
		|| (ch >= 64606 && ch <= 64611);
}

bool IsReplacedBySpace(QChar ch) {
	// \xe2\x80[\xa8 - \xac\xad] // 8232 - 8237
	// QString from1 = QString::fromUtf8("\xe2\x80\xa8"), to1 = QString::fromUtf8("\xe2\x80\xad");
	// \xcc[\xb3\xbf\x8a] // 819, 831, 778
	// QString bad1 = QString::fromUtf8("\xcc\xb3"), bad2 = QString::fromUtf8("\xcc\xbf"), bad3 = QString::fromUtf8("\xcc\x8a");
	// [\x00\x01\x02\x07\x08\x0b-\x1f] // '\t' = 0x09
	return (/*code >= 0x00 && */ch <= 0x02)
		|| (ch >= 0x07 && ch <= 0x09)
		|| (ch >= 0x0b && ch <= 0x1f)
		|| (ch == 819)
		|| (ch == 831)
		|| (ch == 778)
		|| (ch >= 8232 && ch <= 8237);
}

bool IsTrimmed(QChar ch) {
	return (IsSpace(ch) || IsBad(ch));
}

} // namespace Text
} // namespace Ui
