// SPDX-License-Identifier: GPL-2.0
#include <QDir>
#include <QFile>
#include <QFileDevice>
#include <QRegularExpression>
#include <QTextStream>

#include "templatelayout.h"
#include "mainwindow.h"
#include "printoptions.h"
#include "core/divelist.h"
#include "core/selection.h"
#include "core/qthelper.h"
#include "core/string-format.h"

QList<QString> grantlee_templates, grantlee_statistics_templates;

int getTotalWork(const print_options &printOptions)
{
	if (printOptions.print_selected) {
		// return the correct number depending on all/selected dives
		// but don't return 0 as we might divide by this number
		return amount_selected && !in_planner() ? amount_selected : 1;
	}
	return dive_table.nr;
}

void find_all_templates()
{
	const QLatin1String ext(".html");
	grantlee_templates.clear();
	grantlee_statistics_templates.clear();
	QDir dir(getPrintingTemplatePathUser());
	const QStringList list = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
	for (const QString &filename: list) {
		if (filename.at(filename.size() - 1) != '~' && filename.endsWith(ext))
			grantlee_templates.append(filename);
	}

	// find statistics templates
	dir.setPath(getPrintingTemplatePathUser() + QDir::separator() + "statistics");
	const QStringList stat = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
	for (const QString &filename: stat) {
		if (filename.at(filename.size() - 1) != '~' && filename.endsWith(ext))
			grantlee_statistics_templates.append(filename);
	}
}

/* find templates which are part of the bundle in the user path
 * and set them as read only.
 */
void set_bundled_templates_as_read_only()
{
	QDir dir;
	const QString stats("statistics");
	QStringList list, listStats;
	QString pathBundle = getPrintingTemplatePathBundle();
	QString pathUser = getPrintingTemplatePathUser();

	dir.setPath(pathBundle);
	list = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
	dir.setPath(pathBundle + QDir::separator() + stats);
	listStats = dir.entryList(QDir::Files | QDir::NoDotAndDotDot);
	for (int i = 0; i < listStats.length(); i++)
		listStats[i] = stats + QDir::separator() + listStats.at(i);
	list += listStats;

	foreach (const QString& f, list)
		QFile::setPermissions(pathUser + QDir::separator() + f, QFileDevice::ReadOwner | QFileDevice::ReadUser);
}

void copy_bundled_templates(QString src, QString dst, QStringList *templateBackupList)
{
	QDir dir(src);
	if (!dir.exists())
		return;
	const auto dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
	for (const QString &d: dirs) {
		QString dst_path = dst + QDir::separator() + d;
		dir.mkpath(dst_path);
		copy_bundled_templates(src + QDir::separator() + d, dst_path, templateBackupList);
	}
	const auto files = dir.entryList(QDir::Files);
	for (const QString &f: files) {
		QFile fileSrc(src + QDir::separator() + f);
		QFile fileDest(dst + QDir::separator() + f);
		if (fileDest.exists()) {
			// if open() fails the file is either locked or r/o. try to remove it and then overwrite
			if (!fileDest.open(QFile::ReadWrite | QFile::Text)) {
				fileDest.setPermissions(QFileDevice::WriteOwner | QFileDevice::WriteUser);
				fileDest.remove();
			} else { // if the file is not read-only create a backup
				fileDest.close();
				const QString targetFile = fileDest.fileName().replace(".html", "-User.html");
				fileDest.copy(targetFile);
				*templateBackupList << targetFile;
			}
		}
		fileSrc.copy(fileDest.fileName()); // in all cases copy the file
	}
}

TemplateLayout::TemplateLayout(const print_options &printOptions, const template_options &templateOptions) :
	printOptions(printOptions), templateOptions(templateOptions)
{
}

