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

#include "ui/text/text_spoiler_data.h"
#include "styles/style_basic.h"

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

namespace Ui::Text {
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 };

void InitTextItemWithScriptItem(QTextItemInt &ti, const QScriptItem &si) {
	// explicitly initialize flags so that initFontAttributes can be called
	// multiple times on the same TextItem
	ti.flags = { };
	if (si.analysis.bidiLevel % 2)
		ti.flags |= QTextItem::RightToLeft;
	ti.ascent = si.ascent;
	ti.descent = si.descent;

	if (ti.charFormat.hasProperty(QTextFormat::TextUnderlineStyle)) {
		ti.underlineStyle = ti.charFormat.underlineStyle();
	} else if (ti.charFormat.boolProperty(QTextFormat::FontUnderline)
		|| ti.f->underline()) {
		ti.underlineStyle = QTextCharFormat::SingleUnderline;
	}

	// compat
	if (ti.underlineStyle == QTextCharFormat::SingleUnderline)
		ti.flags |= QTextItem::Underline;

	if (ti.f->overline() || ti.charFormat.fontOverline())
		ti.flags |= QTextItem::Overline;
	if (ti.f->strikeOut() || ti.charFormat.fontStrikeOut())
		ti.flags |= QTextItem::StrikeOut;
}

void AppendRange(
		QVarLengthArray<FixedRange> &ranges,
		FixedRange range) {
	for (auto i = ranges.begin(); i != ranges.end(); ++i) {
		if (range.till < i->from) {
			ranges.insert(i, range);
			return;
		} else if (!Distinct(range, *i)) {
			*i = United(*i, range);
			for (auto j = i + 1; j != ranges.end(); ++j) {
				if (j->from > i->till) {
					ranges.erase(i + 1, j);
					return;
				} else {
					*i = United(*i, *j);
				}
			}
			ranges.erase(i + 1, ranges.end());
			return;
		}
	}
	ranges.push_back(range);
}

} // namespace

struct Renderer::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;
};

FixedRange Intersected(FixedRange a, FixedRange b) {
	return {
		.from = std::max(a.from, b.from),
		.till = std::min(a.till, b.till),
	};
}

bool Intersects(FixedRange a, FixedRange b) {
	return (a.till > b.from) && (b.till > a.from);
}

FixedRange United(FixedRange a, FixedRange b) {
	return {
		.from = std::min(a.from, b.from),
		.till = std::max(a.till, b.till),
	};
}

bool Distinct(FixedRange a, FixedRange b) {
	return (a.till < b.from) || (b.till < a.from);
}

Renderer::Renderer(const Ui::Text::String &t)
: _t(&t)
, _spoiler(_t->_spoiler.data.get()) {
}

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

void Renderer::draw(QPainter &p, const PaintContext &context) {
	if (_t->isEmpty()) {
		return;
	}

	_p = &p;
	_p->setFont(_t->_st->font);
	_palette = context.palette ? context.palette : &st::defaultTextPalette;
	_originalPen = _p->pen();
	_originalPenSelected = (_palette->selectFg->c.alphaF() == 0)
		? _originalPen
		: _palette->selectFg->p;

	_x = context.position.x();
	_y = context.position.y();
	_yFrom = context.clip.isNull() ? 0 : context.clip.y();
	_yTo = context.clip.isNull()
		? -1
		: (context.clip.y() + context.clip.height());
	if (const auto lines = context.elisionLines) {
		if (context.clip.isNull())
		if (_yTo < 0 || (_y + (lines - 1) * _t->_st->font->height) < _yTo) {
			_yTo = _y + (lines * _t->_st->font->height);
			_elideLast = true;
			_elideRemoveFromEnd = context.elisionRemoveFromEnd;
		}
		_breakEverywhere = context.elisionBreakEverywhere;
	}
	_spoilerCache = context.spoiler;
	_selection = context.selection;
	_fullWidthSelection = context.fullWidthSelection;
	_w = context.availableWidth;
	_align = context.align;
	_cachedNow = context.now;
	_paused = context.paused;
	_spoilerOpacity = _spoiler
		? (1. - _spoiler->revealAnimation.value(
			_spoiler->revealed ? 1. : 0.))
		: 0.;
	enumerate();
}

