/*
 * Copyright 2022-2023 Soren Stoutner <soren@stoutner.com>.
 *
 * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc>.
 *
 * Privacy Browser PC 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.
 *
 * Privacy Browser PC 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 Privacy Browser PC.  If not, see <http://www.gnu.org/licenses/>.
 */

// Application headers.
#include "TabWidget.h"
#include "Settings.h"
#include "ui_AddTabWidget.h"
#include "ui_TabWidget.h"
#include "databases/CookiesDatabase.h"
#include "dialogs/SaveDialog.h"
#include "filters/MouseEventFilter.h"
#include "helpers/SearchEngineHelper.h"
#include "windows/BrowserWindow.h"

// KDE Framework headers.
#include <KIO/FileCopyJob>
#include <KIO/JobUiDelegate>
#include <KNotification>

// Qt toolkit headers.
#include <QAction>
#include <QFileDialog>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QMessageBox>
#include <QPrintDialog>
#include <QPrintPreviewDialog>
#include <QPrinter>

// Initialize the public static variables.
QString TabWidget::webEngineDefaultUserAgent = QLatin1String("");

// Construct the class.
TabWidget::TabWidget(QWidget *parent) : QWidget(parent)
{
    // Create a QProcess to check if KDE is running.
    QProcess *checkIfRunningKdeQProcessPointer = new QProcess();

    // Create an argument string list that contains `ksmserver` (KDE Session Manager).
    QStringList argument = QStringList(QLatin1String("ksmserver"));

    // Run `pidof` to check for the presence of `ksmserver`.
    checkIfRunningKdeQProcessPointer->start(QLatin1String("pidof"), argument);

    // Monitor any standard output.
    connect(checkIfRunningKdeQProcessPointer, &QProcess::readyReadStandardOutput, [this]
    {
        // If there is any standard output, `ksmserver` is running.
        isRunningKde = true;
    });

    // Instantiate the user agent helper.
    userAgentHelperPointer = new UserAgentHelper();

    // Instantiate the UIs.
    Ui::TabWidget tabWidgetUi;
    Ui::AddTabWidget addTabWidgetUi;

    // Setup the main UI.
    tabWidgetUi.setupUi(this);

    // Get a handle for the tab widget.
    qTabWidgetPointer = tabWidgetUi.tabWidget;

    // Setup the add tab UI.
    addTabWidgetUi.setupUi(qTabWidgetPointer);

    // Get handles for the add tab widgets.
    QWidget *addTabWidgetPointer = addTabWidgetUi.addTabQWidget;
    QPushButton *addTabButtonPointer = addTabWidgetUi.addTabButton;

    // Display the add tab widget.
    qTabWidgetPointer->setCornerWidget(addTabWidgetPointer);

    // Create the loading favorite icon movie.
    loadingFavoriteIconMoviePointer = new QMovie();

    // Set the loading favorite icon movie file name.
    loadingFavoriteIconMoviePointer->setFileName(QStringLiteral(":/icons/loading.gif"));

    // Add the first tab.
    addFirstTab();

    // Process tab events.
    connect(qTabWidgetPointer, SIGNAL(currentChanged(int)), this, SLOT(updateUiWithTabSettings()));
    connect(addTabButtonPointer, SIGNAL(clicked()), this, SLOT(addTab()));
    connect(qTabWidgetPointer, SIGNAL(tabCloseRequested(int)), this, SLOT(deleteTab(int)));

    // Store a copy of the WebEngine default user agent.
    webEngineDefaultUserAgent = currentWebEngineProfilePointer->httpUserAgent();

    // Instantiate the mouse event filter pointer.
    MouseEventFilter *mouseEventFilterPointer = new MouseEventFilter();

    // Install the mouse event filter.
    qApp->installEventFilter(mouseEventFilterPointer);

    // Process mouse forward and back commands.
    connect(mouseEventFilterPointer, SIGNAL(mouseBack()), this, SLOT(mouseBack()));
    connect(mouseEventFilterPointer, SIGNAL(mouseForward()), this, SLOT(mouseForward()));
}

TabWidget::~TabWidget()
{
    // Manually delete each WebEngine page.
    for (int i = 0; i < qTabWidgetPointer->count(); ++i)
    {
        // Get the privacy WebEngine view.
        PrivacyWebEngineView *privacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(qTabWidgetPointer->widget(i));

        // Deletion the WebEngine page to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
        delete privacyWebEngineViewPointer->page();
    }
}

// The cookie is copied instead of referenced so that changes made to the cookie do not create a race condition with the display of the cookie in the dialog.
void TabWidget::addCookieToStore(QNetworkCookie cookie, QWebEngineCookieStore *webEngineCookieStorePointer) const
{
    // Create a URL.
    QUrl url;

    // Check to see if the domain does not start with a `.` because Qt makes this harder than it should be.  <https://doc.qt.io/qt-5/qwebenginecookiestore.html#setCookie>
    if (!cookie.domain().startsWith(QLatin1String(".")))
    {
        // Populate the URL.
        url.setHost(cookie.domain());
        url.setScheme(QLatin1String("https"));

        // Clear the domain from the cookie.
        cookie.setDomain(QLatin1String(""));
    }

    // Add the cookie to the store.
    if (webEngineCookieStorePointer == nullptr)
        currentWebEngineCookieStorePointer->setCookie(cookie, url);
    else
        webEngineCookieStorePointer->setCookie(cookie, url);
}

