// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qopen62541backend.h"
#include "qopen62541client.h"
#include "qopen62541node.h"
#include "qopen62541subscription.h"
#include "qopen62541utils.h"
#include "qopen62541valueconverter.h"
#include "qopen62541utils.h"
#include <private/qopcuanode_p.h>

#include "qopcuacontentfilterelementresult.h"

#include <QtCore/qloggingcategory.h>

QT_BEGIN_NAMESPACE

Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_OPEN62541)

static void monitoredValueHandler(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext, UA_DataValue *value)
{
    Q_UNUSED(client)
    Q_UNUSED(subId)
    Q_UNUSED(subContext)
    QOpen62541Subscription *subscription = static_cast<QOpen62541Subscription *>(monContext);
    subscription->monitoredValueUpdated(monId, value);
}

static void stateChangeHandler(UA_Client *client, UA_UInt32 subId, void *subContext, UA_StatusChangeNotification *notification)
{
    Q_UNUSED(client);
    Q_UNUSED(subId);

    if (notification->status != UA_STATUSCODE_BADTIMEOUT)
        return;

    QOpen62541Subscription *sub = static_cast<QOpen62541Subscription *>(subContext);
    sub->sendTimeoutNotification();
}

static void eventHandler(UA_Client *client, UA_UInt32 subId, void *subContext, UA_UInt32 monId, void *monContext,
                         size_t numFields, UA_Variant *eventFields)
{
    Q_UNUSED(client);
    Q_UNUSED(subId);
    Q_UNUSED(subContext);

    QOpen62541Subscription *subscription = static_cast<QOpen62541Subscription *>(monContext);

    QVariantList list;
    for (size_t i = 0; i < numFields; ++i)
        list.append(QOpen62541ValueConverter::toQVariant(eventFields[i]));
    subscription->eventReceived(monId, list);
}

QOpen62541Subscription::QOpen62541Subscription(Open62541AsyncBackend *backend, const QOpcUaMonitoringParameters &settings)
    : m_backend(backend)
    , m_interval(settings.publishingInterval())
    , m_subscriptionId(0)
    , m_lifetimeCount(settings.lifetimeCount() ? settings.lifetimeCount() : UA_CreateSubscriptionRequest_default().requestedLifetimeCount)
    , m_maxKeepaliveCount(settings.maxKeepAliveCount() ? settings.maxKeepAliveCount() : UA_CreateSubscriptionRequest_default().requestedMaxKeepAliveCount)
    , m_shared(settings.subscriptionType())
    , m_priority(settings.priority())
    , m_maxNotificationsPerPublish(settings.maxNotificationsPerPublish())
    , m_clientHandle(0)
    , m_timeout(false)
{
}

QOpen62541Subscription::~QOpen62541Subscription()
{
    removeOnServer();
}

UA_UInt32 QOpen62541Subscription::createOnServer()
{
    UA_CreateSubscriptionRequest req = UA_CreateSubscriptionRequest_default();
    req.requestedPublishingInterval = m_interval;
    req.requestedLifetimeCount = m_lifetimeCount;
    req.requestedMaxKeepAliveCount = m_maxKeepaliveCount;
    req.priority = m_priority;
    req.maxNotificationsPerPublish = m_maxNotificationsPerPublish;
    UA_CreateSubscriptionResponse res = UA_Client_Subscriptions_create(m_backend->m_uaclient, req, this, stateChangeHandler, nullptr);

    if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
        qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create subscription with interval" << m_interval << UA_StatusCode_name(res.responseHeader.serviceResult);
        return 0;
    }

    m_subscriptionId = res.subscriptionId;
    m_maxKeepaliveCount = res.revisedMaxKeepAliveCount;
    m_lifetimeCount = res.revisedLifetimeCount;
    m_interval = res.revisedPublishingInterval;
    return m_subscriptionId;
}

bool QOpen62541Subscription::removeOnServer()
{
    UA_StatusCode res = UA_STATUSCODE_GOOD;
    if (m_subscriptionId) {
        res = UA_Client_Subscriptions_deleteSingle(m_backend->m_uaclient, m_subscriptionId);
        m_subscriptionId = 0;
    }

    for (auto it : std::as_const(m_itemIdToItemMapping)) {
        QOpcUaMonitoringParameters s;
        s.setStatusCode(m_timeout ? QOpcUa::UaStatusCode::BadTimeout : QOpcUa::UaStatusCode::BadDisconnect);
        emit m_backend->monitoringEnableDisable(it->handle, it->attr, false, s);
    }

    qDeleteAll(m_itemIdToItemMapping);

    m_itemIdToItemMapping.clear();
    m_nodeHandleToItemMapping.clear();

    return (res == UA_STATUSCODE_GOOD) ? true : false;
}

