// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qohoswebcomponentlistener.h"
#include "qohoswebresourceerror.h"
#include "qohoswebview_p.h"
#include "qohoswebviewcontroller.h"
#include <QtWebView/private/qwebviewfactory_p.h>
#include <QtCore/private/qohoslogger_p.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qjsonobject.h>
#include <QtGui/qpa/qplatformwindow_p.h>

QT_BEGIN_NAMESPACE

namespace {

class QOhosWebViewSettingPrivate : public QWebViewSettingsPrivate
{
public:
    explicit QOhosWebViewSettingPrivate(std::shared_ptr<QOhosWebViewController> controller);

private:
    bool doTestAttribute(WebAttribute attribute) const final;
    void doSetAttribute(WebAttribute attribute, bool value) final;

    std::shared_ptr<QOhosWebViewController> m_webViewController;
};

class QOhosWebViewPrivate : public QWebViewPrivate
{
    Q_OBJECT
public:
    explicit QOhosWebViewPrivate(QWebView *view);
    ~QOhosWebViewPrivate() override;

    QString httpUserAgent() const override;
    void setHttpUserAgent(const QString &httpUserAgent) override;
    QUrl url() const override;
    void setUrl(const QUrl &url) override;
    bool canGoBack() const override;
    bool canGoForward() const override;
    QString title() const override;
    int loadProgress() const override;
    bool isLoading() const override;

    void initialize(QObject *context) override;
    void setCookie(const QString &domain, const QString &name, const QString &value) override;
    void deleteCookie(const QString &domain, const QString &name) override;
    void deleteAllCookies() override;
    QWindow *nativeWindow() const override;

    void onErrorReceived(const QString &url, const WebResourceError &error);
    void onPageBegan(const QString &url);
    void onPageEnded(const QString &url);
    void onProgressChanged(int progress);
    void onTitleUpdated(const QString &title, bool isRealTitle);

public Q_SLOTS:
    void goBack() override;
    void goForward() override;
    void reload() override;
    void stop() override;
    void loadHtml(const QString &html, const QUrl &baseUrl) override;

protected:
    void runJavaScript(const QString &script,
                       const std::function<void(const QVariant &)> &resultCallback) override;
    QWebViewSettingsPrivate *settings() const override;

private:
    void emitLoadingChangeSignalAndUpdateLoadStatus(const QWebViewLoadingInfo &loadingInfo);
    void emitSyntheticLoadFailure(const QUrl &url, const QString &errorMessage);

    QPointer<QWindow> m_webViewWindow;
    std::shared_ptr<QOhosWebViewController> m_webViewController;
    std::shared_ptr<QOhosWebComponentListener> m_webComponentListener;

    int m_loadProgress = 0;
    QWebViewLoadingInfo::LoadStatus m_loadStatus = QWebViewLoadingInfo::LoadStatus::Stopped;
    QString m_url;
    QString m_title;
    bool m_loadFailed = false;
};

class QOhosWebComponentListenerImpl : public QOhosWebComponentListener
{
public:
    QOhosWebComponentListenerImpl(QPointer<QOhosWebViewPrivate> webViewPrivate);
    ~QOhosWebComponentListenerImpl();

