/* SPDX-License-Identifier: GPL-3.0-or-later
 * Copyright © 2014-2019 by The qTox Project Contributors
 * Copyright © 2024-2025 The TokTok team.
 */

#include "profileform.h"

#include "ui_profileform.h"

#include "src/core/core.h"
#include "src/model/profile/iprofileinfo.h"
#include "src/persistence/profile.h"
#include "src/persistence/profilelocker.h"
#include "src/persistence/settings.h"
#include "src/widget/commondialogs.h"
#include "src/widget/contentlayout.h"
#include "src/widget/form/setpassworddialog.h"
#include "src/widget/form/settingswidget.h"
#include "src/widget/maskablepixmapwidget.h"
#include "src/widget/style.h"
#include "src/widget/tool/croppinglabel.h"
#include "src/widget/tool/imessageboxmanager.h"
#include "src/widget/translator.h"
#include "src/widget/widget.h"

#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QComboBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QImageReader>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMap>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QWindow>

namespace {
const QMap<IProfileInfo::SetAvatarResult, QString> SET_AVATAR_ERROR = {
    {IProfileInfo::SetAvatarResult::CanNotOpen, ProfileForm::tr("Unable to open this file.")},
    {IProfileInfo::SetAvatarResult::CanNotRead, ProfileForm::tr("Unable to read this image.")},
    {IProfileInfo::SetAvatarResult::TooLarge,
     ProfileForm::tr("The supplied image is too large.\nPlease use another image.")},
    {IProfileInfo::SetAvatarResult::EmptyPath, ProfileForm::tr("Empty path is unavailable")},
};

const QMap<IProfileInfo::RenameResult, QPair<QString, QString>> RENAME_ERROR = {
    {IProfileInfo::RenameResult::Error,
     {ProfileForm::tr("Failed to rename"),
      ProfileForm::tr("Couldn't rename the profile to \"%1\"")}},
    {IProfileInfo::RenameResult::ProfileAlreadyExists,
     {ProfileForm::tr("Profile already exists"),
      ProfileForm::tr("A profile named \"%1\" already exists.")}},
    {IProfileInfo::RenameResult::EmptyName,
     {ProfileForm::tr("Empty name"), ProfileForm::tr("Empty name is unavailable")}},
};

// TODO(iphydf): These should be functions, not static maps. qTox does this a lot
// and it messes with retranslation when changing the language.
const QMap<IProfileInfo::SaveResult, QPair<QString, QString>> SAVE_ERROR = {
    {
        IProfileInfo::SaveResult::NoWritePermission,
        CommonDialogs::NoWritePermission(),
    },
    {IProfileInfo::SaveResult::Error,
     {ProfileForm::tr("Failed to save file"),
      ProfileForm::tr("The file you chose could not be saved.")}},
    {IProfileInfo::SaveResult::EmptyPath,
     {ProfileForm::tr("Empty path"), ProfileForm::tr("Empty path is unavailable.")}},
};

const QPair<QString, QString> CAN_NOT_CHANGE_PASSWORD =
    {ProfileForm::tr("Couldn't change password"),
     ProfileForm::tr("Couldn't change database password, "
                     "it may be corrupted or use the old password.")};
} // namespace

