// SPDX-FileCopyrightText: 2021 kaniini <https://git.pleroma.social/kaniini>
// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org>
// SPDX-License-Identifier: GPL-3.0-only

#include "timeline/timelinemodel.h"

#include <QJsonDocument>
#include <QNetworkReply>

using namespace Qt::Literals::StringLiterals;

TimelineModel::TimelineModel(QObject *parent)
    : AbstractTimelineModel(parent)
    , m_manager(&AccountManager::instance())
{
}

void TimelineModel::init()
{
    m_manager = &AccountManager::instance();
    m_account = m_manager->selectedAccount();

    if (m_account) {
        connect(m_account, &AbstractAccount::streamingEvent, this, &TimelineModel::handleEvent);
    }

    connect(this, &TimelineModel::showBoostsChanged, this, [this] {
        reset();
        fillTimeline();
    });
    connect(this, &TimelineModel::showRepliesChanged, this, [this] {
        reset();
        fillTimeline();
    });
    connect(this, &TimelineModel::showQuotesChanged, this, [this] {
        reset();
        fillTimeline();
    });

    connect(m_manager, &AccountManager::accountSelected, this, [this](AbstractAccount *account) {
        if (m_account == account || account == nullptr) {
            return;
        }

        if (m_account) {
            disconnect(m_account, &AbstractAccount::streamingEvent, this, &TimelineModel::handleEvent);
        }

        m_account = account;

        connect(m_account, &AbstractAccount::streamingEvent, this, &TimelineModel::handleEvent);

        reset();

        setLoading(false);

        Q_EMIT nameChanged();
        fillTimeline();
    });

    fillTimeline();
}

int TimelineModel::fetchedTimeline(const QByteArray &data, bool alwaysAppendToEnd)
{
    QList<Post *> posts;

    const auto doc = QJsonDocument::fromJson(data);

    if (!doc.isArray()) {
        return 0;
    }

    const auto array = doc.array();

    if (array.isEmpty()) {
        return 0;
    }

    std::ranges::transform(std::as_const(array), std::back_inserter(posts), [this](const QJsonValue &value) -> Post * {
        auto post = new Post(m_account, value.toObject(), this);
        if (!post->hidden()) {
            return post;
        }
        return nullptr;
    });

    posts.erase(std::ranges::remove_if(posts,
                                       [this](Post *post) {
                                           if (post == nullptr) {
                                               return true;
                                           }

                                           // Don't show boosts if requested
                                           if (!m_showBoosts && post->boostIdentity()) {
                                               return true;
                                           }
                                           // Don't show replies if requested
                                           if (!m_showReplies && !post->inReplyTo().isEmpty()) {
                                               return true;
                                           }
                                           // Don't show quotes if requested
                                           if (!m_showQuotes && post->quotedPost()) {
                                               return true;
                                           }
                                           // Make sure we aren't adding the same post we already have
                                           const auto it = std::ranges::find_if(std::as_const(m_timeline), [post](const auto &timelinePost) {
                                               return post->postId() == timelinePost->postId();
                                           });
                                           if (it != std::end(m_timeline)) {
                                               return true;
                                           }

                                           return false;
                                       })
                    .begin(),
                posts.end());

    // If we ended up removing all of the posts we were going to add, quit
    if (posts.empty()) {
        return 0;
    }

    for (auto &post : posts) {
        // If we are still waiting on the reply identity, make sure to update it's row
        if (!post->inReplyTo().isEmpty() && post->replyIdentity() == nullptr) {
            connect(
                post,
                &Post::replyIdentityChanged,
                this,
                [this, post] {
                    const int row = m_timeline.indexOf(post);
                    if (row != -1) {
                        Q_EMIT dataChanged(index(row, 0), index(row, 0), {ReplyAuthorIdentityRole});
                    }
                },
                Qt::SingleShotConnection);
        }
    }

    if (!m_timeline.isEmpty()) {
        if (alwaysAppendToEnd) {
            beginInsertRows({}, m_timeline.size(), m_timeline.size() + posts.size() - 1);
            m_timeline += posts;
            endInsertRows();
        } else {
            const auto postOld = m_timeline.first();
            const auto postNew = posts.first();
            if (postOld->originalPostId() > postNew->originalPostId()) {
                const int row = m_timeline.size();
                const int last = row + posts.size() - 1;
                beginInsertRows({}, row, last);
                m_timeline += posts;
                endInsertRows();
            } else {
                beginInsertRows({}, 0, posts.size() - 1);
                m_timeline = posts + m_timeline;
                endInsertRows();
            }
        }
    } else {
        beginInsertRows({}, 0, posts.size() - 1);
        m_timeline = posts;
        endInsertRows();
    }

    return posts.size();
}