void TabWidget::addFirstTab()
{
    // Create the first tab.
    addTab();

    // Update the UI with the tab settings.
    updateUiWithTabSettings();

    // Set the focus on the current tab widget.  This prevents the tab bar from showing a blue bar under the label of the first tab.
    qTabWidgetPointer->currentWidget()->setFocus();
}

PrivacyWebEngineView* TabWidget::addTab(const bool removeUrlLineEditFocus, const bool backgroundTab)
{
    // Create a privacy WebEngine view.
    PrivacyWebEngineView *privacyWebEngineViewPointer = new PrivacyWebEngineView();

    // Add a new tab.
    int newTabIndex = qTabWidgetPointer->addTab(privacyWebEngineViewPointer, i18nc("New tab label.", "New Tab"));

    // Set the default tab icon.
    qTabWidgetPointer->setTabIcon(newTabIndex, defaultFavoriteIcon);

    // Get handles for the WebEngine page and profile.
    QWebEnginePage *webEnginePagePointer = privacyWebEngineViewPointer->page();
    QWebEngineProfile *webEngineProfilePointer = webEnginePagePointer->profile();

    // Get handles for the web engine elements.
    QWebEngineCookieStore *webEngineCookieStorePointer = webEngineProfilePointer->cookieStore();
    QWebEngineSettings *webEngineSettingsPointer = webEnginePagePointer->settings();

    // Update the URL line edit when the URL changes.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::urlChanged, [this, privacyWebEngineViewPointer] (const QUrl &newUrl)
    {
        // Only update the UI if this is the current tab.
        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
        {
            // Update the URL line edit.
            emit updateUrlLineEdit(newUrl);

            // Update the status of the forward and back buttons.
            emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
            emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
        }
    });

    // Update the title when it changes.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::titleChanged, [this, privacyWebEngineViewPointer] (const QString &title)
    {
        // Get the index for this tab.
        int tabIndex = qTabWidgetPointer->indexOf(privacyWebEngineViewPointer);

        // Update the title for this tab.
        qTabWidgetPointer->setTabText(tabIndex, title);

        // Update the window title if this is the current tab.
        if (tabIndex == qTabWidgetPointer->currentIndex())
            emit updateWindowTitle(title);
    });

    // Connect the loading favorite icon movie to the tab icon.
    connect(loadingFavoriteIconMoviePointer, &QMovie::frameChanged, [this, privacyWebEngineViewPointer]
    {
        // Get the index for this tab.
        int tabIndex = qTabWidgetPointer->indexOf(privacyWebEngineViewPointer);

        // Display the loading favorite icon if this tab is loading.
        if (privacyWebEngineViewPointer->isLoading)
            qTabWidgetPointer->setTabIcon(tabIndex, loadingFavoriteIconMoviePointer->currentPixmap());
    });

    // Update the icon when it changes.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::iconChanged, [this, privacyWebEngineViewPointer] (const QIcon &newFavoriteIcon)
    {
        // Store the favorite icon in the privacy web engine view.
        if (newFavoriteIcon.isNull())
            privacyWebEngineViewPointer->favoriteIcon = defaultFavoriteIcon;
        else
            privacyWebEngineViewPointer->favoriteIcon = newFavoriteIcon;

        // Get the index for this tab.
        int tabIndex = qTabWidgetPointer->indexOf(privacyWebEngineViewPointer);

        // Update the icon for this tab.
        if (newFavoriteIcon.isNull())
            qTabWidgetPointer->setTabIcon(tabIndex, defaultFavoriteIcon);
        else
            qTabWidgetPointer->setTabIcon(tabIndex, newFavoriteIcon);
    });

    // Update the progress bar and the favorite icon when a load is started.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadStarted, [this, privacyWebEngineViewPointer] ()
    {
        // Set the privacy web engine view to be loading.
        privacyWebEngineViewPointer->isLoading = true;

        // Store the load progress.
        privacyWebEngineViewPointer->loadProgressInt = 0;

        // Show the progress bar if this is the current tab.
        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
            emit showProgressBar(0);

        // Start the loading favorite icon movie.
        loadingFavoriteIconMoviePointer->start();
    });

    // Update the progress bar when a load progresses.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadProgress, [this, privacyWebEngineViewPointer] (const int progress)
    {
        // Store the load progress.
        privacyWebEngineViewPointer->loadProgressInt = progress;

        // Update the progress bar if this is the current tab.
        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
            emit showProgressBar(progress);
    });

    // Update the progress bar when a load finishes.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::loadFinished, [this, privacyWebEngineViewPointer] ()
    {
        // Set the privacy web engine view to be not loading.
        privacyWebEngineViewPointer->isLoading = false;

        // Store the load progress.
        privacyWebEngineViewPointer->loadProgressInt = -1;

        // Hide the progress bar if this is the current tab.
        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
            emit hideProgressBar();

        // Get the index for this tab.
        int tabIndex = qTabWidgetPointer->indexOf(privacyWebEngineViewPointer);

        // Display the current favorite icon
        qTabWidgetPointer->setTabIcon(tabIndex, privacyWebEngineViewPointer->favoriteIcon);

        // Create a no tabs loading variable.
        bool noTabsLoading = true;

        // Check to see if any other tabs are loading.
        for (int i = 0; i < qTabWidgetPointer->count(); i++)
        {
            // Get the privacy WebEngine view for the tab.
            PrivacyWebEngineView *webEngineViewPointer = qobject_cast<PrivacyWebEngineView*>(qTabWidgetPointer->widget(i));

            // Check to see if it is currently loading.
            if (webEngineViewPointer->isLoading)
                noTabsLoading = false;
        }

        // Stop the loading favorite icon movie if there are no loading tabs.
        if (noTabsLoading)
            loadingFavoriteIconMoviePointer->stop();
    });

    // Display HTTP Ping blocked dialogs.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::displayHttpPingBlockedDialog, [this, privacyWebEngineViewPointer] (const QString &httpPingUrl)
    {
        // Only display the HTTP Ping blocked dialog if this is the current tab.
        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
        {
            // Instantiate an HTTP ping blocked message box.
            QMessageBox httpPingBlockedMessageBox;

            // Set the icon.
            httpPingBlockedMessageBox.setIcon(QMessageBox::Information);

            // Set the window title.
            httpPingBlockedMessageBox.setWindowTitle(i18nc("HTTP Ping blocked dialog title", "HTTP Ping Blocked"));

            // Set the text.
            httpPingBlockedMessageBox.setText(i18nc("HTTP Ping blocked dialog text", "This request has been blocked because it sends a naughty HTTP ping to %1.", httpPingUrl));

            // Set the standard button.
            httpPingBlockedMessageBox.setStandardButtons(QMessageBox::Ok);

            // Display the message box.
            httpPingBlockedMessageBox.exec();
        }
    });

    // Update the zoom factor when changed by CTRL-Scrolling.  This can be modified when <https://redmine.stoutner.com/issues/845> is fixed.
    connect(webEnginePagePointer, &QWebEnginePage::contentsSizeChanged, [webEnginePagePointer, this] ()
    {
        // Only update the zoom factor action text if this is the current tab.
        if (webEnginePagePointer == currentWebEnginePagePointer)
            emit updateZoomFactorAction(webEnginePagePointer->zoomFactor());
    });

    // Display find text results.
    connect(webEnginePagePointer, SIGNAL(findTextFinished(const QWebEngineFindTextResult &)), this, SLOT(findTextFinished(const QWebEngineFindTextResult &)));

    // Handle full screen requests.
    connect(webEnginePagePointer, SIGNAL(fullScreenRequested(QWebEngineFullScreenRequest)), this, SLOT(fullScreenRequested(QWebEngineFullScreenRequest)));

    // Listen for hovered link URLs.
    connect(webEnginePagePointer, SIGNAL(linkHovered(const QString)), this, SLOT(pageLinkHovered(const QString)));

    // Handle file downloads.
    connect(webEngineProfilePointer, SIGNAL(downloadRequested(QWebEngineDownloadItem *)), this, SLOT(showSaveDialog(QWebEngineDownloadItem *)));

    // Set the local storage filter.
    webEngineCookieStorePointer->setCookieFilter([privacyWebEngineViewPointer](const QWebEngineCookieStore::FilterRequest &filterRequest)
    {
        // Block all third party local storage requests, including the sneaky ones that don't register a first party URL.
        if (filterRequest.thirdParty || (filterRequest.firstPartyUrl == QStringLiteral("")))
        {
            //qDebug().noquote().nospace() << "Third-party request blocked:  " << filterRequest.origin;

            // Return false.
            return false;
        }

        // Allow the request if local storage is enabled.
        if (privacyWebEngineViewPointer->localStorageEnabled)
        {
            //qDebug().noquote().nospace() << "Request allowed by local storage:  " << filterRequest.origin;

            // Return true.
            return true;
        }

        //qDebug().noquote().nospace() << "Request blocked by default:  " << filterRequest.origin;

        // Block any remaining local storage requests.
        return false;
    });

    // Disable JavaScript by default (this prevents JavaScript from being enabled on a new tab before domain settings are loaded).
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, false);

    // Don't allow JavaScript to open windows.
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);

    // Allow keyboard navigation.
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::SpatialNavigationEnabled, true);

    // Enable full screen support.
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);

    // Require user interaction to play media.
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::PlaybackRequiresUserGesture, true);

    // Limit WebRTC to public IP addresses.
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::WebRTCPublicInterfacesOnly, true);

    // Enable the PDF viewer (it should be enabled by default, but it is nice to be explicit in case the defaults change).
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::PdfViewerEnabled, true);

    // Plugins must be enabled for the PDF viewer to work.  <https://doc.qt.io/qt-5/qtwebengine-features.html#pdf-file-viewing>
    webEngineSettingsPointer->setAttribute(QWebEngineSettings::PluginsEnabled, true);

    // Update the cookies action.
    connect(privacyWebEngineViewPointer, &PrivacyWebEngineView::updateCookiesAction, [this, privacyWebEngineViewPointer] (const int numberOfCookies)
    {
        // Update the cookie action if the specified privacy WebEngine view is the current privacy WebEngine view.
        if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
            emit updateCookiesAction(numberOfCookies);
    });

    // Process cookie changes.
    connect(webEngineCookieStorePointer, SIGNAL(cookieAdded(QNetworkCookie)), privacyWebEngineViewPointer, SLOT(addCookieToList(QNetworkCookie)));
    connect(webEngineCookieStorePointer, SIGNAL(cookieRemoved(QNetworkCookie)), privacyWebEngineViewPointer, SLOT(removeCookieFromList(QNetworkCookie)));

    // Get a list of durable cookies.
    QList<QNetworkCookie*> *durableCookiesListPointer = CookiesDatabase::getCookies();

    // Add the durable cookies to the store.
    for (QNetworkCookie *cookiePointer : *durableCookiesListPointer)
        addCookieToStore(*cookiePointer, webEngineCookieStorePointer);

    // Enable spell checking.
    webEngineProfilePointer->setSpellCheckEnabled(true);

    // Set the spell check language.
    webEngineProfilePointer->setSpellCheckLanguages(Settings::spellCheckLanguages());

    // Populate the zoom factor.  This is necessary if a URL is being loaded, like a local URL, that does not trigger `applyDomainSettings()`.
    privacyWebEngineViewPointer->setZoomFactor(Settings::zoomFactor());

    // Update the UI when domain settings are applied.
    connect(privacyWebEngineViewPointer, SIGNAL(updateUi(const PrivacyWebEngineView*)), this, SLOT(updateUiFromWebEngineView(const PrivacyWebEngineView*)));

    // Move to the new tab if it is not a background tab.
    if (!backgroundTab)
        qTabWidgetPointer->setCurrentIndex(newTabIndex);

    // Clear the URL line edit focus so that it populates correctly when opening a new tab from the context menu.
    if (removeUrlLineEditFocus)
        emit clearUrlLineEditFocus();

    // Return the privacy WebEngine view pointer.
    return privacyWebEngineViewPointer;
}