ProfileForm::ProfileForm(IProfileInfo* profileInfo_, Settings& settings_, Style& style,
                         IMessageBoxManager& messageBoxManager_, QWidget* parent)
    : QWidget{parent}
    , qr{nullptr}
    , profileInfo{profileInfo_}
    , settings{settings_}
    , messageBoxManager{messageBoxManager_}
{
    bodyUI = new Ui::IdentitySettings;
    bodyUI->setupUi(this);

    const uint32_t maxNameLength = tox_max_name_length();
    const QString toolTip = tr("Tox user names cannot exceed %1 characters.").arg(maxNameLength);
    bodyUI->userNameLabel->setToolTip(toolTip);
    bodyUI->userName->setMaxLength(static_cast<int>(maxNameLength));

    // tox
    toxId = new ClickableTE();
    toxId->setFont(Style::getFont(Style::Font::Small));
    toxId->setToolTip(bodyUI->toxId->toolTip());

    auto* toxIdGroup = qobject_cast<QVBoxLayout*>(bodyUI->toxGroup->layout());
    delete toxIdGroup->replaceWidget(bodyUI->toxId, toxId); // Original toxId is in heap, delete it
    bodyUI->toxId->hide();

    bodyUI->qrLabel->setWordWrap(true);

    profilePicture = new MaskablePixmapWidget(this, QSize(64, 64), ":/img/avatar_mask.svg");
    profilePicture->setPixmap(QPixmap(":/img/contact_dark.svg"));
    profilePicture->setContextMenuPolicy(Qt::CustomContextMenu);
    profilePicture->setClickable(true);
    profilePicture->setObjectName("selfAvatar");
    profilePicture->installEventFilter(this);
    profilePicture->setAccessibleName("Profile avatar");
    profilePicture->setAccessibleDescription("Set a profile avatar shown to all contacts");
    profilePicture->setStyleSheet(style.getStylesheet("window/profile.qss", settings));
    connect(profilePicture, &MaskablePixmapWidget::clicked, this, &ProfileForm::onAvatarClicked);
    connect(profilePicture, &MaskablePixmapWidget::customContextMenuRequested, this,
            &ProfileForm::showProfilePictureContextMenu);

    auto* publicGrouplayout = qobject_cast<QHBoxLayout*>(bodyUI->publicGroup->layout());
    publicGrouplayout->insertWidget(0, profilePicture);
    publicGrouplayout->insertSpacing(1, 7);

    timer.setInterval(750);
    timer.setSingleShot(true);
    connect(&timer, &QTimer::timeout, this, [this] {
        bodyUI->toxIdLabel->setText(bodyUI->toxIdLabel->text().replace(" ✔", ""));
        hasCheck = false;
    });

    connect(bodyUI->toxIdLabel, &CroppingLabel::clicked, this, &ProfileForm::copyIdClicked);
    connect(toxId, &ClickableTE::clicked, this, &ProfileForm::copyIdClicked);
    profileInfo_->connectTo_idChanged(this, [this](const ToxId& id) { setToxId(id); });
    connect(bodyUI->userName, &QLineEdit::editingFinished, this, &ProfileForm::onUserNameEdited);
    connect(bodyUI->statusMessage, &QLineEdit::editingFinished, this,
            &ProfileForm::onStatusMessageEdited);
    connect(bodyUI->renameButton, &QPushButton::clicked, this, &ProfileForm::onRenameClicked);
    connect(bodyUI->exportButton, &QPushButton::clicked, this, &ProfileForm::onExportClicked);
    connect(bodyUI->deleteButton, &QPushButton::clicked, this, &ProfileForm::onDeleteClicked);
    connect(bodyUI->logoutButton, &QPushButton::clicked, this, &ProfileForm::onLogoutClicked);
    connect(bodyUI->deletePassButton, &QPushButton::clicked, this, &ProfileForm::onDeletePassClicked);
    connect(bodyUI->changePassButton, &QPushButton::clicked, this, &ProfileForm::onChangePassClicked);
    connect(bodyUI->deletePassButton, &QPushButton::clicked, this,
            &ProfileForm::setPasswordButtonsText);
    connect(bodyUI->changePassButton, &QPushButton::clicked, this,
            &ProfileForm::setPasswordButtonsText);
    connect(bodyUI->saveQr, &QPushButton::clicked, this, &ProfileForm::onSaveQrClicked);
    connect(bodyUI->copyQr, &QPushButton::clicked, this, &ProfileForm::onCopyQrClicked);

    profileInfo_->connectTo_usernameChanged(this, [this](const QString& val) {
        bodyUI->userName->setText(val);
    });
    profileInfo_->connectTo_statusMessageChanged(this, [this](const QString& val) {
        bodyUI->statusMessage->setText(val);
    });

    for (QComboBox* cb : findChildren<QComboBox*>()) {
        cb->installEventFilter(this);
        cb->setFocusPolicy(Qt::StrongFocus);
    }

    retranslateUi();
    Translator::registerHandler([this] { retranslateUi(); }, this);
}

void ProfileForm::prFileLabelUpdate()
{
    const QString name = profileInfo->getProfileName();
    bodyUI->prFileLabel->setText(tr("Current profile: ") + name + ".tox");
}

ProfileForm::~ProfileForm()
{
    Translator::unregister(this);
    delete qr;
    delete bodyUI;
}

bool ProfileForm::isShown() const
{
    if (profilePicture->isVisible()) {
        window()->windowHandle()->alert(0);
        return true;
    }

    return false;
}

