#include "carouselview.hpp"

#include <QSignalBlocker>
#include <QQmlContext>
#include <QQmlProperty>
#include <QMouseEvent>
#include <QWheelEvent>

#include <cmath>
#include <qobject.h>

namespace ZShell::components {

namespace {

constexpr qreal kDragThreshold = 4.0;
constexpr qreal kFlickFriction = 0.0022;
constexpr qreal kMinFlickVelocity = 60.0;
constexpr qreal kMaxFlickVelocity = 6000.0;
constexpr int kMaxLayoutCapacity = 64;

} // namespace

CarouselView::CarouselView(QQuickItem* parent) : QQuickItem(parent) {
	setAcceptedMouseButtons(Qt::LeftButton);
	setFiltersChildMouseEvents(true);
	setFlag(QQuickItem::ItemHasContents, false);
	setClip(false);

	m_glideAnim = new QVariantAnimation(this);
	m_glideAnim->setEasingCurve(m_glideEasingType);

	connect(
		m_glideAnim,
		&QVariantAnimation::valueChanged,
		this,
		[this](const QVariant& v) {
			m_contentX = v.toReal();

			wrapContentIfNeeded();
			updateCurrentIndexFromContentX(true);
			relayout();
		});

	connect(m_glideAnim, &QVariantAnimation::finished, this, [this]() {
		updateCurrentIndexFromContentX(true);
		relayout();
	});

	m_flickAnim = new QVariantAnimation(this);
	m_flickAnim->setEasingCurve(QEasingCurve::OutQuad);

	connect(
		m_flickAnim,
		&QVariantAnimation::valueChanged,
		this,
		[this](const QVariant& v) {
			m_contentX = v.toReal();

			wrapContentIfNeeded();
			updateCurrentIndexFromContentX(true);
			relayout();
		});

	connect(m_flickAnim, &QVariantAnimation::finished, this, [this]() {
		snapToNearest();
	});
}

CarouselView::~CarouselView() {
	releaseAllItems();
}

void CarouselView::setDelegate(QQmlComponent* c) {
	if (m_delegate == c) return;

	m_delegate = c;

	releaseAllItems();

	emit delegateChanged();

	relayout();
}

void CarouselView::notifyCurrentIndexChanged() {
	if (!m_completed) return;
	emit currentIndexChanged();
}

void CarouselView::notifyCurrentItemChanged() {
	if (!m_completed) return;
	emit currentItemChanged();
}

void CarouselView::notifyPreviewIndexChanged(int realIndex) {
	if (!m_completed || realIndex < 0) return;
	emit previewIndexChanged(realIndex, m_model.at(realIndex));
}

void CarouselView::classBegin() {}

void CarouselView::componentComplete() {
	m_completed = true;
	emit currentIndexChanged();
	if (m_currentRealIndex >= 0 && m_currentRealIndex < m_model.size())
		emit previewIndexChanged(
			m_currentRealIndex, m_model.at(m_currentRealIndex));

	emit currentItemChanged();
}

bool CarouselView::childMouseEventFilter(QQuickItem* item, QEvent* event) {
	Q_UNUSED(item);

	auto* mouseEvent = dynamic_cast<QMouseEvent*>(event);
	if (!mouseEvent) return false;

	const QPointF pos = mapFromItem(item, mouseEvent->position());

	switch (mouseEvent->type()) {
	case QEvent::MouseButtonPress: {
		if (mouseEvent->button() != Qt::LeftButton) return false;

		m_dragging = true;
		m_dragActive = false;

		m_pressPos = pos;
		m_pressContentX = m_contentX;

		m_dragTimer.start();

		m_velocitySamples.clear();
		addVelocitySample(0, pos.x());

		return false;
	}

	case QEvent::MouseMove: {
		if (!m_dragging) return false;

		const qreal dx = pos.x() - m_pressPos.x();

		if (!m_dragActive && std::abs(dx) > kDragThreshold) {
			m_dragActive = true;
			m_pressContentX = m_contentX;

			cancelAnimations();
			grabMouse();
		}

		if (!m_dragActive) return false;

		m_contentX = m_pressContentX - dx;

		wrapContentIfNeeded();
		updateCurrentIndexFromContentX(true);
		relayout();

		addVelocitySample(m_dragTimer.elapsed(), pos.x());

		mouseEvent->accept();
		return true;
	}

	case QEvent::MouseButtonRelease: {
		if (!m_dragging) return false;

		if (!m_dragActive) {
			m_dragging = false;
			return false;
		}

		return false;
	}

	case QEvent::MouseButtonDblClick:
		return false;

	default:
		return false;
	}
}

void CarouselView::setModel(const QVariantList& m) {
	m_model = m;

	emit modelChanged();

	const int newCount = m_model.size();

	if (newCount == 0) {
		m_currentRealIndex = -1;
		m_currentItem = nullptr;
		notifyCurrentItemChanged();

		releaseAllItems();
		rebuildArrangement();

		return;
	}

	if (!m_initializedLayout) {
		m_centerBlockStart = 0;

		const int startVirtual =
			m_pendingCurrentIndex >= 0
				? qBound(0, m_pendingCurrentIndex, newCount - 1)
				: 0;

		m_pendingCurrentIndex = -1;

		m_currentVirtualIndex = startVirtual;
		m_contentX = contentXForIndex(startVirtual);
		m_currentRealIndex = realIndexOf(m_currentVirtualIndex);
		m_previewRealIndex = m_currentRealIndex;

		m_initializedLayout = true;

		m_layoutCapacity =
			qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth));
		setImplicitWidth(targetWidthForMaxWidth(m_maxWidth));