QString TemplateLayout::generate()
{
	int progress = 0;
	int totalWork = getTotalWork(printOptions);

	QString htmlContent;

	State state;

	struct dive *dive;
	if (in_planner()) {
		state.dives.append(&displayed_dive);
		emit progressUpdated(100.0);
	} else {
		int i;
		for_each_dive (i, dive) {
			//TODO check for exporting selected dives only
			if (!dive->selected && printOptions.print_selected)
				continue;
			state.dives.append(dive);
			progress++;
			emit progressUpdated(lrint(progress * 100.0 / totalWork));
		}
	}

	QString templateContents = readTemplate(printOptions.p_template);

	QList<token> tokens = lexer(templateContents);
	QString buffer;
	QTextStream out(&buffer);
	parser(tokens, 0, tokens.size(), out, state);
	htmlContent = out.readAll();
	return htmlContent;
}

QString TemplateLayout::generateStatistics()
{
	QString htmlContent;

	State state;

	int i = 0;
	stats_summary_auto_free stats;
	calculate_stats_summary(&stats, false);
	while (stats.stats_yearly != NULL && stats.stats_yearly[i].period) {
		state.years.append(&stats.stats_yearly[i]);
		i++;
	}

	QString templateFile = QString("statistics") + QDir::separator() + printOptions.p_template;
	QString templateContents = readTemplate(templateFile);

	QList<token> tokens = lexer(templateContents);
	QString buffer;
	QTextStream out(&buffer);
	parser(tokens, 0, tokens.size(), out, state);
	htmlContent = out.readAll();
	return htmlContent;
}

QString TemplateLayout::readTemplate(QString template_name)
{
	QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name);
	if (qfile.open(QFile::ReadOnly | QFile::Text)) {
		QTextStream in(&qfile);
		return in.readAll();
	}
	return "";
}

void TemplateLayout::writeTemplate(QString template_name, QString grantlee_template)
{
	QFile qfile(getPrintingTemplatePathUser() + QDir::separator() + template_name);
	if (qfile.open(QFile::ReadWrite | QFile::Text)) {
		qfile.write(qPrintable(grantlee_template));
		qfile.resize(qfile.pos());
		qfile.close();
	}
}

struct token stringToken(QString s)
{
	struct token newtoken;
	newtoken.type = LITERAL;
	newtoken.contents = s;
	return newtoken;
}

static QRegularExpression keywordFor(R"(\bfor\b)");
static QRegularExpression keywordEndfor(R"(\bendfor\b)");
static QRegularExpression keywordBlock(R"(\bblock\b)");
static QRegularExpression keywordEndblock(R"(\bendblock\b)");
static QRegularExpression keywordIf(R"(\bif\b)");
static QRegularExpression keywordEndif(R"(\bendif\b)");

struct token operatorToken(QString s)
{
	struct token newtoken;

	QRegularExpressionMatch match = keywordFor.match(s);
	if (match.hasMatch()) {
		newtoken.type = FORSTART;
		newtoken.contents = s.mid(match.capturedEnd());
		return newtoken;
	}
	match = keywordEndfor.match(s);
	if (match.hasMatch()) {
		newtoken.type = FORSTOP;
		newtoken.contents = "";
		return newtoken;
	}
	match = keywordBlock.match(s);
	if (match.hasMatch()) {
		newtoken.type = BLOCKSTART;
		newtoken.contents = s.mid(match.capturedEnd());
		return newtoken;
	}
	match = keywordEndblock.match(s);
	if (match.hasMatch()) {
		newtoken.type = BLOCKSTOP;
		newtoken.contents = "";
		return newtoken;
	}
	match = keywordIf.match(s);
	if (match.hasMatch()) {
		newtoken.type = IFSTART;
		newtoken.contents = s.mid(match.capturedEnd());
		return newtoken;
	}
	match = keywordEndif.match(s);
	if (match.hasMatch()) {
		newtoken.type = IFSTOP;
		newtoken.contents = "";
		return newtoken;
	}

	newtoken.type = PARSERERROR;
	newtoken.contents = "";
	return newtoken;
}

static QRegularExpression op(R"(\{%([\w\s\.\|\:]+)%\})");	// Look for {% stuff %}

