/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2024, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project 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 this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#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();

            qDeleteAll(m_queue);
            m_queue.clear();

            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_queue) {
        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) {
            emit updateFinished();
            sendNextRequest();
            return;
        }

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

        emit updateFinished();
        sendNextRequest();
    });

    enqueueRequest(reply);
    return true;
}

QueuedModbusReply *PceWallbox::setChargingCurrent(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;

        sendNextRequest();
        return;
    });

    enqueueRequest(reply, true);
    return reply;
}

QueuedModbusReply *PceWallbox::setLedBrightness(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;

        sendNextRequest();
        return;
    });

    enqueueRequest(reply, true);
    return reply;
}

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

    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...";
    }
}

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();
        }

        sendNextRequest();
        return;
    });

    enqueueRequest(reply, true);
}

void PceWallbox::sendNextRequest()
{
    if (m_queue.isEmpty())
        return;

    if (m_currentReply)
        return;

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

    m_currentReply = m_queue.dequeue();
    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;
        sendNextRequest();
        return;
    }

    if (m_currentReply->reply()->isFinished()) {
        qCWarning(dcPcElectric()) << "Reply immediatly finished";
        m_currentReply->deleteLater();
        m_currentReply = nullptr;
        sendNextRequest();
        return;
    }
}

void PceWallbox::enqueueRequest(QueuedModbusReply *reply, bool prepend)
{
    if (prepend) {
        m_queue.prepend(reply);
    } else {
        m_queue.enqueue(reply);
    }

    sendNextRequest();
}

void PceWallbox::cleanupQueue()
{
    qDeleteAll(m_queue);
    m_queue.clear();
}
