// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "todosortfilterproxymodel.h"

#include <Akonadi/CollectionColorAttribute>

#include "../filter.h"
using namespace Qt::Literals::StringLiterals;
TodoSortFilterProxyModel::TodoSortFilterProxyModel(QObject *parent)
    : QSortFilterProxyModel(parent)
{
    const QString todoMimeType = u"application/x-vnd.akonadi.calendar.todo"_s;
    m_todoTreeModel.reset(new Akonadi::IncidenceTreeModel(QStringList() << todoMimeType, this));

    m_baseTodoModel.reset(new Akonadi::TodoModel(this));
    m_baseTodoModel->setSourceModel(m_todoTreeModel.data());
    setSourceModel(m_baseTodoModel.data());

    setSortCaseSensitivity(Qt::CaseInsensitive);
    setFilterCaseSensitivity(Qt::CaseInsensitive);

    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    KConfigGroup rColorsConfig(config, u"Resources Colors"_s);
    m_colorWatcher = KConfigWatcher::create(config);
    QObject::connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &TodoSortFilterProxyModel::loadColors);

    loadColors();

    m_dateRefreshTimer.setInterval(m_dateRefreshTimerInterval);
    m_dateRefreshTimer.callOnTimeout(this, &TodoSortFilterProxyModel::updateDateLabels);
    m_dateRefreshTimer.start();
}

int TodoSortFilterProxyModel::columnCount(const QModelIndex &) const
{
    return 1;
}

QHash<int, QByteArray> TodoSortFilterProxyModel::roleNames() const
{
    QHash<int, QByteArray> roleNames = QSortFilterProxyModel::roleNames();
    roleNames[Akonadi::TodoModel::SummaryRole] = "text"_ba;
    roleNames[Roles::StartTimeRole] = "startTime"_ba;
    roleNames[Roles::EndTimeRole] = "endTime"_ba;
    roleNames[Roles::DisplayDueDateRole] = "displayDueDate"_ba;
    roleNames[Roles::LocationRole] = "location"_ba;
    roleNames[Roles::AllDayRole] = "allDay"_ba;
    roleNames[Roles::ColorRole] = "color"_ba;
    roleNames[Roles::CompletedRole] = "todoCompleted"_ba;
    roleNames[Roles::PriorityRole] = "priority"_ba;
    roleNames[Roles::CollectionIdRole] = "collectionId"_ba;
    roleNames[Roles::DurationStringRole] = "durationString"_ba;
    roleNames[Roles::RecursRole] = "recurs"_ba;
    roleNames[Roles::IsReadOnly] = "isReadOnly"_ba;
    roleNames[Roles::IsOverdueRole] = "isOverdue"_ba;
    roleNames[Roles::IncidenceIdRole] = "incidenceId"_ba;
    roleNames[Roles::IncidenceTypeRole] = "incidenceType"_ba;
    roleNames[Roles::IncidenceTypeStrRole] = "incidenceTypeStr"_ba;
    roleNames[Roles::IncidenceTypeIconRole] = "incidenceTypeIcon"_ba;
    roleNames[Roles::IncidencePtrRole] = "incidencePtr"_ba;
    roleNames[Roles::TagsRole] = "tags"_ba;
    roleNames[Roles::ItemRole] = "item"_ba;
    roleNames[Roles::CategoriesRole] = "todoCategories"_ba; // Simply 'categories' causes issues
    roleNames[Roles::CategoriesDisplayRole] = "categoriesDisplay"_ba;
    roleNames[Roles::TreeDepthRole] = "treeDepth"_ba;
    roleNames[Roles::TopMostParentDueDateRole] = "topMostParentDueDate"_ba;
    roleNames[Roles::TopMostParentSummaryRole] = "topMostParentSummary"_ba;
    roleNames[Roles::TopMostParentPriorityRole] = "topMostParentPriority"_ba;

    return roleNames;
}