void QOpen62541Subscription::modifyMonitoring(quint64 handle, QOpcUa::NodeAttribute attr, QOpcUaMonitoringParameters::Parameter item, QVariant value)
{
    MonitoredItem *monItem = getItemForAttribute(handle, attr);
    if (!monItem) {
        qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify parameter" << item << "there are no monitored items";
        QOpcUaMonitoringParameters p;
        p.setStatusCode(QOpcUa::UaStatusCode::BadAttributeIdInvalid);
        emit m_backend->monitoringStatusChanged(handle, attr, item, p);
        return;
    }

    QOpcUaMonitoringParameters p = monItem->parameters;
    p.setStatusCode(QOpcUa::UaStatusCode::BadNotImplemented);

    // SetPublishingMode service
    if (item == QOpcUaMonitoringParameters::Parameter::PublishingEnabled) {
        if (value.metaType().id() != QMetaType::Bool) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "New value for PublishingEnabled is not a boolean";
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            return;
        }

        UA_SetPublishingModeRequest req;
        UA_SetPublishingModeRequest_init(&req);
        UaDeleter<UA_SetPublishingModeRequest> requestDeleter(&req, UA_SetPublishingModeRequest_clear);
        req.publishingEnabled = value.toBool();
        req.subscriptionIdsSize = 1;
        req.subscriptionIds = UA_UInt32_new();
        *req.subscriptionIds = m_subscriptionId;
        UA_SetPublishingModeResponse res = UA_Client_Subscriptions_setPublishingMode(m_backend->m_uaclient, req);
        UaDeleter<UA_SetPublishingModeResponse> responseDeleter(&res, UA_SetPublishingModeResponse_clear);

        if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set publishing mode:" << res.responseHeader.serviceResult;
            p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult));
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            return;
        }

        if (res.resultsSize && res.results[0] == UA_STATUSCODE_GOOD)
            p.setPublishingEnabled(value.toBool());

        p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.results[0]));
        emit m_backend->monitoringStatusChanged(handle, attr, item, p);

        return;
    }

    // SetMonitoringMode service
    if (item == QOpcUaMonitoringParameters::Parameter::MonitoringMode) {
        if (value.userType() != QMetaType::fromType<QOpcUaMonitoringParameters::MonitoringMode>().id()) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "New value for MonitoringMode is not a monitoring mode";
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            return;
        }

        UA_SetMonitoringModeRequest req;
        UA_SetMonitoringModeRequest_init(&req);
        UaDeleter<UA_SetMonitoringModeRequest> requestDeleter(&req, UA_SetMonitoringModeRequest_clear);
        req.monitoringMode = static_cast<UA_MonitoringMode>(value.value<QOpcUaMonitoringParameters::MonitoringMode>());
        req.monitoredItemIdsSize = 1;
        req.monitoredItemIds = UA_UInt32_new();
        *req.monitoredItemIds = monItem->monitoredItemId;
        req.subscriptionId = m_subscriptionId;
        UA_SetMonitoringModeResponse res = UA_Client_MonitoredItems_setMonitoringMode(m_backend->m_uaclient, req);
        UaDeleter<UA_SetMonitoringModeResponse> responseDeleter(&res, UA_SetMonitoringModeResponse_clear);

        if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set monitoring mode:" << res.responseHeader.serviceResult;
            p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult));
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            return;
        }

        if (res.resultsSize && res.results[0] == UA_STATUSCODE_GOOD)
            p.setMonitoringMode(value.value<QOpcUaMonitoringParameters::MonitoringMode>());

        p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.results[0]));
        emit m_backend->monitoringStatusChanged(handle, attr, item, p);
        return;
    }

    // SetTriggering service
    if (item == QOpcUaMonitoringParameters::Parameter::TriggeredItemIds) {
        if (!value.canConvert<QSet<quint32>>()) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify triggering item id, value is not a set of quint32";
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            return;
        }

        auto triggeredItemIds = value.value<QSet<quint32>>();

        UA_SetTriggeringRequest triggeringReq;
        UA_SetTriggeringRequest_init(&triggeringReq);
        triggeringReq.subscriptionId = m_subscriptionId;
        triggeringReq.triggeringItemId = monItem->monitoredItemId;

        QList<quint32> itemsToRemove;
        QList<quint32> itemsToAdd;

        if (triggeredItemIds.isEmpty() && !monItem->parameters.triggeredItemIds().isEmpty()) {
            itemsToRemove = monItem->parameters.triggeredItemIds().values();
        } else if (!triggeredItemIds.isEmpty()) {
            itemsToAdd = triggeredItemIds.values();
            itemsToRemove = monItem->parameters.triggeredItemIds().subtract(triggeredItemIds).values();
        }

        if (itemsToAdd.isEmpty() && itemsToRemove.isEmpty()) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Nothing to do for TriggeredItemIds";
            p.setStatusCode(QOpcUa::UaStatusCode::Good);
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            return;
        }

        if (!itemsToAdd.isEmpty()) {
            triggeringReq.linksToAddSize = itemsToAdd.size();
            triggeringReq.linksToAdd = itemsToAdd.data();
        }

        if (!itemsToRemove.isEmpty()) {
            triggeringReq.linksToRemoveSize = itemsToRemove.size();
            triggeringReq.linksToRemove = itemsToRemove.data();
        }

        auto triggeringRes = UA_Client_MonitoredItems_setTriggering(m_backend->m_uaclient, triggeringReq);

        QHash<quint32, QOpcUa::UaStatusCode> failedItems;

        if (triggeringRes.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Modifying TriggeredItemIds failed with"
                                                  << UA_StatusCode_name(triggeringRes.responseHeader.serviceResult);

            for (const auto &entry : itemsToAdd)
                failedItems[entry] = QOpcUa::UaStatusCode(triggeringRes.responseHeader.serviceResult);
            p.setFailedTriggeredItemsStatus(failedItems);
            p.setStatusCode(QOpcUa::UaStatusCode(triggeringRes.responseHeader.serviceResult));
            emit m_backend->monitoringStatusChanged(handle, attr, item, p);
            UA_SetTriggeringResponse_clear(&triggeringRes);
            return;
        }

        for (size_t i = 0; i < triggeringRes.addResultsSize; ++i) {
            if (triggeringRes.addResults[i] != UA_STATUSCODE_GOOD) {
                qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add trigger link" << triggeringReq.triggeringItemId
                                                      << "->" << itemsToAdd.at(i) << "on subscription" << m_subscriptionId
                                                      << "with status"
                                                      << UA_StatusCode_name(triggeringRes.addResults[i]);
                failedItems.insert(itemsToAdd.at(i), QOpcUa::UaStatusCode(triggeringRes.addResults[i]));
                triggeredItemIds.remove(itemsToAdd.at(i));
            }
        }

        UA_SetTriggeringResponse_clear(&triggeringRes);

        monItem->parameters.setTriggeredItemIds(triggeredItemIds);
        p.setStatusCode(QOpcUa::UaStatusCode::Good);
        p.setTriggeredItemIds(triggeredItemIds);
        p.setFailedTriggeredItemsStatus(failedItems);
        emit m_backend->monitoringStatusChanged(handle, attr, item, p);

        return;
    }

    if (modifySubscriptionParameters(handle, attr, item, value))
        return;
    if (modifyMonitoredItemParameters(handle, attr, item, value))
        return;

    qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Modifying" << item << "is not implemented";
    p.setStatusCode(QOpcUa::UaStatusCode::BadNotImplemented);
    emit m_backend->monitoringStatusChanged(handle, attr, item, p);
}

