// 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-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s 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-energy-plugin-nymea.s 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-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include "evcharger.h"

#include "types/action.h"
#include "integrations/thingmanager.h"

#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)

EvCharger::EvCharger(ThingManager *thingManager, Thing *parent):
    QObject{parent},
    m_thingManager(thingManager),
    m_thing(parent)
{
    m_lockEvaluationTimer.setInterval(1000);
    m_lockEvaluationTimer.setSingleShot(true);
    connect(&m_lockEvaluationTimer, &QTimer::timeout, this, [this](){
        evaluateChargingEnabledLock();

        if (m_chargingEnabledLocked) {
            m_lockEvaluationTimer.start();
            m_remainingChargingEnabledLockDuration = m_chargingEnabledLockDuration - m_lastChargingEnabledAction.secsTo(QDateTime::currentDateTime());
            emit remainingChargingEnabledLockDurationChanged(m_remainingChargingEnabledLockDuration);
        }
    });
}

ThingId EvCharger::id() const
{
    return m_thing->id();
}

Thing *EvCharger::thing() const
{
    return m_thing;
}

QString EvCharger::name() const
{
    return m_thing->name();
}

int EvCharger::chargingEnabledLockDuration() const
{
    return m_chargingEnabledLockDuration;
}

void EvCharger::setChargingEnabledLockDuration(uint chargingEnabledLockDuration)
{
    m_chargingEnabledLockDuration = chargingEnabledLockDuration;
}

int EvCharger::remainingChargingEnabledLockDuration()
{
    return m_remainingChargingEnabledLockDuration;
}

bool EvCharger::chargingEnabledLocked() const
{
    return m_chargingEnabledLocked;
}

int EvCharger::chargingCurrentLockDuration() const
{
    return m_chargingCurrentLockDuration;
}

void EvCharger::setChargingCurrentLockDuration(uint chargingCurrentLockDuration)
{
    m_chargingCurrentLockDuration = chargingCurrentLockDuration;
}

bool EvCharger::chargingCurrentLocked(const QDateTime &currentDateTime) const
{
    if (!m_lastChargingCurrentAction.isValid())
        return false;

    return m_lastChargingCurrentAction.secsTo(currentDateTime) < chargingCurrentLockDuration();
}

void EvCharger::resetLocks()
{
    m_lockEvaluationTimer.stop();
    m_chargingEnabledLocked = false;
    m_remainingChargingEnabledLockDuration = -1;
    m_lastChargingEnabledAction = QDateTime();
    m_lastChargingCurrentAction = QDateTime();

    chargingEnabledLockedChanged(m_chargingEnabledLocked);
    remainingChargingEnabledLockDurationChanged(m_remainingChargingEnabledLockDuration);
}

bool EvCharger::available() const
{
    if (thing()->thingClass().interfaces().contains("connectable"))
        return thing()->stateValue("connected").toBool();

    // If not implementing connectable, we assume yes,
    // otherwise it would never start loading
    return true;
}

uint EvCharger::maxChargingCurrentMaxValue() const
{
    return m_thing->state("maxChargingCurrent").maxValue().toUInt();
}

uint EvCharger::maxChargingCurrentMinValue() const
{
    return m_thing->state("maxChargingCurrent").minValue().toUInt();
}

double EvCharger::maxChargingCurrentStepSize() const
{
    return m_thing->thingClass().stateTypes().findByName("maxChargingCurrent").stepSize();
}

uint EvCharger::maxChargingCurrent() const
{
    return m_thing->stateValue("maxChargingCurrent").toUInt();
}

double EvCharger::currentPower() const
{
    // If it has power metering, use that
    if (m_thing->thingClass().hasStateType("currentPower")) {
        return m_thing->stateValue("currentPower").toDouble();
    }

    // If the the charger does not support measuring current consumption, let's see if it supports "charging"
    if (m_thing->thingClass().hasStateType("charging")) {
        // If it is currently charging, return maxChargingCurrent
        bool charging = m_thing->stateValue("charging").toBool();
        if (charging) {
            return m_thing->stateValue("maxChargingCurrent").toInt() * 230 * phaseCount();
        } else {
            return 0;
        }
    }

    // No "charging" state either... Fall back to "power" which is mandatory
    bool powered = m_thing->stateValue("power").toBool();
    if (powered) {
        return m_thing->stateValue("maxChargingCurrent").toDouble() * 230 * phaseCount();
    }

    return 0;
}

