// Copyright (C) 2017 Ford Motor Company.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "passthrucanio.h"

#include <QLoggingCategory>
#include <QtEndian>

#include <cstring>

PassThruCanIO::PassThruCanIO(QObject *parent)
    : QObject(parent)
    , m_ioBuffer (8, J2534::Message(J2534::Protocol::CAN))
{
}

PassThruCanIO::~PassThruCanIO()
{
}

void PassThruCanIO::open(const QString &library, const QByteArray &subDev, uint bitRate)
{
    if (Q_UNLIKELY(m_passThru)) {
        qCCritical(QT_CANBUS_PLUGINS_PASSTHRU, "Pass-thru interface already open");
        emit openFinished(false);
        return;
    }
    qCDebug(QT_CANBUS_PLUGINS_PASSTHRU, "Loading interface library: %ls",
            qUtf16Printable(library));

    m_passThru = new J2534::PassThru(library, this);
    J2534::PassThru::Status openStatus = m_passThru->lastError();

    if (openStatus == J2534::PassThru::NoError)
        openStatus = m_passThru->open(subDev, &m_deviceId);

    if (openStatus == J2534::PassThru::NoError
            && m_passThru->connect(m_deviceId, J2534::Protocol::CAN,
                                   J2534::PassThru::CAN29BitID | J2534::PassThru::CANIDBoth,
                                   bitRate, &m_channelId) == J2534::PassThru::NoError) {
        emit openFinished(true);
        return;
    }
    emit errorOccurred(m_passThru->lastErrorString(),
                       QCanBusDevice::ConnectionError);

    if (openStatus == J2534::PassThru::NoError
            && m_passThru->close(m_deviceId) != J2534::PassThru::NoError)
        qCWarning(QT_CANBUS_PLUGINS_PASSTHRU, "Failed to close pass-thru device");

    delete m_passThru;
    m_passThru = nullptr;

    emit openFinished(false);
}

void PassThruCanIO::close()
{
    if (Q_LIKELY(m_passThru)) {
        delete m_idleNotifier;
        m_idleNotifier = nullptr;

        if (m_passThru->disconnect(m_channelId) != J2534::PassThru::NoError
                || m_passThru->close(m_deviceId) != J2534::PassThru::NoError) {

            qCWarning(QT_CANBUS_PLUGINS_PASSTHRU, "Failed to close pass-thru device");
            emit errorOccurred(m_passThru->lastErrorString(),
                               QCanBusDevice::ConnectionError);
        }
        delete m_passThru;
        m_passThru = nullptr;
    }
    emit closeFinished();
}

void PassThruCanIO::applyConfig(QCanBusDevice::ConfigurationKey key, const QVariant &value)
{
    if (Q_UNLIKELY(!m_passThru)) {
        qCCritical(QT_CANBUS_PLUGINS_PASSTHRU, "Pass-thru interface not open");
        return;
    }
    bool success = true;

    switch (key) {
    case QCanBusDevice::RawFilterKey:
        success = setMessageFilters(qvariant_cast<QList<QCanBusDevice::Filter>>(value));
        break;
    case QCanBusDevice::LoopbackKey:
        success = setConfigValue(J2534::Config::Loopback, value.toBool());
        break;
    case QCanBusDevice::BitRateKey:
        success = setConfigValue(J2534::Config::DataRate, value.toUInt());
        break;
    default:
        emit errorOccurred(tr("Unsupported configuration key: %1").arg(key),
                           QCanBusDevice::ConfigurationError);
        break;
    }
    if (!success) {
        emit errorOccurred(tr("Configuration failed: %1").arg(m_passThru->lastErrorString()),
                           QCanBusDevice::ConfigurationError);
    }
}

void PassThruCanIO::listen()
{
    if (Q_UNLIKELY(!m_passThru)) {
        qCCritical(QT_CANBUS_PLUGINS_PASSTHRU, "Pass-thru interface not open");
        return;
    }
    if (Q_UNLIKELY(m_idleNotifier)) {
        qCCritical(QT_CANBUS_PLUGINS_PASSTHRU, "Idle notifier already created");
        return;
    }
    m_idleNotifier = new QTimer(this);
    connect(m_idleNotifier, &QTimer::timeout, this, &PassThruCanIO::pollForMessages);

    m_idleNotifier->start(0);
}

bool PassThruCanIO::enqueueMessage(const QCanBusFrame &frame)
{
    const QMutexLocker lock (&m_writeGuard);
    m_writeQueue.append(frame);
    return true;
}

bool PassThruCanIO::setMessageFilters(const QList<QCanBusDevice::Filter> &filters)
{
    if (m_passThru->clear(m_channelId, J2534::PassThru::MsgFilters) != J2534::PassThru::NoError)
        return false;

    J2534::Message pattern {J2534::Protocol::CAN};
    pattern.setSize(4);
    J2534::Message mask {J2534::Protocol::CAN};
    mask.setSize(4);

    for (const auto &filter : filters) {
        if (filter.type != QCanBusFrame::DataFrame
                && filter.type != QCanBusFrame::InvalidFrame) {
            emit errorOccurred(tr("Configuration failed: unsupported filter type"),
                               QCanBusDevice::ConfigurationError);
            break;
        }
        if (filter.format & QCanBusDevice::Filter::MatchExtendedFormat)
            pattern.setRxStatus(J2534::Message::InCAN29BitID);
        else
            pattern.setRxStatus({});

        if (filter.format != QCanBusDevice::Filter::MatchBaseAndExtendedFormat)
            mask.setRxStatus(J2534::Message::InCAN29BitID);
        else
            mask.setRxStatus({});

        qToBigEndian<QCanBusFrame::FrameId>(filter.frameId & filter.frameIdMask, pattern.data());
        qToBigEndian<QCanBusFrame::FrameId>(filter.frameIdMask, mask.data());

        if (m_passThru->startMsgFilter(m_channelId, J2534::PassThru::PassFilter,
                                       mask, pattern) != J2534::PassThru::NoError)
            return false;
    }
    return true;
}

