// Copyright (C) 2015 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 "qwinrtwebview_p.h"
#include <private/qwebviewloadrequest_p.h>

#include <functional>

#include <QPointer>
#include <QHash>
#include <QRegularExpression>
#include <QScreen>
#include <qfunctions_winrt.h>
#include <private/qeventdispatcher_winrt_p.h>
#include <private/qhighdpiscaling_p.h>

#include <wrl.h>
#include <windows.graphics.display.h>
#include <windows.system.h>
#include <windows.ui.xaml.h>
#include <windows.ui.xaml.controls.h>
#include <windows.ui.xaml.markup.h>
#include <windows.web.h>
#include <urlmon.h>
#include <WinInet.h>

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Graphics::Display;
using namespace ABI::Windows::System;
using namespace ABI::Windows::UI;
using namespace ABI::Windows::UI::Xaml;
using namespace ABI::Windows::UI::Xaml::Controls;
using namespace ABI::Windows::UI::Xaml::Markup;
using namespace ABI::Windows::Web;

class HStringListIterator : public RuntimeClass<RuntimeClassFlags<WinRt>, IIterator<HSTRING>>
{
public:
    HStringListIterator(HSTRING *data, int size) : data(data), size(size), pos(0)
    {
    }

    HRESULT __stdcall get_Current(HSTRING *current) override
    {
        *current = pos >= size ? nullptr : data[pos];
        return S_OK;
    }

    HRESULT __stdcall get_HasCurrent(boolean *hasCurrent) override
    {
        *hasCurrent = pos < size;
        return S_OK;
    }

    HRESULT __stdcall MoveNext(boolean *hasCurrent) override
    {
        *hasCurrent = ++pos < size;
        return S_OK;
    }

    HRESULT __stdcall GetMany(unsigned capacity, HSTRING *value, unsigned *actual)
    {
        unsigned i = 0;
        for (; i < qMin(capacity, unsigned(size)); ++i)
            value[i] = data[pos + i];
        *actual = i;
        pos += i;
        return S_OK;
    }

private:
    HSTRING *data;
    int size;
    int pos;
};

class HStringList : public RuntimeClass<RuntimeClassFlags<WinRt>, IIterable<HSTRING>>
{
public:
    HStringList(const QList<QString> &stringList)
    {
        d.resize(stringList.size());
        for (int i = 0; i < stringList.size(); ++i) {
            const QString qString = stringList.at(i).trimmed();
            HStringReference hString(reinterpret_cast<const wchar_t *>(qString.utf16()), qString.length());
            hString.CopyTo(&d[i++]);
        }
    }

    ~HStringList()
    {
        for (const HSTRING &hString : std::as_const(d))
            WindowsDeleteString(hString);
    }

    HRESULT __stdcall First(IIterator<HSTRING> **first) override
    {
        ComPtr<HStringListIterator> it = Make<HStringListIterator>(d.data(), d.size());
        return it.Get()->QueryInterface(IID_PPV_ARGS(first));
    }

private:
    QList<HSTRING> d;
};

static QUrl qurlFromUri(IUriRuntimeClass *uri)
{
    HRESULT hr;
    HString uriString;
    hr = uri->get_AbsoluteUri(uriString.GetAddressOf());
    Q_ASSERT_SUCCEEDED(hr);
    quint32 uriStringLength;
    const wchar_t *uriStringBuffer = uriString.GetRawBuffer(&uriStringLength);
    return QUrl(QString::fromWCharArray(uriStringBuffer, uriStringLength));
}

