// 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 "neuroncommon.h"
#include "extern-plugininfo.h"

NeuronCommon::NeuronCommon(QModbusClient *modbusInterface, int slaveAddress, QObject *parent) :
    QObject(parent),
    m_slaveAddress(slaveAddress),
    m_modbusInterface(modbusInterface)
{
    m_inputPollingTimer = new QTimer(this);
    connect(m_inputPollingTimer, &QTimer::timeout, this, &NeuronCommon::onInputPollingTimer);
    m_inputPollingTimer->setTimerType(Qt::TimerType::PreciseTimer);
    m_inputPollingTimer->setInterval(200);

    m_outputPollingTimer = new QTimer(this);
    connect(m_outputPollingTimer, &QTimer::timeout, this, &NeuronCommon::onOutputPollingTimer);
    m_outputPollingTimer->setTimerType(Qt::TimerType::PreciseTimer);
    m_outputPollingTimer->setInterval(1000);

    if (m_modbusInterface->state() == QModbusDevice::State::ConnectedState) {
        m_inputPollingTimer->start();
        m_outputPollingTimer->start();
    }

    connect(m_modbusInterface, &QModbusDevice::stateChanged, this, [this] (QModbusDevice::State state) {
        if (state == QModbusDevice::State::ConnectedState) {
            if (m_inputPollingTimer)
                m_inputPollingTimer->start();
            if (m_outputPollingTimer)
                m_outputPollingTimer->start();
            emit connectionStateChanged(true);
        } else {
            if (m_inputPollingTimer)
                m_inputPollingTimer->stop();
            if (m_outputPollingTimer)
                m_outputPollingTimer->stop();
            emit connectionStateChanged(false);
        }
    });
}

bool NeuronCommon::init()
{
    qCDebug(dcUniPi()) << "Neuron: Init";
    if (!loadModbusMap()) {
        return false;
    }

    if (!m_modbusInterface) {
        qWarning(dcUniPi()) << "Neuron: Modbus interface not available";
        return false;
    }

    if (m_modbusInterface->connectDevice()) {
        qWarning(dcUniPi()) << "Neuron: Could not connect to modbus device";
        return  false;
    }
    return true;
}

int NeuronCommon::slaveAddress()
{
    return m_slaveAddress;
}

void NeuronCommon::setSlaveAddress(int slaveAddress)
{
    qCDebug(dcUniPi()) << "Neuron: Set slave address" << slaveAddress;
    m_slaveAddress = slaveAddress;
}

QList<QString> NeuronCommon::digitalInputs()
{
    return m_modbusDigitalInputRegisters.keys();
}

QList<QString> NeuronCommon::digitalOutputs()
{
    return m_modbusDigitalOutputRegisters.keys();
}

QList<QString> NeuronCommon::analogInputs()
{
    QList<QString> circuits;
    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogInputRegisters.values()) {
        circuits.append(descriptor.circuit);
    }
    return circuits;
}

QList<QString> NeuronCommon::analogOutputs()
{
    QList<QString> circuits;
    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) {
        circuits.append(descriptor.circuit);
    }
    return circuits;
}

QList<QString> NeuronCommon::userLEDs()
{
    return m_modbusUserLEDRegisters.keys();
}

NeuronCommon::RegisterDescriptor NeuronCommon::registerDescriptorFromStringList(const QStringList &data)
{
    RegisterDescriptor descriptor;
    if (data.length() < 7) {
        return descriptor;
    }
    descriptor.address = data[0].toInt();
    descriptor.count = data[2].toInt();
    if (data[3] == "RW") {
        descriptor.readWrite = RWPermissionReadWrite;
    } else if (data[3] == "W") {
        descriptor.readWrite = RWPermissionWrite;
    } else if (data[3] == "R") {
        descriptor.readWrite = RWPermissionRead;
    }
    descriptor.circuit = data[5].split(" ").last();
    descriptor.category = data.last();

    if (data[5].contains("Analog Input Value", Qt::CaseSensitivity::CaseInsensitive)) {
        descriptor.registerType = QModbusDataUnit::RegisterType::InputRegisters;
    } else if (data[5].contains("Analog Output Value", Qt::CaseSensitivity::CaseInsensitive)) {
        descriptor.registerType = QModbusDataUnit::RegisterType::HoldingRegisters;
    }
    return descriptor;
}

