/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * 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.
 *
 * 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 <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <algorithm> /* ::std::sort */
#include <QAction>
#include <QMimeData>

#include "src/datovka_shared/log/log.h"
#include "src/models/action_list_model.h"

bool ActionListModel::Entry::isValid(void) const
{
	return controlObject != Q_NULLPTR;
}

ActionListModel::ActionListModel(QObject *parent)
    : QAbstractListModel(parent),
    m_checking(false),
    m_actionList()
{
}

int ActionListModel::rowCount(const QModelIndex &parent) const
{
	if (parent.isValid()) {
		return 0;
	} else {
		return m_actionList.size();
	}
}

QVariant ActionListModel::data(const QModelIndex &index, int role) const
{
	const int row = index.row();

	if (Q_UNLIKELY((row >= m_actionList.size()) ||
	        (index.column() != COL_ACTION_NAME))) {
		return QVariant();
	}

	switch (role) {
	case Qt::DisplayRole:
		{
			QAction *action = qobject_cast<QAction *>(
			    m_actionList.at(row).controlObject);
			if (action != Q_NULLPTR) {
				if (!action->isSeparator()) {
					return action->text();
				} else {
					return QStringLiteral("----------------");
				}
			}
		}
		break;
	case Qt::DecorationRole:
		{
			QAction *action = qobject_cast<QAction *>(
			    m_actionList.at(row).controlObject);
			if (action != Q_NULLPTR) {
				QIcon icon = action->icon();
				if (!icon.isNull()) {
					return icon;
				}
			}
		}
		break;
	case Qt::ToolTipRole:
		{
			QAction *action = qobject_cast<QAction *>(
			    m_actionList.at(row).controlObject);
			if (action != Q_NULLPTR) {
				return action->toolTip();
			}
		}
		break;
	case Qt::CheckStateRole:
		if (m_checking) {
			QAction *action = qobject_cast<QAction *>(
			    m_actionList.at(row).controlObject);
			if (action != Q_NULLPTR) {
				if (!action->isSeparator()) {
					return m_actionList.at(row).checked ?
					    Qt::Checked : Qt::Unchecked;
				}
			}
		}
		break;
	case Qt::AccessibleTextRole:
		{
			QAction *action = qobject_cast<QAction *>(
			    m_actionList.at(row).controlObject);
			if (action != Q_NULLPTR) {
				if (!action->isSeparator()) {
					return headerData(index.column(),
					    Qt::Horizontal).toString() +
					    QLatin1String(" ") + action->text();
				} else {
					return headerData(index.column(),
					    Qt::Horizontal).toString() +
					    QLatin1String(" ") + tr("separator");
				}
			}
		}
		break;
	default:
		break;
	}

	return QVariant();
}

Qt::DropActions ActionListModel::supportedDropActions(void) const
{
	/* The model must provide removeRows() to be able to use move action. */
	return Qt::MoveAction;
}

Qt::ItemFlags ActionListModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);

	if (index.isValid()) {
		defaultFlags |= Qt::ItemIsDragEnabled;
		if (index.column() == COL_ACTION_NAME) {
			if (m_checking) {
				QAction *action = qobject_cast<QAction *>(
				    m_actionList.at(index.row()).controlObject);
				if ((action != Q_NULLPTR) && (!action->isSeparator())) {
					defaultFlags |= Qt::ItemIsUserCheckable;
				}
			}
		}
	} else {
		defaultFlags |= Qt::ItemIsDropEnabled;
	}

	return defaultFlags;
}

/* Custom MIME type. */
#define itemIndexRowListMimeName \
	QLatin1String("application/x-qitemindexrowlist")

QStringList ActionListModel::mimeTypes(void) const
{
	return QStringList(itemIndexRowListMimeName);
}

QMimeData *ActionListModel::mimeData(const QModelIndexList &indexes) const
{
	if (Q_UNLIKELY(indexes.isEmpty())) {
		return Q_NULLPTR;
	}

	QMimeData *mimeData = new (::std::nothrow) QMimeData;
	if (Q_UNLIKELY(Q_NULLPTR == mimeData)) {
		return Q_NULLPTR;
	}

	/*
	 * TODO -- In order to encompass full entry description then
	 * conversion to QVariant is needed.
	 * See QMimeData::setData() documentation.
	 */

	/* Convert row numbers into mime data. */
	QByteArray data;
	for (const QModelIndex &idx : indexes) {
		if (idx.isValid()) {
			qint64 row = idx.row();
			data.append((const char *)&row, sizeof(row));
		}
	}

	mimeData->setData(itemIndexRowListMimeName, data);

	return mimeData;
}

