/*
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2008 Dirk Mueller <mueller@kde.org>
    SPDX-FileCopyrightText: 2008 Urs Wolfer <uwolfer @ kde.org>
    SPDX-FileCopyrightText: 2009 Dawit Alemayehu <adawit@kde.org>

    SPDX-License-Identifier: LGPL-2.1-or-later
*/

#ifndef WEBENGINEPAGE_H
#define WEBENGINEPAGE_H

#include "websslinfo.h"
#include "kwebenginepartlib_export.h"
#include "browserextension.h"
#include "webenginepartdownloadmanager.h"

#include <kwebenginepartlib_export.h>

#include <QWebEnginePage>
#include <QUrl>
#include <QMultiHash>
#include <QPointer>
#include <QScopedPointer>
#include <QWebEngineFullScreenRequest>
#include <QWebEngineDownloadRequest>
#include <QWebEngineNavigationRequest>
#include <QWebEngineLoadingInfo>

class QAuthenticator;
class WebSslInfo;
class WebEnginePart;
class KPasswdServerClient;
class WebEngineWallet;
class WebEngineDownloadJob;
class WebEnginePartControls;

class KWEBENGINEPARTLIB_EXPORT  WebEnginePage : public QWebEnginePage
{
    Q_OBJECT
public:
    explicit WebEnginePage(WebEnginePart *wpart, QWidget *parent = nullptr);
    ~WebEnginePage() override;

    /**
     * Returns the SSL information for the current page.
     *
     * @see WebSslInfo.
     */
    const WebSslInfo& sslInfo() const;

    /**
     * Sets the page's SSL information to @p other.
     *
     * @see WebSslInfo
     */
    void setSslInfo (const WebSslInfo &other);

    void requestDownload(QWebEngineDownloadRequest *item, bool newWindow, WebEnginePartDownloadManager::DownloadObjective objective);

    void setStatusBarText(const QString &text);

    /**
     * @brief Sets the webengine part to be used by this object.
     * @param part the part
     */
    void setPart(WebEnginePart *part);

    /**
     * @brief Informs the page that a drop operation has been started
     *
     * Calling this method changes the behavior of createNewWindow()
     * @note Since `QWebEngineView` doesn't provide a way to tell when a drop operation actually end,
     * there's no function to inform that the drop operation has ended. The page considers the
     * drop operation to have ended when one the following happens:
     * - createNewWindow() is called
     * - the `loadStarted` signal is emitted
     * - 100ms have elapsed since the last call to this method (this time interval has been chosen arbitrary: it should
     *   be enough for the drop operation to have actually ended but short enough to make it unlikely that
     *   the user has started another action)
     * @see m_dropOperationTimer
     * @see createWindow()
     * @see WebEngineView::dropEvent
     */
    void setDropOperationStarted();

    static bool allowLifeycleStateManagement(bool allow);

    QWidget* view() const;

    /**
     * @brief Execute a function considering all requests to open new windows as being user-initiated
     *
     * @warning This function should *only be used from tests* when the only way of clicking on links is
     * using javascript, which makes WebEnginePage consider them not being user-initiated.
     *
     * @param fnc the function to execute while considering all requests to open new window to be user-initiated
     * @see
     */
    void assumingNewWindowRequestsAreUserInitiated(const std::function<void()> &fnc);

Q_SIGNALS:
    /**
     * This signal is emitted whenever a user cancels/aborts a load resource
     * request.
     */
    void loadAborted(const QUrl &url);

    void mainFrameNavigationRequested(WebEnginePage *page, const QUrl);

    /**
     * @brief Signal emitted when a page starts navigating to another URL
     *
     * It has a similar function to `QWebEnginePage::loadStarted()` but, unlike it,
     * it's not emitted if the URL will be downloaded.
     *
     * @param url the URL of the page which has started loading
     *
     * @warning Since `QWebEnginePage` doesn't provide a way to determine if an URL
     * will be downloaded or opened in this page before the `loadFinished()` signal
     * is emitted, WebEnginePage uses a heuristic algorithm to determine whether to emit
     * it or not:
     * - when the `loadStarted()` signal is emitted, it starts a single-shot timer with a
     * short interval (100ms)
     * - if before the timer times out a download for the URL starts, then this signal isn't emitted
     * and the timer is stopped
     * - if the timer times out or the `urlChanged()` signal is emitted or the `loadFinished()`
     * with a `true` argument, then this signal is emitted (and the timer stopped if still running)
     *
     * There might be situations when this algorithm fails: unfortunately, there aren't better
     * ways to determine whether the URL will be downloaded or not
     */
    void navigationStarted(const QUrl &url);

protected:
    /**
     * Returns the webengine part in use by this object.
     * @internal
     */
    WebEnginePart* part() const;

protected:

