#include "./mainwindow.h"
#include "./fielddelegate.h"

#include "../model/entryfiltermodel.h"
#include "../model/entrymodel.h"
#include "../model/fieldmodel.h"

#include "ui_mainwindow.h"

#include "resources/config.h"

#include <passwordfile/io/cryptoexception.h>
#include <passwordfile/io/entry.h>

#include <passwordfile/util/openssl.h>

#include <qtutilities/aboutdialog/aboutdialog.h>
#include <qtutilities/enterpassworddialog/enterpassworddialog.h>
#include <qtutilities/misc/desktoputils.h>
#include <qtutilities/misc/dialogutils.h>
#include <qtutilities/misc/recentmenumanager.h>
#include <qtutilities/settingsdialog/optioncategorymodel.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <qtutilities/settingsdialog/settingsdialog.h>
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
#include <qtutilities/settingsdialog/optioncategory.h>
#endif

#include <c++utilities/chrono/datetime.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/path.h>

#include <QActionGroup>
#include <QClipboard>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QMimeData>
#include <QSettings>
#include <QTimerEvent>
#include <QUndoStack>
#include <QUndoView>

#include <cassert>
#include <functional>
#include <stdexcept>

using namespace std;
using namespace CppUtilities;
using namespace QtUtilities;
using namespace Io;

namespace QtGui {

/*!
 * \namespace QtGui
 * \brief Contains all GUI related classes and helper functions.
 */

/*!
 * \namespace QtGui::Ui
 * \brief Contains all classes generated by the Qt User Interface Compiler (uic).
 */

/*!
 * \class MainWindow
 * \brief The MainWindow class provides the main window of the widgets-based GUI of the application.
 */

/*!
 * \brief Copies the selected cells to the clipboard.
 */
void MainWindow::copyFields()
{
    copyFieldsForXMilliSeconds(-1);
}

/*!
 * \brief Computes the TOTP for the selected fields and sets it as clipboard text.
 */
void MainWindow::copyFieldsTOTP()
{
    const auto text = selectedFieldsString();
    if (text.startsWith(QLatin1String("otpauth:"))) {
        copyTOTP(text.toStdString());
    } else {
        QMessageBox::warning(this, QApplication::applicationName(), tr("The selection does not contain an \"otpauth:\" URL."));
    }
}

/*!
 * \brief Computes the TOTP for the specified \a url and sets it as clipboard text.
 */
void MainWindow::copyTOTP(std::string url)
{
    try {
        const auto totp = Util::OpenSsl::computeTOTP(url, DateTime::gmtNow());
        QGuiApplication::clipboard()->setText(QString::fromStdString(totp.digits));
        statusBar()->showMessage(
            tr("Copied TOTP, valid for %1").arg(QString::fromStdString(totp.remaining.toString(TimeSpanOutputFormat::WithMeasures))),
            static_cast<int>(totp.remaining.totalMilliseconds()));
    } catch (const std::runtime_error &e) {
        QMessageBox::warning(this, QCoreApplication::applicationName(), tr("Unable to compute TOTP: %1").arg(QString::fromUtf8(e.what())));
    }
}

/*!
 * \brief Inserts fields from the clipboard.
 */
void MainWindow::insertFieldsFromClipboard()
{
    insertFields(QApplication::clipboard()->text());
}

/*!
 * \brief Clears the clipboard.
 */
void MainWindow::clearClipboard()
{
    QApplication::clipboard()->clear();
}

/*!
 * \brief Flags the current file as being changed since the last save.
 */
void MainWindow::setSomethingChanged()
{
    setSomethingChanged(true);
}

/*!
 * \brief Sets whether the current file has been changed since the last save.
 */
void MainWindow::setSomethingChanged(bool somethingChanged)
{
    if (m_somethingChanged != somethingChanged) {
        m_somethingChanged = somethingChanged;
        updateWindowTitle();
    }
}

/*!
 * \brief Updates the style sheet.
 */
void MainWindow::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
    const auto p = palette();
    setStyleSheet(QStringLiteral("%1 #splitter QWidget { background-color: palette(base); color: palette(text); } #splitter QWidget *, #splitter "
                                 "QWidget * { background-color: none; } #leftWidget { border-right: 1px solid %2; }")
            .arg(dialogStyleForPalette(p), windowFrameColorForPalette(p).name()));
#endif
}

/*!
 * \brief Updates the columns sizing according to the checked action/option.
 */
void MainWindow::updateColumnSizing()
{
    auto *const header = m_ui->tableView->horizontalHeader();
    if (m_ui->actionColumnWidthCustom->isChecked()) {
        header->setStretchLastSection(true);
        header->setCascadingSectionResizes(true);
        header->setSectionResizeMode(QHeaderView::Interactive);
    } else {
        header->setSectionResizeMode(QHeaderView::Stretch);
    }
}

/*!
 * \brief Constructs a new main window.
 */
MainWindow::MainWindow(QSettings &settings, QtUtilities::QtSettings *qtSettings, QWidget *parent)
    : QMainWindow(parent)
    , m_ui(new Ui::MainWindow)
    , m_openFlags(PasswordFileOpenFlags::None)
    , m_clearClipboardTimer(0)
    , m_aboutDlg(nullptr)
    , m_settings(settings)
    , m_qtSettings(qtSettings)
    , m_settingsDlg(nullptr)
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
    , m_updateSettingsDlg(nullptr)
