/* SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: 2022-2025 Soren Stoutner <soren@stoutner.com>
 *
 * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc/>.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <https://www.gnu.org/licenses/>.
 */

// Application headers.
#include "GlobalVariables.h"
#include "PrivacyWebEngineView.h"
#include "PrivacyWebEnginePage.h"
#include "Settings.h"
#include "TemporaryWebEngineView.h"
#include "ui_HttpAuthenticationDialog.h"
#include "databases/CookiesDatabase.h"
#include "databases/DomainsDatabase.h"
#include "dialogs/HttpAuthenticationDialog.h"
#include "helpers/FilterListHelper.h"
#include "interceptors/UrlRequestInterceptor.h"
#include "windows/BrowserWindow.h"

// Qt framework headers.
#include <QContextMenuEvent>
#include <QMenu>

// Construct the class.
PrivacyWebEngineView::PrivacyWebEngineView(QWidget *parentWidgetPointer) : QWebEngineView(parentWidgetPointer)
{
    // Create an off-the-record profile (the default when no profile name is specified).
    webEngineProfilePointer = new QWebEngineProfile(QLatin1String(""));

    // Disable the HTTP cache.
    webEngineProfilePointer->setHttpCacheType(QWebEngineProfile::NoCache);

    // Create a WebEngine page.
    PrivacyWebEnginePage *privacyWebEnginePagePointer = new PrivacyWebEnginePage(webEngineProfilePointer);

    // Set the WebEngine page.
    setPage(privacyWebEnginePagePointer);

    // Get handles for the various aspects of the WebEngine.
    webEngineSettingsPointer = privacyWebEnginePagePointer->settings();

    // Instantiate the URL request interceptor.
    UrlRequestInterceptor *urlRequestInterceptorPointer = new UrlRequestInterceptor(this);

    // Set the URL request interceptor.
    webEngineProfilePointer->setUrlRequestInterceptor(urlRequestInterceptorPointer);

    // Connect the URL request interceptor signals.
    connect(urlRequestInterceptorPointer, SIGNAL(applyDomainSettings(const QUrl&, const bool)), this, SLOT(applyDomainSettings(const QUrl&, const bool)));
    connect(urlRequestInterceptorPointer, SIGNAL(newMainFrameResource()), this, SLOT(clearRequestsList()));
    connect(urlRequestInterceptorPointer, SIGNAL(displayHttpPingDialog(const QString&)), this, SLOT(displayHttpPingDialog(const QString&)));
    connect(urlRequestInterceptorPointer, SIGNAL(requestProcessed(RequestStruct*)), this, SLOT(storeRequest(RequestStruct*)));

    // Handle HTTP authentication requests.
    connect(privacyWebEnginePagePointer, SIGNAL(authenticationRequired(const QUrl&, QAuthenticator*)), this, SLOT(handleAuthenticationRequest(const QUrl&, QAuthenticator*)));

    // Store the link URL whenever a link is hovered.
    connect(privacyWebEnginePagePointer, SIGNAL(linkHovered(const QString)), this, SLOT(saveHoveredLink(const QString)));

    // Store the URL when it changes.
    connect(this, SIGNAL(urlChanged(const QUrl&)), this, SLOT(storeUpdatedUrl(const QUrl&)));
}

void PrivacyWebEngineView::addCookieToList(const QNetworkCookie &cookie) const
{
    //qDebug() << "Add cookie:  " << cookie.toRawForm();

    // Add the new cookie to the list.
    cookieListPointer->push_front(cookie);

    // Update the cookie if it is durable and has new data.
    if (CookiesDatabase::isUpdate(cookie))
        CookiesDatabase::updateCookie(cookie);

    // Update the cookies action.
    Q_EMIT numberOfCookiesChanged(cookieListPointer->size());
}

