/*
 *  client_report_control.c
 *
 *  Implementation of the ClientReportControlBlock class
 *
 *  Copyright 2013-2022 Michael Zillgith
 *
 *  This file is part of libIEC61850.
 *
 *  libIEC61850 is 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.
 *
 *  libIEC61850 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with libIEC61850.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  See COPYING file for the complete license text.
 */

#include "iec61850_client.h"

#include "stack_config.h"

#include "ied_connection_private.h"

#include "libiec61850_platform_includes.h"

static bool
isBufferedRcb(const char* objectReference, bool* isValid)
{
    const char* separator = strchr(objectReference, '.');

    if (separator == NULL)
        separator = strchr(objectReference, '$');

    if (separator == NULL) {
        *isValid = false;
        return false;
    }

    *isValid = true;

    if (*(separator + 1) == 'B')
        return true;
    else
        return false;
}

ClientReportControlBlock
ClientReportControlBlock_create(const char* objectReference)
{
    bool isReferenceValid;
    bool isBuffered;

    isBuffered = isBufferedRcb(objectReference, &isReferenceValid);

    if (isReferenceValid == false)
    {
        if (DEBUG_IED_CLIENT)
            printf("DEBUG_IED_CLIENT: RCB reference invalid\n");

        return NULL;
    }

    ClientReportControlBlock self =
        (ClientReportControlBlock)GLOBAL_CALLOC(1, sizeof(struct sClientReportControlBlock));

    if (self)
    {
        self->objectReference = StringUtils_copyString(objectReference);
        self->isBuffered = isBuffered;
    }

    return self;
}

void
ClientReportControlBlock_destroy(ClientReportControlBlock self)
{
    if (self)
    {
        GLOBAL_FREEMEM(self->objectReference);

        MmsValue_delete(self->rptId);
        MmsValue_delete(self->rptEna);
        MmsValue_delete(self->resv);
        MmsValue_delete(self->datSet);
        MmsValue_delete(self->confRev);
        MmsValue_delete(self->optFlds);
        MmsValue_delete(self->bufTm);
        MmsValue_delete(self->sqNum);
        MmsValue_delete(self->trgOps);
        MmsValue_delete(self->intgPd);
        MmsValue_delete(self->gi);
        MmsValue_delete(self->purgeBuf);
        MmsValue_delete(self->entryId);
        MmsValue_delete(self->timeOfEntry);
        MmsValue_delete(self->resvTms);
        MmsValue_delete(self->owner);

        GLOBAL_FREEMEM(self);
    }
}

char*
ClientReportControlBlock_getObjectReference(ClientReportControlBlock self)
{
    return self->objectReference;
}

bool
ClientReportControlBlock_isBuffered(ClientReportControlBlock self)
{
    return self->isBuffered;
}

const char*
ClientReportControlBlock_getRptId(ClientReportControlBlock self)
{
    if (self->rptId)
        return MmsValue_toString(self->rptId);
    else
        return NULL;
}

void
ClientReportControlBlock_setRptId(ClientReportControlBlock self, const char* rptId)
{
    if (self->rptId == NULL)
        self->rptId = MmsValue_newVisibleString(rptId);
    else
        MmsValue_setVisibleString(self->rptId, rptId);
}

bool
ClientReportControlBlock_getRptEna(ClientReportControlBlock self)
{
    if (self->rptEna)
    {
        return MmsValue_getBoolean(self->rptEna);
    }
    else
        return false;
}

void
ClientReportControlBlock_setRptEna(ClientReportControlBlock self, bool rptEna)
{
    if (self->rptEna == NULL)
        self->rptEna = MmsValue_newBoolean(rptEna);
    else
        MmsValue_setBoolean(self->rptEna, rptEna);
}

bool
ClientReportControlBlock_getResv(ClientReportControlBlock self)
{
    if (self->resv)
    {
        return MmsValue_getBoolean(self->resv);
    }
    else
        return false;
}

void
ClientReportControlBlock_setResv(ClientReportControlBlock self, bool resv)
{
    if (self->resv == NULL)
        self->resv = MmsValue_newBoolean(resv);
    else
        MmsValue_setBoolean(self->resv, resv);
}

const char*
ClientReportControlBlock_getDataSetReference(ClientReportControlBlock self)
{
    if (self->datSet)
        return MmsValue_toString(self->datSet);
    else
        return NULL;
}

void
ClientReportControlBlock_setDataSetReference(ClientReportControlBlock self, const char* dataSetReference)
{
    if (self->datSet == NULL)
        self->datSet = MmsValue_newVisibleString(dataSetReference);
    else
        MmsValue_setVisibleString(self->datSet, dataSetReference);
}

uint32_t
ClientReportControlBlock_getConfRev(ClientReportControlBlock self)
{
    if (self->confRev)
        return MmsValue_toUint32(self->confRev);
    else
        return 0;
}