bool QOpen62541Subscription::addAttributeMonitoredItem(quint64 handle, QOpcUa::NodeAttribute attr, const UA_NodeId &id, QOpcUaMonitoringParameters settings)
{
    UA_MonitoredItemCreateRequest req;
    UA_MonitoredItemCreateRequest_init(&req);
    UaDeleter<UA_MonitoredItemCreateRequest> requestDeleter(&req, UA_MonitoredItemCreateRequest_clear);
    req.itemToMonitor.attributeId = QOpen62541ValueConverter::toUaAttributeId(attr);
    UA_NodeId_copy(&id, &(req.itemToMonitor.nodeId));
    if (settings.indexRange().size())
        QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(settings.indexRange(), &req.itemToMonitor.indexRange);
    req.monitoringMode = static_cast<UA_MonitoringMode>(settings.monitoringMode());
    req.requestedParameters.samplingInterval = qFuzzyCompare(settings.samplingInterval(), 0.0) ? m_interval : settings.samplingInterval();
    req.requestedParameters.queueSize = settings.queueSize() == 0 ? 1 : settings.queueSize();
    req.requestedParameters.discardOldest = settings.discardOldest();
    req.requestedParameters.clientHandle = ++m_clientHandle;

    if (settings.filter().isValid()) {
        UA_ExtensionObject filter = createFilter(settings.filter());
        if (filter.content.decoded.data)
            req.requestedParameters.filter = filter;
        else {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create monitored item, filter creation failed";
            QOpcUaMonitoringParameters s;
            s.setStatusCode(QOpcUa::UaStatusCode::BadInternalError);
            emit m_backend->monitoringEnableDisable(handle, attr, true, s);
            return false;
        }
    }

    UA_MonitoredItemCreateResult res;
    UaDeleter<UA_MonitoredItemCreateResult> resultDeleter(&res, UA_MonitoredItemCreateResult_clear);

    if (attr == QOpcUa::NodeAttribute::EventNotifier && settings.filter().canConvert<QOpcUaMonitoringParameters::EventFilter>())
        res = UA_Client_MonitoredItems_createEvent(m_backend->m_uaclient, m_subscriptionId,
                                                   UA_TIMESTAMPSTORETURN_BOTH, req, this, eventHandler, nullptr);
    else
        res = UA_Client_MonitoredItems_createDataChange(m_backend->m_uaclient, m_subscriptionId, UA_TIMESTAMPSTORETURN_BOTH, req, this, monitoredValueHandler, nullptr);

    if (res.statusCode != UA_STATUSCODE_GOOD) {
        qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not add monitored item for" << attr << "of node" << Open62541Utils::nodeIdToQString(id) << ":" << UA_StatusCode_name(res.statusCode);
        QOpcUaMonitoringParameters s;
        s.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.statusCode));
        emit m_backend->monitoringEnableDisable(handle, attr, true, s);
        return false;
    }

    QSet<quint32> successfulTriggerLinks;
    QHash<quint32, QOpcUa::UaStatusCode> failedTriggerLinks;
    if (!settings.triggeredItemIds().isEmpty()) {
        auto triggeredItems = settings.triggeredItemIds().values();

        UA_SetTriggeringRequest req;
        UA_SetTriggeringRequest_init(&req);
        req.subscriptionId = m_subscriptionId;
        req.triggeringItemId = res.monitoredItemId;
        req.linksToAddSize = triggeredItems.size();
        req.linksToAdd = triggeredItems.data();

        auto triggeringRes = UA_Client_MonitoredItems_setTriggering(m_backend->m_uaclient, req);

        if (triggeringRes.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not set triggering item it for" << attr << "of node" << Open62541Utils::nodeIdToQString(id) << ":"
                                                  << UA_StatusCode_name(triggeringRes.responseHeader.serviceResult);

            // Remove the new monitored item
            UA_DeleteMonitoredItemsRequest deleteRequest;
            UA_DeleteMonitoredItemsRequest_init(&deleteRequest);
            deleteRequest.subscriptionId = m_subscriptionId;
            deleteRequest.monitoredItemIdsSize = 1;
            deleteRequest.monitoredItemIds = &res.monitoredItemId;
            UA_Client_MonitoredItems_delete(m_backend->m_uaclient, deleteRequest);

            for (const auto &entry : triggeredItems)
                failedTriggerLinks.insert(entry, QOpcUa::UaStatusCode(triggeringRes.responseHeader.serviceResult));

            QOpcUaMonitoringParameters s;
            s.setStatusCode(static_cast<QOpcUa::UaStatusCode>(triggeringRes.responseHeader.serviceResult));
            s.setFailedTriggeredItemsStatus(failedTriggerLinks);
            emit m_backend->monitoringEnableDisable(handle, attr, true, s);

            UA_SetTriggeringResponse_clear(&triggeringRes);

            return false;
        }

        for (size_t i = 0; i < triggeringRes.addResultsSize; ++i) {
            if (triggeringRes.addResults[i] == UA_STATUSCODE_GOOD) {
                successfulTriggerLinks.insert(triggeredItems.at(i));
            } else {
                qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add trigger link" << res.monitoredItemId
                                                      << "->" << triggeredItems.at(i) << "on subscription" << m_subscriptionId
                                                      << "with status"
                                                      << UA_StatusCode_name(triggeringRes.addResults[i]);
                failedTriggerLinks.insert(triggeredItems.at(i), QOpcUa::UaStatusCode(triggeringRes.addResults[i]));
            }
        }

        UA_SetTriggeringResponse_clear(&triggeringRes);
    }

    MonitoredItem *temp = new MonitoredItem(handle, attr, res.monitoredItemId);
    m_nodeHandleToItemMapping[handle][attr] = temp;
    m_itemIdToItemMapping[res.monitoredItemId] = temp;

    QOpcUaMonitoringParameters s = settings;
    s.setSubscriptionId(m_subscriptionId);
    s.setPublishingInterval(m_interval);
    s.setMaxKeepAliveCount(m_maxKeepaliveCount);
    s.setLifetimeCount(m_lifetimeCount);
    s.setStatusCode(QOpcUa::UaStatusCode::Good);
    s.setSamplingInterval(res.revisedSamplingInterval);
    s.setQueueSize(res.revisedQueueSize);
    s.setMonitoredItemId(res.monitoredItemId);
    s.setTriggeredItemIds(successfulTriggerLinks);
    s.setFailedTriggeredItemsStatus(failedTriggerLinks);
    temp->parameters = s;
    temp->clientHandle = m_clientHandle;

    if (res.filterResult.encoding >= UA_EXTENSIONOBJECT_DECODED &&
            res.filterResult.content.decoded.type == &UA_TYPES[UA_TYPES_EVENTFILTERRESULT])
        s.setFilterResult(convertEventFilterResult(&res.filterResult));
    else
        s.clearFilterResult();

    emit m_backend->monitoringEnableDisable(handle, attr, true, s);

    return true;
}

