// Copyright (C) 2018 Andre Hartmann <aha_1980@gmx.de>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "virtualcanbackend.h"

#include <QtCore/qdatetime.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qmutex.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qthread.h>

#include <QtNetwork/qtcpserver.h>
#include <QtNetwork/qtcpsocket.h>

QT_BEGIN_NAMESPACE

using namespace Qt::Literals::StringLiterals;

Q_DECLARE_LOGGING_CATEGORY(QT_CANBUS_PLUGINS_VIRTUALCAN)

enum {
    ServerDefaultTcpPort = 35468,
    VirtualChannels = 2
};

static const char RemoteRequestFlag    = 'R';
static const char ExtendedFormatFlag   = 'X';
static const char FlexibleDataRateFlag = 'F';
static const char BitRateSwitchFlag    = 'B';
static const char ErrorStateFlag       = 'E';
static const char LocalEchoFlag        = 'L';

VirtualCanServer::VirtualCanServer(QObject *parent)
    : QObject(parent)
{
    qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p] constructed.", this);
}

VirtualCanServer::~VirtualCanServer()
{
    qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p] destructed.", this);
}

void VirtualCanServer::start(quint16 port)
{
    // If there is already a server object, return immediately
    if (m_server) {
        qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p] is already running.", this);
        return;
    }

    if (QThread::currentThread() != this->thread()) {
        // This can happen if this methode is invoked a second time by a different thread
        // than when it was invoked the first time, and the first time the QTcpServer
        // couldn't listen because the TCP port was taken by a different process on the system
        return;
    }

    // Otherwise try to start a new server. If there is already
    // another server listen on the specified port, give up.
    m_server = new QTcpServer(this);
    if (!m_server->listen(QHostAddress::LocalHost, port)) {
        qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN,
               "Server [%p] could not be started, port %d is already in use.", this, port);
        m_server->deleteLater();
        m_server = nullptr;
        return;
    }

    // Server successfully started
    connect(m_server, &QTcpServer::newConnection, this, &VirtualCanServer::connected);
    qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN,
            "Server [%p] started and listening on port %d.", this, port);
    return;
}

void VirtualCanServer::connected()
{
    while (m_server->hasPendingConnections()) {
        qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p] client connected.", this);
        QTcpSocket *next = m_server->nextPendingConnection();
        m_serverSockets.append(next);
        connect(next, &QIODevice::readyRead, this, &VirtualCanServer::readyRead);
        connect(next, &QTcpSocket::disconnected, this, &VirtualCanServer::disconnected);
    }
}

void VirtualCanServer::disconnected()
{
    qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Server [%p] client disconnected.", this);

    auto socket = qobject_cast<QTcpSocket *>(sender());
    Q_ASSERT(socket);

    m_serverSockets.removeOne(socket);
    socket->deleteLater();
}

void VirtualCanServer::readyRead()
{
    auto readSocket = qobject_cast<QTcpSocket *>(sender());
    Q_ASSERT(readSocket);

    while (readSocket->canReadLine()) {
        const QByteArray command = readSocket->readLine().trimmed();
        qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN,
                "Server [%p] received: '%s'.", this, command.constData());

        if (command.startsWith("connect:")) {
            const QVariant interfaces = readSocket->property("interfaces");
            QStringList list = interfaces.toStringList();
            list.append(command.mid(int(strlen("connect:"))));
            readSocket->setProperty("interfaces", list);

        } else if (command.startsWith("disconnect:")) {
            const QVariant interfaces = readSocket->property("interfaces");
            QStringList list = interfaces.toStringList();
            list.removeAll(command.mid(int(strlen("disconnect:"))));
            readSocket->setProperty("interfaces", list);
            readSocket->disconnectFromHost();

        } else {
            const QByteArrayList commandList = command.split(':');
            Q_ASSERT(commandList.size() == 2);

            for (QTcpSocket *writeSocket : std::as_const(m_serverSockets)) {
                // Don't send the frame back to its origin
                if (writeSocket == readSocket)
                    continue;

                // Send frame to all clients registered to the same interface as sender
                const QVariant property = writeSocket->property("interfaces");
                if (!property.isValid())
                    continue;

                const QStringList propertyList = property.toStringList();
                if (propertyList.contains(commandList.first()))
                    writeSocket->write(commandList.last() + '\n');
            }
        }
    }
}