static QString webErrorStatusString(WebErrorStatus status)
{
    switch (status) {
    case WebErrorStatus_Unknown:
        return QStringLiteral("Unknown");
    case WebErrorStatus_CertificateCommonNameIsIncorrect:
        return QStringLiteral("CertificateCommonNameIsIncorrect");
    case WebErrorStatus_CertificateExpired:
        return QStringLiteral("CertificateExpired");
    case WebErrorStatus_CertificateContainsErrors:
        return QStringLiteral("CertificateContainsErrors");
    case WebErrorStatus_CertificateRevoked:
        return QStringLiteral("CertificateRevoked");
    case WebErrorStatus_CertificateIsInvalid:
        return QStringLiteral("CertificateIsInvalid");
    case WebErrorStatus_ServerUnreachable:
        return QStringLiteral("ServerUnreachable");
    case WebErrorStatus_Timeout:
        return QStringLiteral("Timeout");
    case WebErrorStatus_ErrorHttpInvalidServerResponse:
        return QStringLiteral("ErrorHttpInvalidServerResponse");
    case WebErrorStatus_ConnectionAborted:
        return QStringLiteral("ConnectionAborted");
    case WebErrorStatus_ConnectionReset:
        return QStringLiteral("ConnectionReset");
    case WebErrorStatus_Disconnected:
        return QStringLiteral("Disconnected");
    case WebErrorStatus_HttpToHttpsOnRedirection:
        return QStringLiteral("HttpToHttpsOnRedirection");
    case WebErrorStatus_HttpsToHttpOnRedirection:
        return QStringLiteral("HttpsToHttpOnRedirection");
    case WebErrorStatus_CannotConnect:
        return QStringLiteral("CannotConnect");
    case WebErrorStatus_HostNameNotResolved:
        return QStringLiteral("HostNameNotResolved");
    case WebErrorStatus_OperationCanceled:
        return QStringLiteral("OperationCanceled");
    case WebErrorStatus_RedirectFailed:
        return QStringLiteral("RedirectFailed");
    case WebErrorStatus_UnexpectedStatusCode:
        return QStringLiteral("UnexpectedStatusCode");
    case WebErrorStatus_UnexpectedRedirection:
        return QStringLiteral("UnexpectedRedirection");
    case WebErrorStatus_UnexpectedClientError:
        return QStringLiteral("UnexpectedClientError");
    case WebErrorStatus_UnexpectedServerError:
        return QStringLiteral("UnexpectedServerError");
    case WebErrorStatus_MultipleChoices:
        return QStringLiteral("MultipleChoices");
    case WebErrorStatus_MovedPermanently:
        return QStringLiteral("MovedPermanently");
    case WebErrorStatus_Found:
        return QStringLiteral("Found");
    case WebErrorStatus_SeeOther:
        return QStringLiteral("SeeOther");
    case WebErrorStatus_NotModified:
        return QStringLiteral("NotModified");
    case WebErrorStatus_UseProxy:
        return QStringLiteral("UseProxy");
    case WebErrorStatus_TemporaryRedirect:
        return QStringLiteral("TemporaryRedirect");
    case WebErrorStatus_BadRequest:
        return QStringLiteral("BadRequest");
    case WebErrorStatus_Unauthorized:
        return QStringLiteral("Unauthorized");
    case WebErrorStatus_PaymentRequired:
        return QStringLiteral("PaymentRequired");
    case WebErrorStatus_Forbidden:
        return QStringLiteral("Forbidden");
    case WebErrorStatus_NotFound:
        return QStringLiteral("NotFound");
    case WebErrorStatus_MethodNotAllowed:
        return QStringLiteral("MethodNotAllowed");
    case WebErrorStatus_NotAcceptable:
        return QStringLiteral("NotAcceptable");
    case WebErrorStatus_ProxyAuthenticationRequired:
        return QStringLiteral("ProxyAuthenticationRequired");
    case WebErrorStatus_RequestTimeout:
        return QStringLiteral("RequestTimeout");
    case WebErrorStatus_Conflict:
        return QStringLiteral("Conflict");
    case WebErrorStatus_Gone:
        return QStringLiteral("Gone");
    case WebErrorStatus_LengthRequired:
        return QStringLiteral("LengthRequired");
    case WebErrorStatus_PreconditionFailed:
        return QStringLiteral("PreconditionFailed");
    case WebErrorStatus_RequestEntityTooLarge:
        return QStringLiteral("RequestEntityTooLarge");
    case WebErrorStatus_RequestUriTooLong:
        return QStringLiteral("RequestUriTooLong");
    case WebErrorStatus_UnsupportedMediaType:
        return QStringLiteral("UnsupportedMediaType");
    case WebErrorStatus_RequestedRangeNotSatisfiable:
        return QStringLiteral("RequestedRangeNotSatisfiable");
    case WebErrorStatus_ExpectationFailed:
        return QStringLiteral("ExpectationFailed");
    case WebErrorStatus_InternalServerError:
        return QStringLiteral("InternalServerError");
    case WebErrorStatus_NotImplemented:
        return QStringLiteral("NotImplemented");
    case WebErrorStatus_BadGateway:
        return QStringLiteral("BadGateway");
    case WebErrorStatus_ServiceUnavailable:
        return QStringLiteral("ServiceUnavailable");
    case WebErrorStatus_GatewayTimeout:
        return QStringLiteral("GatewayTimeout");
    case WebErrorStatus_HttpVersionNotSupported:
        return QStringLiteral("HttpVersionNotSupported");
    default:
        break;
    }
    return QString();
}

