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

#include <modbusdatautils.h>

PceWallbox::PceWallbox(const QHostAddress &hostAddress, uint port, quint16 slaveId, QObject *parent)
    : EV11ModbusTcpConnection{hostAddress, port, slaveId, parent}
{
    // Timer for resetting the heartbeat register (watchdog)
    m_timer.setInterval(30000);
    m_timer.setSingleShot(false);
    connect(&m_timer, &QTimer::timeout, this, &PceWallbox::sendHeartbeat);

    connect(this, &EV11ModbusTcpConnection::reachableChanged, this, [this](bool reachable) {
        if (!reachable) {
            m_timer.stop();

            cleanupQueues();

            if (m_currentReply) {
                m_currentReply = nullptr;
            }

        } else {
            initialize();
        }
    });

    connect(this, &EV11ModbusTcpConnection::initializationFinished, this, [this](bool success) {
        if (success) {
            qCDebug(dcPcElectric()) << "Connection initialized successfully" << m_modbusTcpMaster->hostAddress().toString();
            m_timer.start();

            sendHeartbeat();
            update();

        } else {
            qCWarning(dcPcElectric()) << "Connection initialization failed for" << m_modbusTcpMaster->hostAddress().toString();
        }
    });
}

bool PceWallbox::update()
{
    if (m_aboutToDelete)
        return false;

    if (!reachable())
        return false;

    // Make sure we only have one update call in the queue
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == readBlockInitInfosDataUnit().startAddress()) {
            return true;
        }
    }

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, readBlockStatusDataUnit(), this);
    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        if (reply->error() != QModbusDevice::NoError) {
            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
            return;
        }

        const QModbusDataUnit unit = reply->reply()->result();
        const QVector<quint16> blockValues = unit.values();
        processBlockStatusRegisterValues(blockValues);

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
    });

    enqueueRequest(reply);

    // charging current register. Contains
    // - power state
    // - chargingcurrent (if power is true)
    // - phases (if power is true)
    bool chargingCurrentQueued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == chargingCurrentDataUnit().startAddress()) {
            chargingCurrentQueued = true;
            break;
        }
    }

    if (!chargingCurrentQueued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, chargingCurrentDataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> values = unit.values();
            processChargingCurrentRegisterValues(values);

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    // Digital input
    bool digitalInputAlreadyQueued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == digitalInputModeDataUnit().startAddress()) {
            digitalInputAlreadyQueued = true;
            break;
        }
    }

    if (!digitalInputAlreadyQueued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, digitalInputModeDataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> values = unit.values();
            processDigitalInputModeRegisterValues(values);

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    // Led brightness
    bool ledBrightnessAlreadyQueued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == ledBrightnessDataUnit().startAddress()) {
            ledBrightnessAlreadyQueued = true;
            break;
        }
    }

    if (!ledBrightnessAlreadyQueued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, ledBrightnessDataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> values = unit.values();
            processLedBrightnessRegisterValues(values);

            if (firmwareRevision() < "0025")
                emit updateFinished();

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    if (firmwareRevision() < "0025")
        return true;

    // ---------------------------------------------------------------------------------------
    // Registers since 0025 (V 0.25)

    // Make sure we only have one update 2 call in the queue
    bool update2Queued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == readBlockUpdate2DataUnit().startAddress()) {
            update2Queued = true;
            break;
        }
    }

    if (!update2Queued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, readBlockUpdate2DataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                qCWarning(dcPcElectric()) << "Failed to fetch update 2 block" << reply->error() << reply->errorString();
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> blockValues = unit.values();
            processBlockUpdate2RegisterValues(blockValues);

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    bool phaseAutoSwitchPauseQueued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == phaseAutoSwitchPauseDataUnit().startAddress()) {
            phaseAutoSwitchPauseQueued = true;
            break;
        }
    }

    if (!phaseAutoSwitchPauseQueued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, phaseAutoSwitchPauseDataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> values = unit.values();
            processPhaseAutoSwitchPauseRegisterValues(values);

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    // Phase auto switch pause (since firmware version 0.25 ...)
    bool phaseAutoSwitchMinChargingTimeQueued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == phaseAutoSwitchMinChargingTimeDataUnit().startAddress()) {
            phaseAutoSwitchMinChargingTimeQueued = true;
            break;
        }
    }

    if (!phaseAutoSwitchMinChargingTimeQueued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, phaseAutoSwitchMinChargingTimeDataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> values = unit.values();
            processPhaseAutoSwitchMinChargingTimeRegisterValues(values);

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    // Phase auto switch pause (since firmware version 0.25 ...)
    bool forceChargingResumeQueued = false;
    foreach (QueuedModbusReply *r, m_readQueue) {
        if (r->dataUnit().startAddress() == forceChargingResumeDataUnit().startAddress()) {
            forceChargingResumeQueued = true;
            break;
        }
    }

    if (!forceChargingResumeQueued) {
        reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeRead, forceChargingResumeDataUnit(), this);
        connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
        connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
            if (m_currentReply == reply)
                m_currentReply = nullptr;

            if (reply->error() != QModbusDevice::NoError) {
                QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
                return;
            }

            const QModbusDataUnit unit = reply->reply()->result();
            const QVector<quint16> values = unit.values();
            processForceChargingResumeRegisterValues(values);

            emit updateFinished();

            QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        });

        enqueueRequest(reply);
    }

    return true;
}

