#include "cachingimagemanager.hpp"

#include <algorithm>

#include <QtQuick/qquickwindow.h>
#include <qcryptographichash.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h>
#include <qimagereader.h>
#include <qpainter.h>
#include <qthread.h>
#include <qthreadpool.h>
#include <qtconcurrentrun.h>

namespace ZShell::internal {

namespace {

QThreadPool* imageIoPool() {
	static QThreadPool pool;
	static const bool init = [] {
		pool.setMaxThreadCount(std::max(2, QThread::idealThreadCount() / 2));
		return true;
	}();
	(void)init;

	return &pool;
}

} // namespace

qreal CachingImageManager::effectiveScale() const {
	if (m_item && m_item->window()) {
		return m_item->window()->devicePixelRatio();
	}

	return 1.0;
}

QSize CachingImageManager::effectiveSize() const {
	if (!m_item) {
		return QSize();
	}

	const qreal scale = effectiveScale();
	const QSize size =
		QSizeF(m_item->width() * scale, m_item->height() * scale).toSize();
	m_item->setProperty("sourceSize", size);
	return size;
}

QQuickItem* CachingImageManager::item() const {
	return m_item;
}

void CachingImageManager::setItem(QQuickItem* item) {
	if (m_item == item) {
		return;
	}

	if (m_widthConn) {
		disconnect(m_widthConn);
	}
	if (m_heightConn) {
		disconnect(m_heightConn);
	}

	m_item = item;
	emit itemChanged();

	if (item) {
		m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() {
			updateSource();
		});
		m_heightConn =
			connect(item, &QQuickItem::heightChanged, this, [this]() {
				updateSource();
			});
		updateSource();
	}
}

QUrl CachingImageManager::cacheDir() const {
	return m_cacheDir;
}

void CachingImageManager::setCacheDir(const QUrl& cacheDir) {
	if (m_cacheDir == cacheDir) {
		return;
	}

	m_cacheDir = cacheDir;
	if (!m_cacheDir.path().endsWith("/")) {
		m_cacheDir.setPath(m_cacheDir.path() + "/");
	}
	emit cacheDirChanged();
}

QString CachingImageManager::path() const {
	return m_path;
}

void CachingImageManager::setPath(const QString& path) {
	if (m_path == path) {
		return;
	}

	m_path = path;
	emit pathChanged();

	if (!path.isEmpty()) {
		updateSource(path);
	}
}

void CachingImageManager::updateSource() {
	updateSource(m_path);
}

void CachingImageManager::updateSource(const QString& path) {
	if (path.isEmpty() || path == m_shaPath) {
		return;
	}

	m_shaPath = path;

	const auto future =
		QtConcurrent::run(imageIoPool(), &CachingImageManager::sha256sum, path);

	const auto watcher = new QFutureWatcher<QString>(this);

	connect(
		watcher,
		&QFutureWatcher<QString>::finished,
		this,
		[watcher, path, this]() {
			if (m_path != path) {
				watcher->deleteLater();
				return;
			}

			const QString key = watcher->result();

			if (key.isEmpty()) {
				if (m_shaPath == path) {
					m_shaPath = QString();
				}
				watcher->deleteLater();
				return;
			}

			const QSize size = effectiveSize();

			if (!m_item || !size.width() || !size.height()) {
				if (m_shaPath == path) {
					m_shaPath = QString();
				}
				watcher->deleteLater();
				return;
			}

			const QString fillMode = m_item->property("fillMode").toString();
			const QString filename =
				QString("%1@%2x%3-%4.png")
					.arg(key)
					.arg(size.width())
					.arg(size.height())
					.arg(
						fillMode == "PreserveAspectCrop"  ? "crop"
						: fillMode == "PreserveAspectFit" ? "fit"
														  : "stretch");

			const QUrl cache = m_cacheDir.resolved(QUrl(filename));
			if (m_cachePath == cache) {
				if (m_shaPath == path) {
					m_shaPath = QString();
				}
				watcher->deleteLater();
				return;
			}

			m_cachePath = cache;
			emit cachePathChanged();

			if (!cache.isLocalFile()) {
				qWarning() << "CachingImageManager::updateSource: cachePath"
						   << cache << "is not a local file";
				if (m_shaPath == path) {
					m_shaPath = QString();
				}
				watcher->deleteLater();
				return;
			}

			const QImageReader reader(cache.toLocalFile());
			if (reader.canRead()) {
				m_item->setProperty("source", cache);
			} else {
				m_item->setProperty("source", QUrl::fromLocalFile(path));
				createCache(path, cache.toLocalFile(), fillMode, size);
			}

			if (m_shaPath == path) {
				m_shaPath = QString();
			}

			watcher->deleteLater();
		});

	watcher->setFuture(future);
}