void PrivacyWebEngineView::applyDomainSettings(const QUrl &newUrl, const bool navigatingHistory)
{
    // Store the current host.
    currentHost = newUrl.host();

    // Create a user agent changed tracker.
    bool userAgentChanged = false;

    // Get the record for the current host.
    QSqlQuery domainQuery = DomainsDatabase::getDomainQuery(currentHost);

    // Check if the current host has domain settings.
    if (domainQuery.isValid())  // The current host has domain settings.
    {
        // Store the domain settings name.
        domainSettingsName = domainQuery.value(DomainsDatabase::DOMAIN_NAME).toString();

        // Set the JavaScript status.
        switch (domainQuery.value(DomainsDatabase::JAVASCRIPT).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled()); break;
            case (DomainsDatabase::ENABLED): webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, true); break;
            case (DomainsDatabase::DISABLED): webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, false); break;
        }

        // Set the local storage status.
        switch (domainQuery.value(DomainsDatabase::LOCAL_STORAGE).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): localStorageEnabled = Settings::localStorageEnabled(); break;
            case (DomainsDatabase::ENABLED): localStorageEnabled = true; break;
            case (DomainsDatabase::DISABLED): localStorageEnabled = false; break;
        }

        // Set the DOM storage status.  QWebEngineSettings confusingly calls this local storage.
        switch (domainQuery.value(DomainsDatabase::DOM_STORAGE).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): webEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled()); break;
            case (DomainsDatabase::ENABLED): webEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, true); break;
            case (DomainsDatabase::DISABLED): webEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, false); break;
        }

        // Get the domain settings user agent.
        QString domainSettingsUserAgent = UserAgentHelper::getResultingDomainSettingsUserAgent(domainQuery.value(DomainsDatabase::USER_AGENT).toString());

        // Determine if the user agent is changing.
        if (webEngineProfilePointer->httpUserAgent() != domainSettingsUserAgent)
            userAgentChanged = true;

        // Set the user agent.  Setting the user agent, even if it isn't changing, causes the pre-rendered page to enter a locked state where reload doesn't work.
        webEngineProfilePointer->setHttpUserAgent(domainSettingsUserAgent);

        // Set the UltraPrivacy status.
        switch (domainQuery.value(DomainsDatabase::ULTRAPRIVACY).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): ultraPrivacyEnabled = Settings::ultraPrivacyEnabled(); break;
            case (DomainsDatabase::ENABLED): ultraPrivacyEnabled = true; break;
            case (DomainsDatabase::DISABLED): ultraPrivacyEnabled = false; break;
        }

        // Set the UltraList status.
        switch (domainQuery.value(DomainsDatabase::ULTRALIST).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): ultraListEnabled = Settings::ultraListEnabled(); break;
            case (DomainsDatabase::ENABLED): ultraListEnabled = true; break;
            case (DomainsDatabase::DISABLED): ultraListEnabled = false; break;
        }

        // Set the EasyPrivacy status.
        switch (domainQuery.value(DomainsDatabase::EASYPRIVACY).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): easyPrivacyEnabled = Settings::easyPrivacyEnabled(); break;
            case (DomainsDatabase::ENABLED): easyPrivacyEnabled = true; break;
            case (DomainsDatabase::DISABLED): easyPrivacyEnabled = false; break;
        }

        // Set the EasyList status.
        switch (domainQuery.value(DomainsDatabase::EASYLIST).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): easyListEnabled = Settings::easyListEnabled(); break;
            case (DomainsDatabase::ENABLED): easyListEnabled = true; break;
            case (DomainsDatabase::DISABLED): easyListEnabled = false; break;
        }

        // Set the Fanboy's Annoyance List status.
        switch (domainQuery.value(DomainsDatabase::FANBOYS_ANNOYANCE_LIST).toInt())
        {
            case (DomainsDatabase::SYSTEM_DEFAULT): fanboysAnnoyanceListEnabled = Settings::fanboysAnnoyanceListEnabled(); break;
            case (DomainsDatabase::ENABLED): fanboysAnnoyanceListEnabled = true; break;
            case (DomainsDatabase::DISABLED): fanboysAnnoyanceListEnabled = false; break;
        }

        // Check if a custom zoom factor is set.
        if (domainQuery.value(DomainsDatabase::ZOOM_FACTOR).toInt())
        {
            // Store the current zoom factor.
            defaultZoomFactor = domainQuery.value(DomainsDatabase::CUSTOM_ZOOM_FACTOR).toDouble();
        }
        else
        {
            // Store the current zoom factor.
            defaultZoomFactor = Settings::zoomFactor();
        }
    }
    else  // The current host does not have domain settings.
    {
        // Reset the domain settings name.
        domainSettingsName = QLatin1String("");

        // Set the JavaScript status.
        webEngineSettingsPointer->setAttribute(QWebEngineSettings::JavascriptEnabled, Settings::javaScriptEnabled());

        // Set the local storage status.
        localStorageEnabled = Settings::localStorageEnabled();

        // Set DOM storage.  In QWebEngineSettings it is called Local Storage.
        webEngineSettingsPointer->setAttribute(QWebEngineSettings::LocalStorageEnabled, Settings::domStorageEnabled());

        // Get the default user agent.
        QString defaultUserAgent = UserAgentHelper::getUserAgentFromDatabaseName(Settings::userAgent());

        // Determine if the user agent is changing.
        if (webEngineProfilePointer->httpUserAgent() != defaultUserAgent)
            userAgentChanged = true;

        // Set the user agent.  Setting the user agent, even if it isn't changing, causes the pre-rendered page to enter a locked state where reload doesn't work.
        webEngineProfilePointer->setHttpUserAgent(defaultUserAgent);

        // Set the status for each filter list.
        ultraPrivacyEnabled = Settings::ultraPrivacyEnabled();
        ultraListEnabled = Settings::ultraListEnabled();
        easyPrivacyEnabled = Settings::easyPrivacyEnabled();
        easyListEnabled = Settings::easyListEnabled();
        fanboysAnnoyanceListEnabled = Settings::fanboysAnnoyanceListEnabled();

        // Store the zoom factor.
        defaultZoomFactor = Settings::zoomFactor();
    }

    // Set the current zoom factor.
    setZoomFactor(defaultZoomFactor);

    // Reset the HTTP authentication dialog counter.
    httpAuthenticationDialogsDisplayed = 0;

    // Update the UI.
    Q_EMIT updateUi(this);

    // Reload the URL.
    // This works around a bug in Qt WebEngine that causes massive problems when the user agent is changed after the page has started pre-rendering.  <https://redmine.stoutner.com/issues/821>
    if (navigatingHistory)
    {
        // Get a handle for the WebEngine history.
        QWebEngineHistory *webEngineHistoryPointer = history();

        // Navigate the history according to the status of the user agent.
        if (userAgentChanged)  // The user agent changed.
        {
            // Go back once.  This current navigation will fail, but the next one will succeed.
            //back();

            if (webEngineHistoryPointer->canGoBack() && (newUrl == webEngineHistoryPointer->backItem().url()))
            {
                // Go back twice.  The first one will fail, but the second will succeed.
                back();
            }
            else
            {
                // Go forward twice.  The first one will fail, but the second will succeed.
                forward();
            }
        }
        else  // The user agent did not change.
        {
            // The WebEngine history will think it has loaded the final URL, and will be in the correct location, but it will have been interrupted and reloaded the original URL.
            // The solution is to direct the WebEngine history to offset one and then return, which will actually load the correct URL because `applyDomainSettings` will not be triggered.
            if (webEngineHistoryPointer->canGoForward())
            {
                // Go forward and back one.
                forward();
                back();
            }
            else
            {
                // Go back and forward one.
                back();
                forward();
            }
        }
    } else {
        // Load the new URL again.
        load(newUrl);
    }
}