bool QOpen62541Subscription::removeAttributeMonitoredItem(quint64 handle, QOpcUa::NodeAttribute attr)
{
    MonitoredItem *item = getItemForAttribute(handle, attr);
    if (!item) {
        qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "There is no monitored item for this attribute";
        QOpcUaMonitoringParameters s;
        s.setStatusCode(QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid);
        emit m_backend->monitoringEnableDisable(handle, attr, false, s);
        return false;
    }

    UA_StatusCode res = UA_Client_MonitoredItems_deleteSingle(m_backend->m_uaclient, m_subscriptionId, item->monitoredItemId);
    if (res != UA_STATUSCODE_GOOD)
        qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not remove monitored item" << item->monitoredItemId << "from subscription" << m_subscriptionId << ":" << UA_StatusCode_name(res);

    m_itemIdToItemMapping.remove(item->monitoredItemId);
    const auto it = m_nodeHandleToItemMapping.find(handle);
    it->remove(attr);
    if (it->empty())
        m_nodeHandleToItemMapping.remove(it.key());

    delete item;

    QOpcUaMonitoringParameters s;
    s.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res));
    emit m_backend->monitoringEnableDisable(handle, attr, false, s);

    return true;
}

void QOpen62541Subscription::monitoredValueUpdated(UA_UInt32 monId, UA_DataValue *value)
{
    auto item = m_itemIdToItemMapping.constFind(monId);
    if (item == m_itemIdToItemMapping.constEnd())
        return;
    QOpcUaReadResult res;

    if (!value || value == UA_EMPTY_ARRAY_SENTINEL) {
        res.setStatusCode(QOpcUa::UaStatusCode::Good);
        emit m_backend->dataChangeOccurred(item.value()->handle, res);
        return;
    }

    res.setValue(QOpen62541ValueConverter::toQVariant(value->value));
    res.setAttribute(item.value()->attr);
    if (value->hasServerTimestamp)
        res.setServerTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime, UA_DateTime>(&value->serverTimestamp));
    if (value->hasSourceTimestamp)
        res.setSourceTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime, UA_DateTime>(&value->sourceTimestamp));
    res.setStatusCode(value->hasStatus ? QOpcUa::UaStatusCode(value->status) : QOpcUa::UaStatusCode::Good);
    emit m_backend->dataChangeOccurred(item.value()->handle, res);
}