Q_GLOBAL_STATIC(VirtualCanServer, g_server)
static QBasicMutex g_serverMutex;

VirtualCanBackend::VirtualCanBackend(const QString &interface, QObject *parent)
    : QCanBusDevice(parent)
{
    m_url = QUrl(interface);
    const QString canDevice = m_url.fileName();

    const QRegularExpression re(QStringLiteral("can(\\d)"));
    const QRegularExpressionMatch match = re.match(canDevice);

    if (Q_UNLIKELY(!match.hasMatch())) {
        qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN,
                "Invalid interface '%ls'.", qUtf16Printable(interface));
        setError(tr("Invalid interface '%1'.").arg(interface), QCanBusDevice::ConnectionError);
        return;
    }

    const uint channel = match.captured(1).toUInt();
    if (Q_UNLIKELY(channel >= VirtualChannels)) {
        qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN,
                "Invalid interface '%ls'.", qUtf16Printable(interface));
        setError(tr("Invalid interface '%1'.").arg(interface), QCanBusDevice::ConnectionError);
        return;
    }

    m_channel = channel;
}

VirtualCanBackend::~VirtualCanBackend()
{
    qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket destructed.", this);
}

bool VirtualCanBackend::open()
{
    setState(QCanBusDevice::ConnectingState);

    const QString host = m_url.host();
    const QHostAddress address = host.isEmpty() ? QHostAddress::LocalHost : QHostAddress(host);
    const quint16 port = static_cast<quint16>(m_url.port(ServerDefaultTcpPort));

    if (address.isLoopback()) {
        const QMutexLocker locker(&g_serverMutex);
        g_server->start(port);
    }

    m_clientSocket = new QTcpSocket(this);
    m_clientSocket->connectToHost(address, port, QIODevice::ReadWrite);
    connect(m_clientSocket, &QAbstractSocket::connected, this, &VirtualCanBackend::clientConnected);
    connect(m_clientSocket, &QAbstractSocket::disconnected, this, &VirtualCanBackend::clientDisconnected);
    connect(m_clientSocket, &QIODevice::readyRead, this, &VirtualCanBackend::clientReadyRead);
    qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket created.", this);
    return true;
}

void VirtualCanBackend::close()
{
    qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] sends disconnect to server.", this);

    m_clientSocket->write(QByteArray("disconnect:can"_ba + QByteArray::number(m_channel) + '\n'));
}

void VirtualCanBackend::setConfigurationParameter(ConfigurationKey key, const QVariant &value)
{
    if (key == QCanBusDevice::ReceiveOwnKey || key == QCanBusDevice::CanFdKey)
        QCanBusDevice::setConfigurationParameter(key, value);
}

/*
    Protocol format: All data is in ASCII, one CAN message per line,
    each line ends with line feed '\n'.

    Format:  "<CAN-Channel>:<Flags>#<CAN-ID>#<Data-Bytes>\n"
    Example: "can0:XF#123#123456\n"

    The first part is the destination CAN channel, "can0" or "can1",
    followed by the flags list:

    * R - Remote Request
    * X - Extended Frame Format
    * F - Flexible Data Rate Format
    * B - Bitrate Switch
    * E - Error State Indicator
    * L - Local Echo

    Afterwards the CAN-ID and the data follows, both separated by '#'.
*/

