/*
 * Copyright 2015 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
 * Copyright 2015 David Faure <david.faure@kdab.com>
 * Copyright 2020 David Redondo <kde@david-redondo.de>
 *
 * 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 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * 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 "shortcutsmodel.h"

class ShortcutsModelPrivate
{
public:
    ShortcutsModelPrivate(ShortcutsModel *model)
        : q(model)
    {
    }

    int computeRowsPrior(const QAbstractItemModel *sourceModel) const;
    QAbstractItemModel *sourceModelForRow(int row, int *sourceRow) const;

    void slotRowsAboutToBeInserted(const QModelIndex &, int start, int end);
    void slotRowsInserted(const QModelIndex &, int start, int end);
    void slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end);
    void slotRowsRemoved(const QModelIndex &, int start, int end);
    void slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end);
    void slotColumnsInserted(const QModelIndex &parent, int, int);
    void slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
    void slotColumnsRemoved(const QModelIndex &parent, int, int);
    void slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles);
    void slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
    void slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
    void slotModelAboutToBeReset();
    void slotModelReset();

    ShortcutsModel *q;
    QList<QAbstractItemModel *> m_models;
    int m_rowCount = 0; // have to maintain it here since we can't compute during model destruction

    // for layoutAboutToBeChanged/layoutChanged
    QVector<QPersistentModelIndex> layoutChangePersistentIndexes;
    QModelIndexList proxyIndexes;
};

ShortcutsModel::ShortcutsModel(QObject *parent)
    : QAbstractItemModel(parent)
    , d(new ShortcutsModelPrivate(this))
{
}

ShortcutsModel::~ShortcutsModel()
{
}

QModelIndex ShortcutsModel::mapFromSource(const QModelIndex &sourceIndex) const
{
    const QAbstractItemModel *sourceModel = sourceIndex.model();
    if (!sourceModel) {
        return {};
    }
    int rowsPrior = d->computeRowsPrior(sourceModel);

    if (sourceIndex.parent().isValid()) {
        return createIndex(sourceIndex.row(), sourceIndex.column(), rowsPrior + sourceIndex.parent().row() + 1);
    }
    return createIndex(rowsPrior + sourceIndex.row(), sourceIndex.column());
}

QModelIndex ShortcutsModel::mapToSource(const QModelIndex &proxyIndex) const
{
    if (!proxyIndex.isValid()) {
        return QModelIndex();
    }

    int sourceRow;
    int topLevelRow = proxyIndex.internalId() ? proxyIndex.internalId() - 1 : proxyIndex.row();
    QAbstractItemModel *sourceModel = d->sourceModelForRow(topLevelRow, &sourceRow);
    if (!sourceModel) {
        return QModelIndex();
    }

    if (proxyIndex.internalId()) {
        return sourceModel->index(proxyIndex.row(), proxyIndex.column(), sourceModel->index(sourceRow, proxyIndex.column()));
    }
    return sourceModel->index(sourceRow, proxyIndex.column());
}

QVariant ShortcutsModel::data(const QModelIndex &index, int role) const
{
    const QModelIndex sourceIndex = mapToSource(index);
    if (!sourceIndex.isValid()) {
        return QVariant();
    }
    return sourceIndex.model()->data(sourceIndex, role);
}

bool ShortcutsModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    const QModelIndex sourceIndex = mapToSource(index);
    if (!sourceIndex.isValid()) {
        return false;
    }
    QAbstractItemModel *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
    return sourceModel->setData(sourceIndex, value, role);
}

QMap<int, QVariant> ShortcutsModel::itemData(const QModelIndex &proxyIndex) const
{
    const QModelIndex sourceIndex = mapToSource(proxyIndex);
    if (!sourceIndex.isValid()) {
        return {};
    }
    return sourceIndex.model()->itemData(sourceIndex);
}

Qt::ItemFlags ShortcutsModel::flags(const QModelIndex &index) const
{
    const QModelIndex sourceIndex = mapToSource(index);
    return sourceIndex.isValid() ? sourceIndex.model()->flags(sourceIndex) : Qt::ItemFlags();
}

QVariant ShortcutsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (d->m_models.isEmpty()) {
        return QVariant();
    }
    if (orientation == Qt::Horizontal) {
        return d->m_models.at(0)->headerData(section, orientation, role);
    } else {
        int sourceRow;
        QAbstractItemModel *sourceModel = d->sourceModelForRow(section, &sourceRow);
        if (!sourceModel) {
            return QVariant();
        }
        return sourceModel->headerData(sourceRow, orientation, role);
    }
}

int ShortcutsModel::columnCount(const QModelIndex &parent) const
{
    if (d->m_models.isEmpty()) {
        return 0;
    }
    if (parent.isValid()) {
        const QModelIndex sourceParent = mapToSource(parent);
        return sourceParent.model()->columnCount(sourceParent);
    }
    return d->m_models.at(0)->columnCount(QModelIndex());
}

QHash<int, QByteArray> ShortcutsModel::roleNames() const
{
    if (d->m_models.isEmpty()) {
        return {};
    }
    return d->m_models.at(0)->roleNames();
}

QModelIndex ShortcutsModel::index(int row, int column, const QModelIndex &parent) const
{
    if (row < 0) {
        return {};
    }
    if (column < 0) {
        return {};
    }

    if (parent.isValid()) {
        const QModelIndex sourceParent = mapToSource(parent);
        return mapFromSource(sourceParent.model()->index(row, column, sourceParent));
    }

    int sourceRow;
    QAbstractItemModel *sourceModel = d->sourceModelForRow(row, &sourceRow);
    if (!sourceModel) {
        return QModelIndex();
    }
    return mapFromSource(sourceModel->index(sourceRow, column, parent));
}

QModelIndex ShortcutsModel::parent(const QModelIndex &child) const
{
    return mapFromSource(mapToSource(child).parent());
}

int ShortcutsModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        const QModelIndex sourceParent = mapToSource(parent);
        return sourceParent.model()->rowCount(sourceParent);
    }

    return d->m_rowCount;
}

void ShortcutsModel::addSourceModel(QAbstractItemModel *sourceModel)
{
    Q_ASSERT(sourceModel);
    Q_ASSERT(!d->m_models.contains(sourceModel));
    // clang-format off
    connect(sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), this, SLOT(slotDataChanged(QModelIndex,QModelIndex,QVector<int>)));
    connect(sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(slotRowsInserted(QModelIndex,int,int)));
    connect(sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(slotRowsRemoved(QModelIndex,int,int)));
    connect(sourceModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(slotRowsAboutToBeInserted(QModelIndex,int,int)));
    connect(sourceModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int)));

    connect(sourceModel, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(slotColumnsInserted(QModelIndex,int,int)));
    connect(sourceModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(slotColumnsRemoved(QModelIndex,int,int)));
    connect(sourceModel, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(slotColumnsAboutToBeInserted(QModelIndex,int,int)));
    connect(sourceModel, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(slotColumnsAboutToBeRemoved(QModelIndex,int,int)));

    connect(sourceModel, SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)),
            this, SLOT(slotSourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)));
    connect(sourceModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)),
            this, SLOT(slotSourceLayoutChanged(QList<QPersistentModelIndex>,QAbstractItemModel::LayoutChangeHint)));
    connect(sourceModel, SIGNAL(modelAboutToBeReset()), this, SLOT(slotModelAboutToBeReset()));
    connect(sourceModel, SIGNAL(modelReset()), this, SLOT(slotModelReset()));
    // clang-format on

    const int newRows = sourceModel->rowCount();
    if (newRows > 0) {
        beginInsertRows(QModelIndex(), d->m_rowCount, d->m_rowCount + newRows - 1);
    }
    d->m_rowCount += newRows;
    d->m_models.append(sourceModel);
    if (newRows > 0) {
        endInsertRows();
    }
}

QList<QAbstractItemModel *> ShortcutsModel::sources() const
{
    return d->m_models;
}

void ShortcutsModel::removeSourceModel(QAbstractItemModel *sourceModel)
{
    Q_ASSERT(d->m_models.contains(sourceModel));
    disconnect(sourceModel, nullptr, this, nullptr);

    const int rowsRemoved = sourceModel->rowCount();
    const int rowsPrior = d->computeRowsPrior(sourceModel); // location of removed section

    if (rowsRemoved > 0) {
        beginRemoveRows(QModelIndex(), rowsPrior, rowsPrior + rowsRemoved - 1);
    }
    d->m_models.removeOne(sourceModel);
    d->m_rowCount -= rowsRemoved;
    if (rowsRemoved > 0) {
        endRemoveRows();
    }
}

void ShortcutsModelPrivate::slotRowsAboutToBeInserted(const QModelIndex &sourceParent, int start, int end)
{
    const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
    if (sourceParent.isValid()) {
        q->beginInsertRows(q->mapFromSource(sourceParent), start, end);
    } else {
        const int rowsPrior = computeRowsPrior(model);
        q->beginInsertRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
    }
}

void ShortcutsModelPrivate::slotRowsInserted(const QModelIndex &sourceParent, int start, int end)
{
    if (!sourceParent.isValid()) {
        m_rowCount += end - start + 1;
    }
    q->endInsertRows();
}

void ShortcutsModelPrivate::slotRowsAboutToBeRemoved(const QModelIndex &sourceParent, int start, int end)
{
    if (sourceParent.isValid()) {
        q->beginRemoveRows(q->mapFromSource(sourceParent), start, end);
    } else {
        const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
        const int rowsPrior = computeRowsPrior(model);
        q->beginRemoveRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
    }
}

void ShortcutsModelPrivate::slotRowsRemoved(const QModelIndex &sourceParent, int start, int end)
{
    if (!sourceParent.isValid()) {
        m_rowCount -= end - start + 1;
    }
    q->endRemoveRows();
}

void ShortcutsModelPrivate::slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)
{
    if (parent.isValid()) { // we are flat
        q->beginInsertColumns(q->mapFromSource(parent), start, end);
    }
    const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
    if (m_models.at(0) == model) {
        q->beginInsertColumns(QModelIndex(), start, end);
    }
}

void ShortcutsModelPrivate::slotColumnsInserted(const QModelIndex &parent, int, int)
{
    const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
    if (m_models.at(0) == model || parent.isValid()) {
        q->endInsertColumns();
    }
}

void ShortcutsModelPrivate::slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
    if (parent.isValid()) {
        q->beginRemoveColumns(q->mapFromSource(parent), start, end);
    }
    const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
    if (m_models.at(0) == model) {
        q->beginRemoveColumns(QModelIndex(), start, end);
    }
}

void ShortcutsModelPrivate::slotColumnsRemoved(const QModelIndex &parent, int, int)
{
    const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender());
    if (m_models.at(0) == model || parent.isValid()) {
        q->endRemoveColumns();
    }
}

void ShortcutsModelPrivate::slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles)
{
    if (!from.isValid()) { // QSFPM bug, it emits dataChanged(invalid, invalid) if a cell in a hidden column changes
        return;
    }
    const QModelIndex myFrom = q->mapFromSource(from);
    const QModelIndex myTo = q->mapFromSource(to);
    emit q->dataChanged(myFrom, myTo, roles);
}

void ShortcutsModelPrivate::slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
{
    QList<QPersistentModelIndex> parents;
    parents.reserve(sourceParents.size());
    for (const QPersistentModelIndex &parent : sourceParents) {
        if (!parent.isValid()) {
            parents << QPersistentModelIndex();
            continue;
        }
        const QModelIndex mappedParent = q->mapFromSource(parent);
        Q_ASSERT(mappedParent.isValid());
        parents << mappedParent;
    }

    emit q->layoutAboutToBeChanged(parents, hint);

    const QModelIndexList persistentIndexList = q->persistentIndexList();
    layoutChangePersistentIndexes.reserve(persistentIndexList.size());

    for (const QPersistentModelIndex &proxyPersistentIndex : persistentIndexList) {
        proxyIndexes << proxyPersistentIndex;
        Q_ASSERT(proxyPersistentIndex.isValid());
        const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex);
        Q_ASSERT(srcPersistentIndex.isValid());
        layoutChangePersistentIndexes << srcPersistentIndex;
    }
}

void ShortcutsModelPrivate::slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
{
    for (int i = 0; i < proxyIndexes.size(); ++i) {
        const QModelIndex proxyIdx = proxyIndexes.at(i);
        QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i));
        q->changePersistentIndex(proxyIdx, newProxyIdx);
    }

    layoutChangePersistentIndexes.clear();
    proxyIndexes.clear();

    QList<QPersistentModelIndex> parents;
    parents.reserve(sourceParents.size());
    for (const QPersistentModelIndex &parent : sourceParents) {
        if (!parent.isValid()) {
            parents << QPersistentModelIndex();
            continue;
        }
        const QModelIndex mappedParent = q->mapFromSource(parent);
        Q_ASSERT(mappedParent.isValid());
        parents << mappedParent;
    }
    emit q->layoutChanged(parents, hint);
}

void ShortcutsModelPrivate::slotModelAboutToBeReset()
{
    const QAbstractItemModel *sourceModel = qobject_cast<const QAbstractItemModel *>(q->sender());
    Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(sourceModel)));
    q->beginResetModel();
}

void ShortcutsModelPrivate::slotModelReset()
{
    m_rowCount = computeRowsPrior(nullptr);
    q->endResetModel();
}

int ShortcutsModelPrivate::computeRowsPrior(const QAbstractItemModel *sourceModel) const
{
    int rowsPrior = 0;
    for (const QAbstractItemModel *model : qAsConst(m_models)) {
        if (model == sourceModel) {
            break;
        }
        rowsPrior += model->rowCount();
    }
    return rowsPrior;
}

QAbstractItemModel *ShortcutsModelPrivate::sourceModelForRow(int row, int *sourceRow) const
{
    int rowCount = 0;
    QAbstractItemModel *selection = nullptr;
    for (QAbstractItemModel *model : qAsConst(m_models)) {
        const int subRowCount = model->rowCount();
        if (rowCount + subRowCount > row) {
            selection = model;
            break;
        }
        rowCount += subRowCount;
    }
    *sourceRow = row - rowCount;
    return selection;
}

#include "moc_shortcutsmodel.cpp"
