/* SPDX-FileCopyrightText: 2026 Project Tick
 * SPDX-FileContributor: Project Tick
 * SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-MeshMC-MMCO-Module-Exception-1.0
 *
 *   MeshMC - A Custom Launcher for Minecraft
 *   Copyright (C) 2026 Project Tick
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version, with the additional permission
 *   described in the MeshMC MMCO Module Exception 1.0.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 *   You should have received a copy of the MeshMC MMCO Module Exception 1.0
 *   along with this program.  If not, see <https://projecttick.org/licenses/>.
 *
 *  This file incorporates work covered by the following copyright and
 *  permission notice:
 *
 * Copyright 2013-2021 MultiMC Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "InstanceView.h"

#include <QPainter>
#include <QApplication>
#include <QtMath>
#include <QMouseEvent>
#include <QListView>
#include <QPersistentModelIndex>
#include <QDrag>
#include <QMimeData>
#include <QCache>
#include <QScrollBar>
#include <QAccessible>

#include "VisualGroup.h"
#include <QDebug>

#include <Application.h>
#include <InstanceList.h>

template <typename T> bool listsIntersect(const QList<T>& l1, const QList<T> t2)
{
	for (auto& item : l1) {
		if (t2.contains(item)) {
			return true;
		}
	}
	return false;
}

InstanceView::InstanceView(QWidget* parent) : QAbstractItemView(parent)
{
	setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
	setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
	setAcceptDrops(true);
	setAutoScroll(true);
}

InstanceView::~InstanceView()
{
	qDeleteAll(m_groups);
	m_groups.clear();
}

void InstanceView::setModel(QAbstractItemModel* model)
{
	QAbstractItemView::setModel(model);
	connect(model, &QAbstractItemModel::modelReset, this,
			&InstanceView::modelReset);
	connect(model, &QAbstractItemModel::rowsRemoved, this,
			&InstanceView::rowsRemoved);
}

void InstanceView::dataChanged(const QModelIndex& topLeft,
							   const QModelIndex& bottomRight,
							   const QVector<int>& roles)
{
	scheduleDelayedItemsLayout();
}
void InstanceView::rowsInserted(const QModelIndex& parent, int start, int end)
{
	scheduleDelayedItemsLayout();
}

void InstanceView::rowsAboutToBeRemoved(const QModelIndex& parent, int start,
										int end)
{
	scheduleDelayedItemsLayout();
}

void InstanceView::modelReset()
{
	scheduleDelayedItemsLayout();
}

void InstanceView::rowsRemoved()
{
	scheduleDelayedItemsLayout();
}

void InstanceView::currentChanged(const QModelIndex& current,
								  const QModelIndex& previous)
{
	QAbstractItemView::currentChanged(current, previous);
	// TODO: for accessibility support, implement+register a factory, steal
	// QAccessibleTable from Qt and return an instance of it for InstanceView.
#ifndef QT_NO_ACCESSIBILITY
	if (QAccessible::isActive() && current.isValid()) {
		QAccessibleEvent event(this, QAccessible::Focus);
		event.setChild(current.row());
		QAccessible::updateAccessibility(&event);
	}
#endif /* !QT_NO_ACCESSIBILITY */
}

class LocaleString : public QString
{
  public:
	LocaleString(const char* s) : QString(s) {}
	LocaleString(const QString& s) : QString(s) {}
};

inline bool operator<(const LocaleString& lhs, const LocaleString& rhs)
{
	return (QString::localeAwareCompare(lhs, rhs) < 0);
}

void InstanceView::updateScrollbar()
{
	int previousScroll = verticalScrollBar()->value();
	if (m_groups.isEmpty()) {
		verticalScrollBar()->setRange(0, 0);
	} else {
		int totalHeight = 0;
		// top margin
		totalHeight += m_categoryMargin;
		int itemScroll = 0;
		for (auto category : m_groups) {
			category->m_verticalPosition = totalHeight;
			totalHeight += category->totalHeight() + m_categoryMargin;
			if (!itemScroll && category->totalHeight() != 0) {
				itemScroll = category->contentHeight() / category->numRows();
			}
		}
		// do not divide by zero
		if (itemScroll == 0)
			itemScroll = 64;

		totalHeight += m_bottomMargin;
		verticalScrollBar()->setSingleStep(itemScroll);
		const int rowsPerPage = qMax(viewport()->height() / itemScroll, 1);
		verticalScrollBar()->setPageStep(rowsPerPage * itemScroll);

		verticalScrollBar()->setRange(0, totalHeight - height());
	}

	verticalScrollBar()->setValue(
		qMin(previousScroll, verticalScrollBar()->maximum()));
}