void ProfileForm::show(ContentLayout* contentLayout)
{
    contentLayout->mainContent->layout()->addWidget(this);
    QWidget::show();
    prFileLabelUpdate();
    const bool portable = settings.getMakeToxPortable();
    const QString defaultPath = QDir(settings.getPaths().getSettingsDirPath()).path().trimmed();
    const QString appPath = QApplication::applicationDirPath();
    const QString dirPath = portable ? appPath : defaultPath;

    const QString dirPrLink =
        tr("Current profile location: %1").arg(QString("<a href=\"file://%1\">%1</a>").arg(dirPath));

    bodyUI->dirPrLink->setText(dirPrLink);
    bodyUI->dirPrLink->setOpenExternalLinks(true);
    bodyUI->dirPrLink->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::TextSelectableByMouse);
    bodyUI->dirPrLink->setMaximumSize(bodyUI->dirPrLink->sizeHint());
    bodyUI->userName->setFocus();
    bodyUI->userName->selectAll();
}

bool ProfileForm::eventFilter(QObject* object, QEvent* event)
{
    if (object == static_cast<QObject*>(profilePicture) && event->type() == QEvent::MouseButtonPress) {
        auto* mouseEvent = static_cast<QMouseEvent*>(event);
        if (mouseEvent->button() == Qt::RightButton)
            return true;
    }
    return false;
}

void ProfileForm::showProfilePictureContextMenu(const QPoint& point)
{
    const QPoint pos = profilePicture->mapToGlobal(point);

    QMenu contextMenu;
    const QIcon icon = style()->standardIcon(QStyle::SP_DialogCancelButton);
    const QAction* removeAction = contextMenu.addAction(icon, tr("Remove"));
    const QAction* selectedItem = contextMenu.exec(pos);

    if (selectedItem == removeAction) {
        profileInfo->removeAvatar();
    }
}

void ProfileForm::copyIdClicked()
{
    profileInfo->copyId();
    if (!hasCheck) {
        bodyUI->toxIdLabel->setText(bodyUI->toxIdLabel->text() + " ✔");
        hasCheck = true;
    }

    timer.start();
}

void ProfileForm::onUserNameEdited()
{
    profileInfo->setUsername(bodyUI->userName->text());
}

void ProfileForm::onStatusMessageEdited()
{
    profileInfo->setStatusMessage(bodyUI->statusMessage->text());
}

void ProfileForm::onSelfAvatarLoaded(const QPixmap& pic)
{
    profilePicture->setPixmap(pic);
}

void ProfileForm::setToxId(const ToxId& id)
{
    const QString idString = id.toString();
    static const QString ToxIdColor = QStringLiteral("%1"
                                                     "<span style='color:blue'>%2</span>"
                                                     "<span style='color:gray'>%3</span>");
    toxId->setText(
        ToxIdColor.arg(idString.mid(0, 64)).arg(idString.mid(64, 8)).arg(idString.mid(72, 4)));

    delete qr;
    qr = new QRWidget();
    qr->setQRData("tox:" + idString);
    bodyUI->qrCode->setPixmap(QPixmap::fromImage(qr->getImage()->scaledToWidth(150)));
}

QString ProfileForm::getSupportedImageFilter()
{
    QString res;
    for (auto type : QImageReader::supportedImageFormats()) {
        res += QString("*.%1 ").arg(QString::fromUtf8(type));
    }

    return tr("Images (%1)", "filetype filter").arg(res.left(res.size() - 1));
}

void ProfileForm::onAvatarClicked()
{
    const QString filter = getSupportedImageFilter();
    const QString path = QFileDialog::getOpenFileName(Q_NULLPTR, tr("Choose a profile picture"),
                                                      QDir::homePath(), filter, nullptr);

    if (path.isEmpty()) {
        return;
    }
    const IProfileInfo::SetAvatarResult result = profileInfo->setAvatar(path);
    if (result == IProfileInfo::SetAvatarResult::OK) {
        return;
    }

    messageBoxManager.showError(tr("Error"), SET_AVATAR_ERROR[result]);
}

void ProfileForm::onRenameClicked()
{
    const QString cur = profileInfo->getProfileName();
    const QString title = tr("Rename \"%1\"", "renaming a profile").arg(cur);
    const QString name = QInputDialog::getText(this, title, title + ":");
    if (name.isEmpty()) {
        return;
    }

    const IProfileInfo::RenameResult result = profileInfo->renameProfile(name);
    if (result == IProfileInfo::RenameResult::OK) {
        return;
    }

    const QPair<QString, QString> error = RENAME_ERROR[result];
    messageBoxManager.showError(error.first, error.second.arg(name));
    prFileLabelUpdate();
}

