#include "wallpaperimage.hpp"

#include <QStandardPaths>
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QImageReader>
#include <QtConcurrent>
#include <QQuickWindow>

namespace ZShell::internal {

QThreadPool* WallpaperImage::loadPool() {
	static QThreadPool* pool = [] {
		auto* p = new QThreadPool();
		p->setMaxThreadCount(4);
		return p;
	}();
	return pool;
}

WallpaperImage::WallpaperImage(QQuickItem* parent) : QQuickItem(parent) {
	setFlag(ItemHasContents, true);

	m_loadDebounceTimer.setSingleShot(true);
	connect(
		&m_loadDebounceTimer,
		&QTimer::timeout,
		this,
		&WallpaperImage::beginLoad);

	connect(
		&m_imageWatcher,
		&QFutureWatcher<QImage>::finished,
		this,
		&WallpaperImage::handleImageLoaded);
}

WallpaperImage::~WallpaperImage() {
	m_requestGeneration->fetch_add(1, std::memory_order_relaxed);
	m_imageWatcher.disconnect();

	if (m_texture) delete m_texture;
	if (m_previousTexture) delete m_previousTexture;
}

void WallpaperImage::setStatus(const Status s) {
	if (m_status == s) return;
	m_status = s;
	emit statusChanged();
}

void WallpaperImage::setSource(const QUrl& source) {
	if (m_source == source) return;
	m_source = source;
	emit sourceChanged();

	setStatus(Loading);
	m_loadDebounceTimer.start(80);
}

void WallpaperImage::setScreenResolution(const QSize& screenResolution) {
	if (m_screenResolution == screenResolution) return;
	m_screenResolution = screenResolution;
	emit screenResolutionChanged();
	beginLoad();
}

void WallpaperImage::setZoom(qreal zoom) {
	if (qFuzzyCompare(m_zoom, zoom)) return;
	m_zoom = zoom;
	emit zoomChanged();
	update();
}

void WallpaperImage::setFadeDuration(int ms) {
	if (ms < 0) ms = 0;
	if (m_fadeDuration == ms) return;
	m_fadeDuration = ms;
	emit fadeDurationChanged();
}

void WallpaperImage::setCropX(qreal x) {
	if (qFuzzyCompare(m_cropX, x)) return;
	m_cropX = x;
	emit cropXChanged();
	update();
}

void WallpaperImage::setCropY(qreal y) {
	if (qFuzzyCompare(m_cropY, y)) return;
	m_cropY = y;
	emit cropYChanged();
	update();
}

void WallpaperImage::setCropWidth(qreal w) {
	if (w <= 0.0) w = 1.0;
	if (qFuzzyCompare(m_cropWidth, w)) return;
	m_cropWidth = w;
	emit cropWidthChanged();
	update();
}

void WallpaperImage::setCropHeight(qreal h) {
	if (h <= 0.0) h = 1.0;
	if (qFuzzyCompare(m_cropHeight, h)) return;
	m_cropHeight = h;
	emit cropHeightChanged();
	update();
}

QString WallpaperImage::getCacheFilePath() const {
	if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString();

	QString cachePath =
		QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) +
		"/zshell/imagecache";
	QDir().mkpath(cachePath);

	QString id = m_source.toString() + "_" +
				 QString::number(m_screenResolution.width()) + "x" +
				 QString::number(m_screenResolution.height());
	QByteArray hash =
		QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();

	return cachePath + "/" + hash + ".png";
}

void WallpaperImage::beginLoad() {
	if (m_source.isEmpty()) {
		setStatus(Null);
		return;
	}

	const quint64 requestId =
		m_requestGeneration->fetch_add(1, std::memory_order_relaxed) + 1;

	QString cacheFile = getCacheFilePath();
	QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile()
												: m_source.toString();
	if (sourceFile.startsWith("qrc:/")) sourceFile = sourceFile.mid(3);

	QSize targetRes = m_screenResolution;
	auto generation = m_requestGeneration;

	QFuture<QImage> future = QtConcurrent::run(
		loadPool(),
		[sourceFile, cacheFile, targetRes, requestId, generation]() -> QImage {
			auto stale = [&] {
				return generation->load(std::memory_order_relaxed) != requestId;
			};

			if (stale()) return QImage();

			if (!targetRes.isEmpty() && !cacheFile.isEmpty() &&
				QFileInfo::exists(cacheFile)) {
				QImage cached(cacheFile);
				if (!cached.isNull()) return cached;
			}

			if (stale()) return QImage();

			QImage original;
			QImageReader reader(sourceFile);
			if (!targetRes.isEmpty() &&
				reader.supportsOption(QImageIOHandler::Size)) {
				QSize actual = reader.size();
				if (actual.isValid() &&
					(actual.width() > targetRes.width() ||
					 actual.height() > targetRes.height())) {
					QSize scaledSize =
						actual.scaled(targetRes, Qt::KeepAspectRatioByExpanding);
					reader.setScaledSize(scaledSize);
				}
				original = reader.read();
			} else {
				original = QImage(sourceFile);
			}
			if (original.isNull()) return QImage();

			if (stale()) return QImage();

			if (targetRes.isEmpty()) return original;

			QImage scaled = (original.width() > targetRes.width() ||
							 original.height() > targetRes.height())
								? original.scaled(
									  targetRes,
									  Qt::KeepAspectRatioByExpanding,
									  Qt::SmoothTransformation)
								: original;

			if (stale()) return scaled;

			if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
			return scaled;
		});

	m_imageWatcher.setFuture(future);
}

