/*
 *   Copyright (C) 2018  Daniel Vrátil <dvratil@kde.org>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as
 *   published by the Free Software Foundation; either version 2, 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 Library General Public
 *   License along with this program; if not, write to the
 *   Free Software Foundation, Inc.,
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "passwordfiltermodel.h"
#include "passwordsmodel.h"
#include "abbreviations.h"

#include <KDescendantsProxyModel>

#include <QSortFilterProxyModel>
#include <QFutureWatcher>
#include <QtConcurrent>

#include <chrono>
#include <iterator>

using namespace PlasmaPass;

namespace {

constexpr const auto invalidateDelay = std::chrono::milliseconds(100);
constexpr const char *newFilterProperty = "newFilter";

class ModelIterator
{
public:
    using reference = const QModelIndex &;
    using pointer = QModelIndex *;
    using value_type = QModelIndex;
    using difference_type = int;
    using iterator_category = std::forward_iterator_tag;

    static ModelIterator begin(QAbstractItemModel *model)
    {
        return ModelIterator{model, model->index(0, 0)};
    }

    static ModelIterator end(QAbstractItemModel *model)
    {
        return ModelIterator{model, {}};
    }

    bool operator==(const ModelIterator &other) const {
        return mModel == other.mModel && mIndex == other.mIndex;
    }

    bool operator!=(const ModelIterator &other) const {
        return !(*this == other);
    }

    QModelIndex operator*() const {
        return mIndex;
    }

    const QModelIndex *operator->() const {
        return &mIndex;
    }

    ModelIterator &operator++() {
        if (mIndex.row() < mModel->rowCount() - 1) {
            mIndex = mModel->index(mIndex.row() + 1, mIndex.column());
        } else {
            mIndex = {};
        }
        return *this;
    }

    ModelIterator operator++(int) {
        ModelIterator it = *this;
        ++*this;
        return it;
    }

private:
    ModelIterator(QAbstractItemModel *model, const QModelIndex &index)
        : mModel(model), mIndex(index)
    {}

private:
    QAbstractItemModel *mModel = nullptr;
    QModelIndex mIndex;
};

} // namespace

PasswordFilterModel::PathFilter::PathFilter(QString filter)
    : filter(std::move(filter))
{}

PasswordFilterModel::PathFilter::PathFilter(const PathFilter &other)
    : filter(other.filter)
{
    updateParts();
}

PasswordFilterModel::PathFilter &PasswordFilterModel::PathFilter::operator=(const PathFilter &other)
{
    filter = other.filter;
    updateParts();
    return *this;
}

PasswordFilterModel::PathFilter::PathFilter(PathFilter &&other) noexcept
    : filter(std::move(other.filter))
{
    updateParts();
}

PasswordFilterModel::PathFilter &PasswordFilterModel::PathFilter::operator=(PathFilter &&other) noexcept
{
    filter = std::move(other.filter);
    updateParts();
    return *this;
}

void PasswordFilterModel::PathFilter::updateParts()
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
    mParts = filter.splitRef(QLatin1Char('/'), QString::SkipEmptyParts);
#else
    mParts = filter.splitRef(QLatin1Char('/'), Qt::SkipEmptyParts);
#endif
}

PasswordFilterModel::PathFilter::result_type PasswordFilterModel::PathFilter::operator()(const QModelIndex &index) const {
    const auto path = index.model()->data(index, PasswordsModel::FullNameRole).toString();
    const auto weight = matchPathFilter(path.splitRef(QLatin1Char('/')), mParts);
    return std::make_pair(index, weight);
}

PasswordFilterModel::PasswordFilterModel(QObject *parent)
    : QSortFilterProxyModel(parent)
    , mFlatModel(new KDescendantsProxyModel(this))
{
    mFlatModel->setDisplayAncestorData(false);
    sort(0); // enable sorting

    mUpdateTimer.setSingleShot(true);
    connect(&mUpdateTimer, &QTimer::timeout, this, &PasswordFilterModel::delayedUpdateFilter);
    connect(&mUpdateTimer, &QTimer::timeout, this, []() { qDebug() << "Update timer timeout, will calculate results lazily."; });
}

void PasswordFilterModel::setSourceModel(QAbstractItemModel *sourceModel)
{
    mFlatModel->setSourceModel(sourceModel);

    if (this->sourceModel() == nullptr) {
        QSortFilterProxyModel::setSourceModel(mFlatModel);
    }
}

QString PasswordFilterModel::passwordFilter() const
{
    return mFilter.filter;
}


void PasswordFilterModel::setPasswordFilter(const QString &filter)
{
    if (mFilter.filter != filter) {
        if (mUpdateTimer.isActive()) {
            mUpdateTimer.stop();
        }

        mUpdateTimer.setProperty(newFilterProperty, filter);
        mUpdateTimer.start(invalidateDelay);

        if (mFuture.isRunning()) {
            mFuture.cancel();
        }
        if (!filter.isEmpty()) {
            mFuture = QtConcurrent::mappedReduced<QHash<QModelIndex, int>>(
                        ModelIterator::begin(sourceModel()), ModelIterator::end(sourceModel()),
                        PathFilter{filter},
                        [](QHash<QModelIndex, int> &result, const std::pair<QModelIndex, int> &value) {
                            result.insert(value.first, value.second);
                        });
            auto watcher = new QFutureWatcher<QHash<QModelIndex, int>>();
            connect(watcher, &QFutureWatcherBase::finished, this,
                    [this, watcher]() {
                        mSortingLookup = mFuture.result();
                        watcher->deleteLater();
                        // If the timer is not active it means we were to slow, so don't invoke
                        // delayedUpdateFilter() again, just update mSortingLookup with our
                        // results.
                        if (mUpdateTimer.isActive()) {
                            mUpdateTimer.stop();
                            delayedUpdateFilter();
                        }
                    });
            connect(watcher, &QFutureWatcherBase::canceled, watcher, &QObject::deleteLater);
            watcher->setFuture(mFuture);
        }
    }
}

void PasswordFilterModel::delayedUpdateFilter()
{
    mFilter = PathFilter(mUpdateTimer.property(newFilterProperty).toString());
    Q_EMIT passwordFilterChanged();
    if (mFuture.isRunning()) {
        // If the future is still running, clear up the lookup table, we will have to
        // calculate the intermediate results ourselves
        mSortingLookup.clear();
    }
    invalidate();
}


QVariant PasswordFilterModel::data(const QModelIndex &index, int role) const
{
    if (role == Qt::DisplayRole) {
        return data(index, PasswordsModel::FullNameRole);
    }

    return QSortFilterProxyModel::data(index, role);
}


bool PasswordFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
    const auto src_index = sourceModel()->index(source_row, 0, source_parent);
    const auto type = static_cast<PasswordsModel::EntryType>(sourceModel()->data(src_index, PasswordsModel::EntryTypeRole).toInt());
    if (type == PasswordsModel::FolderEntry) {
        return false;
    }

    if (mFilter.filter.isEmpty()) {
        return true;
    }

    // Try to lookup the weight in the lookup table, the worker thread may have put it in there
    // while the updateTimer was ticking
    auto weight = mSortingLookup.find(src_index);
    if (weight == mSortingLookup.end()) {
        // It's not there, let's calculate the value now
        const auto result = mFilter(src_index);
        weight = mSortingLookup.insert(result.first, result.second);
    }

    return *weight > -1;
}

bool PasswordFilterModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
    const auto weightLeft = mSortingLookup.value(source_left, -1);
    const auto weightRight = mSortingLookup.value(source_right, -1);

    if (weightLeft == weightRight) {
        const auto nameLeft = source_left.data(PasswordsModel::FullNameRole).toString();
        const auto nameRight = source_right.data(PasswordsModel::FullNameRole).toString();
        return QString::localeAwareCompare(nameLeft, nameRight) < 0;
    }

    return weightLeft < weightRight;
}