		rebuildArrangement();
		relayout();

		return;
	}

	m_layoutCapacity =
		qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth));

	setImplicitWidth(targetWidthForMaxWidth(m_maxWidth));

	rebuildArrangement();

	centerInstantlyOnReal(qBound(0, m_currentRealIndex, newCount - 1));
}

void CarouselView::setFocalWidth(qreal v) {
	if (qFuzzyCompare(m_focalWidth, v)) return;

	m_focalWidth = v;

	emit focalWidthChanged();

	m_layoutCapacity =
		qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth));

	setImplicitWidth(targetWidthForMaxWidth(m_maxWidth));

	rebuildArrangement();
	relayout();
}

void CarouselView::setMinEdgeWidth(qreal v) {
	if (qFuzzyCompare(m_minEdgeWidth, v)) return;

	m_minEdgeWidth = v;

	emit minEdgeWidthChanged();

	m_layoutCapacity =
		qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth));

	setImplicitWidth(targetWidthForMaxWidth(m_maxWidth));

	rebuildArrangement();
	relayout();
}

void CarouselView::setItemSpacing(qreal v) {
	if (qFuzzyCompare(m_itemSpacing, v)) return;

	m_itemSpacing = v;

	emit itemSpacingChanged();

	m_layoutCapacity =
		qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth));

	setImplicitWidth(targetWidthForMaxWidth(m_maxWidth));

	rebuildArrangement();
	relayout();
}

void CarouselView::setTileHeight(qreal v) {
	if (qFuzzyCompare(m_tileHeight, v)) return;

	m_tileHeight = v;

	emit tileHeightChanged();

	for (const Slot& s : std::as_const(m_slots)) {
		if (s.active && s.item) {
			QQmlProperty::write(
				s.item, QStringLiteral("tileHeight"), m_tileHeight);
		}
	}
}

void CarouselView::setMaxWidth(qreal v) {
	if (qFuzzyCompare(m_maxWidth, v)) return;

	m_maxWidth = v;

	emit maxWidthChanged();

	const qreal targetWidth = targetWidthForMaxWidth(m_maxWidth);

	setImplicitWidth(targetWidth);

	m_layoutCapacity =
		qMax(m_layoutCapacity, layoutCapacityForWidth(m_maxWidth));

	rebuildArrangement();
	relayout();
}

void CarouselView::setGlideDuration(int v) {
	if (m_glideDuration == v) return;

	m_glideDuration = v;

	emit glideDurationChanged();
}

void CarouselView::setGlideEasingType(QEasingCurve::Type t) {
	if (m_glideEasingType == t) return;

	m_glideEasingType = t;

	m_glideAnim->setEasingCurve(t);

	emit glideEasingTypeChanged();
}

