#include "./checklistmodel.h"

#include <QSettings>

/*!
    \namespace Models
    \brief Provides common models.
*/

namespace QtUtilities {

/*!
 * \class Models::ChecklistItem
 * \brief The ChecklistItem class provides an item for use with the
 * ChecklistModel class.
 */

/*!
 * \class Models::ChecklistModel
 * \brief The ChecklistModel class provides a generic model for storing
 * checkable items.
 */

/*!
 * \brief Constructs a new checklist model.
 */
ChecklistModel::ChecklistModel(QObject *parent)
    : QAbstractListModel(parent)
{
}

int ChecklistModel::rowCount(const QModelIndex &parent) const
{
    if (!parent.isValid()) {
        return static_cast<int>(m_items.size());
    }
    return 0;
}

Qt::ItemFlags ChecklistModel::flags(const QModelIndex &index) const
{
    if (!index.isValid() || index.row() >= m_items.count() || index.model() != this) {
        return Qt::ItemIsDropEnabled; // allows drops outside the items
    }
    return QAbstractListModel::flags(index) | Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled;
}

QVariant ChecklistModel::data(const QModelIndex &index, int role) const
{
    if (index.isValid() && index.row() < m_items.size()) {
        switch (role) {
        case Qt::DisplayRole:
            return m_items.at(index.row()).label();
        case Qt::CheckStateRole:
            return m_items.at(index.row()).checkState();
        case idRole():
            return m_items.at(index.row()).id();
        default:;
        }
    }
    return QVariant();
}

QMap<int, QVariant> ChecklistModel::itemData(const QModelIndex &index) const
{
    QMap<int, QVariant> roles;
    roles.insert(Qt::DisplayRole, data(index, Qt::DisplayRole));
    roles.insert(Qt::CheckStateRole, data(index, Qt::CheckStateRole));
    roles.insert(idRole(), data(index, idRole()));
    return roles;
}

bool ChecklistModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    bool success = false;
    QVector<int> roles{ role };
    if (index.isValid() && index.row() < m_items.size()) {
        switch (role) {
        case Qt::DisplayRole:
            m_items[index.row()].m_label = value.toString();
            success = true;
            break;
        case Qt::CheckStateRole:
            if (value.canConvert(
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
                    QMetaType::Int
#else
                    QMetaType::fromType<int>()
#endif
                    )) {
                m_items[index.row()].m_checkState = static_cast<Qt::CheckState>(value.toInt());
                success = true;
            }
            break;
        case idRole(): {
            m_items[index.row()].m_id = value;
            success = true;
            auto label = labelForId(value);
            if (!label.isEmpty()) {
                m_items[index.row()].m_label = std::move(label);
                roles << Qt::DisplayRole;
            }
            break;
        }
        default:;
        }
    }
    if (success) {
        emit dataChanged(index, index, roles);
    }
    return success;
}

bool ChecklistModel::setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles)
{
    for (QMap<int, QVariant>::ConstIterator it = roles.constBegin(); it != roles.constEnd(); ++it) {
        setData(index, it.value(), it.key());
    }
    return true;
}

/*!
 * \brief Sets the checked state of the specified item.
 */
bool ChecklistModel::setChecked(int row, Qt::CheckState checked)
{
    if (row < 0 || row >= m_items.size()) {
        return false;
    }
    m_items[row].m_checkState = checked ? Qt::Checked : Qt::Unchecked;
    const auto index(this->index(row));
    emit dataChanged(index, index, QVector<int>{ Qt::CheckStateRole });
    return true;
}

/*!
 * \brief Returns the label for the specified \a id.
 *
 * This method might be reimplemented when subclassing to provide labels
 * for the item IDs.
 *
 * If an item's ID is set (using setData() with idRole() or setItems()) this method
 * is called to update or initialize the item's label as well. If this method returns
 * an empty string (default behaviour) the item's label will not be updated.
 *
 * This is useful when items are moved by the view (eg. for Drag & Drop) and to
 * initialize the ChecklistItem labels more conveniently.
 */
QString ChecklistModel::labelForId(const QVariant &) const
{
    return QString();
}

Qt::DropActions ChecklistModel::supportedDropActions() const
{
    return Qt::MoveAction;
}

