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

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins-modbus.
*
* nymea-plugins-modbus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins-modbus 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins-modbus. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "speedwirediscovery.h"
#include "extern-plugininfo.h"
#include "speedwire.h"

#include <QDataStream>

SpeedwireDiscovery::SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, SpeedwireInterface *speedwireInterface, quint32 localSerialNumber, QObject *parent) :
    QObject(parent),
    m_networkDeviceDiscovery(networkDeviceDiscovery),
    m_speedwireInterface(speedwireInterface),
    m_localSerialNumber(localSerialNumber)
{
    // More details: https://github.com/RalfOGit/libspeedwire/

    // Request: 534d4100000402a00000000100260010 606509a0 ffffffffffff0000 7d0052be283a0000 000000000180 00020000 00000000 00000000 00000000  => command = 0x00000200, first = 0x00000000; last = 0x00000000; trailer = 0x00000000
    // Response 534d4100000402a000000001004e0010 606513a0 7d0052be283a00c0 7a01842a71b30000 000000000180 01020000 00000000 00000000 00030000 00ff0000 00000000 01007a01 842a71b3 00000a00 0c000000 00000000 00000000 01010000 00000000

    m_multicastSearchRequestTimer.setInterval(1000);
    m_multicastSearchRequestTimer.setSingleShot(false);

    connect(m_speedwireInterface, &SpeedwireInterface::dataReceived, this, &SpeedwireDiscovery::processDatagram);
    connect(&m_multicastSearchRequestTimer, &QTimer::timeout, this, &SpeedwireDiscovery::sendDiscoveryRequest);
}

SpeedwireDiscovery::~SpeedwireDiscovery()
{

}

bool SpeedwireDiscovery::startDiscovery()
{
    if (discoveryRunning())
        return true;

    if (!m_speedwireInterface->available()) {
        qCDebug(dcSma()) << "SpeedwireDiscovery: Failed to start discovery because the speedwire interface is not available.";
        return false;
    }

    // Start clean
    m_results.clear();
    m_networkDeviceInfos.clear();

    startUnicastDiscovery();
    startMulticastDiscovery();
    return true;
}

bool SpeedwireDiscovery::discoveryRunning() const
{
    return m_unicastRunning || m_multicastRunning;
}

QList<SpeedwireDiscovery::SpeedwireDiscoveryResult> SpeedwireDiscovery::discoveryResult() const
{
    return m_results;
}

void SpeedwireDiscovery::startMulticastDiscovery()
{
    qCDebug(dcSma()) << "SpeedwireDiscovery: Start multicast discovery...";
    m_multicastRunning = true;
    m_multicastSearchRequestTimer.start();

    // Start sending multicast messages periodically
    sendDiscoveryRequest();
}

void SpeedwireDiscovery::startUnicastDiscovery()
{
    qCDebug(dcSma()) << "SpeedwireDiscovery: Start discovering network...";
    m_unicastRunning = true;

    NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
    connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater);
    connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &SpeedwireDiscovery::sendUnicastDiscoveryRequest);
    connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [this, discoveryReply](){
        qCDebug(dcSma()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().length() << "network devices for unicast requests.";

        m_networkDeviceInfos = discoveryReply->networkDeviceInfos();

        // Wait some extra second in otder to give the last hosts joined some time to respond.
        QTimer::singleShot(3000, this, [this](){
            m_multicastSearchRequestTimer.stop();
            m_multicastRunning = false;

            m_unicastRunning = false;
            evaluateDiscoveryFinished();
        });
    });
}

void SpeedwireDiscovery::sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress)
{
    qCDebug(dcSma()) << "SpeedwireDiscovery: Sent discovery request to unicast address" << targetHostAddress.toString();
    m_speedwireInterface->sendDataUnicast(targetHostAddress, Speedwire::pingRequest(Speedwire::sourceModelId(), m_localSerialNumber));
}