int
ClientReportControlBlock_getOptFlds(ClientReportControlBlock self)
{
    if (self->optFlds)
        return (MmsValue_getBitStringAsInteger(self->optFlds) / 2);
    else
        return 0;
}

void
ClientReportControlBlock_setOptFlds(ClientReportControlBlock self, int optFlds)
{
    if (self->optFlds == NULL)
        self->optFlds = MmsValue_newBitString(10);

    MmsValue_setBitStringFromInteger(self->optFlds, optFlds * 2); /* bit 0 is reserved in MMS mapping */
}

uint32_t
ClientReportControlBlock_getBufTm(ClientReportControlBlock self)
{
    if (self->bufTm)
        return MmsValue_toUint32(self->bufTm);
    else
        return 0;
}

void
ClientReportControlBlock_setBufTm(ClientReportControlBlock self, uint32_t bufTm)
{
    if (self->bufTm == NULL)
        self->bufTm = MmsValue_newUnsignedFromUint32(bufTm);
    else
        MmsValue_setUint32(self->bufTm, bufTm);
}

uint16_t
ClientReportControlBlock_getSqNum(ClientReportControlBlock self)
{
    if (self->sqNum)
        return (uint16_t) MmsValue_toUint32(self->sqNum);
    else
        return 0;
}

int
ClientReportControlBlock_getTrgOps(ClientReportControlBlock self)
{
    int triggerOps = 0;

    if (self->trgOps)
    {
        if (MmsValue_getBitStringBit(self->trgOps, 1))
            triggerOps += TRG_OPT_DATA_CHANGED;
        if (MmsValue_getBitStringBit(self->trgOps, 2))
            triggerOps += TRG_OPT_QUALITY_CHANGED;
        if (MmsValue_getBitStringBit(self->trgOps, 3))
            triggerOps += TRG_OPT_DATA_UPDATE;
        if (MmsValue_getBitStringBit(self->trgOps, 4))
            triggerOps += TRG_OPT_INTEGRITY;
        if (MmsValue_getBitStringBit(self->trgOps, 5))
            triggerOps += TRG_OPT_GI;
    }

    return triggerOps;
}

void
ClientReportControlBlock_setTrgOps(ClientReportControlBlock self, int trgOps)
{
    if (self->trgOps == NULL)
        self->trgOps = MmsValue_newBitString(6);

    MmsValue_setBitStringFromInteger(self->trgOps, trgOps << 1);
}

uint32_t
ClientReportControlBlock_getIntgPd(ClientReportControlBlock self)
{
    if (self->intgPd)
        return MmsValue_toUint32(self->intgPd);
    else
        return 0;
}

void
ClientReportControlBlock_setIntgPd(ClientReportControlBlock self, uint32_t intgPd)
{
    if (self->intgPd == NULL)
        self->intgPd = MmsValue_newUnsignedFromUint32(intgPd);
    else
        MmsValue_setUint32(self->intgPd, intgPd);
}

bool
ClientReportControlBlock_getGI(ClientReportControlBlock self)
{
    if (self->gi)
        return MmsValue_getBoolean(self->gi);
    else
        return false;
}

void
ClientReportControlBlock_setGI(ClientReportControlBlock self, bool gi)
{
    if (self->gi == NULL)
        self->gi = MmsValue_newBoolean(gi);
    else
        MmsValue_setBoolean(self->gi, gi);
}

bool
ClientReportControlBlock_getPurgeBuf(ClientReportControlBlock self)
{
    if (self->purgeBuf)
        return MmsValue_getBoolean(self->purgeBuf);
    else
        return false;
}

void
ClientReportControlBlock_setPurgeBuf(ClientReportControlBlock self, bool purgeBuf)
{
    if (self->purgeBuf == NULL)
        self->purgeBuf = MmsValue_newBoolean(purgeBuf);
    else
        MmsValue_setBoolean(self->purgeBuf, purgeBuf);
}

bool
ClientReportControlBlock_hasResvTms(ClientReportControlBlock self)
{
    return (self->resvTms != NULL);
}

int16_t
ClientReportControlBlock_getResvTms(ClientReportControlBlock self)
{
    if (self->resvTms)
        return (int16_t) MmsValue_toInt32(self->resvTms);
    else
        return 0;
}

void
ClientReportControlBlock_setResvTms(ClientReportControlBlock self, int16_t resvTms)
{
    if (self->resvTms == NULL)
        self->resvTms = MmsValue_newIntegerFromInt16(resvTms);
    else
        MmsValue_setInt32(self->resvTms, (int32_t) resvTms);
}

MmsValue*
ClientReportControlBlock_getEntryId(ClientReportControlBlock self)
{
    return self->entryId;
}