void WallpaperImage::handleImageLoaded() {
	QImage result = m_imageWatcher.result();
	if (result.isNull()) {
		setStatus(Error);
		return;
	}

	m_pendingImage = result;
	m_hasPending = true;
	setStatus(Ready);
	update();
}

QRectF WallpaperImage::computeSourceRect(QSGTexture* texture) const {
	QSize texSize = texture->textureSize();

	qreal cW = m_cropWidth / m_zoom;
	qreal cH = m_cropHeight / m_zoom;

	QRectF reqRect(
		m_cropX * texSize.width(),
		m_cropY * texSize.height(),
		cW * texSize.width(),
		cH * texSize.height());

	QRectF bounds = boundingRect();
	if (bounds.isEmpty() || reqRect.isEmpty()) return reqRect;

	qreal targetRatio = bounds.width() / bounds.height();
	qreal reqRatio = reqRect.width() / reqRect.height();

	QRectF sourceRect = reqRect;

	if (reqRatio > targetRatio) {
		qreal newWidth = reqRect.height() * targetRatio;
		qreal xOffset = (reqRect.width() - newWidth) / 2.0;
		sourceRect.setX(reqRect.x() + xOffset);
		sourceRect.setWidth(newWidth);
	} else if (reqRatio < targetRatio) {
		qreal newHeight = reqRect.width() / targetRatio;
		qreal yOffset = (reqRect.height() - newHeight) / 2.0;
		sourceRect.setY(reqRect.y() + yOffset);
		sourceRect.setHeight(newHeight);
	}

	return sourceRect;
}

void WallpaperImage::configureImageNode(
	QSGImageNode* node, QSGTexture* texture) {
	node->setTexture(texture);
	node->setRect(boundingRect());
	node->setFiltering(QSGTexture::Linear);

	QRectF sourceRect = computeSourceRect(texture);
	node->setSourceRect(sourceRect);

	if (texture == m_texture) {
		QSize texSize = texture->textureSize();
		QRectF normalizedActual(
			sourceRect.x() / texSize.width(),
			sourceRect.y() / texSize.height(),
			sourceRect.width() / texSize.width(),
			sourceRect.height() / texSize.height());

		bool changed = false;
		auto updateIfChanged = [&](qreal& dst, qreal value) {
			if (!qFuzzyCompare(dst, value)) {
				dst = value;
				changed = true;
			}
		};
		updateIfChanged(m_actualCropX, normalizedActual.x());
		updateIfChanged(m_actualCropY, normalizedActual.y());
		updateIfChanged(m_actualCropWidth, normalizedActual.width());
		updateIfChanged(m_actualCropHeight, normalizedActual.height());
		if (changed) emit actualCropChanged();
	}
}

QSGNode* WallpaperImage::updatePaintNode(
	QSGNode* oldNode, UpdatePaintNodeData*) {
	Q_UNUSED(oldNode);

	if (m_hasPending) {
		if (m_previousTexture) {
			delete m_previousTexture;
			m_previousTexture = nullptr;
		}
		m_previousTexture = m_texture;
		m_texture = window()->createTextureFromImage(
			m_pendingImage, QQuickWindow::TextureHasAlphaChannel);
		m_pendingImage = QImage();
		m_hasPending = false;

		if (m_previousTexture && m_fadeDuration > 0) {
			m_fading = true;
			m_fadeTimer.restart();
		} else {
			if (m_previousTexture) {
				delete m_previousTexture;
				m_previousTexture = nullptr;
			}
			m_fading = false;
		}
	}

	if (!m_texture) {
		return m_rootNode;
	}

	if (!m_rootNode) {
		m_rootNode = new QSGNode();

		m_newOpacityNode = new QSGOpacityNode();
		m_newImageNode = window()->createImageNode();
		m_newOpacityNode->appendChildNode(m_newImageNode);
		m_rootNode->appendChildNode(m_newOpacityNode);

		m_oldOpacityNode = new QSGOpacityNode();
		m_oldImageNode = window()->createImageNode();
		m_oldOpacityNode->appendChildNode(m_oldImageNode);
		m_rootNode->appendChildNode(m_oldOpacityNode);
	}

	configureImageNode(m_newImageNode, m_texture);
	m_newOpacityNode->setOpacity(1.0);

	if (m_fading && m_previousTexture) {
		qreal progress =
			qBound(0.0, qreal(m_fadeTimer.elapsed()) / m_fadeDuration, 1.0);

		configureImageNode(m_oldImageNode, m_previousTexture);
		m_oldOpacityNode->setOpacity(1.0 - progress);

		if (progress >= 1.0) {
			m_fading = false;
			delete m_previousTexture;
			m_previousTexture = nullptr;
			m_oldOpacityNode->setOpacity(0.0);
		} else {
			update();
		}
	} else {
		m_oldOpacityNode->setOpacity(0.0);
	}

	return m_rootNode;
}

} // namespace ZShell::internal