QVariant TodoSortFilterProxyModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || m_calendar.isNull()) {
        return {};
    }

    const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0));
    if (!sourceIndex.isValid()) {
        return {};
    }
    Q_ASSERT(sourceIndex.isValid());

    auto todoItem = sourceIndex.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();

    if (!todoItem.isValid()) {
        return {};
    }

    auto collectionId = todoItem.parentCollection().id();
    auto todoPtr = Akonadi::CalendarUtils::todo(todoItem);

    if (!todoPtr) {
        return {};
    }

    if (role == Roles::StartTimeRole) {
        return todoPtr->dtStart();
    } else if (role == Roles::EndTimeRole) {
        return todoPtr->dtDue();
    } else if (role == Roles::DisplayDueDateRole) {
        return todoDueDateDisplayString(todoPtr, DisplayDateTimeAndIfOverdue);
    } else if (role == Roles::LocationRole) {
        return todoPtr->location();
    } else if (role == Roles::AllDayRole) {
        return todoPtr->allDay();
    } else if (role == Roles::ColorRole) {
        if (m_colors.contains(QString::number(collectionId))) {
            return m_colors[QString::number(collectionId)];
        }

        // TODO: We should use the same approach as we do in calendarmanager. We can probably create a shared collection color cache
        if (const auto collection = m_calendar->collection(collectionId); collection.hasAttribute<Akonadi::CollectionColorAttribute>()) {
            if (const auto colorAttr = collection.attribute<Akonadi::CollectionColorAttribute>(); colorAttr->color().isValid()) {
                return colorAttr->color();
            }
        }

        return QColor();
    } else if (role == Roles::CompletedRole) {
        return todoPtr->isCompleted();
    } else if (role == Roles::PriorityRole) {
        return todoPtr->priority();
    } else if (role == Roles::CollectionIdRole) {
        return collectionId;
    } else if (role == DurationStringRole) {
        const auto duration = KCalendarCore::Duration(todoPtr->dtStart(), todoPtr->dtDue());

        if (todoPtr->allDay() && !todoPtr->dtStart().isValid()) {
            return m_format.formatSpelloutDuration(24 * 60 * 60 * 1000); // format milliseconds in 1 day
        } else if (!todoPtr->dtStart().isValid() || duration.asSeconds() == 0) {
            return QString();
        }

        return m_format.formatSpelloutDuration(duration.asSeconds() * 1000);
    } else if (role == Roles::RecursRole) {
        return todoPtr->recurs();
    } else if (role == Roles::IsOverdueRole) {
        return todoPtr->isOverdue();
    } else if (role == Roles::IsReadOnly) {
        return todoPtr->isReadOnly();
    } else if (role == Roles::IncidenceIdRole) {
        return todoPtr->uid();
    } else if (role == Roles::IncidenceTypeRole) {
        return todoPtr->type();
    } else if (role == Roles::IncidenceTypeStrRole) {
        return todoPtr->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n(todoPtr->typeStr().constData());
    } else if (role == Roles::IncidenceTypeIconRole) {
        return todoPtr->iconName();
    } else if (role == Roles::IncidencePtrRole) {
        return QVariant::fromValue(Akonadi::CalendarUtils::incidence(todoItem));
    } else if (role == Roles::TagsRole) {
        return QVariant::fromValue(todoItem.tags());
    } else if (role == Roles::ItemRole) {
        return QVariant::fromValue(todoItem);
    } else if (role == Roles::CategoriesRole) {
        return todoPtr->categories();
    } else if (role == Roles::CategoriesDisplayRole) {
        return todoPtr->categories().join(i18nc("List separator", ", "));
    } else if (role == Roles::TreeDepthRole || role == TopMostParentSummaryRole || role == TopMostParentDueDateRole || role == TopMostParentPriorityRole) {
        int depth = 0;
        auto idx = index;
        while (idx.parent().isValid()) {
            idx = idx.parent();
            depth++;
        }

        auto todo = idx.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();

        switch (role) {
        case Roles::TreeDepthRole:
            return depth;
        case TopMostParentSummaryRole:
            return todo->summary();
        case TopMostParentDueDateRole: {
            if (!todo->hasDueDate()) {
                return i18n("No set date");
            }

            if (todo->isOverdue()) {
                return i18n("Overdue");
            }

            const auto dateInCurrentTZ = todo->dtDue().toLocalTime().date();
            const auto isToday = dateInCurrentTZ == QDate::currentDate();

            return isToday ? i18n("Today") : todoDueDateDisplayString(todo, DisplayDateOnly);
        }
        case TopMostParentPriorityRole:
            return todo->priority();
        }
    }
    return QSortFilterProxyModel::data(index, role);
}