uint EvCharger::phaseCount() const
{
    if (m_thing->hasState("phaseCount")) {
        uint phaseCount = m_thing->stateValue("phaseCount").toUInt();
        // Adjusting to be not 0 so this doesn't need to be checked on every division.
        // If a plugin reports 0 phases, something is probably not working somewhere in the communication with the charger
        if (phaseCount == 0) {
            qCWarning(dcNymeaEnergy()) << "EV charger reports 0 phases... That can't be right... Adjusting to 1.";
            phaseCount = 1;
        }
        return phaseCount;
    }

    // Assuming 1 phases if we don't know...
    return 1;
}

Electricity::Phases EvCharger::phases() const
{
    if (m_thing->hasState("phaseCount")) {
        uint phaseCount = m_thing->stateValue("phaseCount").toUInt();
        if (phaseCount == 3) {
            return Electricity::PhaseAll;
        }
    }

    // TODO: Can we figure out from the root meter on which phase we're attached?
    // One idea would be to sign up on root meter changes. When chargingEnabled is set or unset,
    // memorize root meter values and compare the next (or some more) cycles if a phase changed
    // by a similar value as we'd expect    

    // Until we have this detection, we must ask the user how many phases will be used while charging

    return Electricity::PhaseNone;
}

bool EvCharger::canSetPhaseCount() const
{
    return m_thing->hasState("desiredPhaseCount");
}

ThingActionInfo *EvCharger::setDesiredPhaseCount(int desiredPhases)
{
    StateType desiredPhaseCountStateType = m_thing->thingClass().stateTypes().findByName("desiredPhaseCount");

    Action maxChargingAction(desiredPhaseCountStateType.id(), m_thing->id(), Action::TriggeredByRule);
    maxChargingAction.setParams(ParamList() << Param(desiredPhaseCountStateType.id(), desiredPhases));
    return m_thingManager->executeAction(maxChargingAction);
}

void EvCharger::evaluateChargingEnabledLock()
{
    if (m_lastChargingEnabledAction.isNull()) {
        m_chargingEnabledLocked = false;
        return;
    }

    int secondsSinceLastAction = m_lastChargingEnabledAction.secsTo(QDateTime::currentDateTime());
    if (m_chargingEnabledLocked && secondsSinceLastAction > m_chargingEnabledLockDuration) {
        qCWarning(dcNymeaEnergy()) << "EV charger" << m_thing->name() << "power is now unlocked";

        m_remainingChargingEnabledLockDuration = -1;
        emit remainingChargingEnabledLockDurationChanged(m_remainingChargingEnabledLockDuration);

        m_chargingEnabledLocked = false;
        emit chargingEnabledLockedChanged(m_chargingEnabledLocked);

        m_lockEvaluationTimer.stop();
    } else if (!m_chargingEnabledLocked && secondsSinceLastAction <= m_chargingEnabledLockDuration) {
        qCWarning(dcNymeaEnergy()) << "EV charger" << m_thing->name() << "power is now locked for" << m_chargingEnabledLockDuration << "seconds";

        m_remainingChargingEnabledLockDuration = m_chargingEnabledLockDuration - secondsSinceLastAction;
        emit remainingChargingEnabledLockDurationChanged(m_remainingChargingEnabledLockDuration);

        m_chargingEnabledLocked = true;
        emit chargingEnabledLockedChanged(m_chargingEnabledLocked);
        m_lockEvaluationTimer.start();
    }
}

Electricity::Phases EvCharger::meteredPhases() const
{
    Electricity::Phases phases = Electricity::PhaseNone;
    if (m_thing->hasState("currentPower")) {
        double currentPower = m_thing->stateValue("currentPower").toDouble();
        if (currentPower != 0) {

            /* Note: we prefere the current data per phase over power, since some wallboxes show a few watts on phases not beeing really used.
               Also, if we use the Ampere, the smaller resolution mostly filters away some induction on non active phases. Seen so far on webasto NEXT.
            */
            if (m_thing->hasState("currentPhaseA")) {
                if (m_thing->stateValue("currentPhaseA").toDouble() != 0) {
                    phases.setFlag(Electricity::PhaseA);
                }
            } else if (m_thing->hasState("currentPowerPhaseA") && m_thing->stateValue("currentPowerPhaseA").toDouble() != 0) {
                phases.setFlag(Electricity::PhaseA);
            }

            if (m_thing->hasState("currentPhaseB")) {
                if (m_thing->stateValue("currentPhaseB").toDouble() != 0) {
                    phases.setFlag(Electricity::PhaseB);
                }
            } else if (m_thing->hasState("currentPowerPhaseB") && m_thing->stateValue("currentPowerPhaseB").toDouble() != 0) {
                phases.setFlag(Electricity::PhaseB);
            }

            if (m_thing->hasState("currentPhaseC")) {
                if (m_thing->stateValue("currentPhaseC").toDouble() != 0) {
                    phases.setFlag(Electricity::PhaseC);
                }
            } else if (m_thing->hasState("currentPowerPhaseC") && m_thing->stateValue("currentPowerPhaseC").toDouble() != 0) {
                phases.setFlag(Electricity::PhaseC);
            }
        }
    }

    return phases;
}