void CarouselView::setDelegateProperties(const QVariantMap& v) {
	m_delegateProperties = v;

	emit delegatePropertiesChanged();

	for (const Slot& s : std::as_const(m_slots)) {
		if (!s.active || !s.item) continue;

		for (auto it = m_delegateProperties.constBegin();
			 it != m_delegateProperties.constEnd();
			 ++it) {
			QQmlProperty::write(s.item, it.key(), it.value());
		}
	}
}

void CarouselView::setCurrentIndex(int idx) {
	if (m_model.isEmpty()) {
		m_pendingCurrentIndex = idx;

		if (m_currentRealIndex != idx) {
			m_currentRealIndex = idx;
			notifyCurrentIndexChanged();
		}

		return;
	}

	if (!m_completed) {
		centerInstantlyOnReal(idx);
		return;
	}

	goToIndex(idx);
}

void CarouselView::updateCurrentItem() {
	QQuickItem* item = nullptr;

	for (const Slot& s : std::as_const(m_slots)) {
		if (s.active && s.virtualIndex == m_currentVirtualIndex) {
			item = s.item;
			break;
		}
	}

	if (item == m_currentItem) return;

	m_currentItem = item;
	notifyCurrentItemChanged();
}

void CarouselView::geometryChange(const QRectF& newGeo, const QRectF& oldGeo) {
	QQuickItem::geometryChange(newGeo, oldGeo);

	const qreal oldWidth = oldGeo.width();

	const qreal newWidth = newGeo.width();

	if (qFuzzyCompare(oldWidth, newWidth)) {
		return;
	}

	const qreal delta = (newWidth - oldWidth) / 2.0;

	m_contentX -= delta;

	auto shiftAnimation = [delta](QVariantAnimation* anim) {
		if (anim->state() != QAbstractAnimation::Running) {
			return;
		}

		anim->setStartValue(anim->startValue().toReal() - delta);

		anim->setEndValue(anim->endValue().toReal() - delta);
	};

	shiftAnimation(m_glideAnim);
	shiftAnimation(m_flickAnim);

	rebuildArrangement();
	relayout();
}

CarouselView::Arrangement CarouselView::arrangementForCount(int n) const {
	Arrangement a;

	const qreal F = m_focalWidth;

	const qreal M = m_minEdgeWidth;

	const qreal sp = m_itemSpacing;

	if (F <= 0) {
		a.n = 0;
		a.width = 0;
		a.outerSpacing = 0;
		a.sizes = {F};
		a.centers = {0};

		return a;
	}

	if (n <= 0) {
		a.n = 0;
		a.width = F;
		a.outerSpacing = 0;

		a.sizes = {F};
		a.centers = {0};

		return a;
	}

	a.n = n;
	a.outerSpacing = sp;

	a.sizes.reserve(n + 1);
	a.centers.reserve(n + 1);

	a.sizes.append(F);

	const qreal ratio = std::pow(M / F, 1.0 / n);

	qreal sideWidth = 0;

	for (int i = 1; i <= n; ++i) {
		const qreal size = (i == n) ? M : F * std::pow(ratio, i);

		a.sizes.append(size);
		sideWidth += size;
	}

	a.width = F + 2.0 * n * sp + 2.0 * sideWidth;

	a.centers.append(0);

	qreal center = 0;

	for (int i = 1; i <= n; ++i) {
		center += a.sizes[i - 1] / 2.0 + sp + a.sizes[i] / 2.0;

		a.centers.append(center);
	}

	a.effectiveExtraWidth = 0;

	for (int i = 1; i <= n; ++i) {
		a.effectiveExtraWidth += pitch() - a.sizes[i];
	}

	return a;
}

int CarouselView::layoutCapacityForWidth(qreal width) const {
	if (width <= 0 || m_focalWidth <= 0) {
		return 0;
	}

	int bestN = 0;

	for (int n = 1; n <= kMaxLayoutCapacity; ++n) {
		const Arrangement a = arrangementForCount(n);

		if (a.width > width) break;

		bestN = n;
	}

	return bestN;
}