QString TodoSortFilterProxyModel::todoDueDateDisplayString(const KCalendarCore::Todo::Ptr todo,
                                                           const TodoSortFilterProxyModel::DueDateDisplayFormat format) const
{
    if (!todo || !todo->hasDueDate()) {
        return {};
    }

    const auto systemLocale = QLocale::system();
    const auto includeTime = !todo->allDay() && format != DisplayDateOnly;
    const auto includeOverdue = todo->isOverdue() && format == DisplayDateTimeAndIfOverdue;

    const auto todoDateTimeDue = todo->dtDue().toLocalTime();
    const auto todoDateDue = todoDateTimeDue.date();
    const auto todoTimeDueString =
        includeTime ? i18nc("Please retain space", " at %1", systemLocale.toString(todoDateTimeDue.time(), QLocale::NarrowFormat)) : u" "_s;
    const auto todoOverdueString = includeOverdue ? i18nc("Please retain parenthesis and space", " (overdue)") : QString();

    const auto currentDate = QDate::currentDate();
    const auto dateFormat = todoDateDue.year() == currentDate.year() ? u"dddd dd MMMM"_s : u"dddd dd MMMM yyyy"_s;

    static constexpr char translationExplainer[] =
        "No spaces -- the (optional) %1 string, which includes the time, includes this space"
        " as does the %2 string which is the overdue string (also optional!)";

    if (currentDate == todoDateDue) {
        return i18nc(translationExplainer, "Today%1%2", todoTimeDueString, todoOverdueString);
    } else if (currentDate.daysTo(todoDateDue) == 1) {
        return i18nc(translationExplainer, "Tomorrow%1%2", todoTimeDueString, todoOverdueString);
    } else if (currentDate.daysTo(todoDateDue) == -1) {
        return i18nc(translationExplainer, "Yesterday%1%2", todoTimeDueString, todoOverdueString);
    }

    const auto dateDueString = systemLocale.toString(todoDateDue, dateFormat);
    return dateDueString + todoTimeDueString + todoOverdueString;
}

void TodoSortFilterProxyModel::updateDateLabels()
{
    if (rowCount() == 0 || !sourceModel()) {
        return;
    }

    emitDateDataChanged({});
    sortTodoModel();
    m_lastDateRefreshDate = QDate::currentDate();
}