#if (QT_VERSION < QT_VERSION_CHECK(5, 4, 1))
/*
 * There are documented bugs with regard to
 * QAbstractItemModel::canDropMimeData() in Qt prior to version 5.4.1.
 * See QTBUG-32362 and QTBUG-30534.
 */
#warning "Compiling against version < Qt-5.4.1 which may have bugs around QAbstractItemModel::dropMimeData()."
#endif /* < Qt-5.4.1 */

bool ActionListModel::canDropMimeData(const QMimeData *data,
    Qt::DropAction action, int row, int column, const QModelIndex &parent) const
{
	Q_UNUSED(row);
	Q_UNUSED(column);

	if ((Q_NULLPTR == data) || (Qt::MoveAction != action) ||
	        parent.isValid()) {
		return false;
	}

	return data->hasFormat(itemIndexRowListMimeName);
}

bool ActionListModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
    int row, int column, const QModelIndex &parent)
{
	if (!canDropMimeData(data, action, row, column, parent)) {
		return false;
	}

	if (row < 0) {
		/*
		 * Dropping onto root node. This usually occurs when dropping
		 * on empty space behind last entry. Treat as dropping past
		 * last element.
		 */
		row = rowCount();
	}

	QList<int> droppedRows;
	{
		/* Convert mime data into row numbers. */
		QByteArray bytes(data->data(itemIndexRowListMimeName));
		qint64 row;
		const char *constBytes = bytes.constData();
		for (int offs = 0; offs < bytes.size(); offs += sizeof(row)) {
			row = *(qint64 *)(constBytes + offs);
			droppedRows.append(row);
		}
	}

	if (Q_UNLIKELY(droppedRows.isEmpty())) {
		logErrorNL("%s", "Got drop with no entries.");
		Q_ASSERT(0);
		return false;
	}

	::std::sort(droppedRows.begin(), droppedRows.end());

	if (droppedRows.size() > 1) {
		logWarningNL("%s",
		    "Cannot process drops of multiple entries at once.");
		return false;
	}

	moveRows(QModelIndex(), droppedRows.at(0), 1, parent, row);

	return false; /* If false is returned then removeRows() won't be triggered. */
}

bool ActionListModel::setData(const QModelIndex &index, const QVariant &value,
    int role)
{
	if (Q_UNLIKELY(!index.isValid())) {
		return false;
	}

	const int row = index.row();
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return false;
	}

	if ((index.column() != COL_ACTION_NAME) || (role != Qt::CheckStateRole)) {
		return QAbstractListModel::setData(index, value, role);
	}

	const bool newVal = (value == Qt::Checked);
	if (newVal != m_actionList.at(row).checked) {
		m_actionList[row].checked = newVal;
		emit dataChanged(index, index, {Qt::CheckStateRole});
		return true;
	}

	return false;
}

QVariant ActionListModel::headerData(int section, Qt::Orientation orientation,
    int role) const
{
	Q_UNUSED(orientation);

	if (role != Qt::DisplayRole) {
		return QVariant();
	}

	if (0 == section) {
		return tr("Operations");
	} else {
		return QVariant();
	}
}

bool ActionListModel::moveRows(const QModelIndex &sourceParent, int sourceRow,
    int count, const QModelIndex &destinationParent,
    int destinationChild)
{
	if (Q_UNLIKELY(sourceParent.isValid() || destinationParent.isValid())) {
		/* Only moves within root node are allowed. */
		return false;
	}

	if (Q_UNLIKELY((sourceRow < 0) || (sourceRow >= rowCount()) ||
	        (count < 0) || ((sourceRow + count) > rowCount()) ||
	        (destinationChild < 0) || (destinationChild > rowCount()))) {
		/* Boundaries or location outside limits. */
		return false;
	}

	if (Q_UNLIKELY(count == 0)) {
		return true;
	}

	if (Q_UNLIKELY((sourceRow <= destinationChild) &&
	        (destinationChild <= (sourceRow + count)))) {
		/* Destination lies within source. */
		return true;
	}

	int newPosition; /* Position after removal. */
	if (destinationChild < sourceRow) {
		newPosition = destinationChild;
	} else if (destinationChild > (sourceRow + count)) {
		newPosition = destinationChild - count;
	} else {
		Q_ASSERT(0);
		return false;
	}

	beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1,
	    destinationParent, destinationChild);

	QList<Entry> movedData;
	for (int i = sourceRow; i < (sourceRow + count); ++i) {
		movedData.append(m_actionList.takeAt(sourceRow));
	}

	for (int i = 0; i < count; ++i) {
		m_actionList.insert(newPosition + i, movedData.takeAt(0));
	}
	Q_ASSERT(movedData.isEmpty());

	endMoveRows();

	return true;
}