qreal CarouselView::targetWidthForMaxWidth(qreal maxWidth) const {
	if (maxWidth <= 0 || m_focalWidth <= 0) {
		return 0;
	}

	const int n = layoutCapacityForWidth(maxWidth);

	return arrangementForCount(n).width;
}

void CarouselView::rebuildArrangement() {
	Arrangement& a = m_arrangement;

	a = Arrangement{};

	const qreal W = width() > 0 ? width() : targetWidthForMaxWidth(m_maxWidth);

	if (W <= 0 || m_focalWidth <= 0) {
		a = arrangementForCount(0);
		a.width = W;

		return;
	}

	if (m_layoutCapacity <= 0) {
		m_layoutCapacity = layoutCapacityForWidth(m_maxWidth);
	}

	if (m_layoutCapacity <= 0) {
		a = arrangementForCount(0);
		a.width = W;

		return;
	}

	const Arrangement full = arrangementForCount(m_layoutCapacity);

	if (W >= full.width) {
		a = full;
		return;
	}

	int highN = 1;

	for (int n = 1; n <= m_layoutCapacity; ++n) {
		const Arrangement candidate = arrangementForCount(n);

		if (W < candidate.width) {
			highN = n;
			break;
		}
	}

	const int lowN = qMax(0, highN - 1);

	const Arrangement high = arrangementForCount(highN);

	const Arrangement low = arrangementForCount(lowN);

	if (high.width <= low.width) {
		a = low;
		return;
	}

	qreal t = (high.width - W) / (high.width - low.width);

	t = qBound<qreal>(0.0, t, 1.0);

	a.n = highN;
	a.width = W;

	a.sizes.resize(highN + 1);

	a.sizes[0] = m_focalWidth;

	for (int i = 1; i < highN; ++i) {
		a.sizes[i] = high.sizes[i] + (low.sizes[i] - high.sizes[i]) * t;
	}

	a.sizes[highN] = high.sizes[highN] * (1.0 - t);

	a.outerSpacing = m_itemSpacing * (1.0 - t);

	a.centers.append(0);

	qreal center = 0;

	for (int i = 1; i <= highN; ++i) {
		const qreal gap = i == highN ? a.outerSpacing : m_itemSpacing;

		center += a.sizes[i - 1] / 2.0 + gap + a.sizes[i] / 2.0;

		a.centers.append(center);
	}

	a.effectiveExtraWidth = 0;

	for (int i = 1; i <= highN; ++i) {
		a.effectiveExtraWidth += pitch() - a.sizes[i];
	}
}

qreal CarouselView::keylineSize(int io) const {
	const int n = m_arrangement.n;

	const int i = std::abs(io);

	if (i == 0) return m_focalWidth;

	if (i <= n) return m_arrangement.sizes[i];

	return 0;
}

qreal CarouselView::sampleSize(qreal childLoc) const {
	const int n = m_arrangement.n;

	if (n <= 0) return 0;

	const int minIo = -(n + 2);

	const int maxIo = n + 2;

	const qreal p = pitch();

	const int a = static_cast<int>(std::floor(childLoc / p));

	if (a < minIo) return keylineSize(minIo);

	if (a >= maxIo) return keylineSize(maxIo);

	const int b = a + 1;

	const qreal t = (childLoc - a * p) / p;

	return keylineSize(a) + (keylineSize(b) - keylineSize(a)) * t;
}

int CarouselView::realIndexOf(int virtualIndex) const {
	const int count = m_model.size();

	if (count == 0) return -1;

	return ((virtualIndex % count) + count) % count;
}

qreal CarouselView::contentXForIndex(qreal index) const {
	return index * pitch() + pitch() / 2.0 - width() / 2.0;
}

int CarouselView::indexForContentX(qreal x) const {
	return static_cast<int>(
		std::round((x + width() / 2.0 - pitch() / 2.0) / pitch()));
}

qreal CarouselView::snapTargetX(qreal x) const {
	return contentXForIndex(indexForContentX(x));
}