bool VirtualCanBackend::writeFrame(const QCanBusFrame &frame)
{
    if (Q_UNLIKELY(state() != ConnectedState)) {
        qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN, "Error: Cannot write frame as client is not connected!");
        return false;
    }

    bool canFdEnabled = configurationParameter(QCanBusDevice::CanFdKey).toBool();
    if (Q_UNLIKELY(frame.hasFlexibleDataRateFormat() && !canFdEnabled)) {
        qCWarning(QT_CANBUS_PLUGINS_VIRTUALCAN,
                "Error: Cannot write CAN FD frame as CAN FD is not enabled!");
        return false;
    }

    QByteArray flags;
    if (frame.frameType() == QCanBusFrame::RemoteRequestFrame)
        flags.append(RemoteRequestFlag);
    if (frame.hasExtendedFrameFormat())
        flags.append(ExtendedFormatFlag);
    if (frame.hasFlexibleDataRateFormat())
        flags.append(FlexibleDataRateFlag);
    if (frame.hasBitrateSwitch())
        flags.append(BitRateSwitchFlag);
    if (frame.hasErrorStateIndicator())
        flags.append(ErrorStateFlag);
    if (frame.hasLocalEcho())
        flags.append(LocalEchoFlag);
    const QByteArray frameId = QByteArray::number(frame.frameId());
    const QByteArray command = "can" + QByteArray::number(m_channel)
            + ':' + frameId + '#' + flags + '#' + frame.payload().toHex() + '\n';
    m_clientSocket->write(command);

    if (configurationParameter(QCanBusDevice::ReceiveOwnKey).toBool()) {
        const qint64 timeStamp = QDateTime::currentDateTime().toMSecsSinceEpoch();
        QCanBusFrame echoFrame = frame;
        echoFrame.setLocalEcho(true);
        echoFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(timeStamp * 1000));
        enqueueReceivedFrames({echoFrame});
    }

    emit framesWritten(qint64(1));
    return true;
}

QString VirtualCanBackend::interpretErrorFrame(const QCanBusFrame &errorFrame)
{
    Q_UNUSED(errorFrame);
    return QString();
}

QCanBusDeviceInfo VirtualCanBackend::virtualCanDeviceInfo(uint channel)
{
    return createDeviceInfo(
                QStringLiteral("virtualcan"),
                QStringLiteral("can%1").arg(channel), QString(),
                QStringLiteral("Qt Virtual CAN bus"), QString(),
                channel, true, true);
}

QList<QCanBusDeviceInfo> VirtualCanBackend::interfaces()
{
    QList<QCanBusDeviceInfo> result;

    for (uint channel = 0; channel < VirtualChannels; ++channel)
        result.append(virtualCanDeviceInfo(channel));

    return result;
}

QCanBusDeviceInfo VirtualCanBackend::deviceInfo() const
{
    return virtualCanDeviceInfo(m_channel);
}

void VirtualCanBackend::clientConnected()
{
    qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket connected.", this);
    m_clientSocket->write(QByteArray("connect:can"_ba + QByteArray::number(m_channel) + '\n'));

    setState(QCanBusDevice::ConnectedState);
}

void VirtualCanBackend::clientDisconnected()
{
    qCInfo(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] socket disconnected.", this);

    setState(UnconnectedState);
}

void VirtualCanBackend::clientReadyRead()
{
    while (m_clientSocket->canReadLine()) {
        const QByteArray answer = m_clientSocket->readLine().trimmed();
        qCDebug(QT_CANBUS_PLUGINS_VIRTUALCAN, "Client [%p] received: '%s'.",
                this, answer.constData());

        if (answer.startsWith(QByteArray("disconnect:can"_ba + QByteArray::number(m_channel)))) {
            m_clientSocket->disconnectFromHost();
            continue;
        }

        const QByteArrayList list = answer.split('#');
        Q_ASSERT(list.size() == 3);

        const QCanBusFrame::FrameId id = list.at(0).toUInt();
        const QByteArray flags = list.at(1);
        const QByteArray data = QByteArray::fromHex(list.at(2));
        const qint64 timeStamp = QDateTime::currentDateTime().toMSecsSinceEpoch();
        QCanBusFrame frame(id, data);
        frame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(timeStamp * 1000));
        if (flags.contains(RemoteRequestFlag))
            frame.setFrameType(QCanBusFrame::RemoteRequestFrame);
        frame.setExtendedFrameFormat(flags.contains(ExtendedFormatFlag));
        frame.setFlexibleDataRateFormat(flags.contains(FlexibleDataRateFlag));
        frame.setBitrateSwitch(flags.contains(BitRateSwitchFlag));
        frame.setErrorStateIndicator(flags.contains(ErrorStateFlag));
        frame.setLocalEcho(flags.contains(LocalEchoFlag));
        enqueueReceivedFrames({frame});
    }
}

QT_END_NAMESPACE