ThingActionInfo* EvCharger::setMaxChargingCurrent(double maxChargingCurrent, const QDateTime &currentDateTime, bool force)
{
    QDateTime now = currentDateTime;
    if (!force && chargingCurrentLocked(now)) {
        uint secondsLeft = m_lastChargingCurrentAction.secsTo(now);
        qCDebug(dcNymeaEnergy()) << "Adjust the charging current is locked since" << secondsLeft << "seconds. Lock duration is"
                                 << chargingCurrentLockDuration() << "seconds."
                                 << "Last action:" << m_lastChargingCurrentAction.toString("dd.MM.yyyy hh:mm:ss")
                                 << "now:" << now.toString("dd.MM.yyyy hh:mm:ss");
        return nullptr;
    }

    StateType maxChargingCurrentStateType = m_thing->thingClass().stateTypes().findByName("maxChargingCurrent");

    double paramValue = qMax(maxChargingCurrentStateType.minValue().toDouble(), maxChargingCurrent);
    paramValue = qMin(maxChargingCurrentStateType.maxValue().toDouble(), paramValue);
    bool chargingCurrentActionRequired = (this->maxChargingCurrent() != paramValue);

    Action maxChargingAction(maxChargingCurrentStateType.id(), m_thing->id(), Action::TriggeredByRule);
    maxChargingAction.setParams(ParamList() << Param(maxChargingCurrentStateType.id(), paramValue));

    ThingActionInfo *info = m_thingManager->executeAction(maxChargingAction);
    connect(info, &ThingActionInfo::finished, this, [this, info, now, chargingCurrentActionRequired](){
        if (info->status() == Thing::ThingErrorNoError && chargingCurrentActionRequired) {
            m_lastChargingCurrentAction = now;
        }
    });

    return info;
}

bool EvCharger::hasPowerMeter() const
{
    return m_thing->thingClass().interfaces().contains("smartmeterconsumer");
}

bool EvCharger::chargingEnabled() const
{
    return m_thing->stateValue("power").toBool();
}

ThingActionInfo* EvCharger::setChargingEnabled(bool power, const QDateTime &currentDateTime, bool force)
{
    StateType powerStateType = m_thing->thingClass().stateTypes().findByName("power");
    bool lockActionRequired = (chargingEnabled() != power);

    QDateTime now = currentDateTime;
    if (!force && m_chargingEnabledLocked && lockActionRequired) {
        uint secondsLeft = m_lastChargingEnabledAction.secsTo(now);
        qCDebug(dcNymeaEnergy()) << "Charging enabled is locked since" << secondsLeft << "seconds. Lock duration is"
                                 << chargingEnabledLockDuration() << "seconds."
                                 << "Last action:" << m_lastChargingEnabledAction.toString("dd.MM.yyyy hh:mm:ss")
                                 << "now:" << now.toString("dd.MM.yyyy hh:mm:ss");
        return nullptr;
    }

    Action powerAction(powerStateType.id(), m_thing->id(), Action::TriggeredByRule);
    powerAction.setParams(ParamList() << Param(powerStateType.id(), power));

    ThingActionInfo *info = m_thingManager->executeAction(powerAction);
    connect(info, &ThingActionInfo::finished, this, [this, info, now, lockActionRequired](){
        if (info->status() == Thing::ThingErrorNoError && lockActionRequired) {
            m_lastChargingEnabledAction = now;
            evaluateChargingEnabledLock();
        }
    });
    return info;
}

bool EvCharger::pluggedIn() const
{
    if (thing()->hasState("pluggedIn"))
        return thing()->stateValue("pluggedIn").toBool();

    // We assume yes if the state is not available (since optional),
    // otherwise we will never start charging...
    return true;
}

bool EvCharger::charging() const
{
    if (m_thing->hasState("charging"))
        return m_thing->stateValue("charging").toBool();

    return chargingEnabled() && pluggedIn();
}