bool NeuronCommon::circuitValueChanged(const QString &circuit, quint32 value)
{
    if (m_previousCircuitValue.contains(circuit)) {
        if (m_previousCircuitValue.value(circuit) == value) {
            // Only emit status change of the circuit value has changed
            return false;
        } else  {
            m_previousCircuitValue.insert(circuit, value); //update existing value
            return true;
        }
    } else {
        m_previousCircuitValue.insert(circuit, value);
        return true;
    }
}

void NeuronCommon::getAllDigitalInputs()
{
    getCoils(m_modbusDigitalInputRegisters.values());
}

void NeuronCommon::getAllDigitalOutputs()
{
    getCoils(m_modbusDigitalOutputRegisters.values());
}

void NeuronCommon::getAllAnalogInputs()
{
    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogInputRegisters.values()) {
        getAnalogIO(descriptor);
    }
}

void NeuronCommon::getAllAnalogOutputs()
{
    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) {
        getAnalogIO(descriptor);
    }
}

bool NeuronCommon::getDigitalInput(const QString &circuit)
{
    if (!m_modbusDigitalInputRegisters.contains(circuit)) {
        qCWarning(dcUniPi()) << "Neuron: Digital input circuit not found" << circuit;
        return "";
    }
    int modbusAddress = m_modbusDigitalInputRegisters.value(circuit);
    //qDebug(dcUniPi()) << "Neuron: Reading digital Input" << circuit << modbusAddress;

    QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1);
    if (m_readRequestQueue.isEmpty()) {
        return modbusReadRequest(request);
    } else if (m_readRequestQueue.length() > 100) {
        qCWarning(dcUniPi()) << "Neuron: Too many pending read requests";
        return false;
    } else {
        m_readRequestQueue.append(request);
    }
    return true;
}

bool NeuronCommon::getAnalogOutput(const QString &circuit)
{
    //qDebug(dcUniPi()) << "Neuron: Get analog output" << circuit;
    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) {
        if (descriptor.circuit == circuit) {
            return getAnalogIO(descriptor);
        }
    }
    qCWarning(dcUniPi()) << "Neuron: Analog output circuit not found" << circuit;
    return false;
}


QUuid NeuronCommon::setDigitalOutput(const QString &circuit, bool value)
{
    if (!m_modbusDigitalOutputRegisters.contains(circuit)) {
        qCWarning(dcUniPi()) << "Neuron: Digital output circuit not found" << circuit;
        return QUuid();
    }
    int modbusAddress = m_modbusDigitalOutputRegisters.value(circuit);
    //qDebug(dcUniPi()) << "Neuron: Setting digital ouput" << circuit << modbusAddress << value;

    Request request;
    request.data = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1);
    request.data.setValue(0, static_cast<uint16_t>(value));
    request.id = QUuid::createUuid();

    if (m_writeRequestQueue.isEmpty()) {
        modbusWriteRequest(request);
    } else if (m_writeRequestQueue.length() > 100) {
        return QUuid();
    } else {
        m_writeRequestQueue.append(request);
    }
    return request.id;
}


bool NeuronCommon::getDigitalOutput(const QString &circuit)
{
    if (!m_modbusDigitalOutputRegisters.contains(circuit)) {
        qCWarning(dcUniPi()) << "Neuron: Digital output circuit not found" << circuit;
        return false;
    }
    int modbusAddress = m_modbusDigitalOutputRegisters.value(circuit);
    //qDebug(dcUniPi()) << "Reading digital Output" << circuit << modbusAddress;

    QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, modbusAddress, 1);
    if (m_readRequestQueue.isEmpty()) {
        return modbusReadRequest(request);
    } else if (m_readRequestQueue.length() > 100) {
        qCWarning(dcUniPi()) << "Neuron: Too many pending read requests";
        return false;
    } else {
        m_readRequestQueue.append(request);
    }
    return true;
}


