// SPDX-FileCopyrightText: 2017 Linus Jahn <lnj@kaidan.im>
// SPDX-FileCopyrightText: 2019 Jonah Brüchert <jbb@kaidan.im>
// SPDX-FileCopyrightText: 2019 Melvin Keskin <melvo@olomono.de>
// SPDX-FileCopyrightText: 2019 Yury Gubich <blue@macaw.me>
// SPDX-FileCopyrightText: 2022 Bhavy Airi <airiragahv@gmail.com>
// SPDX-FileCopyrightText: 2022 Tibor Csötönyi <dev@taibsu.de>
// SPDX-FileCopyrightText: 2023 Filipe Azevedo <pasnox@gmail.com>
//
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

// Qt
#include <QAbstractListModel>
// Kaidan
#include "Message.h"

class AccountSettings;
class AtmController;
class ChatController;
class Connection;
class EncryptionController;
class MessageController;
class NotificationController;

struct DisplayedMessageReaction {
    Q_GADGET
    Q_PROPERTY(QString emoji MEMBER emoji)
    Q_PROPERTY(int count MEMBER count)
    Q_PROPERTY(bool ownReactionIncluded MEMBER ownReactionIncluded)
    Q_PROPERTY(MessageReactionDeliveryState::Enum deliveryState MEMBER deliveryState)

public:
    QString emoji;
    int count;
    bool ownReactionIncluded;
    MessageReactionDeliveryState::Enum deliveryState;

    auto operator<=>(const DisplayedMessageReaction &other) const;
};

Q_DECLARE_METATYPE(DisplayedMessageReaction)

struct DetailedMessageReaction {
    QString senderId;
    QString senderJid;
    QString senderName;
    QStringList emojis;

    bool operator==(const DetailedMessageReaction &other) const = default;
    bool operator<(const DetailedMessageReaction &other) const;
};

Q_DECLARE_METATYPE(DetailedMessageReaction)

class MessageModel : public QAbstractListModel
{
    Q_OBJECT

    Q_PROPERTY(bool mamLoading READ mamLoading NOTIFY mamLoadingChanged)

public:
    enum MessageRoles {
        SenderJid = Qt::UserRole + 1,
        GroupChatSenderId,
        SenderName,
        Id,
        IsLastReadOwnMessage,
        IsLatestOldMessage,
        IsEdited,
        ReplyToJid,
        ReplyToGroupChatParticipantId,
        ReplyToName,
        ReplyId,
        ReplyQuote,
        Date,
        NextDate,
        Time,
        Body,
        Encryption,
        TrustLevel,
        DeliveryState,
        DeliveryStateIcon,
        DeliveryStateName,
        IsSpoiler,
        SpoilerHint,
        IsOwn,
        Files,
        DisplayedReactions,
        DetailedReactions,
        OwnReactionsFailed,
        GroupChatInvitationJid,
        GeoCoordinate,
        Marked,
        ErrorText,
    };
    Q_ENUM(MessageRoles)

    MessageModel(AccountSettings *accountSettings,
                 Connection *connection,
                 AtmController *atmController,
                 ChatController *chatController,
                 EncryptionController *encryptionController,
                 MessageController *messageController,
                 NotificationController *notificationController,
                 QObject *parent = nullptr);