void CarouselView::wrapContentIfNeeded() {
	const int count = m_model.size();

	if (count == 0) return;

	const qreal blockWidth = count * pitch();

	const qreal centerX = contentXForIndex(m_centerBlockStart);

	const int blocksFromCenter =
		static_cast<int>(std::round((m_contentX - centerX) / blockWidth));

	if (std::abs(blocksFromCenter) < 2) return;

	const int indexShift = blocksFromCenter * count;

	const qreal shift = indexShift * pitch();

	m_contentX -= shift;
	m_currentVirtualIndex -= indexShift;

	auto shiftAnimation = [shift](QVariantAnimation* anim) {
		if (anim->state() != QAbstractAnimation::Running) {
			return;
		}

		anim->setStartValue(anim->startValue().toReal() - shift);

		anim->setEndValue(anim->endValue().toReal() - shift);
	};

	shiftAnimation(m_glideAnim);
	shiftAnimation(m_flickAnim);

	for (Slot& s : m_slots) {
		if (s.active) s.virtualIndex -= indexShift;
	}
}

void CarouselView::updateCurrentIndexFromContentX(bool emitPreview) {
	const int count = m_model.size();

	if (count == 0) return;

	const int newVirtual = static_cast<int>(
		std::round((m_contentX + width() / 2.0 - pitch() / 2.0) / pitch()));

	if (newVirtual == m_currentVirtualIndex) {
		return;
	}

	m_currentVirtualIndex = newVirtual;

	const int newReal = realIndexOf(newVirtual);

	if (newReal != m_currentRealIndex) {
		m_currentRealIndex = newReal;

		notifyCurrentIndexChanged();
	}

	if (emitPreview && newReal != m_previewRealIndex) {
		m_previewRealIndex = newReal;
		notifyPreviewIndexChanged(newReal);
	}
}

QQuickItem* CarouselView::acquireItem() {
	if (!m_delegate) return nullptr;

	QQmlContext* ctx = QQmlEngine::contextForObject(this);

	QVariantMap initial;

	initial.insert(QStringLiteral("isCurrent"), false);

	initial.insert(QStringLiteral("modelData"), QVariant());

	initial.insert(QStringLiteral("tileHeight"), m_tileHeight);

	initial.insert(QStringLiteral("tileWidth"), m_focalWidth);

	for (auto it = m_delegateProperties.constBegin();
		 it != m_delegateProperties.constEnd();
		 ++it) {
		initial.insert(it.key(), it.value());
	}

	QObject* obj = m_delegate->createWithInitialProperties(initial, ctx);

	if (!obj) {
		qWarning(
			"CarouselView: failed to create delegate instance: %s",
			qPrintable(m_delegate->errorString()));

		return nullptr;
	}

	QQuickItem* item = qobject_cast<QQuickItem*>(obj);

	if (!item) {
		qWarning("CarouselView: delegate root is not an Item");

		delete obj;

		return nullptr;
	}

	item->setParentItem(this);

	QQmlEngine::setObjectOwnership(item, QQmlEngine::CppOwnership);

	const QMetaMethod activateSignal = item->metaObject()->method(
		item->metaObject()->indexOfSignal("requestActivate()"));

	if (activateSignal.isValid()) {
		static const QMetaMethod slot = metaObject()->method(
			metaObject()->indexOfSlot("handleDelegateActivate()"));

		connect(item, activateSignal, this, slot);
	}

	return item;
}

void CarouselView::applyPropertiesToItem(QQuickItem* item, int realIndex) const {
	if (!item) return;

	const bool isCurrent = realIndex == m_currentRealIndex;

	QQmlProperty::write(item, QStringLiteral("isCurrent"), isCurrent);

	QQmlProperty::write(
		item,
		QStringLiteral("modelData"),
		realIndex >= 0 && realIndex < m_model.size() ? m_model.at(realIndex)
													 : QVariant());

	QQmlProperty::write(item, QStringLiteral("tileHeight"), m_tileHeight);
}

void CarouselView::updateSlotContent(Slot& slot, int virtualIndex) {
	if (!slot.item) slot.item = acquireItem();

	if (!slot.item) return;

	slot.virtualIndex = virtualIndex;

	slot.realIndex = realIndexOf(virtualIndex);

	slot.active = slot.realIndex >= 0;

	slot.item->setVisible(slot.active);

	if (slot.active) {
		applyPropertiesToItem(slot.item, slot.realIndex);
	}
}

