#include "zutils.hpp"

#include <QtConcurrent/qtconcurrentrun.h>
#include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h>
#include <qcontainerfwd.h>
#include <qdir.h>
#include <qdiriterator.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h>
#include <qjsprimitivevalue.h>
#include <qloggingcategory.h>
#include <qregularexpression.h>
#include <qqmlengine.h>
#include <qfile.h>

#include "util/metaenum.hpp"

Q_LOGGING_CATEGORY(lcZUtils, "ZShell.cutils", QtInfoMsg)

namespace ZShell {

void ZUtils::saveItem(QQuickItem* target, const QUrl& path) {
	this->saveItem(target, path, QRect(), QJSValue(), QJSValue());
}

void ZUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) {
	this->saveItem(target, path, rect, QJSValue(), QJSValue());
}

void ZUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) {
	this->saveItem(target, path, QRect(), onSaved, QJSValue());
}

void ZUtils::saveItem(
	QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) {
	this->saveItem(target, path, QRect(), onSaved, onFailed);
}

void ZUtils::saveItem(
	QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) {
	this->saveItem(target, path, rect, onSaved, QJSValue());
}

void ZUtils::saveItem(
	QQuickItem* target,
	const QUrl& path,
	const QRect& rect,
	QJSValue onSaved,
	QJSValue onFailed) {
	if (!target) {
		qCWarning(lcZUtils) << "saveItem: a target is required";
		return;
	}

	if (!path.isLocalFile()) {
		qCWarning(lcZUtils) << "saveItem:" << path << "is not a local file";
		return;
	}

	if (!target->window()) {
		qCWarning(lcZUtils) << "saveItem: unable to save target" << target
							<< "without a window";
		return;
	}

	auto scaledRect = rect;
	const qreal scale = target->window()->devicePixelRatio();
	if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) {
		scaledRect = QRectF(
						 rect.left() * scale,
						 rect.top() * scale,
						 rect.width() * scale,
						 rect.height() * scale)
						 .toRect();
	}

	const QSharedPointer<const QQuickItemGrabResult> grabResult =
		target->grabToImage();

	QObject::connect(
		grabResult.data(),
		&QQuickItemGrabResult::ready,
		this,
		[grabResult, scaledRect, path, onSaved, onFailed, this]() {
			const auto future = QtConcurrent::run([=]() {
				QImage image = grabResult->image();

				if (scaledRect.isValid()) {
					image = image.copy(scaledRect);
				}

				const QString file = path.toLocalFile();
				const QString parent = QFileInfo(file).absolutePath();
				return QDir().mkpath(parent) && image.save(file);
			});

			auto* watcher = new QFutureWatcher<bool>(this);
			auto* engine = qmlEngine(this);

			QObject::connect(
				watcher, &QFutureWatcher<bool>::finished, this, [=]() {
					if (watcher->result()) {
						if (onSaved.isCallable()) {
							QJSValueList args = {QJSValue(path.toLocalFile())};
							if (engine) {
								args << engine->toScriptValue(
									QVariant::fromValue(path));
							}
							onSaved.call(args);
						}
					} else {
						qCWarning(lcZUtils)
							<< "saveItem: failed to save" << path;
						if (onFailed.isCallable()) {
							if (engine) {
								onFailed.call({engine->toScriptValue(
									QVariant::fromValue(path))});
							} else {
								onFailed.call();
							}
						}
					}
					watcher->deleteLater();
				});
			watcher->setFuture(future);
		});
}

bool ZUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) {
	if (!source.isLocalFile()) {
		qCWarning(lcZUtils)
			<< "copyFile: source" << source << "is not a local file";
		return false;
	}
	if (!target.isLocalFile()) {
		qCWarning(lcZUtils)
			<< "copyFile: target" << target << "is not a local file";
		return false;
	}

	if (overwrite && QFile::exists(target.toLocalFile())) {
		if (!QFile::remove(target.toLocalFile())) {
			qCWarning(lcZUtils)
				<< "copyFile: overwrite was specified but failed to remove"
				<< target.toLocalFile();
			return false;
		}
	}

	return QFile::copy(source.toLocalFile(), target.toLocalFile());
}

bool ZUtils::deleteFile(const QUrl& path) {
	if (!path.isLocalFile()) {
		qCWarning(lcZUtils)
			<< "deleteFile: path" << path << "is not a local file";
		return false;
	}

	return QFile::remove(path.toLocalFile());
}

QString ZUtils::toLocalFile(const QUrl& url) {
	if (!url.isLocalFile()) {
		qCWarning(lcZUtils)
			<< "toLocalFile: given url is not a local file" << url;
		return QString();
	}

	return url.toLocalFile();
}

