/*
 * Oak Video Editor - Non-Linear Video Editor
 * Copyright (C) 2025 Olive CE Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "html.h"

#include <QDebug>
#include <QTextBlock>

#include "xmlutils.h"

namespace olive
{

const QVector<QString> Html::kBlockTags = { QStringLiteral("p"),
											QStringLiteral("div") };

inline bool StrEquals(const QStringView &a, const QStringView &b)
{
	return !a.compare(b, Qt::CaseInsensitive);
}

QString Html::DocToHtml(const QTextDocument *doc)
{
	QString html;
	QXmlStreamWriter writer(&html);

	//writer.setAutoFormatting(true);

	for (auto it = doc->begin(); it != doc->end(); it = it.next()) {
		WriteBlock(&writer, it);
	}

	return html;
}

struct HtmlNode {
	QString tag;
	QTextCharFormat format;
};

QTextCharFormat MergeHtmlFormats(const QVector<HtmlNode> &stack)
{
	QTextCharFormat f;

	for (int i = 0; i < stack.size(); i++) {
		f.merge(stack.at(i).format);
	}

	return f;
}

void Html::HtmlToDoc(QTextDocument *doc, const QString &html)
{
	// Empty doc
	doc->clear();
	bool inside_block = true;

	// Create cursor, which appears to be Qt's official way of inserting blocks and fragments
	QTextCursor c(doc);

	QString wrapped = QStringLiteral("<html>").append(html).append("</html>");
	QXmlStreamReader reader(wrapped);

	QVector<HtmlNode> fmt_stack;

	QTextCharFormat default_fmt;
	default_fmt.setFontWeight(QFont::Normal);
	fmt_stack.append({ QStringLiteral("html"), default_fmt });

	QTextCharFormat current_fmt;

	while (!reader.atEnd()) {
		reader.readNext();

		if (reader.tokenType() == QXmlStreamReader::StartElement) {
			QString tag = reader.name().toString().toLower();

			fmt_stack.append({ tag, ReadCharFormat(reader.attributes()) });
			current_fmt = MergeHtmlFormats(fmt_stack);

			if (kBlockTags.contains(tag)) {
				QTextBlockFormat block_fmt =
					ReadBlockFormat(reader.attributes());
				if (inside_block) {
					c.setBlockFormat(block_fmt);
					c.setBlockCharFormat(current_fmt);
				} else {
					c.insertBlock(block_fmt, current_fmt);
					inside_block = true;
				}
			}

		} else if (reader.tokenType() == QXmlStreamReader::Characters) {
			QString characters = reader.text().toString();
			c.insertText(characters, current_fmt);

		} else if (reader.tokenType() == QXmlStreamReader::EndElement) {
			QString tag = reader.name().toString().toLower();

			for (int i = fmt_stack.size() - 1; i >= 0; i--) {
				if (fmt_stack.at(i).tag == tag) {
					fmt_stack.removeAt(i);
					current_fmt = MergeHtmlFormats(fmt_stack);

					if (kBlockTags.contains(tag)) {
						inside_block = false;
					}
					break;
				}
			}
		}
	}

	if (reader.error()) {
		qCritical() << "Failed to parse HTML:" << reader.errorString();
	}
}

void Html::WriteBlock(QXmlStreamWriter *writer, const QTextBlock &block)
{
	writer->writeStartElement(QStringLiteral("p"));

	const QTextBlockFormat &fmt = block.blockFormat();

	// Write block alignment
	if (!(fmt.alignment() & Qt::AlignLeft)) {
		if (fmt.alignment() & Qt::AlignRight) {
			writer->writeAttribute(QStringLiteral("align"),
								   QStringLiteral("right"));
		} else if (fmt.alignment() & Qt::AlignHCenter) {
			writer->writeAttribute(QStringLiteral("align"),
								   QStringLiteral("center"));
		} else if (fmt.alignment() & Qt::AlignJustify) {
			writer->writeAttribute(QStringLiteral("align"),
								   QStringLiteral("justify"));
		}
	}

	// RTL support
	if (block.textDirection() == Qt::RightToLeft) {
		writer->writeAttribute(QStringLiteral("dir"), QStringLiteral("rtl"));
	}

	// Write CSS attributes
	QString style;

	if (fmt.lineHeightType() != QTextBlockFormat::SingleHeight) {
		WriteCSSProperty(&style, QStringLiteral("line-height"),
						 QStringLiteral("%1%").arg(fmt.lineHeight()));
	}

	WriteCharFormat(&style, block.charFormat());

	if (!style.isEmpty()) {
		writer->writeAttribute(QStringLiteral("style"), style);
	}

	auto it = block.begin();

	if (it != block.end()) {
		for (; it != block.end(); it++) {
			WriteFragment(writer, it.fragment());
		}
	}

	writer->writeEndElement(); // p
}

void Html::WriteFragment(QXmlStreamWriter *writer,
						 const QTextFragment &fragment)
{
	const QTextCharFormat &fmt = fragment.charFormat();

	writer->writeStartElement(QStringLiteral("span"));

	// Write CSS attributes
	QString style;

	WriteCharFormat(&style, fmt);

	if (!style.isEmpty()) {
		writer->writeAttribute(QStringLiteral("style"), style);
	}

	QStringList lines = fragment.text().split(QChar::LineSeparator);
	bool first_line = true;
	foreach (const QString &l, lines) {
		if (first_line) {
			first_line = false;
		} else {
			writer->writeEmptyElement(QStringLiteral("br"));
		}
		writer->writeCharacters(l);
	}

	writer->writeEndElement(); // span
}

void Html::WriteCSSProperty(QString *style, const QString &key,
							const QStringList &values)
{
	QString value;
	foreach (QString v, values) {
		if (v.contains(' ')) {
			v = QStringLiteral("'%1'").arg(v);
		}

		AppendStringAutoSpace(&value, v);
	}

	AppendStringAutoSpace(style, QStringLiteral("%1: %2;").arg(key, value));
}

void Html::WriteCharFormat(QString *style, const QTextCharFormat &fmt)
{
	QStringList families = fmt.fontFamilies().toStringList();
	if (!families.isEmpty()) {
		WriteCSSProperty(style, QStringLiteral("font-family"),
						 families.first());
	}

	if (fmt.hasProperty(QTextFormat::FontPointSize)) {
		WriteCSSProperty(
			style, QStringLiteral("font-size"),
			QStringLiteral("%1pt").arg(QString::number(fmt.fontPointSize())));
	}

	if (fmt.hasProperty(QTextFormat::FontWeight)) {
		WriteCSSProperty(style, QStringLiteral("font-weight"),
						 QString::number(fmt.fontWeight() * 8));
	}

	if (fmt.hasProperty(QTextFormat::FontItalic)) {
		WriteCSSProperty(style, QStringLiteral("font-style"),
						 fmt.fontItalic() ? QStringLiteral("italic") :
											QStringLiteral("normal"));
	}

	if (fmt.hasProperty(QTextFormat::FontStyleName)) {
		WriteCSSProperty(style, QStringLiteral("-ove-font-style"),
						 fmt.fontStyleName().toString());
	}

	QStringList deco;

	if (fmt.fontUnderline()) {
		deco.append(QStringLiteral("underline"));
	}

	if (fmt.fontStrikeOut()) {
		deco.append(QStringLiteral("line-through"));
	}

	if (fmt.fontOverline()) {
		deco.append(QStringLiteral("overline"));
	}

	if (!deco.isEmpty()) {
		WriteCSSProperty(style, QStringLiteral("text-decoration"), deco);
	}

	if (fmt.foreground().style() != Qt::NoBrush) {
		const QColor &color = fmt.foreground().color();
		QString cs;

		if (color.alpha() == 255) {
			cs = color.name();
		} else if (color.alpha()) {
			cs = QStringLiteral("rgba(%1, %2, %3, %4)")
					 .arg(QString::number(color.red()),
						  QString::number(color.green()),
						  QString::number(color.blue()),
						  QString::number(color.alphaF()));
		}

		WriteCSSProperty(style, QStringLiteral("color"), cs);
	}

	if (fmt.fontCapitalization() != QFont::MixedCase) {
		if (fmt.fontCapitalization() == QFont::SmallCaps) {
			WriteCSSProperty(style, QStringLiteral("font-variant"),
							 QStringLiteral("small-caps"));
			// TODO: Add others
		}
	}

	if (fmt.fontLetterSpacing() != 0.0) {
		WriteCSSProperty(style, QStringLiteral("letter-spacing"),
						 QStringLiteral("%1%").arg(
							 QString::number(fmt.fontLetterSpacing())));
	}

	if (fmt.fontStretch() != 0) {
		WriteCSSProperty(
			style, QStringLiteral("font-stretch"),
			QStringLiteral("%1%").arg(QString::number(fmt.fontStretch())));
	}
}

QTextCharFormat Html::ReadCharFormat(const QXmlStreamAttributes &attributes)
{
	QTextCharFormat fmt;

	foreach (const QXmlStreamAttribute &attr, attributes) {
		if (StrEquals(attr.name(), QStringLiteral("style"))) {
			auto css = GetCSSFromStyle(attr.value().toString());

			for (auto it = css.begin(); it != css.end(); it++) {
				const QString &first_val = it.value().first();

				if (it.key() == QStringLiteral("font-family")) {
					fmt.setFontFamilies({ first_val });
				} else if (it.key() == QStringLiteral("font-size")) {
					if (first_val.endsWith(QStringLiteral("pt"),
										   Qt::CaseInsensitive)) {
						fmt.setFontPointSize(first_val.chopped(2).toDouble());
					}
				} else if (it.key() == QStringLiteral("font-weight")) {
					fmt.setFontWeight(first_val.toInt() / 8);
				} else if (it.key() == QStringLiteral("font-style")) {
					fmt.setFontItalic(
						StrEquals(first_val, QStringLiteral("italic")));
				} else if (it.key() == QStringLiteral("text-decoration")) {
					foreach (const QString &v, it.value()) {
						if (StrEquals(v, QStringLiteral("underline"))) {
							fmt.setFontUnderline(true);
						} else if (StrEquals(v,
											 QStringLiteral("line-through"))) {
							fmt.setFontStrikeOut(true);
						} else if (StrEquals(v, QStringLiteral("overline"))) {
							fmt.setFontOverline(true);
						}
					}
				} else if (it.key() == QStringLiteral("color")) {
					if (first_val.startsWith(QStringLiteral("rgba"),
											 Qt::CaseInsensitive)) {
						QString vals_only = first_val;
						vals_only.remove(QStringLiteral("rgba"));
						vals_only.remove(QStringLiteral("("));
						vals_only.remove(QStringLiteral(")"));
						QStringList rgba = vals_only.split(',');
						if (rgba.size() == 4) {
							QColor c;
							c.setRedF(rgba.at(0).toDouble());
							c.setGreenF(rgba.at(1).toDouble());
							c.setBlueF(rgba.at(2).toDouble());
							c.setAlphaF(rgba.at(3).toDouble());
							fmt.setForeground(c);
						}
					} else {
						fmt.setForeground(QColor(first_val));
					}
				} else if (it.key() == QStringLiteral("font-variant")) {
					if (StrEquals(first_val, QStringLiteral("small-caps"))) {
						fmt.setFontCapitalization(QFont::SmallCaps);
					}
				} else if (it.key() == QStringLiteral("letter-spacing")) {
					if (first_val.contains(QChar('%'))) {
						fmt.setFontLetterSpacing(
							first_val.chopped(1).toDouble());
					}
				} else if (it.key() == QStringLiteral("font-stretch")) {
					if (first_val.contains(QChar('%'))) {
						fmt.setFontStretch(first_val.chopped(1).toInt());
					}
				} else if (it.key() == QStringLiteral("-ove-font-style")) {
					fmt.setFontStyleName(first_val);
				}
			}
		}
	}
	return fmt;
}

QTextBlockFormat Html::ReadBlockFormat(const QXmlStreamAttributes &attributes)
{
	QTextBlockFormat block_fmt;

	foreach (const QXmlStreamAttribute &attr, attributes) {
		if (StrEquals(attr.name(), QStringLiteral("align"))) {
			if (StrEquals(attr.value(), QStringLiteral("right"))) {
				block_fmt.setAlignment(Qt::AlignRight);
			} else if (StrEquals(attr.value(), QStringLiteral("center"))) {
				block_fmt.setAlignment(Qt::AlignHCenter);
			} else if (StrEquals(attr.value(), QStringLiteral("justify"))) {
				block_fmt.setAlignment(Qt::AlignJustify);
			}
		} else if (StrEquals(attr.name(), QStringLiteral("dir"))) {
			if (StrEquals(attr.value(), QStringLiteral("rtl"))) {
				block_fmt.setLayoutDirection(Qt::RightToLeft);
			}
		} else if (StrEquals(attr.name(), QStringLiteral("style"))) {
			auto css = GetCSSFromStyle(attr.value().toString());

			for (auto it = css.begin(); it != css.end(); it++) {
				if (it.key() == QStringLiteral("line-height")) {
					const QString &first_val = it.value().constFirst();
					if (first_val.contains(QChar('%'))) {
						block_fmt.setLineHeight(
							first_val.chopped(1).toDouble(),
							QTextBlockFormat::ProportionalHeight);
					}
				}
			}
		}
	}

	return block_fmt;
}

void Html::AppendStringAutoSpace(QString *s, const QString &append)
{
	if (!s->isEmpty()) {
		s->append(QChar(' '));
	}

	s->append(append);
}

QMap<QString, QStringList> Html::GetCSSFromStyle(const QString &s)
{
	QMap<QString, QStringList> map;

	QStringList list = s.split(QChar(';'));

	foreach (const QString &a, list) {
		QStringList kv = a.split(QChar(':'));

		if (kv.size() != 2) {
			continue;
		}

		// I'm sure there's regex that could do this, but I couldn't figure it out. It needs to split
		// by space EXCEPT within quotes OR double-quotes, and said quotes should be EXCLUDED from each
		// match. Also commas should be filtered out.
		QStringList values;
		const QString &val = kv.at(1);
		QChar in_quote(0);
		QString current_str;
		for (int i = 0; i < val.size(); i++) {
			const QChar &current_char = val.at(i);

			if (!in_quote.isNull()) {
				// If inside quotes and character isn't quote, indiscriminately append char
				if (current_char == in_quote) {
					in_quote = QChar(0);
				} else {
					current_str.append(current_char);
				}
			} else if (current_char.isSpace() || current_char == QChar(',')) {
				// Dump current
				if (!current_str.isEmpty()) {
					values.append(current_str);
					current_str.clear();
				}
			} else if (in_quote.isNull() && (current_char == QChar('\'') ||
											 current_char == QChar('"'))) {
				in_quote = current_char;
			} else {
				current_str.append(current_char);
			}
		}

		if (!current_str.isEmpty()) {
			values.append(current_str);
		}

		// Not sure if this will ever happen, but just in case, we will avoid assert failures with this
		if (values.isEmpty()) {
			values.append(QString());
		}

		map[kv.at(0).trimmed().toLower()] = values;
	}

	return map;
}

}