void InstanceView::updateGeometries()
{
	geometryCache.clear();

	QMap<LocaleString, VisualGroup*> cats;

	for (int i = 0; i < model()->rowCount(); ++i) {
		const QString groupName =
			model()->index(i, 0).data(InstanceViewRoles::GroupRole).toString();
		if (!cats.contains(groupName)) {
			VisualGroup* old = this->category(groupName);
			if (old) {
				auto cat = new VisualGroup(old);
				cats.insert(groupName, cat);
				cat->update();
			} else {
				auto cat = new VisualGroup(groupName, this);
				if (fVisibility) {
					cat->collapsed = fVisibility(groupName);
				}
				cats.insert(groupName, cat);
				cat->update();
			}
		}
	}

	qDeleteAll(m_groups);
	m_groups = cats.values();
	updateScrollbar();
	viewport()->update();
}

bool InstanceView::isIndexHidden(const QModelIndex& index) const
{
	VisualGroup* cat = category(index);
	if (cat) {
		return cat->collapsed;
	} else {
		return false;
	}
}

VisualGroup* InstanceView::category(const QModelIndex& index) const
{
	return category(index.data(InstanceViewRoles::GroupRole).toString());
}

VisualGroup* InstanceView::category(const QString& cat) const
{
	for (auto group : m_groups) {
		if (group->text == cat) {
			return group;
		}
	}
	return nullptr;
}

VisualGroup* InstanceView::categoryAt(const QPoint& pos,
									  VisualGroup::HitResults& result) const
{
	for (auto group : m_groups) {
		result = group->hitScan(pos);
		if (result != VisualGroup::NoHit) {
			return group;
		}
	}
	result = VisualGroup::NoHit;
	return nullptr;
}

QString InstanceView::groupNameAt(const QPoint& point)
{
	executeDelayedItemsLayout();

	VisualGroup::HitResults hitresult;
	auto group = categoryAt(point + offset(), hitresult);
	if (group &&
		(hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit))) {
		return group->text;
	}
	return QString();
}

int InstanceView::calculateItemsPerRow() const
{
	return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing));
}

int InstanceView::contentWidth() const
{
	return width() - m_leftMargin - m_rightMargin;
}

int InstanceView::itemWidth() const
{
	return m_itemWidth;
}

void InstanceView::mousePressEvent(QMouseEvent* event)
{
	executeDelayedItemsLayout();

	QPoint visualPos = event->pos();
	QPoint geometryPos = event->pos() + offset();

	QPersistentModelIndex index = indexAt(visualPos);

	m_pressedIndex = index;
	m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
	m_pressedPosition = geometryPos;

	VisualGroup::HitResults hitresult;
	m_pressedCategory = categoryAt(geometryPos, hitresult);
	if (m_pressedCategory && hitresult & VisualGroup::CheckboxHit) {
		setState(m_pressedCategory->collapsed ? ExpandingState
											  : CollapsingState);
		event->accept();
		return;
	}

	if (index.isValid() && (index.flags() & Qt::ItemIsEnabled)) {
		if (index != currentIndex()) {
			// FIXME: better!
			m_currentCursorColumn = -1;
		}
		// we disable scrollTo for mouse press so the item doesn't change
		// position when the user is interacting with it (ie. clicking on it)
		bool autoScroll = hasAutoScroll();
		setAutoScroll(false);
		selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);

		setAutoScroll(autoScroll);
		QRect rect(visualPos, visualPos);
		setSelection(rect, QItemSelectionModel::ClearAndSelect);

		// signal handlers may change the model
		emit pressed(index);
	} else {
		// Forces a finalize() even if mouse is pressed, but not on a item
		selectionModel()->select(QModelIndex(), QItemSelectionModel::Select);
	}
}

