// 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 "zigbeeclusterlibrary.h"
#include "loggingcategory.h"
#include "zigbeedatatype.h"
#include "zigbeeutils.h"

#include <QDataStream>

quint8 ZigbeeClusterLibrary::buildFrameControlByte(const ZigbeeClusterLibrary::FrameControl &frameControl)
{
    quint8 byte = 0x00;

    // Bit 0-1
    byte |= frameControl.frameType;

    // Bit 2
    if (frameControl.manufacturerSpecific)
        byte |= 0x01 << 2;

    // Bit 3
    if (frameControl.direction == DirectionServerToClient)
        byte |= 0x01 << 3;

    // Bit 4
    if (frameControl.disableDefaultResponse)
        byte |= 0x01 << 4;

    return byte;
}

ZigbeeClusterLibrary::FrameControl ZigbeeClusterLibrary::parseFrameControlByte(quint8 frameControlByte)
{
    FrameControl frameControl;

    if (ZigbeeUtils::checkBitUint8(frameControlByte, 0)) {
        frameControl.frameType = FrameTypeClusterSpecific;
    } else {
        frameControl.frameType = FrameTypeGlobal;
    }

    frameControl.manufacturerSpecific = ZigbeeUtils::checkBitUint8(frameControlByte, 2);

    if (ZigbeeUtils::checkBitUint8(frameControlByte, 3)) {
        frameControl.direction = DirectionServerToClient;
    } else {
        frameControl.direction = DirectionClientToServer;
    }

    frameControl.disableDefaultResponse = ZigbeeUtils::checkBitUint8(frameControlByte, 4);

    return frameControl;
}

QByteArray ZigbeeClusterLibrary::buildHeader(const ZigbeeClusterLibrary::Header &header)
{
    QByteArray headerData;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QDataStream stream(&headerData, QDataStream::WriteOnly);
#else
    QDataStream stream(&headerData, QIODevice::WriteOnly);
#endif

    stream.setByteOrder(QDataStream::LittleEndian);
    stream << buildFrameControlByte(header.frameControl);

    // Include manufacturer only if the frame control indicates manufacturer specific
    if (header.frameControl.manufacturerSpecific) {
        stream << header.manufacturerCode;
    }

    stream << header.transactionSequenceNumber;
    stream << static_cast<quint8>(header.command);

    return headerData;
}

QList<ZigbeeClusterLibrary::ReadAttributeStatusRecord> ZigbeeClusterLibrary::parseAttributeStatusRecords(const QByteArray &payload)
{
    // Read attribute status records
    QList<ReadAttributeStatusRecord> attributeStatusRecords;

    qCDebug(dcZigbeeClusterLibrary()) << "Parse attribute status records from" << ZigbeeUtils::convertByteArrayToHexString(payload);

    QDataStream stream(payload);
    stream.setByteOrder(QDataStream::LittleEndian);
    quint16 attributeId; quint8 statusInt; quint8 dataTypeInt;

    while (!stream.atEnd()) {
        // Reset variables
        attributeId = 0; statusInt = 0; dataTypeInt = 0;

        // Read attribute id and status
        stream >> attributeId >> statusInt;
        ZigbeeClusterLibrary::Status status = static_cast<ZigbeeClusterLibrary::Status>(statusInt);

        qCDebug(dcZigbeeClusterLibrary()) << "Parse:" << ZigbeeUtils::convertUint16ToHexString(attributeId) << status;

        if (status != ZigbeeClusterLibrary::StatusSuccess) {
            qCWarning(dcZigbeeCluster()) << "Attribute status record" << ZigbeeUtils::convertUint16ToHexString(attributeId) << "finished with error" << status;
            // If not success, we are done and can continue with the next status record
            continue;
        } else {
            stream >> dataTypeInt;
            Zigbee::DataType dataType = static_cast<Zigbee::DataType>(dataTypeInt);

            qCDebug(dcZigbeeClusterLibrary()) << "Parse data type:" << dataType;
            ZigbeeDataType type = readDataType(&stream, dataType);
            if (!type.isValid())
                continue;

            ReadAttributeStatusRecord attributeRecord;
            attributeRecord.attributeId = attributeId;
            attributeRecord.attributeStatus = status;
            attributeRecord.dataType = type;
            qCDebug(dcZigbeeClusterLibrary()) << attributeRecord;
            attributeStatusRecords.append(attributeRecord);
        }
    }

    return attributeStatusRecords;
}

