//
// Created by magino on 18.7.2019.
//

#ifndef UACLIENT_H
#define UACLIENT_H

#include <thread>
#include <open62541/plugin/log_stdout.h>
#include <open62541/client_highlevel.h>
#include <open62541/client_highlevel_async.h>

#include "UaSubscription.h"
#include "core/UaObjects.h"
#include "core/UaVariant.h"

typedef std::shared_ptr<UaSubscription> UaSubscriptionRef;
typedef std::map<unsigned, UaSubscriptionRef> UaSubscriptionMap;

class UaClient {
private:
    UA_Client* _client;
    UA_ClientConfig* _config;
    UaSubscriptionMap _subscriptions;

    std::thread _thread;
    volatile bool _running;

    static void StateCallback(UA_Client* client, UA_ClientState clientState);
    static void SubscriptionInactivityCallback(UA_Client* client, UA_UInt32 subscriptionId, void* subContext);
    static void ClientAsyncServiceCallback (UA_Client* client, void* userdata, UA_UInt32 requestId, void* response);
public:
    UaClient();
    ~UaClient();

    virtual void StateChange(UA_ClientState clientState);
    UA_Client* GetClient() { return _client; };

    /**
    * Client Lifecycle
    * ---------------- */
    UA_ClientState GetState() const { return UA_Client_getState(_client); }
    UA_ClientConfig& GetConfig() const { return *UA_Client_getConfig(_client); }
    void* GetContext() const { return UA_Client_getContext(_client); }
    void ResetClient() { UA_Client_reset(_client); }

    /**
     * Connect to a Server
     * ------------------- */
    bool ConnectAnonymous(const std::string& url) const;
    bool ConnectAnonymousAsync(const std::string& url) const;
    bool ConnectNoSession(const std::string& url) const;
    bool ConnectWithCredentials(const std::string& url, const std::string& userName, const std::string& password) const;

    bool Disconnect();
    bool DisconnectAsync();

    /**
     * Discovery Service set
     * ^^^^^^^^^^^^^^^^^^^^^ */
    bool FindServers(const std::string& url, StringArray& serverUris, StringArray& localeIds, ApplicationDescriptionArray& registeredServers);
    bool FindServersOnNetwork(const std::string& url, unsigned startingRecordId, unsigned maxRecordsToReturn,
                              StringArray& serverCapabilityFilter, ServerOnNetworkArray& serversOnNetwork);
    bool GetEndpoints(const std::string& url, EndpointDescriptionArray& endpoints);

    bool FindServersByUri(const std::string& serverUri,  ApplicationDescriptionArray& registeredServers);
    // TODO Get endpoints with filter

    /*
     * Attribute Service Set
     * ^^^^^^^^^^^^^^^^^^^^^ */
    /**
     * Read Attributes
     * ---------------*/
    // Or more specific in client_high_level.h (for work with one node at once)
    bool ReadAttribute(UaNodeId& nodeId, UA_AttributeId attributeId, const UA_DataType* outDataType, void* outData);

    template<typename T>
    bool ReadValue(UaNodeId& nodeId, T* value) {
        UaVariant variant;
        auto status = UA_Client_readValueAttribute(_client, nodeId.GetNodeId(), variant.GetVariantRef());
        if (status != UA_STATUSCODE_GOOD) {
            value = nullptr;
            return false;
        }
        *value = variant.GetVariantValue<T>();
        return status == UA_STATUSCODE_GOOD;
    }

    template<typename T>
    bool WriteValue(UaNodeId& nodeId, T value, UA_DataType type) {
        UaVariant variant{};
        UA_Variant_setScalarCopy(variant.GetVariantRef(), &value, &type);

        auto status = UA_Client_writeValueAttribute(_client, nodeId.GetNodeId(), variant.GetVariantRef());
        return status == UA_STATUSCODE_GOOD;
    }

    /**
     * Write Attributes
     * ----------------*/
    // Or more specific in client_high_level.h (for work with one node at once)
    bool WriteAttribute(UaNodeId& nodeId, UA_AttributeId attributeId, const UA_DataType* inDataType, const void* inData);

    /*
    * Historical Access Service Set
    * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
    // UA_Client_Service_historyRead
    // UA_Client_Service_historyUpdate
    // Or more specific in client_high_level.h

    /*
     * Method Service Set
     * ^^^^^^^^^^^^^^^^^^ */
    // UA_Client_Service_call (client.h)
    // UA_Client_call (client_high_level.h)
    /*
    UA_Client_call(UA_Client *client, const UA_NodeId objectId,
    const UA_NodeId methodId, size_t inputSize, const UA_Variant *input,
            size_t *outputSize, UA_Variant **output);
    */
    bool CallMethod(UaNodeId& objectId, UaNodeId& methodId, VariantArray& inputs, VariantArray& outputs) {
        auto status = UA_Client_call(_client, objectId.GetNodeId(), methodId.GetNodeId(), inputs.length(), inputs.data(), outputs.lengthRef(), outputs.dataRef());
        return status == UA_STATUSCODE_GOOD;
    }


