/*!
 * Copyright (C) 2015-2024 Savoir-faire Linux Inc.
 * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
 * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
 * Author: Isa Nanic <isa.nanic@savoirfairelinux.com
 * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
 * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
 *
 * 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 "utils.h"

#include "jamiavatartheme.h"
#include "lrcinstance.h"

#include <qrencode.h>

#include <QApplication>
#include <QBitmap>
#include <QErrorMessage>
#include <QFile>
#include <QMessageBox>
#include <QObject>
#include <QPainter>
#include <QPropertyAnimation>
#include <QScreen>
#include <QDateTime>
#include <QSvgRenderer>
#include <QTranslator>
#include <QtConcurrent/QtConcurrent>
#include <QUuid>

#ifdef Q_OS_WIN
#include <lmcons.h>
#include <shlguid.h>
#include <shlobj.h>
#include <shlwapi.h>
#include <shobjidl.h>
#include <windows.h>
#endif

// Removes the given argument from the command line arguments, and invokes the callback
// function with the removed argument if it was found.
// This is required for custom args as quick_test_main_with_setup() will
// fail if given an invalid command-line argument.
void
Utils::remove_argument(char** argv,
                       int& argc,
                       const std::string& arg_to_remove,
                       std::function<void()> callback)
{
    // Remove arg_to_remove from argv, as quick_test_main_with_setup() will
    // fail if given an invalid command-line argument.
    auto new_end = std::remove_if(argv + 1, argv + argc, [&](char* arg_ptr) {
        if (std::string(arg_ptr).compare(arg_to_remove) == 0) {
            // Invoke the callback function with the removed argument.
            callback();
            return true;
        } else {
            return false;
        }
    });

    // If any occurrences were removed...
    if (new_end != argv + argc) {
        // Adjust the argument count.
        argc = std::distance(argv, new_end);
    }
}

void
Utils::testVulkanSupport()
{
#if defined(Q_OS_WIN)
    // Checks Vulkan support using the vulkan functions loaded directly
    // from vulkan-1.dll.
    struct DllLoader
    {
        explicit DllLoader(const std::string& filename)
            : module(LoadLibraryA(filename.c_str()))
        {
            if (module == nullptr) {
                throw std::runtime_error("Can't load module.");
            }
        }
        ~DllLoader()
        {
            FreeLibrary(module);
        }
        HMODULE module;
    } vk {"vulkan-1.dll"};

    typedef void*(__stdcall * PFN_vkGetInstanceProcAddr)(void*, const char*);
    auto vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)
        GetProcAddress(vk.module, "vkGetInstanceProcAddr");
    if (!vkGetInstanceProcAddr) {
        throw std::runtime_error("Missing vkGetInstanceProcAddr proc.");
    }

    typedef int(__stdcall * PFN_vkCreateInstance)(int*, void*, void**);
    auto vkCreateInstance = (PFN_vkCreateInstance) vkGetInstanceProcAddr(vk.module,
                                                                         "vkCreateInstance");
    if (!vkCreateInstance) {
        throw std::runtime_error("Missing vkCreateInstance proc.");
    }

    void* instance = 0;
    int VkInstanceCreateInfo[16] = {1};
    auto result = vkCreateInstance(VkInstanceCreateInfo, 0, &instance);
    if (!instance || result != 0) {
        throw std::runtime_error("Can't create Vulkan instance.");
    }
#endif
}

bool
Utils::CreateStartupLink(const std::wstring& wstrAppName)
{
#ifdef Q_OS_WIN
    TCHAR szPath[MAX_PATH];
    GetModuleFileName(NULL, szPath, MAX_PATH);

    std::wstring programPath(szPath);

    TCHAR startupPath[MAX_PATH];
    SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);

    std::wstring linkPath(startupPath);
    linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));

    return Utils::CreateLink(programPath.c_str(), linkPath.c_str());
#else
    Q_UNUSED(wstrAppName)
    QString desktopPath;
    /* cmake should set JAMI_INSTALL_PREFIX, otherwise it checks the following dirs
     *  - /usr/<data dir>
     *  - /usr/local/<data dir>
     *  - default install data dir
     */

#ifdef JAMI_INSTALL_PREFIX
    desktopPath = JAMI_INSTALL_PREFIX;
    desktopPath += "/jami/jami.desktop";