void PrivacyWebEngineView::clearRequestsList()
{
    // Reset the number of blocked requests.
    blockedRequestsVector[ULTRAPRIVACY] = 0;
    blockedRequestsVector[ULTRALIST] = 0;
    blockedRequestsVector[EASYPRIVACY] = 0;
    blockedRequestsVector[EASYLIST] = 0;
    blockedRequestsVector[FANBOYS_ANNOYANCE_LIST] = 0;
    blockedRequestsVector[TOTAL] = 0;

    // Clear the requests list.
    requestsListPointer->clear();

    // Update the blocked requests action.
    Q_EMIT requestBlocked(blockedRequestsVector);
}

void PrivacyWebEngineView::contextMenuEvent(QContextMenuEvent *contextMenuEvent)
{
    // Get a handle for the WebEngine page.
    QWebEnginePage *webEnginePagePointer = page();

    // Get a handle for the menu.
    QMenu *contextMenu = createStandardContextMenu();

    // Get the list of context menu actions.
    const QList<QAction *> contextMenuActionsList = contextMenu->actions();

    // Add the open link in new background tab action if the context menu already contains the open link in new window action.
    if (contextMenuActionsList.contains(webEnginePagePointer->action(QWebEnginePage::OpenLinkInNewWindow)))
    {
        // Create a manual open in new tab action.
        QAction *manualOpenLinkInNewTabActionPointer = new QAction(i18nc("Open link in new tab action", "Open link in new tab"), contextMenu);

        // Connect the actions.
        connect(manualOpenLinkInNewTabActionPointer, SIGNAL(triggered()), this, SLOT(manualOpenInNewTab()));

        // Move the open in new tab action above the back action.
        contextMenu->insertAction(webEnginePagePointer->action(QWebEnginePage::Back), manualOpenLinkInNewTabActionPointer);

        // Add the open link in background tab action above the back action.
        contextMenu->insertAction(webEnginePagePointer->action(QWebEnginePage::Back), webEnginePagePointer->action(QWebEnginePage::OpenLinkInNewBackgroundTab));

        // Move the open in new window action above the back action.
        contextMenu->insertAction(webEnginePagePointer->action(QWebEnginePage::Back), webEnginePagePointer->action(QWebEnginePage::OpenLinkInNewWindow));

        // Add a separator above the back action.
        contextMenu->insertSeparator(webEnginePagePointer->action(QWebEnginePage::Back));

        if (globalFirefoxInstalled || globalChromiumInstalled)
        {
            // Add the open with Firefox action if Firefox is installed.
            if (globalFirefoxInstalled)
            {
                // Create an open with Firefox action.
                QAction *openWithFirefoxActionPointer = new QAction(QIcon::fromTheme(QLatin1String("firefox-esr")), i18nc("Open with Firefox context menu action", "Open with Firefox"), contextMenu);

                // Add the open with Firefox action above the back action.
                contextMenu->insertAction(webEnginePagePointer->action(QWebEnginePage::Back), openWithFirefoxActionPointer);

                // Connect the action.
                connect(openWithFirefoxActionPointer, SIGNAL(triggered()), this, SLOT(openWithFirefox()));
            }

            // Add the open with Chromium action if Chromium is installed.
            if (globalChromiumInstalled)
            {
                // Create an open with Chromium action.
                QAction *openWithChromiumActionPointer = new QAction(QIcon::fromTheme(QLatin1String("chromium")), i18nc("Open with Chromium context menu action", "Open with Chromium"), contextMenu);

                // Add the open with Chromium action above the back action.
                contextMenu->insertAction(webEnginePagePointer->action(QWebEnginePage::Back), openWithChromiumActionPointer);

                // Connect the action.
                connect(openWithChromiumActionPointer, SIGNAL(triggered()), this, SLOT(openWithChromium()));
            }


            // Add a separator above the back action.
            contextMenu->insertSeparator(webEnginePagePointer->action(QWebEnginePage::Back));
        }
    }

    // Display the menu using the location in the context menu event.
    contextMenu->popup(contextMenuEvent->globalPos());
}