    void onErrorReceived(const std::string &url, const WebResourceError &error) override;
    void onPageBegan(const std::string &url) override;
    void onPageEnded(const std::string &url) override;
    void onProgressChanged(int progress) override;
    void onTitleReceived(const std::string &title, bool isRealTitle) override;

private:
    QPointer<QOhosWebViewPrivate> m_webViewPrivate;
};

QOhosWebViewSettingPrivate::QOhosWebViewSettingPrivate(std::shared_ptr<QOhosWebViewController> controller)
    : m_webViewController(controller)
{
}

bool QOhosWebViewSettingPrivate::doTestAttribute(WebAttribute attribute) const
{
    return m_webViewController->testAttribute(attribute);
}

void QOhosWebViewSettingPrivate::doSetAttribute(WebAttribute attribute, bool value)
{
    m_webViewController->setAttribute(attribute, value);
}

QWindow *createEmbeddedWebViewWindow(
    std::shared_ptr<QOhosWebViewController> webViewController,
    std::shared_ptr<QOhosWebComponentListener> webComponentListener,
    QObject *webComponentListenerContext)
{
    auto webViewNode = webViewController->createEmbeddedWebComponentNodeOrFail(
        webComponentListener, webComponentListenerContext);

    return QNativeInterface::Private::QOhosWindow::fromNative(webViewNode);
}

QVariant convertJavaScriptResultToQVariantOrNull(std::optional<std::string> javaScriptResult)
{
    if (!javaScriptResult.has_value())
        return QVariant();

    const QByteArray jsonData = "{ \"data\": " + QByteArray(javaScriptResult.value().c_str()) + " }";

    QJsonParseError parseError;
    const auto jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
    if (parseError.error != QJsonParseError::NoError) {
        qOhosCritical(QtForOhos)
            << Q_FUNC_INFO
            << "Failed to parse javaScript result, reason:"
            << parseError.errorString();
        return QVariant();
    }

    return jsonDoc.isObject()
        ? jsonDoc.object()[QStringLiteral("data")].toVariant()
        : QVariant();
}

std::string cookieUrlFromDomain(const QString &domain)
{
    QString host = domain;
    if (host.startsWith(u'.'))
        host.remove(0, 1);
    if (!host.contains(QLatin1String("://")))
        host.prepend(QLatin1String("https://"));
    return host.toStdString();
}

bool cookieHeaderHasName(const QString &cookieHeader, const QString &name)
{
    const QStringList parts = cookieHeader.split(u';', Qt::SkipEmptyParts);
    for (const QString &part : parts) {
        const QString token = part.trimmed();
        const qsizetype eq = token.indexOf(u'=');
        const QString key = (eq < 0 ? token : token.left(eq)).trimmed();
        if (key == name)
            return true;
    }
    return false;
}

QOhosWebViewPrivate::QOhosWebViewPrivate(QWebView *view)
    : QWebViewPrivate(view)
    , m_webViewController(makeOhosWebViewController())
{
    m_webComponentListener = std::make_shared<QOhosWebComponentListenerImpl>(this);
    m_webViewWindow = createEmbeddedWebViewWindow(m_webViewController, m_webComponentListener, this);
    if (m_webViewWindow) {
        m_webViewController->bindQrcSchemeHandler();
        m_webViewWindow->setParent(view);
        connect(view, &QWindow::widthChanged, m_webViewWindow.data(), &QWindow::setWidth);
        connect(view, &QWindow::heightChanged, m_webViewWindow.data(), &QWindow::setHeight);
        connect(view, &QWindow::visibleChanged, m_webViewWindow.data(), &QWindow::setVisible);
    }
}

QOhosWebViewPrivate::~QOhosWebViewPrivate() = default;

QString QOhosWebViewPrivate::httpUserAgent() const
{
    return convertJavaScriptResultToQVariantOrNull(
        m_webViewController->tryRunJavaScript("navigator.userAgent")).toString();
}

void QOhosWebViewPrivate::setHttpUserAgent(const QString &httpUserAgent)
{
    if (m_webViewController->trySetCustomUserAgent(httpUserAgent.toStdString()))
        Q_EMIT q_ptr->httpUserAgentStringChanged(httpUserAgent);
}

QUrl QOhosWebViewPrivate::url() const
{
    auto controllerUrl = QString::fromStdString(m_webViewController->getUrl());

    return QFileInfo::exists(controllerUrl)
        ? QUrl::fromLocalFile(controllerUrl)
        : QUrl::fromEncoded(controllerUrl.toUtf8());
}

void QOhosWebViewPrivate::setUrl(const QUrl &url)
{
    auto controllerUrl = url.isLocalFile()
        ? QFileInfo(url.toLocalFile()).absoluteFilePath().toStdString()
        : url.toString().toStdString();

    if (!m_webViewController->tryLoadUrl(controllerUrl)) {
        qOhosWarning(QtForOhos) << Q_FUNC_INFO << "Failed to set url:" << controllerUrl.c_str();
        emitSyntheticLoadFailure(
            url, QStringLiteral("Failed to load: %1").arg(url.toString()));
    }
}

void QOhosWebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl)
{
    const std::string mimeType = "text/html;charset=UTF-8";
    const std::string encoding = "UTF-8";

    bool ok;
    if (baseUrl.isEmpty() || baseUrl.scheme() == QLatin1String("data")) {
        const std::string data = QUrl::toPercentEncoding(html).toStdString();
        ok = m_webViewController->tryLoadHtml(data, mimeType, encoding, {}, {});
    } else {
        const std::string base = baseUrl.toString().toStdString();
        ok = m_webViewController->tryLoadHtml(html.toStdString(), mimeType, encoding, base, base);
    }
    if (!ok)
        qOhosWarning(QtForOhos) << Q_FUNC_INFO << "Failed to load html:" << html;
}

