// Copyright (C) 2017 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

#ifndef QMODBUSTCPCLIENT_P_H
#define QMODBUSTCPCLIENT_P_H

#include <QtCore/qloggingcategory.h>
#include <QtNetwork/qhostaddress.h>
#include <QtNetwork/qtcpsocket.h>
#include "QtSerialBus/qmodbustcpclient.h"

#include "private/qmodbusclient_p.h"

//
//  W A R N I N G
//  -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS)
Q_DECLARE_LOGGING_CATEGORY(QT_MODBUS_LOW)

class QModbusTcpClientPrivate : public QModbusClientPrivate
{
    Q_DECLARE_PUBLIC(QModbusTcpClient)

public:
    void setupTcpSocket()
    {
        Q_Q(QModbusTcpClient);

        m_socket = new QTcpSocket(q);

        QObject::connect(m_socket, &QAbstractSocket::connected, q, [this]() {
            qCDebug(QT_MODBUS) << "(TCP client) Connected to" << m_socket->peerAddress()
                               << "on port" << m_socket->peerPort();
            Q_Q(QModbusTcpClient);
            responseBuffer.clear();
            q->setState(QModbusDevice::ConnectedState);
        });

        QObject::connect(m_socket, &QAbstractSocket::disconnected, q, [this]() {
           qCDebug(QT_MODBUS)  << "(TCP client) Connection closed.";
           Q_Q(QModbusTcpClient);
           q->setState(QModbusDevice::UnconnectedState);
           cleanupTransactionStore();
        });

        QObject::connect(m_socket, &QAbstractSocket::errorOccurred, q,
                         [this](QAbstractSocket::SocketError /*error*/)
        {
            Q_Q(QModbusTcpClient);

            if (m_socket->state() == QAbstractSocket::UnconnectedState) {
                cleanupTransactionStore();
                q->setState(QModbusDevice::UnconnectedState);
            }
            q->setError(QModbusClient::tr("TCP socket error (%1).").arg(m_socket->errorString()),
                        QModbusDevice::ConnectionError);
        });

        QObject::connect(m_socket, &QIODevice::readyRead, q, [this](){
            responseBuffer += m_socket->read(m_socket->bytesAvailable());
            qCDebug(QT_MODBUS_LOW) << "(TCP client) Response buffer:" << responseBuffer.toHex();

            while (!responseBuffer.isEmpty()) {
                // can we read enough for Modbus ADU header?
                if (responseBuffer.size() < mbpaHeaderSize) {
                    qCDebug(QT_MODBUS_LOW) << "(TCP client) MBPA header too short. Waiting for more data.";
                    return;
                }

                quint8 serverAddress;
                quint16 transactionId, bytesPdu, protocolId;
                QDataStream input(responseBuffer);
                input >> transactionId >> protocolId >> bytesPdu >> serverAddress;

                // stop the timer as soon as we know enough about the transaction
                const bool knownTransaction = m_transactionStore.contains(transactionId);
                if (knownTransaction && m_transactionStore[transactionId].timer)
                    m_transactionStore[transactionId].timer->stop();

                qCDebug(QT_MODBUS) << "(TCP client) tid:" << Qt::hex << transactionId << "size:"
                    << bytesPdu << "server address:" << serverAddress;

                // The length field is the byte count of the following fields, including the Unit
                // Identifier and the PDU, so we remove on byte.
                bytesPdu--;

                int tcpAduSize = mbpaHeaderSize + bytesPdu;
                if (responseBuffer.size() < tcpAduSize) {
                    qCDebug(QT_MODBUS) << "(TCP client) PDU too short. Waiting for more data";
                    return;
                }

                QModbusResponse responsePdu;
                input >> responsePdu;
                qCDebug(QT_MODBUS) << "(TCP client) Received PDU:" << responsePdu.functionCode()
                                   << responsePdu.data().toHex();

                responseBuffer.remove(0, tcpAduSize);

                if (!knownTransaction) {
                    qCDebug(QT_MODBUS) << "(TCP client) No pending request for response with "
                        "given transaction ID, ignoring response message.";
                } else {
                    processQueueElement(responsePdu, m_transactionStore[transactionId]);
                }
            }
        });
    }