void InstanceView::mouseMoveEvent(QMouseEvent* event)
{
	executeDelayedItemsLayout();

	QPoint topLeft;
	QPoint visualPos = event->pos();
	QPoint geometryPos = event->pos() + offset();

	if (state() == ExpandingState || state() == CollapsingState) {
		return;
	}

	if (state() == DraggingState) {
		topLeft = m_pressedPosition - offset();
		if ((topLeft - event->pos()).manhattanLength() >
			QApplication::startDragDistance()) {
			m_pressedIndex = QModelIndex();
			startDrag(model()->supportedDragActions());
			setState(NoState);
			stopAutoScroll();
		}
		return;
	}

	if (selectionMode() != SingleSelection) {
		topLeft = m_pressedPosition - offset();
	} else {
		topLeft = geometryPos;
	}

	if (m_pressedIndex.isValid() && (state() != DragSelectingState) &&
		(event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty()) {
		setState(DraggingState);
		return;
	}

	if ((event->buttons() & Qt::LeftButton) && selectionModel()) {
		setState(DragSelectingState);

		setSelection(QRect(visualPos, visualPos),
					 QItemSelectionModel::ClearAndSelect);
		QModelIndex index = indexAt(visualPos);

		// set at the end because it might scroll the view
		if (index.isValid() && (index != selectionModel()->currentIndex()) &&
			(index.flags() & Qt::ItemIsEnabled)) {
			selectionModel()->setCurrentIndex(index,
											  QItemSelectionModel::NoUpdate);
		}
	}
}

void InstanceView::mouseReleaseEvent(QMouseEvent* event)
{
	executeDelayedItemsLayout();

	QPoint visualPos = event->pos();
	QPoint geometryPos = event->pos() + offset();
	QPersistentModelIndex index = indexAt(visualPos);

	VisualGroup::HitResults hitresult;

	bool click = (index == m_pressedIndex && index.isValid()) ||
				 (m_pressedCategory &&
				  m_pressedCategory == categoryAt(geometryPos, hitresult));

	if (click && m_pressedCategory) {
		if (state() == ExpandingState) {
			m_pressedCategory->collapsed = false;
			emit groupStateChanged(m_pressedCategory->text, false);

			updateGeometries();
			viewport()->update();
			event->accept();
			m_pressedCategory = nullptr;
			setState(NoState);
			return;
		} else if (state() == CollapsingState) {
			m_pressedCategory->collapsed = true;
			emit groupStateChanged(m_pressedCategory->text, true);

			updateGeometries();
			viewport()->update();
			event->accept();
			m_pressedCategory = nullptr;
			setState(NoState);
			return;
		}
	}

	m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate;

	setState(NoState);

	if (click) {
		if (event->button() == Qt::LeftButton) {
			emit clicked(index);
		}
		QStyleOptionViewItem option = viewOptions();
		if (m_pressedAlreadySelected) {
			option.state |= QStyle::State_Selected;
		}
		if ((model()->flags(index) & Qt::ItemIsEnabled) &&
			style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick,
							   &option, this)) {
			emit activated(index);
		}
	}
}

void InstanceView::mouseDoubleClickEvent(QMouseEvent* event)
{
	executeDelayedItemsLayout();

	QModelIndex index = indexAt(event->pos());
	if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) ||
		(m_pressedIndex != index)) {
		QMouseEvent me(QEvent::MouseButtonPress, event->localPos(),
					   event->windowPos(), event->screenPos(), event->button(),
					   event->buttons(), event->modifiers());
		mousePressEvent(&me);
		return;
	}
	// signal handlers may change the model
	QPersistentModelIndex persistent = index;
	emit doubleClicked(persistent);

	QStyleOptionViewItem option = viewOptions();
	if ((model()->flags(index) & Qt::ItemIsEnabled) &&
		!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick,
							&option, this)) {
		emit activated(index);
	}
}