bool QOhosWebViewPrivate::canGoBack() const
{
    return m_webViewController->canGoBack();
}

void QOhosWebViewPrivate::goBack()
{
    m_webViewController->goBack();
}

bool QOhosWebViewPrivate::canGoForward() const
{
    return m_webViewController->canGoForward();
}

void QOhosWebViewPrivate::goForward()
{
    m_webViewController->goForward();
}

void QOhosWebViewPrivate::reload()
{
    m_webViewController->refresh();
}

QString QOhosWebViewPrivate::title() const
{
    return m_title;
}

void QOhosWebViewPrivate::runJavaScript(const QString &script,
                                        const std::function<void(const QVariant &)> &resultCallback)
{
    auto result = convertJavaScriptResultToQVariantOrNull(m_webViewController->tryRunJavaScript(script.toStdString()));
    if (resultCallback)
        resultCallback(result);
}

QWebViewSettingsPrivate *QOhosWebViewPrivate::settings() const
{
    return new QOhosWebViewSettingPrivate(m_webViewController);
}

int QOhosWebViewPrivate::loadProgress() const
{
    return m_loadProgress;
}

bool QOhosWebViewPrivate::isLoading() const
{
    return m_loadStatus == QWebViewLoadingInfo::LoadStatus::Started;
}

void QOhosWebViewPrivate::initialize(QObject *context)
{
    Q_UNUSED(context);
}

void QOhosWebViewPrivate::setCookie(const QString &domain, const QString &name, const QString &value)
{
    const std::string url = cookieUrlFromDomain(domain);
    const std::string cookie = QStringLiteral("%1=%2").arg(name, value).toStdString();

    if (m_webViewController->trySetCookie(url, cookie))
        Q_EMIT q_ptr->cookieAdded(domain, name);
}

void QOhosWebViewPrivate::deleteCookie(const QString &domain, const QString &name)
{
    const std::string url = cookieUrlFromDomain(domain);

    const std::optional<std::string> existing = m_webViewController->tryFetchCookie(url);
    if (!existing || !cookieHeaderHasName(QString::fromStdString(*existing), name))
        return;

    const std::string expired = QStringLiteral("%1=; Max-Age=0; Path=/").arg(name).toStdString();
    if (m_webViewController->trySetCookie(url, expired))
        Q_EMIT q_ptr->cookieRemoved(domain, name);
}

void QOhosWebViewPrivate::deleteAllCookies()
{
    const auto cookies = m_webViewController->fetchAllCookies();
    if (!m_webViewController->tryClearAllCookies())
        return;

    for (const auto &[domain, name] : cookies)
        Q_EMIT q_ptr->cookieRemoved(QString::fromStdString(domain), QString::fromStdString(name));
}

QWindow *QOhosWebViewPrivate::nativeWindow() const
{
    return m_webViewWindow.data();
}

void QOhosWebViewPrivate::stop()
{
    m_webViewController->stop();

    QMetaObject::invokeMethod(
        this,
        [this]() {
            emitLoadingChangeSignalAndUpdateLoadStatus(
                QWebViewFactory::LoadingInfo::create(QUrl(m_url), QWebViewLoadingInfo::LoadStatus::Stopped, QString()));
        },
        Qt::QueuedConnection);
}

void QOhosWebViewPrivate::onErrorReceived(const QString &url, const WebResourceError &error)
{
    auto errorMessage =
        QString(QLatin1String("Error %1: '%3'")).arg(error.errorCode()).arg(error.errorInfo().c_str());

    if (m_url != url) {
        qOhosWarning(QtForOhos)
            << Q_FUNC_INFO << "Received onErrorReceive event on other url:" << url << errorMessage;
        return;
    }

    qOhosCritical(QtForOhos) << Q_FUNC_INFO << "Received onErrorReceive for main page:" << errorMessage;

    m_loadFailed = true;
    emitLoadingChangeSignalAndUpdateLoadStatus(
        QWebViewFactory::LoadingInfo::create(QUrl(m_url), QWebViewLoadingInfo::LoadStatus::Failed, errorMessage));
}

