/*
 * Copyright (C) 2020-2026 Savoir-faire Linux Inc.
 *
 * 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 3 of the License, 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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "messagesadapter.h"

#include "appsettingsmanager.h"
#include "qtutils.h"
#include "messageparser.h"
#include "previewengine.h"

#include <api/datatransfermodel.h>
#include <api/contact.h>

#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
#include <QFileInfo>
#include <QImageReader>
#include <QList>
#include <QMimeData>
#include <QMimeDatabase>
#include <QUrl>
#include <QtMath>
#include <QRegularExpression>

MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
                                 PreviewEngine* previewEngine,
                                 LRCInstance* instance,
                                 QObject* parent)
    : QmlAdapterBase(instance, parent)
    , settingsManager_(settingsManager)
    , messageParser_(new MessageParser(previewEngine, this))
    , filteredMsgListModel_(new FilteredMsgListModel(this))
    , mediaInteractions_(std::make_unique<MessageListModel>(nullptr))
    , timestampTimer_(new QTimer(this))
    , curLocale_(QLocale(settingsManager_->getLanguage()))
{
    setObjectName(typeid(*this).name());
    updateDateFormats();

    set_messageListModel(QVariant::fromValue(filteredMsgListModel_));

    connect(settingsManager_,
            &AppSettingsManager::reloadHistory,
            &lrcInstance_->accountModel(),
            &AccountModel::reloadHistory);

    connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, this, [this]() {
        set_replyToId("");
        set_editId("");
        const QString& convId = lrcInstance_->get_selectedConvUid();
        const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);

        // Reset the source model for the proxy model.
        filteredMsgListModel_->setSourceModel(conversation.interactions.get());

        set_currentConvComposingList(conversationTypersUrlToName(conversation.typers));
    });

    connect(messageParser_, &MessageParser::messageParsed, this, &MessagesAdapter::onMessageParsed);
    connect(messageParser_, &MessageParser::originalMessageParsed, this, &MessagesAdapter::onOriginalMessageParsed);
    connect(messageParser_, &MessageParser::linkInfoReady, this, &MessagesAdapter::onLinkInfoReady);

    connect(settingsManager_, &AppSettingsManager::localeChanged, this, [this]() {
        curLocale_ = QLocale((settingsManager_->getLanguage()));
        updateDateFormats();
    });
    connect(timestampTimer_, &QTimer::timeout, this, &MessagesAdapter::timestampUpdated);
    timestampTimer_->start(timestampUpdateIntervalMs_);

    connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, this, &MessagesAdapter::connectConversationModel);

    connectConversationModel();
}

bool
MessagesAdapter::isDocument(const interaction::Type& type)
{
    return interaction::Type::DATA_TRANSFER == type;
}

void
MessagesAdapter::loadMoreMessages()
{
    auto accountId = lrcInstance_->get_currentAccountId();
    auto convId = lrcInstance_->get_selectedConvUid();
    try {
        const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId);
        if (convInfo.isSwarm())
            lrcInstance_->getCurrentConversationModel()->loadConversationMessages(convId, loadChunkSize_);
    } catch (const std::exception& e) {
        qWarning() << e.what();
    }
}

void
MessagesAdapter::connectConversationModel()
{
    auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
    if (currentConversationModel == nullptr) {
        return;
    }

    QObject::connect(currentConversationModel,
                     &ConversationModel::newInteraction,
                     this,
                     &MessagesAdapter::onNewInteraction,
                     Qt::UniqueConnection);

    QObject::connect(currentConversationModel,
                     &ConversationModel::conversationMessagesLoaded,
                     this,
                     &MessagesAdapter::onConversationMessagesLoaded,
                     Qt::UniqueConnection);

    QObject::connect(currentConversationModel,
                     &ConversationModel::composingStatusChanged,
                     this,
                     &MessagesAdapter::onComposingStatusChanged,
                     Qt::UniqueConnection);

    QObject::connect(currentConversationModel,
                     &ConversationModel::messagesFoundProcessed,
                     this,
                     &MessagesAdapter::onMessagesFoundProcessed,
                     Qt::UniqueConnection);

    mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this));
    set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
}

void
MessagesAdapter::sendConversationRequest()
{
    lrcInstance_->makeConversationPermanent();
}

void
MessagesAdapter::sendMessage(const QString& message)
{
    try {
        const auto convUid = lrcInstance_->get_selectedConvUid();
        lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message, replyToId_);
    } catch (...) {
        qDebug() << "Exception during sendMessage:" << message;
    }
}

void
MessagesAdapter::sendMessageToUid(const QString& message, const QString& convUid)
{
    try {
        lrcInstance_->getCurrentConversationModel()->sendMessage(convUid, message, replyToId_);
    } catch (...) {
        qDebug() << "Exception during sendMessage:" << message;
    }
}

void
MessagesAdapter::editMessage(const QString& convId, const QString& newBody, const QString& messageId)
{
    try {
        auto editId = !messageId.isEmpty() ? messageId : editId_;
        if (editId.isEmpty()) {
            return;
        }
        set_editId("");
        lrcInstance_->getCurrentConversationModel()->editMessage(convId, newBody, editId);
    } catch (...) {
        qDebug() << "Exception during message edition:" << messageId;
    }
}

void
MessagesAdapter::removeEmojiReaction(const QString& convId, const QString& emoji, const QString& messageId)
{
    try {
        // check if this emoji has already been added by this author
        editMessage(convId, "", messageId);
    } catch (...) {
        qDebug() << "Exception during removeEmojiReaction():" << messageId;
    }
}

void
MessagesAdapter::addEmojiReaction(const QString& convId, const QString& emoji, const QString& messageId)
{
    try {
        lrcInstance_->getCurrentConversationModel()->reactMessage(convId, emoji, messageId);
    } catch (...) {
        qDebug() << "Exception during addEmojiReaction():" << messageId;
    }
}

void
MessagesAdapter::sendFile(const QString& message)
{
    QFileInfo fi(message);
    QString fileName = fi.fileName();
    try {
        auto convUid = lrcInstance_->get_selectedConvUid();
        lrcInstance_->getCurrentConversationModel()->sendFile(convUid, message, fileName, replyToId_);
    } catch (...) {
        qDebug() << "Exception during sendFile";
    }
}

void
MessagesAdapter::sendFileToUid(const QString& message, const QString& convUid)
{
    QFileInfo fi(message);
    QString fileName = fi.fileName();
    try {
        lrcInstance_->getCurrentConversationModel()->sendFile(convUid, message, fileName, replyToId_);
    } catch (...) {
        qDebug() << "Exception during sendFile";
    }
}

void
MessagesAdapter::joinCall(const QString& uri, const QString& deviceId, const QString& confId, bool videoMuted)
{
    lrcInstance_->getCurrentConversationModel()->joinCall(lrcInstance_->get_selectedConvUid(),
                                                          uri,
                                                          deviceId,
                                                          confId,
                                                          videoMuted);
}

void
MessagesAdapter::copyToDownloads(const QString& interactionId, const QString& displayName)
{
    auto downloadDir = lrcInstance_->accountModel().downloadDirectory;
    if (auto accInfo = &lrcInstance_->getCurrentAccountInfo()) {
        auto dest = accInfo->dataTransferModel->copyTo(lrcInstance_->get_currentAccountId(),
                                                       lrcInstance_->get_selectedConvUid(),
                                                       interactionId,
                                                       downloadDir,
                                                       displayName);

        QString downloadDirName = QDir(downloadDir).dirName();
        if (!dest.isEmpty()) {
            Q_EMIT fileCopied(displayName, downloadDirName);
        }
    }
}

void
MessagesAdapter::openUrl(const QString& url)
{
    if (!QDesktopServices::openUrl(url)) {
        qDebug() << "Couldn't open url: " << url;
    }
}

void
MessagesAdapter::openDirectory(const QString& path)
{
    QString p = path;
    QFileInfo f(p);
    if (f.exists()) {
        if (!f.isDir())
            p = f.dir().absolutePath();
        QString url;
        if (!p.startsWith("file:/"))
            url = "file:///" + p;
        else
            url = p;
        openUrl(url);
    }
}

void
MessagesAdapter::removeFile(const QString& interactionId, const QString& path)
{
    auto convUid = lrcInstance_->get_selectedConvUid();
    lrcInstance_->getCurrentConversationModel()->removeFile(convUid, interactionId, path);
}

void
MessagesAdapter::acceptFile(const QString& interactionId)
{
    auto convUid = lrcInstance_->get_selectedConvUid();
    lrcInstance_->getCurrentConversationModel()->acceptTransfer(convUid, interactionId);
}

void
MessagesAdapter::cancelFile(const QString& interactionId)
{
    const auto convUid = lrcInstance_->get_selectedConvUid();
    lrcInstance_->getCurrentConversationModel()->cancelTransfer(convUid, interactionId);
}

void
MessagesAdapter::onPaste()
{
    const QMimeData* mimeData = QApplication::clipboard()->mimeData();

    if (mimeData->hasImage()) {
        // Save temp data into a temp file.
        QPixmap pixmap = qvariant_cast<QPixmap>(mimeData->imageData());

        auto img_name_hash = QCryptographicHash::hash(QString::number(pixmap.cacheKey()).toLocal8Bit(),
                                                      QCryptographicHash::Sha1);
        QString fileName = "img_" + QString(img_name_hash.toHex()) + ".png";
        QString path = QDir::temp().filePath(fileName);

        if (!pixmap.save(path, "PNG")) {
            qDebug().noquote() << "Errors during QPixmap save" << "\n";
            return;
        }

        Q_EMIT newFilePasted(path);
    } else if (mimeData->hasUrls()) {
        const QList<QUrl> urlList = mimeData->urls();

        // Attach local files; also paste text when a non-local URL is
        // present (e.g. copied web links) so it is not dropped.
        bool pastedLocalFile = false;
        bool hasNonLocalUrl = false;
        for (const QUrl& url : urlList) {
            if (url.isLocalFile()) {
                Q_EMIT newFilePasted(url.toLocalFile());
                pastedLocalFile = true;
            } else {
                hasNonLocalUrl = true;
            }
        }
        if (!pastedLocalFile || hasNonLocalUrl)
            Q_EMIT newTextPasted();
    } else {
        // Treat as text content, make chatview.js handle in order to
        // avoid string escape problems
        Q_EMIT newTextPasted();
    }
}

QVariantMap
MessagesAdapter::getTransferStats(const QString& msgId, int status)
{
    Q_UNUSED(status)
    auto convModel = lrcInstance_->getCurrentConversationModel();
    lrc::api::datatransfer::Info info = {};
    convModel->getTransferInfo(lrcInstance_->get_selectedConvUid(), msgId, info);
    return {{"totalSize", qint64(info.totalSize)}, {"progress", qint64(info.progress)}};
}

QVariant
MessagesAdapter::dataForInteraction(const QString& interactionId, int role) const
{
    if (auto* model = getMsgListSourceModel()) {
        return model->data(interactionId, role);
    }
    return {};
}

void
MessagesAdapter::userIsComposing(bool isComposing)
{
    if (lrcInstance_->get_selectedConvUid().isEmpty())
        return;
    lrcInstance_->getCurrentConversationModel()->setIsComposing(lrcInstance_->get_selectedConvUid(), isComposing);
}

void
MessagesAdapter::onNewInteraction(const QString& convUid,
                                  const QString& interactionId,
                                  const interaction::Info& interaction)
{
    Q_UNUSED(interactionId);
    try {
        if (convUid.isEmpty() || convUid != lrcInstance_->get_selectedConvUid()) {
            return;
        }
        auto accountId = lrcInstance_->get_currentAccountId();
        auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
        auto& convModel = accountInfo.conversationModel;
        convModel->clearUnreadInteractions(convUid);
        Q_EMIT newInteraction(interactionId, static_cast<int>(interaction.type));
    } catch (...) {
    }
}

void
MessagesAdapter::onMessageParsed(const QString& messageId, const QString& parsed)
{
    if (messageId.isEmpty()) {
        Q_EMIT messageParsed(messageId, parsed);
        return;
    }
    const QString& convId = lrcInstance_->get_selectedConvUid();
    const QString& accId = lrcInstance_->get_currentAccountId();
    auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
    conversation.interactions->setParsedMessage(messageId, parsed);
}

void
MessagesAdapter::onOriginalMessageParsed(const QString& messageId, const QString& parsed)
{
    const QString& convId = lrcInstance_->get_selectedConvUid();
    const QString& accId = lrcInstance_->get_currentAccountId();
    auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
    conversation.interactions->setParsedOriginalBody(messageId, parsed);
}

void
MessagesAdapter::onLinkInfoReady(const QString& messageId, const QVariantMap& info)
{
    const QString& convId = lrcInstance_->get_selectedConvUid();
    const QString& accId = lrcInstance_->get_currentAccountId();
    auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
    conversation.interactions->addHyperlinkInfo(messageId, info);
}

void
MessagesAdapter::acceptInvitation(const QString& convId)
{
    auto conversationId = convId.isEmpty() ? lrcInstance_->get_selectedConvUid() : convId;
    auto* convModel = lrcInstance_->getCurrentConversationModel();
    convModel->acceptConversationRequest(conversationId);
}

void
MessagesAdapter::declineInvitation(const QString& convUid)
{
    const auto currentConvUid = convUid.isEmpty() ? lrcInstance_->get_selectedConvUid() : convUid;
    lrcInstance_->getCurrentConversationModel()->removeConversation(currentConvUid, false);
}

void
MessagesAdapter::blockConversation(const QString& convUid)
{
    const auto currentConvUid = convUid.isEmpty() ? lrcInstance_->get_selectedConvUid() : convUid;
    lrcInstance_->getCurrentConversationModel()->removeConversation(currentConvUid, true);
}

void
MessagesAdapter::unbanContact(int index)
{
    auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
    auto bannedContactList = accountInfo.contactModel->getBannedContacts();
    auto it = bannedContactList.begin();
    std::advance(it, index);

    try {
        auto contactInfo = accountInfo.contactModel->getContact(*it);
        accountInfo.contactModel->addContact(contactInfo);
    } catch (const std::out_of_range& e) {
        qDebug() << e.what();
    }
}

void
MessagesAdapter::unbanConversation(const QString& convUid)
{
    auto& accInfo = lrcInstance_->getCurrentAccountInfo();
    try {
        const auto contactUri = accInfo.conversationModel->peersForConversation(convUid).at(0);
        auto contactInfo = accInfo.contactModel->getContact(contactUri);
        accInfo.contactModel->addContact(contactInfo);
    } catch (const std::out_of_range& e) {
        qDebug() << e.what();
    }
}

void
MessagesAdapter::clearConversationHistory(const QString& accountId, const QString& convUid)
{
    lrcInstance_->getAccountInfo(accountId).conversationModel->clearHistory(convUid);
}

void
MessagesAdapter::removeConversation(const QString& convUid, bool keepContact)
{
    auto& accInfo = lrcInstance_->getCurrentAccountInfo();
    accInfo.conversationModel->removeConversation(convUid, false, keepContact);
}

void
MessagesAdapter::removeConversationMember(const QString& convUid, const QString& memberUri)
{
    auto& accInfo = lrcInstance_->getCurrentAccountInfo();
    accInfo.conversationModel->removeConversationMember(convUid, memberUri);
}

void
MessagesAdapter::addConversationMember(const QString& convUid, const QString& memberUri)
{
    auto& accInfo = lrcInstance_->getCurrentAccountInfo();
    accInfo.conversationModel->addConversationMember(convUid, memberUri);
}

void
MessagesAdapter::removeContact(const QString& convUid, bool banContact)
{
    auto& accInfo = lrcInstance_->getCurrentAccountInfo();

    // remove the uri from the default moderators list
    // TODO: seems like this should be done in libringclient
    QStringList list = lrcInstance_->accountModel().getDefaultModerators(accInfo.id);
    const auto contactUri = accInfo.conversationModel->peersForConversation(convUid).at(0);
    if (!contactUri.isEmpty() && list.contains(contactUri)) {
        lrcInstance_->accountModel().setDefaultModerator(accInfo.id, contactUri, false);
    }

    // actually remove the contact
    accInfo.contactModel->removeContact(contactUri, banContact);
}

void
MessagesAdapter::onConversationMessagesLoaded(uint32_t loadingRequestId, const QString& convId)
{
    if (convId != lrcInstance_->get_selectedConvUid())
        return;
    Q_EMIT moreMessagesLoaded(loadingRequestId);
}

void
MessagesAdapter::parseMessage(
    const QString& msgId, const QString& msg, bool showPreview, const QColor& linkColor, const QColor& backgroundColor)
{
    messageParser_->parseMessage(msgId, msg, showPreview, linkColor, backgroundColor);
}

void
MessagesAdapter::parseOriginalMessage(const QString& msgId,
                                      const QString& msg,
                                      const QColor& linkColor,
                                      const QColor& backgroundColor)
{
    messageParser_->parseOriginalMessage(msgId, msg, linkColor, backgroundColor);
}

void
MessagesAdapter::onComposingStatusChanged(const QString& convId, const QString& contactUri, bool isComposing)
{
    Q_UNUSED(contactUri)
    if (lrcInstance_->get_selectedConvUid() == convId) {
        const QString& accId = lrcInstance_->get_currentAccountId();
        auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
        set_currentConvComposingList(conversationTypersUrlToName(conversation.typers));
    }
}

void
MessagesAdapter::onMessagesFoundProcessed(const QString& accountId,
                                          const QMap<QString, interaction::Info>& messageInformation)
{
    if (lrcInstance_->get_currentAccountId() != accountId) {
        return;
    }

    bool isSearchInProgress = messageInformation.size();
    if (isSearchInProgress) {
        for (auto it = messageInformation.begin(); it != messageInformation.end(); it++) {
            mediaInteractions_->append(it.key(), it.value());
        }
    } else {
        set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));
    }
}

QList<QString>
MessagesAdapter::conversationTypersUrlToName(const QSet<QString>& typersSet)
{
    QList<QString> nameList;
    for (const auto& id : typersSet) {
        auto name = lrcInstance_->getCurrentContactModel()->bestNameForContact(id);
        nameList.append(name);
    }

    return nameList;
}

QVariantMap
MessagesAdapter::isLocalImage(const QString& mimename)
{
    if (mimename.startsWith("image/")) {
        QString fileFormat = mimename;

        QImageReader reader;
        QList<QByteArray> supportedFormats = reader.supportedMimeTypes();
        auto iterator = std::find_if(supportedFormats.begin(),
                                     supportedFormats.end(),
                                     [fileFormat](const QByteArray& format) { return format == fileFormat; });
        if (iterator != supportedFormats.end() && *iterator == "image/gif") {
            return {{"isAnimatedImage", true}};
        }
        return {{"isImage", iterator != supportedFormats.end()}};
    }
    return {{"isImage", false}};
}

QVariantMap
MessagesAdapter::getMediaInfo(const QString& msg)
{
    auto filePath = QFileInfo(msg).absoluteFilePath();
    static const QString html = "<body style='margin:0;padding:0;'>"
                                "<%1 style='width:100%;height:%2;outline:none;background-color:#f1f3f4;"
                                "object-fit:cover;' "
                                "controls controlsList='nodownload noplaybackrate' src='file://%3' type='%4'/></body>";
    QMimeDatabase db;
    QMimeType mime = db.mimeTypeForFile(filePath);
    QVariantMap fileInfo = isLocalImage(mime.name());
    if (fileInfo["isImage"].toBool() || fileInfo["isAnimatedImage"].toBool()) {
        return fileInfo;
    }
    static const QRegularExpression vPattern("(video/)(avi|mov|webm|webp|rmvb)$",
                                             QRegularExpression::CaseInsensitiveOption);
    auto match = vPattern.match(mime.name());
    auto captured = match.capturedTexts();
    QString type = captured.size() == 3 ? captured[1] : "";
    if (!type.isEmpty()) {
        return {
            {"isVideo", true},
            {"isAudio", false},
            {"html", html.arg("video", "100%", filePath, mime.name())},
        };
    } else {
        static const QRegularExpression aPattern("(audio/)(ogg|flac|wav|mpeg|mp3)$",
                                                 QRegularExpression::CaseInsensitiveOption);
        match = aPattern.match(mime.name());
        captured = match.capturedTexts();
        type = captured.size() == 3 ? captured[1] : "";
        if (!type.isEmpty()) {
            return {
                {"isVideo", false},
                {"isAudio", true},
                {"html", html.arg("audio", "54px", filePath, mime.name())},
            };
        }
    }
    return {};
}

bool
MessagesAdapter::isRemoteImage(const QString& msg)
{
    // TODO: test if all these open in the AnimatedImage component
    const static QRegularExpression imageRe("[^\\s]+(.*?)\\.(jpg|jpeg|png|gif|apng|webp|avif|flif)$",
                                            QRegularExpression::CaseInsensitiveOption);
    QRegularExpressionMatch match = imageRe.match(msg);
    return match.hasMatch();
}

QString
MessagesAdapter::getFormattedTime(const quint64 timestamp)
{
    const auto currentTime = QDateTime::currentDateTime();
    const auto seconds = currentTime.toSecsSinceEpoch() - timestamp;
    if (seconds > 60) {
        auto curTime = QDateTime::fromSecsSinceEpoch(timestamp).time();
        return curLocale_.toString(curTime, curLocale_.ShortFormat).toLower();
    }
    return QObject::tr("Just now");
}

QString
MessagesAdapter::getBestFormattedDate(const quint64 timestamp)
{
    auto currentDate = QDate::currentDate();
    auto timestampDate = QDateTime::fromSecsSinceEpoch(timestamp).date();
    if (timestampDate == currentDate)
        return getFormattedTime(timestamp);
    return getFormattedDay(timestamp);
}

void
MessagesAdapter::updateDateFormats()
{
    // Derive the preferred field order from the LongFormat (e.g., "Sunday, 5 October 2025").
    // This avoids issues with ShortFormat reflecting numeric preferences (like ISO in English)
    // rather than the cultural text convention.
    QString pattern = curLocale_.dateFormat(QLocale::LongFormat);

    // Remove Day-Of-Week tokens to avoid counting 'd' from 'dddd' as the day of month position.
    // We remove "dddd" first, then "ddd" to handle longest match.
    pattern.remove("dddd");
    pattern.remove("ddd");

    int idxY = pattern.indexOf('y');
    int idxM = pattern.indexOf('M');
    int idxD = pattern.indexOf('d');

    if (idxY != -1 && idxY < idxM && idxY < idxD) {
        // Year first (e.g. CJK, Hungarian)
        dateFormatCurrentYear_ = "MMM d";
        dateFormatOtherYears_ = "yyyy MMM d";
    } else if (idxD != -1 && idxM != -1 && idxD < idxM) {
        // Day before Month (e.g. European, Persian)
        dateFormatCurrentYear_ = "d MMM";
        dateFormatOtherYears_ = "d MMM yyyy";
    } else {
        // Month before Day (e.g. US)
        dateFormatCurrentYear_ = "MMM d";
        dateFormatOtherYears_ = "MMM d, yyyy";
    }
}

QString
MessagesAdapter::getFormattedDay(const quint64 timestamp)
{
    auto currentDate = QDate::currentDate();
    auto timestampDate = QDateTime::fromSecsSinceEpoch(timestamp).date();
    if (timestampDate == currentDate)
        return QObject::tr("Today");

    auto daysDiff = timestampDate.daysTo(currentDate);
    if (daysDiff == 1)
        return QObject::tr("Yesterday");

    if (daysDiff < 7 && daysDiff > 0)
        return curLocale_.dayName(timestampDate.dayOfWeek());

    if (timestampDate.year() == currentDate.year())
        return curLocale_.toString(timestampDate, dateFormatCurrentYear_);
    return curLocale_.toString(timestampDate, dateFormatOtherYears_);
}

void
MessagesAdapter::startSearch(const QString& text, bool isMedia)
{
    auto accountId = lrcInstance_->get_currentAccountId();
    mediaInteractions_.reset(new MessageListModel(&lrcInstance_->getCurrentAccountInfo(), this));
    set_mediaMessageListModel(QVariant::fromValue(mediaInteractions_.get()));

    if (text.isEmpty() && !isMedia)
        return;

    auto convId = lrcInstance_->get_selectedConvUid();

    try {
        lrcInstance_->getCurrentConversationModel()->getConvMediasInfos(accountId, convId, text, isMedia);
    } catch (...) {
        qDebug() << "Exception during startSearch()";
    }
}

MessageListModel*
MessagesAdapter::getMsgListSourceModel() const
{
    // We are certain that filteredMsgListModel_'s source model is a MessageListModel,
    // However it may be a nullptr if not yet set.
    return static_cast<MessageListModel*>(filteredMsgListModel_->sourceModel());
}