void InstanceView::paintEvent(QPaintEvent* event)
{
	executeDelayedItemsLayout();

	QPainter painter(this->viewport());

	QStyleOptionViewItem option(viewOptions());
	option.widget = this;

	int wpWidth = viewport()->width();
	option.rect.setWidth(wpWidth);
	for (int i = 0; i < m_groups.size(); ++i) {
		VisualGroup* category = m_groups.at(i);
		int y = category->verticalPosition();
		y -= verticalOffset();
		QRect backup = option.rect;
		int height = category->totalHeight();
		option.rect.setTop(y);
		option.rect.setHeight(height);
		option.rect.setLeft(m_leftMargin);
		option.rect.setRight(wpWidth - m_rightMargin);
		category->drawHeader(&painter, option);
		y += category->totalHeight() + m_categoryMargin;
		option.rect = backup;
	}

	for (int i = 0; i < model()->rowCount(); ++i) {
		const QModelIndex index = model()->index(i, 0);
		if (isIndexHidden(index)) {
			continue;
		}
		Qt::ItemFlags flags = index.flags();
		option.rect = visualRect(index);
		option.features |= QStyleOptionViewItem::WrapText;
		if (flags & Qt::ItemIsSelectable &&
			selectionModel()->isSelected(index)) {
			option.state |= selectionModel()->isSelected(index)
								? QStyle::State_Selected
								: QStyle::State_None;
		} else {
			option.state &= ~QStyle::State_Selected;
		}
		option.state |= (index == currentIndex()) ? QStyle::State_HasFocus
												  : QStyle::State_None;
		if (!(flags & Qt::ItemIsEnabled)) {
			option.state &= ~QStyle::State_Enabled;
		}
		itemDelegate()->paint(&painter, option, index);
	}

	/*
	 * Drop indicators for manual reordering...
	 */
#if 0
    if (!m_lastDragPosition.isNull())
    {
        QPair<Group *, int> pair = rowDropPos(m_lastDragPosition);
        Group *category = pair.first;
        int row = pair.second;
        if (category)
        {
            int internalRow = row - category->firstItemIndex;
            QLine line;
            if (internalRow >= category->numItems())
            {
                QRect toTheRightOfRect = visualRect(category->lastItem());
                line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight());
            }
            else
            {
                QRect toTheLeftOfRect = visualRect(model()->index(row, 0));
                line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft());
            }
            painter.save();
            painter.setPen(QPen(Qt::black, 3));
            painter.drawLine(line);
            painter.restore();
        }
    }
#endif
}

void InstanceView::resizeEvent(QResizeEvent* event)
{
	int newItemsPerRow = calculateItemsPerRow();
	if (newItemsPerRow != m_currentItemsPerRow) {
		m_currentCursorColumn = -1;
		m_currentItemsPerRow = newItemsPerRow;
		updateGeometries();
	} else {
		updateScrollbar();
	}
}

void InstanceView::dragEnterEvent(QDragEnterEvent* event)
{
	executeDelayedItemsLayout();

	if (!isDragEventAccepted(event)) {
		return;
	}
	m_lastDragPosition = event->pos() + offset();
	viewport()->update();
	event->accept();
}

void InstanceView::dragMoveEvent(QDragMoveEvent* event)
{
	executeDelayedItemsLayout();

	if (!isDragEventAccepted(event)) {
		return;
	}
	m_lastDragPosition = event->pos() + offset();
	viewport()->update();
	event->accept();
}

void InstanceView::dragLeaveEvent(QDragLeaveEvent* event)
{
	executeDelayedItemsLayout();

	m_lastDragPosition = QPoint();
	viewport()->update();
}

void InstanceView::dropEvent(QDropEvent* event)
{
	executeDelayedItemsLayout();

	m_lastDragPosition = QPoint();

	stopAutoScroll();
	setState(NoState);

	auto mimedata = event->mimeData();

	if (event->source() == this) {
		if (event->possibleActions() & Qt::MoveAction) {
			QPair<VisualGroup*, VisualGroup::HitResults> dropPos =
				rowDropPos(event->pos());
			const VisualGroup* group = dropPos.first;
			auto hitresult = dropPos.second;

			if (hitresult == VisualGroup::HitResult::NoHit) {
				viewport()->update();
				return;
			}
			auto instanceId =
				QString::fromUtf8(mimedata->data("application/x-instanceid"));
			auto instanceList = APPLICATION->instances().get();
			instanceList->setInstanceGroup(instanceId, group->text);
			event->setDropAction(Qt::MoveAction);
			event->accept();

			updateGeometries();
			viewport()->update();
		}
		return;
	}

	// check if the action is supported
	if (!mimedata) {
		return;
	}

	// files dropped from outside?
	if (mimedata->hasUrls()) {
		auto urls = mimedata->urls();
		event->accept();
		emit droppedURLs(urls);
	}
}

void InstanceView::startDrag(Qt::DropActions supportedActions)
{
	executeDelayedItemsLayout();

	QModelIndexList indexes = selectionModel()->selectedIndexes();
	if (indexes.count() == 0)
		return;

	QMimeData* data = model()->mimeData(indexes);
	if (!data) {
		return;
	}
	QRect rect;
	QPixmap pixmap = renderToPixmap(indexes, &rect);
	QDrag* drag = new QDrag(this);
	drag->setPixmap(pixmap);
	drag->setMimeData(data);
	drag->setHotSpot(m_pressedPosition - rect.topLeft());
	Qt::DropAction defaultDropAction = Qt::IgnoreAction;
	if (this->defaultDropAction() != Qt::IgnoreAction &&
		(supportedActions & this->defaultDropAction())) {
		defaultDropAction = this->defaultDropAction();
	}
	/*auto action = */
	drag->exec(supportedActions, defaultDropAction);
}