struct WinRTWebView
{
    ComPtr<IWebView> base;
    ComPtr<IWebView2> ext;
    ComPtr<IPanel> host;

    ComPtr<ICanvasStatics> canvas;
    ComPtr<IUriRuntimeClassFactory> uriFactory;
    ComPtr<IDisplayInformation> displayInformation;
    ComPtr<ILauncherStatics> launcherStatics;

    QPointer<QWindow> window;
    QHash<IAsyncOperation<HSTRING> *, int> callbacks;

    EventRegistrationToken navigationStartingToken;
    EventRegistrationToken navigationCompletedToken;
    EventRegistrationToken unviewableContentToken;
    bool isLoading : 1;
};

#define LSTRING(str) L#str
static const wchar_t webviewXaml[] = LSTRING(
<WebView xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
);

QWinRTWebViewPrivate::QWinRTWebViewPrivate(QObject *parent)
    : QAbstractWebView(parent), d(new WinRTWebView)
{
    d->isLoading = false;

    QEventDispatcherWinRT::runOnXamlThread([this]() {
        HRESULT hr;
        ComPtr<IXamlReaderStatics> xamlReader;
        hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_UI_Xaml_Markup_XamlReader).Get(),
                                    IID_PPV_ARGS(&xamlReader));
        Q_ASSERT_SUCCEEDED(hr);

        // Directly instantiating a WebView works, but it throws an exception
        // when navigating. Using a XamlReader appears to set it up properly.
        ComPtr<IInspectable> element;
        hr = xamlReader->Load(HString::MakeReference(webviewXaml).Get(), &element);
        Q_ASSERT_SUCCEEDED(hr);
        hr = element.As(&d->base);
        Q_ASSERT_SUCCEEDED(hr);
        hr = d->base.As(&d->ext);
        Q_ASSERT_SUCCEEDED(hr);
        hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_System_Launcher).Get(),
            IID_PPV_ARGS(&d->launcherStatics));
        Q_ASSERT_SUCCEEDED(hr);

        hr = d->ext->add_NavigationStarting(
                    Callback<ITypedEventHandler<WebView *, WebViewNavigationStartingEventArgs *>>(this, &QWinRTWebViewPrivate::onNavigationStarted).Get(),
                    &d->navigationStartingToken);
        Q_ASSERT_SUCCEEDED(hr);
        hr = d->ext->add_NavigationCompleted(
                    Callback<ITypedEventHandler<WebView *, WebViewNavigationCompletedEventArgs *>>(this, &QWinRTWebViewPrivate::onNavigationCompleted).Get(),
                    &d->navigationCompletedToken);
        Q_ASSERT_SUCCEEDED(hr);
        hr = d->ext->add_UnviewableContentIdentified(
                    Callback<ITypedEventHandler<WebView *, WebViewUnviewableContentIdentifiedEventArgs *>>(this, &QWinRTWebViewPrivate::onUnviewableContent).Get(),
                    &d->unviewableContentToken);
        Q_ASSERT_SUCCEEDED(hr);

        ComPtr<IDisplayInformationStatics> displayInformationStatics;
        hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Graphics_Display_DisplayInformation).Get(),
                                    IID_PPV_ARGS(&displayInformationStatics));
        Q_ASSERT_SUCCEEDED(hr);
        hr = displayInformationStatics->GetForCurrentView(&d->displayInformation);
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });

    HRESULT hr;
    hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_UI_Xaml_Controls_Canvas).Get(),
                                IID_PPV_ARGS(&d->canvas));
    Q_ASSERT_SUCCEEDED(hr);
    hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Foundation_Uri).Get(),
                                IID_PPV_ARGS(&d->uriFactory));
    Q_ASSERT_SUCCEEDED(hr);
}