void TodoSortFilterProxyModel::emitDateDataChanged(const QModelIndex &idx)
{
    const auto idxRowCount = rowCount(idx);
    const auto srcModel = sourceModel();

    if (idxRowCount == 0) {
        return;
    }

    const auto bottomRow = idxRowCount - 1;
    const auto currentDate = QDate::currentDate();
    const auto currentDateTime = QDateTime::currentDateTime();

    const auto iterateOverChildren = [this, &idx, &srcModel, &currentDate, &currentDateTime](const int row) {
        const auto childIdx = index(row, 0, idx);

        const auto todo = childIdx.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
        const auto isOverdue = todo->isOverdue();
        const auto dtDue = todo->dtDue();
        const auto isRecentlyOverdue = isOverdue && currentDateTime.msecsTo(dtDue) >= -m_dateRefreshTimerInterval;

        if (isRecentlyOverdue || m_lastDateRefreshDate != currentDate) {
            Q_EMIT dataChanged(childIdx, childIdx, {DisplayDueDateRole, TopMostParentDueDateRole});

            // For the proxy model to re-sort items into their correct section we also need to emit a
            // dataChanged() signal for the column we are sorting by in the source model
            const auto srcChildIdx = mapToSource(childIdx).siblingAtColumn(Akonadi::TodoModel::DueDateColumn);
            Q_EMIT srcModel->dataChanged(srcChildIdx, srcChildIdx, {Akonadi::TodoModel::DueDateRole});
        }

        // We recursively do the same for children
        emitDateDataChanged(childIdx);
    };

    // This is a workaround for weird sorting behaviour. If one of the items changes because it becomes
    // overdue, for example, the way in which we emit dataChanged() signals of the sourceModel will
    // dictate how the model gets sorted.
    //
    // Example: In a case where the sort is ascending (i.e. overdue at top), a 0 to rowCount() -1 sort
    // will move the overdue item up by only one row due to how the QSortFilterProxyModel uses lessThan().
    // If we go the opposite way then the QSFPM calls lessThan() on the overdue item more than once, moving
    // it upwards.

    if (m_sortAscending) {
        for (auto i = bottomRow; i >= 0; --i) {
            iterateOverChildren(i);
        }
    } else {
        for (auto i = 0; i < idxRowCount; ++i) {
            iterateOverChildren(i);
        }
    }
}

bool TodoSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &sourceParent) const
{
    if (filterAcceptsRowCheck(row, sourceParent)) {
        return true;
    }

    // Accept if any parent is accepted itself, and if we are the model for the incomplete tasks view, only do this if the config says to show all
    // of a tasks' incomplete subtasks. By default we include all of a tasks' subtasks, regardless of if they are complete or not, as long as the parent
    // passes the filter check. If this is not the case, we only include subtasks that pass the filter themselves.

    if ((m_showCompletedSubtodosInIncomplete && m_showCompleted == ShowComplete::ShowIncompleteOnly) || m_showCompleted != ShowComplete::ShowIncompleteOnly) {
        QModelIndex parent = sourceParent;
        while (parent.isValid()) {
            if (filterAcceptsRowCheck(parent.row(), parent.parent()))
                return true;
            parent = parent.parent();
        }
    }

    // Accept if any child is accepted itself
    return hasAcceptedChildren(row, sourceParent);
}

bool TodoSortFilterProxyModel::filterAcceptsRowCheck(int row, const QModelIndex &sourceParent) const
{
    const QModelIndex sourceIndex = sourceModel()->index(row, 0, sourceParent);
    Q_ASSERT(sourceIndex.isValid());

    if (m_filterObject == nullptr) {
        return QSortFilterProxyModel::filterAcceptsRow(row, sourceParent);
    }

    bool acceptRow = true;

    const auto todoItem = sourceIndex.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();

    if (!todoItem.isValid()) {
        return {};
    }

    if (m_filterObject->showCurrentDayOnly()) {
        const auto todoPtr = Akonadi::CalendarUtils::todo(todoItem);
        const auto todo = sourceIndex.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();

        if (!todo->hasDueDate()) {
            return {};
        }

        if (todoPtr && todoPtr->dtDue().isValid()) {
            const auto dueDate = todoPtr->dtDue().date();
            const auto today = QDate::currentDate();
            acceptRow = acceptRow && (dueDate == today);
        }
    }

    if (m_filterObject->collectionId() > -1) {
        const auto collectionId = todoItem.parentCollection().id();
        acceptRow = acceptRow && collectionId == m_filterObject->collectionId();
    }

    switch (m_showCompleted) {
    case ShowComplete::ShowCompleteOnly:
        acceptRow = acceptRow && sourceIndex.data(Akonadi::TodoModel::PercentRole).toInt() == 100;
        break;
    case ShowComplete::ShowIncompleteOnly:
        acceptRow = acceptRow && sourceIndex.data(Akonadi::TodoModel::PercentRole).toInt() < 100;
    case ShowComplete::ShowAll:
    default:
        break;
    }

    if (!m_filterObject->tags().isEmpty()) {
        const auto tags = m_filterObject->tags();
        bool containsTag = false;
        for (const auto &tag : tags) {
            const auto todoPtr = sourceIndex.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
            if (todoPtr->categories().contains(tag)) {
                containsTag = true;
                break;
            }
        }
        acceptRow = acceptRow && containsTag;
    }

    return acceptRow ? QSortFilterProxyModel::filterAcceptsRow(row, sourceParent) : acceptRow;
}