#endif
{
    // setup ui
    updateStyleSheet();
    m_ui->setupUi(this);

    // set default values
    setSomethingChanged(false);
    m_dontUpdateSelection = false;
    updateUiStatus();

    // load settings
    settings.beginGroup(QStringLiteral("mainwindow"));

    // init recent menu manager
    m_recentMgr = new RecentMenuManager(m_ui->menuRecent, this);
    m_recentMgr->restore(settings.value(QStringLiteral("recententries"), QStringList()).toStringList());
    connect(m_recentMgr, &RecentMenuManager::fileSelected, this, static_cast<bool (MainWindow::*)(const QString &)>(&MainWindow::openFile));

    // set position and size
    restoreGeometry(settings.value(QStringLiteral("geometry")).toByteArray());
    restoreState(settings.value(QStringLiteral("state")).toByteArray());

    // setup undo stack and related actions
    m_undoStack = new QUndoStack(this);
    m_undoView = nullptr;
    m_ui->actionUndo->setShortcuts(QKeySequence::Undo);
    m_ui->actionRedo->setShortcuts(QKeySequence::Redo);

    // setup models, tree and table view
    m_ui->treeView->setModel(m_entryFilterModel = new EntryFilterModel(this));
    m_ui->tableView->setModel(m_fieldModel = new FieldModel(m_undoStack, this));
    m_ui->tableView->setItemDelegate(new FieldDelegate(this));
    m_entryFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_entryFilterModel->setSourceModel(m_entryModel = new EntryModel(m_undoStack, this));
#ifdef Q_OS_WIN32
    m_ui->treeView->setFrameShape(QFrame::NoFrame);
    m_ui->tableView->setFrameShape(QFrame::NoFrame);
#else
    m_ui->treeView->setFrameShape(QFrame::StyledPanel);
    m_ui->tableView->setFrameShape(QFrame::StyledPanel);
#endif

    // setup column sizing
    auto *const columnSizingGroup = new QActionGroup(this);
    columnSizingGroup->addAction(m_ui->actionColumnWidthAuto);
    columnSizingGroup->addAction(m_ui->actionColumnWidthCustom);
    if (settings.value(QStringLiteral("interactivecolumns")).toBool()) {
        m_ui->actionColumnWidthCustom->setChecked(true);
    } else {
        m_ui->actionColumnWidthAuto->setChecked(true);
    }
    updateColumnSizing();

    // splitter sizes
    m_ui->splitter->setSizes(QList<int>() << 100 << 800);

    // password visibility group
    auto *const passwordVisibilityGroup = new QActionGroup(this);
    passwordVisibilityGroup->addAction(m_ui->actionShowAlways);
    passwordVisibilityGroup->addAction(m_ui->actionShowOnlyWhenEditing);
    passwordVisibilityGroup->addAction(m_ui->actionHideAlways);
    const QString pwVisibility(settings.value(QStringLiteral("pwvisibility")).toString());
    QAction *pwVisibilityAction;
    if (pwVisibility == QStringLiteral("always")) {
        pwVisibilityAction = m_ui->actionShowAlways;
    } else if (pwVisibility == QStringLiteral("hidden")) {
        pwVisibilityAction = m_ui->actionHideAlways;
    } else {
        pwVisibilityAction = m_ui->actionShowOnlyWhenEditing;
    }
    pwVisibilityAction->setChecked(true);
    setPasswordVisibility(pwVisibilityAction);

    // add action for updater if setup tools are enabled
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
    auto *const updaterAction = m_ui->menu->addAction(QIcon::fromTheme(QStringLiteral("install")), tr("Check for updates"));
#endif

    // connect signals and slots
    // -> file related actions
    connect(m_ui->actionSave, &QAction::triggered, this, &MainWindow::saveFile);
    connect(m_ui->actionExport, &QAction::triggered, this, &MainWindow::exportFile);
    connect(m_ui->actionDetails, &QAction::triggered, this, &MainWindow::showFileDetails);
    connect(m_ui->actionShowContainingDirectory, &QAction::triggered, this, &MainWindow::showContainingDirectory);
    connect(m_ui->actionClose, &QAction::triggered, this, &MainWindow::closeFile);
    connect(m_ui->actionCreate, &QAction::triggered, this, static_cast<bool (MainWindow::*)(void)>(&MainWindow::createFile));
    connect(m_ui->actionQuit, &QAction::triggered, this, &MainWindow::close);
    connect(m_ui->actionChangepassword, &QAction::triggered, this, &MainWindow::changePassword);
    // -> showing dialogs
    connect(m_ui->actionPasswordGenerator, &QAction::triggered, this, &MainWindow::showPassowrdGeneratorDialog);
    connect(m_ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutDialog);
    connect(m_ui->actionOpen, &QAction::triggered, this, &MainWindow::showOpenFileDialog);
    connect(m_ui->actionSaveAs, &QAction::triggered, this, &MainWindow::showSaveFileDialog);
    connect(m_ui->actionQtSettings, &QAction::triggered, this, &MainWindow::showSettingsDialog);
#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
    connect(updaterAction, &QAction::triggered, this, &MainWindow::showUpdaterDialog);
#endif
    // -> add/remove account
    connect(m_ui->actionAddAccount, &QAction::triggered, this, &MainWindow::addAccount);
    connect(m_ui->actionAddCategory, &QAction::triggered, this, &MainWindow::addCategory);
    connect(m_ui->actionRemoveAccount, &QAction::triggered, this, &MainWindow::removeEntry);
    // -> insert/remove fields
    connect(m_ui->actionInsertRow, &QAction::triggered, this, &MainWindow::insertRow);
    connect(m_ui->actionRemoveRows, &QAction::triggered, this, &MainWindow::removeRows);
    connect(m_ui->actionCopyFields, &QAction::triggered, this, &MainWindow::copyFields);
    connect(m_ui->actionCopyTOTP, &QAction::triggered, this, &MainWindow::copyFieldsTOTP);
    connect(m_ui->actionPasteFields, &QAction::triggered, this, &MainWindow::insertFieldsFromClipboard);
    // -> undo/redo
    connect(m_ui->actionUndo, &QAction::triggered, m_undoStack, &QUndoStack::undo);
    connect(m_ui->actionRedo, &QAction::triggered, m_undoStack, &QUndoStack::redo);
    connect(m_undoStack, &QUndoStack::canUndoChanged, m_ui->actionUndo, &QAction::setEnabled);
    connect(m_undoStack, &QUndoStack::canRedoChanged, m_ui->actionRedo, &QAction::setEnabled);
    // -> view
    connect(passwordVisibilityGroup, &QActionGroup::triggered, this, &MainWindow::setPasswordVisibility);
    connect(columnSizingGroup, &QActionGroup::triggered, this, &MainWindow::updateColumnSizing);
    connect(m_ui->actionShowUndoStack, &QAction::triggered, this, &MainWindow::showUndoView);
    // -> models
    connect(m_ui->treeView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::accountSelected);
    connect(m_entryModel, &QAbstractItemModel::dataChanged, this, static_cast<void (MainWindow::*)(void)>(&MainWindow::setSomethingChanged));
    connect(m_fieldModel, &QAbstractItemModel::dataChanged, this, static_cast<void (MainWindow::*)(void)>(&MainWindow::setSomethingChanged));
    // -> context menus
    connect(m_ui->treeView, &QTableView::customContextMenuRequested, this, &MainWindow::showTreeViewContextMenu);
    connect(m_ui->tableView, &QTableView::customContextMenuRequested, this, &MainWindow::showTableViewContextMenu);
    // -> filter
    connect(m_ui->accountFilterLineEdit, &QLineEdit::textChanged, this, &MainWindow::applyFilter);

    // setup other controls
    m_ui->actionAlwaysCreateBackup->setChecked(settings.value(QStringLiteral("alwayscreatebackup"), true).toBool());
    m_ui->accountFilterLineEdit->setText(settings.value(QStringLiteral("accountfilter"), QString()).toString());
    m_ui->centralWidget->installEventFilter(this);
    settings.endGroup();
}