void TimelineModel::fetchMore(const QModelIndex &parent)
{
    Q_UNUSED(parent);

    if (m_timeline.empty()) {
        return;
    }

    const auto p = m_timeline.last();

    if (m_shouldLoadMore) {
        fillTimeline(p->originalPostId());
    } else {
        m_shouldLoadMore = true;
    }
}

void TimelineModel::setShouldLoadMore(bool shouldLoadMore)
{
    m_shouldLoadMore = shouldLoadMore;
}

QString TimelineModel::findLatestPostId(const QStringList &postIds) const
{
    if (postIds.isEmpty()) {
        return QString();
    }

    // Mastodon API documentation claims that a server is responsible for sorting IDs before generating the API
    // response: https://docs.joinmastodon.org/api/guidelines/#id
    // So, we just need to find a first post ID on m_timeline.
    for (const auto post : std::as_const(m_timeline)) {
        if (postIds.contains(post->originalPostId())) {
            return post->originalPostId();
        }
    }

    qWarning() << "None of postIds found. It shouldn't happen.";
    return QString();
}

bool TimelineModel::canFetchMore(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return !loading();
}

int TimelineModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return m_timeline.size();
}

QVariant TimelineModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return {};
    }
    if (role == TypeRole) {
        return false;
    }
    return postData(m_timeline[index.row()], role);
}

void TimelineModel::actionReply(const QModelIndex &index)
{
    int row = index.row();
    auto p = m_timeline[row];

    Q_EMIT wantReply(m_account, p, index);
}

void TimelineModel::actionFavorite(const QModelIndex &index)
{
    const int row = index.row();
    const auto post = m_timeline[row];
    AbstractTimelineModel::actionFavorite(index, post);
}

void TimelineModel::actionRepeat(const QModelIndex &index)
{
    const int row = index.row();
    const auto post = m_timeline[row];
    AbstractTimelineModel::actionRepeat(index, post);
}

void TimelineModel::actionVote(const QModelIndex &index, const QList<int> &choices)
{
    const int row = index.row();
    const auto post = m_timeline[row];
    const auto poll = post->poll();
    Q_ASSERT(poll);

    QJsonObject obj;
    QJsonArray array;
    std::ranges::transform(std::as_const(choices), std::back_inserter(array), [](int choice) -> auto {
        return choice;
    });
    obj["choices"_L1] = array;
    QJsonDocument doc(obj);
    const auto id = poll->id();

    m_account->post(m_account->apiUrl(QStringLiteral("/api/v1/polls/%1/votes").arg(id)), doc, true, this, [this, id](QNetworkReply *reply) {
        int i = 0;
        for (auto &post : m_timeline) {
            if (post->poll() && post->poll()->id() == id) {
                const auto newPoll = QJsonDocument::fromJson(reply->readAll()).object();
                post->setPollJson(newPoll);
                Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0), {PollRole});
                break;
            }
            i++;
        }
    });
}

void TimelineModel::actionBookmark(const QModelIndex &index)
{
    int row = index.row();
    const auto post = m_timeline[row];

    AbstractTimelineModel::actionBookmark(index, post);
}

void TimelineModel::actionPin(const QModelIndex &index)
{
    int row = index.row();
    const auto post = m_timeline[row];

    AbstractTimelineModel::actionPin(index, post);
}

void TimelineModel::actionRedraft(const QModelIndex &index, bool isEdit)
{
    int row = index.row();
    auto p = m_timeline[row];

    AbstractTimelineModel::actionRedraft(index, p, isEdit);
}

void TimelineModel::actionDelete(const QModelIndex &index)
{
    int row = index.row();
    auto p = m_timeline[row];

    AbstractTimelineModel::actionDelete(index, p);

    beginRemoveRows({}, row, row);
    m_timeline.removeAt(row);
    endRemoveRows();
}

void TimelineModel::actionMute(const QModelIndex &index)
{
    int row = index.row();
    auto p = m_timeline[row];

    AbstractTimelineModel::actionMute(index, p);
}

void TimelineModel::handleEvent(AbstractAccount::StreamingEventType eventType, const QByteArray &payload)
{
    if (eventType == AbstractAccount::StreamingEventType::DeleteEvent) {
        int i = 0;
        for (const auto &post : std::as_const(m_timeline)) {
            if (post->originalPostId().toUtf8() == payload) {
                beginRemoveRows({}, i, i);
                m_timeline.removeAt(i);
                endRemoveRows();
                break;
            }
            i++;
        }
    }
}

#include "moc_timelinemodel.cpp"