    /**
     * Reimplemented for internal reasons, the API is not affected.
     * @internal
     */
    // bool acceptNavigationRequest(const QUrl& request, NavigationType type, bool isMainFrame) override;

    /**
     * @brief Helper function to easily access the single WebEnginePartControls instance
     *
     * This is a shortcut for `WebEnginePartControls::self()`
     *
     * @return the single WebEnginePartControls instance
     */
    WebEnginePartControls* controls() const;

protected Q_SLOTS:
    void slotLoadFinished(bool ok);
    virtual void slotGeometryChangeRequested(const QRect& rect);
    void slotFeaturePermissionRequested(const QUrl& url, QWebEnginePage::Feature feature);
    void slotAuthenticationRequired(const QUrl &requestUrl, QAuthenticator *auth);
    void changeFullScreenMode(QWebEngineFullScreenRequest req);

    /**
     * @brief Changes the life cycle state of the page
     *
     * This will make the page either `Active` or `Frozen` (it's never `Discarded`).
     * A page will be made `Frozen` only if:
     * - @p recommendedState is't `Active`
     * - the page is not visible
     * - the page is not loading (according to #m_loadStatus)
     * - life cycle state management is enabled (according to WebEnginePartControls::isPageLifecycleManagementEnabled()
     *
     * @param recommendedState the life cycle state recommended by `QWebEnginePage`
     */
    void changeLifecycleState(QWebEnginePage::LifecycleState recommendedState);
#ifdef MEDIAREQUEST_SUPPORTED
    void chooseDesktopMedia(const QWebEngineDesktopMediaRequest &request);
#endif

    /**
     * @brief Slot connected to the `newWindowRequested()` signal
     *
     * It asks the browser to create a new window.
     *
     * If a drag & drop operation is happening (meaning that #m_dropOperationTimer is active)
     * this assumes that the drop operation should be concluded in this page, so that
     * the dropped URL will be opened in this page and no new window will be created.
     *
     * @note This slot doesn't call `QWebEngineNewWindowRequest::openIn()` because the slot
     * connected to this signal (KonqMainWindow::slotCreateNewWindow()) already calls
     * KonqMainWindow::openUrl() on the new window. This isn't optimal, but it's the
     * simplest way to avoid duplicating calls to KonqMainWindow::openUrl().
     */
    void createNewWindow(QWebEngineNewWindowRequest &req);

    void print();
#ifdef WEBENGINE_FRAMES_SUPPORTED
    void printFrame(QWebEngineFrame frame);
#endif

    /**
    * @brief Handles a certificate error
    * @see CertificateErrorDialogManager::handleCertificateError
    *
    * @param _ce the certificate error
    */
    void handleCertificateError(const QWebEngineCertificateError &ce);

    void handleNavigationRequest(QWebEngineNavigationRequest &req);

    /**
     * @brief Updates the stylesheet applied to the page
     *
     * Since `QtWebEngine` doesn't directly support custom stylesheets, this is done using javascript
     * @param script the script to run to update the stylesheet
     * @see WebEnginePartControls::updateUserStyleSheet
     * @see WebEnginePartControls::updateStyleSheet
     */
    void updateUserStyleSheet(const QString &script);

private:
    bool checkFormData(const QUrl& url) const;
    bool handleMailToUrl (const QUrl& , QWebEngineNavigationRequest::NavigationType type) const;
    void setPageJScriptPolicy(const QUrl& url);
    bool askBrowserToOpenUrl(const QUrl &url, const QString &mimetype=QString(), const KParts::OpenUrlArguments &args = KParts::OpenUrlArguments(), const BrowserArguments &bargs = BrowserArguments());
    bool downloadWithExternalDonwloadManager(const QUrl &url);
    /**
     * @brief Decide whether the page is allowed to open a new window requested by javascript or not
     * @param url the URL to load in the new window
     * @return `true` if the page is allowed to open the new window and `false` if it isn't
     */
    bool decideHandlingOfJavascripWindow(const QUrl url) const;