/*!
 * \brief Destroys the main window.
 */
MainWindow::~MainWindow()
{
}

bool MainWindow::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::PaletteChange:
        if (m_qtSettings) {
            m_qtSettings->reevaluatePaletteAndDefaultIconTheme();
        }
        updateStyleSheet();
        break;
    default:;
    }
    return QMainWindow::event(event);
}

bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == m_undoView) {
        switch (event->type()) {
        case QEvent::Hide:
            m_ui->actionShowUndoStack->setChecked(false);
            break;
        default:;
        }
    } else if (obj == m_ui->centralWidget) {
        switch (event->type()) {
        case QEvent::DragEnter:
        case QEvent::Drop:
            if (const QDropEvent *const dropEvent = static_cast<const QDropEvent *>(event)) {
                QString data;
                const QMimeData *mimeData = dropEvent->mimeData();
                if (mimeData->hasUrls()) {
                    const auto urls = mimeData->urls();
                    const auto &url = urls.front();
                    if (url.scheme() == QLatin1String("file")) {
                        data = url.path();
                    }
                } else if (mimeData->hasText()) {
                    data = mimeData->text();
                }
                if (!data.isEmpty()) {
                    event->accept();
                    if (event->type() == QEvent::Drop) {
                        openFile(data);
                    }
                }
                return true;
            }
            [[fallthrough]];
        default:;
        }
    }
    return QMainWindow::eventFilter(obj, event);
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    // ask if file is opened
    if (m_file.hasRootEntry() && !closeFile()) {
        event->ignore();
        return;
    }

    // close undow view
    if (m_undoView) {
        m_undoView->close();
    }

    // save settings
    m_settings.beginGroup(QStringLiteral("mainwindow"));
    m_settings.setValue(QStringLiteral("geometry"), QVariant(saveGeometry()));
    m_settings.setValue(QStringLiteral("state"), QVariant(saveState()));
    m_settings.setValue(QStringLiteral("recententries"), QVariant(m_recentMgr->save()));
    m_settings.setValue(QStringLiteral("accountfilter"), QVariant(m_ui->accountFilterLineEdit->text()));
    m_settings.setValue(QStringLiteral("alwayscreatebackup"), m_ui->actionAlwaysCreateBackup->isChecked());
    QString pwVisibility;
    if (m_ui->actionShowAlways->isChecked()) {
        pwVisibility = QStringLiteral("always");
    } else if (m_ui->actionHideAlways->isChecked()) {
        pwVisibility = QStringLiteral("hidden");
    } else {
        pwVisibility = QStringLiteral("editing");
    }
    m_settings.setValue(QStringLiteral("pwvisibility"), QVariant(pwVisibility));
    m_settings.setValue(QStringLiteral("interactivecolumns"), m_ui->actionColumnWidthCustom->isChecked());
    m_settings.endGroup();
    if (m_qtSettings) {
        m_qtSettings->save(m_settings);
    }
}

void MainWindow::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == m_clearClipboardTimer) {
        clearClipboard();
        m_clearClipboardTimer = 0;
    }
}

/*!
 * \brief Shows the settings dialog (which currently only consists of the Qt settings category).
 */
void MainWindow::showSettingsDialog()
{
    if (!m_settingsDlg) {
        m_settingsDlg = new SettingsDialog(this);
        if (m_qtSettings) {
            m_settingsDlg->setWindowTitle(tr("Qt settings"));
            m_settingsDlg->setSingleCategory(m_qtSettings->category());
            connect(m_settingsDlg, &SettingsDialog::applied, this, [this] { m_qtSettings->apply(); });
        }
    }
    if (m_settingsDlg->isHidden()) {
        m_settingsDlg->showNormal();
    } else {
        m_settingsDlg->activateWindow();
    }
}

/*!
 * \brief Shows the about dialog.
 */
void MainWindow::showAboutDialog()
{
    if (!m_aboutDlg) {
        m_aboutDlg = new AboutDialog(this, QStringLiteral(APP_URL), tr("A simple password store using AES-256-CBC encryption via OpenSSL."),
            QImage(":/icons/hicolor/128x128/apps/passwordmanager.png"));
    }
    m_aboutDlg->show();
}

#ifdef PASSWORD_MANAGER_SETUP_TOOLS_ENABLED
/*!
 * \brief Shows the updater dialog.
 */
void MainWindow::showUpdaterDialog()
{
    if (!m_updateSettingsDlg) {
        m_updateSettingsDlg = new UpdateDialog(this);
        m_updateSettingsDlg->page()->setRestartHandler(m_restartHandler.requester());
    }
    if (m_updateSettingsDlg->isHidden()) {
        m_updateSettingsDlg->showNormal();
    } else {
        m_updateSettingsDlg->activateWindow();
    }
}
#endif

/*!
 * \brief Shows the password generator dialog.
 */
void MainWindow::showPassowrdGeneratorDialog()
{
    PasswordGeneratorDialog *const pwgDialog = new PasswordGeneratorDialog(this);
    pwgDialog->show();
}

/*!
 * \brief Shows the open file dialog and opens the selected file.
 */
void MainWindow::showOpenFileDialog()
{
    if (m_file.hasRootEntry() && !closeFile()) {
        return;
    }
    const QString fileName
        = QFileDialog::getOpenFileName(this, tr("Select a password list"), QString(), tr("Password Manager files (*.pwmgr);;All files (*)"));
    if (!fileName.isEmpty()) {
        openFile(fileName);
    }
}

/*!
 * \brief Shows the save file dialog and saves the file at the selected location.
 */