void QOhosWebViewPrivate::onPageBegan(const QString &url)
{
    m_url = url;
    m_loadFailed = false;

    Q_EMIT q_ptr->urlChanged(QUrl(url));
    emitLoadingChangeSignalAndUpdateLoadStatus(
        QWebViewFactory::LoadingInfo::create(QUrl(url), QWebViewLoadingInfo::LoadStatus::Started, QString()));
    onProgressChanged(0);
}

void QOhosWebViewPrivate::onPageEnded(const QString &url)
{
    if (m_loadFailed) {
        qOhosWarning(QtForOhos)
            << Q_FUNC_INFO
            << "Skipping LoadSucceededStatus; load already failed, url:" << url;
        return;
    }

    if (m_url != url) {
        qOhosWarning(QtForOhos) << Q_FUNC_INFO << "Received onPageEnd event from other url:" << url;
        return;
    }

    emitLoadingChangeSignalAndUpdateLoadStatus(
        QWebViewFactory::LoadingInfo::create(QUrl(url), QWebViewLoadingInfo::LoadStatus::Succeeded, QString()));
}

void QOhosWebViewPrivate::onProgressChanged(int progress)
{
    if (m_loadProgress == progress) {
        return;
    }

    m_loadProgress = progress;
    Q_EMIT q_ptr->loadProgressChanged(progress);
}

void QOhosWebViewPrivate::onTitleUpdated(const QString &title, bool isRealTitle)
{
    if (!isRealTitle) {
        qOhosDebug(QtForOhos) << Q_FUNC_INFO << "Ignoring URL-generated title:" << title;
        return;
    }

    if (m_title == title)
        return;

    m_title = title;
    Q_EMIT q_ptr->titleChanged(title);
}

void QOhosWebViewPrivate::emitLoadingChangeSignalAndUpdateLoadStatus(
    const QWebViewLoadingInfo &loadingInfo)
{
    m_loadStatus = loadingInfo.status();
    Q_EMIT q_ptr->loadingChanged(loadingInfo);
}

void QOhosWebViewPrivate::emitSyntheticLoadFailure(const QUrl &url, const QString &errorMessage)
{
    QMetaObject::invokeMethod(
        this,
        [this, url, errorMessage]() {
            m_url = url.toString();
            Q_EMIT q_ptr->urlChanged(url);
            emitLoadingChangeSignalAndUpdateLoadStatus(
                QWebViewFactory::LoadingInfo::create(
                    url, QWebViewLoadingInfo::LoadStatus::Started, QString()));
            emitLoadingChangeSignalAndUpdateLoadStatus(
                QWebViewFactory::LoadingInfo::create(
                    url, QWebViewLoadingInfo::LoadStatus::Failed, errorMessage));
        },
        Qt::QueuedConnection);
}

QOhosWebComponentListenerImpl::QOhosWebComponentListenerImpl(QPointer<QOhosWebViewPrivate> webViewPrivate)
    : QOhosWebComponentListener()
    , m_webViewPrivate(webViewPrivate)
{
}

QOhosWebComponentListenerImpl::~QOhosWebComponentListenerImpl() = default;

void QOhosWebComponentListenerImpl::onErrorReceived(const std::string &url, const WebResourceError &error)
{
    if (m_webViewPrivate)
        m_webViewPrivate->onErrorReceived(QString::fromStdString(url), error);
}

void QOhosWebComponentListenerImpl::onPageBegan(const std::string &url)
{
    if (m_webViewPrivate)
        m_webViewPrivate->onPageBegan(QString::fromStdString(url));
}

void QOhosWebComponentListenerImpl::onPageEnded(const std::string &url)
{
    if (m_webViewPrivate)
        m_webViewPrivate->onPageEnded(QString::fromStdString(url));
}

void QOhosWebComponentListenerImpl::onProgressChanged(int progress)
{
    if (m_webViewPrivate)
        m_webViewPrivate->onProgressChanged(progress);
}

void QOhosWebComponentListenerImpl::onTitleReceived(const std::string &title, bool isRealTitle)
{
    if (m_webViewPrivate)
        m_webViewPrivate->onTitleUpdated(QString::fromStdString(title), isRealTitle);
}

}

QWebViewPrivate *makeOhosWebView(QWebView *view)
{
    return new QOhosWebViewPrivate(view);
}

void prepareOhosWebView()
{
    initializeOhosWebEngine();
}

QT_END_NAMESPACE

#include "qohoswebview.moc"