void QOpen62541Subscription::sendTimeoutNotification()
{
    QList<QPair<quint64, QOpcUa::NodeAttribute>> items;
    for (const auto &it : std::as_const(m_nodeHandleToItemMapping)) {
        for (auto item : it) {
            items.push_back({item->handle, item->attr});
        }
    }
    emit timeout(this, items);
    m_timeout = true;
}

void QOpen62541Subscription::eventReceived(UA_UInt32 monId, QVariantList list)
{
    auto item = m_itemIdToItemMapping.constFind(monId);
    if (item == m_itemIdToItemMapping.constEnd())
        return;
    emit m_backend->eventOccurred(item.value()->handle, list);
}

double QOpen62541Subscription::interval() const
{
    return m_interval;
}

UA_UInt32 QOpen62541Subscription::subscriptionId() const
{
    return m_subscriptionId;
}

int QOpen62541Subscription::monitoredItemsCount() const
{
    return m_itemIdToItemMapping.size();
}

QOpcUaMonitoringParameters::SubscriptionType QOpen62541Subscription::shared() const
{
    return m_shared;
}

QOpen62541Subscription::MonitoredItem *QOpen62541Subscription::getItemForAttribute(quint64 nodeHandle, QOpcUa::NodeAttribute attr)
{
    auto nodeEntry = m_nodeHandleToItemMapping.constFind(nodeHandle);

    if (nodeEntry == m_nodeHandleToItemMapping.constEnd())
        return nullptr;

    auto item = nodeEntry->constFind(attr);
    if (item == nodeEntry->constEnd())
        return nullptr;

    return item.value();
}