bool TodoSortFilterProxyModel::hasAcceptedChildren(int row, const QModelIndex &sourceParent) const
{
    QModelIndex index = sourceModel()->index(row, 0, sourceParent);
    if (!index.isValid()) {
        return false;
    }

    int childCount = index.model()->rowCount(index);
    if (childCount == 0)
        return false;

    for (int i = 0; i < childCount; ++i) {
        if (filterAcceptsRowCheck(i, index))
            return true;

        if (hasAcceptedChildren(i, index))
            return true;
    }

    return false;
}

Akonadi::ETMCalendar::Ptr TodoSortFilterProxyModel::calendar() const
{
    return m_calendar;
}

void TodoSortFilterProxyModel::setCalendar(Akonadi::ETMCalendar::Ptr &calendar)
{
    // No need to manually emit beginResetModel(), source model does it for us
    m_calendar = calendar;
    m_todoTreeModel->setSourceModel(calendar->model());
    m_baseTodoModel->setCalendar(m_calendar);
    Q_EMIT calendarChanged();
}

Akonadi::IncidenceChanger *TodoSortFilterProxyModel::incidenceChanger() const
{
    return m_lastSetChanger;
}

void TodoSortFilterProxyModel::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
{
    m_baseTodoModel->setIncidenceChanger(changer);
    m_lastSetChanger = changer;

    Q_EMIT incidenceChangerChanged();
}

void TodoSortFilterProxyModel::setColorCache(const QHash<QString, QColor> &colorCache)
{
    m_colors = colorCache;
}

void TodoSortFilterProxyModel::loadColors()
{
    Q_EMIT layoutAboutToBeChanged();
    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    KConfigGroup rColorsConfig(config, u"Resources Colors"_s);
    const QStringList colorKeyList = rColorsConfig.keyList();

    for (const QString &key : colorKeyList) {
        const QColor color = rColorsConfig.readEntry(key, QColor("blue"));
        m_colors[key] = color;
    }
    Q_EMIT layoutChanged();
}

int TodoSortFilterProxyModel::showCompleted() const
{
    return m_showCompleted;
}

void TodoSortFilterProxyModel::setShowCompleted(int showCompleted)
{
    Q_EMIT layoutAboutToBeChanged();
    m_showCompleted = showCompleted;
    m_showCompletedStore = showCompleted; // For when we search
    invalidateFilter();
    Q_EMIT showCompletedChanged();
    Q_EMIT layoutChanged();

    sortTodoModel();
}

Filter *TodoSortFilterProxyModel::filterObject() const
{
    return m_filterObject;
}