void MainWindow::showSaveFileDialog()
{
    if (askForCreatingFile()) {
        saveFile();
    }
}

/*!
 * \brief Shows the undo view.
 */
void MainWindow::showUndoView()
{
    if (m_ui->actionShowUndoStack->isChecked()) {
        if (!m_undoView) {
            m_undoView = new QUndoView(m_undoStack);
            m_undoView->setWindowTitle(tr("Undo stack"));
            m_undoView->setWindowFlags(Qt::Tool);
            m_undoView->setAttribute(Qt::WA_QuitOnClose);
            m_undoView->setWindowIcon(QIcon::fromTheme(QStringLiteral("edit-undo")));
            m_undoView->installEventFilter(this);
        }
        m_undoView->show();
    } else if (m_undoView) {
        m_undoView->hide();
    }
}

/*!
 * \brief Opens a file with the specified \a path and updates all widgets to show its contents.
 * \returns Returns true on success; otherwise false
 */
bool MainWindow::openFile(const QString &path, PasswordFileOpenFlags openFlags)
{
    // close previous file
    if (m_file.hasRootEntry() && !closeFile()) {
        return false;
    }

    // set path and open file
    m_file.setPath(path.toStdString());
    try {
        m_file.open(m_openFlags = openFlags);
    } catch (const std::ios_base::failure &failure) {
        // try read-only
        if (!(m_openFlags & PasswordFileOpenFlags::ReadOnly)) {
            return openFile(path, m_openFlags | PasswordFileOpenFlags::ReadOnly);
        }

        // show error message
        const QString errmsg
            = tr("An IO error occurred when opening the specified file \"%1\": %2").arg(path, QString::fromLocal8Bit(failure.what()));
        m_ui->statusBar->showMessage(errmsg, 5000);
        QMessageBox::critical(this, QApplication::applicationName(), errmsg);
        return false;
    }

    // warn before loading a very big file
    if (m_file.size() > 10485760
        && QMessageBox::warning(this, QApplication::applicationName(),
               tr("The file you want to load seems to be very big. Do you really want to open it?"), QMessageBox::Yes, QMessageBox::No)
            == QMessageBox::No) {
        m_file.clear();
        return false;
    }

    // ask for a password if required
    if (m_file.isEncryptionUsed()) {
        EnterPasswordDialog pwDlg(this);
        pwDlg.setWindowTitle(tr("Opening file") + QStringLiteral(" - " APP_NAME));
        pwDlg.setInstruction(tr("Enter the password to open the file \"%1\"").arg(path));
        pwDlg.setPasswordRequired(true);
        switch (pwDlg.exec()) {
        case QDialog::Accepted:
            if (pwDlg.password().isEmpty()) {
                m_ui->statusBar->showMessage(tr("A password is needed to open the file."), 5000);
                QMessageBox::warning(this, QApplication::applicationName(), tr("A password is needed to open the file."));
                m_file.clear();
                return false;
            } else {
                break;
            }
        case QDialog::Rejected:
            m_file.clear();
            return false;
        default:;
        }
        m_file.setPassword(pwDlg.password().toStdString());
    }

    // load the contents of the file
    QString msg;
    try {
        m_file.load();
    } catch (const CryptoException &e) {
        msg = tr("The file couldn't be decrypted.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(e.what()));
    } catch (const std::ios_base::failure &failure) {
        try {
            msg = QString::fromLocal8Bit(failure.what());
        } catch (const runtime_error &e) {
            msg = tr("Unable to parse the file. %1").arg(QString::fromLocal8Bit(e.what()));
        }
    }

    // show warning if a file is encrypted but no auth tag is present
    if ((m_file.saveOptions() & PasswordFileSaveFlags::Encryption) && !(m_file.saveOptions() & PasswordFileSaveFlags::AuthenticationTag)) {
        QMessageBox::warning(this, windowTitle(), tr("The encrypted file could be opened but its authenticity could not be verified because it was written with an old version of the password manager that did not include an authentication tag."));
    }

    // show contents
    if (msg.isEmpty()) {
        return showFile();
    }

    // clear file and show a message in the error case
    m_file.clear();
    m_ui->statusBar->showMessage(msg, 5000);
    if (QMessageBox::critical(this, QApplication::applicationName(), msg, QMessageBox::Cancel, QMessageBox::Retry) == QMessageBox::Retry) {
        return openFile(path, openFlags); // retry
    } else {
        return false;
    }
}

/*!
 * \brief Creates a new file.
 * \returns Returns true on success; otherwise false
 */
bool MainWindow::createFile()
{
    // close previous file
    if (m_file.hasRootEntry() && !closeFile()) {
        return false;
    }
    m_file.generateRootEntry();
    return showFile();
}

/*!
 * \brief Creates a new file with the specified \a path.
 * \returns Returns true on success; otherwise false
 */
void MainWindow::createFile(const QString &path)
{
    createFile(path, QString());
}

/*!
 * \brief Creates a new file with the specified \a path and \a password.
 * \returns Returns true on success; otherwise false
 */
void MainWindow::createFile(const QString &path, const QString &password)
{
    // close previous file
    if (m_file.hasRootEntry() && !closeFile()) {
        return;
    }

    // set path and password
    m_file.setPath(path.toStdString());
    m_file.setPassword(password.toStdString());

    // create the file and show it
    try {
        m_openFlags = PasswordFileOpenFlags::Default;
        m_file.create();
    } catch (const std::ios_base::failure &failure) {
        QMessageBox::critical(this, QApplication::applicationName(),
            tr("The file <i>%1</i> couldn't be created: %2").arg(path, QString::fromLocal8Bit(failure.what())));
        return;
    }
    m_file.generateRootEntry();
    showFile();
}

/*!
 * \brief Shows the previously opened file. Called within openFile() and createFile().
 * \returns Returns true on success; otherwise false
 */
bool MainWindow::showFile()
{
    m_fieldModel->reset();
    m_entryModel->setRootEntry(m_file.rootEntry());
    applyDefaultExpanding(QModelIndex());
    if (m_file.path().empty()) {
        m_ui->statusBar->showMessage(tr("A new password list has been created."), 5000);
    } else {
        m_recentMgr->addEntry(QString::fromStdString(m_file.path()));
        m_ui->statusBar->showMessage(tr("The password list has been load."), 5000);
    }
    updateWindowTitle();
    updateUiStatus();
    applyFilter(m_ui->accountFilterLineEdit->text());
    setSomethingChanged(false);
    return true;
}

/*!
 * \brief Updates the status of the UI elements.
 */
void MainWindow::updateUiStatus()
{
    const bool fileOpened = m_file.hasRootEntry();
    m_ui->actionCreate->setEnabled(true);
    m_ui->actionOpen->setEnabled(true);
    m_ui->actionSave->setEnabled(fileOpened);
    m_ui->actionSaveAs->setEnabled(fileOpened);
    m_ui->actionExport->setEnabled(fileOpened);
    m_ui->actionDetails->setEnabled(fileOpened);
    m_ui->actionShowContainingDirectory->setEnabled(fileOpened);
    m_ui->actionClose->setEnabled(fileOpened);
    m_ui->actionChangepassword->setEnabled(fileOpened);
    m_ui->menuEdit->setEnabled(fileOpened);
    m_ui->accountFilterLineEdit->setEnabled(true);
}

/*!
 * \brief Updates the window title.
 */
void MainWindow::updateWindowTitle()
{
    DocumentStatus docStatus;
    if (m_file.hasRootEntry()) {
        if (m_somethingChanged) {
            docStatus = DocumentStatus::Unsaved;
        } else {
            docStatus = DocumentStatus::Saved;
        }
    } else {
        docStatus = DocumentStatus::NoDocument;
    }
    auto documentPath(QString::fromStdString(m_file.path()));
    if (m_openFlags & PasswordFileOpenFlags::ReadOnly) {
        documentPath += tr(" [read-only]");
    }
    setWindowTitle(generateWindowTitle(docStatus, documentPath));
}

void MainWindow::applyDefaultExpanding(const QModelIndex &parent)
{
    for (int row = 0, rows = m_entryFilterModel->rowCount(parent); row < rows; ++row) {
        const QModelIndex index = m_entryFilterModel->index(row, 0, parent);
        if (!index.isValid()) {
            return;
        }
        applyDefaultExpanding(index);
        m_ui->treeView->setExpanded(index, m_entryFilterModel->data(index, DefaultExpandedRole).toBool());
    }
}

/*!
 * \brief Returns the save options (to be) used when saving the file next time.
 */
PasswordFileSaveFlags MainWindow::saveOptions() const
{
    auto options = PasswordFileSaveFlags::Compression;
    if (!m_file.password().empty()) {
        options |= PasswordFileSaveFlags::Encryption | PasswordFileSaveFlags::PasswordHashing | PasswordFileSaveFlags::AuthenticationTag;
    }
    return options;
}

/*!
 * \brief Returns a string with the values of all selected fields.
 * \remarks Columns are sparated with \\t, rows with \\n.
 */
QString MainWindow::selectedFieldsString() const
{
    const QModelIndexList selectedIndexes(m_ui->tableView->selectionModel()->selectedIndexes());
    if (selectedIndexes.isEmpty()) {
        return QString();
    }
    if (selectedIndexes.size() == 1) {
        return selectedIndexes.front().data(Qt::EditRole).toString();
    }
    QString text;
    int maxRow = m_fieldModel->rowCount() - 1;
    int firstRow = maxRow, lastRow = 0;
    int maxCol = m_fieldModel->columnCount() - 1;
    int firstCol = maxCol, lastCol = 0;
    for (const QModelIndex &index : selectedIndexes) {
        if (index.row() < firstRow) {
            firstRow = index.row();
        }
        if (index.row() > lastRow) {
            lastRow = index.row();
        }
        if (index.column() < firstCol) {
            firstCol = index.column();
        }
        if (index.column() > lastCol) {
            lastCol = index.column();
        }
    }
    for (int row = firstRow; row <= lastRow; ++row) {
        for (int col = firstCol; col <= lastCol; ++col) {
            const QModelIndex index(m_fieldModel->index(row, col));
            if (selectedIndexes.contains(index)) {
                text.append(index.data(Qt::EditRole).toString());
            }
            text.append('\t');
        }
        text.append('\n');
    }
    return text;
}

/*!
 * \brief Inserts fields from the specified \a fieldsString.
 */
void MainWindow::insertFields(const QString &fieldsString)
{
    const auto selectedIndexes(m_ui->tableView->selectionModel()->selectedIndexes());
    if (selectedIndexes.size() != 1) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("Exactly one field needs to be selected (top-left corner for insertion)."));
        return;
    }

    const auto cols = m_fieldModel->columnCount();
    const auto initCol = selectedIndexes.front().column();
    const auto rowValues = [&] {
        QStringList lines = fieldsString.split('\n');
        if (lines.back().isEmpty()) {
            lines.pop_back();
        }
        return lines;
    }();

    auto row = selectedIndexes.front().row();
    m_fieldModel->insertRows(row, static_cast<int>(rowValues.size()), QModelIndex());

    for (const auto &rowValue : rowValues) {
        int col = initCol;
        for (const auto &cellValue : rowValue.split('\t')) {
            if (col < cols) {
                m_fieldModel->setData(m_fieldModel->index(row, col), cellValue, Qt::EditRole);
                ++col;
            } else {
                break;
            }
        }
        ++row;
    }
}