UA_ExtensionObject QOpen62541Subscription::createFilter(const QVariant &filterData)
{
    UA_ExtensionObject obj;
    UA_ExtensionObject_init(&obj);

    if (filterData.canConvert<QOpcUaMonitoringParameters::DataChangeFilter>()) {
        createDataChangeFilter(filterData.value<QOpcUaMonitoringParameters::DataChangeFilter>(), &obj);
        return obj;
    }

    if (filterData.canConvert<QOpcUaMonitoringParameters::EventFilter>()) {
        Open62541Utils::createEventFilter(filterData.value<QOpcUaMonitoringParameters::EventFilter>(), &obj);
        return obj;
    }

    if (filterData.isValid())
        qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create filter, invalid input.";

    return obj;
}

void QOpen62541Subscription::createDataChangeFilter(const QOpcUaMonitoringParameters::DataChangeFilter &filter, UA_ExtensionObject *out)
{
    UA_DataChangeFilter *uaFilter = UA_DataChangeFilter_new();
    uaFilter->deadbandType = static_cast<UA_UInt32>(filter.deadbandType());
    uaFilter->deadbandValue = filter.deadbandValue();
    uaFilter->trigger = static_cast<UA_DataChangeTrigger>(filter.trigger());
    out->encoding = UA_EXTENSIONOBJECT_DECODED;
    out->content.decoded.type = &UA_TYPES[UA_TYPES_DATACHANGEFILTER];
    out->content.decoded.data = uaFilter;
}

QOpcUaEventFilterResult QOpen62541Subscription::convertEventFilterResult(UA_ExtensionObject *obj)
{
    QOpcUaEventFilterResult result;

    if (!obj)
        return result;

    if (obj->encoding == UA_EXTENSIONOBJECT_DECODED && obj->content.decoded.type == &UA_TYPES[UA_TYPES_EVENTFILTERRESULT]) {
        UA_EventFilterResult *filterResult = static_cast<UA_EventFilterResult *>(obj->content.decoded.data);

        for (size_t i = 0; i < filterResult->selectClauseResultsSize; ++i)
            result.selectClauseResultsRef().append(static_cast<QOpcUa::UaStatusCode>(filterResult->selectClauseResults[i]));

        for (size_t i = 0; i < filterResult->whereClauseResult.elementResultsSize; ++i) {
            QOpcUaContentFilterElementResult temp;
            temp.setStatusCode(static_cast<QOpcUa::UaStatusCode>(filterResult->whereClauseResult.elementResults[i].statusCode));
            for (size_t j = 0; j < filterResult->whereClauseResult.elementResults[i].operandStatusCodesSize; ++j)
                temp.operandStatusCodesRef().append(static_cast<QOpcUa::UaStatusCode>(
                                                        filterResult->whereClauseResult.elementResults[i].operandStatusCodes[j]));
            result.whereClauseResultsRef().append(temp);
        }
    }

    return result;
}