void Renderer::enumerate() {
	_blocksSize = _t->_blocks.size();
	_wLeft = _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();
		}
	}

	_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);

	const auto guard = gsl::finally([&] {
		if (_p) {
			paintSpoilerRects();
		}
	});

	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;
	}
}

StateResult Renderer::getState(QPoint point, int w, StateRequest request) {
	if (_t->isEmpty() || point.y() < 0) {
		return {};
	}
	_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)) {
		return {};
	}
	_w = w;
	_yFrom = _lookupY;
	_yTo = _lookupY + 1;
	_align = _lookupRequest.align;
	enumerate();
	return _lookupResult;
}

StateResult Renderer::getStateElided(QPoint point, int w, StateRequestElided request) {
	if (_t->isEmpty() || point.y() < 0 || request.lines <= 0) {
		return {};
	}
	_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)) {
		return {};
	}
	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;
	}
	_w = w;
	_yFrom = _lookupY;
	_yTo = _lookupY + 1;
	_align = _lookupRequest.align;
	enumerate();
	return _lookupResult;
}

crl::time Renderer::now() const {
	if (!_cachedNow) {
		_cachedNow = crl::now();
	}
	return _cachedNow;
}

void Renderer::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 Renderer::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;
			}
			const auto type = (*i)->type();
			if (type != TextBlockTEmoji
				&& type != TextBlockTCustomEmoji
				&& *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 Renderer::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 == TextBlockTCustomEmoji
				|| _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) {
						if (const auto link = lookupLink(currentBlock)) {
							_lookupResult.link = link;
						}
					}
				}
				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 || _type == TextBlockTCustomEmoji)) {
				auto glyphX = x;
				auto spacesWidth = (si.width - currentBlock->f_width());
				if (rtl) {
					glyphX += spacesWidth;
				}
				FixedRange fillSelect;
				FixedRange 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.spoiler
					&& (_spoilerOpacity > 0.);
				if (hasSpoiler) {
					fillSpoiler = { x, x + si.width };
				}
				fillSelectRange(fillSelect);
				const auto opacity = _p->opacity();
				if (!hasSpoiler || _spoilerOpacity < 1.) {
					if (hasSpoiler) {
						_p->setOpacity(opacity * (1. - _spoilerOpacity));
					}
					const auto x = (glyphX + st::emojiPadding).toInt();
					const auto y = _y + _yDelta + emojiY;
					if (_type == TextBlockTEmoji) {
						Emoji::Draw(
							*_p,
							static_cast<const EmojiBlock*>(currentBlock)->_emoji,
							Emoji::GetSizeNormal(),
							x,
							y);
					} else if (const auto custom = static_cast<const CustomEmojiBlock*>(currentBlock)->_custom.get()) {
						if (!_customEmojiSize) {
							_customEmojiSize = AdjustCustomEmojiSize(st::emojiSize);
							_customEmojiSkip = (st::emojiSize - _customEmojiSize) / 2;
						}
						custom->paint(*_p, {
							.preview = _palette->spoilerFg->c,
							.now = now(),
							.position = { x + _customEmojiSkip, y + _customEmojiSkip },
							.paused = _paused,
						});
					}
					if (hasSpoiler) {
						_p->setOpacity(opacity);
					}
				}
				if (hasSpoiler) {
					pushSpoilerRange(fillSpoiler, fillSelect, blockIndex);
				}
			//} 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) {
					if (const auto link = lookupLink(currentBlock)) {
						_lookupResult.link = link;
					}
				}
			}
			_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) {
			QTextItemInt gf;
			gf.glyphs = glyphs.mid(glyphsStart, glyphsEnd - glyphsStart);
			gf.f = &_e->fnt;
			gf.chars = engine.layoutData->string.unicode() + itemStart;
			gf.num_chars = itemEnd - itemStart;
			gf.fontEngine = engine.fontEngine(si);
			gf.logClusters = logClusters + itemStart - si.position;
			gf.width = itemWidth;
			gf.justified = false;
			InitTextItemWithScriptItem(gf, si);

			auto itemRange = FixedRange{ x, x + itemWidth };
			auto fillSelect = FixedRange();
			auto hasSelected = false;
			auto hasNotSelected = true;
			auto selectedRect = QRect();
			if (_background.selectActiveBlock) {
				fillSelect = itemRange;
				fillSelectRange(fillSelect);
			} 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);
				fillSelect = { selX, selX + selWidth };
				fillSelectRange(fillSelect);
			}
			const auto hasSpoiler = _background.spoiler
				&& (_spoilerOpacity > 0.);
			const auto opacity = _p->opacity();
			const auto isElidedBlock = !rtl
				&& (_indexOfElidedBlock == blockIndex);
			const auto complexClipping = hasSpoiler
				&& isElidedBlock
				&& (_spoilerOpacity == 1.);
			if (!hasSpoiler || (_spoilerOpacity < 1.) || isElidedBlock) {
				const auto complexClippingEnabled = complexClipping
					&& _p->hasClipping();
				const auto complexClippingRegion = complexClipping
					? _p->clipRegion()
					: QRegion();
				if (complexClipping) {
					const auto elided = (_indexOfElidedBlock == blockIndex)
						? (_elideRemoveFromEnd + _f->elidew)
						: 0;
					_p->setClipRect(
						QRect(
							(x + itemWidth).toInt() - elided,
							_y - _lineHeight,
							elided,
							_y + 2 * _lineHeight),
						Qt::IntersectClip);
				} else 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 (complexClipping) {
					if (complexClippingEnabled) {
						_p->setClipRegion(complexClippingRegion);
					} else {
						_p->setClipping(false);
					}
				} else if (hasSpoiler && !isElidedBlock) {
					_p->setOpacity(opacity);
				}
			}

			if (hasSpoiler) {
				pushSpoilerRange(itemRange, fillSelect, blockIndex);
			}
		}

		x += itemWidth;
	}
	fillSpoilerRects();
	return true;
}