QWebEngineView* PrivacyWebEngineView::createWindow(QWebEnginePage::WebWindowType webWindowType)
{
    // Get a handle for the browser window.
    BrowserWindow *browserWindowPointer = qobject_cast<BrowserWindow*>(window());

    // Create the requested window type.
    switch (webWindowType)
    {
        case QWebEnginePage::WebBrowserTab:
        {
            if (manuallyOpeninginNewTab)
            {
                // Reset the manually opening in new tab flag.
                manuallyOpeninginNewTab = false;

                // Create the new tab and return the privacy WebEngine view pointer.  `true` removes the focus from the blank URL line edit.  `true` adds the new tab adjacent to the current tab.
                // The new privacy WebEngine view pointer is returned so it can be populated with the link from the context menu.
                return browserWindowPointer->tabWidgetPointer->addTab(true, true);
            }
            else
            {
                // The web page is trying to automatically open a new tab by setting the target to be "_blank".  Create a temporary WebEngine view and open it there.
                // The URL cannot be loaded into this privacy WebEngine view because it will clear the web history.
                // But the temporary WebEngine will capture the URL and then pass it back to this privacy WebEngine view.
                TemporaryWebEngineView *temporaryWebEngineViewPointer = new TemporaryWebEngineView(this);

                // Return the temporary WebEngine view.
                return temporaryWebEngineViewPointer;
            }
        }

        case QWebEnginePage::WebBrowserWindow:
        {
            // Create a new browser window.
            BrowserWindow *newBrowserWindowPointer = new BrowserWindow();

            // Show the new browser window.
            newBrowserWindowPointer->show();

            // The new privacy WebEngine view pointer is returned so it can be populated with the link from the context menu.
            return newBrowserWindowPointer->tabWidgetPointer->loadBlankInitialWebsite();
        }

        case QWebEnginePage::WebBrowserBackgroundTab:
        {
            // Create the new tab and return the privacy WebEngine view pointer.  `false` does not clear the URL line edit.  `true` adds the new tab adjacent to the current tab.
            // `true` creates a background tab.
            // The new privacy WebEngine view pointer is returned so it can be populated with the link from the context menu.
            return browserWindowPointer->tabWidgetPointer->addTab(false, true, true);
        }

        default:
        {
            // Return a null pointer for opening a web dialog.
            return nullptr;
        }
    }
}

