/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "tests/test_main.h"

#include "base/invoke_queued.h"
#include "base/integration.h"
#include "ui/effects/animations.h"
#include "ui/style/style_core.h"
#include "ui/text/text.h"
#include "ui/text/text_custom_emoji.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/rp_window.h"
#include "ui/painter.h"

#include <QApplication>
#include <QAbstractNativeEventFilter>
#include <QThread>
#include <QDir>
#include <QImage>

#include <algorithm>
#include <span>
#include <utility>

namespace Test {

namespace {

[[nodiscard]] bool HasEntityType(
		const EntitiesInText &entities,
		EntityType type) {
	for (const auto &entity : entities) {
		if (entity.type() == type) {
			return true;
		}
	}
	return false;
}

[[nodiscard]] QImage MakeObjectImage(
		QSize size,
		QColor background,
		const QString &label) {
	auto image = QImage(size, QImage::Format_ARGB32_Premultiplied);
	image.fill(Qt::transparent);
	auto painter = QPainter(&image);
	auto hq = PainterHighQualityEnabler(painter);
	painter.setPen(Qt::NoPen);
	painter.setBrush(background);
	painter.drawRoundedRect(
		QRect(QPoint(), size),
		scale(6),
		scale(6));
	painter.setPen(Qt::black);
	painter.drawText(QRect(QPoint(), size), Qt::AlignCenter, label);
	return image;
}

[[nodiscard]] int RenderTextPadding() {
	return scale(6);
}

[[nodiscard]] QImage RenderTextOffscreen(
		const Ui::Text::String &text,
		int availableWidth,
		std::optional<TextSelection> selection = std::nullopt,
		std::span<Ui::Text::SpecialColor> colors = {},
		std::optional<QPen> pen = std::nullopt) {
	const auto padding = RenderTextPadding();
	const auto image = QImage(
		QSize(
			std::max(text.maxWidth(), availableWidth) + (2 * padding),
			std::max(text.countHeight(availableWidth), text.minHeight())
				+ (2 * padding)),
		QImage::Format_ARGB32_Premultiplied);
	auto result = image;
	result.fill(Qt::transparent);
	auto painter = QPainter(&result);
	if (pen) {
		painter.setPen(*pen);
	}
	text.draw(painter, {
		.position = QPoint(padding, padding),
		.availableWidth = availableWidth,
		.colors = colors,
		.selection = selection.value_or(TextSelection()),
	});
	return result;
}

[[nodiscard]] std::optional<QRect> ChangedBoundsInRect(
		const QImage &first,
		const QImage &second,
		QRect rect) {
	if (first.size() != second.size()) {
		return std::nullopt;
	}
	rect = rect.intersected(QRect(QPoint(), first.size()));
	if (rect.isEmpty()) {
		return std::nullopt;
	}
	auto left = rect.right();
	auto top = rect.bottom();
	auto right = rect.left() - 1;
	auto bottom = rect.top() - 1;
	for (auto y = rect.top(); y <= rect.bottom(); ++y) {
		for (auto x = rect.left(); x <= rect.right(); ++x) {
			if (first.pixel(x, y) == second.pixel(x, y)) {
				continue;
			}
			left = std::min(left, x);
			top = std::min(top, y);
			right = std::max(right, x);
			bottom = std::max(bottom, y);
		}
	}
	return (right >= left) && (bottom >= top)
		? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom)))
		: std::nullopt;
}

[[nodiscard]] std::optional<QRect> SymbolHitBounds(
		const Ui::Text::String &text,
		int availableWidth,
		int offset) {
	if ((availableWidth <= 0) || (offset < 0)) {
		return std::nullopt;
	}
	const auto padding = RenderTextPadding();
	const auto height = std::max(
		text.countHeight(availableWidth),
		text.minHeight());
	auto flags = Ui::Text::StateRequest::Flags();
	flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
	auto request = Ui::Text::StateRequest();
	request.flags = flags;
	auto left = padding + availableWidth;
	auto top = padding + height;
	auto right = -1;
	auto bottom = -1;
	for (auto y = 0; y != height; ++y) {
		for (auto x = 0; x != availableWidth; ++x) {
			const auto hit = text.getState(QPoint(x, y), availableWidth, request);
			if (!hit.uponSymbol || (int(hit.symbol) != offset)) {
				continue;
			}
			left = std::min(left, x + padding);
			top = std::min(top, y + padding);
			right = std::max(right, x + padding);
			bottom = std::max(bottom, y + padding);
		}
	}
	return (right >= left) && (bottom >= top)
		? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom)))
		: std::nullopt;
}