QUrl CachingImageManager::cachePath() const {
	return m_cachePath;
}

void CachingImageManager::createCache(
	const QString& path,
	const QString& cache,
	const QString& fillMode,
	const QSize& size) const {
	auto* watcher =
		new QFutureWatcher<bool>(const_cast<CachingImageManager*>(this));

	connect(
		watcher,
		&QFutureWatcher<bool>::finished,
		this,
		[this, watcher, cache, path]() {
			if (watcher->result() && m_item && m_path == path &&
				m_cachePath.toLocalFile() == cache) {
				m_item->setProperty("source", QUrl::fromLocalFile(cache));
			}

			watcher->deleteLater();
		});

	const auto future =
		QtConcurrent::run(
			imageIoPool(), [path, cache, fillMode, size]() -> bool {
				QImageReader reader(path);

				if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
					const QSize full = reader.size();
					if (full.isValid() && !full.isEmpty()) {
						const QSize hint = size * 2;
						QSize scaled = full;
						scaled.scale(hint, Qt::KeepAspectRatioByExpanding);
						scaled = QSize(
							std::min(scaled.width(), full.width()),
							std::min(scaled.height(), full.height()));
						reader.setScaledSize(scaled);
					}
				}

				QImage image = reader.read();

				if (image.isNull()) {
					qWarning()
						<< "CachingImageManager::createCache: failed to read"
						<< path << reader.errorString();
					return false;
				}

				image.convertTo(QImage::Format_ARGB32);

				if (fillMode == "PreserveAspectCrop") {
					image = image.scaled(
						size,
						Qt::KeepAspectRatioByExpanding,
						Qt::SmoothTransformation);
				} else if (fillMode == "PreserveAspectFit") {
					image = image.scaled(
						size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
				} else {
					image = image.scaled(
						size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
				}

				if (fillMode == "PreserveAspectCrop" ||
					fillMode == "PreserveAspectFit") {
					QImage canvas(size, QImage::Format_ARGB32);
					canvas.fill(Qt::transparent);

					QPainter painter(&canvas);
					painter.drawImage(
						(size.width() - image.width()) / 2,
						(size.height() - image.height()) / 2,
						image);
					painter.end();

					image = canvas;
				}

				const QString parent = QFileInfo(cache).absolutePath();
				if (!QDir().mkpath(parent) || !image.save(cache)) {
					qWarning()
						<< "CachingImageManager::createCache: failed to save to"
						<< cache;
					return false;
				}

				return true;
			});

	watcher->setFuture(future);
}

QString CachingImageManager::sha256sum(const QString& path) {
	const QFileInfo info(path);

	if (!info.exists() || !info.isFile()) {
		qWarning() << "CachingImageManager::sha256sum: failed to stat" << path;
		return QString();
	}

	QCryptographicHash hash(QCryptographicHash::Sha256);
	hash.addData(path.toUtf8());
	hash.addData(QByteArray::number(info.size()));
	hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch()));

	return hash.result().toHex();
}

} // namespace ZShell::internal