void TodoSortFilterProxyModel::setFilterObject(Filter *filterObject)
{
    if (m_filterObject == filterObject) {
        return;
    }

    if (m_filterObject) {
        disconnect(m_filterObject, nullptr, this, nullptr);
    }

    Q_EMIT filterObjectAboutToChange();
    Q_EMIT layoutAboutToBeChanged();
    m_filterObject = filterObject;
    Q_EMIT filterObjectChanged();

    const auto nameFilter = m_filterObject->name();
    const auto handleFilterNameChange = [this] {
        Q_EMIT filterObjectAboutToChange();
        setFilterFixedString(m_filterObject->name());
        Q_EMIT layoutChanged();
        Q_EMIT filterObjectChanged();
    };
    const auto handleFilterObjectChange = [this] {
        Q_EMIT filterObjectAboutToChange();
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
        beginFilterChange();
        endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
        invalidateFilter();
#endif
        Q_EMIT layoutChanged();
        Q_EMIT filterObjectChanged();
    };

    connect(m_filterObject, &Filter::nameChanged, this, handleFilterNameChange);
    connect(m_filterObject, &Filter::tagsChanged, this, handleFilterObjectChange);
    connect(m_filterObject, &Filter::collectionIdChanged, this, handleFilterObjectChange);
    connect(m_filterObject, &Filter::showCurrentDayOnlyChanged, this, handleFilterObjectChange);

    if (!nameFilter.isEmpty()) {
        setFilterFixedString(nameFilter);
    }

#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
    beginFilterChange();
    endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
    invalidateFilter();
#endif

    Q_EMIT layoutChanged();

    sortTodoModel();
}

void TodoSortFilterProxyModel::sortTodoModel()
{
    const auto order = m_sortAscending ? Qt::AscendingOrder : Qt::DescendingOrder;
    QSortFilterProxyModel::sort(m_sortColumn, order);
}

void TodoSortFilterProxyModel::filterTodoName(const QString &name, const int showCompleted)
{
    Q_EMIT layoutAboutToBeChanged();
    setFilterFixedString(name);
    if (!name.isEmpty()) {
        m_showCompleted = showCompleted;
    } else {
        setShowCompleted(m_showCompletedStore);
    }
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
    beginFilterChange();
    endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
    invalidateFilter();
#endif
    Q_EMIT layoutChanged();

    sortTodoModel();
}

int TodoSortFilterProxyModel::compareStartDates(const QModelIndex &left, const QModelIndex &right) const
{
    Q_ASSERT(left.column() == Akonadi::TodoModel::StartDateColumn);
    Q_ASSERT(right.column() == Akonadi::TodoModel::StartDateColumn);

    // The start date column is a QString, so fetch the to-do.
    // We can't compare QStrings because it won't work if the format is MM/DD/YYYY
    const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
    const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();

    if (!leftTodo || !rightTodo) {
        return 0;
    }

    const bool leftIsEmpty = !leftTodo->hasStartDate();
    const bool rightIsEmpty = !rightTodo->hasStartDate();

    if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a start date
        // For sorting, no date is considered a very big date
        return rightIsEmpty ? -1 : 1;
    } else if (!leftIsEmpty) { // Both have start dates
        const auto leftDateTime = leftTodo->dtStart();
        const auto rightDateTime = rightTodo->dtStart();

        if (leftDateTime == rightDateTime) {
            return 0;
        } else {
            return leftDateTime < rightDateTime ? -1 : 1;
        }
    } else { // Neither has a start date
        return 0;
    }
}

int TodoSortFilterProxyModel::compareCompletedDates(const QModelIndex &left, const QModelIndex &right) const
{
    Q_ASSERT(left.column() == Akonadi::TodoModel::CompletedDateColumn);
    Q_ASSERT(right.column() == Akonadi::TodoModel::CompletedDateColumn);

    const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
    const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();

    if (!leftTodo || !rightTodo) {
        return 0;
    }

    const bool leftIsEmpty = !leftTodo->hasCompletedDate();
    const bool rightIsEmpty = !rightTodo->hasCompletedDate();

    if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a completed date.
        // For sorting, no date is considered a very big date.
        return rightIsEmpty ? -1 : 1;
    } else if (!leftIsEmpty) { // Both have completed dates.
        const auto leftDateTime = leftTodo->completed();
        const auto rightDateTime = rightTodo->completed();

        if (leftDateTime == rightDateTime) {
            return 0;
        } else {
            return leftDateTime < rightDateTime ? -1 : 1;
        }
    } else { // Neither has a completed date.
        return 0;
    }
}

/* -1 - less than
 *  0 - equal
 *  1 - bigger than
 */