[[nodiscard]] std::optional<QRect> SymbolRangeHitBounds(
		const Ui::Text::String &text,
		int availableWidth,
		int offset,
		int length) {
	if ((availableWidth <= 0) || (offset < 0) || (length <= 0)) {
		return std::nullopt;
	}
	const auto padding = RenderTextPadding();
	const auto height = std::max(
		text.countHeight(availableWidth),
		text.minHeight());
	auto flags = Ui::Text::StateRequest::Flags();
	flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
	auto request = Ui::Text::StateRequest();
	request.flags = flags;
	auto left = padding + availableWidth;
	auto top = padding + height;
	auto right = -1;
	auto bottom = -1;
	const auto end = offset + length;
	for (auto y = 0; y != height; ++y) {
		for (auto x = 0; x != availableWidth; ++x) {
			const auto hit = text.getState(QPoint(x, y), availableWidth, request);
			if (!hit.uponSymbol) {
				continue;
			}
			const auto symbol = int(hit.symbol);
			if ((symbol < offset) || (symbol >= end)) {
				continue;
			}
			left = std::min(left, x + padding);
			top = std::min(top, y + padding);
			right = std::max(right, x + padding);
			bottom = std::max(bottom, y + padding);
		}
	}
	return (right >= left) && (bottom >= top)
		? std::make_optional(QRect(QPoint(left, top), QPoint(right, bottom)))
		: std::nullopt;
}

[[nodiscard]] bool ImagesEqualInRect(
		const QImage &first,
		const QImage &second,
		QRect rect) {
	return !ChangedBoundsInRect(first, second, rect).has_value();
}

[[nodiscard]] bool HasPixelColorInRect(
		const QImage &image,
		QRect rect,
		QRgb color) {
	rect = rect.intersected(QRect(QPoint(), image.size()));
	if (rect.isEmpty()) {
		return false;
	}
	for (auto y = rect.top(); y <= rect.bottom(); ++y) {
		for (auto x = rect.left(); x <= rect.right(); ++x) {
			if (image.pixel(x, y) == color) {
				return true;
			}
		}
	}
	return false;
}

[[nodiscard]] bool HasPaintedPixels(const QImage &image) {
	const auto bits = image.constBits();
	if (!bits) {
		return false;
	}
	const auto count = image.sizeInBytes();
	for (auto i = qsizetype(0); i != count; ++i) {
		if (bits[i] != 0) {
			return true;
		}
	}
	return false;
}

class FormulaLikeObject final : public Ui::Text::CustomEmoji {
public:
	FormulaLikeObject(QString entityData, QString replacementText, QImage image);

	int width() override;
	QString entityData() override;
	std::optional<Ui::Text::CustomEmojiVerticalMetrics> vertical(
		const style::TextStyle &) override;
	QString replacementText() override;
	Ui::Text::CustomEmojiSemantics semantics() override;
	void paint(QPainter &p, const Context &context) override;
	void unload() override;
	bool ready() override;
	bool readyInDefaultState() override;

private:
	QString _entityData;
	QString _replacementText;
	QImage _image;

};

FormulaLikeObject::FormulaLikeObject(
	QString entityData,
	QString replacementText,
	QImage image)
: _entityData(std::move(entityData))
, _replacementText(std::move(replacementText))
, _image(std::move(image)) {
}

int FormulaLikeObject::width() {
	return _image.width();
}

QString FormulaLikeObject::entityData() {
	return _entityData;
}

std::optional<Ui::Text::CustomEmojiVerticalMetrics>
FormulaLikeObject::vertical(const style::TextStyle &) {
	const auto height = _image.height();
	const auto descent = std::max(height / 5, 1);
	return Ui::Text::CustomEmojiVerticalMetrics{
		.ascent = height - descent,
		.descent = descent,
	};
}

QString FormulaLikeObject::replacementText() {
	return _replacementText;
}

Ui::Text::CustomEmojiSemantics FormulaLikeObject::semantics() {
	return {
		.isEmoji = false,
		.isRealCustomEmoji = false,
		.exportEntity = false,
		.unloadPersistentAnimation = false,
		.allowCustomEmojiClick = false,
	};
}

void FormulaLikeObject::paint(QPainter &p, const Context &context) {
	p.drawImage(context.position, _image);
}

void FormulaLikeObject::unload() {
}