void
ClientReportControlBlock_setEntryId(ClientReportControlBlock self, MmsValue* entryId)
{
    if (self->entryId)
    {
        MmsValue_update(self->entryId, entryId);
    }
    else
    {
        if (MmsValue_getType(entryId) != MMS_OCTET_STRING)
        {
            if (DEBUG_IED_CLIENT)
                printf("IED_CLIENT: ClientReportControlBlock_setEntryId invalid argument type\n");
        }
        else
            self->entryId = MmsValue_clone(entryId);
    }
}

uint64_t
ClientReportControlBlock_getEntryTime(ClientReportControlBlock self)
{
    if (self->timeOfEntry)
        return MmsValue_getBinaryTimeAsUtcMs(self->timeOfEntry);
    else
        return 0;
}

MmsValue*
ClientReportControlBlock_getOwner(ClientReportControlBlock self)
{
    return self->owner;
}

static void
updateOrClone(MmsValue** valuePtr, MmsValue* values, int index)
{
    if (*valuePtr)
        MmsValue_update(*valuePtr, MmsValue_getElement(values, index));
    else
        *valuePtr = MmsValue_clone(MmsValue_getElement(values, index));
}

static bool
checkElementType(MmsValue* value, int index, MmsType type)
{
    MmsValue* element = MmsValue_getElement(value, index);

    if (element == NULL)
        return false;

    if (MmsValue_getType(element) == type)
        return true;
    else
        return false;
}

bool
clientReportControlBlock_updateValues(ClientReportControlBlock self, MmsValue* values)
{
    /* check type and return false if type is wrong */

    if (MmsValue_getType(values) != MMS_STRUCTURE)
        return false;

    int rcbElementCount = MmsValue_getArraySize(values);

    if (self->isBuffered)
    {
        if ((rcbElementCount < 13) || (rcbElementCount > 15))
        {
            return false;
        }

        if (!checkElementType(values, 0, MMS_VISIBLE_STRING))
            return false;
        if (!checkElementType(values, 1, MMS_BOOLEAN))
            return false;
        if (!checkElementType(values, 2, MMS_VISIBLE_STRING))
            return false;
        if (!checkElementType(values, 3, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 4, MMS_BIT_STRING))
            return false;
        if (!checkElementType(values, 5, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 6, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 7, MMS_BIT_STRING))
            return false;
        if (!checkElementType(values, 8, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 9, MMS_BOOLEAN))
            return false;
        if (!checkElementType(values, 10, MMS_BOOLEAN))
            return false;
        if (!checkElementType(values, 11, MMS_OCTET_STRING))
            return false;
        if (!checkElementType(values, 12, MMS_BINARY_TIME))
            return false;

        if (rcbElementCount == 14)
        {
            if (!checkElementType(values, 13, MMS_OCTET_STRING) && !checkElementType(values, 13, MMS_INTEGER))
                return false;
        }
        else if (rcbElementCount == 15)
        {
            if (!checkElementType(values, 13, MMS_INTEGER))
                return false;
            if (!checkElementType(values, 14, MMS_OCTET_STRING))
                return false;
        }
    }
    else
    {
        if ((rcbElementCount < 11) || (rcbElementCount > 12))
        {
            return false;
        }

        if (!checkElementType(values, 0, MMS_VISIBLE_STRING))
            return false;
        if (!checkElementType(values, 1, MMS_BOOLEAN))
            return false;
        if (!checkElementType(values, 2, MMS_BOOLEAN))
            return false;
        if (!checkElementType(values, 3, MMS_VISIBLE_STRING))
            return false;
        if (!checkElementType(values, 4, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 5, MMS_BIT_STRING))
            return false;
        if (!checkElementType(values, 6, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 7, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 8, MMS_BIT_STRING))
            return false;
        if (!checkElementType(values, 9, MMS_UNSIGNED))
            return false;
        if (!checkElementType(values, 10, MMS_BOOLEAN))
            return false;

        if (rcbElementCount == 12)
        {
            if (!checkElementType(values, 11, MMS_OCTET_STRING))
                return false;
        }
    }

    /* when type is correct, update local copy */

    updateOrClone(&(self->rptId), values, 0);
    updateOrClone(&(self->rptEna), values, 1);

    if (self->isBuffered)
    {
        updateOrClone(&(self->datSet), values, 2);
        updateOrClone(&(self->confRev), values, 3);
        updateOrClone(&(self->optFlds), values, 4);
        updateOrClone(&(self->bufTm), values, 5);
        updateOrClone(&(self->sqNum), values, 6);
        updateOrClone(&(self->trgOps), values, 7);
        updateOrClone(&(self->intgPd), values, 8);
        updateOrClone(&(self->gi), values, 9);
        updateOrClone(&(self->purgeBuf), values, 10);
        updateOrClone(&(self->entryId), values, 11);
        updateOrClone(&(self->timeOfEntry), values, 12);

        if (rcbElementCount > 13)
        {
            MmsValue* element13 = MmsValue_getElement(values, 13);

            if (MmsValue_getType(element13) == MMS_OCTET_STRING)
                updateOrClone(&(self->owner), values, 13);
            else
            {
                updateOrClone(&(self->resvTms), values, 13);

                if (rcbElementCount > 14)
                    updateOrClone(&(self->owner), values, 14);
            }
        }
    }
    else
    {
        updateOrClone(&(self->resv), values, 2);
        updateOrClone(&(self->datSet), values, 3);
        updateOrClone(&(self->confRev), values, 4);
        updateOrClone(&(self->optFlds), values, 5);
        updateOrClone(&(self->bufTm), values, 6);
        updateOrClone(&(self->sqNum), values, 7);
        updateOrClone(&(self->trgOps), values, 8);
        updateOrClone(&(self->intgPd), values, 9);
        updateOrClone(&(self->gi), values, 10);

        if (rcbElementCount == 12) /* owner is optional */
            updateOrClone(&(self->owner), values, 11);
    }

    return true;
}