ZigbeeDataType ZigbeeClusterLibrary::readDataType(QDataStream *stream, Zigbee::DataType dataType)
{
    QByteArray data; quint16 numberOfElenemts = 0; quint8 elementType = 0;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QDataStream dataStream(&data, QDataStream::WriteOnly);
#else
    QDataStream dataStream(&data, QIODevice::WriteOnly);
#endif
    dataStream.setByteOrder(QDataStream::LittleEndian);

    // Parse data depending on the type
    if (dataType == Zigbee::Array || dataType == Zigbee::Set || dataType == Zigbee::Bag) {
        *stream >> elementType >> numberOfElenemts;
        dataStream << elementType << numberOfElenemts;
        qCDebug(dcZigbeeClusterLibrary()) << "Parse (array, set, bag): Element type" << ZigbeeUtils::convertByteToHexString(elementType) << "Number of elements:" << numberOfElenemts;
        if (numberOfElenemts == 0xffff) {
            qCWarning(dcZigbeeClusterLibrary()) << "ZigbeeStatusRecord contains invalid data elements" << dataType;
            return ZigbeeDataType(dataType);
        } else {
            for (int i = 0; i < numberOfElenemts; i++) {
                quint8 element = 0;
                *stream >> element;
                dataStream << element;
            }
        }
    } else if (dataType == Zigbee::Structure) {
        *stream >> numberOfElenemts;
        dataStream << numberOfElenemts;
        qCDebug(dcZigbeeClusterLibrary()) << "Parse (structure)" << "Number of elements:" << numberOfElenemts;
        if (numberOfElenemts == 0xffff) {
            qCWarning(dcZigbeeClusterLibrary()) << "ZigbeeStatusRecord contains invalid data elements" << dataType;
            return ZigbeeDataType(dataType);
        } else {
            *stream >> elementType;
            qCDebug(dcZigbeeClusterLibrary()) << "Parse (structure)" << "Element type:" << ZigbeeUtils::convertByteToHexString(elementType);
            for (int i = 0; i < numberOfElenemts; i++) {
                quint8 element = 0;
                *stream >> element;
                //qCDebug(dcZigbeeClusterLibrary()) << "Parse (structure)" << "Element value:" << ZigbeeUtils::convertByteToHexString(element);
                dataStream << element;
            }
        }
    } else if (dataType == Zigbee::OctetString || dataType == Zigbee::CharString) {
        quint8 length = 0;
        *stream >> length;
        dataStream << length;
        qCDebug(dcZigbeeClusterLibrary()) << "Parse (octet string, character string)" << "Length:" << length;
        for (int i = 0; i < length; i++) {
            quint8 element = 0;
            *stream >> element;
            dataStream << element;
        }
    } else if (dataType == Zigbee::LongOctetString || dataType == Zigbee::LongCharString) {
        quint16 length = 0;
        *stream >> length;
        dataStream << length;

        qCDebug(dcZigbeeClusterLibrary()) << "Parse (long octet string, long character string)" << "Length:" << length;
        for (int i = 0; i < length; i++) {
            quint8 element = 0;
            *stream >> element;
            data.append(element);
            dataStream << element;
        }
    } else {
        // Normal data type
        int length = ZigbeeDataType::typeLength(dataType);
        qCDebug(dcZigbeeClusterLibrary()) << "Parse (normal data type)" << "Number of elements:" << length;
        for (int i = 0; i < length; i++) {
            quint8 element = 0;
            *stream >> element;
            dataStream << element;
        }
    }

    qCDebug(dcZigbeeClusterLibrary()) << "Parsed data:" << ZigbeeUtils::convertByteArrayToHexString(data);
    return ZigbeeDataType(dataType, data);
}

ZigbeeClusterLibrary::Frame ZigbeeClusterLibrary::parseFrameData(const QByteArray &frameData)
{
    QDataStream stream(frameData);
    stream.setByteOrder(QDataStream::LittleEndian);

    // Read the header and then the payload
    quint8 offset = 0;
    quint8 frameControlByte = 0;
    quint8 commandByte = 0;

    Header header;
    stream >> frameControlByte;
    offset += 1;

    header.frameControl = parseFrameControlByte(frameControlByte);
    if (header.frameControl.manufacturerSpecific) {
        stream >> header.manufacturerCode;
        offset += 2;
    }

    stream >> header.transactionSequenceNumber;
    offset += 1;

    stream >> commandByte;
    header.command = static_cast<Command>(commandByte);
    offset += 1;

    Frame frame;
    frame.header = header;
    frame.payload = frameData.right(frameData.length() - offset);
    return frame;
}

QByteArray ZigbeeClusterLibrary::buildFrame(const ZigbeeClusterLibrary::Frame &frame)
{
    return buildHeader(frame.header) + frame.payload;
}

QByteArray ZigbeeClusterLibrary::buildAttributeReportingConfiguration(const ZigbeeClusterLibrary::AttributeReportingConfiguration &reportingConfiguration)
{
    QByteArray payload;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QDataStream stream(&payload, QDataStream::WriteOnly);
#else
    QDataStream stream(&payload, QIODevice::WriteOnly);
#endif
    stream.setByteOrder(QDataStream::LittleEndian);
    stream << static_cast<quint8>(reportingConfiguration.direction);
    stream << reportingConfiguration.attributeId;
    stream << static_cast<quint8>(reportingConfiguration.dataType);
    stream << reportingConfiguration.minReportingInterval;
    stream << reportingConfiguration.maxReportingInterval;

    for (int i = 0; i < reportingConfiguration.reportableChange.size(); i++) {
        stream << static_cast<quint8>(reportingConfiguration.reportableChange.at(i));
    }

    // Note: for reporting the timeoutPeriod is omitted
    if (reportingConfiguration.direction == ReportingDirectionReceiving) {
        stream << reportingConfiguration.timeoutPeriod;
    }

    return payload;
}