bool ChecklistModel::insertRows(int row, int count, const QModelIndex &parent)
{
    if (count < 1 || row < 0 || row > rowCount() || parent.isValid()) {
        return false;
    }
    beginInsertRows(QModelIndex(), row, row + count - 1);
    for (int index = row, end = row + count; index < end; ++index) {
        m_items.insert(index, ChecklistItem());
    }
    endInsertRows();
    return true;
}

bool ChecklistModel::removeRows(int row, int count, const QModelIndex &parent)
{
    if (count < 1 || row < 0 || (row + count) > rowCount() || parent.isValid()) {
        return false;
    }
    beginRemoveRows(QModelIndex(), row, row + count - 1);
    for (int index = row, end = row + count; index < end; ++index) {
        m_items.removeAt(index);
    }
    endRemoveRows();
    return true;
}

/*!
 * \brief Sets the items. Resets the model.
 */
void ChecklistModel::setItems(const QList<ChecklistItem> &items)
{
    beginResetModel();
    m_items = items;
    for (auto &item : m_items) {
        if (item.m_label.isEmpty()) {
            item.m_label = labelForId(item.id());
        }
    }
    endResetModel();
}

/*!
 * \brief Restores the IDs and checkstates read from the specified \a settings
 * object.
 *
 * The items will be read from the array with the specified \a name.
 *
 * Resets the model (current items are cleared).
 *
 * Does not restore any labels. Labels are meant to be restored from the ID.
 */
void ChecklistModel::restore(QSettings &settings, const QString &name)
{
    beginResetModel();
    auto currentItems = m_items;
    QList<QVariant> restoredIds;
    m_items.clear();
    int rows = settings.beginReadArray(name);
    m_items.reserve(rows);
    for (int i = 0; i < rows; ++i) {
        settings.setArrayIndex(i);
        const auto id = settings.value(QStringLiteral("id"));
        const auto isIdValid = [&] {
            for (const auto &item : currentItems) {
                if (item.id() == id) {
                    return true;
                }
            }
            return false;
        }();
        if (!isIdValid) {
            continue;
        }
        const auto selected = settings.value(QStringLiteral("selected"));
        if (!id.isNull() && !selected.isNull()
            && selected.canConvert(
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
                QMetaType::Bool
#else
                QMetaType::fromType<bool>()
#endif
                )
            && !restoredIds.contains(id)) {
            m_items << ChecklistItem(id, labelForId(id), selected.toBool() ? Qt::Checked : Qt::Unchecked);
            restoredIds << id;
        }
    }
    settings.endArray();
    for (const ChecklistItem &item : currentItems) {
        if (!restoredIds.contains(item.id())) {
            m_items << item;
        }
    }
    endResetModel();
}

/*!
 * \brief Saves the IDs and checkstates to the specified \a settings object.
 *
 * The items will be stored using an array with the specified \a name.
 *
 * Does not save any labels.
 */
void ChecklistModel::save(QSettings &settings, const QString &name) const
{
    settings.beginWriteArray(name, static_cast<int>(m_items.size()));
    auto index = int();
    for (const ChecklistItem &item : m_items) {
        settings.setArrayIndex(index);
        settings.setValue(QStringLiteral("id"), item.id());
        settings.setValue(QStringLiteral("selected"), item.isChecked());
        ++index;
    }
    settings.endArray();
}

/*!
 * \brief Returns the checked IDs.
 */
QVariantList ChecklistModel::toVariantList() const
{
    QVariantList checkedIds;
    checkedIds.reserve(m_items.size());
    for (const auto &item : m_items) {
        if (item.isChecked()) {
            checkedIds << item.id();
        }
    }
    return checkedIds;
}

/*!
 * \brief Checks all items contained by \a checkedIds and unchecks other items.
 */
void ChecklistModel::applyVariantList(const QVariantList &checkedIds)
{
    for (auto &item : m_items) {
        item.m_checkState = checkedIds.contains(item.id()) ? Qt::Checked : Qt::Unchecked;
    }
    emit dataChanged(index(0), index(static_cast<int>(m_items.size())), { Qt::CheckStateRole });
}

} // namespace QtUtilities
