// SPDX-License-Identifier: LGPL-3.0-or-later

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* nymea-zigbee
* Zigbee integration module for nymea
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-zigbee.
*
* nymea-zigbee is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* nymea-zigbee is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with nymea-zigbee. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "zigbeeinterface.h"
#include "loggingcategory.h"
#include "zigbeeutils.h"

ZigbeeInterface::ZigbeeInterface(QObject *parent) :
    QObject(parent)
{
    m_reconnectTimer = new QTimer(this);
    m_reconnectTimer->setSingleShot(true);
    m_reconnectTimer->setInterval(5000);

    connect(m_reconnectTimer, &QTimer::timeout, this, &ZigbeeInterface::onReconnectTimeout);
}

ZigbeeInterface::~ZigbeeInterface()
{
    qCDebug(dcZigbeeInterface()) << "Destroy interface";
    disable();
}

bool ZigbeeInterface::available() const
{
    if (!m_serialPort)
        return false;

    return m_serialPort->isOpen();
}

QString ZigbeeInterface::serialPort() const
{
    return m_serialPort->portName();
}

quint8 ZigbeeInterface::calculateCrc(const quint16 &messageTypeValue, const quint16 &lenghtValue, const QByteArray &data)
{
    quint8 crc = 0;

    crc ^= (messageTypeValue >> 8) & 0xff;
    crc ^= (messageTypeValue >> 0) & 0xff;

    crc ^= (lenghtValue >> 8) & 0xff;
    crc ^= (lenghtValue >> 0) & 0xff;

    for (int i = 0; i < lenghtValue; i++) {
        crc ^= quint8(data.at(i));
    }

    return crc;
}

void ZigbeeInterface::streamByte(quint8 byte, bool specialCharacter)
{
    if (!specialCharacter && byte < 0x10) {
        // Byte stuffing ESC char before stuffed data byte
        qCDebug(dcZigbeeInterfaceTraffic()) << "[out]" << ZigbeeUtils::convertByteToHexString(0x02);
        if (m_serialPort->write(QByteArray::fromRawData("\x02", 1)) < 0) {
            qCWarning(dcZigbeeInterface()) << "Could not stream ESC byte" << ZigbeeUtils::convertByteArrayToHexString(QByteArray::fromRawData("\x02", 1));
        }

        byte ^= 0x10;
    }

    qCDebug(dcZigbeeInterfaceTraffic()) << "[out]" << ZigbeeUtils::convertByteToHexString(byte);
    if (m_serialPort->write(QByteArray(1, static_cast<char>(byte))) < 0) {
        qCWarning(dcZigbeeInterface()) << "Could not stream byte" << ZigbeeUtils::convertByteToHexString(byte);
    }

    m_serialPort->flush();
}

void ZigbeeInterface::setAvailable(bool available)
{
    if (m_available == available)
        return;

    m_available = available;
    emit availableChanged(m_available);
}

void ZigbeeInterface::setReadingState(const ZigbeeInterface::ReadingState &state)
{
    if (m_readingState == state)
        return;

    m_readingState = state;
    qCDebug(dcZigbeeInterfaceTraffic()) << m_readingState;
}

void ZigbeeInterface::onReconnectTimeout()
{
    if (m_serialPort && !m_serialPort->isOpen()) {
        if (!m_serialPort->open(QSerialPort::ReadWrite)) {
            setAvailable(false);
            m_reconnectTimer->start();
        } else {
            qCDebug(dcZigbeeInterface()) << "The controller showed up again.";
            setAvailable(true);
        }
    }
}