#else
    desktopPath = "share/jami/jami.desktop";
    QStringList paths = {"/usr/" + desktopPath,
                         "/usr/local/" + desktopPath,
                         QDir::currentPath() + "/../../install/client-qt/" + desktopPath};
    for (QString filename : paths) {
        if (QFile::exists(filename)) {
            desktopPath = filename;
            break;
        }
    }
#endif

    if (desktopPath.isEmpty() || !(QFile::exists(desktopPath))) {
        qDebug() << "Could not locate .desktop file at" << desktopPath;
        return false;
    }

    qDebug() << "Linking autostart file from" << desktopPath;

    QString desktopFile = QStandardPaths::locate(QStandardPaths::ConfigLocation,
                                                 "autostart/jami.desktop");
    if (!desktopFile.isEmpty()) {
        QFileInfo symlinkInfo(desktopFile);
        if (symlinkInfo.isSymLink()) {
            if (symlinkInfo.symLinkTarget() == desktopPath) {
                qDebug() << desktopFile << "already points to" << desktopPath;
                return true;
            } else {
                qDebug() << desktopFile << "exists but does not point to" << desktopPath;
                QFile::remove(desktopFile);
            }
        }
    } else {
        QString autoStartDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
                               + "/autostart";

        if (!QDir(autoStartDir).exists()) {
            if (QDir().mkdir(autoStartDir)) {
                qDebug() << "Created autostart directory:" << autoStartDir;
            } else {
                qWarning() << "Could not create autostart directory:" << autoStartDir;
                return false;
            }
        }
        desktopFile = autoStartDir + "/jami.desktop";
    }

    QFile srcFile(desktopPath);
    bool result = srcFile.link(desktopFile);
    qDebug() << desktopFile
             << (result ? "-> " + desktopPath + " successfully created" : "could not be created");
    return result;
#endif
}

bool
Utils::CreateLink(LPCWSTR lpszPathObj, LPCWSTR lpszPathLink)
{
#ifdef Q_OS_WIN
    HRESULT hres;
    IShellLink* psl;

    hres = CoCreateInstance(CLSID_ShellLink,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IShellLink,
                            (LPVOID*) &psl);
    if (SUCCEEDED(hres)) {
        IPersistFile* ppf;
        psl->SetPath(lpszPathObj);
        psl->SetArguments(TEXT("--minimized"));

        hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
        if (SUCCEEDED(hres)) {
            hres = ppf->Save(lpszPathLink, TRUE);
            ppf->Release();
        }
        psl->Release();
    }
    return hres;
#else
    Q_UNUSED(lpszPathObj)
    Q_UNUSED(lpszPathLink)
    return true;
#endif
}

void
Utils::DeleteStartupLink(const std::wstring& wstrAppName)
{
#ifdef Q_OS_WIN
    TCHAR startupPath[MAX_PATH];
    SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);

    std::wstring linkPath(startupPath);
    linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));

    DeleteFile(linkPath.c_str());

#else
    Q_UNUSED(wstrAppName)
    QString desktopFile = QStandardPaths::locate(QStandardPaths::ConfigLocation,
                                                 "autostart/jami.desktop");
    if (!desktopFile.isEmpty()) {
        try {
            QFile::remove(desktopFile);
            qDebug() << "Autostart disabled," << desktopFile << "removed";
        } catch (...) {
            qDebug() << "Could not remove" << desktopFile;
        }
    } else {
        qDebug() << desktopFile << "does not exist";
    }
#endif
}

bool
Utils::CheckStartupLink(const std::wstring& wstrAppName)
{
#ifdef Q_OS_WIN
    TCHAR startupPath[MAX_PATH];
    SHGetFolderPathW(NULL, CSIDL_STARTUP, NULL, 0, startupPath);

    std::wstring linkPath(startupPath);
    linkPath += std::wstring(TEXT("\\") + wstrAppName + TEXT(".lnk"));
    return PathFileExists(linkPath.c_str());
#else
    Q_UNUSED(wstrAppName)
    return (
        !QStandardPaths::locate(QStandardPaths::ConfigLocation, "autostart/jami.desktop").isEmpty());
#endif
}