QByteArray ZigbeeClusterLibrary::buildWriteAttributeRecord(const ZigbeeClusterLibrary::WriteAttributeRecord &writeAttributeRecord)
{
    QByteArray payload;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    QDataStream stream(&payload, QDataStream::WriteOnly);
#else
    QDataStream stream(&payload, QIODevice::WriteOnly);
#endif
    stream.setByteOrder(QDataStream::LittleEndian);
    stream << writeAttributeRecord.attributeId;
    stream << static_cast<quint8>(writeAttributeRecord.dataType);
    for (int i = 0; i < writeAttributeRecord.data.size(); i++) {
        stream << static_cast<quint8>(writeAttributeRecord.data.at(i));
    }

    return payload;
}

QList<ZigbeeClusterLibrary::AttributeReportingStatusRecord> ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(const QByteArray &payload)
{
    QList<ZigbeeClusterLibrary::AttributeReportingStatusRecord> statusRecords;
    QDataStream stream(payload);
    stream.setByteOrder(QDataStream::LittleEndian);
    while (!stream.atEnd()) {
        ZigbeeClusterLibrary::AttributeReportingStatusRecord statusRecord;
        quint8 status; quint8 direction = 0;
        stream >> status >> direction >> statusRecord.attributeId;
        statusRecord.direction = static_cast<ReportingDirection>(direction);
        statusRecord.status = static_cast<ZigbeeClusterLibrary::Status>(status);
        statusRecords.append(statusRecord);
    }

    return statusRecords;
}

QDebug operator<<(QDebug debug, const ZigbeeClusterLibrary::FrameControl &frameControl)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "FrameControl(";
    if (frameControl.frameType == ZigbeeClusterLibrary::FrameTypeGlobal) {
        debug.nospace() << "Frame Type: Global" << ", ";
    } else {
        debug.nospace() << "Frame Type: Cluster specific" << ", ";
    }

    debug.nospace() << "Manufacturer specific: " << (frameControl.manufacturerSpecific ? "1" : "0") << ", ";
    debug.nospace() << "Direction: ";
    if (frameControl.direction == ZigbeeClusterLibrary::DirectionClientToServer) {
        debug.nospace() << "Client to server, ";
    } else {
        debug.nospace() << "Server to client, ";
    }

    debug.nospace() << "Disable default response: " << (frameControl.disableDefaultResponse ? "1" : "0") << ")";
    return debug;
}

QDebug operator<<(QDebug debug, const ZigbeeClusterLibrary::Header &header)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "Header(";
    debug.nospace() << header.frameControl;
    if (header.frameControl.manufacturerSpecific) {
        debug.nospace() << "Manufacturer code: " << ZigbeeUtils::convertUint16ToHexString(header.manufacturerCode) << ", ";
    }
    debug.nospace() << "TSN:" << header.transactionSequenceNumber << ", ";
    debug.nospace() << header.command << ")";
    return debug;
}

QDebug operator<<(QDebug debug, const ZigbeeClusterLibrary::Frame &frame)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "Frame(";
    debug.nospace() << frame.header;
    debug.nospace() << ZigbeeUtils::convertByteArrayToHexString(frame.payload) << ")";
    return debug;
}

QDebug operator<<(QDebug debug, const ZigbeeClusterLibrary::ReadAttributeStatusRecord &attributeStatusRecord)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "ReadAttributeStatusRecord("
                    << ZigbeeUtils::convertUint16ToHexString(attributeStatusRecord.attributeId) << ", "
                    << attributeStatusRecord.attributeStatus;
    if (attributeStatusRecord.attributeStatus == ZigbeeClusterLibrary::StatusSuccess) {
        debug.nospace() << ", " << attributeStatusRecord.dataType;
    }
    debug.nospace() << ")";
    return debug;
}

QDebug operator<<(QDebug debug, const ZigbeeClusterLibrary::AttributeReportingConfiguration &attributeReportingConfiguration)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "AttributeReportingConfiguration("
                    << attributeReportingConfiguration.direction << ", "
                    << ZigbeeUtils::convertUint16ToHexString(attributeReportingConfiguration.attributeId) << ", "
                    << attributeReportingConfiguration.dataType << ", "
                    << "Min interval: " << attributeReportingConfiguration.minReportingInterval << "[s], "
                    << "Max interval: " << attributeReportingConfiguration.maxReportingInterval << "[s], "
                    << "Change: " << ZigbeeUtils::convertByteArrayToHexString(attributeReportingConfiguration.reportableChange) << ", "
                    << "Timeout period: " << attributeReportingConfiguration.timeoutPeriod << "[s]"
                    << ")";
    return debug;
}

QDebug operator<<(QDebug debug, const ZigbeeClusterLibrary::AttributeReportingStatusRecord &attributeReportingStatusRecord)
{
    QDebugStateSaver saver(debug);
    debug.nospace() << "AttributeReportingConfiguration("
                    << attributeReportingStatusRecord.status << ", "
                    << attributeReportingStatusRecord.attributeId << ", "
                    << attributeReportingStatusRecord.direction
                    << ")";
    return debug;

}
