//
// Created by magino on 17. 10. 2019.
//

#ifndef MANAGEMENT_H
#define MANAGEMENT_H

#include <core/UaNodeContext.h>
#include "Reservation.h"
#include <iterator>
#include <algorithm>
#include <list>
#include <utility>

class Management: public UaNodeContext {
private:
    UaServer& _server;
    UaNodeId _reservationsArray; // Holds all reservation objects
    std::list<Reservation> _reservations;

    int _state = 1;
    int _availability = 2;

    int _reservationIdCounter = 100;

public:
    static UaNodeId TYPE_NODEID;
    // Browse names - are static because i want reach them from everywhere in code
    static const std::string TYPE_OBJ;
    static const std::string INSTANCE_OBJ;

    static const std::string StateVar;
    static const std::string ReservationCountVar;
    static const std::string AvailabilityVar;
    static const std::string ActualReservationIdVar;

    static const std::string ReservationArrayObj; // Holds all reservation objects

    static const std::string MakeReservationMethod;
    static const std::string DeleteReservationMethod;
    static const std::string DeleteAllReservationsMethod;

    static void CreateUaObjectType(UaServer& server) {
        TYPE_NODEID = UaNodeId(server.GetAppNsIndex(), "MngmntType");
        server.AddObjectTypeNode("ManagementType",
                                 Management::TYPE_OBJ,
                                 "Management take care of reservations for process cell.",
                                 TYPE_NODEID);

        UaNodeId tmpNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddVariableNode("State",
                               Management::StateVar,
                               "State of management service.",
                               true,
                               UA_ACCESSLEVELMASK_READ,
                               UA_TYPES_INT32,
                               UA_VALUERANK_SCALAR,
                               0,
                               nullptr,
                               TYPE_NODEID,
                               tmpNodeId);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddVariableNode("Reservation Count",
                               Management::ReservationCountVar,
                               "Number of successful reservations.",
                               true,
                               UA_ACCESSLEVELMASK_READ,
                               UA_TYPES_INT32,
                               UA_VALUERANK_SCALAR,
                               0,
                               nullptr,
                               TYPE_NODEID,
                               tmpNodeId);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddVariableNode("Availability",
                               Management::AvailabilityVar,
                               "Availability of the process cell services.",
                               true,
                               UA_ACCESSLEVELMASK_READ,
                               UA_TYPES_INT32,
                               UA_VALUERANK_SCALAR,
                               0,
                               nullptr,
                               TYPE_NODEID,
                               tmpNodeId);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddVariableNode("Actual Reservation Id",
                               Management::ActualReservationIdVar,
                               "Id of reservation, which is on turn",
                               true,
                               UA_ACCESSLEVELMASK_READ,
                               UA_TYPES_UINT64,
                               UA_VALUERANK_SCALAR,
                               0,
                               nullptr,
                               TYPE_NODEID,
                               tmpNodeId);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddObjectNode("Reservation Array",
                             Management::ReservationArrayObj,
                             "Holds all reservations objects.",
                             true,
                             TYPE_NODEID,
                             tmpNodeId);

        ArgumentList _inMakeRes;
        _inMakeRes.AddScalarArgument("Product Id", "Unique identifier of product.", UA_TYPES[UA_TYPES_UINT64].typeIndex, nullptr);
        _inMakeRes.AddScalarArgument("Action Id", "Numeric identifier of action.", UA_TYPES[UA_TYPES_BYTE].typeIndex, nullptr);
        _inMakeRes.AddScalarArgument("Operation Order", "Order of reserved operation in product recipe.", UA_TYPES[UA_TYPES_BYTE].typeIndex, nullptr);
        _inMakeRes.AddScalarArgument("Parameter A", "Input parameter A for process cell", UA_TYPES[UA_TYPES_FLOAT].typeIndex, nullptr);
        _inMakeRes.AddScalarArgument("Parameter B", "Input parameter B for process cell",UA_TYPES[UA_TYPES_FLOAT].typeIndex, nullptr);

        ArgumentList _outMakeRes;
        _outMakeRes.AddScalarArgument("Status", "True if making reservation was successful, otherwise false.", UA_TYPES[UA_TYPES_BOOLEAN].typeIndex, nullptr);
        _outMakeRes.AddScalarArgument("Reservation Id", "Unique Id of created reservation.", UA_TYPES[UA_TYPES_UINT64].typeIndex, nullptr);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddMethodNode("Make Reservation",
                             Management::MakeReservationMethod,
                             "Method for create reservation of desired ActionId with parameters.",
                             true,
                             _inMakeRes,
                             _outMakeRes,
                             TYPE_NODEID,
                             tmpNodeId);


        ArgumentList _inCanRes;
        _inCanRes.AddScalarArgument("Reservation Id", "Unique reservation Id which has to be deleted.",UA_TYPES[UA_TYPES_UINT64].typeIndex, nullptr);
        ArgumentList _outCanRes;
        _outCanRes.AddScalarArgument("Status", "True if reservation was successfully deleted, otherwise false.",UA_TYPES[UA_TYPES_BOOLEAN].typeIndex, nullptr);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddMethodNode("Delete Reservation",
                             Management::DeleteReservationMethod,
                             "Method for delete reservation.",
                             true,
                             _inCanRes,
                             _outCanRes,
                             TYPE_NODEID,
                             tmpNodeId);

        ArgumentList _inDelAll;
        ArgumentList _outDelAll;
        _outDelAll.AddScalarArgument("Status", "True if deleting reservations was successful, otherwise false." ,UA_TYPES[UA_TYPES_BOOLEAN].typeIndex, nullptr);

        tmpNodeId = UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId());
        server.AddMethodNode("Delete all Reservations",
                             Management::DeleteAllReservationsMethod,
                             "Method for delete all reservations (For Debug Puposes).",
                             true,
                             _inDelAll,
                             _outDelAll,
                             TYPE_NODEID,
                             tmpNodeId);