void
Utils::removeOldVersions()
{
#ifdef Q_OS_WIN
    /*
     * As per: https://git.jami.net/savoirfairelinux/ring-client-windows/issues/429
     * NB: As only the 64-bit version of this application is distributed, we will only
     * remove 1. the configuration reg keys for Ring-x64, 2. the startup links for Ring,
     * 3. the winsparkle reg keys. The NSIS uninstall reg keys for Jami-x64 are removed
     * by the MSI installer.
     * Uninstallation of Ring, either 32 or 64 bit, is left to the user.
     * The current version of Jami will attempt to kill Ring.exe upon start if a startup
     * link is found.
     */
    QString node64 = "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node";
    QString hkcuSoftwareKey = "HKEY_CURRENT_USER\\Software\\";
    QString uninstKey = "\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";
    QString company = "Savoir-faire Linux Inc.";

    /*
     * 1. Configuration reg keys for Ring-x64.
     */
    QSettings(hkcuSoftwareKey + "jami.net\\Ring", QSettings::NativeFormat).remove("");
    QSettings(hkcuSoftwareKey + "ring.cx", QSettings::NativeFormat).remove("");

    /*
     * 2. Unset Ring as a startup application.
     */
    if (Utils::CheckStartupLink(TEXT("Ring"))) {
        qDebug() << "Found startup link for Ring. Removing it and killing Ring.exe.";
        Utils::DeleteStartupLink(TEXT("Ring"));
        QProcess process;
        process.start("taskkill",
                      QStringList() << "/im"
                                    << "Ring.exe"
                                    << "/f");
        process.waitForFinished();
    }

    /*
     * 3. Remove registry entries for winsparkle(both Jami-x64 and Ring-x64).
     */
    QSettings(hkcuSoftwareKey + company, QSettings::NativeFormat).remove("");
#else
    return;
#endif
}

QString
Utils::GetRingtonePath()
{
#ifdef Q_OS_WIN
    return QCoreApplication::applicationDirPath() + "\\ringtones\\default.opus";
#else
    return QString("/usr/share/jami/ringtones/default.opus");
#endif
}

QString
Utils::GenGUID()
{
#ifdef Q_OS_WIN
    GUID gidReference;
    wchar_t* str;
    HRESULT hCreateGuid = CoCreateGuid(&gidReference);
    if (hCreateGuid == S_OK) {
        StringFromCLSID(gidReference, &str);
        auto gStr = QString::fromWCharArray(str);
        return gStr.remove("{").remove("}").toLower();
    } else
        return QString();
#else
    return QString("");
#endif
}

QString
Utils::GetISODate()
{
#ifdef Q_OS_WIN
    SYSTEMTIME lt;
    GetSystemTime(&lt);
    return QString("%1-%2-%3T%4:%5:%6Z")
        .arg(lt.wYear)
        .arg(lt.wMonth, 2, 10, QChar('0'))
        .arg(lt.wDay, 2, 10, QChar('0'))
        .arg(lt.wHour, 2, 10, QChar('0'))
        .arg(lt.wMinute, 2, 10, QChar('0'))
        .arg(lt.wSecond, 2, 10, QChar('0'));
#else
    return QString();
#endif
}

QImage
Utils::accountPhoto(LRCInstance* instance, const QString& accountId, const QSize& size)
{
    QImage photo;
    try {
        auto& accInfo = instance->accountModel().getAccountInfo(
            accountId.isEmpty() ? instance->get_currentAccountId() : accountId);
        auto bestName = instance->accountModel().bestNameForAccount(accInfo.id);
        if (!accInfo.profileInfo.avatar.isEmpty()) {
            photo = imageFromBase64String(accInfo.profileInfo.avatar);
            if (photo.isNull()) {
                qWarning() << "Invalid image for account " << bestName;
            }
        }
        if (photo.isNull()) {
            QString name = bestName == accInfo.profileInfo.uri ? QString() : bestName;
            QString prefix = accInfo.profileInfo.type == profile::Type::JAMI ? "jami:" : "sip:";
            photo = fallbackAvatar(prefix + accInfo.profileInfo.uri, name, size);
        }
    } catch (const std::exception& e) {
        qDebug() << e.what() << "; Using default avatar";
        photo = fallbackAvatar(QString(), QString(), size);
    }
    return Utils::scaleAndFrame(photo, size);
}