void PrivacyWebEngineView::displayHttpPingDialog(const QString &httpPingUrl) const
{
    // Display the HTTP Ping blocked dialog.
    Q_EMIT displayHttpPingBlockedDialog(httpPingUrl);
}

void PrivacyWebEngineView::handleAuthenticationRequest(const QUrl &requestUrl, QAuthenticator *authenticatorPointer)
{
    // Only display the HTTP authentication dialog if it hasn't already been displayed three times for this URL.
    if (httpAuthenticationDialogsDisplayed < 3) {
        // Increment the HTTP authentication dialog display counter.
        ++httpAuthenticationDialogsDisplayed;

        // Instantiate an HTTP authentication dialog.
        HttpAuthenticationDialog *httpAuthenticationDialogPointer = new HttpAuthenticationDialog(parentWidget(), requestUrl, authenticatorPointer);

        // Display the dialog.  This must be `exec()` instead of `show()` so that the website doesn't proceed before populating the authentication pointer.
        httpAuthenticationDialogPointer->exec();
    }
}

void PrivacyWebEngineView::manualOpenInNewTab()
{
    // Set the manually opening in new tab flag.
    manuallyOpeninginNewTab = true;

    // Open the url in a new tab.
    triggerPageAction(QWebEnginePage::OpenLinkInNewTab);
}

void PrivacyWebEngineView::openWithChromium() const
{
    // Open the current URL in Chromium
    QProcess::startDetached(QLatin1String("chromium"), QStringList(hoveredLinkString));
}

void PrivacyWebEngineView::openWithFirefox() const
{
    // Open the current URL in Firefox.
    QProcess::startDetached(QLatin1String("firefox-esr"), QStringList(hoveredLinkString));
}

void PrivacyWebEngineView::removeCookieFromList(const QNetworkCookie &cookie) const
{
    //qDebug() << "Remove cookie:  " << cookie.toRawForm();

    // Remove the cookie from the list.
    cookieListPointer->remove(cookie);

    // Update the cookies action.
    Q_EMIT numberOfCookiesChanged(cookieListPointer->size());
}

void PrivacyWebEngineView::saveHoveredLink(const QString &hoveredLink)
{
    // Save the hovered link.
    hoveredLinkString = hoveredLink;
}

void PrivacyWebEngineView::storeRequest(RequestStruct *requestStructPointer)
{
    // Store the request struct in the list.
    requestsListPointer->append(requestStructPointer);

    // Track blocked requests.
    if (requestStructPointer->dispositionInt == FilterListHelper::BLOCKED)
    {
        // Update the individual filter list block counters.
        if (requestStructPointer->filterListTitle == QLatin1String("UltraPrivacy"))
            ++blockedRequestsVector[ULTRAPRIVACY];
        else if (requestStructPointer->filterListTitle == QLatin1String("UltraList"))
            ++blockedRequestsVector[ULTRALIST];
        else if (requestStructPointer->filterListTitle == QLatin1String("EasyPrivacy"))
            ++blockedRequestsVector[EASYPRIVACY];
        else if (requestStructPointer->filterListTitle == QLatin1String("EasyList"))
            ++blockedRequestsVector[EASYLIST];
        else if (requestStructPointer->filterListTitle == QLatin1String("Fanboy's Annoyance List"))
            ++blockedRequestsVector[FANBOYS_ANNOYANCE_LIST];

        // Update the total requests blocked counter.
        ++blockedRequestsVector[TOTAL];

        // Update the blocked requests action.
        Q_EMIT(requestBlocked(blockedRequestsVector));
    }
}

void PrivacyWebEngineView::storeUpdatedUrl(const QUrl &newUrl)
{
    // Store the new URL string (which changes when a new page is loaded as the current URL text.  This is important when loading a new tab from a link.  <https://redmine.stoutner.com/issues/1228>
    currentUrlText = newUrl.toString();
}