void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quint16 senderPort, const QByteArray &datagram, bool multicast)
{
    Q_UNUSED(multicast)

    // Check min size of SMA datagrams
    if (datagram.size() < 18) {
        qCDebug(dcSma()) << "SpeedwireDiscovery: Received datagram is to short to be a SMA speedwire message. Ignoring data...";
        return;
    }

    QDataStream stream(datagram);
    Speedwire::Header header = Speedwire::parseHeader(stream);
    if (!header.isValid()) {
        qCWarning(dcSma()) << "SpeedwireDiscovery: Datagram header is not valid. Ignoring data...";
        return;
    }

    qCDebug(dcSma()) << "SpeedwireDiscovery:" << header;

    if (header.protocolId == Speedwire::ProtocolIdDiscoveryResponse) {
        qCDebug(dcSma()) << "SpeedwireDiscovery: Received discovery response from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort);

        // "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0003 0004 0020 0000 0001 0004 0030 c0a8 b219 0004 0040 0000 0000 0002 0070 ef0c 00000000"
        // "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0001 0004 0020 0000 0001 0004 0030 c0a8 b216 0004 0040 0000 0001 00000000"

        if (!datagram.startsWith(Speedwire::discoveryResponseDatagram())) {
            qCWarning(dcSma()) << "SpeedwireDiscovery: Received discovery reply but the message start does not match the required schema. Ignoring data...";
            return;
        }

        qCDebug(dcSma()) << "SpeedwireDiscovery: --> Found SMA device on" << senderAddress.toString();
        return;
    }

    // We received SMA data, let's parse depending on the protocol id

    if (header.protocolId == Speedwire::ProtocolIdMeter) {
        // Example: 010e 714369ae
        quint16 modelId;
        quint32 serialNumber;
        stream >> modelId >> serialNumber;
        qCDebug(dcSma()) << "SpeedwireDiscovery: Meter identifier: Model ID:" << modelId << "Serial number:" << serialNumber;

        if (!m_resultMeters.contains(senderAddress)) {
            SpeedwireDiscoveryResult result;
            result.address = senderAddress;
            result.deviceType = Speedwire::DeviceTypeMeter;
            m_resultMeters.insert(senderAddress, result);
        }

        m_resultMeters[senderAddress].modelId = modelId;
        m_resultMeters[senderAddress].serialNumber = serialNumber;
    } else if (header.protocolId == Speedwire::ProtocolIdInverter) {
        Speedwire::InverterPacket inverterPacket = Speedwire::parseInverterPacket(stream);
        // Response from inverter 534d4100 0004 02a0 0000 0001 004e 0010 6065 1390 7d00 52be283a 0000 b500 c2c12e12 0000 0000 00000 1800102000000000000000000000003000000ff0000ecd5ff1f0100b500c2c12e1200000a000c00000000000000030000000101000000000000
        qCDebug(dcSma()) << "SpeedwireDiscovery:" << inverterPacket;

        if (!m_resultInverters.contains(senderAddress)) {
            SpeedwireDiscoveryResult result;
            result.address = senderAddress;
            result.deviceType = Speedwire::DeviceTypeInverter;
            m_resultInverters.insert(senderAddress, result);
        }

        m_resultInverters[senderAddress].modelId = inverterPacket.sourceModelId;
        m_resultInverters[senderAddress].serialNumber = inverterPacket.sourceSerialNumber;

        // Send the default login request, maybe it activates the Energy meter measurment data streaming

        SpeedwireInverter *inverter = nullptr;
        if (m_inverters.contains(senderAddress)) {
            inverter = m_inverters.value(senderAddress);
        } else {
            inverter = new SpeedwireInverter(m_speedwireInterface, senderAddress, Speedwire::sourceModelId(), m_localSerialNumber, this);
            m_inverters.insert(senderAddress, inverter);
        }

        SpeedwireInverterReply *reply = inverter->sendIdentifyRequest();
        qCDebug(dcSma()) << "SpeedwireDiscovery: Send identify request to" << senderAddress.toString();
        connect(reply, &SpeedwireInverterReply::finished, this, [/*this, inverter,*/ senderAddress, reply](){
            qCDebug(dcSma()) << "SpeedwireDiscovery: Identify request finished from" << senderAddress.toString() << reply->error();

            // SpeedwireInverterReply *loginReply = inverter->sendLoginRequest();
            // qCDebug(dcSma()) << "SpeedwireDiscovery: make login attempt using the default password.";
            // connect(loginReply, &SpeedwireInverterReply::finished, this, [loginReply, senderAddress](){
            //     qCDebug(dcSma()) << "SpeedwireDiscovery: login attempt finished" << senderAddress.toString() << loginReply->error();
            // });
        });

    } else {
        qCWarning(dcSma()) << "SpeedwireDiscovery: Unhandled data received" << datagram.toHex();
        return;
    }
}

void SpeedwireDiscovery::sendDiscoveryRequest()
{
    qCDebug(dcSma()) << "SpeedwireDiscovery: Sent discovery request to multicast address" << Speedwire::multicastAddress().toString();
    m_speedwireInterface->sendDataMulticast(Speedwire::discoveryDatagramMulticast());
}

void SpeedwireDiscovery::evaluateDiscoveryFinished()
{
    if (!m_multicastRunning && !m_unicastRunning) {
        finishDiscovery();
    }
}

void SpeedwireDiscovery::finishDiscovery()
{
    m_results = m_resultMeters.values() + m_resultInverters.values();

    // Fill in all network device infos we have
    for (int i = 0; i < m_results.length(); i++)
        m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address);

    qCDebug(dcSma()) << "SpeedwireDiscovery: Discovey finished. Found" << m_results.length() << "SMA devices in the network";
    m_multicastSearchRequestTimer.stop();

    foreach (const SpeedwireDiscoveryResult &result, m_results) {
        qCDebug(dcSma()) << "SpeedwireDiscovery: ============================================";
        qCDebug(dcSma()) << "SpeedwireDiscovery: Device type:" << result.deviceType;
        qCDebug(dcSma()) << "SpeedwireDiscovery: Address:" << result.address.toString();
        qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << result.modelId;
        qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << result.serialNumber;
        if (result.networkDeviceInfo.isValid()) {
            qCDebug(dcSma()) << "SpeedwireDiscovery:" << result.networkDeviceInfo;
        }
    }

    emit discoveryFinished();
}