QRect InstanceView::visualRect(const QModelIndex& index) const
{
	const_cast<InstanceView*>(this)->executeDelayedItemsLayout();

	return geometryRect(index).translated(-offset());
}

QRect InstanceView::geometryRect(const QModelIndex& index) const
{
	const_cast<InstanceView*>(this)->executeDelayedItemsLayout();

	if (!index.isValid() || isIndexHidden(index) || index.column() > 0) {
		return QRect();
	}

	int row = index.row();
	if (geometryCache.contains(row)) {
		return *geometryCache[row];
	}

	const VisualGroup* cat = category(index);
	QPair<int, int> pos = cat->positionOf(index);
	int x = pos.first;
	// int y = pos.second;

	QRect out;
	out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 +
			   cat->rowTopOf(index));
	out.setLeft(m_spacing + x * (itemWidth() + m_spacing));
	out.setSize(itemDelegate()->sizeHint(viewOptions(), index));
	geometryCache.insert(row, new QRect(out));
	return out;
}

QModelIndex InstanceView::indexAt(const QPoint& point) const
{
	const_cast<InstanceView*>(this)->executeDelayedItemsLayout();

	for (int i = 0; i < model()->rowCount(); ++i) {
		QModelIndex index = model()->index(i, 0);
		if (visualRect(index).contains(point)) {
			return index;
		}
	}
	return QModelIndex();
}

void InstanceView::setSelection(
	const QRect& rect, const QItemSelectionModel::SelectionFlags commands)
{
	executeDelayedItemsLayout();

	for (int i = 0; i < model()->rowCount(); ++i) {
		QModelIndex index = model()->index(i, 0);
		QRect itemRect = visualRect(index);
		if (itemRect.intersects(rect)) {
			selectionModel()->select(index, commands);
			update(itemRect.translated(-offset()));
		}
	}
}

QPixmap InstanceView::renderToPixmap(const QModelIndexList& indices,
									 QRect* r) const
{
	Q_ASSERT(r);
	auto paintPairs = draggablePaintPairs(indices, r);
	if (paintPairs.isEmpty()) {
		return QPixmap();
	}
	QPixmap pixmap(r->size());
	pixmap.fill(Qt::transparent);
	QPainter painter(&pixmap);
	QStyleOptionViewItem option = viewOptions();
	option.state |= QStyle::State_Selected;
	for (int j = 0; j < paintPairs.count(); ++j) {
		option.rect = paintPairs.at(j).first.translated(-r->topLeft());
		const QModelIndex& current = paintPairs.at(j).second;
		itemDelegate()->paint(&painter, option, current);
	}
	return pixmap;
}

QList<QPair<QRect, QModelIndex>>
InstanceView::draggablePaintPairs(const QModelIndexList& indices,
								  QRect* r) const
{
	Q_ASSERT(r);
	QRect& rect = *r;
	QList<QPair<QRect, QModelIndex>> ret;
	for (int i = 0; i < indices.count(); ++i) {
		const QModelIndex& index = indices.at(i);
		const QRect current = geometryRect(index);
		ret += qMakePair(current, index);
		rect |= current;
	}
	return ret;
}

bool InstanceView::isDragEventAccepted(QDropEvent* event)
{
	return true;
}

QPair<VisualGroup*, VisualGroup::HitResults>
InstanceView::rowDropPos(const QPoint& pos)
{
	VisualGroup::HitResults hitresult;
	auto group = categoryAt(pos + offset(), hitresult);
	return qMakePair(group, hitresult);
}

QPoint InstanceView::offset() const
{
	return QPoint(horizontalOffset(), verticalOffset());
}

QRegion
InstanceView::visualRegionForSelection(const QItemSelection& selection) const
{
	QRegion region;
	for (auto& range : selection) {
		int start_row = range.top();
		int end_row = range.bottom();
		for (int row = start_row; row <= end_row; ++row) {
			int start_column = range.left();
			int end_column = range.right();
			for (int column = start_column; column <= end_column; ++column) {
				QModelIndex index = model()->index(row, column, rootIndex());
				region += visualRect(index); // OK
			}
		}
	}
	return region;
}