qreal CarouselView::continuousIndex() const {
	return (m_contentX + width() / 2.0 - pitch() / 2.0) / pitch();
}

void CarouselView::positionSlot(const Slot& slot) const {
	if (!slot.item || !slot.active) return;

	const qreal current = continuousIndex();

	const int leftIndex = static_cast<int>(std::floor(current));

	const qreal fraction = current - leftIndex;

	const int v = slot.virtualIndex;

	const qreal childLoc = (qreal(v) - current) * pitch();

	const qreal size = sampleSize(childLoc);

	if (size <= 0.5) {
		slot.item->setVisible(false);
		return;
	}

	const int n = m_arrangement.n;

	const auto rightGap = [this, n](int rightIndex) {
		return rightIndex == n ? m_arrangement.outerSpacing : m_itemSpacing;
	};

	const auto leftGap = [this, n](int leftIndex) {
		return leftIndex == -n ? m_arrangement.outerSpacing : m_itemSpacing;
	};

	qreal center = 0;

	if (std::abs(fraction) < 1e-6) {
		if (v == leftIndex) {
			center = 0;
		} else if (v < leftIndex) {
			qreal x = 0;

			qreal previousSize = sampleSize(0);

			for (int i = leftIndex - 1; i >= v; --i) {
				const qreal s = sampleSize((qreal(i) - current) * pitch());

				const qreal gap = leftGap(i);

				x -= previousSize / 2.0 + gap + s / 2.0;

				previousSize = s;
			}

			center = x;
		} else {
			qreal x = 0;

			qreal previousSize = sampleSize(0);

			for (int i = leftIndex + 1; i <= v; ++i) {
				const qreal s = sampleSize((qreal(i) - current) * pitch());

				const qreal gap = rightGap(i);

				x += previousSize / 2.0 + gap + s / 2.0;

				previousSize = s;
			}

			center = x;
		}
	} else {
		const qreal leftSize =
			sampleSize((qreal(leftIndex) - current) * pitch());

		const qreal rightSize =
			sampleSize((qreal(leftIndex + 1) - current) * pitch());

		qreal centerGap = m_itemSpacing;

		if (leftIndex == -n || leftIndex + 1 == n) {
			centerGap = m_arrangement.outerSpacing;
		}

		const qreal distance = leftSize / 2.0 + centerGap + rightSize / 2.0;

		const qreal leftCenter = -fraction * distance;

		const qreal rightCenter = (1.0 - fraction) * distance;

		if (v == leftIndex) {
			center = leftCenter;
		} else if (v == leftIndex + 1) {
			center = rightCenter;
		} else if (v < leftIndex) {
			qreal x = leftCenter;

			qreal previousSize = leftSize;

			for (int i = leftIndex - 1; i >= v; --i) {
				const qreal s = sampleSize((qreal(i) - current) * pitch());

				const qreal gap = leftGap(i);

				x -= previousSize / 2.0 + gap + s / 2.0;

				previousSize = s;
			}

			center = x;
		} else {
			qreal x = rightCenter;

			qreal previousSize = rightSize;

			for (int i = leftIndex + 2; i <= v; ++i) {
				const qreal s = sampleSize((qreal(i) - current) * pitch());

				const qreal gap = rightGap(i);

				x += previousSize / 2.0 + gap + s / 2.0;

				previousSize = s;
			}

			center = x;
		}
	}

	const qreal itemX = width() / 2.0 + center - size / 2.0;

	QQmlProperty::write(slot.item, QStringLiteral("tileWidth"), size);

	slot.item->setX(itemX);
	slot.item->setY(0);
	slot.item->setVisible(size > 0.5);
}

void CarouselView::releaseAllItems() {
	for (Slot& s : m_slots) {
		if (s.item) s.item->deleteLater();
	}

	m_slots.clear();
}