void TabWidget::applyApplicationSettings()
{
    // Set the tab position.
    if (Settings::tabsOnTop())
        qTabWidgetPointer->setTabPosition(QTabWidget::North);
    else
        qTabWidgetPointer->setTabPosition(QTabWidget::South);

    // Set the search engine URL.
    searchEngineUrl = SearchEngineHelper::getSearchUrl(Settings::searchEngine());

    // Emit the update search engine actions signal.
    emit updateSearchEngineActions(Settings::searchEngine(), true);
}

void TabWidget::applyDomainSettingsAndReload()
{
    // Apply the domain settings.  `true` reloads the website.
    currentPrivacyWebEngineViewPointer->applyDomainSettings(currentPrivacyWebEngineViewPointer->url().host(), true);
}

void TabWidget::applyOnTheFlySearchEngine(QAction *searchEngineActionPointer)
{
    // Store the search engine name.
    QString searchEngineName = searchEngineActionPointer->text();

    // Strip out any `&` characters.
    searchEngineName.remove('&');

    // Store the search engine string.
    searchEngineUrl = SearchEngineHelper::getSearchUrl(searchEngineName);

    // Update the search engine actions.
    emit updateSearchEngineActions(searchEngineName, false);
}

void TabWidget::applyOnTheFlyUserAgent(QAction *userAgentActionPointer) const
{
    // Get the user agent name.
    QString userAgentName = userAgentActionPointer->text();

    // Strip out any `&` characters.
    userAgentName.remove('&');

    // Apply the user agent.
    currentWebEngineProfilePointer->setHttpUserAgent(userAgentHelperPointer->getUserAgentFromTranslatedName(userAgentName));

    // Update the user agent actions.
    emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), false);

    // Reload the website.
    currentPrivacyWebEngineViewPointer->reload();
}