QueuedModbusReply *PceWallbox::setChargingCurrentAsync(quint16 chargingCurrent)
{
    if (m_aboutToDelete)
        return nullptr;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setChargingCurrentDataUnit(chargingCurrent), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
    return reply;
}

QueuedModbusReply *PceWallbox::setLedBrightnessAsync(quint16 percentage)
{
    if (m_aboutToDelete)
        return nullptr;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setLedBrightnessDataUnit(percentage), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
    return reply;
}

QueuedModbusReply *PceWallbox::setPhaseAutoSwitchPauseAsync(quint16 seconds)
{
    if (m_aboutToDelete)
        return nullptr;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setPhaseAutoSwitchPauseDataUnit(seconds), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
    return reply;
}

QueuedModbusReply *PceWallbox::setPhaseAutoSwitchMinChargingTimeAsync(quint16 seconds)
{
    if (m_aboutToDelete)
        return nullptr;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setPhaseAutoSwitchMinChargingTimeDataUnit(seconds), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
    return reply;
}

QueuedModbusReply *PceWallbox::setForceChargingResumeAsync(quint16 value)
{
    if (m_aboutToDelete)
        return nullptr;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setForceChargingResumeDataUnit(value), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
    return reply;
}

QueuedModbusReply *PceWallbox::setDigitalInputModeAsync(DigitalInputMode digitalInputMode)
{
    if (m_aboutToDelete)
        return nullptr;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setDigitalInputModeDataUnit(digitalInputMode), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
    return reply;
}

void PceWallbox::gracefullDeleteLater()
{
    // Clean up the queue
    m_aboutToDelete = true;
    cleanupQueues();

    m_timer.stop();

    if (!m_currentReply) {
        qCDebug(dcPcElectric()) << "Deleting object without pending request...";
        // No pending request, we can close the connection and delete the object
        disconnect(this, nullptr, nullptr, nullptr);
        disconnectDevice();
        deleteLater();
    } else {
        qCDebug(dcPcElectric()) << "Pending request, deleting object once the request is finished...";
    }
}

quint16 PceWallbox::deriveRegisterFromStates(PceWallbox::ChargingCurrentState state)
{
    quint16 registerValue = 0;
    if (!state.power)
        return registerValue; // 0

    registerValue = state.maxChargingCurrent * 1000; // convert to mA
    if (state.desiredPhaseCount > 1) {
        registerValue |= static_cast<quint16>(1) << 15;
    }

    return registerValue;
}

PceWallbox::ChargingCurrentState PceWallbox::deriveStatesFromRegister(quint16 registerValue)
{
    PceWallbox::ChargingCurrentState chargingCurrentState;
    chargingCurrentState.power = (registerValue != 0);

    // Only set max charging current if power, otherwise we use default 6A
    if (chargingCurrentState.power) {
        bool threePhaseCharging = (registerValue & (1 << 15));
        chargingCurrentState.desiredPhaseCount = (threePhaseCharging ? 3 : 1);

        chargingCurrentState.maxChargingCurrent = (registerValue & 0x7FFF) / 1000.0;
    }

    return chargingCurrentState;
}