QList<token> TemplateLayout::lexer(QString input)
{
	QList<token> tokenList;

	int last = 0;
	QRegularExpressionMatch match = op.match(input);
	while (match.hasMatch()) {
		tokenList << stringToken(input.mid(last, match.capturedStart() - last));
		tokenList << operatorToken(match.captured(1));
		last = match.capturedEnd();
		match = op.match(input, last);
	}
	tokenList << stringToken(input.mid(last));
	return tokenList;
}

static QRegularExpression var(R"(\{\{\s*(\w+)\.(\w+)\s*(\|\s*(\w+))?\s*\}\})");	// Look for {{ stuff.stuff|stuff }}

QString TemplateLayout::translate(QString s, State &state)
{
	QString out;
	int last = 0;
	QRegularExpressionMatch match = var.match(s);
	while (match.hasMatch()) {
		QString obname = match.captured(1);
		QString memname = match.captured(2);
		out +=  s.mid(last, match.capturedStart() - last);
		QString listname = state.types.value(obname, obname);
		QVariant value = getValue(listname, memname, state);
		out += value.toString();
		last = match.capturedEnd();
		match = var.match(s, last);
	}
	out += s.mid(last);
	return out;
}

static QRegularExpression forloop(R"(\s*(\w+)\s+in\s+(\w+))");	// Look for "VAR in LISTNAME"
static QRegularExpression ifstatement(R"(forloop\.counter\|\s*divisibleby\:\s*(\d+))");	// Look for forloop.counter|divisibleby: NUMBER

template<typename V, typename T>
void TemplateLayout::parser_for(QList<token> tokenList, int from, int to, QTextStream &out, State &state,
				const V &data, const T *&act)
{
	const T *old = act;
	int i = 1; // Loop iterators start at one
	int olditerator = state.forloopiterator;
	for (const T &item: data) {
		act = &item;
		state.forloopiterator = i++;
		parser(tokenList, from, to, out, state);
	}
	act = old;
	state.forloopiterator = olditerator;
}

// Find end of for or if block. Keeps track of nested blocks.
// Pos should point one past the starting tag.
// Returns -1 if no matching end tag found.
static int findEnd(const QList<token> &tokenList, int from, int to, token_t start, token_t end)
{
	int depth = 1;
	for (int pos = from; pos < to; ++pos) {
		if (tokenList[pos].type == start) {
			++depth;
		} else if (tokenList[pos].type == end) {
			if (--depth <= 0)
				return pos;
		}
	}
	return -1;
}

static std::vector<const cylinder_t *> cylinderList(const dive *d)
{
	std::vector<const cylinder_t *> res;
	res.reserve(d->cylinders.nr);
	for (int i = 0; i < d->cylinders.nr; ++i)
		res.push_back(&d->cylinders.cylinders[i]);
	return res;
}