QUuid NeuronCommon::setAnalogOutput(const QString &circuit, double value)
{
    qDebug(dcUniPi()) << "Neuron: Set analog output" << circuit << value;

    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters) {
        if (descriptor.circuit == circuit) {
            Request request;
            request.id = QUuid::createUuid();
            request.data = QModbusDataUnit(QModbusDataUnit::RegisterType::HoldingRegisters, descriptor.address, descriptor.count);
            if (descriptor.count == 1) {
                request.data.setValue(0, (static_cast<uint>(value*400))); // 0 to 4000 = 0 to 10.0 V
            } else if (descriptor.count == 2) {
                request.data.setValue(0, (static_cast<uint32_t>(value) >> 16));
                request.data.setValue(1, (static_cast<uint32_t>(value) & 0xffff));
            }

            if (m_writeRequestQueue.isEmpty()) {
                modbusWriteRequest(request);
            } else if (m_writeRequestQueue.length() > 100) {
                return QUuid();
            } else {
                m_writeRequestQueue.append(request);
            }
            return request.id;
        }
    }
    qCWarning(dcUniPi()) << "Neuron: Analog output circuit not found" << circuit;
    return QUuid();
}


bool NeuronCommon::getAnalogInput(const QString &circuit)
{
    //qDebug(dcUniPi()) << "Neuron: Get analog input" << circuit;

    Q_FOREACH(RegisterDescriptor descriptor, m_modbusAnalogOutputRegisters.values()) {
        if (descriptor.circuit == circuit) {
            return getAnalogIO(descriptor);
        }
    }
    return false;
}

QUuid NeuronCommon::setUserLED(const QString &circuit, bool value)
{
    int modbusAddress = m_modbusUserLEDRegisters.value(circuit);
    //qDebug(dcUniPi()) << "Neuron: Setting user led" << circuit << modbusAddress << value;

    if (!m_modbusInterface)
        return QUuid();

    Request request;
    request.id = QUuid::createUuid();

    request.data = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1);
    request.data.setValue(0, static_cast<uint16_t>(value));

    if (m_writeRequestQueue.isEmpty()) {
        if (!modbusWriteRequest(request)) {
            return QUuid();
        }
    } else if (m_writeRequestQueue.length() > 100) {
        return QUuid();
    } else {
        m_writeRequestQueue.append(request);
    }

    return request.id;
}


bool NeuronCommon::getUserLED(const QString &circuit)
{
    int modbusAddress = m_modbusUserLEDRegisters.value(circuit);
    //qDebug(dcUniPi()) << "Neuron: Get user LED" << circuit << modbusAddress;

    QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, modbusAddress, 1);
    if (m_readRequestQueue.isEmpty()) {
        return modbusReadRequest(request);
    } else if (m_readRequestQueue.length() > 100) {
        qCWarning(dcUniPi()) << "Neuron: Too many pending read requests";
        return false;
    } else {
        m_readRequestQueue.append(request);
    }
    return true;
}

bool NeuronCommon::getAnalogIO(const RegisterDescriptor &descriptor)
{
    if (!m_modbusInterface)
        return false;

    if (m_modbusInterface->state() != QModbusDevice::State::ConnectedState)
        return false;

    QModbusDataUnit request = QModbusDataUnit(descriptor.registerType, descriptor.address, descriptor.count);
    if (m_readRequestQueue.isEmpty()) {
        return modbusReadRequest(request);
    } else if (m_readRequestQueue.length() > 100) {
        qCWarning(dcUniPi()) << "Neuron: Too many pending read requests";
        return false;
    } else {
        m_readRequestQueue.append(request);
    }
    return true;
}