QWinRTWebViewPrivate::~QWinRTWebViewPrivate()
{
    QEventDispatcherWinRT::runOnXamlThread([this]() {
        HRESULT hr;
        hr = d->ext->remove_NavigationStarting(d->navigationStartingToken);
        Q_ASSERT_SUCCEEDED(hr);
        hr = d->ext->remove_NavigationCompleted(d->navigationCompletedToken);
        Q_ASSERT_SUCCEEDED(hr);
        hr = d->ext->remove_UnviewableContentIdentified(d->unviewableContentToken);
        Q_ASSERT_SUCCEEDED(hr);

        ComPtr<IVector<UIElement *>> children;
        hr = d->host->get_Children(&children);
        Q_ASSERT_SUCCEEDED(hr);
        ComPtr<IUIElement> uiElement;
        hr = d->base.As(&uiElement);
        Q_ASSERT_SUCCEEDED(hr);
        quint32 index;
        boolean found;
        hr = children->IndexOf(uiElement.Get(), &index, &found);
        Q_ASSERT_SUCCEEDED(hr);
        if (found) {
            hr = children->RemoveAt(index);
            Q_ASSERT_SUCCEEDED(hr);
        }
        return hr;
    });
}

QString QWinRTWebViewPrivate::httpUserAgent() const
{
#ifdef QT_WINRT_URLMKGETSESSIONOPTION_NOT_AVAILABLE
    qWarning() << "Used Windows SDK version (" << QString::number(QT_UCRTVERSION) << ") does not "
                  "support getting or setting of the user agent property. Consider updating to a more recent Windows 10 "
                  "SDK (16299 or above).";
    return "";
#else
    char buffer[255];
    unsigned long stringLength;
    HRESULT hr = UrlMkGetSessionOption(0x10000001,&buffer,255,&stringLength,0);
    Q_ASSERT_SUCCEEDED(hr);
    return QString(buffer);
#endif
}

void QWinRTWebViewPrivate::setHttpUserAgent(const QString &userAgent)
{
#ifdef QT_WINRT_URLMKSETSESSIONOPTION_NOT_AVAILABLE
    Q_UNUSED(userAgent);
    qWarning() << "Used Windows SDK version (" << QString::number(QT_UCRTVERSION) << ") does not "
                  "support getting or setting of the user agent property. Consider updating to a more recent Windows 10 "
                  "SDK (16299 or above).";
#else
    HRESULT hr = UrlMkSetSessionOption(0x10000001,userAgent.toLocal8Bit().data(),userAgent.size(),0);
    Q_ASSERT_SUCCEEDED(hr);
    emit httpUserAgentChanged(userAgent);
#endif
}

