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

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

#include "stateevaluator.h"
#include "nymeacore.h"
#include "integrations/thingmanager.h"
#include "loggingcategories.h"
#include "nymeasettings.h"

#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
#include <QColor>
#endif

namespace nymeaserver {

StateEvaluator::StateEvaluator(const StateDescriptor &stateDescriptor):
    m_stateDescriptor(stateDescriptor),
    m_operatorType(Types::StateOperatorAnd)
{

}

StateEvaluator::StateEvaluator(QList<StateEvaluator> childEvaluators, Types::StateOperator stateOperator):
    m_stateDescriptor(),
    m_childEvaluators(childEvaluators),
    m_operatorType(stateOperator)
{
}

StateDescriptor StateEvaluator::stateDescriptor() const
{
    return m_stateDescriptor;
}

void StateEvaluator::setStateDescriptor(const StateDescriptor &stateDescriptor)
{
    m_stateDescriptor = stateDescriptor;
}

StateEvaluators StateEvaluator::childEvaluators() const
{
    return m_childEvaluators;
}

void StateEvaluator::setChildEvaluators(const StateEvaluators &stateEvaluators)
{
    m_childEvaluators = stateEvaluators;
}

void StateEvaluator::appendEvaluator(const StateEvaluator &stateEvaluator)
{
    m_childEvaluators.append(stateEvaluator);
}

Types::StateOperator StateEvaluator::operatorType() const
{
    return m_operatorType;
}

void StateEvaluator::setOperatorType(Types::StateOperator operatorType)
{
    m_operatorType = operatorType;
}

bool StateEvaluator::evaluate() const
{
    qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "Evaluating: Operator type" << m_operatorType << "Valid descriptor:" << m_stateDescriptor.isValid() << "Childs:" << m_childEvaluators.count();
    bool descriptorMatching = true;
    if (m_stateDescriptor.isValid()) {
        descriptorMatching = evaluateDescriptor(m_stateDescriptor);
    }

    if (m_operatorType == Types::StateOperatorOr) {
        if (m_stateDescriptor.isValid() && descriptorMatching) {
            qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "Descriptor is matching. Operator is OR => Evaluation result: true";
            return true;
        }
        foreach (const StateEvaluator &stateEvaluator, m_childEvaluators) {
            if (stateEvaluator.evaluate()) {
                qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "Child evaluator evaluated to true. Operator is OR => Evaluation result: true";
                return true;
            }
        }
        qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "No child evaluator evaluated to true => Evaluation result: false";
        return false;
    }

    if (!descriptorMatching) {
        qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "StateDescriptor not matching and operator is AND => Evaluation result: false";
        return false;
    }

    foreach (const StateEvaluator &stateEvaluator, m_childEvaluators) {
        if (!stateEvaluator.evaluate()) {
            qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "Child evaluator not matching => Evaluation result: false";
            return false;
        }
    }
    qCDebug(dcRuleEngineDebug()) << "StateEvaluator:" << this << "StateDescriptor and all child evaluators matching => Evaluation result: true";
    return true;
}

bool StateEvaluator::containsThing(const ThingId &thingId) const
{
    if (m_stateDescriptor.thingId() == thingId || m_stateDescriptor.valueThingId() == thingId)
        return true;

    foreach (const StateEvaluator &childEvaluator, m_childEvaluators) {
        if (childEvaluator.containsThing(thingId)) {
            return true;
        }
    }
    return false;
}

void StateEvaluator::removeThing(const ThingId &thingId)
{
    if (m_stateDescriptor.thingId() == thingId || m_stateDescriptor.valueThingId() == thingId)
        m_stateDescriptor = StateDescriptor();

    for (int i = 0; i < m_childEvaluators.count(); i++) {
        m_childEvaluators[i].removeThing(thingId);
    }
}

QList<ThingId> StateEvaluator::containedThings() const
{
    QList<ThingId> ret;
    if (!m_stateDescriptor.thingId().isNull()) {
        ret.append(m_stateDescriptor.thingId());
    }
    if (!m_stateDescriptor.valueThingId().isNull()) {
        ret.append(m_stateDescriptor.valueThingId());
    }
    foreach (const StateEvaluator &childEvaluator, m_childEvaluators) {
        ret.append(childEvaluator.containedThings());
    }
    return ret;
}

void StateEvaluator::dumpToSettings(NymeaSettings &settings, const QString &groupName) const
{
    settings.beginGroup(groupName);

    settings.beginGroup("stateDescriptor");
    settings.setValue("stateTypeId", m_stateDescriptor.stateTypeId().toString());
    settings.setValue("thingId", m_stateDescriptor.thingId().toString());
    settings.setValue("interface", m_stateDescriptor.interface());
    settings.setValue("interfaceState", m_stateDescriptor.interfaceState());
    settings.setValue("value", m_stateDescriptor.stateValue());
    settings.setValue("valueType", (int)m_stateDescriptor.stateValue().type());
    settings.setValue("valueThingId", m_stateDescriptor.valueThingId().toString());
    settings.setValue("valueStateTypeId", m_stateDescriptor.valueStateTypeId().toString());
    settings.setValue("operator", m_stateDescriptor.operatorType());
    settings.endGroup();

    settings.setValue("operator", m_operatorType);

    settings.beginGroup("childEvaluators");
    for (int i = 0; i < m_childEvaluators.count(); i++) {
        m_childEvaluators.at(i).dumpToSettings(settings, "stateEvaluator-" + QString::number(i));
    }
    settings.endGroup();

    settings.endGroup();
}

StateEvaluator StateEvaluator::loadFromSettings(NymeaSettings &settings, const QString &groupName)
{
    settings.beginGroup(groupName);
    settings.beginGroup("stateDescriptor");
    StateTypeId stateTypeId(settings.value("stateTypeId").toString());
    ThingId thingId(settings.value("thingId").toString());
    if (thingId.isNull()) { // Retry with deviceId for backwards compatibility (<0.19)
        thingId = ThingId(settings.value("deviceId").toString());
    }
    QVariant stateValue = settings.value("value");
    if (settings.contains("valueType")) {
        QMetaType::Type valueType = (QMetaType::Type)settings.value("valueType").toInt();
        // Note: only warn, and continue with the QVariant guessed type
        if (valueType == QMetaType::UnknownType) {
            qCWarning(dcRuleEngine()) << "Could not load the value type of the state evaluator. The value type will be guessed by QVariant" << stateValue;
        } else if (!stateValue.canConvert(valueType)) {
            qCWarning(dcRuleEngine()) << "Could not convert the state evaluator value" << stateValue << "to the stored type" << valueType << ". The value type will be guessed by QVariant.";
        } else {
            stateValue.convert(valueType);
        }
    }

    ThingId valueThingId = settings.value("valueThingId").toUuid();
    StateTypeId valueStateTypeId = settings.value("valueStateTypeId").toUuid();

    QString interface = settings.value("interface").toString();
    QString interfaceState = settings.value("interfaceState").toString();
    Types::ValueOperator valueOperator = (Types::ValueOperator)settings.value("operator").toInt();
    StateDescriptor stateDescriptor;
    if (!thingId.isNull() && !stateTypeId.isNull()) {
        stateDescriptor = StateDescriptor(stateTypeId, thingId, stateValue, valueOperator);
    } else {
        stateDescriptor = StateDescriptor(interface, interfaceState, stateValue, valueOperator);
    }
    stateDescriptor.setValueThingId(valueThingId);
    stateDescriptor.setValueStateTypeId(valueStateTypeId);

    settings.endGroup();

    StateEvaluator ret(stateDescriptor);
    ret.setOperatorType((Types::StateOperator)settings.value("operator").toInt());

    settings.beginGroup("childEvaluators");
    foreach (const QString &evaluatorGroup, settings.childGroups()) {
        ret.appendEvaluator(StateEvaluator::loadFromSettings(settings, evaluatorGroup));
    }
    settings.endGroup();
    settings.endGroup();
    return ret;
}

bool StateEvaluator::isValid() const
{
    if (m_stateDescriptor.isValid()) {
        if (m_stateDescriptor.type() == StateDescriptor::TypeThing) {
            Thing *thing = NymeaCore::instance()->thingManager()->findConfiguredThing(m_stateDescriptor.thingId());
            if (!thing) {
                qCWarning(dcRuleEngine) << "State evaluator thing does not exist!";
                return false;
            }

            if (!thing->hasState(m_stateDescriptor.stateTypeId())) {
                qCWarning(dcRuleEngine) << "State evaluator thing found, but it does not appear to have such a state!";
                return false;
            }

            ThingClass thingClass = NymeaCore::instance()->thingManager()->findThingClass(thing->thingClassId());
            foreach (const StateType &stateType, thingClass.stateTypes()) {
                if (stateType.id() == m_stateDescriptor.stateTypeId()) {

                    // If comparing to a static value
                    if (!m_stateDescriptor.stateValue().isNull()) {

                        QVariant stateValue = m_stateDescriptor.stateValue();
                        if (!stateValue.convert(stateType.type())) {
                            qCWarning(dcRuleEngine) << "Could not convert value of state descriptor" << m_stateDescriptor.stateTypeId() << " to:" << QVariant::typeToName(stateType.type()) << " Got:" << m_stateDescriptor.stateValue();
                            return false;
                        }
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
                        QPartialOrdering ordering = QPartialOrdering::Unordered;
                        ordering = QVariant::compare(stateValue, stateType.maxValue());
                        if (stateType.maxValue().isValid() && ordering == QPartialOrdering::Greater) {
                            qCWarning(dcRuleEngine) << "Value out of range for state descriptor" << m_stateDescriptor.stateTypeId() << " Got:" << m_stateDescriptor.stateValue() << " Max:" << stateType.maxValue();
                            return false;
                        }
                        ordering = QVariant::compare(stateValue, stateType.minValue());
                        if (stateType.minValue().isValid() && ordering == QPartialOrdering::Less) {
                            qCWarning(dcRuleEngine) << "Value out of range for state descriptor" << m_stateDescriptor.stateTypeId() << " Got:" << m_stateDescriptor.stateValue() << " Min:" << stateType.minValue();
                            return false;
                        }
#else
                        if (stateType.maxValue().isValid() && stateValue > stateType.maxValue()) {
                            qCWarning(dcRuleEngine) << "Value out of range for state descriptor" << m_stateDescriptor.stateTypeId() << " Got:" << m_stateDescriptor.stateValue() << " Max:" << stateType.maxValue();
                            return false;
                        }

                        if (stateType.minValue().isValid() && stateValue < stateType.minValue()) {
                            qCWarning(dcRuleEngine) << "Value out of range for state descriptor" << m_stateDescriptor.stateTypeId() << " Got:" << m_stateDescriptor.stateValue() << " Min:" << stateType.minValue();
                            return false;
                        }
#endif
                        if (!stateType.possibleValues().isEmpty() && !stateType.possibleValues().contains(stateValue)) {
                            QStringList possibleValues;
                            foreach (const QVariant &value, stateType.possibleValues()) {
                                possibleValues.append(value.toString());
                            }

                            qCWarning(dcRuleEngine) << "Value not in possible values for state type" << m_stateDescriptor.stateTypeId() << " Got:" << m_stateDescriptor.stateValue() << " Possible values:" << possibleValues.join(", ");
                            return false;
                        }

                        // if comparing to another state
                    } else if (!m_stateDescriptor.valueThingId().isNull() && !m_stateDescriptor.valueStateTypeId().isNull()) {
                        Thing *valueThing = NymeaCore::instance()->thingManager()->findConfiguredThing(m_stateDescriptor.valueThingId());
                        if (!valueThing) {
                            qCWarning(dcRuleEngine()) << "State descriptor valueThing does not exist" << m_stateDescriptor.valueThingId().toString();
                            return false;
                        }
                        StateType valueStateType = valueThing->thingClass().stateTypes().findById(m_stateDescriptor.valueStateTypeId());
                        if (!valueStateType.isValid()) {
                            qCWarning(dcRuleEngine()) << "State descriptor value state type" << m_stateDescriptor.valueStateTypeId().toString() << "does not exist in thing" << valueThing->name();
                            return false;
                        }
                    } else {
                        qCWarning(dcRuleEngine()) << "State descriptor contains neither stateValue nor valueThingId and valueStateTypeId";
                        return false;
                    }
                }
            }
        } else { // TypeInterface
            Interface iface = NymeaCore::instance()->thingManager()->supportedInterfaces().findByName(m_stateDescriptor.interface());
            if (!iface.isValid()) {
                qCWarning(dcRuleEngine()) << "No such interface:" << m_stateDescriptor.interface();
                return false;
            }
            if (iface.stateTypes().findByName(m_stateDescriptor.interfaceState()).name().isEmpty()) {
                qCWarning(dcRuleEngine()) << "Interface" << iface.name() << "has no such state:" << m_stateDescriptor.interfaceState();
                return false;
            }
        }
    }

    if (m_operatorType == Types::StateOperatorOr) {
        foreach (const StateEvaluator &stateEvaluator, m_childEvaluators) {
            if (stateEvaluator.isValid()) {
                return true;
            }
        }
        return false;
    }

    foreach (const StateEvaluator &stateEvaluator, m_childEvaluators) {
        if (!stateEvaluator.isValid()) {
            return false;
        }
    }
    return true;
}

bool StateEvaluator::isEmpty() const
{
    return !m_stateDescriptor.isValid() && m_childEvaluators.isEmpty();
}

bool StateEvaluator::evaluateDescriptor(const StateDescriptor &descriptor) const
{
    if (descriptor.type() == StateDescriptor::TypeThing) {
        qCDebug(dcRuleEngineDebug()) << "Evaluating thing based state descriptor";

        Thing *thing = NymeaCore::instance()->thingManager()->findConfiguredThing(descriptor.thingId());
        if (!thing) {
            qCWarning(dcRuleEngine()) << "Thing listed in state descriptor not found in system.";
            return false;
        }
        State state = thing->state(descriptor.stateTypeId());
        if (state.stateTypeId().isNull()) {
            qCWarning(dcRuleEngine()) << "State" << descriptor.stateTypeId() << "not found in thing" << thing->name() << thing->id().toString();
            return false;
        }

        if (!descriptor.stateValue().isNull()) {
            QVariant stateValue = state.value();
            QVariant descriptorValue = descriptor.stateValue();

#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
            bool res = descriptorValue.convert(state.value().metaType());
            if (!res) {
                return false;
            }

            // FIXME: not sure if this is a bug in Qt, but compairing two equivalient QVariant containing QColor
            // with QPartialOrdering::equivalent results in false. Until this has been fixed or the error has been found,
            // we convert colors to strings and compare them

            // Comparing QVariant(QColor, QColor(ARGB 1, 0, 0, 0)) to QVariant(QColor, QColor(ARGB 1, 0, 1, 0)) with operator Types::ValueOperatorEquals --> false

            if (state.value().typeId() == QMetaType::QColor) {
                QColor stateColor = stateValue.value<QColor>();
                QColor desciptorColor = descriptorValue.value<QColor>();

                QString sateColorName = stateColor.name();
                QString desciptorColorName = desciptorColor.name();

                stateValue = QVariant(sateColorName);
                descriptorValue = QVariant(desciptorColorName);
            }

            QPartialOrdering ordering = QVariant::compare(stateValue, descriptorValue);
            bool result = false;
            switch (descriptor.operatorType()) {
            case Types::ValueOperatorEquals:
                result = (ordering == QPartialOrdering::equivalent);
                break;
            case Types::ValueOperatorGreater:
                result = (ordering == QPartialOrdering::greater);
                break;
            case Types::ValueOperatorGreaterOrEqual:
                result = (ordering == QPartialOrdering::greater || ordering == QPartialOrdering::equivalent);
                break;
            case Types::ValueOperatorLess:
                result = (ordering == QPartialOrdering::less);
                break;
            case Types::ValueOperatorLessOrEqual:
                result = (ordering == QPartialOrdering::less || ordering == QPartialOrdering::equivalent);
                break;
            case Types::ValueOperatorNotEquals:
                result = (ordering != QPartialOrdering::equivalent);
                break;
            }

            qCDebug(dcRuleEngineDebug()) << "Comparing" << stateValue << "to" << descriptorValue << "with operator" << descriptor.operatorType() << "-->" << result;

            return result;
#else
            bool res = descriptorValue.convert(state.value().type());
            if (!res) {
                return false;
            }

            switch (descriptor.operatorType()) {
            case Types::ValueOperatorEquals:
                return state.value() == descriptorValue;
            case Types::ValueOperatorGreater:
                return state.value() > descriptorValue;
            case Types::ValueOperatorGreaterOrEqual:
                return state.value() >= descriptorValue;
            case Types::ValueOperatorLess:
                return state.value() < descriptorValue;
            case Types::ValueOperatorLessOrEqual:
                return state.value() <= descriptorValue;
            case Types::ValueOperatorNotEquals:
                return state.value() != descriptorValue;
            }
#endif
        } else if (!descriptor.valueThingId().isNull() && !descriptor.valueStateTypeId().isNull()) {
            Thing *valueThing = NymeaCore::instance()->thingManager()->findConfiguredThing(descriptor.valueThingId());
            if (!valueThing) {
                qCWarning(dcRuleEngine()) << "Thing" << descriptor.valueThingId().toString() << "defined in statedescriptor value but not found in system.";
                return false;
            }
            State valueState = valueThing->state(descriptor.valueStateTypeId());
            if (valueState.stateTypeId().isNull()) {
                qCWarning(dcRuleEngine()) << "State" << descriptor.valueStateTypeId().toString() << "defined in statedescriptor value not found in thing" << thing->name() << thing->id().toString();
                return false;
            }

            qCDebug(dcRuleEngineDebug()) << "Comparing" << state.value() << "to" << valueState.value() << "with operator" << descriptor.operatorType();
#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
            QPartialOrdering ordering = QVariant::compare(state.value(), valueState.value());
            switch (descriptor.operatorType()) {
            case Types::ValueOperatorEquals:
                return ordering == QPartialOrdering::Equivalent;
            case Types::ValueOperatorGreater:
                return ordering == QPartialOrdering::Greater;
            case Types::ValueOperatorGreaterOrEqual:
                return ordering == QPartialOrdering::Greater || ordering == QPartialOrdering::Equivalent;
            case Types::ValueOperatorLess:
                return ordering == QPartialOrdering::Less;
            case Types::ValueOperatorLessOrEqual:
                return ordering == QPartialOrdering::Less || ordering == QPartialOrdering::Equivalent;
            case Types::ValueOperatorNotEquals:
                return ordering != QPartialOrdering::Equivalent;
            }
#else
            switch (descriptor.operatorType()) {
            case Types::ValueOperatorEquals:
                return state.value() == valueState.value();
            case Types::ValueOperatorGreater:
                return state.value() > valueState.value();
            case Types::ValueOperatorGreaterOrEqual:
                return state.value() >= valueState.value();
            case Types::ValueOperatorLess:
                return state.value() < valueState.value();
            case Types::ValueOperatorLessOrEqual:
                return state.value() <= valueState.value();
            case Types::ValueOperatorNotEquals:
                return state.value() != valueState.value();
            }
#endif
        }

    } else { // Interface based
        qCDebug(dcRuleEngineDebug()) << "Evaluating interface based state descriptor" << descriptor.interface();

        foreach (Thing* thing, NymeaCore::instance()->thingManager()->configuredThings()) {
            if (thing->thingClass().interfaces().contains(descriptor.interface())) {
                qCDebug(dcRuleEngineDebug()) << "Thing" << thing->name() << "has matching interface";
                StateType stateType = thing->thingClass().stateTypes().findByName(descriptor.interfaceState());
                State state = thing->state(stateType.id());
                // Generate a thing based state descriptor and run again
                StateDescriptor temporaryDescriptor(stateType.id(), thing->id(), descriptor.stateValue(), descriptor.operatorType());
                temporaryDescriptor.setValueThingId(descriptor.valueThingId());
                temporaryDescriptor.setValueStateTypeId(descriptor.valueStateTypeId());
                if (evaluateDescriptor(temporaryDescriptor)) {
                    return true;
                }
            }
        }
    }

    return false;
}

QDebug operator<<(QDebug dbg, const StateEvaluator &stateEvaluator)
{
    QDebugStateSaver saver(dbg);
    dbg.nospace() << "StateEvaluator: Operator:" << stateEvaluator.operatorType() << '\n' << "  " << stateEvaluator.stateDescriptor() << '\n';
    for (int i = 0; i < stateEvaluator.childEvaluators().count(); i++) {
        dbg.nospace() << "    " << i << ": " << stateEvaluator.childEvaluators().at(i);
    }
    return dbg;
}

StateEvaluators::StateEvaluators()
{

}

StateEvaluators::StateEvaluators(const QList<StateEvaluator> &other): QList<StateEvaluator>(other)
{

}

QVariant StateEvaluators::get(int index) const
{
    return QVariant::fromValue(at(index));
}

void StateEvaluators::put(const QVariant &variant)
{
    append(variant.value<StateEvaluator>());
}

}