bool NeuronCommon::modbusWriteRequest(const Request &request)
{
    if (!m_modbusInterface) {
        emit requestExecuted(request.id, false);
        emit requestError(request.id, "Modbus interface not available");
        return false;
    }
    if (m_modbusInterface->state() != QModbusDevice::State::ConnectedState) {
        emit requestExecuted(request.id, false);
        emit requestError(request.id, "Device not connected");
        return false;
    };

    if (QModbusReply *reply = m_modbusInterface->sendWriteRequest(request.data, m_slaveAddress)) {
        if (!reply->isFinished()) {
            connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
            connect(reply, &QModbusReply::finished, this, [reply, request, this] {

                if (!m_writeRequestQueue.isEmpty()) {
                    modbusWriteRequest(m_writeRequestQueue.takeFirst());
                }

                if (reply->error() == QModbusDevice::NoError) {
                    emit requestExecuted(request.id, true);
                    const QModbusDataUnit unit = reply->result();
                    int modbusAddress = unit.startAddress();
                    if(m_modbusDigitalOutputRegisters.values().contains(modbusAddress)){
                        QString circuit = m_modbusDigitalOutputRegisters.key(modbusAddress);
                        emit digitalOutputStatusChanged(circuit, unit.value(0));
                    } else if(m_modbusAnalogOutputRegisters.contains(modbusAddress)){
                        QString circuit = m_modbusAnalogOutputRegisters.value(modbusAddress).circuit;
                        emit analogOutputStatusChanged(circuit, unit.value(0));
                    } else if(m_modbusUserLEDRegisters.values().contains(modbusAddress)){
                        QString circuit = m_modbusUserLEDRegisters.key(modbusAddress);
                        emit userLEDStatusChanged(circuit, unit.value(0));
                    }
                } else {
                    emit requestExecuted(request.id, false);
                    qCWarning(dcUniPi()) << "Neuron: Write response error:" << reply->error();
                    emit requestError(request.id, reply->errorString());
                }
            });
            QTimer::singleShot(m_responseTimeoutTime, reply, &QModbusReply::deleteLater);
        } else {
            reply->deleteLater(); // broadcast replies return immediately
            return false;
        }
    } else {
        qCWarning(dcUniPi()) << "Neuron: Read error: " << m_modbusInterface->errorString();
        return false;
    }
    return true;
}