void QWinRTWebViewPrivate::setUrl(const QUrl &url)
{
    QEventDispatcherWinRT::runOnXamlThread([this, &url]() {
        HRESULT hr;
        const QString urlString = url.toString();
        ComPtr<IUriRuntimeClass> uri;
        HStringReference uriString(reinterpret_cast<const wchar_t *>(urlString.utf16()),
                                   urlString.length());
        hr = d->uriFactory->CreateUri(uriString.Get(), &uri);
        Q_ASSERT_SUCCEEDED(hr);
        hr = d->base->Navigate(uri.Get());
        // Directly running into an abort means, that the URI is not supported. Ask the user what to do
        if (hr == E_ABORT) {
            ComPtr<IAsyncOperation<bool>> op;
            hr = d->launcherStatics->LaunchUriAsync(uri.Get(), &op);
        }
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
}

bool QWinRTWebViewPrivate::canGoBack() const
{
    boolean canGoBack;
    QEventDispatcherWinRT::runOnXamlThread([this, &canGoBack]() {
        HRESULT hr;
        hr = d->ext->get_CanGoBack(&canGoBack);
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
    return canGoBack;
}

bool QWinRTWebViewPrivate::canGoForward() const
{
    boolean canGoForward;
    QEventDispatcherWinRT::runOnXamlThread([this, &canGoForward]() {
        HRESULT hr;
        hr = d->ext->get_CanGoForward(&canGoForward);
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
    return canGoForward;
}

QString QWinRTWebViewPrivate::title() const
{
    HString title;
    QEventDispatcherWinRT::runOnXamlThread([this, &title]() {
        HRESULT hr;
        hr = d->ext->get_DocumentTitle(title.GetAddressOf());
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
    quint32 titleLength;
    const wchar_t *titleBuffer = title.GetRawBuffer(&titleLength);
    return QString::fromWCharArray(titleBuffer, titleLength);
}

int QWinRTWebViewPrivate::loadProgress() const
{
    return d->isLoading ? 0 : 100;
}

bool QWinRTWebViewPrivate::isLoading() const
{
    return d->isLoading;
}

QWindow *QWinRTWebViewPrivate::nativeWindow()
{
    return d->window;
}

void QWinRTWebViewPrivate::goBack()
{
    QEventDispatcherWinRT::runOnXamlThread([this]() {
        HRESULT hr;
        hr = d->ext->GoBack();
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
}

void QWinRTWebViewPrivate::goForward()
{
    QEventDispatcherWinRT::runOnXamlThread([this]() {
        HRESULT hr;
        hr = d->ext->GoForward();
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
}

void QWinRTWebViewPrivate::reload()
{
    QEventDispatcherWinRT::runOnXamlThread([this]() {
        HRESULT hr;
        hr = d->ext->Refresh();
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
}

void QWinRTWebViewPrivate::stop()
{
    QEventDispatcherWinRT::runOnXamlThread([this]() {
        HRESULT hr;
        hr = d->ext->Stop();
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
}

void QWinRTWebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl)
{
    if (!baseUrl.isEmpty())
        qWarning("Base URLs for loadHtml() are not supported under WinRT.");

    HRESULT hr;
    HStringReference htmlText(reinterpret_cast<const wchar_t *>(html.utf16()), html.length());
    hr = d->base->NavigateToString(htmlText.Get());
    Q_ASSERT_SUCCEEDED(hr);
}

void QWinRTWebViewPrivate::runJavaScriptPrivate(const QString &script, int callbackId)
{
    static QRegularExpression functionTemplate(QStringLiteral("^(.*)\\((.*)\\)[\\s;]*?$"));
    QRegularExpressionMatch match = functionTemplate.match(script);
    if (!match.hasMatch()) {
        qWarning("The WinRT WebView only supports calling global functions, so please make your call"
                 " in the form myFunction(a, b, c). Also note that only string arguments can be passed.");
        return;
    }

    const QString method = match.captured(1).trimmed();
    HStringReference methodString(reinterpret_cast<const wchar_t *>(method.utf16()), method.length());
    ComPtr<HStringList> argumentStrings = Make<HStringList>(match.captured(2).split(QLatin1Char(',')));
    QEventDispatcherWinRT::runOnXamlThread([this, &methodString, &argumentStrings, &callbackId]() {
        HRESULT hr;
        ComPtr<IAsyncOperation<HSTRING>> op;
        hr = d->ext->InvokeScriptAsync(methodString.Get(), argumentStrings.Get(), &op);
        Q_ASSERT_SUCCEEDED(hr);

        d->callbacks.insert(op.Get(), callbackId);
        hr = op->put_Completed(Callback<IAsyncOperationCompletedHandler<HSTRING>>([this](IAsyncOperation<HSTRING> *op, AsyncStatus status) {
            int callbackId = d->callbacks.take(op);
            HRESULT hr;
            if (status != Completed) {
                ComPtr<IAsyncInfo> info;
                hr = op->QueryInterface(IID_PPV_ARGS(&info));
                Q_ASSERT_SUCCEEDED(hr);
                HRESULT errorCode;
                hr = info->get_ErrorCode(&errorCode);
                Q_ASSERT_SUCCEEDED(hr);
                emit javaScriptResult(callbackId, qt_error_string(errorCode));
                return S_OK;
            }
            HString result;
            hr = op->GetResults(result.GetAddressOf());
            Q_ASSERT_SUCCEEDED(hr);
            quint32 resultLength;
            const wchar_t *resultBuffer = result.GetRawBuffer(&resultLength);
            emit javaScriptResult(callbackId, QString::fromWCharArray(resultBuffer, resultLength));
            return S_OK;
        }).Get());
        Q_ASSERT_SUCCEEDED(hr);
        return hr;
    });
}

HRESULT QWinRTWebViewPrivate::onNavigationStarted(IWebView *, IWebViewNavigationStartingEventArgs *args)
{
    d->isLoading = true;
    HRESULT hr;
    ComPtr<IUriRuntimeClass> uri;
    hr = args->get_Uri(&uri);
    Q_ASSERT_SUCCEEDED(hr);
    const QUrl url = qurlFromUri(uri.Get());

    emit loadingChanged(QWebViewLoadRequestPrivate(url, QWebView::LoadStartedStatus, QString()));
    emit loadProgressChanged(0);
    return S_OK;
}

HRESULT QWinRTWebViewPrivate::onNavigationCompleted(IWebView *, IWebViewNavigationCompletedEventArgs *args)
{
    d->isLoading = false;
    HRESULT hr;
    ComPtr<IUriRuntimeClass> uri;
    hr = args->get_Uri(&uri);
    Q_ASSERT_SUCCEEDED(hr);
    const QUrl url = qurlFromUri(uri.Get());

    boolean isSuccess;
    hr = args->get_IsSuccess(&isSuccess);
    Q_ASSERT_SUCCEEDED(hr);
    const QWebView::LoadStatus status = isSuccess ? QWebView::LoadSucceededStatus : QWebView::LoadFailedStatus;

    WebErrorStatus errorStatus;
    hr = args->get_WebErrorStatus(&errorStatus);
    Q_ASSERT_SUCCEEDED(hr);
    const QString errorString = webErrorStatusString(errorStatus);

    emit loadingChanged(QWebViewLoadRequestPrivate(url, status, errorString));
    emit titleChanged(title());
    emit loadProgressChanged(100);
    return S_OK;
}

HRESULT QWinRTWebViewPrivate::onUnviewableContent(IWebView *, IWebViewUnviewableContentIdentifiedEventArgs *args)
{
    HRESULT hr;
    ComPtr<IUriRuntimeClass> uri;
    hr = args->get_Uri(&uri);
    Q_ASSERT_SUCCEEDED(hr);
    ComPtr<IAsyncOperation<bool>> op;
    hr = d->launcherStatics->LaunchUriAsync(uri.Get(), &op);
    Q_ASSERT_SUCCEEDED(hr);
    return S_OK;
}