int TodoSortFilterProxyModel::compareDueDates(const QModelIndex &left, const QModelIndex &right) const
{
    Q_ASSERT(left.column() == Akonadi::TodoModel::DueDateColumn);
    Q_ASSERT(right.column() == Akonadi::TodoModel::DueDateColumn);

    // The due date column is a QString, so fetch the to-do.
    // We can't compare QStrings because it won't work if the format is MM/DD/YYYY
    const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
    const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
    Q_ASSERT(leftTodo);
    Q_ASSERT(rightTodo);

    if (!leftTodo || !rightTodo) {
        return 0;
    }

    const auto leftOverdue = leftTodo->isOverdue();
    const auto rightOverdue = rightTodo->isOverdue();

    if (leftOverdue != rightOverdue) {
        return leftOverdue ? -1 : 1;
    }

    const bool leftIsEmpty = !leftTodo->hasDueDate();
    const bool rightIsEmpty = !rightTodo->hasDueDate();

    if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a due date
        // For sorting, no date is considered a very big date
        return rightIsEmpty ? -1 : 1;
    } else if (!leftIsEmpty) { // Both have due dates
        const auto leftDateTime = leftTodo->dtDue();
        const auto rightDateTime = rightTodo->dtDue();

        if (leftDateTime == rightDateTime) {
            return 0;
        } else {
            return leftDateTime < rightDateTime ? -1 : 1;
        }
    } else { // Neither has a due date
        return 0;
    }
}

/* -1 - less than
 *  0 - equal
 *  1 - bigger than
 */
int TodoSortFilterProxyModel::compareCompletion(const QModelIndex &left, const QModelIndex &right) const
{
    Q_ASSERT(left.column() == Akonadi::TodoModel::PercentColumn);
    Q_ASSERT(right.column() == Akonadi::TodoModel::PercentColumn);

    const int leftValue = sourceModel()->data(left).toInt();
    const int rightValue = sourceModel()->data(right).toInt();

    if (leftValue == 100 && rightValue == 100) {
        // Break ties with the completion date.
        const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
        const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
        Q_ASSERT(leftTodo);
        Q_ASSERT(rightTodo);
        if (!leftTodo || !rightTodo) {
            return 0;
        } else {
            return (leftTodo->completed() > rightTodo->completed()) ? -1 : 1;
        }
    } else {
        return (leftValue < rightValue) ? -1 : 1;
    }
}

/* -1 - less than
 *  0 - equal
 *  1 - bigger than
 * Sort in numeric order (1 < 9) rather than priority order (lowest 9 < highest 1).
 * There are arguments either way, but this is consistent with KCalendarCore.
 */
int TodoSortFilterProxyModel::comparePriorities(const QModelIndex &left, const QModelIndex &right) const
{
    Q_ASSERT(left.isValid());
    Q_ASSERT(right.isValid());

    const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
    const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
    Q_ASSERT(leftTodo);
    Q_ASSERT(rightTodo);
    // Todos with no priority have a priority of 0 -- push these to list end in ascending order
    if (m_sortAscending && leftTodo->priority() == 0) {
        return 1;
    } else if (!leftTodo || !rightTodo || leftTodo->priority() == rightTodo->priority()) {
        return 0;
    } else if (leftTodo->priority() < rightTodo->priority()) {
        return -1;
    } else {
        return 1;
    }
}