void TemplateLayout::parser(QList<token> tokenList, int from, int to, QTextStream &out, State &state)
{
	for (int pos = from; pos < to; ++pos) {
		switch (tokenList[pos].type) {
		case LITERAL:
			out << translate(tokenList[pos].contents, state);
			break;
		case BLOCKSTART:
		case BLOCKSTOP:
			break;
		case FORSTART:
		{
			QString argument = tokenList[pos].contents;
			++pos;
			QRegularExpressionMatch match = forloop.match(argument);
			if (match.hasMatch()) {
				QString itemname = match.captured(1);
				QString listname = match.captured(2);
				state.types[itemname] = listname;
				QString buffer;
				QTextStream capture(&buffer);
				int loop_end = findEnd(tokenList, pos, to, FORSTART, FORSTOP);
				if (loop_end < 0) {
					out << "UNMATCHED FOR: '" << argument << "'";
					break;
				}
				if (listname == "years") {
					parser_for(tokenList, pos, loop_end, capture, state, state.years, state.currentYear);
				} else if (listname == "dives") {
					parser_for(tokenList, pos, loop_end, capture, state, state.dives, state.currentDive);
				} else if (listname == "cylinders") {
					if (state.currentDive)
						parser_for(tokenList, pos, loop_end, capture, state, formatCylinders(*state.currentDive), state.currentCylinder);
					else
						qWarning("cylinders loop outside of dive");
				} else if (listname == "cylinderObjects") {
					if (state.currentDive)
						parser_for(tokenList, pos, loop_end, capture, state, cylinderList(*state.currentDive), state.currentCylinderObject);
					else
						qWarning("cylinderObjects loop outside of dive");
				} else {
					qWarning("unknown loop: %s", qPrintable(listname));
				}
				state.types.remove(itemname);
				out << capture.readAll();
				pos = loop_end;
			} else {
				out << "PARSING ERROR: '" << argument << "'";
			}
		}
			break;
		case IFSTART:
		{
			QString argument = tokenList[pos].contents;
			++pos;
			QRegularExpressionMatch match = ifstatement.match(argument);
			if (match.hasMatch()) {
				int if_end = findEnd(tokenList, pos, to, IFSTART, IFSTOP);
				if (if_end < 0) {
					out << "UNMATCHED IF: '" << argument << "'";
					break;
				}
				int divisor = match.captured(1).toInt();
				int counter = std::max(0, state.forloopiterator);
				if (!(counter % divisor)) {
					QString buffer;
					QTextStream capture(&buffer);
					parser(tokenList, pos, if_end, capture, state);
					out << capture.readAll();
				}
				pos = if_end;
			} else {
				out << "PARSING ERROR: '" << argument << "'";
			}
		}
			break;
		case FORSTOP:
		case IFSTOP:
			out << "UNEXPECTED END: " << tokenList[pos].contents;
			return;
		case PARSERERROR:
			out << "PARSING ERROR";
		}
	}
}