        SetTypeLifeCycle(server, TYPE_NODEID);
    }

    Management(UaServer &server):
            _server(server),
            UaNodeContext(UaNodeId(server.GetAppNsIndex(), UaNodeContext::GetUniqueNumericNodeId())) {}
    ~Management() override = default;

    bool Instantiate(UaNodeId& parent, UaNodeId& reference, std::string name) {
        UaNodeId returnNodeId = _server.AddObjectNode(name, Management::INSTANCE_OBJ, "", false, parent, instanceNodeId, reference, Management::TYPE_NODEID, this);

        return !returnNodeId.IsNull() && instanceNodeId == returnNodeId;
    }

    void DeleteNode() {
        if(!instanceNodeId.IsNull()) {
            _server.DeleteNode(instanceNodeId, true);
            UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Management Node  with nodeid:%s  has been deleted",
                        instanceNodeId.ToString().c_str());
        }
    }

    bool TypeConstruct(UaNodeId& instanceNodeId, UaNodeId& typeNodeId, void* instanceCtx = nullptr) override {
        UaNodeId foundedNodeId = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::StateVar, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            SetValueCallback(_server, foundedNodeId);
        }

        foundedNodeId = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::ReservationCountVar, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            SetValueCallback(_server, foundedNodeId);
        }

        foundedNodeId = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::AvailabilityVar, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            SetValueCallback(_server, foundedNodeId);
        }

        foundedNodeId = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::ActualReservationIdVar, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            SetValueCallback(_server, foundedNodeId);
        }


        _reservationsArray = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::ReservationArrayObj, _server.GetAppNsIndex());

        foundedNodeId = _server.FindSingleNodeId(instanceNodeId, Management::MakeReservationMethod, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            _server.SetMethodCallback(foundedNodeId, MethodCallback);
        }

        foundedNodeId = _server.FindSingleNodeId(instanceNodeId, Management::DeleteReservationMethod, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            _server.SetMethodCallback(foundedNodeId, MethodCallback);
        }

        foundedNodeId = _server.FindSingleNodeId(instanceNodeId, Management::DeleteAllReservationsMethod, _server.GetAppNsIndex());
        if(!foundedNodeId.IsNull()) {
            _server.SetNodeContext(foundedNodeId, this);
            _server.SetMethodCallback(foundedNodeId, MethodCallback);
        }

        return true;
    }

    void ReadValue(UaNodeId& node, const UA_NumericRange* range, const UA_DataValue* value) override {
        std::string name = _server.ReadBrowseName(node);
        UA_Variant val;
        if(name == Management::StateVar) {
            UA_Variant_setScalar(&val, &_state, &UA_TYPES[UA_TYPES_INT32]);
            UA_Server_writeValue(_server.GetServer(), node.GetNodeId(), val);
        }

        if(name == Management::ReservationCountVar) {
            int size = _reservations.size();
            UA_Variant_setScalar(&val, &size, &UA_TYPES[UA_TYPES_INT32]);
            UA_Server_writeValue(_server.GetServer(), node.GetNodeId(), val);
        }

        if(name == Management::AvailabilityVar) {
            UA_Variant_setScalar(&val, &_availability, &UA_TYPES[UA_TYPES_INT32]);
            UA_Server_writeValue(_server.GetServer(), node.GetNodeId(), val);
        }

        if(name == Management::ActualReservationIdVar) {
            uint64_t actualResId = 0;
            if(!_reservations.empty())
                actualResId = _reservations.front().id;

            UA_Variant_setScalar(&val, &actualResId, &UA_TYPES[UA_TYPES_UINT64]);
            UA_Server_writeValue(_server.GetServer(), node.GetNodeId(), val);
        }
    }

    // Don't need to implement write because all nodes are read only
    //void WriteValue(UaNodeId& node, const UA_NumericRange* range, const UA_DataValue& value) override {}

    UA_StatusCode CallBack(UaNodeId& methodNodeId,
                           size_t inputSize,
                           const UA_Variant* input,
                           size_t outputSize,
                           UA_Variant* output) override {
        std::string name = _server.ReadBrowseName(methodNodeId);

        if(name == Management::MakeReservationMethod) {
            if(inputSize == 5) {
                auto productId = GetVariantValue<uint64_t>(input[0]);
                auto actionId = GetVariantValue<UA_Byte>(input[1]);
                auto operationOrder = GetVariantValue<UA_Byte>(input[2]);
                auto parameterA = GetVariantValue<float>(input[3]);
                auto parameterB = GetVariantValue<float>(input[4]);

                uint64_t resId = MakeReservation(productId, actionId, operationOrder, parameterA, parameterB);
                UA_Boolean returnVal = true;
                if(resId == 0) returnVal = false;

                UA_Variant_setScalarCopy(output, &returnVal, &UA_TYPES[UA_TYPES_BOOLEAN]);
                UA_Variant_setScalarCopy(output+1, &resId, &UA_TYPES[UA_TYPES_UINT64]);
            }
            else return UA_STATUSCODE_BADMETHODINVALID;
        }

        if(name == Management::DeleteReservationMethod) {
            if(inputSize == 1) {
                auto resId = GetVariantValue<uint64_t>(input[0]);
                UA_Boolean returnVal = DeleteReservation(resId);

                //Update new reservation
                if(!_reservations.empty())
                {
                    UaNodeId foundedNodeId = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::ActualReservationIdVar, _server.GetAppNsIndex());
                    UA_Variant val;
                    uint64_t actualResId = _reservations.front().id;
                    UA_Variant_setScalar(&val, &actualResId, &UA_TYPES[UA_TYPES_UINT64]);
                    UA_Server_writeValue(_server.GetServer(), foundedNodeId.GetNodeId(), val);
                }

                UA_Variant_setScalarCopy(output, &returnVal, &UA_TYPES[UA_TYPES_BOOLEAN]);
            } else return UA_STATUSCODE_BADMETHODINVALID;
        }

        if(name == Management::DeleteAllReservationsMethod) {
            for(auto& res: _reservations)
                res.DeleteNode();

            _reservations.clear();

            UA_Boolean returnVal = true;
            UA_Variant_setScalarCopy(output, &returnVal, &UA_TYPES[UA_TYPES_BOOLEAN]);
        }
        return UA_STATUSCODE_GOOD;
    }

    uint64_t MakeReservation(uint64_t productId, UA_Byte actionId, UA_Byte operationOrder, float parameterA, float parameterB) {
        std::string uuid_name = "Reservation " + std::to_string(_reservationIdCounter);
        _reservations.emplace_back(_server, _reservationIdCounter, productId, actionId, operationOrder, parameterA, parameterB);

        if (_reservations.back().Instantiate(_reservationsArray, UaNodeId::HasComponent, uuid_name)) {
            auto oldReservationId = _reservationIdCounter;
            ++_reservationIdCounter;
            return oldReservationId;
        }

        _reservations.pop_back();
        return 0;
    }

    bool DeleteReservation(uint64_t reservationid) {
        for (auto itr = _reservations.begin(); itr != _reservations.end(); ++itr) {
            if((*itr).id == reservationid) {
                (*itr).DeleteNode();
                _reservations.erase(itr);
                return true;
            }
        }
        return false;
    }

    // This method needs to be called when reservation will be processed by process cell
     void RemoveActualReservation() {
        if(_reservations.empty()) return;
        _reservations.pop_front();
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Deleting actual reservation.");

        if(_reservations.empty()) return;
        // Ak je dalsia v rade nastavim jej id ako aktualne

        UaNodeId foundedNodeId = _server.BrowseSimplifiedSingleNodeId(instanceNodeId, Management::ActualReservationIdVar, _server.GetAppNsIndex());

        UA_Variant val;
        uint64_t actualResId = _reservations.front().id;
        UA_Variant_setScalar(&val, &actualResId, &UA_TYPES[UA_TYPES_UINT64]);
        UA_Server_writeValue(_server.GetServer(), foundedNodeId.GetNodeId(), val);
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Setting new actual reservation id.");
    }
};
UaNodeId Management::TYPE_NODEID = UaNodeId();

const std::string Management::INSTANCE_OBJ = "Management";
const std::string Management::TYPE_OBJ = "ManagementType";

const std::string Management::StateVar = "State";
const std::string Management::ReservationCountVar = "ReservationCount";
const std::string Management::AvailabilityVar = "Availability";
const std::string Management::ActualReservationIdVar = "ActualReservationId";

const std::string Management::ReservationArrayObj = "ReservationArray";

const std::string Management::MakeReservationMethod = "MakeReservation";
const std::string Management::DeleteReservationMethod = "DeleteReservation";
const std::string Management::DeleteAllReservationsMethod = "DeleteAllReservations";

#endif //MANAGEMENT_H