/****************************************************************************
**
** Copyright (C) 2017 Ford Motor Company
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtRemoteObjects module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qremoteobjectsourceio_p.h"

#include "qremoteobjectpacket_p.h"
#include "qremoteobjectsource_p.h"
#include "qremoteobjectnode_p.h"
#include "qremoteobjectpendingcall.h"
#include "qtremoteobjectglobal.h"

#include <QtCore/qstringlist.h>

QT_BEGIN_NAMESPACE

using namespace QtRemoteObjects;

QRemoteObjectSourceIo::QRemoteObjectSourceIo(const QUrl &address, QObject *parent)
    : QObject(parent)
    , m_server(QtROServerFactory::instance()->isValid(address) ?
               QtROServerFactory::instance()->create(address, this) : nullptr)
    , m_address(address)
{
    if (m_server == nullptr)
        qRODebug(this) << "Using" << m_address << "as external url.";
}

QRemoteObjectSourceIo::QRemoteObjectSourceIo(QObject *parent)
    : QObject(parent)
    , m_server(nullptr)
{
}

QRemoteObjectSourceIo::~QRemoteObjectSourceIo()
{
    qDeleteAll(m_sourceRoots.values());
}

bool QRemoteObjectSourceIo::startListening()
{
    if (!m_server->listen(m_address)) {
        qROCritical(this) << "Listen failed for URL:" << m_address;
        qROCritical(this) << m_server->serverError();
        return false;
    }

    qRODebug(this) << "QRemoteObjectSourceIo is Listening" << m_address;
    connect(m_server.data(), &QConnectionAbstractServer::newConnection, this,
            &QRemoteObjectSourceIo::handleConnection);
    return true;
}

bool QRemoteObjectSourceIo::enableRemoting(QObject *object, const QMetaObject *meta, const QString &name, const QString &typeName)
{
    if (m_sourceRoots.contains(name)) {
        qROWarning(this) << "Tried to register QRemoteObjectRootSource twice" << name;
        return false;
    }

    return enableRemoting(object, new DynamicApiMap(object, meta, name, typeName));
}

bool QRemoteObjectSourceIo::enableRemoting(QObject *object, const SourceApiMap *api, QObject *adapter)
{
    const QString name = api->name();
    if (!api->isDynamic() && m_sourceRoots.contains(name)) {
        qROWarning(this) << "Tried to register QRemoteObjectRootSource twice" << name;
        return false;
    }

    new QRemoteObjectRootSource(object, api, adapter, this);
    QRemoteObjectPackets::serializeObjectListPacket(m_packet, {QRemoteObjectPackets::ObjectInfo{api->name(), api->typeName(), api->objectSignature()}});
    for (auto conn : m_connections)
        conn->write(m_packet.array, m_packet.size);
    if (const int count = m_connections.size())
        qRODebug(this) << "Wrote new QObjectListPacket for" << api->name() << "to" << count << "connections";
    return true;
}

bool QRemoteObjectSourceIo::disableRemoting(QObject *object)
{
    QRemoteObjectRootSource *source = m_objectToSourceMap.take(object);
    if (!source)
        return false;

    delete source;
    return true;
}

void QRemoteObjectSourceIo::registerSource(QRemoteObjectSourceBase *source)
{
    Q_ASSERT(source);
    const QString &name = source->name();
    m_sourceObjects[name] = source;
    if (source->isRoot()) {
        QRemoteObjectRootSource *root = static_cast<QRemoteObjectRootSource *>(source);
        qRODebug(this) << "Registering" << name;
        m_sourceRoots[name] = root;
        m_objectToSourceMap[source->m_object] = root;
        if (serverAddress().isValid()) {
            const auto &type = source->m_api->typeName();
            emit remoteObjectAdded(qMakePair(name, QRemoteObjectSourceLocationInfo(type, serverAddress())));
        }
    }
}

void QRemoteObjectSourceIo::unregisterSource(QRemoteObjectSourceBase *source)
{
    Q_ASSERT(source);
    const QString &name = source->name();
    m_sourceObjects.remove(name);
    if (source->isRoot()) {
        const auto type = source->m_api->typeName();
        m_objectToSourceMap.remove(source->m_object);
        m_sourceRoots.remove(name);
        if (serverAddress().isValid())
            emit remoteObjectRemoved(qMakePair(name, QRemoteObjectSourceLocationInfo(type, serverAddress())));
    }
}

void QRemoteObjectSourceIo::onServerDisconnect(QObject *conn)
{
    IoDeviceBase *connection = qobject_cast<IoDeviceBase*>(conn);
    m_connections.remove(connection);

    qRODebug(this) << "OnServerDisconnect";

    for (QRemoteObjectRootSource *root : qAsConst(m_sourceRoots))
        root->removeListener(connection);

    const QUrl location = m_registryMapping.value(connection);
    emit serverRemoved(location);
    m_registryMapping.remove(connection);
    connection->close();
    connection->deleteLater();
}

void QRemoteObjectSourceIo::onServerRead(QObject *conn)
{
    // Assert the invariant here conn is of type QIODevice
    IoDeviceBase *connection = qobject_cast<IoDeviceBase*>(conn);
    QRemoteObjectPacketTypeEnum packetType;

    do {

        if (!connection->read(packetType, m_rxName))
            return;

        using namespace QRemoteObjectPackets;

        switch (packetType) {
        case Ping:
            serializePongPacket(m_packet, m_rxName);
            connection->write(m_packet.array, m_packet.size);
            break;
        case AddObject:
        {
            bool isDynamic;
            deserializeAddObjectPacket(connection->stream(), isDynamic);
            qRODebug(this) << "AddObject" << m_rxName << isDynamic;
            if (m_sourceRoots.contains(m_rxName)) {
                QRemoteObjectRootSource *root = m_sourceRoots[m_rxName];
                root->addListener(connection, isDynamic);
            } else {
                qROWarning(this) << "Request to attach to non-existent RemoteObjectSource:" << m_rxName;
            }
            break;
        }
        case RemoveObject:
        {
            qRODebug(this) << "RemoveObject" << m_rxName;
            if (m_sourceRoots.contains(m_rxName)) {
                QRemoteObjectRootSource *root = m_sourceRoots[m_rxName];
                const int count = root->removeListener(connection);
                Q_UNUSED(count);
                //TODO - possible to have a timer that closes connections if not reopened within a timeout?
            } else {
                qROWarning(this) << "Request to detach from non-existent RemoteObjectSource:" << m_rxName;
            }
            qRODebug(this) << "RemoveObject finished" << m_rxName;
            break;
        }
        case InvokePacket:
        {
            int call, index, serialId, propertyId;
            deserializeInvokePacket(connection->stream(), call, index, m_rxArgs, serialId, propertyId);
            if (m_rxName == QLatin1String("Registry") && !m_registryMapping.contains(connection)) {
                const QRemoteObjectSourceLocation loc = m_rxArgs.first().value<QRemoteObjectSourceLocation>();
                m_registryMapping[connection] = loc.second.hostUrl;
            }
            if (m_sourceObjects.contains(m_rxName)) {
                QRemoteObjectSourceBase *source = m_sourceObjects[m_rxName];
                if (call == QMetaObject::InvokeMetaMethod) {
                    const int resolvedIndex = source->m_api->sourceMethodIndex(index);
                    if (resolvedIndex < 0) { //Invalid index
                        qROWarning(this) << "Invalid method invoke packet received.  Index =" << index <<"which is out of bounds for type"<<m_rxName;
                        //TODO - consider moving this to packet validation?
                        break;
                    }
                    if (source->m_api->isAdapterMethod(index))
                        qRODebug(this) << "Adapter (method) Invoke-->" << m_rxName << source->m_adapter->metaObject()->method(resolvedIndex).name();
                    else {
                        qRODebug(this) << "Source (method) Invoke-->" << m_rxName << source->m_object->metaObject()->method(resolvedIndex).methodSignature();
                        auto method = source->m_object->metaObject()->method(resolvedIndex);
                        const int parameterCount = method.parameterCount();
                        for (int i = 0; i < parameterCount; i++)
                            decodeVariant(m_rxArgs[i], method.parameterType(i));
                    }
                    int typeId = QMetaType::type(source->m_api->typeName(index).constData());
                    if (!QMetaType(typeId).sizeOf())
                        typeId = QVariant::Invalid;
                    QVariant returnValue(typeId, nullptr);
                    // If a Replica is used as a Source (which node->proxy() does) we can have a PendingCall return value.
                    // In this case, we need to wait for the pending call and send that.
                    if (source->m_api->typeName(index) == QByteArrayLiteral("QRemoteObjectPendingCall"))
                        returnValue = QVariant::fromValue<QRemoteObjectPendingCall>(QRemoteObjectPendingCall());
                    source->invoke(QMetaObject::InvokeMetaMethod, index, m_rxArgs, &returnValue);
                    // send reply if wanted
                    if (serialId >= 0) {
                        if (returnValue.canConvert<QRemoteObjectPendingCall>()) {
                            QRemoteObjectPendingCall call = returnValue.value<QRemoteObjectPendingCall>();
                            // Watcher will be destroyed when connection is, or when the finished lambda is called
                            QRemoteObjectPendingCallWatcher *watcher = new QRemoteObjectPendingCallWatcher(call, connection);
                            QObject::connect(watcher, &QRemoteObjectPendingCallWatcher::finished, connection, [this, serialId, connection, watcher]() {
                                if (watcher->error() == QRemoteObjectPendingCall::NoError) {
                                    serializeInvokeReplyPacket(this->m_packet, this->m_rxName, serialId, encodeVariant(watcher->returnValue()));
                                    connection->write(m_packet.array, m_packet.size);
                                }
                                watcher->deleteLater();
                            });
                        } else {
                            serializeInvokeReplyPacket(m_packet, m_rxName, serialId, encodeVariant(returnValue));
                            connection->write(m_packet.array, m_packet.size);
                        }
                    }
                } else {
                    const int resolvedIndex = source->m_api->sourcePropertyIndex(index);
                    if (resolvedIndex < 0) {
                        qROWarning(this) << "Invalid property invoke packet received.  Index =" << index <<"which is out of bounds for type"<<m_rxName;
                        //TODO - consider moving this to packet validation?
                        break;
                    }
                    if (source->m_api->isAdapterProperty(index))
                        qRODebug(this) << "Adapter (write property) Invoke-->" << m_rxName << source->m_adapter->metaObject()->property(resolvedIndex).name();
                    else
                        qRODebug(this) << "Source (write property) Invoke-->" << m_rxName << source->m_object->metaObject()->property(resolvedIndex).name();
                    source->invoke(QMetaObject::WriteProperty, index, m_rxArgs);
                }
            }
            break;
        }
        default:
            qRODebug(this) << "OnReadReady invalid type" << packetType;
        }
    } while (connection->bytesAvailable()); // have bytes left over, so do another iteration
}

void QRemoteObjectSourceIo::handleConnection()
{
    qRODebug(this) << "handleConnection" << m_connections;

    ServerIoDevice *conn = m_server->nextPendingConnection();
    newConnection(conn);
}

void QRemoteObjectSourceIo::newConnection(IoDeviceBase *conn)
{
    m_connections.insert(conn);
    connect(conn, &IoDeviceBase::readyRead, this, [this, conn]() {
        onServerRead(conn);
    });
    connect(conn, &IoDeviceBase::disconnected, this, [this, conn]() {
        onServerDisconnect(conn);
    });

    serializeHandshakePacket(m_packet);
    conn->write(m_packet.array, m_packet.size);

    QRemoteObjectPackets::ObjectInfoList infos;
    infos.reserve(m_sourceRoots.size());
    for (auto remoteObject : qAsConst(m_sourceRoots)) {
        infos << QRemoteObjectPackets::ObjectInfo{remoteObject->m_api->name(), remoteObject->m_api->typeName(), remoteObject->m_api->objectSignature()};
    }
    serializeObjectListPacket(m_packet, infos);
    conn->write(m_packet.array, m_packet.size);
    qRODebug(this) << "Wrote ObjectList packet from Server" << QStringList(m_sourceRoots.keys());
}

QUrl QRemoteObjectSourceIo::serverAddress() const
{
    if (m_server)
        return m_server->address();
    return m_address;
}

QT_END_NAMESPACE