static void
readObjectHandlerInternal(uint32_t invokeId, void* parameter, MmsError err, MmsValue* value)
{
    IedConnection self = (IedConnection) parameter;

    IedConnectionOutstandingCall call = iedConnection_lookupOutstandingCall(self, invokeId);

    if (call)
    {
        IedConnection_GetRCBValuesHandler handler =  (IedConnection_GetRCBValuesHandler) call->callback;
        ClientReportControlBlock updateRcb = (ClientReportControlBlock) call->specificParameter;
        char* rcbReference = (char*) call->specificParameter2.pointer;

        if (err != MMS_ERROR_NONE)
        {
            handler(invokeId, call->callbackParameter, iedConnection_mapMmsErrorToIedError(err), NULL);
        }
        else
        {
            if (value == NULL)
            {
                handler(invokeId, call->callbackParameter, IED_ERROR_OBJECT_DOES_NOT_EXIST, NULL);
            }
            else
            {
                if (MmsValue_getType(value) == MMS_DATA_ACCESS_ERROR)
                {
                    if (DEBUG_IED_CLIENT)
                        printf("DEBUG_IED_CLIENT: getRCBValues returned data-access-error!\n");

                    handler(invokeId, call->callbackParameter, iedConnection_mapDataAccessErrorToIedError(MmsValue_getDataAccessError(value)), NULL);
                }
                else
                {
                    ClientReportControlBlock returnRcb = updateRcb;

                    if (returnRcb == NULL)
                        returnRcb = ClientReportControlBlock_create(rcbReference);

                    if (clientReportControlBlock_updateValues(returnRcb, value))
                    {
                        handler(invokeId, call->callbackParameter, IED_ERROR_OK, returnRcb);
                    }
                    else
                    {
                        if (DEBUG_IED_CLIENT)
                            printf("DEBUG_IED_CLIENT: getRCBValues returned wrong type!\n");

                        handler(invokeId, call->callbackParameter, IED_ERROR_TYPE_INCONSISTENT , NULL);

                        if (updateRcb == NULL)
                            ClientReportControlBlock_destroy(returnRcb);
                    }
                }

                MmsValue_delete(value);
            }
        }

        GLOBAL_FREEMEM(rcbReference);

        iedConnection_releaseOutstandingCall(self, call);
    }
    else
    {
        if (DEBUG_IED_CLIENT)
            printf("IED_CLIENT: internal error - no matching outstanding call!\n");
    }
}

uint32_t
IedConnection_getRCBValuesAsync(IedConnection self, IedClientError* error, const char* rcbReference, ClientReportControlBlock updateRcb,
        IedConnection_GetRCBValuesHandler handler, void* parameter)
{
    *error = IED_ERROR_OK;

    char domainId[65];
    char itemId[65];

    char* domainName = MmsMapping_getMmsDomainFromObjectReference(rcbReference, domainId);

    if (domainName == NULL)
    {
        *error = IED_ERROR_USER_PROVIDED_INVALID_ARGUMENT;
        return 0;
    }

    StringUtils_copyStringMax(itemId, 65, rcbReference + strlen(domainId) + 1);
    StringUtils_replace(itemId, '.', '$');

    IedConnectionOutstandingCall call = iedConnection_allocateOutstandingCall(self);

    if (call == NULL)
    {
        *error = IED_ERROR_OUTSTANDING_CALL_LIMIT_REACHED;
        return 0;
    }

    call->callback = handler;
    call->callbackParameter = parameter;
    call->specificParameter = updateRcb;
    call->specificParameter2.pointer = StringUtils_copyString(rcbReference);

    if (DEBUG_IED_CLIENT)
        printf("DEBUG_IED_CLIENT: readRCBValues for %s\n", rcbReference);

    MmsError err = MMS_ERROR_NONE;

    MmsConnection_readVariableAsync(self->connection, &(call->invokeId), &err, domainId, itemId, readObjectHandlerInternal, self);

    *error = iedConnection_mapMmsErrorToIedError(err);

    if (err != MMS_ERROR_NONE)
    {
        GLOBAL_FREEMEM(call->specificParameter2.pointer);
        iedConnection_releaseOutstandingCall(self, call);
        return 0;
    }

    return call->invokeId;
}