void TabWidget::applyOnTheFlyZoomFactor(const double &zoomFactor) const
{
    // Set the zoom factor.
    currentPrivacyWebEngineViewPointer->setZoomFactor(zoomFactor);
}

void TabWidget::applySpellCheckLanguages() const
{
    // Get the number of tab.
    int numberOfTabs = qTabWidgetPointer->count();

    // Set the spell check languages for each tab.
    for (int i = 0; i < numberOfTabs; ++i)
    {
        // Get the WebEngine view pointer.
        PrivacyWebEngineView *webEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(qTabWidgetPointer->currentWidget());

        // Get the WebEngine page pointer.
        QWebEnginePage *webEnginePagePointer = webEngineViewPointer->page();

        // Get the WebEngine profile pointer.
        QWebEngineProfile *webEngineProfilePointer = webEnginePagePointer->profile();

        // Set the spell check languages.
        webEngineProfilePointer->setSpellCheckLanguages(Settings::spellCheckLanguages());
    }
}

void TabWidget::back() const
{
    // Go back.
    currentPrivacyWebEngineViewPointer->back();
}

void TabWidget::deleteAllCookies() const
{
    // Delete all the cookies.
    currentWebEngineCookieStorePointer->deleteAllCookies();
}

void TabWidget::deleteCookieFromStore(const QNetworkCookie &cookie) const
{
    // Delete the cookie.
    currentWebEngineCookieStorePointer->deleteCookie(cookie);
}

