// Copyright (C) 2018 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 "qwebsocket_p.h"

#if QT_CONFIG(thread)
#include <QtCore/qthread.h>
#include <emscripten/threading.h>
#endif

#include <emscripten.h>
#include <emscripten/websocket.h>
#include <emscripten/val.h>

#include <QHostAddress>

static EM_BOOL q_onWebSocketErrorCallback(int eventType,
                                          const EmscriptenWebSocketErrorEvent *e,
                                          void *userData)
{
    Q_UNUSED(eventType)
    Q_UNUSED(e)

    QWebSocketPrivate *wsp = reinterpret_cast<QWebSocketPrivate *>(userData);
    Q_ASSERT (wsp);

    emit wsp->q_func()->errorOccurred(wsp->error());
    return EM_FALSE;
}

static EM_BOOL q_onWebSocketCloseCallback(int eventType,
                                          const EmscriptenWebSocketCloseEvent *emCloseEvent,
                                          void *userData)
{
    Q_UNUSED(eventType)
    QWebSocketPrivate *wsp = reinterpret_cast<QWebSocketPrivate *>(userData);
    Q_ASSERT (wsp);

    wsp->setSocketClosed(emCloseEvent);
    return EM_FALSE;
}

static EM_BOOL q_onWebSocketOpenCallback(int eventType,
                                         const EmscriptenWebSocketOpenEvent *e, void *userData)
{
    Q_UNUSED(eventType)
    Q_UNUSED(e)

    QWebSocketPrivate *wsp = reinterpret_cast<QWebSocketPrivate *>(userData);
    Q_ASSERT (wsp);

    wsp->setSocketState(QAbstractSocket::ConnectedState);
    emit wsp->q_func()->connected();
    return EM_FALSE;
}

static EM_BOOL q_onWebSocketIncomingMessageCallback(int eventType,
                                                    const EmscriptenWebSocketMessageEvent *e,
                                                    void *userData)
{
    Q_UNUSED(eventType)
    QWebSocketPrivate *wsp = reinterpret_cast<QWebSocketPrivate *>(userData);
    Q_ASSERT(wsp);

    if (!e->isText) {
        QByteArray buffer(reinterpret_cast<const char *>(e->data), e->numBytes);
        if (!buffer.isEmpty())
            emit wsp->q_func()->binaryMessageReceived(buffer);
    } else {
        QString buffer = QString::fromUtf8(reinterpret_cast<const char *>(e->data), e->numBytes - 1);
        emit wsp->q_func()->textMessageReceived(buffer);
    }

    return 0;
}

qint64 QWebSocketPrivate::sendTextMessage(const QString &message)
{
    int result = 0;
    emscripten_websocket_get_ready_state(m_socketContext, &m_readyState);

    if (m_readyState == 1) {
        QByteArray messageArray = message.toUtf8();
        result = emscripten_websocket_send_utf8_text(m_socketContext, messageArray);
        if (result < 0)
            emitErrorOccurred(QAbstractSocket::UnknownSocketError);
        else
            return messageArray.length();
    } else
        qWarning() << "Could not send message. Websocket is not open";

    return result;
}

qint64 QWebSocketPrivate::sendBinaryMessage(const QByteArray &data)
{
    int result = 0;
    emscripten_websocket_get_ready_state(m_socketContext, &m_readyState);
    if (m_readyState == 1) {
        result = emscripten_websocket_send_binary(
                m_socketContext, const_cast<void *>(reinterpret_cast<const void *>(data.constData())),
                data.size());
        if (result < 0)
            emitErrorOccurred(QAbstractSocket::UnknownSocketError);
        else
            return data.size();
    } else
        qWarning() << "Could not send message. Websocket is not open";

    return result;
}