QModelIndex
InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction,
						 Qt::KeyboardModifiers modifiers)
{
	auto current = currentIndex();
	if (!current.isValid()) {
		return current;
	}
	auto cat = category(current);
	int group_index = m_groups.indexOf(cat);
	if (group_index < 0)
		return current;

	QPair<int, int> pos = cat->positionOf(current);
	int column = pos.first;
	int row = pos.second;
	if (m_currentCursorColumn < 0) {
		m_currentCursorColumn = column;
	}
	switch (cursorAction) {
		case MoveUp: {
			if (row == 0) {
				int prevgroupindex = group_index - 1;
				while (prevgroupindex >= 0) {
					auto prevgroup = m_groups[prevgroupindex];
					if (prevgroup->collapsed) {
						prevgroupindex--;
						continue;
					}
					int newRow = prevgroup->numRows() - 1;
					int newRowSize = prevgroup->rows[newRow].size();
					int newColumn = m_currentCursorColumn;
					if (m_currentCursorColumn >= newRowSize) {
						newColumn = newRowSize - 1;
					}
					return prevgroup->rows[newRow][newColumn];
				}
			} else {
				int newRow = row - 1;
				int newRowSize = cat->rows[newRow].size();
				int newColumn = m_currentCursorColumn;
				if (m_currentCursorColumn >= newRowSize) {
					newColumn = newRowSize - 1;
				}
				return cat->rows[newRow][newColumn];
			}
			return current;
		}
		case MoveDown: {
			if (row == cat->rows.size() - 1) {
				int nextgroupindex = group_index + 1;
				while (nextgroupindex < m_groups.size()) {
					auto nextgroup = m_groups[nextgroupindex];
					if (nextgroup->collapsed) {
						nextgroupindex++;
						continue;
					}
					int newRowSize = nextgroup->rows[0].size();
					int newColumn = m_currentCursorColumn;
					if (m_currentCursorColumn >= newRowSize) {
						newColumn = newRowSize - 1;
					}
					return nextgroup->rows[0][newColumn];
				}
			} else {
				int newRow = row + 1;
				int newRowSize = cat->rows[newRow].size();
				int newColumn = m_currentCursorColumn;
				if (m_currentCursorColumn >= newRowSize) {
					newColumn = newRowSize - 1;
				}
				return cat->rows[newRow][newColumn];
			}
			return current;
		}
		case MoveLeft: {
			if (column > 0) {
				m_currentCursorColumn = column - 1;
				return cat->rows[row][column - 1];
			}
			// TODO: moving to previous line
			return current;
		}
		case MoveRight: {
			if (column < cat->rows[row].size() - 1) {
				m_currentCursorColumn = column + 1;
				return cat->rows[row][column + 1];
			}
			// TODO: moving to next line
			return current;
		}
		case MoveHome: {
			m_currentCursorColumn = 0;
			return cat->rows[row][0];
		}
		case MoveEnd: {
			auto last = cat->rows[row].size() - 1;
			m_currentCursorColumn = last;
			return cat->rows[row][last];
		}
		default:
			break;
	}
	return current;
}

int InstanceView::horizontalOffset() const
{
	return horizontalScrollBar()->value();
}

int InstanceView::verticalOffset() const
{
	return verticalScrollBar()->value();
}

void InstanceView::scrollContentsBy(int dx, int dy)
{
	scrollDirtyRegion(dx, dy);
	viewport()->scroll(dx, dy);
}

void InstanceView::scrollTo(const QModelIndex& index, ScrollHint hint)
{
	if (!index.isValid())
		return;

	const QRect rect = visualRect(index);
	if (hint == EnsureVisible && viewport()->rect().contains(rect)) {
		viewport()->update(rect);
		return;
	}

	verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint));
}

int InstanceView::verticalScrollToValue(const QModelIndex& index,
										const QRect& rect,
										QListView::ScrollHint hint) const
{
	const QRect area = viewport()->rect();
	const bool above =
		(hint == QListView::EnsureVisible && rect.top() < area.top());
	const bool below =
		(hint == QListView::EnsureVisible && rect.bottom() > area.bottom());

	int verticalValue = verticalScrollBar()->value();
	QRect adjusted =
		rect.adjusted(-spacing(), -spacing(), spacing(), spacing());
	if (hint == QListView::PositionAtTop || above)
		verticalValue += adjusted.top();
	else if (hint == QListView::PositionAtBottom || below)
		verticalValue +=
			qMin(adjusted.top(), adjusted.bottom() - area.height() + 1);
	else if (hint == QListView::PositionAtCenter)
		verticalValue +=
			adjusted.top() - ((area.height() - adjusted.height()) / 2);
	return verticalValue;
}