void TabWidget::deleteTab(const int tabIndex)
{
    // Get the privacy WebEngine view.
    PrivacyWebEngineView *privacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(qTabWidgetPointer->widget(tabIndex));

    // Process the tab delete according to the number of tabs.
    if (qTabWidgetPointer->count() > 1)  // There is more than one tab.
    {
        // Delete the tab.
        qTabWidgetPointer->removeTab(tabIndex);

        // Delete the WebEngine page to prevent the following error:  `Release of profile requested but WebEnginePage still not deleted. Expect troubles !`
        delete privacyWebEngineViewPointer->page();

        // Delete the privacy WebEngine view.
        delete privacyWebEngineViewPointer;
    }
    else  // There is only one tab.
    {
        // Close Privacy Browser.
        window()->close();
    }
}

void TabWidget::findPrevious(const QString &text) const
{
    // Store the current text.
    currentPrivacyWebEngineViewPointer->findString = text;

    // Find the previous text in the current privacy WebEngine.
    if (currentPrivacyWebEngineViewPointer->findCaseSensitive)
        currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindCaseSensitively|QWebEnginePage::FindBackward);
    else
        currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindBackward);
}

void TabWidget::findText(const QString &text) const
{
    // Store the current text.
    currentPrivacyWebEngineViewPointer->findString = text;

    // Find the text in the current privacy WebEngine.
    if (currentPrivacyWebEngineViewPointer->findCaseSensitive)
        currentPrivacyWebEngineViewPointer->findText(text, QWebEnginePage::FindCaseSensitively);
    else
        currentPrivacyWebEngineViewPointer->findText(text);

    // Clear the currently selected text in the WebEngine page if the find text is empty.
    if (text.isEmpty())
        currentWebEnginePagePointer->action(QWebEnginePage::Unselect)->activate(QAction::Trigger);
}

void TabWidget::findTextFinished(const QWebEngineFindTextResult &findTextResult)
{
    // Update the find text UI if it wasn't simply wiping the current find text selection.  Otherwise the UI temporarily flashes `0/0`.
    if (wipingCurrentFindTextSelection)  // The current selection is being wiped.
    {
        // Reset the flag.
        wipingCurrentFindTextSelection = false;
    }
    else  // A new search has been performed.
    {
        // Store the result.
        currentPrivacyWebEngineViewPointer->findTextResult = findTextResult;

        // Update the UI.
        emit updateFindTextResults(findTextResult);
    }
}

void TabWidget::forward() const
{
    // Go forward.
    currentPrivacyWebEngineViewPointer->forward();
}

void TabWidget::fullScreenRequested(QWebEngineFullScreenRequest fullScreenRequest) const
{
    // Make it so.
    emit fullScreenRequested(fullScreenRequest.toggleOn());

    // Accept the request.
    fullScreenRequest.accept();
}

std::list<QNetworkCookie>* TabWidget::getCookieList() const
{
    // Return the current cookie list.
    return currentPrivacyWebEngineViewPointer->cookieListPointer;
}

QString& TabWidget::getDomainSettingsName() const
{
    // Return the domain settings name.
    return currentPrivacyWebEngineViewPointer->domainSettingsName;
}

void TabWidget::home() const
{
    // Load the homepage.
    currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(Settings::homepage()));
}

PrivacyWebEngineView* TabWidget::loadBlankInitialWebsite()
{
    // Apply the application settings.
    applyApplicationSettings();

    // Return the current privacy WebEngine view pointer.
    return currentPrivacyWebEngineViewPointer;
}

void TabWidget::loadInitialWebsite()
{
    // Apply the application settings.
    applyApplicationSettings();

    // Get the arguments.
    QStringList argumentsStringList = qApp->arguments();

    // Check to see if the arguments lists contains a URL.
    if (argumentsStringList.size() > 1)
    {
        // Load the URL from the arguments list.
        currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(argumentsStringList.at(1)));
    }
    else
    {
        // Load the homepage.
        home();
    }
}

void TabWidget::loadUrlFromLineEdit(QString url) const
{
    // Decide if the text is more likely to be a URL or a search.
    if (url.startsWith("file://"))  // The text is likely a file URL.
    {
        // Load the URL.
        currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(url));
    }
    else if (url.contains("."))  // The text is likely a URL.
    {
        // Check if the URL does not start with a valid protocol.
        if (!url.startsWith("http"))
        {
            // Add `https://` to the beginning of the URL.
            url = "https://" + url;
        }

        // Load the URL.
        currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(url));
    }
    else  // The text is likely a search.
    {
        // Load the search.
        currentPrivacyWebEngineViewPointer->load(QUrl::fromUserInput(searchEngineUrl + url));
    }
}