void CarouselView::relayout() {
	const int count = m_model.size();

	if (count == 0 || width() <= 0 || m_focalWidth <= 0) {
		for (Slot& s : m_slots) {
			if (s.item) s.item->setVisible(false);
		}

		updateCurrentItem();
		return;
	}

	const int half = m_layoutCapacity + 2;

	const int lo = m_currentVirtualIndex - half;

	const int hi = m_currentVirtualIndex + half;

	const int needed = hi - lo + 1;

	while (m_slots.size() < needed)
		m_slots.append(Slot{});

	QHash<int, int> virtualToSlot;

	virtualToSlot.reserve(m_slots.size());

	for (int i = 0; i < m_slots.size(); ++i) {
		if (m_slots[i].active) {
			virtualToSlot.insert(m_slots[i].virtualIndex, i);
		}
	}

	QVector<bool> slotUsedThisPass(m_slots.size(), false);

	for (int v = lo; v <= hi; ++v) {
		auto it = virtualToSlot.find(v);

		if (it != virtualToSlot.end()) {
			slotUsedThisPass[it.value()] = true;

			applyPropertiesToItem(
				m_slots[it.value()].item, m_slots[it.value()].realIndex);

			continue;
		}

		int freeSlot = -1;

		for (int i = 0; i < m_slots.size(); ++i) {
			if (slotUsedThisPass[i]) continue;

			if (m_slots[i].active && m_slots[i].virtualIndex >= lo &&
				m_slots[i].virtualIndex <= hi) {
				continue;
			}

			freeSlot = i;
			break;
		}

		if (freeSlot < 0) {
			m_slots.append(Slot{});
			slotUsedThisPass.append(false);

			freeSlot = m_slots.size() - 1;
		}

		updateSlotContent(m_slots[freeSlot], v);

		slotUsedThisPass[freeSlot] = true;
	}

	for (int i = 0; i < m_slots.size(); ++i) {
		if (!slotUsedThisPass[i] && m_slots[i].item) {
			m_slots[i].active = false;
			m_slots[i].item->setVisible(false);
		}
	}

	for (const Slot& s : std::as_const(m_slots)) {
		positionSlot(s);
	}

	updateCurrentItem();
}

void CarouselView::cancelAnimations() {
	if (m_glideAnim->state() == QAbstractAnimation::Running) {
		m_glideAnim->stop();
	}

	if (m_flickAnim->state() == QAbstractAnimation::Running) {
		m_flickAnim->stop();
	}
}

void CarouselView::glideTo(qreal dest) {
	if (std::abs(dest - m_contentX) < 0.5) return;

	cancelAnimations();

	{
		QSignalBlocker blocker(m_glideAnim);

		m_glideAnim->setDuration(m_glideDuration);
		m_glideAnim->setStartValue(m_contentX);
		m_glideAnim->setEndValue(dest);
		m_glideAnim->start();
	}
}

void CarouselView::snapToNearest() {
	if (m_model.isEmpty()) return;

	glideTo(contentXForIndex(m_currentVirtualIndex));
}

void CarouselView::goToIndex(int realIndex) {
	const int count = m_model.size();

	if (count == 0) return;

	realIndex = qBound(0, realIndex, count - 1);

	const int curBlock =
		static_cast<int>(std::floor(qreal(m_currentVirtualIndex) / count));

	const int current = m_currentVirtualIndex;

	int target = curBlock * count + realIndex;

	if (target - current > count / 2)
		target -= count;
	else if (current - target > count / 2)
		target += count;

	glideTo(contentXForIndex(target));
}

void CarouselView::incrementCurrentIndex() {
	if (m_model.isEmpty()) return;

	glideTo(contentXForIndex(m_currentVirtualIndex + 1));
}

void CarouselView::decrementCurrentIndex() {
	if (m_model.isEmpty()) return;

	glideTo(contentXForIndex(m_currentVirtualIndex - 1));
}

void CarouselView::centerInstantlyOnReal(int realIndex) {
	const int count = m_model.size();

	if (count == 0) return;

	realIndex = qBound(0, realIndex, count - 1);

	cancelAnimations();

	const int curBlock =
		static_cast<int>(std::floor(qreal(m_currentVirtualIndex) / count));

	const int current = m_currentVirtualIndex;

	int target = curBlock * count + realIndex;

	if (target - current > count / 2)
		target -= count;
	else if (current - target > count / 2)
		target += count;

	m_currentVirtualIndex = target;

	m_contentX = contentXForIndex(target);

	const bool changed = realIndex != m_currentRealIndex;

	m_currentRealIndex = realIndex;

	m_previewRealIndex = realIndex;

	if (changed) emit currentIndexChanged();

	notifyPreviewIndexChanged(realIndex);

	relayout();
}