void ZigbeeInterface::onReadyRead()
{
    QByteArray data = m_serialPort->readAll();

    // Parse serial data and built InterfaceMessage
    for (int i = 0; i < data.length(); i++) {
        quint8 byte = static_cast<quint8>(data.at(i));

        qCDebug(dcZigbeeInterfaceTraffic()) << "[ in]" << ZigbeeUtils::convertByteToHexString(byte);

        switch (byte) {
        case 0x01:
            m_crcValue = 0;
            m_messageTypeValue = 0;
            m_lengthValue = 0;
            m_escapeDetected = false;
            m_data.clear();
            if (!m_unhandledBuffer.isEmpty()) {
                qCDebug(dcZigbeeInterfaceTraffic()) << "Controller debug message:" << QString::fromUtf8(m_unhandledBuffer);
                m_unhandledBuffer.clear();
            }
            setReadingState(WaitForTypeMsb);
            break;
        case 0x02:
            m_escapeDetected = true;
            break;
        case 0x03: {
            Zigbee::InterfaceMessageType messageType = static_cast<Zigbee::InterfaceMessageType>(m_messageTypeValue);

            // Check message sanity
            quint8 crc = calculateCrc(m_messageTypeValue, m_lengthValue, m_data);
            if (crc != m_crcValue) {
                qCWarning(dcZigbeeInterface()) << "Received message end: Invalid CRC value" << crc << "!=" << m_crcValue;
            } else if (m_data.count() != m_lengthValue) {
                qCWarning(dcZigbeeInterface()) << "Received message end: Invalid data length of message" << m_data.count() << "!=" << m_lengthValue;
            } else {
                // We got a valid message
                ZigbeeInterfaceMessage message(messageType, m_data);
                qCDebug(dcZigbeeInterface()) << "<--" << message << "|" << "crc:" << ZigbeeUtils::convertByteToHexString(m_crcValue) << ", length:" << ZigbeeUtils::convertUint16ToHexString(m_lengthValue);
                emit messageReceived(message);

                // Clear all information for the next byte
                m_crcValue = 0;
                m_messageTypeValue = 0;
                m_lengthValue = 0;
                m_escapeDetected = false;
                m_data.clear();
            }
            setReadingState(WaitForStart);
            break;
        }
        default:

            // If the previous byte was an escape character, XOR the byte with 0x10 according documentation
            if (m_escapeDetected) {
                byte ^= 0x10;
                m_escapeDetected = false;
            }

            // Read data bytes depending on the reading state
            switch (m_readingState) {
            case WaitForStart:
                qCDebug(dcZigbeeInterfaceTraffic()) << "Wait for start but reviced data:" << byte;
                m_unhandledBuffer.append(static_cast<char>(byte));
                break;
            case WaitForTypeMsb:
                m_messageTypeValue = byte;
                m_messageTypeValue <<= 8;
                setReadingState(WaitForTypeLsb);
                break;
            case WaitForTypeLsb:
                m_messageTypeValue |= byte;
                setReadingState(WaitForLenghtMsb);
                break;
            case WaitForLenghtMsb:
                m_lengthValue = byte;
                m_lengthValue <<= 8;
                setReadingState(WaitForLengthLsb);
                break;
            case WaitForLengthLsb:
                m_lengthValue |= byte;
                setReadingState(WaitForCrc);
                break;
            case WaitForCrc:
                m_crcValue = byte;
                setReadingState(WaitForData);
                break;
            case WaitForData:
                m_data.append(static_cast<char>(byte));
                break;
            }

            break;
        }
    }
}

void ZigbeeInterface::onError(const QSerialPort::SerialPortError &error)
{
    if (error != QSerialPort::NoError && m_serialPort->isOpen()) {
        qCWarning(dcZigbeeInterface()) << "Serial port error:" << error << m_serialPort->errorString();
        m_reconnectTimer->start();
        m_serialPort->close();
        setAvailable(false);
    }
}

bool ZigbeeInterface::enable(const QString &serialPort, qint32 baudrate)
{
    if (m_serialPort) {
        delete m_serialPort;
        m_serialPort = nullptr;
    }

    setAvailable(false);

    m_serialPort = new QSerialPort(serialPort, this);
    m_serialPort->setBaudRate(baudrate);
    m_serialPort->setDataBits(QSerialPort::Data8);
    m_serialPort->setParity(QSerialPort::NoParity);
    m_serialPort->setFlowControl(QSerialPort::NoFlowControl);

    connect(m_serialPort, &QSerialPort::readyRead, this, &ZigbeeInterface::onReadyRead);
    connect(m_serialPort, SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(onError(QSerialPort::SerialPortError)));

    if (!m_serialPort->open(QSerialPort::ReadWrite)) {
        qCWarning(dcZigbeeInterface()) << "Could not open serial port" << serialPort << baudrate;
        m_reconnectTimer->start();
        return false;
    }

    qCDebug(dcZigbeeInterface()) << "Interface enabled successfully on" << serialPort;
    setAvailable(true);
    return true;
}

void ZigbeeInterface::disable()
{
    if (!m_serialPort)
        return;

    if (m_serialPort->isOpen())
        m_serialPort->close();

    delete m_serialPort;
    m_serialPort = nullptr;
    setAvailable(false);
    qCDebug(dcZigbeeInterface()) << "Interface disabled";
}

void ZigbeeInterface::sendMessage(const ZigbeeInterfaceMessage &message)
{
    if (!available()) {
        qCWarning(dcZigbeeInterface()) << "Could not send message. The serial port is not available.";
        return;
    }

    quint16 messageTypeValue = static_cast<quint16>(message.messageType());
    quint16 lengthValue = static_cast<quint16>(message.data().count());
    quint8 crcValue = calculateCrc(messageTypeValue, lengthValue, message.data());

    qCDebug(dcZigbeeInterface()) << "-->" << message << "|" << "crc:" << ZigbeeUtils::convertByteToHexString(crcValue) << ", length:" << ZigbeeUtils::convertUint16ToHexString(lengthValue);

    streamByte(0x01, true);
    streamByte((messageTypeValue >> 8) & 0xff);
    streamByte((messageTypeValue >> 0) & 0xff);
    streamByte((lengthValue >> 8) & 0xff);
    streamByte((lengthValue >> 0) & 0xff);
    streamByte(crcValue);

    for (int i = 0; i < message.data().count(); i++) {
        streamByte(static_cast<quint8>(message.data().at(i)));
    }
    streamByte(0x03, true);
}