ClientReportControlBlock
IedConnection_getRCBValues(IedConnection self, IedClientError* error, const char* rcbReference,
                           ClientReportControlBlock updateRcb)
{
    MmsError mmsError = MMS_ERROR_NONE;

    ClientReportControlBlock returnRcb = updateRcb;

    char domainId[65];
    char itemId[65];

    char* domainName = MmsMapping_getMmsDomainFromObjectReference(rcbReference, domainId);

    if (domainName == NULL)
        return NULL;

    StringUtils_copyStringMax(itemId, 65, rcbReference + strlen(domainId) + 1);
    StringUtils_replace(itemId, '.', '$');

    if (DEBUG_IED_CLIENT)
        printf("DEBUG_IED_CLIENT: readRCBValues for %s\n", rcbReference);

    MmsValue* rcb = MmsConnection_readVariable(self->connection, &mmsError, domainId, itemId);

    if (mmsError != MMS_ERROR_NONE)
    {
        *error = iedConnection_mapMmsErrorToIedError(mmsError);

        return NULL;
    }

    if (rcb == NULL)
    {
        *error = IED_ERROR_OBJECT_DOES_NOT_EXIST;
        return NULL;
    }

    if (MmsValue_getType(rcb) == MMS_DATA_ACCESS_ERROR)
    {
        if (DEBUG_IED_CLIENT)
            printf("DEBUG_IED_CLIENT: getRCBValues returned data-access-error!\n");

        *error = iedConnection_mapDataAccessErrorToIedError(MmsValue_getDataAccessError(rcb));

        MmsValue_delete(rcb);

        return NULL;
    }

    if (MmsValue_getType(rcb) != MMS_STRUCTURE)
    {
        if (DEBUG_IED_CLIENT)
            printf("DEBUG_IED_CLIENT: getRCBValues returned wrong type!\n");

        MmsValue_delete(rcb);

        *error = IED_ERROR_UNKNOWN;
        return NULL;
    }

    if (returnRcb == NULL)
        returnRcb = ClientReportControlBlock_create(rcbReference);

    if (clientReportControlBlock_updateValues(returnRcb, rcb))
    {
        *error = IED_ERROR_OK;
    }
    else
    {
        if (DEBUG_IED_CLIENT)
            printf("DEBUG_IED_CLIENT: getRCBValues returned wrong type!\n");

        *error = IED_ERROR_TYPE_INCONSISTENT;

        if (updateRcb == NULL)
        {
            ClientReportControlBlock_destroy(returnRcb);
            returnRcb = NULL;
        }
    }

    MmsValue_delete(rcb);

    return returnRcb;
}

static void
writeMultipleVariablesHandler(uint32_t invokeId, void* parameter, MmsError mmsError,
                              LinkedList /* <MmsValue*> */ accessResults)
{
    IedConnection self = (IedConnection)parameter;

    IedConnectionOutstandingCall call = iedConnection_lookupOutstandingCall(self, invokeId);

    if (call)
    {
        IedConnection_GenericServiceHandler handler = (IedConnection_GenericServiceHandler)call->callback;

        if (accessResults)
        {

            IedClientError error = IED_ERROR_OK;

            LinkedList accessResult = LinkedList_getNext(accessResults);

            while (accessResult)
            {
                MmsValue* dataAccessError = (MmsValue*)accessResult->data;

                if (MmsValue_getDataAccessError(dataAccessError) != DATA_ACCESS_ERROR_SUCCESS)
                {
                    error = iedConnection_mapDataAccessErrorToIedError(MmsValue_getDataAccessError(dataAccessError));
                    break;
                }

                accessResult = LinkedList_getNext(accessResult);
            }

            LinkedList_destroyDeep(accessResults, (LinkedListValueDeleteFunction)MmsValue_delete);

            handler(invokeId, call->callbackParameter, error);
        }
        else
        {
            handler(invokeId, call->callbackParameter, iedConnection_mapMmsErrorToIedError(mmsError));
        }

        iedConnection_releaseOutstandingCall(self, call);
    }
    else
    {
        if (DEBUG_IED_CLIENT)
            printf("IED_CLIENT: internal error - no matching outstanding call with invoke ID: %u!\n", invokeId);
    }
}

struct sWriteRcbVariablesParameter
{
    LinkedList itemIds;
    LinkedList values;
    LinkedList currentItemId;
    LinkedList currentValue;
    char* domainId;
    uint32_t originalInvokeId;
};