void QWebSocketPrivate::close(QWebSocketProtocol::CloseCode closeCode, QString reason)
{
    Q_Q(QWebSocket);
    m_closeCode = closeCode;
    m_closeReason = reason;
    Q_EMIT q->aboutToClose();
    setSocketState(QAbstractSocket::ClosingState);

    emscripten_websocket_get_ready_state(m_socketContext, &m_readyState);

    if (m_readyState == 1 || m_readyState == 0) {
        emscripten_websocket_close(m_socketContext, (int)closeCode, reason.toUtf8());
    }
    setSocketState(QAbstractSocket::UnconnectedState);
    emit q->disconnected();
    emscripten_websocket_get_ready_state(m_socketContext, &m_readyState);
}

void QWebSocketPrivate::open(const QNetworkRequest &request,
                             const QWebSocketHandshakeOptions &options, bool mask)
{
    Q_UNUSED(mask);
    Q_UNUSED(options)

    emscripten_websocket_get_ready_state(m_socketContext, &m_readyState);

    if ((m_readyState == 1 || m_readyState == 3) && m_socketContext != 0) {
        emitErrorOccurred(QAbstractSocket::OperationError);
        return;
    }

    const QUrl url = request.url();

    emscripten::val navProtocol = emscripten::val::global("self")["location"]["protocol"];

    //  An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.
    // and causes emscripten to assert
    bool isSecureContext = (navProtocol.as<std::string>().find("https") == 0);

    if (!url.isValid()
            || url.toString().contains(QStringLiteral("\r\n"))) {
        setErrorString(QWebSocket::tr("Connection refused"));

        emitErrorOccurred(QAbstractSocket::ConnectionRefusedError);

        return;
    }
    // exception for localhost/127.0.0.1/[::1]
    // localhost has special privledges

    QHostAddress hostAddress(url.host());

    bool hostAddressIsLocal = (hostAddress == QHostAddress::LocalHost
            || hostAddress == QHostAddress::LocalHostIPv6);

    if (url.host() != QStringLiteral("localhost") && !hostAddressIsLocal) {
        if (isSecureContext && url.scheme() == QStringLiteral("ws")) {
            const QString message =
                    QWebSocket::tr("Unsupported WebSocket scheme: %1").arg(url.scheme());
            setErrorString(message);
            emitErrorOccurred(QAbstractSocket::UnsupportedSocketOperationError);
            return;
        }
    }

    EmscriptenWebSocketCreateAttributes attr;

    emscripten_websocket_init_create_attributes(&attr); // memset
    QByteArray thisUrl = url.toString(QUrl::FullyEncoded).toUtf8();
    attr.url = thisUrl.constData();

#if QT_CONFIG(thread)
    // see https://github.com/emscripten-core/emscripten/blob/main/system/include/emscripten/websocket.h
    // choose a default: create websocket on calling thread
    attr.createOnMainThread = false;
#endif
    // HTML WebSockets do not support arbitrary request headers, but
    // do support the WebSocket protocol header. This header is
    // required for some use cases like MQTT.

    // add user subprotocol options
    QStringList protocols = requestedSubProtocols();
    QByteArray secProto;
    if (!protocols.isEmpty()) {
        // comma-separated list of protocol strings, no spaces
        secProto = protocols.join(QStringLiteral(",")).toLatin1();
        attr.protocols = secProto.constData();
    }

    // create and connect
    setSocketState(QAbstractSocket::ConnectingState);
    m_socketContext = emscripten_websocket_new(&attr);

    if (m_socketContext <= 0) { // m_readyState might not be changed yet
        // error
        emitErrorOccurred(QAbstractSocket::UnknownSocketError);
        return;
    }

#if QT_CONFIG(thread)
    emscripten_websocket_set_onopen_callback_on_thread(m_socketContext, (void *)this,
                                                       q_onWebSocketOpenCallback,
                                                       (quintptr)QThread::currentThreadId());
    emscripten_websocket_set_onmessage_callback_on_thread(m_socketContext, (void *)this,
                                                          q_onWebSocketIncomingMessageCallback,
                                                          (quintptr)QThread::currentThreadId());
    emscripten_websocket_set_onerror_callback_on_thread(m_socketContext, (void *)this,
                                                        q_onWebSocketErrorCallback,
                                                        (quintptr)QThread::currentThreadId());
    emscripten_websocket_set_onclose_callback_on_thread(m_socketContext, (void *)this,
                                                        q_onWebSocketCloseCallback,
                                                        (quintptr)QThread::currentThreadId());
#else
    emscripten_websocket_set_onopen_callback(m_socketContext, (void *)this,
                                             q_onWebSocketOpenCallback);
    emscripten_websocket_set_onmessage_callback(m_socketContext, (void *)this,
                                                q_onWebSocketIncomingMessageCallback);
    emscripten_websocket_set_onerror_callback(m_socketContext, (void *)this,
                                              q_onWebSocketErrorCallback);
    emscripten_websocket_set_onclose_callback(m_socketContext, (void *)this,
                                              q_onWebSocketCloseCallback);
#endif
}