bool NeuronCommon::modbusReadRequest(const QModbusDataUnit &request)
{
    if (!m_modbusInterface) {
        return false;
    }
    if (m_modbusInterface->state() != QModbusDevice::State::ConnectedState)
        return false;

    if (QModbusReply *reply = m_modbusInterface->sendReadRequest(request, m_slaveAddress)) {
        if (!reply->isFinished()) {
            connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater);
            connect(reply, &QModbusReply::finished, this, [reply, this] {

                int modbusAddress = 0;

                if (reply->error() == QModbusDevice::NoError) {
                    const QModbusDataUnit unit = reply->result();

                    for (uint i = 0; i < unit.valueCount(); i++) {
                        //qCDebug(dcUniPi()) << "Start Address:" << unit.startAddress() << "Register Type:" << unit.registerType() << "Value:" << unit.value(i);
                        modbusAddress = unit.startAddress() + i;

                        QString circuit;
                        switch (unit.registerType()) {
                        case QModbusDataUnit::RegisterType::Coils:
                            if(m_modbusDigitalInputRegisters.values().contains(modbusAddress)){
                                circuit = m_modbusDigitalInputRegisters.key(modbusAddress);
                                if (circuitValueChanged(circuit, unit.value(i)))
                                    emit digitalInputStatusChanged(circuit, unit.value(i));
                            } else if(m_modbusDigitalOutputRegisters.values().contains(modbusAddress)){
                                circuit = m_modbusDigitalOutputRegisters.key(modbusAddress);
                                if (circuitValueChanged(circuit, unit.value(i)))
                                    emit digitalOutputStatusChanged(circuit, unit.value(i));
                            } else if(m_modbusUserLEDRegisters.values().contains(modbusAddress)){
                                circuit = m_modbusUserLEDRegisters.key(modbusAddress);
                                if (circuitValueChanged(circuit, unit.value(i)))
                                    emit userLEDStatusChanged(circuit, unit.value(i));
                            } else {
                                qCWarning(dcUniPi()) << "Neuron: Received unrecognised coil register" << modbusAddress;
                            }
                            break;

                        case QModbusDataUnit::RegisterType::HoldingRegisters: {
                            if (m_modbusAnalogOutputRegisters.keys().contains(modbusAddress)) {
                                RegisterDescriptor descriptor =  m_modbusAnalogOutputRegisters.value(modbusAddress);
                                circuit = descriptor.circuit;
                                quint32 value = 0;
                                if (descriptor.count == 1) {
                                    value = unit.value(i);
                                } else if (descriptor.count == 2) {
                                    if (unit.valueCount() >= (i+1)) {
                                        value = (unit.value(i) << 16 | unit.value(i+1));
                                        i++;
                                    } else {
                                        qCWarning(dcUniPi()) << "Neuron: Received analog output, but value count is too short";
                                    }
                                }
                                if (circuitValueChanged(circuit, value))
                                    emit analogOutputStatusChanged(circuit, value);

                            } else {
                                qCWarning(dcUniPi()) << "Neuron: Received unrecognised holding register" << modbusAddress;
                            }
                        } break;
                        case QModbusDataUnit::RegisterType::InputRegisters:
                            if(m_modbusAnalogInputRegisters.keys().contains(modbusAddress)){
                                RegisterDescriptor descriptor = m_modbusAnalogInputRegisters.value(modbusAddress);
                                circuit = descriptor.circuit;
                                quint32 value = 0;
                                if (descriptor.count == 1) {
                                    value = unit.value(i);
                                } else if (descriptor.count == 2) {
                                    if (unit.valueCount() >= (i+1)) {
                                        value = (unit.value(i) << 16 | unit.value(i+1));
                                        i++;
                                    } else {
                                        qCWarning(dcUniPi()) << "Neuron: Received analog input, but value count is too short";
                                    }
                                }
                                if (circuitValueChanged(circuit, value))
                                    emit analogInputStatusChanged(circuit, value);

                            } else {
                                qCWarning(dcUniPi()) << "Neuron: Received unrecognised input register" << modbusAddress;
                            }
                            break;
                        case QModbusDataUnit::RegisterType::DiscreteInputs:
                        case QModbusDataUnit::RegisterType::Invalid:
                            qCWarning(dcUniPi()) << "Neuron: Invalide register type";
                            break;
                        }
                    }
                } else if (reply->error() == QModbusDevice::ProtocolError) {
                    qCWarning(dcUniPi()) << "Neuron: Read response error:" << reply->errorString() << reply->rawResult().exceptionCode();
                } else {
                    qCWarning(dcUniPi()) << "Neuron: Read response error:" << reply->error() << reply->errorString();
                }
            });
            QTimer::singleShot(m_responseTimeoutTime, reply, &QModbusReply::deleteLater);
        } else {
            reply->deleteLater(); // broadcast replies return immediately
            return false;
        }
    } else {
        qCWarning(dcUniPi()) << "Neuron: Read error: " << m_modbusInterface->errorString();
        return false;
    }
    return true;
}

void NeuronCommon::getCoils(QList<int> registerList)
{
    if (registerList.isEmpty()) {
        return;
    }

    std::sort(registerList.begin(), registerList.end());
    int previousReg = registerList.first(); //first register to read and starting point to get the following registers
    int startAddress;

    QHash<int, int> registerGroups;

    foreach (int reg, registerList) {
        //qDebug(dcUniPi()) << "Register" << reg << "previous Register" << previousReg;
        if (reg == previousReg) { //first register
            startAddress = reg;
            registerGroups.insert(startAddress, 1);
        } else if (reg == (previousReg + 1)) { //next register in block
            previousReg = reg;
            registerGroups.insert(startAddress, (registerGroups.value(startAddress) + 1)); //update block length
        } else {    // new block
            startAddress = reg;
            previousReg = reg;
            registerGroups.insert(startAddress, 1);
        }
    }

    foreach (int startAddress, registerGroups.keys()) {
        QModbusDataUnit request = QModbusDataUnit(QModbusDataUnit::RegisterType::Coils, startAddress, registerGroups.value(startAddress));
        if (m_readRequestQueue.isEmpty()) {
            modbusReadRequest(request);
        } else if (m_readRequestQueue.length() > 100) {
            qCWarning(dcUniPi()) << "Neuron: Too many pending read requests";
        } else {
            m_readRequestQueue.append(request);
        }
    }
}

void NeuronCommon::onOutputPollingTimer()
{
    getAllDigitalOutputs();
    getAllAnalogOutputs();
}

void NeuronCommon::onInputPollingTimer()
{
    getAllDigitalInputs();
    getAllAnalogInputs();
}