    /*
     * NodeManagement Service Set
     * ^^^^^^^^^^^^^^^^^^^^^^^^^^ */
    // UA_Client_Service_addNodes
    // UA_Client_Service_addReferences
    // UA_Client_Service_deleteNodes
    // UA_Client_Service_deleteReferences
    // Or more specific in client_high_level.h (for work with one node at once)

    /*
     * View Service Set
     * ^^^^^^^^^^^^^^^^ */
    // UA_Client_Service_browse
    bool Browse(UA_BrowseRequest& br, ReferenceDescriptionArray& references, unsigned maxReferences = 0) {
        UA_BrowseResponse response = UA_Client_Service_browse(_client, br);
        if(response.responseHeader.serviceResult != UA_STATUSCODE_GOOD) return false;
        references.setList(response.results[0].referencesSize, response.results[0].references);
        // result.continuationPoint TODO continuationPoint?
        return true;
    }

    void BrowseObjects() {
        UA_BrowseRequest bReq;
        UA_BrowseRequest_init(&bReq);
        bReq.requestedMaxReferencesPerNode = 0;
        bReq.nodesToBrowse = UA_BrowseDescription_new();
        bReq.nodesToBrowseSize = 1;
        bReq.nodesToBrowse[0].nodeId = UaNodeId::ObjectsFolder.GetNodeId();
        bReq.nodesToBrowse[0].resultMask = UA_BROWSERESULTMASK_ALL;
        bReq.nodesToBrowse[0].browseDirection = UA_BROWSEDIRECTION_FORWARD;
        bReq.nodesToBrowse[0].nodeClassMask = UA_NODECLASS_OBJECT;
        bReq.nodesToBrowse[0].includeSubtypes = false;
        bReq.nodesToBrowse[0].referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);

        UA_BrowseResponse bResp = UA_Client_Service_browse(_client, bReq);
    }

    //UA_Client_Service_translateBrowsePathsToNodeIds()
    UA_TranslateBrowsePathsToNodeIdsResponse TranslateBrowsePathToNodeIds(UA_TranslateBrowsePathsToNodeIdsRequest& bpr) {
        UA_TranslateBrowsePathsToNodeIdsResponse browsePathResult = UA_Client_Service_translateBrowsePathsToNodeIds(_client, bpr);
        return browsePathResult;
    }
    UaNodeId FindSingleNodeId(UaNodeId& startingNodeId, UaNodeId& reference, const std::string& name, int namespaceIndex) {
        UA_BrowsePath bp;
        UA_BrowsePath_init(&bp);
        bp.startingNode = startingNodeId.GetNodeId();
        bp.relativePath.elementsSize = 1;
        bp.relativePath.elements = (UA_RelativePathElement*)UA_Array_new(1, &UA_TYPES[UA_TYPES_RELATIVEPATHELEMENT]);

        UA_RelativePathElement* rpe = &bp.relativePath.elements[0];
        rpe->referenceTypeId = reference.GetNodeId();
        //rpe.isInverse = false;
        //rpe.includeSubtypes = false;
        rpe->targetName = UA_QUALIFIEDNAME_ALLOC(namespaceIndex, (char*)&name[0]);

        UA_TranslateBrowsePathsToNodeIdsRequest request;
        UA_TranslateBrowsePathsToNodeIdsRequest_init(&request);
        request.browsePaths = &bp;
        request.browsePathsSize = 1;

        UA_TranslateBrowsePathsToNodeIdsResponse result = TranslateBrowsePathToNodeIds(request);
        if(result.resultsSize == 1 && result.responseHeader.serviceResult == UA_STATUSCODE_GOOD) {
            return result.results[0].targets[0].targetId.nodeId;
        }
        return UaNodeId::Null;
    }
    // UA_Client_Service_browseNext
    // UA_Client_Service_translateBrowsePathsToNodeIds
    // UA_Client_Service_registerNodes
    // UA_Client_Service_unregisterNodes

    /*
     * Query Service Set
     * ^^^^^^^^^^^^^^^^^ */
    // UA_Client_Service_queryFirst
    // UA_Client_Service_queryNext

    /**
     * Misc Highlevel Functionality
     * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
     // UA_Client_NamespaceGetIndex
     // UA_Client_forEachChildNodeCall

    /**
     * Asynchronous Services
     * --------------------- */
    void Run(unsigned short timeout) {
        _thread = std::thread([this] () {
            _running = true;
            while (_running) {
                UA_Client_run_iterate(_client, 500);
                UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Running!");
            }
        });
    }
    // TODO one thread for more clients

    void Stop() {
        _running = false;
    }

    /**
    * Timed Callbacks
    * --------------- */
    // UA_Client_addTimedCallback
    // UA_Client_addRepeatedCallback
    // UA_Client_changeRepeatedCallbackInterval
    // UA_Client_removeCallback

    /*
     * Subscription Service Set
     * ^^^^^^^^^^^^^^^^^^^^^^^^ */
    // Wrapper in UaSubscription.h with MonitoredItems Service Set
    UaSubscriptionRef CreateSubscription();
    bool RemoveSubscription(unsigned id);
    // RemoveSubscriptions();
};
#endif //UACLIENT_H