void PceWallbox::sendHeartbeat()
{
    if (m_aboutToDelete)
        return;

    QueuedModbusReply *reply = new QueuedModbusReply(QueuedModbusReply::RequestTypeWrite, setHeartbeatDataUnit(m_heartbeat++), this);

    connect(reply, &QueuedModbusReply::finished, reply, &QueuedModbusReply::deleteLater);
    connect(reply, &QueuedModbusReply::finished, this, [this, reply]() {
        if (m_currentReply == reply)
            m_currentReply = nullptr;

        if (reply->error() != QModbusDevice::NoError) {
            qCWarning(dcPcElectric()) << "Failed to send heartbeat to" << m_modbusTcpMaster->hostAddress().toString() << reply->errorString();
        } else {
            qCDebug(dcPcElectric()) << "Successfully sent heartbeat to" << m_modbusTcpMaster->hostAddress().toString();
        }

        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    });

    enqueueRequest(reply);
}

void PceWallbox::sendNextRequest()
{
    if (m_writeQueue.isEmpty() && m_readQueue.isEmpty())
        return;

    if (m_currentReply)
        return;

    if (m_aboutToDelete) {
        disconnect(this, nullptr, nullptr, nullptr);
        disconnectDevice();
        deleteLater();
        return;
    }

    // Note: due to the fact that we have one register which controls 3 states,
    // the order of the execution is critical at this point. We have to make sure
    // the register gets written in the same order as they where requested by the action
    // execution (and the dedicated ChargingCurrentState buffer)

    if (!m_writeQueue.isEmpty()) {
        // Prioritize write requests
        m_currentReply = m_writeQueue.dequeue();
        qCDebug(dcPcElectric()) << "Dequeued write request. Queue count: W" << m_writeQueue.length() << "| R:" << m_readQueue.length();
    } else {
        m_currentReply = m_readQueue.dequeue();
        qCDebug(dcPcElectric()) << "Dequeued read request. Queue count: W" << m_writeQueue.length() << "| R:" << m_readQueue.length();
    }

    switch (m_currentReply->requestType()) {
    case QueuedModbusReply::RequestTypeRead:
        qCDebug(dcPcElectric())
            << "--> Reading"
            << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType())
            << "register:"
            << m_currentReply->dataUnit().startAddress()
            << "length"
            << m_currentReply->dataUnit().valueCount();
        m_currentReply->setReply(m_modbusTcpMaster->sendReadRequest(m_currentReply->dataUnit(), m_slaveId));
        break;
    case QueuedModbusReply::RequestTypeWrite:
        qCDebug(dcPcElectric())
            << "--> Writing"
            << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType())
            << "register:"
            << m_currentReply->dataUnit().startAddress()
            << "length:"
            << m_currentReply->dataUnit().valueCount()
            << "values:"
            << m_currentReply->dataUnit().values();
        m_currentReply->setReply(m_modbusTcpMaster->sendWriteRequest(m_currentReply->dataUnit(), m_slaveId));
        break;
    }

    if (!m_currentReply->reply()) {
        qCWarning(dcPcElectric())
            << "Error occurred while sending"
            << m_currentReply->requestType()
            << ModbusDataUtils::registerTypeToString(m_currentReply->dataUnit().registerType())
            << "register:"
            << m_currentReply->dataUnit().startAddress()
            << "length:"
            << m_currentReply->dataUnit().valueCount()
            << "to"
            << m_modbusTcpMaster->hostAddress().toString()
            << m_modbusTcpMaster->errorString();
        m_currentReply->deleteLater();
        m_currentReply = nullptr;
        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    }

    if (m_currentReply->reply()->isFinished()) {
        qCWarning(dcPcElectric()) << "Reply immediatly finished";
        m_currentReply->deleteLater();
        m_currentReply = nullptr;
        QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
        return;
    }
}

void PceWallbox::enqueueRequest(QueuedModbusReply *reply)
{
    switch (reply->requestType()) {
    case QueuedModbusReply::RequestTypeRead:
        m_readQueue.enqueue(reply);
        break;
    case QueuedModbusReply::RequestTypeWrite:
        m_writeQueue.enqueue(reply);
        break;
    }

    QTimer::singleShot(0, this, &PceWallbox::sendNextRequest);
}

void PceWallbox::cleanupQueues()
{
    qDeleteAll(m_readQueue);
    m_readQueue.clear();

    qDeleteAll(m_writeQueue);
    m_writeQueue.clear();
}

QDebug operator<<(QDebug debug, const PceWallbox::ChargingCurrentState &chargingCurrentState)
{
    QDebugStateSaver saver(debug);
    debug.nospace()
        << "ChargingCurrentState("
        << chargingCurrentState.power
        << ", "
        << chargingCurrentState.maxChargingCurrent
        << " [A], "
        << chargingCurrentState.desiredPhaseCount
        << ')';
    return debug;
}