bool QWebSocketPrivate::isValid() const
{
    return (m_socketContext > 0 && m_socketState == QAbstractSocket::ConnectedState);
}

void QWebSocketPrivate::setSocketClosed(const EmscriptenWebSocketCloseEvent *emCloseEvent)
{
    Q_Q(QWebSocket);
    m_closeCode = (QWebSocketProtocol::CloseCode)emCloseEvent->code;

    m_closeReason = QString::fromUtf8(emCloseEvent->reason);

    if (m_closeReason.isEmpty()) {
        m_closeReason = closeCodeToString(m_closeCode);
    }

    if (m_socketState == QAbstractSocket::ConnectedState) {
        Q_EMIT q->aboutToClose();
        setSocketState(QAbstractSocket::ClosingState);
    }

    if (!emCloseEvent->wasClean) {
        m_errorString = QStringLiteral("The remote host closed the connection");
        emit q->errorOccurred(error());
    }

    emscripten_websocket_get_ready_state(m_socketContext, &m_readyState);

    if (m_readyState == 3) { // closed
        setSocketState(QAbstractSocket::UnconnectedState);
        emit q->disconnected();
        emscripten_websocket_delete(emCloseEvent->socket);
        m_socketContext = 0;
    }
}

QString QWebSocketPrivate::closeCodeToString(QWebSocketProtocol::CloseCode code)
{
    switch (code) {
        case QWebSocketProtocol::CloseCodeNormal: return QStringLiteral("Normal closure");
        case QWebSocketProtocol::CloseCodeGoingAway: return QStringLiteral("Going away");
        case QWebSocketProtocol::CloseCodeProtocolError: return QStringLiteral("Protocol error");
        case QWebSocketProtocol::CloseCodeDatatypeNotSupported: return QStringLiteral("Unsupported data");
        case QWebSocketProtocol::CloseCodeReserved1004: return QStringLiteral("Reserved");
        case QWebSocketProtocol::CloseCodeMissingStatusCode: return QStringLiteral("No status received");
        case QWebSocketProtocol::CloseCodeAbnormalDisconnection: return QStringLiteral("Abnormal closure");
        case QWebSocketProtocol::CloseCodeWrongDatatype: return QStringLiteral("Invalid frame payload data");
        case QWebSocketProtocol::CloseCodePolicyViolated: return QStringLiteral("Policy violation");
        case QWebSocketProtocol::CloseCodeTooMuchData: return QStringLiteral("Message too big");
        case QWebSocketProtocol::CloseCodeMissingExtension: return QStringLiteral("Mandatory extension missing");
        case QWebSocketProtocol::CloseCodeBadOperation: return QStringLiteral("Internal server error");
        case QWebSocketProtocol::CloseCodeTlsHandshakeFailed: return QStringLiteral("TLS handshake failed");
    };
    return QStringLiteral("");
}
