/* CheckableListView.cpp, (Created on 14.05.2024) */

/* Copyright (C) 2011-2024 Michael Lugmair
 *
 * This file is part of Sayonara Player
 *
 * 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/>.
 */
#include "CheckableListView.h"
#include "Gui/Utils/Delegates/StyledItemDelegate.h"
#include "Utils/Algorithm.h"

#include <QCheckBox>
#include <QPainter>
#include <QMouseEvent>

namespace Gui
{
	namespace
	{
		constexpr const auto ItemText = Qt::UserRole;
		constexpr const auto ItemCheckState = Qt::UserRole + 1;

		QCheckBox* createCheckboxFromData(const QModelIndex& index, const QRect rect)
		{
			const auto isChecked = index.data(ItemCheckState).toBool();
			const auto text = index.data(ItemText).toString();

			auto* checkBox = new QCheckBox(text);
			checkBox->setChecked(isChecked);
			checkBox->setStyleSheet("background: transparent; padding: 0; margin: 0;");
			checkBox->resize(rect.width(), rect.height());
			checkBox->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);

			return checkBox;
		}
	}

	class Delegate :
		public Gui::StyledItemDelegate
	{
		Q_OBJECT
		public:
			void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
			{
				Gui::StyledItemDelegate::paint(painter, option, index);

				painter->save();
				painter->translate(option.rect.left(), option.rect.top());

				auto* checkBox = createCheckboxFromData(index, option.rect);
				checkBox->render(painter);
				painter->restore();
			}
	};

	class CheckableListModel :
		public QAbstractListModel
	{
		Q_OBJECT
		public:
			explicit CheckableListModel(QObject* parent) :
				QAbstractListModel(parent) {}

			~CheckableListModel() override = default;

			[[nodiscard]] int rowCount([[maybe_unused]] const QModelIndex& parent = {}) const override
			{
				return m_items.count();
			}

			[[nodiscard]] QVariant data(const QModelIndex& index, const int role) const override
			{
				switch(role)
				{
					case ItemText:
						return m_items[index.row()].text;
					case ItemCheckState:
						return m_items[index.row()].checked;
					default:
						return {};
				}
			}

			[[nodiscard]] QList<CheckableItem> items() const { return m_items; }

			void insertItem(const int row, const CheckableItem& item)
			{
				if(row < 0)
				{
					m_items.prepend(item);
				}

				else if(row >= m_items.count())
				{
					m_items.append(item);
				}

				else
				{
					m_items.insert(row, item);
				}

				beginInsertRows({}, 0, 0);
				endInsertRows();

				emit dataChanged(index(0, 0), index(rowCount(), 0));
			}

			void setChecked(const int row, const bool b)
			{
				if((row >= 0) && (row < m_items.count()))
				{
					m_items[row].checked = b;
					emit dataChanged(index(0, 0), index(rowCount(), 0));
				}
			}

			[[nodiscard]] bool isChecked(const int row) const
			{
				return ((row >= 0) && (row < m_items.count()))
				       ? m_items[row].checked
				       : false;
			}

			void clear()
			{
				beginRemoveRows({}, 0, rowCount() - 1);
				m_items.clear();
				endRemoveRows();
			}

			[[nodiscard]] int indexOf(const QString& data) const
			{
				return Util::Algorithm::indexOf(m_items, [&](const auto& item) {
					return (item.text == data);
				});
			}

			[[nodiscard]] Qt::DropActions supportedDropActions() const override { return Qt::MoveAction; }

			[[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override
			{
				const auto defaultFlags = QAbstractListModel::flags(index);
				return index.isValid()
				       ? Qt::ItemIsDragEnabled | defaultFlags
				       : Qt::ItemIsDropEnabled | defaultFlags;
			}

			bool moveRows(const QModelIndex& /*sourceParent*/,
			              const int sourceRow, int /*count*/,
			              const QModelIndex& /*destinationParent*/, int targetRow) override
			{
				// because of single selection we assume only one row here
				const auto item = m_items[sourceRow];
				if(sourceRow > targetRow)
				{
					m_items.removeAt(sourceRow);
					m_items.insert(targetRow, item);
				}

				else if(sourceRow < targetRow)
				{
					m_items.insert(targetRow, item);
					m_items.removeAt(sourceRow);
				}

				emit dataChanged(index(0, 0), index(rowCount(), 0));

				return true;
			}

		private:
			QList<CheckableItem> m_items;
	};

	struct CheckableListView::Private
	{
		CheckableListModel* model;
		bool editStarted {false};

		explicit Private(CheckableListView* view) :
			model {new CheckableListModel(view)} {}
	};

	CheckableListView::CheckableListView(QWidget* parent) :
		QListView {parent},
		m {Pimpl::make<Private>(this)}
	{
		setModel(m->model);
		setItemDelegate(new Delegate());
	}

	CheckableListView::~CheckableListView() = default;

	void CheckableListView::append(const CheckableItem& item) { m->model->insertItem(m->model->rowCount(), item); }

	void CheckableListView::clear() { m->model->clear(); }

	QList<CheckableItem> CheckableListView::items() const { return m->model->items(); }

	void CheckableListView::init()
	{
		setSelectionMode(QAbstractItemView::SingleSelection);
		setSelectionBehavior(QAbstractItemView::SelectRows);
		setDragEnabled(true);
		setAcceptDrops(true);
		setDragDropMode(QAbstractItemView::DragDrop);
		setDefaultDropAction(Qt::MoveAction);
		setDragDropOverwriteMode(false);
		setDropIndicatorShown(true);
		setEditTriggers(QAbstractItemView::NoEditTriggers);
	}

	void CheckableListView::mousePressEvent(QMouseEvent* e)
	{
		QListView::mousePressEvent(e);

		const auto pos = e->pos();
		const auto index = QListView::indexAt(pos);
		const auto isPositionValid = pos.x() < (viewport()->width() - 25);

		m->editStarted = (index.isValid() && isPositionValid);
	}

	void CheckableListView::dragMoveEvent(QDragMoveEvent* e)
	{
		QListView::dragMoveEvent(e);
		m->editStarted = false;
	}

	void CheckableListView::mouseReleaseEvent(QMouseEvent* e)
	{
		if(m->editStarted)
		{
			const auto pos = e->pos();
			const auto index = QListView::indexAt(pos);
			const auto isPositionValid = pos.x() < (viewport()->width() - 25);

			if(index.isValid() && isPositionValid)
			{
				m->model->setChecked(index.row(), !m->model->isChecked(index.row()));
			}
		}

		m->editStarted = false;

		QListView::mouseReleaseEvent(e);
	}
} // Gui

#include "CheckableListView.moc"