bool FormulaLikeObject::ready() {
	return true;
}

bool FormulaLikeObject::readyInDefaultState() {
	return true;
}

} // namespace

QString name() {
	return u"text"_q;
}

void test(not_null<Ui::RpWindow*> window, not_null<Ui::RpWidget*> body) {
	(void)window;

	const auto formulaEntityData = u"test-formula-like-object"_q;
	const auto formulaReplacementText = u"$\\frac{a}{b}$"_q;
	const auto controlEntityData = u"test-custom-emoji"_q;
	const auto formulaImage = MakeObjectImage(
		QSize(scale(64), scale(28)),
		QColor(32, 96, 192, 48),
		u"a / b"_q);
	const auto controlImage = MakeObjectImage(
		QSize(scale(24), scale(24)),
		QColor(224, 176, 32, 160),
		u"*"_q);

	auto context = Ui::Text::MarkedContext();
	context.customEmojiFactory = [
		formulaEntityData,
		formulaReplacementText,
		formulaImage,
		controlImage
	](QStringView data, const Ui::Text::MarkedContext &)
	-> std::unique_ptr<Ui::Text::CustomEmoji> {
		if (data == formulaEntityData) {
			return std::make_unique<FormulaLikeObject>(
				data.toString(),
				formulaReplacementText,
				formulaImage);
		}
		if (data == u"test-custom-emoji"_q) {
			return std::make_unique<Ui::Text::PaletteDependentCustomEmoji>(
				[controlImage] {
					return controlImage;
				},
				data.toString());
		}
		return std::unique_ptr<Ui::Text::CustomEmoji>();
	};

	auto formulaData = TextWithEntities();
	formulaData.append(u"Alpha "_q);
	const auto formulaPosition = formulaData.text.size();
	formulaData.append(QChar::ObjectReplacementCharacter);
	formulaData.entities.push_back(EntityInText(
		EntityType::CustomEmoji,
		formulaPosition,
		1,
		formulaEntityData));
	formulaData.append(u" omega"_q);

	const auto formulaText = new Ui::Text::String(scale(64));
	formulaText->setMarkedText(
		st::defaultTextStyle,
		formulaData,
		kMarkupTextOptions,
		context);

	const auto formulaCollapsedText = Ui::Text::String(
		st::defaultTextStyle,
		u"Alpha   omega"_q,
		kMarkupTextOptions,
		scale(64));
	const auto objectPlaceholderWidth = st::defaultTextStyle.font->width(u" "_q);
	Expects(
		formulaText->maxWidth()
			>= formulaCollapsedText.maxWidth()
				- objectPlaceholderWidth
				+ formulaImage.width());

	const auto expectedFormulaExport = u"Alpha $\\frac{a}{b}$ omega"_q;
	Expects(formulaText->toString() == expectedFormulaExport);
	const auto formulaMime = formulaText->toTextForMimeData();
	Expects(formulaMime.expanded == expectedFormulaExport);
	Expects(formulaMime.rich.text == expectedFormulaExport);
	Expects(!HasEntityType(formulaMime.rich.entities, EntityType::CustomEmoji));
	const auto formulaRich = formulaText->toTextWithEntities();
	Expects(formulaRich.text == expectedFormulaExport);
	Expects(!HasEntityType(formulaRich.entities, EntityType::CustomEmoji));
	Expects(
		formulaText->adjustSelection(
			TextSelection(uint16(formulaPosition), uint16(formulaPosition)),
			TextSelectType::Words)
		== TextSelection(
			uint16(formulaPosition),
			uint16(formulaPosition + 1)));
	Expects(
		formulaText->adjustSelection(
			TextSelection(uint16(formulaPosition), uint16(formulaPosition + 1)),
			TextSelectType::Paragraphs)
		== TextSelection(
			uint16(formulaPosition),
			uint16(formulaPosition + 1)));
	Expects(!formulaText->hasCustomEmoji());
	Expects(!formulaText->isOnlyCustomEmoji());
	Expects(!formulaText->isIsolatedEmoji());

	auto controlData = TextWithEntities();
	controlData.append(QChar::ObjectReplacementCharacter);
	controlData.entities.push_back(EntityInText(
		EntityType::CustomEmoji,
		0,
		1,
		controlEntityData));

	const auto controlText = new Ui::Text::String(scale(64));
	controlText->setMarkedText(
		st::defaultTextStyle,
		controlData,
		kMarkupTextOptions,
		context);
	Expects(controlText->hasCustomEmoji());
	Expects(controlText->isOnlyCustomEmoji());
	Expects(controlText->isIsolatedEmoji());
	Expects(
		HasEntityType(
			controlText->toTextWithEntities().entities,
			EntityType::CustomEmoji));
	const auto controlProbeText = u"a / b"_q;
	const auto controlLineSource = u"Alpha a / b omega"_q;
	const auto controlProbePosition = controlLineSource.indexOf(controlProbeText);
	Expects(controlProbePosition >= 0);
	const auto controlLineText = Ui::Text::String(
		st::defaultTextStyle,
		controlLineSource,
		kMarkupTextOptions,
		scale(64));
	const auto selectionWidth = std::max(
		formulaText->maxWidth(),
		controlLineText.maxWidth());
	const auto formulaLinesGeometry = formulaText->countLinesGeometry(
		selectionWidth);
	const auto controlLinesGeometry = controlLineText.countLinesGeometry(
		selectionWidth);
	Expects(formulaLinesGeometry.size() == 1);
	Expects(controlLinesGeometry.size() == 1);
	if ((formulaLinesGeometry.size() == 1)
		&& (controlLinesGeometry.size() == 1)) {
		const auto &formulaLine = formulaLinesGeometry.front();
		const auto &controlLine = controlLinesGeometry.front();
		Expects(formulaLine.baseline > controlLine.baseline);
		Expects(formulaLine.baseline < formulaLine.bottom);
		Expects(controlLine.baseline < controlLine.bottom);
	}
	const auto formulaSelection = TextSelection(
		0,
		uint16(formulaData.text.size()));
	const auto formulaUnselected = RenderTextOffscreen(
		*formulaText,
		selectionWidth);
	const auto formulaSelected = RenderTextOffscreen(
		*formulaText,
		selectionWidth,
		formulaSelection);
	const auto formulaHitBounds = SymbolHitBounds(
		*formulaText,
		selectionWidth,
		formulaPosition);
	Expects(formulaHitBounds.has_value());
	const auto controlSelection = TextSelection(
		0,
		uint16(controlLineSource.size()));
	const auto controlUnselected = RenderTextOffscreen(
		controlLineText,
		selectionWidth);
	const auto controlSelected = RenderTextOffscreen(
		controlLineText,
		selectionWidth,
		controlSelection);
	const auto controlHitBounds = SymbolRangeHitBounds(
		controlLineText,
		selectionWidth,
		controlProbePosition,
		controlProbeText.size());
	Expects(controlHitBounds.has_value());
	if (formulaHitBounds && controlHitBounds) {
		const auto formulaChangedBounds = ChangedBoundsInRect(
			formulaUnselected,
			formulaSelected,
			QRect(
				formulaHitBounds->x(),
				0,
				formulaHitBounds->width(),
				formulaSelected.height()));
		const auto controlChangedBounds = ChangedBoundsInRect(
			controlUnselected,
			controlSelected,
			QRect(
				controlHitBounds->x(),
				0,
				controlHitBounds->width(),
				controlSelected.height()));
		Expects(formulaChangedBounds.has_value());
		Expects(controlChangedBounds.has_value());
		if (formulaChangedBounds && controlChangedBounds) {
			Expects(
				formulaChangedBounds->top()
					<= controlChangedBounds->top());
			Expects(
				formulaChangedBounds->bottom()
					>= controlChangedBounds->bottom());
			Expects(
				(formulaChangedBounds->top()
					< controlChangedBounds->top())
				|| (formulaChangedBounds->bottom()
					> controlChangedBounds->bottom()));
		}
	}

	auto leadingFormulaData = TextWithEntities();
	leadingFormulaData.append(QChar::ObjectReplacementCharacter);
	leadingFormulaData.entities.push_back(EntityInText(
		EntityType::CustomEmoji,
		0,
		1,
		formulaEntityData));
	const auto leadingFormulaText = Ui::Text::String(
		st::defaultTextStyle,
		leadingFormulaData,
		kMarkupTextOptions,
		scale(32),
		context);
	const auto leadingFormulaRender = RenderTextOffscreen(
		leadingFormulaText,
		std::max(formulaImage.width() / 2, 1));
	Expects(HasPaintedPixels(leadingFormulaRender));

	auto skipOnlyText = Ui::Text::String(
		st::defaultTextStyle,
		u""_q,
		kDefaultTextOptions,
		scale(32));
	const auto skipBlockWidth = scale(36);
	const auto skipBlockHeight = scale(7);
	Expects(skipOnlyText.updateSkipBlock(skipBlockWidth, skipBlockHeight));
	Expects(skipOnlyText.countHeight(skipBlockWidth * 2) == skipBlockHeight);
	Expects(
		skipOnlyText.countDimensions(Ui::Text::SimpleGeometry(
			skipBlockWidth * 2,
			0,
			0,
			false)).height == skipBlockHeight);

	const auto colorizedText = u"Colorized link span"_q;
	const auto colorizedWidth = scale(280);
	const auto plainColorizedText = Ui::Text::String(
		st::defaultTextStyle,
		colorizedText,
		kMarkupTextOptions,
		scale(96));
	const auto emptyColorizedText = Ui::Text::String(
		st::defaultTextStyle,
		Ui::Text::Colorized(colorizedText),
		kMarkupTextOptions,
		scale(96));
	const auto emptyColorizedRender = RenderTextOffscreen(
		emptyColorizedText,
		colorizedWidth);
	const auto colorizedBoundsRect = QRect(
		QPoint(),
		emptyColorizedRender.size());
	const auto linkPenRender = RenderTextOffscreen(
		plainColorizedText,
		colorizedWidth,
		std::nullopt,
		{},
		st::defaultTextPalette.linkFg->p);
	const auto plainColorizedRender = RenderTextOffscreen(
		plainColorizedText,
		colorizedWidth);
	Expects(ImagesEqualInRect(
		emptyColorizedRender,
		linkPenRender,
		colorizedBoundsRect));
	Expects(!ImagesEqualInRect(
		emptyColorizedRender,
		plainColorizedRender,
		colorizedBoundsRect));

	const auto &specialFg = st::defaultTextPalette.linkFg;
	const auto &specialBg = st::defaultTextPalette.markBg;
	auto specialColors = std::vector<Ui::Text::SpecialColor>{
		Ui::Text::SpecialColor{
			&specialFg->p,
			&specialFg->p,
			&specialBg->b,
			&specialBg->b },
	};
	const auto explicitColorizedNoBgText = Ui::Text::String(
		st::defaultTextStyle,
		Ui::Text::Colorized(colorizedText, 1, 0),
		kMarkupTextOptions,
		scale(96));
	const auto explicitColorizedBgText = Ui::Text::String(
		st::defaultTextStyle,
		Ui::Text::Colorized(colorizedText, 1, 1),
		kMarkupTextOptions,
		scale(96));
	const auto explicitColorizedNoBgRender = RenderTextOffscreen(
		explicitColorizedNoBgText,
		colorizedWidth,
		std::nullopt,
		specialColors);
	const auto explicitColorizedBgRender = RenderTextOffscreen(
		explicitColorizedBgText,
		colorizedWidth,
		std::nullopt,
		specialColors);
	const auto specialPenRender = RenderTextOffscreen(
		plainColorizedText,
		colorizedWidth,
		std::nullopt,
		{},
		specialFg->p);
	const auto colorizedSymbolBounds = SymbolRangeHitBounds(
		explicitColorizedBgText,
		colorizedWidth,
		0,
		colorizedText.size());
	Expects(ImagesEqualInRect(
		explicitColorizedNoBgRender,
		specialPenRender,
		colorizedBoundsRect));
	Expects(colorizedSymbolBounds.has_value());
	if (colorizedSymbolBounds) {
		Expects(!ImagesEqualInRect(
			explicitColorizedBgRender,
			explicitColorizedNoBgRender,
			*colorizedSymbolBounds));
		Expects(HasPixelColorInRect(
			explicitColorizedBgRender,
			*colorizedSymbolBounds,
			specialBg->c.rgba()));
		Expects(!HasPixelColorInRect(
			explicitColorizedNoBgRender,
			*colorizedSymbolBounds,
			specialBg->c.rgba()));
	}

	body->paintRequest() | rpl::on_next([=](QRect clip) {
		auto p = QPainter(body);
		p.fillRect(clip, QColor(255, 255, 255));
		const auto left = scale(24);
		const auto top = scale(24);
		const auto width = body->width() - (2 * left);
		formulaText->draw(p, {
			.position = QPoint(left, top),
			.availableWidth = width,
		});
		controlText->draw(p, {
			.position = QPoint(
				left,
				top + formulaText->countHeight(width) + scale(20)),
			.availableWidth = width,
		});
	}, body->lifetime());
}

} // namespace Test