QImage
Utils::contactPhoto(LRCInstance* instance,
                    const QString& contactUri,
                    const QSize& size,
                    const QString& accountId)
{
    QImage photo;
    try {
        auto& accInfo = instance->accountModel().getAccountInfo(
            accountId.isEmpty() ? instance->get_currentAccountId() : accountId);
        auto contactInfo = accInfo.contactModel->getContact(contactUri);
        auto contactPhoto = accInfo.contactModel->avatar(contactUri);
        auto bestName = accInfo.contactModel->bestNameForContact(contactUri);
        if (accInfo.profileInfo.type == profile::Type::SIP
            && contactInfo.profileInfo.type == profile::Type::TEMPORARY) {
            photo = Utils::fallbackAvatar(QString(), QString());
        } else if (contactInfo.profileInfo.type == profile::Type::TEMPORARY
                   && contactInfo.profileInfo.uri.isEmpty()) {
            photo = Utils::fallbackAvatar(QString(), QString());
        } else if (!contactPhoto.isEmpty()) {
            photo = imageFromBase64String(contactPhoto);
            if (photo.isNull()) {
                auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
                photo = Utils::fallbackAvatar("jami:" + contactInfo.profileInfo.uri, avatarName);
            }
        } else {
            auto avatarName = contactInfo.profileInfo.uri == bestName ? QString() : bestName;
            photo = Utils::fallbackAvatar("jami:" + contactInfo.profileInfo.uri, avatarName);
        }
    } catch (const std::exception&) {
        photo = fallbackAvatar("jami:" + contactUri, QString(), size);
    }
    return Utils::scaleAndFrame(photo, size);
}

QImage
Utils::conversationAvatar(LRCInstance* instance,
                          const QString& convId,
                          const QSize& size,
                          const QString& accountId)
{
    QImage avatar(size, QImage::Format_ARGB32_Premultiplied);
    avatar.fill(Qt::transparent);
    QPainter painter(&avatar);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    try {
        auto& accInfo = instance->accountModel().getAccountInfo(
            accountId.isEmpty() ? instance->get_currentAccountId() : accountId);
        auto* convModel = accInfo.conversationModel.get();
        auto avatarb64 = convModel->avatar(convId);
        if (!avatarb64.isEmpty()) {
            auto photo = imageFromBase64String(avatarb64, true);
            if (!photo.isNull()) {
                return scaleAndFrame(photo, size);
            }
            qWarning() << "Couldn't load image from base 64 data for conversation " << convId;
        }
        // Else, generate an avatar
        auto members = convModel->peersForConversation(convId);
        if (members.size() < 1)
            return avatar;
        auto getPhoto = [&](const auto& uri) {
            return uri == accInfo.profileInfo.uri ? accountPhoto(instance, accountId, size)
                                                  : contactPhoto(instance, uri, size, "");
        };
        if (members.size() == 1) {
            // Only member in the swarm or 1:1, draw only peer's avatar
            auto peerAvatar = getPhoto(members[0]);
            painter.drawImage(avatar.rect(), peerAvatar);
            return avatar;
        }
        // Else, combine avatars
        auto peerAAvatar = getPhoto(members[0]);
        auto peerBAvatar = getPhoto(members[1]);
        peerAAvatar = Utils::halfCrop(peerAAvatar, true);
        peerBAvatar = Utils::halfCrop(peerBAvatar, false);
        painter.drawImage(avatar.rect(), peerAAvatar);
        painter.drawImage(avatar.rect(), peerBAvatar);
    } catch (const std::exception& e) {
        qDebug() << Q_FUNC_INFO << e.what();
    }
    return avatar;
}

QImage
Utils::tempConversationAvatar(const QSize& size)
{
    QString img = QByteArrayFromFile(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
                                     + "tmpSwarmImage");
    if (img.isEmpty())
        return fallbackAvatar(QString(), QString(), size);
    return scaleAndFrame(imageFromBase64String(img, true), size);
}

QImage
Utils::imageFromBase64String(const QString& str, bool circleCrop)
{
    if (str.isEmpty())
        return {};
    return imageFromBase64Data(Utils::base64StringToByteArray(str), circleCrop);
}

QImage
Utils::imageFromBase64Data(const QByteArray& data, bool circleCrop)
{
    QImage img;

    if (img.loadFromData(data)) {
        if (circleCrop) {
            return Utils::getCirclePhoto(img, img.size().width());
        }
        return img;
    }
    return {};
}