void Renderer::fillSelectRange(FixedRange range) {
	if (range.empty()) {
		return;
	}
	const auto left = range.from.toInt();
	const auto width = range.till.toInt() - left;
	_p->fillRect(left, _y + _yDelta, width, _fontHeight, _palette->selectBg);
}

void Renderer::pushSpoilerRange(
		FixedRange range,
		FixedRange selected,
		int currentBlockIndex) {
	if (!_background.spoiler || !_spoiler) {
		return;
	}
	const auto elided = (_indexOfElidedBlock == currentBlockIndex)
		? (_elideRemoveFromEnd + _f->elidew)
		: 0;
	range.till -= elided;
	if (range.empty()) {
		return;
	} else if (selected.empty() || !Intersects(range, selected)) {
		AppendRange(_spoilerRanges, range);
	} else {
		AppendRange(_spoilerRanges, { range.from, selected.from });
		AppendRange(_spoilerSelectedRanges, Intersected(range, selected));
		AppendRange(_spoilerRanges, { selected.till, range.till });
	}
}

void Renderer::fillSpoilerRects() {
	fillSpoilerRects(_spoilerRects, _spoilerRanges);
	fillSpoilerRects(_spoilerSelectedRects, _spoilerSelectedRanges);
}

void Renderer::fillSpoilerRects(
		QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
		QVarLengthArray<FixedRange> &ranges) {
	if (ranges.empty()) {
		return;
	}
	auto lastTill = ranges.front().from.toInt() - 1;
	const auto y = _y + _yDelta;
	for (const auto &range : ranges) {
		auto from = range.from.toInt();
		auto till = range.till.toInt();
		if (from <= lastTill) {
			auto &last = rects.back();
			from = std::min(from, last.x());
			till = std::max(till, last.x() + last.width());
			last = { from, y, till - from, _fontHeight };
		} else {
			rects.push_back({ from, y, till - from, _fontHeight });
		}
		lastTill = till;
	}
	ranges.clear();
}