bool PassThruCanIO::setConfigValue(J2534::Config::Parameter param, ulong value)
{
    const J2534::Config config {param, value};

    return (m_passThru->setConfig(m_channelId, &config) == J2534::PassThru::NoError);
}

void PassThruCanIO::pollForMessages()
{
    if (Q_UNLIKELY(!m_passThru)) {
        qCCritical(QT_CANBUS_PLUGINS_PASSTHRU, "Pass-thru interface not open");
        return;
    }
    const bool writePending = writeMessages();
    readMessages(writePending);
}

bool PassThruCanIO::writeMessages()
{
    ulong numMsgs = m_ioBuffer.size();
    {
        const QMutexLocker lock (&m_writeGuard);
        numMsgs = qMin<ulong>(m_writeQueue.size(), numMsgs);

        for (ulong i = 0; i < numMsgs; ++i) {
            const QCanBusFrame &frame = m_writeQueue.at(i);
            J2534::Message &msg = m_ioBuffer[i];

            const QByteArray payload = frame.payload();
            const ulong payloadSize = qMin<ulong>(payload.size(),
                                                  J2534::Message::maxSize - 4);
            msg.setRxStatus({});
            msg.setTimestamp(0);
            msg.setSize(4 + payloadSize);
            msg.setExtraDataIndex(0);

            if (frame.hasExtendedFrameFormat())
                msg.setTxFlags(J2534::Message::OutCAN29BitID);
            else
                msg.setTxFlags({});

            qToBigEndian<QCanBusFrame::FrameId>(frame.frameId(), msg.data());
            std::memcpy(msg.data() + 4, payload.data(), payloadSize);
        }
    }
    if (numMsgs == 0)
        return false;

    const auto status = m_passThru->writeMsgs(m_channelId, m_ioBuffer.constData(),
                                              &numMsgs, pollTimeout);
    if (status == J2534::PassThru::BufferFull)
        return false;

    if (status != J2534::PassThru::NoError && status != J2534::PassThru::Timeout) {
        emit errorOccurred(tr("Message write failed: %1").arg(m_passThru->lastErrorString()),
                           QCanBusDevice::WriteError);
        return false;
    }
    if (numMsgs == 0)
        return false;

    bool morePending;
    {
        const QMutexLocker lock (&m_writeGuard);
        // De-queue successfully written frames.
        m_writeQueue.erase(m_writeQueue.begin(), m_writeQueue.begin() + numMsgs);
        morePending = !m_writeQueue.isEmpty();
    }
    emit messagesSent(numMsgs);

    return morePending;
}

void PassThruCanIO::readMessages(bool writePending)
{
    // If there are outgoing messages waiting to be written, just check
    // for already received messages but do not block waiting for more.
    const uint timeout = (writePending) ? 0 : pollTimeout;

    ulong numMsgs = m_ioBuffer.size();
    const auto status = m_passThru->readMsgs(m_channelId, m_ioBuffer.data(),
                                             &numMsgs, timeout);
    if (status == J2534::PassThru::BufferEmpty)
        return;

    if (status != J2534::PassThru::NoError && status != J2534::PassThru::Timeout) {
        emit errorOccurred(tr("Message read failed: %1").arg(m_passThru->lastErrorString()),
                           QCanBusDevice::ReadError);
        if (status != J2534::PassThru::BufferOverflow)
            return;
    }
    const int numFrames = qMin<ulong>(m_ioBuffer.size(), numMsgs);
    QList<QCanBusFrame> frames;
    frames.reserve(numFrames);

    for (int i = 0; i < numFrames; ++i) {
        const J2534::Message &msg = m_ioBuffer.at(i);
        if (Q_UNLIKELY(msg.size() < 4)
                || Q_UNLIKELY(msg.size() > J2534::Message::maxSize)) {
            // This normally shouldn't happen, so a log message is appropriate.
            qCWarning(QT_CANBUS_PLUGINS_PASSTHRU,
                      "Message with invalid size %lu received", msg.size());
            continue;
        }
        const QCanBusFrame::FrameId msgId = qFromBigEndian<QCanBusFrame::FrameId>(msg.data());
        const QByteArray payload (msg.data() + 4, msg.size() - 4);

        QCanBusFrame frame (msgId, payload);
        frame.setExtendedFrameFormat((msg.rxStatus() & J2534::Message::InCAN29BitID) != 0);
        frame.setLocalEcho((msg.rxStatus() & J2534::Message::InTxMsgType) != 0);
        frame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(msg.timestamp()));

        frames.append(std::move(frame));
    }
    if (!frames.isEmpty())
        emit messagesReceived(std::move(frames));
}