void TabWidget::mouseBack() const
{
    // Go back if possible.
    if (currentPrivacyWebEngineViewPointer->isActiveWindow() && currentWebEngineHistoryPointer->canGoBack())
    {
        // Clear the URL line edit focus.
        emit clearUrlLineEditFocus();

        // Go back.
        currentPrivacyWebEngineViewPointer->back();
    }
}

void TabWidget::mouseForward() const
{
    // Go forward if possible.
    if (currentPrivacyWebEngineViewPointer->isActiveWindow() && currentWebEngineHistoryPointer->canGoForward())
    {
        // Clear the URL line edit focus.
        emit clearUrlLineEditFocus();

        // Go forward.
        currentPrivacyWebEngineViewPointer->forward();
    }
}

void TabWidget::pageLinkHovered(const QString &linkUrl) const
{
    // Emit a signal so that the browser window can update the status bar.
    emit linkHovered(linkUrl);
}

void TabWidget::print() const
{
    // Create a printer.
    QPrinter printer;

    // Set the resolution to be 300 dpi.
    printer.setResolution(300);

    // Create a printer dialog.
    QPrintDialog printDialog(&printer, currentPrivacyWebEngineViewPointer);

    // Display the dialog and print the page if instructed.
    if (printDialog.exec() == QDialog::Accepted)
        printWebpage(&printer);
}

void TabWidget::printPreview() const
{
    // Create a printer.
    QPrinter printer;

    // Set the resolution to be 300 dpi.
    printer.setResolution(300);

    // Create a print preview dialog.
    QPrintPreviewDialog printPreviewDialog(&printer, currentPrivacyWebEngineViewPointer);

    // Generate the print preview.
    connect(&printPreviewDialog, SIGNAL(paintRequested(QPrinter *)), this, SLOT(printWebpage(QPrinter *)));

    // Display the dialog.
    printPreviewDialog.exec();
}

void TabWidget::printWebpage(QPrinter *printerPointer) const
{
    // Create an event loop.  For some reason, the print preview doesn't produce any output unless it is run inside an event loop.
    QEventLoop eventLoop;

    // Print the webpage, converting the callback above into a `QWebEngineCallback<bool>`.
    // Printing requires that the printer be a pointer, not a reference, or it will crash with much cursing.
    currentWebEnginePagePointer->print(printerPointer, [&eventLoop](bool printSuccess)
    {
        // Instruct the compiler to ignore the unused parameter.
        (void) printSuccess;

        // Quit the loop.
        eventLoop.quit();
    });

    // Execute the loop.
    eventLoop.exec();
}

void TabWidget::refresh() const
{
    // Reload the website.
    currentPrivacyWebEngineViewPointer->reload();
}

void TabWidget::setTabBarVisible(const bool visible) const
{
    // Set the tab bar visibility.
    qTabWidgetPointer->tabBar()->setVisible(visible);
}