void CarouselView::mousePressEvent(QMouseEvent* event) {
	m_dragging = true;
	m_dragActive = false;

	m_pressPos = event->position();

	m_pressContentX = m_contentX;

	m_dragTimer.start();

	m_velocitySamples.clear();
	addVelocitySample(0, event->position().x());

	event->accept();
}

void CarouselView::mouseMoveEvent(QMouseEvent* event) {
	if (!m_dragging) return;

	const qreal dx = event->position().x() - m_pressPos.x();

	if (!m_dragActive && std::abs(dx) > kDragThreshold) {
		m_dragActive = true;
		m_pressContentX = m_contentX;

		cancelAnimations();
		grabMouse();
	}

	if (!m_dragActive) return;

	m_contentX = m_pressContentX - dx;

	wrapContentIfNeeded();
	updateCurrentIndexFromContentX(true);
	relayout();

	addVelocitySample(m_dragTimer.elapsed(), event->position().x());

	event->accept();
}

void CarouselView::addVelocitySample(qint64 t, qreal x) {
	m_velocitySamples.append({t, x});

	while (m_velocitySamples.size() > 1 &&
		   t - m_velocitySamples.first().t > kVelocitySampleWindowMs) {
		m_velocitySamples.removeFirst();
	}
}

qreal CarouselView::sampledVelocity() const {
	if (m_velocitySamples.size() < 2) return 0;

	const VelocitySample& oldest = m_velocitySamples.first();
	const VelocitySample& newest = m_velocitySamples.last();

	const qreal dt = newest.t - oldest.t;

	if (dt < 8) return 0;

	return (oldest.x - newest.x) / (dt / 1000.0);
}

void CarouselView::mouseReleaseEvent(QMouseEvent* event) {
	if (!m_dragging) return;

	m_dragging = false;

	const bool wasDragActive = m_dragActive;

	ungrabMouse();

	if (!wasDragActive) {
		cancelAnimations();
		snapToNearest();

		event->accept();
		return;
	}

	m_dragActive = false;

	qreal v = qBound(-kMaxFlickVelocity, sampledVelocity(), kMaxFlickVelocity);

	if (std::abs(v) < kMinFlickVelocity) {
		snapToNearest();

		event->accept();
		return;
	}

	const qreal duration = std::abs(v) / (kFlickFriction * 1000.0);
	const qreal distance = (v * (duration / 1000.0)) / 2.0;
	const qreal rawDest = m_contentX + distance;
	const qreal dest = contentXForIndex(indexForContentX(rawDest));

	cancelAnimations();

	{
		QSignalBlocker blocker(m_flickAnim);

		m_flickAnim->setDuration(
			static_cast<int>(qBound<qreal>(120, duration, 900)));
		m_flickAnim->setEasingCurve(QEasingCurve::OutQuad);
		m_flickAnim->setStartValue(m_contentX);
		m_flickAnim->setEndValue(dest);
		m_flickAnim->start();
	}

	event->accept();
}

void CarouselView::mouseUngrabEvent() {
	m_dragging = false;
	m_dragActive = false;
}

void CarouselView::wheelEvent(QWheelEvent* event) {
	const int steps = event->angleDelta().y() / 120;

	if (steps > 0)
		decrementCurrentIndex();
	else if (steps < 0)
		incrementCurrentIndex();

	event->accept();
}

void CarouselView::handleDelegateActivate() {
	QQuickItem* item = qobject_cast<QQuickItem*>(sender());

	if (!item) return;

	for (const Slot& s : std::as_const(m_slots)) {
		if (s.item != item || !s.active) {
			continue;
		}

		if (s.realIndex == m_currentRealIndex) {
			emit activated(s.realIndex, m_model.at(s.realIndex));
		} else {
			goToIndex(s.realIndex);
		}

		return;
	}
}

} // namespace ZShell::components