/*!
 * \brief Asks the user to create a new file.
 */
bool MainWindow::askForCreatingFile()
{
    if (showNoFileOpened()) {
        return false;
    }

    const QString fileName = QFileDialog::getSaveFileName(
        this, tr("Select where you want to save the password list"), QString(), tr("Password Manager files (*.pwmgr);;All files (*)"));
    if (fileName.isEmpty()) {
        m_ui->statusBar->showMessage(tr("The file was not be saved."), 7000);
        return false;
    } else {
        m_file.setPath(fileName.toStdString());
        try {
            m_file.create();
            updateWindowTitle();
        } catch (const std::ios_base::failure &failure) {
            QMessageBox::critical(this, QApplication::applicationName(), QString::fromLocal8Bit(failure.what()));
            return false;
        }
    }
    return true;
}

/*!
 * \brief Shows an warning if no file is opened.
 * \returns Returns whether the warning has been shown.
 */
bool MainWindow::showNoFileOpened()
{
    if (!m_file.hasRootEntry()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("There is no password list opened."));
        return true;
    }
    return false;
}

/*!
 * \brief Shows an warning if no account is selected.
 * \returns Returns whether the warning has been shown.
 */
bool MainWindow::showNoAccount()
{
    if (!m_fieldModel->fields()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("There's no account selected."));
        return true;
    }
    return false;
}