void TabWidget::showSaveDialog(QWebEngineDownloadItem *webEngineDownloadItemPointer)
{
    // Get the download attributes.
    QUrl downloadUrl = webEngineDownloadItemPointer->url();
    QString mimeTypeString = webEngineDownloadItemPointer->mimeType();
    QString suggestedFileName = webEngineDownloadItemPointer->suggestedFileName();
    int totalBytes = webEngineDownloadItemPointer->totalBytes();

    // Check to see if Privacy Browser is not running KDE or if local storage (cookies) is enabled.
    if (!isRunningKde || currentPrivacyWebEngineViewPointer->localStorageEnabled)  // KDE is not running or local storage (cookies) is enabled.  Use WebEngine's downloader.
    {
        // Instantiate the save dialog.
        SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes);

        // Display the save dialog.
        int saveDialogResult = saveDialogPointer->exec();

        // Process the save dialog results.
        if (saveDialogResult == QDialog::Accepted)  // Save was selected.
        {
            // Get the download directory.
            QString downloadDirectory = Settings::downloadLocation();

            // Resolve the system download directory if specified.
            if (downloadDirectory == QLatin1String("System Download Directory"))
                downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);

            // Get a file path from the file picker.
            QString saveFilePath = QFileDialog::getSaveFileName(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory + QLatin1Char('/') + suggestedFileName);

            // Process the save file path.
            if (!saveFilePath.isEmpty())  // The file save path is populated.
            {
                // Create a save file path file info.
                QFileInfo saveFilePathFileInfo = QFileInfo(saveFilePath);

                // Get the canonical save path and file name.
                QString absoluteSavePath = saveFilePathFileInfo.absolutePath();
                QString saveFileName = saveFilePathFileInfo.fileName();

                // Set the download directory and file name.
                webEngineDownloadItemPointer->setDownloadDirectory(absoluteSavePath);
                webEngineDownloadItemPointer->setDownloadFileName(saveFileName);

                // Create a file download notification.
                KNotification *fileDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));

                // Set the notification title.
                fileDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));

                // Set the notification text.
                fileDownloadNotificationPointer->setText(i18nc("Downloading notification text", "Downloading %1", saveFileName));

                // Set the notification icon.
                fileDownloadNotificationPointer->setIconName(QLatin1String("download"));

                // Set the action list cancel button.
                fileDownloadNotificationPointer->setActions(QStringList({i18nc("Download notification action","Cancel")}));

                // Set the notification to display indefinitely.
                fileDownloadNotificationPointer->setFlags(KNotification::Persistent);

                // Prevent the notification from being autodeleted if it is closed.  Otherwise, the updates to the notification below cause a crash.
                fileDownloadNotificationPointer->setAutoDelete(false);

                // Display the notification.
                fileDownloadNotificationPointer->sendEvent();

                // Handle clicks on the cancel button.
                connect(fileDownloadNotificationPointer, &KNotification::action1Activated, [webEngineDownloadItemPointer, saveFileName] ()
                {
                    // Cancel the download.
                    webEngineDownloadItemPointer->cancel();

                    // Create a file download notification.
                    KNotification *canceledDownloadNotificationPointer = new KNotification(QLatin1String("FileDownload"));

                    // Set the notification title.
                    canceledDownloadNotificationPointer->setTitle(i18nc("Download notification title", "Download"));

                    // Set the new text.
                    canceledDownloadNotificationPointer->setText(i18nc("Download canceled notification", "%1 download canceled", saveFileName));

                    // Set the notification icon.
                    canceledDownloadNotificationPointer->setIconName(QLatin1String("download"));

                    // Display the notification.
                    canceledDownloadNotificationPointer->sendEvent();
                });

                // Update the notification when the download progresses.
                connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::downloadProgress, [fileDownloadNotificationPointer, saveFileName] (qint64 bytesReceived, qint64 totalBytes)
                {
                    // Set the new text.  Total bytes will be 0 if the download size is unknown.
                    if (totalBytes > 0)
                    {
                        // Calculate the download percentage.
                        int downloadPercentage = 100 * bytesReceived / totalBytes;

                        // Set the file download notification text.
                        fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1\% of %2 downloaded (%3 of %4 bytes)", downloadPercentage, saveFileName,
                                                                    bytesReceived, totalBytes));
                    }
                    else
                    {
                        // Set the file download notification text.
                        fileDownloadNotificationPointer->setText(i18nc("Download progress notification text", "%1:  %2 bytes downloaded", saveFileName, bytesReceived));
                    }

                    // Display the updated notification.
                    fileDownloadNotificationPointer->update();
                });

                // Update the notification when the download finishes.  The save file name must be copied into the lambda or a crash occurs.
                connect(webEngineDownloadItemPointer, &QWebEngineDownloadItem::finished, [fileDownloadNotificationPointer, saveFileName, saveFilePath] ()
                {
                    // Set the new text.
                    fileDownloadNotificationPointer->setText(i18nc("Download finished notification text", "%1 download finished", saveFileName));

                    // Set the URL so the file options will be displayed.
                    fileDownloadNotificationPointer->setUrls(QList<QUrl> {QUrl(saveFilePath)});

                    // Remove the actions from the notification.
                    fileDownloadNotificationPointer->setActions(QStringList());

                    // Set the notification to disappear after a timeout.
                    fileDownloadNotificationPointer->setFlags(KNotification::CloseOnTimeout);

                    // Display the updated notification.
                    fileDownloadNotificationPointer->update();
                });

                // Start the download.
                webEngineDownloadItemPointer->accept();
            }
            else  // The file save path is not populated.
            {
                // Cancel the download.
                webEngineDownloadItemPointer->cancel();
            }
        }
        else  // Cancel was selected.
        {
            // Cancel the download.
            webEngineDownloadItemPointer->cancel();
        }
    }
    else  // KDE is running and local storage (cookies) is disabled.  Use KDE's native downloader.
          // This must use the show command to launch a separate dialog which cancels WebEngine's automatic background download of the file to a temporary location.
    {
        // Instantiate the save dialog.  `true` instructs it to use the native downloader
        SaveDialog *saveDialogPointer = new SaveDialog(downloadUrl, mimeTypeString, totalBytes, suggestedFileName, true);

        // Connect the save button.
        connect(saveDialogPointer, SIGNAL(useNativeKdeDownloader(QUrl &, QString &)), this, SLOT(useNativeKdeDownloader(QUrl &, QString &)));

        // Show the dialog.
        saveDialogPointer->show();
    }
}

void TabWidget::toggleDomStorage() const
{
    // Toggle DOM storage.
    currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, !currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));

    // Update the DOM storage action.
    emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));

    // Reload the website.
    currentPrivacyWebEngineViewPointer->reload();
}

void TabWidget::toggleFindCaseSensitive(const QString &text)
{
    // Toggle find case sensitive.
    currentPrivacyWebEngineViewPointer->findCaseSensitive = !currentPrivacyWebEngineViewPointer->findCaseSensitive;

    // Set the wiping current find text selection flag.
    wipingCurrentFindTextSelection = true;

    // Wipe the previous search.  Otherwise currently highlighted words will remain highlighted.
    findText(QLatin1String(""));

    // Update the find text.
    findText(text);
}

void TabWidget::toggleJavaScript() const
{
    // Toggle JavaScript.
    currentWebEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, !currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));

    // Update the JavaScript action.
    emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));

    // Reload the website.
    currentPrivacyWebEngineViewPointer->reload();
}