QImage
Utils::getCirclePhoto(const QImage original, int sizePhoto)
{
    QImage target(sizePhoto, sizePhoto, QImage::Format_ARGB32_Premultiplied);
    target.fill(Qt::transparent);

    QPainter painter(&target);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    painter.setBrush(QBrush(Qt::white));
    auto scaledPhoto = original
                           .scaled(sizePhoto,
                                   sizePhoto,
                                   Qt::KeepAspectRatioByExpanding,
                                   Qt::SmoothTransformation)
                           .convertToFormat(QImage::Format_ARGB32_Premultiplied);
    int margin = 0;
    if (scaledPhoto.width() > sizePhoto) {
        margin = (scaledPhoto.width() - sizePhoto) / 2;
    }
    painter.drawEllipse(0, 0, sizePhoto, sizePhoto);
    painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
    painter.drawImage(0, 0, scaledPhoto, margin, 0);
    return target;
}

QImage
Utils::halfCrop(const QImage original, bool leftSide)
{
    auto width = original.size().width();
    auto height = original.size().height();
    QImage target(width, height, QImage::Format_ARGB32_Premultiplied);
    target.fill(Qt::transparent);

    QPainter painter(&target);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    painter.setBrush(QBrush(Qt::white));
    painter.drawRect(leftSide ? 0 : width / 2 + 2, 0, width / 2 - (leftSide ? 2 : 0), height);
    painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
    painter.drawImage(0, 0, original, 0, 0);
    return target;
}

QSize
Utils::getRealSize(QScreen* screen)
{
#ifdef Q_OS_WIN
    DEVMODE dmThisScreen;
    ZeroMemory(&dmThisScreen, sizeof(dmThisScreen));
    EnumDisplaySettings((const wchar_t*) screen->name().utf16(),
                        ENUM_CURRENT_SETTINGS,
                        (DEVMODE*) &dmThisScreen);
    return QSize(dmThisScreen.dmPelsWidth, dmThisScreen.dmPelsHeight);
#else
    Q_UNUSED(screen)
    return {};
#endif
}

void
Utils::forceDeleteAsync(const QString& path)
{
    /*
     * Keep deleting file until the process holding it let go,
     * or the file itself does not exist anymore.
     */
    auto futureResult = QtConcurrent::run([path] {
        QFile file(path);
        if (!QFile::exists(path))
            return;
        int retries {0};
        while (!file.remove() && retries < 5) {
            qDebug().noquote() << "\n" << file.errorString() << "\n";
            QThread::msleep(10);
            ++retries;
        }
    });
}

QString
Utils::getProjectCredits()
{
    QFile projectCreditsFile(":/misc/projectcredits.html");
    if (!projectCreditsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug().noquote() << " Project Credits failed to load";
        return {};
    }
    QTextStream in(&projectCreditsFile);
    return in.readAll();
}

inline QString
removeEndlines(const QString& str)
{
    QString trimmed(str);
    trimmed.remove(QChar('\n'));
    trimmed.remove(QChar('\r'));
    return trimmed;
}

lrc::api::profile::Type
Utils::profileType(const lrc::api::conversation::Info& conv,
                   const lrc::api::ConversationModel& model)
{
    try {
        auto contact = model.owner.contactModel->getContact(conv.participants[0].uri);
        return contact.profileInfo.type;
    } catch (const std::out_of_range& e) {
        qDebug() << e.what();
        return lrc::api::profile::Type::INVALID;
    }
}

bool
Utils::isInteractionGenerated(const lrc::api::interaction::Type& type)
{
    return type == lrc::api::interaction::Type::CALL
           || type == lrc::api::interaction::Type::CONTACT;
}

bool
Utils::isContactValid(const QString& contactUid, const lrc::api::ConversationModel& model)
{
    try {
        const auto contact = model.owner.contactModel->getContact(contactUid);
        return (contact.profileInfo.type == lrc::api::profile::Type::PENDING
                || contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY
                || contact.profileInfo.type == lrc::api::profile::Type::JAMI
                || contact.profileInfo.type == lrc::api::profile::Type::SIP)
               && !contact.profileInfo.uri.isEmpty();
    } catch (const std::out_of_range& e) {
        qDebug() << e.what();
        return false;
    }
}

bool
Utils::getReplyMessageBox(QWidget* widget, const QString& title, const QString& text)
{
    if (QMessageBox::question(widget, title, text, QMessageBox::Yes | QMessageBox::No)
        == QMessageBox::Yes)
        return true;
    return false;
}

