// Copyright (C) 2016 Kurt Pattyn <pattyn.kurt@gmail.com>.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

/*!
    \qmltype WebSocket
    \nativetype QQmlWebSocket
    \since 5.3

    \inqmlmodule QtWebSockets
    \ingroup websockets-qml
    \brief QML interface to QWebSocket.

    WebSockets is a web technology providing full-duplex communications channels over a
    single TCP connection.
    The WebSocket protocol was standardized by the IETF as \l {RFC 6455} in 2011.
*/

/*!
  \qmlproperty url WebSocket::url
  Server url to connect to. The url must have one of 2 schemes: \e ws:// or \e wss://.
  When not supplied, then \e ws:// is used.
*/

/*!
  \qmlproperty list<string> WebSocket::requestedSubprotocols
  \since 6.4
  The list of WebSocket subprotocols to send in the WebSocket handshake.
*/

/*!
  \qmlproperty string WebSocket::negotiatedSubprotocol
  \since 6.4
  The WebSocket subprotocol that has been negotiated with the server.
*/

/*!
  \qmlproperty Status WebSocket::status
  Status of the WebSocket.

  The status can have the following values:
  \list
  \li WebSocket.Connecting
  \li WebSocket.Open
  \li WebSocket.Closing
  \li WebSocket.Closed
  \li WebSocket.Error
  \endlist
*/

/*!
  \qmlproperty string WebSocket::errorString
  Contains a description of the last error that occurred. When no error occurrred,
  this string is empty.
*/

/*!
  \qmlproperty bool WebSocket::active
  When set to true, a connection is made to the server with the given url.
  When set to false, the connection is closed.
  The default value is false.
*/

/*!
  \qmlsignal WebSocket::textMessageReceived(QString message)
  This signal is emitted when a text message is received.
  \a message contains the bytes received.
*/

/*!
  \qmlsignal WebSocket::binaryMessageReceived(QString message)
  \since 5.8
  This signal is emitted when a binary message is received.
  \a message contains the bytes received.
*/

/*!
  \qmlsignal WebSocket::statusChanged(Status status)
  This signal is emitted when the status of the WebSocket changes.
  The \a status argument provides the current status.

  \sa {QtWebSockets::}{WebSocket::status}
*/

/*!
  \qmlmethod void WebSocket::sendTextMessage(string message)
  Sends \a message to the server.
*/

/*!
  \qmlmethod void WebSocket::sendBinaryMessage(ArrayBuffer message)
  \since 5.8
  Sends the parameter \a message to the server.
*/

#include "qqmlwebsocket.h"
#include <QtWebSockets/QWebSocket>
#include <QtWebSockets/QWebSocketHandshakeOptions>

QT_BEGIN_NAMESPACE

QQmlWebSocket::QQmlWebSocket(QObject *parent) :
    QObject(parent),
    m_webSocket(),
    m_status(Closed),
    m_url(),
    m_isActive(false),
    m_componentCompleted(true),
    m_errorString()
{
}

QQmlWebSocket::QQmlWebSocket(QWebSocket *socket, QObject *parent) :
    QObject(parent),
    m_status(Closed),
    m_url(socket->requestUrl()),
    m_requestedProtocols(socket->handshakeOptions().subprotocols()),
    m_isActive(true),
    m_componentCompleted(true),
    m_errorString(socket->errorString())
{
    setSocket(socket);
    onStateChanged(socket->state());
}

QQmlWebSocket::~QQmlWebSocket()
{
    if (m_webSocket)
        m_webSocket->disconnect();
}

qint64 QQmlWebSocket::sendTextMessage(const QString &message)
{
    if (m_status != Open) {
        setErrorString(tr("Messages can only be sent when the socket is open."));
        setStatus(Error);
        return 0;
    }
    return m_webSocket->sendTextMessage(message);
}

qint64 QQmlWebSocket::sendBinaryMessage(const QByteArray &message)
{
    if (m_status != Open) {
        setErrorString(tr("Messages can only be sent when the socket is open."));
        setStatus(Error);
        return 0;
    }
    return m_webSocket->sendBinaryMessage(message);
}

QStringList QQmlWebSocket::requestedSubprotocols() const
{
    return m_requestedProtocols;
}

void QQmlWebSocket::setRequestedSubprotocols(const QStringList &requestedSubprotocols)
{
    if (m_requestedProtocols == requestedSubprotocols)
        return;

    m_requestedProtocols = requestedSubprotocols;
    emit requestedSubprotocolsChanged();
}