void TabWidget::toggleLocalStorage()
{
    // Toggle local storage.
    currentPrivacyWebEngineViewPointer->localStorageEnabled = !currentPrivacyWebEngineViewPointer->localStorageEnabled;

    // Update the local storage action.
    emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);

    // Reload the website.
    currentPrivacyWebEngineViewPointer->reload();
}

void TabWidget::updateUiFromWebEngineView(const PrivacyWebEngineView *privacyWebEngineViewPointer) const
{
    // Only update the UI if the signal was emitted from the current privacy WebEngine.
    if (privacyWebEngineViewPointer == currentPrivacyWebEngineViewPointer)
    {
        // Update the UI.
        emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
        emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
        emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
        emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
        emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
        emit updateZoomFactorAction(currentPrivacyWebEngineViewPointer->zoomFactor());
    }
}

void TabWidget::updateUiWithTabSettings()
{
    // Update the current WebEngine pointers.
    currentPrivacyWebEngineViewPointer = qobject_cast<PrivacyWebEngineView *>(qTabWidgetPointer->currentWidget());
    currentWebEngineSettingsPointer = currentPrivacyWebEngineViewPointer->settings();
    currentWebEnginePagePointer = currentPrivacyWebEngineViewPointer->page();
    currentWebEngineProfilePointer = currentWebEnginePagePointer->profile();
    currentWebEngineHistoryPointer = currentWebEnginePagePointer->history();
    currentWebEngineCookieStorePointer = currentWebEngineProfilePointer->cookieStore();

    // Clear the URL line edit focus.
    emit clearUrlLineEditFocus();

    // Update the actions.
    emit updateBackAction(currentWebEngineHistoryPointer->canGoBack());
    emit updateCookiesAction(currentPrivacyWebEngineViewPointer->cookieListPointer->size());
    emit updateDomStorageAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::LocalStorageEnabled));
    emit updateForwardAction(currentWebEngineHistoryPointer->canGoForward());
    emit updateJavaScriptAction(currentWebEngineSettingsPointer->testAttribute(QWebEngineSettings::JavascriptEnabled));
    emit updateLocalStorageAction(currentPrivacyWebEngineViewPointer->localStorageEnabled);
    emit updateUserAgentActions(currentWebEngineProfilePointer->httpUserAgent(), true);
    emit updateZoomFactorAction(currentPrivacyWebEngineViewPointer->zoomFactor());

    // Update the URL.
    emit updateWindowTitle(currentPrivacyWebEngineViewPointer->title());
    emit updateDomainSettingsIndicator(currentPrivacyWebEngineViewPointer->domainSettingsName != QLatin1String(""));
    emit updateUrlLineEdit(currentPrivacyWebEngineViewPointer->url());

    // Update the find text.
    emit updateFindText(currentPrivacyWebEngineViewPointer->findString, currentPrivacyWebEngineViewPointer->findCaseSensitive);
    emit updateFindTextResults(currentPrivacyWebEngineViewPointer->findTextResult);

    // Update the progress bar.
    if (currentPrivacyWebEngineViewPointer->loadProgressInt >= 0)
        emit showProgressBar(currentPrivacyWebEngineViewPointer->loadProgressInt);
    else
        emit hideProgressBar();
}

void TabWidget::useNativeKdeDownloader(QUrl &downloadUrl, QString &suggestedFileName)
{
    // Get the download directory.
    QString downloadDirectory = Settings::downloadLocation();

    // Resolve the system download directory if specified.
    if (downloadDirectory == QLatin1String("System Download Directory"))
        downloadDirectory = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);

    // Create a save file dialog.
    QFileDialog *saveFileDialogPointer = new QFileDialog(this, i18nc("Save file dialog caption", "Save File"), downloadDirectory);

    // Tell the dialog to use a save button.
    saveFileDialogPointer->setAcceptMode(QFileDialog::AcceptSave);

    // Populate the file name from the download item pointer.
    saveFileDialogPointer->selectFile(suggestedFileName);

    // Prevent interaction with the parent window while the dialog is open.
    saveFileDialogPointer->setWindowModality(Qt::WindowModal);

    // Process the saving of the file.  The save file dialog pointer must be captured directly instead of by reference or nasty crashes occur.
    auto saveFile = [saveFileDialogPointer, downloadUrl] ()
    {
        // Get the save location.  The dialog box should only allow the selecting of one file location.
        QUrl saveLocation = saveFileDialogPointer->selectedUrls().value(0);

        // Create a file copy job.  `-1` creates the file with default permissions.
        KIO::FileCopyJob *fileCopyJobPointer = KIO::file_copy(downloadUrl, saveLocation, -1, KIO::Overwrite);

        // Set the download job to display any warning and error messages.
        fileCopyJobPointer->uiDelegate()->setAutoWarningHandlingEnabled(true);
        fileCopyJobPointer->uiDelegate()->setAutoErrorHandlingEnabled(true);

        // Start the download.
        fileCopyJobPointer->start();
    };

    // Handle clicks on the save button.
    connect(saveFileDialogPointer, &QDialog::accepted, this, saveFile);

    // Show the dialog.
    saveFileDialogPointer->show();
}