/*!
 * \brief Closes the currently opened file. Asks the user to save changes if the file has been modified.
 * \returns Returns whether the file has been closed.
 */
bool MainWindow::closeFile()
{
    if (showNoFileOpened()) {
        return false;
    }
    if (m_somethingChanged) {
        QMessageBox msg(this);
        msg.setText(tr("The password file has been modified."));
        msg.setInformativeText(tr("Do you want to save the changes before closing?"));
        msg.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
        msg.setDefaultButton(QMessageBox::Save);
        msg.setIcon(QMessageBox::Warning);
        switch (msg.exec()) {
        case QMessageBox::Save:
            if (saveFile()) {
                break;
            } else {
                return false;
            }
        case QMessageBox::Cancel:
            return false;
        default:;
        }
    }
    m_fieldModel->reset();
    m_entryModel->reset();
    m_file.clear();
    m_ui->statusBar->showMessage(tr("The password list has been closed."));
    updateWindowTitle();
    updateUiStatus();
    setSomethingChanged(false);
    return true;
}

/*!
 * \brief Saves the currently opened file.
 * \returns Returns whether the file could be saved.
 */
bool MainWindow::saveFile()
{
    if (showNoFileOpened()) {
        return false;
    }

    // create backup
    if (!m_file.path().empty() && QFile::exists(QString::fromStdString(m_file.path()))) {
        if (m_ui->actionAlwaysCreateBackup->isChecked()) {
            try {
                m_file.doBackup();
            } catch (const std::ios_base::failure &failure) {
                const QString message(tr("An IO error occurred when making the backup file: %1").arg(QString::fromLocal8Bit(failure.what())));
                QMessageBox::critical(this, QApplication::applicationName(), message);
                m_ui->statusBar->showMessage(message, 7000);
                return false;
            }
        }
    } else if (!askForCreatingFile()) {
        return false;
    }

    // ask for a password if none is set
    if (m_file.password().empty()) {
        EnterPasswordDialog pwDlg(this);
        pwDlg.setWindowTitle(tr("Saving file") + QStringLiteral(" - " APP_NAME));
        pwDlg.setInstruction(tr("Enter a password to save the file"));
        pwDlg.setVerificationRequired(true);

        switch (pwDlg.exec()) {
        case QDialog::Accepted:
            m_file.setPassword(pwDlg.password().toStdString());
            break;
        default:
            m_ui->statusBar->showMessage(tr("The file hasn't been saved."), 7000);
            return false;
        }
    }

    // save the file
    QString msg;
    try {
        m_file.save(saveOptions());
    } catch (const CryptoException &ex) {
        msg = tr("The password list couldn't be saved due to encryption failure.\nOpenSSL error queue: %1").arg(QString::fromLocal8Bit(ex.what()));
    } catch (const std::ios_base::failure &failure) {
        msg = QString::fromLocal8Bit(failure.what());
    }
    // show status
    if (!msg.isEmpty()) {
        m_ui->statusBar->showMessage(msg, 5000);
        QMessageBox::critical(this, QApplication::applicationName(), msg);
        return false;
    }
    if ((m_openFlags & PasswordFileOpenFlags::ReadOnly) || m_somethingChanged) {
        m_openFlags = PasswordFileOpenFlags::Default;
        m_somethingChanged = false;
        updateWindowTitle();
    }
    m_recentMgr->addEntry(QString::fromStdString(m_file.path()));
    m_ui->statusBar->showMessage(tr("The password list has been saved."), 5000);
    return true;
}

/*!
 * \brief Exports the files contents to a plain text file.
 */
void MainWindow::exportFile()
{
    if (showNoFileOpened()) {
        return;
    }
    const QString targetPath
        = QFileDialog::getSaveFileName(this, QApplication::applicationName(), QString(), tr("Plain text document (*.txt);;All files (*.*)"));
    if (targetPath.isEmpty()) {
        return;
    }
    QString errmsg;
    try {
        m_file.exportToTextfile(targetPath.toStdString());
    } catch (const std::ios_base::failure &failure) {
        errmsg = tr("An IO error occurred when exporting the password list: %1").arg(QString::fromLocal8Bit(failure.what()));
    }
    if (errmsg.isEmpty()) {
        m_ui->statusBar->showMessage(tr("The password list has been exported."), 5000);
    } else {
        m_ui->statusBar->showMessage(errmsg, 5000);
        QMessageBox::critical(this, QApplication::applicationName(), errmsg);
    }
}

/*!
 * \brief Shows the containing directory for the currently opened file.
 */
void MainWindow::showContainingDirectory()
{
    if (showNoFileOpened()) {
        return;
    }
    if (m_file.path().empty()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("The currently opened file hasn't been saved yet."));
        return;
    }
    const QFileInfo file(QString::fromStdString(m_file.path()));
    if (file.dir().exists()) {
        openLocalFileOrDir(file.dir().absolutePath());
    }
}

/*!
 * \brief Adds a new account entry to the selected category.
 */
void MainWindow::addAccount()
{
    addEntry(EntryType::Account);
}

/*!
 * \brief Adds a new category/node entry to the selected category.
 */
void MainWindow::addCategory()
{
    addEntry(EntryType::Node);
}

/*!
 * \brief Adds a new entry to the selected category.
 * \param type Specifies the type of the entry to be created.
 * \param title Specifies the title of the user prompt which will be shown to ask for the entry label.
 */
void MainWindow::addEntry(EntryType type)
{
    if (showNoFileOpened()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0);
    const QModelIndex selected = selectedIndexes.size() == 1 ? m_entryFilterModel->mapToSource(selectedIndexes.at(0)) : QModelIndex();
    if (!selected.isValid() || !m_entryModel->isNode(selected)) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("No node element selected."));
        return;
    }
    bool result;
    const QString text = QInputDialog::getText(this, type == EntryType::Account ? tr("Add account") : tr("Add category"), tr("Enter the entry name"),
        QLineEdit::Normal, tr("new entry"), &result);
    if (!result) {
        return;
    }
    if (text.isEmpty()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("You didn't enter text."));
        return;
    }
    int row = m_entryModel->rowCount(selected);
    m_entryModel->setInsertType(type);
    if (!m_entryModel->insertRow(row, selected)) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to create new entry."));
        return;
    }
    m_entryModel->setData(m_entryModel->index(row, 0, selected), QVariant(text), Qt::DisplayRole);
    setSomethingChanged(true);
}