void ProfileForm::onExportClicked()
{
    const QString current = profileInfo->getProfileName() + Core::TOX_EXT;
    const QString path = QFileDialog::getSaveFileName(Q_NULLPTR,
                                                      //: save dialog title
                                                      tr("Export profile"), current,
                                                      //: save dialog filter
                                                      tr("Tox save file (*.tox)"));
    if (path.isEmpty()) {
        return;
    }

    const IProfileInfo::SaveResult result = profileInfo->exportProfile(path);
    if (result == IProfileInfo::SaveResult::OK) {
        return;
    }

    const QPair<QString, QString> error = SAVE_ERROR[result];
    messageBoxManager.showWarning(error.first, error.second);
}

void ProfileForm::onDeleteClicked()
{
    const QString title = tr("Delete profile", "deletion confirmation title");
    const QString question =
        tr("Are you sure you want to delete this profile?", "deletion confirmation text");
    if (!messageBoxManager.askQuestion(title, question)) {
        return;
    }

    const QStringList manualDeleteFiles = profileInfo->removeProfile();
    if (manualDeleteFiles.empty()) {
        return;
    }

    //: deletion failed text part 1
    QString message = tr("The following files could not be deleted:") + "\n\n";
    for (const QString& file : manualDeleteFiles) {
        message += file + "\n";
    }

    //: deletion failed text part 2
    message += "\n" + tr("Please manually remove them.");

    messageBoxManager.showError(tr("Files could not be deleted!", "deletion failed title"), message);
}

void ProfileForm::onLogoutClicked()
{
    profileInfo->logout();
}

void ProfileForm::setPasswordButtonsText()
{
    if (profileInfo->isEncrypted()) {
        bodyUI->changePassButton->setText(tr("Change password", "button text"));
        bodyUI->deletePassButton->setVisible(true);
    } else {
        bodyUI->changePassButton->setText(tr("Set profile password", "button text"));
        bodyUI->deletePassButton->setVisible(false);
    }
}

void ProfileForm::onCopyQrClicked()
{
    profileInfo->copyQr(*qr->getImage());
}

void ProfileForm::onSaveQrClicked()
{
    const QString current = profileInfo->getProfileName() + ".png";
    const QString path =
        QFileDialog::getSaveFileName(Q_NULLPTR, tr("Save", "save qr image"), current,
                                     tr("Save QrCode (*.png)", "save dialog filter"));
    if (path.isEmpty()) {
        return;
    }

    const IProfileInfo::SaveResult result = profileInfo->saveQr(*qr->getImage(), path);
    if (result == IProfileInfo::SaveResult::OK) {
        return;
    }

    const QPair<QString, QString> error = SAVE_ERROR[result];
    messageBoxManager.showWarning(error.first, error.second);
}

void ProfileForm::onDeletePassClicked()
{
    if (!profileInfo->isEncrypted()) {
        messageBoxManager.showInfo(tr("Nothing to remove"),
                                   tr("Your profile does not have a password!"));
        return;
    }

    const QString title = tr("Remove password", "deletion confirmation title");
    //: deletion confirmation text
    const QString body = tr("Are you sure you want to remove your password?");
    if (!messageBoxManager.askQuestion(title, body)) {
        return;
    }

    if (!profileInfo->deletePassword()) {
        messageBoxManager.showInfo(CAN_NOT_CHANGE_PASSWORD.first, CAN_NOT_CHANGE_PASSWORD.second);
    }
}

void ProfileForm::onChangePassClicked()
{
    const QString title = tr("Please enter a new password.");
    auto* dialog = new SetPasswordDialog(title, QString{}, nullptr);
    if (dialog->exec() == QDialog::Rejected) {
        return;
    }

    const QString newPass = dialog->getPassword();
    if (!profileInfo->setPassword(newPass)) {
        messageBoxManager.showInfo(CAN_NOT_CHANGE_PASSWORD.first, CAN_NOT_CHANGE_PASSWORD.second);
    }
}

void ProfileForm::retranslateUi()
{
    bodyUI->retranslateUi(this);
    setPasswordButtonsText();
    // We have to add the toxId tooltip here and not in the .ui or Qt won't know how to translate it
    // dynamically
    toxId->setToolTip(
        tr("This ID allows other Tox users to add and contact you.\n"
           "Share it with your friends to begin chatting.\n\n"
           "This ID includes the NoSpam code (in blue), and the checksum (in gray)."));
}