static void
releaseWriteCall(IedConnection self, IedConnectionOutstandingCall call, struct sWriteRcbVariablesParameter* param)
{
    GLOBAL_FREEMEM(param->domainId);

    LinkedList_destroy(param->itemIds);
    LinkedList_destroyStatic(param->values);

    GLOBAL_FREEMEM(param);

    iedConnection_releaseOutstandingCall(self, call);
}

static void
writeVariableHandler(uint32_t invokeId, void* parameter, MmsError mmsError, MmsDataAccessError accessError)
{
    IedConnection self = (IedConnection)parameter;

    IedConnectionOutstandingCall call = iedConnection_lookupOutstandingCall(self, invokeId);

    if (call)
    {
        IedConnection_GenericServiceHandler handler = (IedConnection_GenericServiceHandler)call->callback;

        struct sWriteRcbVariablesParameter* param =
            (struct sWriteRcbVariablesParameter*)call->specificParameter2.pointer;

        if ((mmsError != MMS_ERROR_NONE) || (accessError != DATA_ACCESS_ERROR_SUCCESS))
        {
            IedClientError err;

            if (mmsError != MMS_ERROR_NONE)
                err = iedConnection_mapMmsErrorToIedError(mmsError);
            else
                err = iedConnection_mapDataAccessErrorToIedError(accessError);

            handler(param->originalInvokeId, call->callbackParameter, err);

            releaseWriteCall(self, call, param);

            goto exit_function;
        }

        param->currentItemId = LinkedList_getNext(param->currentItemId);

        if (param->currentItemId == NULL)
        {
            handler(param->originalInvokeId, call->callbackParameter, IED_ERROR_OK);

            releaseWriteCall(self, call, param);
        }
        else
        {
            param->currentValue = LinkedList_getNext(param->currentValue);

            char* itemId = (char*)LinkedList_getData(param->currentItemId);
            MmsValue* value = (MmsValue*)LinkedList_getData(param->currentValue);

            MmsError writeError;

            MmsConnection_writeVariableAsync(self->connection, &(call->invokeId), &writeError, param->domainId, itemId,
                                             value, writeVariableHandler, self);

            if (writeError != MMS_ERROR_NONE)
            {

                handler(param->originalInvokeId, call->callbackParameter,
                        iedConnection_mapMmsErrorToIedError(writeError));

                releaseWriteCall(self, call, param);
            }
        }
    }
    else
    {
        if (DEBUG_IED_CLIENT)
            printf("IED_CLIENT: internal error - no matching outstanding call!\n");
    }

exit_function:

    return;
}