QColor
Utils::getAvatarColor(const QString& canonicalUri)
{
    if (canonicalUri.isEmpty()) {
        return JamiAvatarTheme::defaultAvatarColor_;
    }
    auto h = QString(
        QCryptographicHash::hash(canonicalUri.toLocal8Bit(), QCryptographicHash::Md5).toHex());
    if (h.isEmpty() || h.isNull()) {
        return JamiAvatarTheme::defaultAvatarColor_;
    }
    auto colorIndex = std::string("0123456789abcdef").find(h.at(0).toLatin1());
    return JamiAvatarTheme::avatarColors_[colorIndex];
}

/*!
 * Generate a QImage representing a default user avatar, when the user doesn't provide it.
 * If the name passed is empty, then the default avatar picture will be displayed instead
 * of a letter.
 *
 * @param canonicalUri uri containing the account type prefix used to obtain the bgcolor
 * @param name the string used to acquire the letter centered in the avatar
 * @param size the dimensions of the desired image
 */
QImage
Utils::fallbackAvatar(const QString& canonicalUri, const QString& name, const QSize& size)
{
    auto sizeToUse = size.height() >= defaultAvatarSize.height() ? size : defaultAvatarSize;

    QImage avatar(sizeToUse, QImage::Format_ARGB32);
    avatar.fill(Qt::transparent);

    QPainter painter(&avatar);
    painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
    painter.setPen(Qt::transparent);

    // background circle
    painter.setBrush(getAvatarColor(canonicalUri).lighter(110));
    painter.drawEllipse(avatar.rect());

    // if a letter was passed, then we paint a letter in the circle,
    // otherwise we draw the default avatar icon
    QString trimmedName(name);
    const static QRegularExpression newlineRe("[\\n\\t\\r]");
    if (!trimmedName.remove(newlineRe).isEmpty()) {
        auto unicode = trimmedName.toUcs4().at(0);
        if (unicode >= 0x1F000 && unicode <= 0x1FFFF) {
            // emoticon
            auto letter = QString::fromUcs4(reinterpret_cast<char32_t*>(&unicode), 1);
            QFont font(QString("Segoe UI Emoji").split(QLatin1Char(',')),
                       avatar.height() / 2.66667,
                       QFont::Medium);
            painter.setFont(font);
            QRect emojiRect(avatar.rect());
            emojiRect.moveTop(-6);
            painter.drawText(emojiRect, letter, QTextOption(Qt::AlignCenter));
        } else if (unicode >= 0x0000 && unicode <= 0x00FF) {
            // basic Latin
            auto letter = trimmedName.at(0).toUpper();
            QFont font(QString("Arial").split(QLatin1Char(',')),
                       avatar.height() / 2.66667,
                       QFont::Medium);
            painter.setFont(font);
            painter.setPen(Qt::white);
            painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
        } else {
            auto letter = QString::fromUcs4(reinterpret_cast<char32_t*>(&unicode), 1);
            QFont font(QString("Arial").split(QLatin1Char(',')),
                       avatar.height() / 2.66667,
                       QFont::Medium);
            painter.setFont(font);
            painter.setPen(Qt::white);
            painter.drawText(avatar.rect(), QString(letter), QTextOption(Qt::AlignCenter));
        }
    } else {
        QRect overlayRect = avatar.rect();
        qreal margin = (0.05 * overlayRect.width());
        overlayRect.moveLeft(overlayRect.left() + margin * 0.5);
        overlayRect.moveTop(overlayRect.top() + margin * 0.5);
        overlayRect.setWidth(overlayRect.width() - margin);
        overlayRect.setHeight(overlayRect.height() - margin);
        painter.drawPixmap(overlayRect, QPixmap(":/images/default_avatar_overlay.svg"));
    }

    return avatar.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

QImage
Utils::fallbackAvatar(const std::string& alias, const std::string& uri, const QSize& size)
{
    return fallbackAvatar(QString::fromStdString(uri), QString::fromStdString(alias), size);
}

QByteArray
Utils::QImageToByteArray(QImage image)
{
    QByteArray ba;
    QBuffer buffer(&ba);
    buffer.open(QIODevice::WriteOnly);
    image.save(&buffer, "PNG");
    return ba;
}

QString
Utils::byteArrayToBase64String(QByteArray byteArray)
{
    return QString::fromLatin1(byteArray.toBase64().data());
}

QByteArray
Utils::base64StringToByteArray(QString base64)
{
    return QByteArray::fromBase64(base64.toLatin1());
}

QImage
Utils::cropImage(const QImage& img)
{
    auto w = img.width();
    auto h = img.height();
    if (w > h) {
        return img.copy({(w - h) / 2, 0, h, h});
    }
    return img.copy({0, (h - w) / 2, w, w});
}

QPixmap
Utils::pixmapFromSvg(const QString& svg_resource, const QSize& size)
{
    QSvgRenderer svgRenderer(svg_resource);
    QPixmap pixmap(size);
    pixmap.fill(Qt::transparent);
    QPainter pixPainter(&pixmap);
    svgRenderer.render(&pixPainter);
    return pixmap;
}

QImage
Utils::getQRCodeImage(QString data, int margin)
{
    auto qrcode = QRcode_encodeString(data.toStdString().c_str(),
                                      0,            // Let the version be decided by libqrencode
                                      QR_ECLEVEL_L, // Lowest level of error correction
                                      QR_MODE_8,    // 8-bit data mode
                                      1);
    if (not qrcode) {
        qWarning() << "Failed to generate QR code";
        return QImage();
    }

    int qrwidth = qrcode->width + margin * 2;
    QImage result(QSize(qrwidth, qrwidth), QImage::Format_Mono);
    QPainter painter;
    painter.begin(&result);
    painter.setClipRect(QRect(0, 0, qrwidth, qrwidth));
    painter.setPen(QPen(Qt::black, 0.1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
    painter.setBrush(Qt::black);
    painter.fillRect(QRect(0, 0, qrwidth, qrwidth), Qt::white);
    unsigned char* p;
    p = qrcode->data;
    for (int y = 0; y < qrcode->width; y++) {
        unsigned char* row = (p + (y * qrcode->width));
        for (int x = 0; x < qrcode->width; x++) {
            if (*(row + x) & 0x1) {
                painter.drawRect(margin + x, margin + y, 1, 1);
            }
        }
    }
    painter.end();
    QRcode_free(qrcode);
    return result;
}

QByteArray
Utils::QByteArrayFromFile(const QString& filename)
{
    QFile file(filename);
    if (!file.exists()) {
        qDebug() << "QByteArrayFromFile: file does not exist" << filename;
    }
    if (file.open(QIODevice::ReadOnly)) {
        return file.readAll();
    }
    qDebug() << "QByteArrayFromFile: can't open file" << filename;
    return {};
}

QPixmap
Utils::generateTintedPixmap(const QString& filename, QColor color)
{
    QPixmap px(filename);
    QImage tmpImage = px.toImage();
    for (int y = 0; y < tmpImage.height(); y++) {
        for (int x = 0; x < tmpImage.width(); x++) {
            color.setAlpha(tmpImage.pixelColor(x, y).alpha());
            tmpImage.setPixelColor(x, y, color);
        }
    }
    return QPixmap::fromImage(tmpImage);
}

QPixmap
Utils::generateTintedPixmap(const QPixmap& pix, QColor color)
{
    QPixmap px = pix;
    QImage tmpImage = px.toImage();
    for (int y = 0; y < tmpImage.height(); y++) {
        for (int x = 0; x < tmpImage.width(); x++) {
            color.setAlpha(tmpImage.pixelColor(x, y).alpha());
            tmpImage.setPixelColor(x, y, color);
        }
    }
    return QPixmap::fromImage(tmpImage);
}

QImage
Utils::scaleAndFrame(const QImage photo, const QSize& size)
{
    return photo.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

QString
Utils::humanFileSize(qint64 fileSize)
{
    float fileSizeF = static_cast<float>(fileSize);
    float thresh = 1024;

    if (abs(fileSizeF) < thresh) {
        return QString::number(fileSizeF) + " B";
    }
    QString units[] = {"KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
    int unit_position = -1;
    do {
        fileSizeF /= thresh;
        ++unit_position;
    } while (abs(fileSizeF) >= thresh && unit_position < units->size() - 1);
    /*
     * Round up to two decimal.
     */
    fileSizeF = roundf(fileSizeF * 100) / 100;
    return QString::number(fileSizeF) + " " + units[unit_position];
}

QString
Utils::getDebugFilePath()
{
    QDir logPath(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
    logPath.cdUp();
    return QString(logPath.absolutePath() + "/jami/jami.log");
}

bool
Utils::isImage(const QString& fileExt)
{
    if (fileExt == "png" || fileExt == "jpg" || fileExt == "jpeg")
        return true;
    return false;
}

QString
Utils::generateUid()
{
    return QUuid::createUuid().toString(QUuid::Id128);
}