    [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    [[nodiscard]] QHash<int, QByteArray> roleNames() const override;
    [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;

    Q_INVOKABLE void fetchMore(const QModelIndex &parent) override;
    Q_SIGNAL void messageFetchingFinished();
    Q_INVOKABLE bool canFetchMore(const QModelIndex &parent) const override;

    Q_INVOKABLE void resendMessage(int index);

    Q_INVOKABLE void handleMessageRead(int readMessageIndex);
    Q_INVOKABLE int firstUnreadContactMessageIndex() const;
    Q_INVOKABLE void setMessageMarked(int index, bool marked);

    /**
     * Adds a message reaction or undoes its pending/failed removal.
     *
     * @param messageId ID of the message to react to
     * @param emoji emoji in reaction to the message
     */
    Q_INVOKABLE void addMessageReaction(const QString &messageId, const QString &emoji);

    /**
     * Removes a message reaction or undoes its pending/failed addition.
     *
     * This must only be called if the reaction to be removed is already stored.
     *
     * @param messageId ID of the message to react to
     * @param emoji emoji in reaction to the message
     */
    Q_INVOKABLE void removeMessageReaction(const QString &messageId, const QString &emoji);

    /**
     * Sends message reactions again in case their delivery failed.
     *
     * This should only be called if there is at least one message reaction with the deliveryState
     * "ErrorOnAddition", "ErrorOnRemovalAfterSent" or "ErrorOnRemovalAfterDelivered".
     *
     * @param messageId ID of the message to react to
     */
    Q_INVOKABLE void resendMessageReactions(const QString &messageId);

    Q_INVOKABLE int nextCorrectableMessageIndex(int indexOffset) const;
    Q_INVOKABLE int previousCorrectableMessageIndex(int indexOffset) const;

    Q_INVOKABLE bool canCorrectMessage(int index) const;

    Q_INVOKABLE void deleteFile(int index, const File &file);

    /**
     * Removes a message locally.
     *
     * @param messageId ID of the message
     */
    Q_INVOKABLE void removeMessage(const QString &messageId);

    void removeAllMessages();

    /**
     * Searches a message locally by its ID.
     *
     * @param messageId ID of the message
     *
     * @return index of the found message, otherwise -1
     */
    Q_INVOKABLE int searchMessageById(const QString &messageId);

    /**
     * Emitted when searching a message in the DB and if found, fetching all messages until the
     * found one, is finished.
     *
     * @param foundMessageIndex index of the found message, otherwise -1
     */
    Q_SIGNAL void messageSearchByIdInDbFinished(int foundMessageIndex);

    /**
     * Searches from the most recent to the oldest message to find a given substring (case insensitive).
     *
     * If no index is passed, the search begins from the most recent message.
     *
     * @param searchString substring to search for
     * @param startIndex index of the first message to search for the given string
     *
     * @return index of the first found message containing the given string or -1 if no message containing the given string could be found
     */
    Q_INVOKABLE int searchForMessageFromNewToOld(const QString &searchString, int startIndex = 0);

    /**
     * Searches from the oldest to the most recent message to find a given substring (case insensitive).
     *
     * If no index is passed, the search begins from the oldest message.
     *
     * @param searchString substring to search for
     * @param startIndex index of the first message to search for the given string
     *
     * @return index of the first found message containing the given string or -1 if no message containing the given string could be found
     */
    Q_INVOKABLE int searchForMessageFromOldToNew(const QString &searchString, int startIndex = -1);

    /**
     * Emitted when fetching messages for a query string is completed.
     *
     * @param queryStringMessageIndex message index of found query string
     */
    Q_SIGNAL void messageSearchFinished(int queryStringMessageIndex);

    bool mamLoading() const;
    void setMamLoading(bool mamLoading);
    Q_SIGNAL void mamLoadingChanged();

private:
    void handleMessagesFetched(const QList<Message> &m_messages);
    void handleMamBacklogRetrieved(bool complete);

    void handleMessage(Message msg, MessageOrigin);
    void handleMessageUpdated(Message message);

    void handleDevicesChanged(QList<QString> jids);

    void addMessage(const Message &msg);
    void insertMessage(int i, const Message &msg);

    /**
     * Removes all messages of an account or an account's chat.
     *
     * @param accountJid JID of the account whose messages are being removed
     * @param chatJid JID of the chat whose messages are being removed (optional)
     */
    void removeMessages(const QString &accountJid, const QString &chatJid = {});

    void updateLastReadOwnMessageId();
    void updateFirstUnreadContactMessageIndex();

    void emitMessagesUpdated(const QList<QString> &messageIds, MessageRoles role);

    /**
     * Undoes a pending or failed removal of a message reaction.
     *
     * This should only be called if the removal of the reaction could not be submitted (yet).
     * That is either the case if Kaidan has not been connected since the user removed the reaction
     * or when there was an error on sending the change.
     *
     * @param messageId ID of the message to react to
     * @param senderJid JID of the reaction's sender
     * @param emoji emoji in reaction to the message
     * @param reactions message's reactions to be updated
     *
     * @return whether the message reaction's removal has been undone
     */
    bool undoMessageReactionRemoval(const QString &messageId, const QString &senderJid, const QString &emoji, const QList<MessageReaction> &reactions);

    /**
     * Undoes a pending or failed addition of a message reaction.
     *
     * This should only be called if the addition of the reaction could not be submitted (yet).
     * That is either the case if Kaidan has not been connected since the user added the reaction
     * or when there was an error on sending the change.
     *
     * @param messageId ID of the message to react to
     * @param senderJid JID of the reaction's sender
     * @param emoji emoji in reaction to the message
     * @param reactions message's reactions to be updated
     *
     * @return whether the message reaction's addition has been undone
     */
    bool undoMessageReactionAddition(const QString &messageId, const QString &senderJid, const QString &emoji, const QList<MessageReaction> &reactions);

    /**
     * Searches a message with a more recent date and returns that date.
     *
     * This is needed as a workaround for a bug in Qt Quick's implementation of the ListView section
     * for "verticalLayoutDirection: ListView.BottomToTop".
     * That bug results in each section label being displayed at the bottom of its corresponding
     * section instead of displaying it at the top of it.
     *
     * @param messageStartIndex index of the message to start the search with
     *
     * @return the date of the first message with a more recent date
     */
    QDate searchNextDate(int messageStartIndex) const;

    QString formatDate(QDate localDate) const;
    QString determineReplyToName(const Message::Reply &reply) const;

    AccountSettings *const m_accountSettings;
    Connection *const m_connection;

    AtmController *const m_atmController;
    ChatController *const m_chatController;
    MessageController *const m_messageController;
    NotificationController *const m_notificationController;

    QList<Message> m_messages;
    QString m_lastReadOwnMessageId;
    int m_firstUnreadContactMessageIndex = -1;
    bool m_fetchedAllFromDb = false;
    bool m_fetchedAllFromMam = false;
    bool m_mamLoading = false;
};