void ActionListModel::setChecking(bool checking)
{
	if (m_checking == checking) {
		return;
	}

	m_checking = checking;

	if (rowCount() > 0) {
		emit dataChanged(index(0, 0),
		    index(rowCount() - 1, COL_ACTION_NAME));
	}
}

void ActionListModel::insertAction(const Entry &entry)
{
	if (Q_UNLIKELY(!entry.isValid())) {
		Q_ASSERT(0);
		return;
	}

	int pos = m_actionList.size();

	beginInsertRows(QModelIndex(), pos, pos);

	m_actionList.append(entry);

	endInsertRows();
}

void ActionListModel::insertActions(const QList<Entry> &entryList)
{
	if (Q_UNLIKELY(entryList.isEmpty())) {
		return;
	}

	for (const Entry &entry : entryList) {
		if (Q_UNLIKELY(!entry.isValid())) {
			Q_ASSERT(0);
			return;
		}
	}

	int first = m_actionList.size();
	int last = first + entryList.size() - 1;

	beginInsertRows(QModelIndex(), first, last);

	m_actionList.append(entryList);

	endInsertRows();
}

void  ActionListModel::insertAction(int pos, const Entry &entry)
{
	if (Q_UNLIKELY(pos < 0)) {
		return;
	}

	if (pos >= m_actionList.size()) {
		/* Just append new data. */
		insertAction(entry);
		return;
	}

	if (Q_UNLIKELY(!entry.isValid())) {
		Q_ASSERT(0);
		return;
	}

	beginInsertRows(QModelIndex(), pos, pos);

	m_actionList.insert(pos, entry);

	endInsertRows();
}

void ActionListModel::insertActions(int pos, const QList<Entry> &entryList)
{
	if (Q_UNLIKELY(pos < 0)) {
		return;
	}

	if (pos >= m_actionList.size()) {
		/* Just append new data. */
		insertActions(entryList);
		return;
	}

	if (Q_UNLIKELY(entryList.isEmpty())) {
		return;
	}

	for (const Entry &entry : entryList) {
		if (Q_UNLIKELY(!entry.isValid())) {
			Q_ASSERT(0);
			return;
		}
	}

	int first = pos;
	int last = first + entryList.size() - 1;

	beginInsertRows(QModelIndex(), first, last);

	for (const Entry &entry : entryList) {
		m_actionList.insert(pos, entry);
		++pos;
	}

	endInsertRows();
}

void ActionListModel::deleteActions(const QList<int> &rows)
{
	if (Q_UNLIKELY(rows.isEmpty())) {
		return;
	}

	/* Sort rows. */
	QList<int> sorted = rows;
	::std::sort(sorted.begin(), sorted.end());

	/* Iterate in reverse. */
	QList<int>::const_iterator it = sorted.constEnd();
	while (it != sorted.constBegin()) {
		--it;
		int row = *it;

		if (row < m_actionList.size()) {
			beginRemoveRows(QModelIndex(), row, row);

			m_actionList.removeAt(row);

			endRemoveRows();
		}
	}
}

ActionListModel::Entry ActionListModel::entryAt(int row) const
{
	if (Q_UNLIKELY((row < 0) || (row >= m_actionList.size()))) {
		Q_ASSERT(0);
		return Entry();
	}

	return m_actionList.at(row);
}

const QList<ActionListModel::Entry> &ActionListModel::entries(void) const
{
	return m_actionList;
}

void ActionListModel::removeAllRows(void)
{
	beginResetModel();
	m_actionList.clear();
	endResetModel();
}

void ActionListModel::setSeparatorPointer(int pos, QObject *controlObject)
{
	if (Q_UNLIKELY(pos >= rowCount())) {
		Q_ASSERT(0);
		return;
	}

	/*
	 * Held control object may not be valid here.
	 * There's not point in checking whether it is a separator.
	 */

	QAction *action = qobject_cast<QAction *>(controlObject);
	if (Q_UNLIKELY((action == Q_NULLPTR) || (!action->isSeparator()))) {
		Q_ASSERT(0);
		return;
	}

	m_actionList[pos].controlObject = controlObject;

	QModelIndex idx = index(pos, COL_ACTION_NAME);
	emit dataChanged(idx, idx);
}