    /**
     * @brief Whether WebEnginePart should open an URL by itself or delegate the main application
     *
     * This tries to take into account the user preferences and it's needed, for example, to ensure that
     * the link "Home Folder" in the introduction page is displayed in the part the user choose for directories
     * rather than in WebEnginePart.
     *
     * This function is meant to be used by acceptNavigationRequest(), which unfortunately means it doesn't know
     * the URL mimetype. Because of this, only preferences related to the URL's scheme can be applied. In particular:
     * - if the URL has the `file` scheme, its mimetype is detected using QMimeTypeDatabase and the appropriate
     *  preferences are enforced
     * - if the URL has the `trash` or `remote` schemes, it is handled by the application
     * @param url the URL to open
     * @return `true` if WebEnginePart should open the URL and `false` if it should let the application open it
     */
    bool shouldOpenUrl(const QUrl &url) const;

private:
    enum WebEnginePageSecurity { PageUnencrypted, PageEncrypted, PageMixed };

    int m_kioErrorCode;
    bool m_ignoreError;

    WebSslInfo m_sslInfo;
    QPointer<WebEnginePart> m_part;

    QScopedPointer<KPasswdServerClient> m_passwdServerClient;

    /**
     * @brief Timer used to decide whether a drop operation is happening
     *
     * A drop operation is happening if this timer is active
     * @see setDropOperationStarted for more details
     */
    QTimer *m_dropOperationTimer;

    QMultiHash<QUrl, QWebEngineDownloadRequest*> m_downloadItems;

    /**
     * @brief The loading status of the page
     *
     * This is needed in changeLifecycleState() to know whether a page is being loaded,
     * since `QWebEnginePage::isLoading()` sometimes returns `false` even if the
     * page is loading
     */
    QWebEngineLoadingInfo::LoadStatus m_loadStatus;

    /**
     * @brief Helper struct which encapsulates data needed to correctly emit the navigationStarted() signal
     *
     * It manages the timer used to determine whether the page should emit the WebEnginePage::navigationStarted()
     * signal and stores the URL associated with the last navigation request.
     *
     * @see WebEnginePage::navigationStarted()
     */
    class NavigationStartedHelper
    {
    public:

        /**
         * @brief Constructor
         * @param page the page this object refers to
         */
        NavigationStartedHelper(WebEnginePage *page);

        ~NavigationStartedHelper(); ///!< Destructor

        /**
         * @brief Starts the timer and changes the URL associated with it
         *
         * This should be called in response to the `QWebEnginePage::loadingChanged()` signal
         * when the status is `QWebEngineLoadingInfo::LoadStartedStatus`.
         *
         * @param newUrl the URL which starts being loaded
         */
        void start(const QUrl &newUrl);

        /**
         * @brief Stops the timer if it's running
         *
         * This will also clear the URL associated with the timer.
         *
         * If @p emitSignal is `true` and the timer was running, the page's WebEnginePage::navigationStarted()
         * signal is emitted with the URL associated with the timer as parameter.
         *
         * @param emitSignal whether or not to have the page emit the WebEnginePage::navigationStarted()
         * after stopping the timer (only if the timer was actually running)
         */
        void stop(bool emitSignal);

    private:
        static constexpr std::chrono::duration<int, std::milli> s_timerInterval{100}; //!< The timer timeout
        QPointer<WebEnginePage> m_page; //!< The page associated with this object
        QTimer *m_timer; //!< The timer
        QUrl m_url; //!< The URL associated with the timer
    };

    NavigationStartedHelper m_navigationStartedHelper;

    /**
     * @brief Makes createNewWindow() behave as requests to create new windows are always user-initiated
     *
     * @warning This should only used in tests
     * @warning This variable should only be set by assumingNewWindowRequestsAreUserInitiated()
     *
     * When this is true, createNewWindow() behaves as if `QWebEngineNewWindowRequest::isUserInitiated()`
     * returned `true`
     */
    bool m_assumeNewWindowRequestsUserInitiated = false;
};

#endif // WEBENGINEPAGE_H