qreal ZUtils::clamp(qreal value, qreal min, qreal max) {
	return qBound(min, value, max);
}

QString ZUtils::enumToString(
	QObject* target, const QString& property, const QVariant& value) {
	if (!target) {
		qCWarning(lcZUtils) << "enumToString: a target is required";
		return {};
	}

	const auto* meta = target->metaObject();
	const auto index = meta->indexOfProperty(property.toUtf8().constData());
	if (index < 0) {
		qCWarning(lcZUtils)
			<< "enumToString:" << target << "has no property" << property;
		return {};
	}

	const auto prop = meta->property(index);
	const auto metaEnum = prop.isEnumType()
							  ? prop.enumerator()
							  : util::metaEnumFor(prop.metaType());
	if (!metaEnum.isValid() || metaEnum.is64Bit()) {
		qCWarning(lcZUtils) << "enumToString: property" << property << "of"
							<< target << "is not a supported enum";
		return {};
	}

	const auto val = value.isValid() ? value : prop.read(target);
	const auto* key = util::enumKeyFor(metaEnum, val);
	if (!key) {
		qCWarning(
			lcZUtils,
			"enumToString: no enumerator of %s::%s has the value %lld",
			metaEnum.scope(),
			metaEnum.name(),
			val.toLongLong());
		return {};
	}

	return QString::fromUtf8(key);
}

namespace {

template <typename Predicate>
QQuickItem* findChildDfs(QQuickItem* root, Predicate&& match) {
	const auto children = root->childItems();
	for (QQuickItem* const child : children) {
		if (match(child)) {
			return child;
		}
		if (QQuickItem* const found = findChildDfs(child, match)) {
			return found;
		}
	}
	return nullptr;
}

template <typename Predicate>
void findChildrenDfs(
	QQuickItem* root, Predicate&& match, QList<QQuickItem*>& out) {
	const auto children = root->childItems();
	for (QQuickItem* const child : children) {
		if (match(child)) {
			out.append(child);
		}
		findChildrenDfs(child, match, out);
	}
}

} // namespace

QQuickItem* ZUtils::findChild(QQuickItem* root, const QString& name) {
	if (!root) {
		return nullptr;
	}

	return findChildDfs(root, [&name](const QQuickItem* item) {
		return item->objectName() == name;
	});
}

QList<QQuickItem*> ZUtils::findChildren(QQuickItem* root, const QString& name) {
	QList<QQuickItem*> children;
	if (root) {
		findChildrenDfs(
			root,
			[&name](const QQuickItem* item) {
				return item->objectName() == name;
			},
			children);
	}
	return children;
}

QList<QQuickItem*> ZUtils::findChildrenMatching(
	QQuickItem* root, const QString& pattern) {
	QList<QQuickItem*> children;
	if (root) {
		const QRegularExpression re(pattern);
		findChildrenDfs(
			root,
			[&re](const QQuickItem* item) {
				return re.match(item->objectName()).hasMatch();
			},
			children);
	}
	return children;
}

#ifndef ZSHELL_VERSION
#define ZSHELL_VERSION ""
#endif

QString ZUtils::version() const {
	return QStringLiteral(ZSHELL_VERSION);
}

QString ZUtils::qtVersion() const {
	return QStringLiteral(QT_VERSION_STR);
}

QString ZUtils::gitRevision() {
#ifdef GIT_REVISION
	return QStringLiteral(GIT_REVISION);
#else
	return QString();
#endif
}

QString ZUtils::readTextFile(const QString& path) {
	QFile file(path);
	if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
		return QString();
	}
	return QString::fromUtf8(file.readAll());
}

bool ZUtils::writeTextFile(const QString& path, const QString& text) {
	const QFileInfo info(path);
	if (!QDir().mkpath(info.absolutePath())) {
		qCWarning(lcZUtils) << "Failed to create directory for" << path;
		return false;
	}
	QFile file(path);
	if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
		qCWarning(lcZUtils) << "Failed to open" << path << "for writing";
		return false;
	}
	return file.write(text.toUtf8()) >= 0;
}

QStringList ZUtils::listFiles(const QString& dir, const QString& suffix) {
	QStringList out;
	QDirIterator it(dir, QDirIterator::Subdirectories);
	while (it.hasNext()) {
		const QString path = it.next();
		if (it.fileInfo().isFile() && path.endsWith(suffix)) {
			out.append(path);
		}
	}
	return out;
}

} // namespace ZShell