uint32_t
IedConnection_setRCBValuesAsync(IedConnection self, IedClientError* error, ClientReportControlBlock rcb,
                                uint32_t parametersMask, bool singleRequest,
                                IedConnection_GenericServiceHandler handler, void* parameter)
{
    uint32_t invokeId = 0;

    bool isBuffered = ClientReportControlBlock_isBuffered(rcb);
    bool sendGILast = false; /* GI should be sent last when RptEna=TRUE is included */

    char domainId[65];
    char itemId[130];

    char* rcbReference = ClientReportControlBlock_getObjectReference(rcb);

    MmsMapping_getMmsDomainFromObjectReference(rcbReference, domainId);

    StringUtils_copyStringMax(itemId, 130, rcbReference + strlen(domainId) + 1);
    StringUtils_replace(itemId, '.', '$');

    int itemIdLen = strlen(itemId);

    if (DEBUG_IED_CLIENT)
        printf("DEBUG_IED_CLIENT: setRCBValues for %s\n", rcbReference);

    /* prepare data to send -> create the list of requested itemIds references */
    LinkedList itemIds = LinkedList_create();
    LinkedList values = LinkedList_create();

    /* add resv/resvTms as first element and rptEna as last element */
    if (parametersMask & RCB_ELEMENT_RESV)
    {
        if (isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$Resv");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->resv);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_RESV_TMS)
    {
        if (!isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$ResvTms");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->resvTms);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_RPT_ID)
    {
        StringUtils_appendString(itemId, 130, "$RptID");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->rptId);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_DATSET)
    {
        StringUtils_appendString(itemId, 130, "$DatSet");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->datSet);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_ENTRY_ID)
    {
        StringUtils_appendString(itemId, 130, "$EntryID");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->entryId);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_OPT_FLDS)
    {
        StringUtils_appendString(itemId, 130, "$OptFlds");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->optFlds);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_BUF_TM)
    {
        StringUtils_appendString(itemId, 130, "$BufTm");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->bufTm);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_TRG_OPS)
    {
        StringUtils_appendString(itemId, 130, "$TrgOps");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->trgOps);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_INTG_PD)
    {
        StringUtils_appendString(itemId, 130, "$IntgPd");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->intgPd);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_GI)
    {
        if (parametersMask & RCB_ELEMENT_RPT_ENA)
        {
            if (MmsValue_getBoolean(rcb->rptEna))
                sendGILast = true;
        }

        if (sendGILast == false)
        {
            StringUtils_appendString(itemId, 130, "$GI");

            LinkedList_add(itemIds, StringUtils_copyString(itemId));
            LinkedList_add(values, rcb->gi);

            itemId[itemIdLen] = 0;
        }
    }

    if (parametersMask & RCB_ELEMENT_PURGE_BUF)
    {
        if (!isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$PurgeBuf");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->purgeBuf);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_TIME_OF_ENTRY)
    {
        if (!isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$TimeofEntry");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->timeOfEntry);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_RPT_ENA)
    {
        StringUtils_appendString(itemId, 130, "$RptEna");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->rptEna);

        itemId[itemIdLen] = 0;
    }

    if (sendGILast)
    {
        StringUtils_appendString(itemId, 130, "$GI");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->gi);

        itemId[itemIdLen] = 0;
    }

    IedConnectionOutstandingCall call = iedConnection_allocateOutstandingCall(self);

    if (call == NULL)
    {
        *error = IED_ERROR_OUTSTANDING_CALL_LIMIT_REACHED;
        goto exit_function;
    }

    call->callback = handler;
    call->callbackParameter = parameter;
    call->specificParameter = rcb;

    MmsError err;

    if (singleRequest)
    {
        MmsConnection_writeMultipleVariablesAsync(self->connection, &(call->invokeId), &err, domainId, itemIds, values,
                                                  writeMultipleVariablesHandler, self);

        *error = iedConnection_mapMmsErrorToIedError(err);

        if (err != MMS_ERROR_NONE)
        {
            iedConnection_releaseOutstandingCall(self, call);
        }
        else
        {
            invokeId = call->invokeId;
        }

        goto exit_function;
    }
    else
    {
        struct sWriteRcbVariablesParameter* param =
            (struct sWriteRcbVariablesParameter*)GLOBAL_MALLOC(sizeof(struct sWriteRcbVariablesParameter));

        if (param == NULL)
        {
            *error = IED_ERROR_UNKNOWN;
            goto exit_function;
        }

        call->specificParameter2.pointer = param;

        param->itemIds = itemIds;
        param->values = values;

        param->currentItemId = LinkedList_getNext(itemIds);
        param->currentValue = LinkedList_getNext(values);
        param->domainId = StringUtils_copyString(domainId);

        char* variableId = (char*)LinkedList_getData(param->currentItemId);
        MmsValue* value = (MmsValue*)LinkedList_getData(param->currentValue);

        MmsConnection_writeVariableAsync(self->connection, &(call->invokeId), &err, domainId, variableId, value,
                                         writeVariableHandler, self);

        param->originalInvokeId = call->invokeId;

        invokeId = call->invokeId;

        *error = iedConnection_mapMmsErrorToIedError(err);

        if (err != MMS_ERROR_NONE)
        {
            iedConnection_releaseOutstandingCall(self, call);
            GLOBAL_FREEMEM(param->domainId);
            GLOBAL_FREEMEM(param);
            goto exit_function;
        }
        else
            return invokeId;
    }

error_invalid_parameter:
    *error = IED_ERROR_USER_PROVIDED_INVALID_ARGUMENT;

exit_function:
    LinkedList_destroy(itemIds);
    LinkedList_destroyStatic(values);

    return invokeId;
}

void
IedConnection_setRCBValues(IedConnection self, IedClientError* error, ClientReportControlBlock rcb,
                           uint32_t parametersMask, bool singleRequest)
{
    *error = IED_ERROR_OK;

    MmsError mmsError = MMS_ERROR_NONE;

    bool isBuffered = ClientReportControlBlock_isBuffered(rcb);
    bool sendGILast = false; /* GI should be sent last when RptEna=TRUE is included */

    char domainId[65];
    char itemId[130];

    char* rcbReference = ClientReportControlBlock_getObjectReference(rcb);

    MmsMapping_getMmsDomainFromObjectReference(rcbReference, domainId);

    StringUtils_copyStringMax(itemId, 130, rcbReference + strlen(domainId) + 1);
    StringUtils_replace(itemId, '.', '$');

    int itemIdLen = strlen(itemId);

    if (DEBUG_IED_CLIENT)
        printf("DEBUG_IED_CLIENT: setRCBValues for %s\n", rcbReference);

    /* create the list of requested itemIds references */
    LinkedList itemIds = LinkedList_create();
    LinkedList values = LinkedList_create();

    /* add rptEna = false as first element */
    if (ClientReportControlBlock_getRptEna(rcb) == false)
    {
        if (parametersMask & RCB_ELEMENT_RPT_ENA)
        {
            StringUtils_appendString(itemId, 130, "$RptEna");

            LinkedList_add(itemIds, StringUtils_copyString(itemId));
            LinkedList_add(values, rcb->rptEna);

            itemId[itemIdLen] = 0;
        }
    }

    /* add resv/resvTms as first element and rptEna as last element when enabling a report */
    if (parametersMask & RCB_ELEMENT_RESV)
    {
        if (isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$Resv");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->resv);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_RESV_TMS)
    {
        if (!isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$ResvTms");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->resvTms);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_RPT_ID)
    {
        StringUtils_appendString(itemId, 130, "$RptID");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->rptId);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_DATSET)
    {
        StringUtils_appendString(itemId, 130, "$DatSet");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->datSet);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_ENTRY_ID)
    {
        StringUtils_appendString(itemId, 130, "$EntryID");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->entryId);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_OPT_FLDS)
    {
        StringUtils_appendString(itemId, 130, "$OptFlds");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->optFlds);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_BUF_TM)
    {
        StringUtils_appendString(itemId, 130, "$BufTm");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->bufTm);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_TRG_OPS)
    {
        StringUtils_appendString(itemId, 130, "$TrgOps");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->trgOps);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_INTG_PD)
    {
        StringUtils_appendString(itemId, 130, "$IntgPd");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->intgPd);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_GI)
    {
        if (parametersMask & RCB_ELEMENT_RPT_ENA)
        {
            if (MmsValue_getBoolean(rcb->rptEna))
                sendGILast = true;
        }

        if (sendGILast == false)
        {
            StringUtils_appendString(itemId, 130, "$GI");

            LinkedList_add(itemIds, StringUtils_copyString(itemId));
            LinkedList_add(values, rcb->gi);

            itemId[itemIdLen] = 0;
        }
    }

    if (parametersMask & RCB_ELEMENT_PURGE_BUF)
    {
        if (!isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$PurgeBuf");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->purgeBuf);

        itemId[itemIdLen] = 0;
    }

    if (parametersMask & RCB_ELEMENT_TIME_OF_ENTRY)
    {
        if (!isBuffered)
            goto error_invalid_parameter;

        StringUtils_appendString(itemId, 130, "$TimeofEntry");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->timeOfEntry);

        itemId[itemIdLen] = 0;
    }

    if (ClientReportControlBlock_getRptEna(rcb) == true)
    {
        if (parametersMask & RCB_ELEMENT_RPT_ENA)
        {
            StringUtils_appendString(itemId, 130, "$RptEna");

            LinkedList_add(itemIds, StringUtils_copyString(itemId));
            LinkedList_add(values, rcb->rptEna);

            itemId[itemIdLen] = 0;
        }
    }

    if (sendGILast)
    {
        StringUtils_appendString(itemId, 130, "$GI");

        LinkedList_add(itemIds, StringUtils_copyString(itemId));
        LinkedList_add(values, rcb->gi);

        itemId[itemIdLen] = 0;
    }

    if (singleRequest)
    {
        LinkedList accessResults = NULL;

        MmsConnection_writeMultipleVariables(self->connection, &mmsError, domainId, itemIds, values, &accessResults);

        if (accessResults)
        {

            LinkedList accessResult = LinkedList_getNext(accessResults);

            while (accessResult)
            {
                MmsValue* dataAccessError = (MmsValue*)accessResult->data;

                if (MmsValue_getDataAccessError(dataAccessError) != DATA_ACCESS_ERROR_SUCCESS)
                {
                    *error = iedConnection_mapDataAccessErrorToIedError(MmsValue_getDataAccessError(dataAccessError));
                    break;
                }

                accessResult = LinkedList_getNext(accessResult);
            }

            LinkedList_destroyDeep(accessResults, (LinkedListValueDeleteFunction)MmsValue_delete);
        }
        else
            *error = iedConnection_mapMmsErrorToIedError(mmsError);

        goto exit_function;
    }
    else
    {
        LinkedList itemIdElement = LinkedList_getNext(itemIds);
        LinkedList valueElement = LinkedList_getNext(values);

        while (itemIdElement)
        {
            char* rcbItemId = (char*)itemIdElement->data;
            MmsValue* value = (MmsValue*)valueElement->data;

            MmsConnection_writeVariable(self->connection, &mmsError, domainId, rcbItemId, value);

            if (mmsError != MMS_ERROR_NONE)
                break;

            itemIdElement = LinkedList_getNext(itemIdElement);
            valueElement = LinkedList_getNext(valueElement);
        }

        *error = iedConnection_mapMmsErrorToIedError(mmsError);
        goto exit_function;
    }

error_invalid_parameter:
    *error = IED_ERROR_USER_PROVIDED_INVALID_ARGUMENT;

exit_function:
    LinkedList_destroy(itemIds);
    LinkedList_destroyStatic(values);
}