bool TodoSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
    // Workaround for cases where lessThan will receive invalid left index
    if (!left.isValid()) {
        return true;
    }

    // To-dos without due date should appear last when sorting ascending,
    // so you can see the most urgent tasks first. (bug #174763)
    if (right.column() == Akonadi::TodoModel::DueDateColumn) {
        QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn); // Prevent possible assert fail

        const int comparison = compareDueDates(leftDueDateIndex, right);

        if (comparison != 0) {
            return comparison == -1;
        } else {
            // Due dates are equal, but the user still expects sorting by importance
            // Fallback to the PriorityColumn
            QModelIndex leftPriorityIndex = left.sibling(left.row(), Akonadi::TodoModel::PriorityColumn);
            QModelIndex rightPriorityIndex = right.sibling(right.row(), Akonadi::TodoModel::PriorityColumn);
            const int fallbackComparison = comparePriorities(leftPriorityIndex, rightPriorityIndex);

            if (fallbackComparison != 0) {
                return fallbackComparison == 1;
            }
        }
    } else if (right.column() == Akonadi::TodoModel::StartDateColumn) {
        return compareStartDates(left, right) == -1;
    } else if (right.column() == Akonadi::TodoModel::CompletedDateColumn) {
        return compareCompletedDates(left, right) == -1;
    } else if (right.column() == Akonadi::TodoModel::PriorityColumn) {
        const int comparison = comparePriorities(left, right);

        if (comparison != 0) {
            return comparison == -1;
        } else {
            // Priorities are equal, but the user still expects sorting by importance
            // Fallback to the DueDateColumn
            QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn);
            QModelIndex rightDueDateIndex = right.sibling(right.row(), Akonadi::TodoModel::DueDateColumn);
            const int fallbackComparison = compareDueDates(leftDueDateIndex, rightDueDateIndex);

            if (fallbackComparison != 0) {
                return fallbackComparison == 1;
            }
        }
    } else if (right.column() == Akonadi::TodoModel::PercentColumn) {
        const int comparison = compareCompletion(left, right);
        if (comparison != 0) {
            return comparison == -1;
        }
    }

    if (left.data() == right.data()) {
        // If both are equal, lets choose an order, otherwise Qt will display them randomly.
        // Fixes to-dos jumping around when you have calendar A selected, and then check/uncheck
        // a calendar B with no to-dos. No to-do is added/removed because calendar B is empty,
        // but you see the existing to-dos switching places.
        QModelIndex leftSummaryIndex = left.sibling(left.row(), Akonadi::TodoModel::SummaryColumn);
        QModelIndex rightSummaryIndex = right.sibling(right.row(), Akonadi::TodoModel::SummaryColumn);

        // This patch is not about fallingback to the SummaryColumn for sorting.
        // It's about avoiding jumping due to random reasons.
        // That's why we ignore the sort direction...
        return m_sortAscending ? QSortFilterProxyModel::lessThan(leftSummaryIndex, rightSummaryIndex)
                               : QSortFilterProxyModel::lessThan(rightSummaryIndex, leftSummaryIndex);

        // ...so, if you have 4 to-dos, all with CompletionColumn = "55%",
        // and click the header multiple times, nothing will happen because
        // it is already sorted by Completion.
    } else {
        return QSortFilterProxyModel::lessThan(left, right);
    }
}

int TodoSortFilterProxyModel::sortBy() const
{
    return m_sortColumn;
}

void TodoSortFilterProxyModel::setSortBy(int sortBy)
{
    m_sortColumn = sortBy;
    Q_EMIT sortByChanged();
    sortTodoModel();
}

bool TodoSortFilterProxyModel::sortAscending() const
{
    return m_sortAscending;
}

void TodoSortFilterProxyModel::setSortAscending(bool sortAscending)
{
    m_sortAscending = sortAscending;
    Q_EMIT sortAscendingChanged();
    sortTodoModel();
}

bool TodoSortFilterProxyModel::showCompletedSubtodosInIncomplete() const
{
    return m_showCompletedSubtodosInIncomplete;
}

void TodoSortFilterProxyModel::setShowCompletedSubtodosInIncomplete(bool showCompletedSubtodosInIncomplete)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
    beginFilterChange();
#endif
    m_showCompletedSubtodosInIncomplete = showCompletedSubtodosInIncomplete;
    Q_EMIT showCompletedSubtodosInIncompleteChanged();
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
    endFilterChange(QSortFilterProxyModel::Direction::Rows);
#else
    invalidateFilter();
#endif
}

Q_DECLARE_METATYPE(KCalendarCore::Incidence::Ptr)

#include "moc_todosortfilterproxymodel.cpp"