/*!
 * \brief Removes the selected entry.
 */
void MainWindow::removeEntry()
{
    if (showNoFileOpened()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0);
    if (selectedIndexes.size() != 1) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("No entry selected."));
        return;
    }
    const QModelIndex selected = m_entryFilterModel->mapToSource(selectedIndexes.at(0));
    if (!m_entryModel->removeRow(selected.row(), selected.parent())) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to remove the entry."));
    }
}

/*!
 * \brief Applies the entered filter.
 * \remarks Called when the textChanged signal of m_ui->accountFilterLineEdit is emittet.
 */
void MainWindow::applyFilter(const QString &filterText)
{
    m_entryFilterModel->
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
        setFilterRegularExpression(QRegularExpression(filterText, QRegularExpression::CaseInsensitiveOption))
#else
        setFilterRegExp(filterText)
#endif
        ;
    if (filterText.isEmpty()) {
        applyDefaultExpanding(QModelIndex());
    } else {
        m_ui->treeView->expandAll();
    }
}

/*!
 * \brief Called when the user \a selected an entry.
 */
void MainWindow::accountSelected(const QModelIndex &selected, const QModelIndex &)
{
    if (Entry *entry = m_entryModel->entry(m_entryFilterModel->mapToSource(selected))) {
        if (entry->type() == EntryType::Account) {
            m_fieldModel->setAccountEntry(static_cast<AccountEntry *>(entry));
            m_ui->tableView->resizeRowsToContents();
            return;
        }
    }
    m_fieldModel->setAccountEntry(nullptr);
}

/*!
 * \brief Inserts an empty row before the selected one.
 */