bool QOpen62541Subscription::modifySubscriptionParameters(quint64 nodeHandle, QOpcUa::NodeAttribute attr, const QOpcUaMonitoringParameters::Parameter &item, const QVariant &value)
{
    UA_ModifySubscriptionRequest req;
    UA_ModifySubscriptionRequest_init(&req);
    req.subscriptionId = m_subscriptionId;
    req.requestedPublishingInterval = m_interval;
    req.requestedLifetimeCount = m_lifetimeCount;
    req.requestedMaxKeepAliveCount = m_maxKeepaliveCount;
    req.maxNotificationsPerPublish = m_maxNotificationsPerPublish;

    bool match = true;

    switch (item) {
    case QOpcUaMonitoringParameters::Parameter::PublishingInterval: {
        bool ok;
        req.requestedPublishingInterval = value.toDouble(&ok);
        if (!ok) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify PublishingInterval, value is not a double";
            QOpcUaMonitoringParameters p;
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::LifetimeCount: {
        bool ok;
        req.requestedLifetimeCount = value.toUInt(&ok);
        if (!ok) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify LifetimeCount, value is not an integer";
            QOpcUaMonitoringParameters p;
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::MaxKeepAliveCount: {
        bool ok;
        req.requestedMaxKeepAliveCount = value.toUInt(&ok);
        if (!ok) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify MaxKeepAliveCount, value is not an integer";
            QOpcUaMonitoringParameters p;
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::Priority: {
        bool ok;
        req.priority = value.toUInt(&ok);
        if (!ok) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify Priority, value is not an integer";
            QOpcUaMonitoringParameters p;
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::MaxNotificationsPerPublish: {
        bool ok;
        req.maxNotificationsPerPublish = value.toUInt(&ok);
        if (!ok) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify MaxNotificationsPerPublish, value is not an integer";
            QOpcUaMonitoringParameters p;
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        break;
    }
    default:
        match = false;
        break;
    }

    if (match) {
        UA_ModifySubscriptionResponse res = UA_Client_Subscriptions_modify(m_backend->m_uaclient, req);

        if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
            QOpcUaMonitoringParameters p;
            p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult));
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
        } else {
            QOpcUaMonitoringParameters::Parameters changed = item;
            if (!qFuzzyCompare(res.revisedPublishingInterval, m_interval))
                changed |= QOpcUaMonitoringParameters::Parameter::PublishingInterval;
            if (res.revisedLifetimeCount != m_lifetimeCount)
                changed |= QOpcUaMonitoringParameters::Parameter::LifetimeCount;
            if (res.revisedMaxKeepAliveCount != m_maxKeepaliveCount)
                changed |= QOpcUaMonitoringParameters::Parameter::MaxKeepAliveCount;

            m_lifetimeCount = res.revisedLifetimeCount;
            m_maxKeepaliveCount = res.revisedMaxKeepAliveCount;
            m_interval = res.revisedPublishingInterval;
            if (item == QOpcUaMonitoringParameters::Parameter::Priority)
                m_priority = value.toUInt();
            if (item == QOpcUaMonitoringParameters::Parameter::MaxNotificationsPerPublish)
                m_maxNotificationsPerPublish = value.toUInt();

            QOpcUaMonitoringParameters p;
            p.setStatusCode(QOpcUa::UaStatusCode::Good);
            p.setPublishingInterval(m_interval);
            p.setLifetimeCount(m_lifetimeCount);
            p.setMaxKeepAliveCount(m_maxKeepaliveCount);
            p.setPriority(m_priority);
            p.setMaxNotificationsPerPublish(m_maxNotificationsPerPublish);

            for (auto it : std::as_const(m_itemIdToItemMapping))
                emit m_backend->monitoringStatusChanged(it->handle, it->attr, changed, p);
        }
        return true;
    }
    return false;
}

bool QOpen62541Subscription::modifyMonitoredItemParameters(quint64 nodeHandle, QOpcUa::NodeAttribute attr, const QOpcUaMonitoringParameters::Parameter &item, const QVariant &value)
{
    MonitoredItem *monItem = getItemForAttribute(nodeHandle, attr);
    QOpcUaMonitoringParameters p = monItem->parameters;

    UA_ModifyMonitoredItemsRequest req;
    UA_ModifyMonitoredItemsRequest_init(&req);
    UaDeleter<UA_ModifyMonitoredItemsRequest> requestDeleter(&req, UA_ModifyMonitoredItemsRequest_clear);
    req.subscriptionId = m_subscriptionId;
    req.itemsToModifySize = 1;
    req.itemsToModify = UA_MonitoredItemModifyRequest_new();
    UA_MonitoredItemModifyRequest_init(req.itemsToModify);
    req.itemsToModify->monitoredItemId = monItem->monitoredItemId;
    req.itemsToModify->requestedParameters.discardOldest = monItem->parameters.discardOldest();
    req.itemsToModify->requestedParameters.queueSize = monItem->parameters.queueSize();
    req.itemsToModify->requestedParameters.samplingInterval = monItem->parameters.samplingInterval();
    req.itemsToModify->monitoredItemId = monItem->monitoredItemId;
    req.itemsToModify->requestedParameters.clientHandle = monItem->clientHandle;

    bool match = true;

    switch (item) {
    case QOpcUaMonitoringParameters::Parameter::DiscardOldest: {
        if (value.metaType().id() != QMetaType::Bool) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify DiscardOldest, value is not a bool";
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        req.itemsToModify->requestedParameters.discardOldest = value.toBool();
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::QueueSize: {
        if (value.metaType().id() != QMetaType::UInt) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify QueueSize, value is not an integer";
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        req.itemsToModify->requestedParameters.queueSize = value.toUInt();
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::SamplingInterval: {
        if (value.metaType().id() != QMetaType::Double) {
            qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify SamplingInterval, value is not a double";
            p.setStatusCode(QOpcUa::UaStatusCode::BadTypeMismatch);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        req.itemsToModify->requestedParameters.samplingInterval = value.toDouble();
        break;
    }
    case QOpcUaMonitoringParameters::Parameter::Filter: {
        UA_ExtensionObject filter = createFilter(value);
        if (filter.content.decoded.data)
            req.itemsToModify->requestedParameters.filter = filter;
        else {
            qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to modify filter, filter creation failed";
            p.setStatusCode(QOpcUa::UaStatusCode::BadInternalError);
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        }
        break;
    }
    default:
        match = false;
        break;
    }

    if (match) {
        if (item != QOpcUaMonitoringParameters::Parameter::Filter && p.filter().isValid()) {
            UA_ExtensionObject filter = createFilter(monItem->parameters.filter());
            if (filter.content.decoded.data)
                req.itemsToModify->requestedParameters.filter = filter;
            else {
                qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify monitored item, filter creation failed";
                p.setStatusCode(QOpcUa::UaStatusCode::BadInternalError);
                emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
                return true;
            }
        }

        UA_ModifyMonitoredItemsResponse res = UA_Client_MonitoredItems_modify(m_backend->m_uaclient, req);
        UaDeleter<UA_ModifyMonitoredItemsResponse> responseDeleter(
                    &res, UA_ModifyMonitoredItemsResponse_clear);

        if (res.responseHeader.serviceResult != UA_STATUSCODE_GOOD || res.results[0].statusCode != UA_STATUSCODE_GOOD) {
            p.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res.responseHeader.serviceResult == UA_STATUSCODE_GOOD ? res.results[0].statusCode : res.responseHeader.serviceResult));
            emit m_backend->monitoringStatusChanged(nodeHandle, attr, item, p);
            return true;
        } else {
            p.setStatusCode(QOpcUa::UaStatusCode::Good);
            QOpcUaMonitoringParameters::Parameters changed = item;
            if (!qFuzzyCompare(p.samplingInterval(), res.results[0].revisedSamplingInterval)) {
                p.setSamplingInterval(res.results[0].revisedSamplingInterval);
                changed |= QOpcUaMonitoringParameters::Parameter::SamplingInterval;
            }
            if (p.queueSize() != res.results[0].revisedQueueSize) {
                p.setQueueSize(res.results[0].revisedQueueSize);
                changed |= QOpcUaMonitoringParameters::Parameter::QueueSize;
            }

            if (item == QOpcUaMonitoringParameters::Parameter::DiscardOldest) {
                p.setDiscardOldest(value.toBool());
                changed |= QOpcUaMonitoringParameters::Parameter::DiscardOldest;
            }

            if (item == QOpcUaMonitoringParameters::Parameter::Filter) {
                changed |= QOpcUaMonitoringParameters::Parameter::Filter;
                if (value.canConvert<QOpcUaMonitoringParameters::DataChangeFilter>())
                    p.setFilter(value.value<QOpcUaMonitoringParameters::DataChangeFilter>());
                else if (value.canConvert<QOpcUaMonitoringParameters::EventFilter>())
                    p.setFilter(value.value<QOpcUaMonitoringParameters::EventFilter>());
                if (res.results[0].filterResult.content.decoded.type == &UA_TYPES[UA_TYPES_EVENTFILTERRESULT])
                    p.setFilterResult(convertEventFilterResult(&res.results[0].filterResult));
            }

            emit m_backend->monitoringStatusChanged(nodeHandle, attr, changed, p);

            monItem->parameters = p;
        }
        return true;
    }
    return false;
}

QT_END_NAMESPACE