QUrl QQmlWebSocket::url() const
{
    return m_url;
}

void QQmlWebSocket::setUrl(const QUrl &url)
{
    if (m_url == url) {
        return;
    }
    if (m_webSocket && (m_status == Open)) {
        m_webSocket->close();
    }
    m_url = url;
    Q_EMIT urlChanged();
    open();
}

QString QQmlWebSocket::negotiatedSubprotocol() const
{
    return m_negotiatedProtocol;
}

QQmlWebSocket::Status QQmlWebSocket::status() const
{
    return m_status;
}

QString QQmlWebSocket::errorString() const
{
    return m_errorString;
}

void QQmlWebSocket::classBegin()
{
    m_componentCompleted = false;
    m_errorString = tr("QQmlWebSocket is not ready.");
    m_status = Closed;
}

void QQmlWebSocket::componentComplete()
{
    setSocket(new QWebSocket);

    m_componentCompleted = true;

    open();
}

void QQmlWebSocket::setSocket(QWebSocket *socket)
{
    m_webSocket.reset(socket);
    if (m_webSocket) {
        // explicit ownership via QScopedPointer
        m_webSocket->setParent(nullptr);
        connect(m_webSocket.data(), &QWebSocket::textMessageReceived,
                this, &QQmlWebSocket::textMessageReceived);
        connect(m_webSocket.data(), &QWebSocket::binaryMessageReceived,
                this, &QQmlWebSocket::binaryMessageReceived);
        connect(m_webSocket.data(), &QWebSocket::errorOccurred,
                this, &QQmlWebSocket::onError);
        connect(m_webSocket.data(), &QWebSocket::stateChanged,
                this, &QQmlWebSocket::onStateChanged);
    }
}

void QQmlWebSocket::onError(QAbstractSocket::SocketError error)
{
    Q_UNUSED(error);
    setErrorString(m_webSocket->errorString());
    setStatus(Error);
}

void QQmlWebSocket::onStateChanged(QAbstractSocket::SocketState state)
{
    switch (state)
    {
        case QAbstractSocket::ConnectingState:
        case QAbstractSocket::BoundState:
        case QAbstractSocket::HostLookupState:
        {
            setStatus(Connecting);
            break;
        }
        case QAbstractSocket::UnconnectedState:
        {
            setStatus(Closed);
            break;
        }
        case QAbstractSocket::ConnectedState:
        {
            setStatus(Open);
            break;
        }
        case QAbstractSocket::ClosingState:
        {
            setStatus(Closing);
            break;
        }
        default:
        {
            setStatus(Connecting);
            break;
        }
    }
}

void QQmlWebSocket::setStatus(QQmlWebSocket::Status status)
{
    if (m_status == status) {
        return;
    }
    m_status = status;
    if (status != Error) {
        setErrorString();
    }
    Q_EMIT statusChanged(m_status);

    const auto protocol = m_status == Open && m_webSocket ? m_webSocket->subprotocol() : QString();
    if (m_negotiatedProtocol != protocol) {
        m_negotiatedProtocol = protocol;
        Q_EMIT negotiatedSubprotocolChanged();
    }
}

void QQmlWebSocket::setActive(bool active)
{
    if (m_isActive == active) {
        return;
    }
    m_isActive = active;
    Q_EMIT activeChanged(m_isActive);
    if (!m_componentCompleted) {
        return;
    }
    if (m_isActive) {
        open();
    } else {
        close();
    }
}

bool QQmlWebSocket::isActive() const
{
    return m_isActive;
}

void QQmlWebSocket::open()
{
    if (m_componentCompleted && m_isActive && m_url.isValid() && Q_LIKELY(m_webSocket)) {
        QWebSocketHandshakeOptions opt;
        opt.setSubprotocols(m_requestedProtocols);
        m_webSocket->open(m_url, opt);
    }
}

void QQmlWebSocket::close()
{
    if (m_componentCompleted && Q_LIKELY(m_webSocket)) {
        m_webSocket->close();
    }
}

void QQmlWebSocket::setErrorString(QString errorString)
{
    if (m_errorString == errorString) {
        return;
    }
    m_errorString = errorString;
    Q_EMIT errorStringChanged(m_errorString);
}

QT_END_NAMESPACE