void MainWindow::insertRow()
{
    if (showNoFileOpened() || showNoAccount()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes();
    if (selectedIndexes.empty()) {
        QMessageBox::warning(
            this, windowTitle(), tr("A field has to be selected since new fields are always inserted before the currently selected field."));
        return;
    }
    int row = m_fieldModel->rowCount();
    for (const QModelIndex &index : selectedIndexes) {
        if (index.row() < row) {
            row = index.row();
        }
    }
    if (row < m_fieldModel->rowCount() - 1) {
        m_fieldModel->insertRow(row);
    }
}

/*!
 * \brief Removes the selected rows.
 */
void MainWindow::removeRows()
{
    if (showNoFileOpened() || showNoAccount()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes();
    QList<int> rows;
    for (const QModelIndex &index : selectedIndexes) {
        rows << index.row();
    }
    if (rows.empty()) {
        QMessageBox::warning(this, windowTitle(), tr("No fields have been removed since there are currently no fields selected."));
        return;
    }
    for (int i = m_fieldModel->rowCount() - 1; i >= 0; --i) {
        if (rows.contains(i)) {
            m_fieldModel->removeRow(i);
        }
    }
}

/*!
 * \brief Marks the selected field as password field.
 */
void MainWindow::markAsPasswordField()
{
    setFieldType(FieldType::Password);
}

/*!
 * \brief Marks the selected field as normal field.
 */
void MainWindow::markAsNormalField()
{
    setFieldType(FieldType::Normal);
}

/*!
 * \brief Sets the type of the selected field to the specified \a fieldType.
 */
void MainWindow::setFieldType(FieldType fieldType)
{
    if (showNoFileOpened() || showNoAccount()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes();
    if (selectedIndexes.isEmpty()) {
        QMessageBox::warning(this, windowTitle(), tr("No fields have been changed since there are currently no fields selected."));
        return;
    }
    const QVariant typeVariant(static_cast<int>(fieldType));
    for (const QModelIndex &index : selectedIndexes) {
        m_fieldModel->setData(index, typeVariant, FieldTypeRole);
    }
}

/*!
 * \brief Sets the password visibility of m_fieldModel depending on which action has been chosen.
 *
 * This private slot is connected to the triggered signal of the passwordVisibility QActionGroup.
 */
void MainWindow::setPasswordVisibility(QAction *selectedAction)
{
    if (selectedAction == m_ui->actionShowAlways) {
        m_fieldModel->setPasswordVisibility(PasswordVisibility::Always);
    } else if (selectedAction == m_ui->actionShowOnlyWhenEditing) {
        m_fieldModel->setPasswordVisibility(PasswordVisibility::OnlyWhenEditing);
    } else if (selectedAction == m_ui->actionHideAlways) {
        m_fieldModel->setPasswordVisibility(PasswordVisibility::Never);
    }
}

/*!
 * \brief Asks the user to change the password which will be used when calling saveFile() next time.
 */
void MainWindow::changePassword()
{
    if (showNoFileOpened()) {
        return;
    }
    EnterPasswordDialog pwDlg(this);
    pwDlg.setWindowTitle(tr("Changing password") + QStringLiteral(" - " APP_NAME));
    pwDlg.setVerificationRequired(true);
    switch (pwDlg.exec()) {
    case QDialog::Accepted:
        if (pwDlg.password().isEmpty()) {
            m_file.clearPassword();
            setSomethingChanged(true);
            QMessageBox::warning(this, QApplication::applicationName(),
                tr("You didn't enter a password. <strong>No encryption</strong> will be used when saving the file next time."));
        } else {
            m_file.setPassword(pwDlg.password().toStdString());
            setSomethingChanged(true);
            QMessageBox::warning(this, QApplication::applicationName(), tr("The new password will be used next time you save the file."));
        }
        break;
    default:
        QMessageBox::warning(
            this, QApplication::applicationName(), tr("You aborted. The old password will still be used when saving the file next time."));
    }
}

/*!
 * \brief Shows the tree view context menu.
 */
void MainWindow::showTreeViewContextMenu(const QPoint &pos)
{
    if (!m_file.hasRootEntry()) {
        return;
    }
    const QModelIndexList selectedIndexes = m_ui->treeView->selectionModel()->selectedRows(0);
    if (selectedIndexes.size() != 1) {
        return;
    }
    QMenu contextMenu(this);
    const QModelIndex selected(m_entryFilterModel->mapToSource(selectedIndexes.at(0)));
    const Entry *const entry = m_entryModel->entry(selected);
    if (entry->type() == EntryType::Node) {
        contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add account"), this, &MainWindow::addAccount);
        contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Add category"), this, &MainWindow::addCategory);
    }
    contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-remove")), tr("Remove entry"), this, &MainWindow::removeEntry);
    if (entry->type() == EntryType::Node) {
        const auto *const nodeEntry = static_cast<const NodeEntry *>(entry);
        contextMenu.addSeparator();
        auto *const action = new QAction(&contextMenu);
        action->setCheckable(true);
        action->setText(tr("Expanded by default"));
        action->setChecked(nodeEntry->isExpandedByDefault());
        connect(action, &QAction::triggered,
            std::bind(&EntryModel::setData, m_entryModel, std::cref(selected), QVariant(!nodeEntry->isExpandedByDefault()), DefaultExpandedRole));
        contextMenu.addAction(action);
    }
    contextMenu.exec(m_ui->treeView->viewport()->mapToGlobal(pos));
}

/*!
 * \brief Shows the table view context menu.
 */
void MainWindow::showTableViewContextMenu(const QPoint &pos)
{
    // check whether there is a selection at all
    const QModelIndexList selectedIndexes = m_ui->tableView->selectionModel()->selectedIndexes();
    if (!m_file.hasRootEntry() || !m_fieldModel->fields() || selectedIndexes.isEmpty()) {
        return;
    }

    // check what kind of fields have been selected
    auto firstType = FieldType::Normal;
    bool allOfSameType = true;
    bool hasFirstFieldType = false;
    int row = selectedIndexes.front().row();
    int multipleRows = 1;
    QUrl serviceUrl;
    auto totpUrl = std::string_view();
    static const string protocols[] = { "http:", "https:", "file:" };
    for (const QModelIndex &index : selectedIndexes) {
        if (!index.isValid()) {
            continue;
        }
        if (const Field *field = m_fieldModel->field(static_cast<size_t>(index.row()))) {
            if (serviceUrl.isEmpty() && field->type() != FieldType::Password) {
                for (const string &protocol : protocols) {
                    if (startsWith(field->value(), protocol)) {
                        serviceUrl = QString::fromUtf8(field->value().data());
                        break;
                    }
                }
            }
            if (totpUrl.empty() && startsWith(field->value(), "otpauth:")) {
                totpUrl = field->value();
            }
            if (hasFirstFieldType) {
                if (firstType != field->type()) {
                    allOfSameType = false;
                    break;
                }
            } else {
                firstType = field->type();
                hasFirstFieldType = true;
            }
        }
        if (multipleRows == 1 && index.row() != row) {
            ++multipleRows;
        }
    }

    // create context menu
    QMenu contextMenu(this);
    // -> insertion and removal
    contextMenu.addAction(QIcon::fromTheme(QStringLiteral("list-add")), tr("Insert field"), this, &MainWindow::insertRow);
    contextMenu.addAction(
        QIcon::fromTheme(QStringLiteral("list-remove")), tr("Remove field(s)", nullptr, multipleRows), this, &MainWindow::removeRows);
    // -> show the "Mark as ..." action only when all selected indexes are of the same type
    if (hasFirstFieldType && allOfSameType) {
        switch (firstType) {
        case FieldType::Normal:
            contextMenu.addAction(
                QIcon::fromTheme(QStringLiteral("flag-black")), tr("Mark as password field"), this, &MainWindow::markAsPasswordField);
            break;
        case FieldType::Password:
            contextMenu.addAction(QIcon::fromTheme(QStringLiteral("flag-blue")), tr("Mark as normal field"), this, &MainWindow::markAsNormalField);
            break;
        }
    }
    // -> insert copy & paste
    contextMenu.addSeparator();
    contextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy"), this, &MainWindow::copyFields);
    contextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), tr("Copy for 5 seconds"), this, &MainWindow::copyFieldsForXMilliSeconds);
    if (multipleRows == 1 && !totpUrl.empty()) {
        auto *copyTotpAction = new QAction(QIcon::fromTheme(QStringLiteral("clock-symbolic")), tr("Copy TOTP"), &contextMenu);
        connect(copyTotpAction, &QAction::triggered, this, std::bind(&MainWindow::copyTOTP, this, std::string(totpUrl)));
        contextMenu.addAction(copyTotpAction);
    }
    const auto *const mimeData = QGuiApplication::clipboard()->mimeData();
    if (mimeData && mimeData->hasText()) {
        contextMenu.addAction(QIcon::fromTheme(QStringLiteral("edit-paste")), tr("Paste"), this, &MainWindow::insertFieldsFromClipboard);
    }
    // -> insert open URL
    if (multipleRows == 1 && !serviceUrl.isEmpty()) {
        auto *openUrlAction = new QAction(QIcon::fromTheme(QStringLiteral("applications-internet")), tr("Open URL"), &contextMenu);
        connect(openUrlAction, &QAction::triggered, bind(&QDesktopServices::openUrl, serviceUrl));
        contextMenu.addAction(openUrlAction);
    }

    contextMenu.exec(m_ui->tableView->viewport()->mapToGlobal(pos));
}

void MainWindow::showFileDetails()
{
    if (!m_file.isOpen()) {
        return;
    }
    QMessageBox msgBox;
    msgBox.setWindowTitle(tr("File details"));
    msgBox.setText(QString::fromStdString(m_file.summary(saveOptions())));
    msgBox.setIcon(QMessageBox::NoIcon);
    msgBox.setWindowIcon(windowIcon());
    msgBox.setWindowIconText(windowIconText());
    msgBox.exec();
}

/*!
 * \brief Copies the selected cells to the clipboard and clears the clipboard after \a x milli seconds again.
 */
void MainWindow::copyFieldsForXMilliSeconds(int x)
{
    const auto text = selectedFieldsString();
    if (text.isEmpty()) {
        QMessageBox::warning(this, QApplication::applicationName(), tr("The selection is empty."));
        return;
    }
    if (m_clearClipboardTimer) {
        killTimer(m_clearClipboardTimer);
    }
    QApplication::clipboard()->setText(text);
    if (x > 0) {
        m_clearClipboardTimer = startTimer(x, Qt::CoarseTimer);
        statusBar()->showMessage(tr("Copied values, clearing clipboard in %1 s").arg(x / 1000), x);
    } else {
        statusBar()->showMessage(tr("Copied values"), 10000);
    }
}
} // namespace QtGui