    QModbusReply *enqueueRequest(const QModbusRequest &request, int serverAddress,
                                 const QModbusDataUnit &unit,
                                 QModbusReply::ReplyType type) override
    {
        auto writeToSocket = [this](quint16 tId, const QModbusRequest &request, int address) {
            QByteArray buffer;
            QDataStream output(&buffer, QIODevice::WriteOnly);
            output << tId << quint16(0) << quint16(request.size() + 1) << quint8(address) << request;

            int writtenBytes = m_socket->write(buffer);
            if (writtenBytes == -1 || writtenBytes < buffer.size()) {
                Q_Q(QModbusTcpClient);
                qCDebug(QT_MODBUS) << "(TCP client) Cannot write request to socket.";
                q->setError(QModbusTcpClient::tr("Could not write request to socket."),
                            QModbusDevice::WriteError);
                return false;
            }
            qCDebug(QT_MODBUS_LOW) << "(TCP client) Sent TCP ADU:" << buffer.toHex();
            qCDebug(QT_MODBUS) << "(TCP client) Sent TCP PDU:" << request << "with tId:" <<Qt:: hex
                << tId;
            return true;
        };

        const int tId = transactionId();
        if (!writeToSocket(tId, request, serverAddress))
            return nullptr;

        Q_Q(QModbusTcpClient);
        auto reply = new QModbusReply(type, serverAddress, q);
        const auto element = QueueElement{ reply, request, unit, m_numberOfRetries,
            m_responseTimeoutDuration };
        m_transactionStore.insert(tId, element);

        q->connect(reply, &QObject::destroyed, q, [this, tId](QObject *) {
            if (!m_transactionStore.contains(tId))
                return;
            const QueueElement element = m_transactionStore.take(tId);
            if (element.timer)
                element.timer->stop();
        });

        if (element.timer) {
            q->connect(q, &QModbusClient::timeoutChanged,
                       element.timer.data(), QOverload<int>::of(&QTimer::setInterval));
            QObject::connect(element.timer.data(), &QTimer::timeout, q, [this, writeToSocket, tId]() {
                if (!m_transactionStore.contains(tId))
                    return;

                QueueElement elem = m_transactionStore.take(tId);
                if (elem.reply.isNull())
                    return;

                if (elem.numberOfRetries > 0) {
                    elem.numberOfRetries--;
                    if (!writeToSocket(tId, elem.requestPdu, elem.reply->serverAddress()))
                        return;
                    m_transactionStore.insert(tId, elem);
                    elem.timer->start();
                    qCDebug(QT_MODBUS) << "(TCP client) Resend request with tId:" << Qt::hex << tId;
                } else {
                    qCDebug(QT_MODBUS) << "(TCP client) Timeout of request with tId:" <<Qt::hex << tId;
                    elem.reply->setError(QModbusDevice::TimeoutError,
                        QModbusClient::tr("Request timeout."));
                }
            });
            element.timer->start();
        } else {
            qCWarning(QT_MODBUS) << "(TCP client) No response timeout timer for request with tId:"
                << Qt::hex << tId << ". Expected timeout:" << m_responseTimeoutDuration;
        }
        incrementTransactionId();

        return reply;
    }

    // TODO: Review once we have a transport layer in place.
    bool isOpen() const override
    {
        if (m_socket)
            return m_socket->isOpen();
        return false;
    }

    void cleanupTransactionStore()
    {
        if (m_transactionStore.isEmpty())
            return;

        qCDebug(QT_MODBUS) << "(TCP client) Cleanup of pending requests";

        for (const auto &elem : std::as_const(m_transactionStore)) {
            if (elem.reply.isNull())
                continue;
            elem.reply->setError(QModbusDevice::ReplyAbortedError,
                                 QModbusClient::tr("Reply aborted due to connection closure."));
        }
        m_transactionStore.clear();
    }

    // This doesn't overflow, it rather "wraps around". Expected.
    inline void incrementTransactionId() { m_transactionId++; }
    inline int transactionId() const { return m_transactionId; }

    QIODevice *device() const override { return m_socket; }

    QTcpSocket *m_socket = nullptr;
    QByteArray responseBuffer;
    QHash<quint16, QueueElement> m_transactionStore;
    int mbpaHeaderSize = 7;

private:   // Private to avoid using the wrong id inside the timer lambda,
    quint16 m_transactionId = 0; // capturing 'this' will not copy the id.
};

QT_END_NAMESPACE

#endif // QMODBUSTCPCLIENT_P_H