void Renderer::paintSpoilerRects() {
	Expects(_p != nullptr);

	if (!_spoiler) {
		return;
	}
	const auto opacity = _p->opacity();
	if (_spoilerOpacity < 1.) {
		_p->setOpacity(opacity * _spoilerOpacity);
	}
	const auto index = _spoiler->animation.index(now(), _paused);
	paintSpoilerRects(
		_spoilerRects,
		_palette->spoilerFg,
		index);
	paintSpoilerRects(
		_spoilerSelectedRects,
		_palette->selectSpoilerFg,
		index);
	if (_spoilerOpacity < 1.) {
		_p->setOpacity(opacity);
	}
}

void Renderer::paintSpoilerRects(
		const QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
		const style::color &color,
		int index) {
	if (rects.empty()) {
		return;
	}
	const auto frame = _spoilerCache->lookup(color->c)->frame(index);
	if (_spoilerCache) {
		for (const auto &rect : rects) {
			Ui::FillSpoilerRect(*_p, rect, frame, -rect.topLeft());
		}
	} else {
		// Show forgotten spoiler context part.
		for (const auto &rect : rects) {
			_p->fillRect(rect, Qt::red);
		}
	}
}

void Renderer::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 Renderer::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 Renderer::prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat) {
	_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 == TextBlockTCustomEmoji
				|| _type == TextBlockTSkip) {
				si.width = currentBlock->f_width() + currentBlock->f_rpadding();
			}
		}
		if (_type == TextBlockTEmoji
			|| _type == TextBlockTCustomEmoji
			|| _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 Renderer::restoreAfterElided() {
	if (_elideSavedBlock) {
		const_cast<String*>(_t)->_blocks[_elideSavedIndex] = std::move(*_elideSavedBlock);
	}
}

// COPIED FROM qtextengine.cpp AND MODIFIED
void Renderer::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;
}

void Renderer::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 Renderer::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 Renderer::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)
			) || (_palette && _palette->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 Renderer::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 == TextBlockTCustomEmoji
			|| _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 "[lookup-in-git]." has no word breaks
			// but the word "[lookup-too]" 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 Renderer::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
			|| _itype == TextBlockTCustomEmoji)
			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 Renderer::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 || _stype == TextBlockTCustomEmoji)
		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
			|| _itype == TextBlockTCustomEmoji)
			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;
}

void Renderer::applyBlockProperties(const AbstractBlock *block) {
	eSetFont(block);
	if (_p) {
		const auto isMono = IsMono(block->flags());
		_background = {};
		if (block->spoilerIndex() && _spoiler) {
			_background.spoiler = true;
		}
		if (isMono
			&& block->lnkIndex()
			&& (!_background.spoiler || _spoiler->revealed)) {
			_background.selectActiveBlock = ClickHandler::showAsPressed(
				_t->_links.at(block->lnkIndex() - 1));
		}

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

ClickHandlerPtr Renderer::lookupLink(const AbstractBlock *block) const {
	const auto spoilerLink = (_spoiler
		&& !_spoiler->revealed
		&& block->spoilerIndex())
		? _spoiler->link
		: ClickHandlerPtr();
	return (spoilerLink || !block->lnkIndex())
		? spoilerLink
		: _t->_links.at(block->lnkIndex() - 1);
}

} // namespace Ui::Text