QVariant TemplateLayout::getValue(QString list, QString property, const State &state)
{
	if (list == "template_options") {
		if (property == "font") {
			switch (templateOptions.font_index) {
			case 0:
				return "Arial, Helvetica, sans-serif";
			case 1:
				return "Impact, Charcoal, sans-serif";
			case 2:
				return "Georgia, serif";
			case 3:
				return "Courier, monospace";
			case 4:
				return "Verdana, Geneva, sans-serif";
			}
		} else if (property == "borderwidth") {
			return templateOptions.border_width;
		} else if (property == "font_size") {
			return templateOptions.font_size / 9.0;
		} else if (property == "line_spacing") {
			return templateOptions.line_spacing;
		} else if (property == "color1") {
			return templateOptions.color_palette.color1.name();
		} else if (property == "color2") {
			return templateOptions.color_palette.color2.name();
		} else if (property == "color3") {
			return templateOptions.color_palette.color3.name();
		} else if (property == "color4") {
			return templateOptions.color_palette.color4.name();
		} else if (property == "color5") {
			return templateOptions.color_palette.color5.name();
		} else if (property == "color6") {
			return templateOptions.color_palette.color6.name();
		}
	} else if (list ==  "print_options") {
		if (property == "grayscale") {
			if (printOptions.color_selected) {
				return "";
			} else {
				return "-webkit-filter: grayscale(100%)";
			}
		}
	} else if (list =="years") {
		if (!state.currentYear)
			return QVariant();
		const stats_t *object = *state.currentYear;
		if (property == "year") {
			return object->period;
		} else if (property == "dives") {
			return object->selection_size;
		} else if (property == "min_temp") {
			return object->min_temp.mkelvin == 0 ? "0" : get_temperature_string(object->min_temp, true);
		} else if (property == "max_temp") {
			return object->max_temp.mkelvin == 0 ? "0" : get_temperature_string(object->max_temp, true);
		} else if (property == "total_time") {
			return get_dive_duration_string(object->total_time.seconds, gettextFromC::tr("h"),
							gettextFromC::tr("min"), gettextFromC::tr("sec"), " ");
		} else if (property == "avg_time") {
			return get_minutes(object->total_time.seconds / object->selection_size);
		} else if (property == "shortest_time") {
			return get_minutes(object->shortest_time.seconds);
		} else if (property == "longest_time") {
			return get_minutes(object->longest_time.seconds);
		} else if (property == "avg_depth") {
			return get_depth_string(object->avg_depth);
		} else if (property == "min_depth") {
			return get_depth_string(object->min_depth);
		} else if (property == "max_depth") {
			return get_depth_string(object->max_depth);
		} else if (property == "avg_sac") {
			return get_volume_string(object->avg_sac);
		} else if (property == "min_sac") {
			return get_volume_string(object->min_sac);
		} else if (property == "max_sac") {
			return get_volume_string(object->max_sac);
		}
	} else if (list == "cylinders") {
		if (state.currentCylinder && property == "description") {
			return *state.currentCylinder;
		}
	} else if (list == "cylinderObjects") {
		if (!state.currentCylinderObject)
			return QVariant();
		const cylinder_t *cylinder = *state.currentCylinderObject;
		if (property == "description") {
			return cylinder->type.description;
		} else if (property == "size") {
			return get_volume_string(cylinder->type.size, true);
		} else if (property == "workingPressure") {
			return get_pressure_string(cylinder->type.workingpressure, true);
		} else if (property == "startPressure") {
			return get_pressure_string(cylinder->start, true);
		} else if (property == "endPressure") {
			return get_pressure_string(cylinder->end, true);
		} else if (property == "gasMix") {
			return get_gas_string(cylinder->gasmix);
		}
	} else if (list == "dives") {
		if (!state.currentDive)
			return QVariant();
		const dive *d = *state.currentDive;
		if (property == "number") {
			return d->number;
		} else if (property == "id") {
			return d->id;
		} else if (property == "rating") {
			return d->rating;
		} else if (property == "visibility") {
			return d->visibility;
		} else if (property == "wavesize") {
			return d->wavesize;
		} else if (property == "current") {
			return d->current;
		} else if (property == "surge") {
			return d->surge;
		} else if (property == "chill") {
			return d->chill;
		} else if (property == "date") {
			return formatDiveDate(d);
		} else if (property == "time") {
			return formatDiveTime(d);
		} else if (property == "timestamp") {
			return QVariant::fromValue(d->when);
		} else if (property == "location") {
			return get_dive_location(d);
		} else if (property == "gps") {
			return formatDiveGPS(d);
		} else if (property == "gps_decimal") {
			return format_gps_decimal(d);
		} else if (property == "duration") {
			return formatDiveDuration(d);
		} else if (property == "noDive") {
			return d->duration.seconds == 0 && d->dc.duration.seconds == 0;
		} else if (property == "depth") {
			return get_depth_string(d->dc.maxdepth.mm, true, true);
		} else if (property == "divemaster") {
			return d->divemaster;
		} else if (property == "buddy") {
			return d->buddy;
		} else if (property == "airTemp") {
			return get_temperature_string(d->airtemp, true);
		} else if (property == "waterTemp") {
			return get_temperature_string(d->watertemp, true);
		} else if (property == "notes") {
			return formatNotes(d);
		} else if (property == "tags") {
			return get_taglist_string(d->tag_list);
		} else if (property == "gas") {
			return formatGas(d);
		} else if (property == "sac") {
			return formatSac(d);
		} else if (property == "weightList") {
			return formatWeightList(d);
		} else if (property == "weights") {
			return formatWeights(d);
		} else if (property == "singleWeight") {
			return d->weightsystems.nr <= 1;
		} else if (property == "suit") {
			return d->suit;
		} else if (property == "cylinderList") {
			return formatFullCylinderList();
		} else if (property == "cylinders") {
			return formatCylinders(d);
		} else if (property == "maxcns") {
			return d->maxcns;
		} else if (property == "otu") {
			return d->otu;
		} else if (property == "sumWeight") {
			return formatSumWeight(d);
		} else if (property == "getCylinder") {
			return formatGetCylinder(d);
		} else if (property == "startPressure") {
			return formatStartPressure(d);
		} else if (property == "endPressure") {
			return formatEndPressure(d);
		} else if (property == "firstGas") {
			return formatFirstGas(d);
		}
	}
	return QVariant();
}