/*
 * Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package org.openmuc.openiec61850;

import java.io.EOFException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeoutException;

import org.openmuc.jasn1.ber.BerByteArrayOutputStream;
import org.openmuc.jasn1.ber.types.BerBoolean;
import org.openmuc.jasn1.ber.types.BerInteger;
import org.openmuc.jasn1.ber.types.BerNull;
import org.openmuc.jasn1.ber.types.string.BerVisibleString;
import org.openmuc.josistack.AcseAssociation;
import org.openmuc.josistack.ByteBufferInputStream;
import org.openmuc.josistack.DecodingException;
import org.openmuc.openiec61850.internal.mms.asn1.AccessResult;
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedErrorPDU;
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedRequestPDU;
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedResponsePDU;
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedServiceRequest;
import org.openmuc.openiec61850.internal.mms.asn1.ConfirmedServiceResponse;
import org.openmuc.openiec61850.internal.mms.asn1.Data;
import org.openmuc.openiec61850.internal.mms.asn1.DataAccessError;
import org.openmuc.openiec61850.internal.mms.asn1.DefineNamedVariableListRequest;
import org.openmuc.openiec61850.internal.mms.asn1.DefineNamedVariableListResponse;
import org.openmuc.openiec61850.internal.mms.asn1.DeleteNamedVariableListRequest;
import org.openmuc.openiec61850.internal.mms.asn1.DeleteNamedVariableListResponse;
import org.openmuc.openiec61850.internal.mms.asn1.GetNameListRequest;
import org.openmuc.openiec61850.internal.mms.asn1.GetNameListResponse;
import org.openmuc.openiec61850.internal.mms.asn1.GetNameListResponse.ListOfIdentifier;
import org.openmuc.openiec61850.internal.mms.asn1.GetNamedVariableListAttributesResponse;
import org.openmuc.openiec61850.internal.mms.asn1.GetVariableAccessAttributesRequest;
import org.openmuc.openiec61850.internal.mms.asn1.GetVariableAccessAttributesResponse;
import org.openmuc.openiec61850.internal.mms.asn1.Identifier;
import org.openmuc.openiec61850.internal.mms.asn1.InitiateRequestPDU;
import org.openmuc.openiec61850.internal.mms.asn1.InitiateResponsePDU;
import org.openmuc.openiec61850.internal.mms.asn1.Integer16;
import org.openmuc.openiec61850.internal.mms.asn1.Integer32;
import org.openmuc.openiec61850.internal.mms.asn1.Integer8;
import org.openmuc.openiec61850.internal.mms.asn1.MMSpdu;
import org.openmuc.openiec61850.internal.mms.asn1.ObjectName;
import org.openmuc.openiec61850.internal.mms.asn1.ObjectName.DomainSpecific;
import org.openmuc.openiec61850.internal.mms.asn1.ParameterSupportOptions;
import org.openmuc.openiec61850.internal.mms.asn1.ReadRequest;
import org.openmuc.openiec61850.internal.mms.asn1.ReadResponse;
import org.openmuc.openiec61850.internal.mms.asn1.ReadResponse.ListOfAccessResult;
import org.openmuc.openiec61850.internal.mms.asn1.ServiceError.ErrorClass;
import org.openmuc.openiec61850.internal.mms.asn1.ServiceSupportOptions;
import org.openmuc.openiec61850.internal.mms.asn1.TypeDescription;
import org.openmuc.openiec61850.internal.mms.asn1.TypeDescription.Structure;
import org.openmuc.openiec61850.internal.mms.asn1.TypeDescription.Structure.Components;
import org.openmuc.openiec61850.internal.mms.asn1.TypeSpecification;
import org.openmuc.openiec61850.internal.mms.asn1.Unsigned32;
import org.openmuc.openiec61850.internal.mms.asn1.VariableAccessSpecification;
import org.openmuc.openiec61850.internal.mms.asn1.VariableDefs;
import org.openmuc.openiec61850.internal.mms.asn1.WriteRequest;
import org.openmuc.openiec61850.internal.mms.asn1.WriteResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class ServerAssociation {

    private static final Logger logger = LoggerFactory.getLogger(ServerAssociation.class);

    private static final WriteResponse.CHOICE writeSuccess = new WriteResponse.CHOICE();
    static {
        writeSuccess.setSuccess(new BerNull());
    }

    private AcseAssociation acseAssociation = null;

    private int negotiatedMaxPduSize;
    private ByteBuffer pduBuffer;
    private final ServerSap serverSap;
    final ServerModel serverModel;

    private final BerByteArrayOutputStream berOStream = new BerByteArrayOutputStream(500, true);
    private boolean insertRef;
    private String continueAfter;

    ScheduledExecutorService executor = null;

    HashMap<String, DataSet> nonPersistentDataSets = new HashMap<>();

    List<FcModelNode> selects = new ArrayList<>();
    List<Urcb> rsvdURCBs = new ArrayList<>();

    private static String[] mmsFcs = { "MX", "ST", "CO", "CF", "DC", "SP", "SG", "RP", "LG", "BR", "GO", "GS", "SV",
            "SE", "EX", "SR", "OR", "BL" };

    public ServerAssociation(ServerSap serverSap) {
        this.serverSap = serverSap;
        serverModel = serverSap.serverModel;
        executor = Executors.newScheduledThreadPool(2);
    }

    public void handleNewAssociation(AcseAssociation acseAssociation, ByteBuffer associationRequest) {

        this.acseAssociation = acseAssociation;

        try {
            associate(acseAssociation, associationRequest);
        } catch (IOException e) {
            logger.warn("Error during association build up", e);
            return;
        }

        handleConnection();

    }

    private void associate(AcseAssociation acseAssociation, ByteBuffer associationRequest) throws IOException {

        MMSpdu mmsPdu = new MMSpdu();

        mmsPdu.decode(new ByteBufferInputStream(associationRequest), null);

        MMSpdu initiateResponseMmsPdu = constructAssociationResponsePdu(mmsPdu.getInitiateRequestPDU());

        initiateResponseMmsPdu.encode(berOStream, true);

        acseAssociation.accept(berOStream.getByteBuffer());

    }

    private MMSpdu constructAssociationResponsePdu(InitiateRequestPDU associationRequestMMSpdu) {

        negotiatedMaxPduSize = serverSap.getMaxMmsPduSize();

        if (associationRequestMMSpdu.getLocalDetailCalling() != null) {
            int proposedMaxMmsPduSize = (int) associationRequestMMSpdu.getLocalDetailCalling().value;
            if (negotiatedMaxPduSize > proposedMaxMmsPduSize
                    && proposedMaxMmsPduSize >= ServerSap.MINIMUM_MMS_PDU_SIZE) {
                negotiatedMaxPduSize = proposedMaxMmsPduSize;
            }
        }

        int negotiatedMaxServOutstandingCalling = serverSap.getProposedMaxServOutstandingCalling();
        int proposedMaxServOutstandingCalling = (int) associationRequestMMSpdu
                .getProposedMaxServOutstandingCalling().value;

        if (negotiatedMaxServOutstandingCalling > proposedMaxServOutstandingCalling
                && proposedMaxServOutstandingCalling > 0) {
            negotiatedMaxServOutstandingCalling = proposedMaxServOutstandingCalling;
        }

        int negotiatedMaxServOutstandingCalled = serverSap.getProposedMaxServOutstandingCalled();
        int proposedMaxServOutstandingCalled = (int) associationRequestMMSpdu
                .getProposedMaxServOutstandingCalled().value;

        if (negotiatedMaxServOutstandingCalled > proposedMaxServOutstandingCalled
                && proposedMaxServOutstandingCalled > 0) {
            negotiatedMaxServOutstandingCalled = proposedMaxServOutstandingCalled;
        }

        int negotiatedDataStructureNestingLevel = serverSap.getProposedDataStructureNestingLevel();

        if (associationRequestMMSpdu.getProposedDataStructureNestingLevel() != null) {
            int proposedDataStructureNestingLevel = (int) associationRequestMMSpdu
                    .getProposedDataStructureNestingLevel().value;
            if (negotiatedDataStructureNestingLevel > proposedDataStructureNestingLevel) {
                negotiatedDataStructureNestingLevel = proposedDataStructureNestingLevel;
            }
        }

        pduBuffer = ByteBuffer.allocate(negotiatedMaxPduSize + 500);

        byte[] negotiatedParameterCbbBitString = serverSap.cbbBitString;

        byte[] servicesSupportedCalledBitString = serverSap.servicesSupportedCalled;

        InitiateResponsePDU.InitResponseDetail initRespDetail = new InitiateResponsePDU.InitResponseDetail();
        initRespDetail.setNegotiatedVersionNumber(new Integer16(1));
        initRespDetail.setNegotiatedParameterCBB(new ParameterSupportOptions(negotiatedParameterCbbBitString,
                negotiatedParameterCbbBitString.length * 8 - 5));
        initRespDetail.setServicesSupportedCalled(new ServiceSupportOptions(servicesSupportedCalledBitString,
                servicesSupportedCalledBitString.length * 8 - 3));

        InitiateResponsePDU initRespPdu = new InitiateResponsePDU();
        initRespPdu.setLocalDetailCalled(new Integer32(negotiatedMaxPduSize));
        initRespPdu.setNegotiatedMaxServOutstandingCalling(new Integer16(negotiatedMaxServOutstandingCalling));
        initRespPdu.setNegotiatedMaxServOutstandingCalled(new Integer16(negotiatedMaxServOutstandingCalled));
        initRespPdu.setNegotiatedDataStructureNestingLevel(new Integer8(negotiatedDataStructureNestingLevel));
        initRespPdu.setInitResponseDetail(initRespDetail);

        MMSpdu initiateResponseMMSpdu = new MMSpdu();
        initiateResponseMMSpdu.setInitiateResponsePDU(initRespPdu);

        return initiateResponseMMSpdu;
    }

    private void handleConnection() {

        while (true) {

            MMSpdu mmsRequestPdu = listenForMmsRequest(acseAssociation);
            if (mmsRequestPdu == null) {
                return;
            }

            ConfirmedRequestPDU confirmedRequestPdu = mmsRequestPdu.getConfirmedRequestPDU();
            // Do not have to check whether confirmedRequestPdu is null because that was already done by
            // listenForMmsRequest()

            if (confirmedRequestPdu.getInvokeID() == null) {
                // cannot respond with ServiceError because no InvokeID was received
                logger.warn("Got unexpected MMS PDU or no invokeID");
                continue;
            }
            int invokeId = (int) confirmedRequestPdu.getInvokeID().value;

            try {
                if (confirmedRequestPdu.getService() == null) {
                    throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "Got an invalid MMS packet: confirmedServiceRequest empty");
                }

                ConfirmedServiceRequest confirmedServiceRequest = confirmedRequestPdu.getService();

                ConfirmedServiceResponse confirmedServiceResponse = new ConfirmedServiceResponse();

                if (confirmedServiceRequest.getGetNameList() != null) {

                    GetNameListRequest getNameListRequest = confirmedServiceRequest.getGetNameList();
                    GetNameListResponse response = null;

                    if (getNameListRequest.getObjectClass().getBasicObjectClass() == null) {
                        throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                                "Got an invalid MMS packet: ObjectClass was not selected in GetNameList request");
                    }

                    long basicObjectClass = getNameListRequest.getObjectClass().getBasicObjectClass().value;
                    if (basicObjectClass == 9) {
                        logger.debug("Got a GetServerDirectory (MMS GetNameList[DOMAIN]) request");
                        response = handleGetServerDirectoryRequest(getNameListRequest);
                    }
                    else if (basicObjectClass == 0) {
                        logger.debug("Got a Get{LD|LN}Directory (MMS GetNameList[NAMED_VARIABLE]) request");
                        response = handleGetDirectoryRequest(getNameListRequest);
                    }
                    else if (basicObjectClass == 2) {
                        logger.debug(
                                "Got a GetLogicalNodeDirectory[DataSet] (MMS GetNameList[NAMED_VARIABLE_LIST]) request");
                        response = handleGetDataSetNamesRequest(getNameListRequest);
                    }
                    else {
                        throw new ServiceError(ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT,
                                "Unable to handle Get directory request for basic object class: " + basicObjectClass);
                    }
                    // else if (basicObjectClass == 8) {
                    // logger.debug("Got a GetLogicalNodeDirectory[Log] (MMS GetNameList[JOURNAL]) request");
                    // response =
                    // handleGetNameListJournalRequest(getNameListRequest);
                    // }

                    confirmedServiceResponse.setGetNameList(response);

                }
                else if (confirmedServiceRequest.getGetVariableAccessAttributes() != null) {
                    logger.debug("Got a GetDataDirectory/GetDataDefinition (MMS GetVariableAccessAttributes) request");
                    GetVariableAccessAttributesResponse response = handleGetVariableAccessAttributesRequest(

                            confirmedServiceRequest.getGetVariableAccessAttributes());

                    confirmedServiceResponse.setGetVariableAccessAttributes(response);

                }
                else if (confirmedServiceRequest.getRead() != null) {
                    // GetDataValues, GetDataSetValues, GetBRCBValues and GetURCBValues map to this
                    ReadResponse response = handleGetDataValuesRequest(confirmedServiceRequest.getRead());

                    confirmedServiceResponse.setRead(response);
                }
                else if (confirmedServiceRequest.getWrite() != null) {
                    logger.debug("Got a Write request");

                    WriteResponse response = handleSetDataValuesRequest(confirmedServiceRequest.getWrite());

                    confirmedServiceResponse.setWrite(response);

                }
                // for Data Sets
                else if (confirmedServiceRequest.getDefineNamedVariableList() != null) {
                    logger.debug("Got a CreateDataSet request");

                    DefineNamedVariableListResponse response = handleCreateDataSetRequest(
                            confirmedServiceRequest.getDefineNamedVariableList());

                    confirmedServiceResponse.setDefineNamedVariableList(response);
                }

                else if (confirmedServiceRequest.getGetNamedVariableListAttributes() != null) {
                    logger.debug("Got a GetDataSetDirectory request");
                    GetNamedVariableListAttributesResponse response = handleGetDataSetDirectoryRequest(
                            confirmedServiceRequest.getGetNamedVariableListAttributes());

                    confirmedServiceResponse.setGetNamedVariableListAttributes(response);

                }

                else if (confirmedServiceRequest.getDeleteNamedVariableList() != null) {
                    logger.debug("Got a DeleteDataSet request");
                    DeleteNamedVariableListResponse response = handleDeleteDataSetRequest(
                            confirmedServiceRequest.getDeleteNamedVariableList());

                    confirmedServiceResponse.setDeleteNamedVariableList(response);
                }

                else {
                    throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "invalid MMS packet: unknown request type.");
                }

                ConfirmedResponsePDU confirmedResponsePdu = new ConfirmedResponsePDU();
                confirmedResponsePdu.setInvokeID(confirmedRequestPdu.getInvokeID());
                confirmedResponsePdu.setService(confirmedServiceResponse);

                MMSpdu mmsResponsePdu = new MMSpdu();
                mmsResponsePdu.setConfirmedResponsePDU(confirmedResponsePdu);

                if (!sendAnMmsPdu(mmsResponsePdu)) {
                    return;
                }
            } catch (ServiceError e) {
                logger.warn(e.getMessage());
                if (!sendAnMmsPdu(createServiceErrorResponse(e, invokeId))) {
                    return;
                }
            }
        }
    }

    void cleanUpConnection() {
        synchronized (serverModel) {
            for (FcModelNode selectedCdo : selects) {
                selectedCdo.deselect();
            }
            for (Urcb rsvdUrcb : rsvdURCBs) {
                synchronized (rsvdUrcb) {
                    if (rsvdUrcb.enabled) {
                        rsvdUrcb.disable();
                    }
                    rsvdUrcb.reserved = null;
                    rsvdUrcb.getResv().setValue(false);
                }
            }
        }
    }

    boolean sendAnMmsPdu(MMSpdu mmsResponsePdu) {

        synchronized (berOStream) {
            berOStream.reset();
            try {
                mmsResponsePdu.encode(berOStream, false);
            } catch (IOException e1) {
                logger.error("IOException while encoding MMS PDU. Closing association.", e1);
                return false;
            }
            try {
                acseAssociation.send(berOStream.getByteBuffer());
            } catch (IOException e) {
                logger.warn("IOException while sending MMS PDU. Closing association.", e);
                return false;
            }
        }
        return true;
    }

    private MMSpdu listenForMmsRequest(AcseAssociation acseAssociation) {

        while (true) {
            MMSpdu mmsRequestPdu = null;
            pduBuffer.clear();
            try {
                acseAssociation.receive(pduBuffer);
            } catch (EOFException e) {
                logger.debug("Connection was closed by client.");
                return null;
            } catch (SocketTimeoutException e) {
                logger.warn("Message fragment timeout occured while receiving request. Closing association.", e);
                return null;
            } catch (IOException e) {
                logger.warn("IOException at lower layers while listening for incoming request. Closing association.",
                        e);
                return null;
            } catch (DecodingException e) {
                logger.error("Error decoding request at OSI layers.", e);
                continue;
            } catch (TimeoutException e) {
                logger.error(
                        "Illegal state: message timeout while receiving request though this timeout should 0 and never be thrown",
                        e);
                return null;
            }
            mmsRequestPdu = new MMSpdu();

            try {
                mmsRequestPdu.decode(new ByteBufferInputStream(pduBuffer), null);
            } catch (IOException e) {
                logger.warn("IOException decoding received MMS request PDU.", e);
                continue;
            }

            if (mmsRequestPdu.getConfirmedRequestPDU() == null) {
                if (mmsRequestPdu.getConcludeRequestPDU() != null) {
                    logger.debug("Got Conclude request, will close connection");
                    return null;
                }
                else {
                    logger.warn("Got unexpected MMS PDU, will ignore it");
                    continue;
                }
            }

            return mmsRequestPdu;
        }
    }

    private MMSpdu createServiceErrorResponse(ServiceError e, int invokeId) {

        ErrorClass errClass = new ErrorClass();

        switch (e.getErrorCode()) {

        case ServiceError.NO_ERROR:

            break;
        case ServiceError.INSTANCE_NOT_AVAILABLE:
            errClass.setAccess(new BerInteger(e.getErrorCode()));
            break;
        case ServiceError.INSTANCE_IN_USE:
            errClass.setDefinition(new BerInteger(e.getErrorCode()));
            break;
        case ServiceError.ACCESS_VIOLATION:
            errClass.setAccess(new BerInteger(e.getErrorCode()));
            break;
        case ServiceError.ACCESS_NOT_ALLOWED_IN_CURRENT_STATE:
            errClass.setOthers(new BerInteger(e.getErrorCode()));
            break;
        case ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT:
            errClass.setFile(new BerInteger(2));
            break;
        case ServiceError.TYPE_CONFLICT:
            errClass.setFile(new BerInteger(4));
            break;
        default:
            errClass.setOthers(new BerInteger(e.getErrorCode()));
        }
        org.openmuc.openiec61850.internal.mms.asn1.ServiceError asn1ServiceError = null;

        asn1ServiceError = new org.openmuc.openiec61850.internal.mms.asn1.ServiceError();
        asn1ServiceError.setErrorClass(errClass);
        asn1ServiceError.setAdditionalDescription(new BerVisibleString(e.getMessage()));

        ConfirmedErrorPDU confirmedErrorPDU = new ConfirmedErrorPDU();
        confirmedErrorPDU.setInvokeID(new Unsigned32(invokeId));
        confirmedErrorPDU.setServiceError(asn1ServiceError);

        MMSpdu mmsPdu = new MMSpdu();
        mmsPdu.setConfirmedErrorPDU(confirmedErrorPDU);

        return mmsPdu;
    }

    private GetNameListResponse handleGetServerDirectoryRequest(GetNameListRequest getNameListRequest)
            throws ServiceError {

        ListOfIdentifier listOfIdentifier = new ListOfIdentifier();
        List<Identifier> identifiers = listOfIdentifier.getIdentifier();

        for (ModelNode ld : serverModel) {
            identifiers.add(new Identifier(ld.getName().getBytes()));
        }

        GetNameListResponse getNameListResponse = new GetNameListResponse();
        getNameListResponse.setListOfIdentifier(listOfIdentifier);
        getNameListResponse.setMoreFollows(new BerBoolean(false));

        return getNameListResponse;
    }

    private GetNameListResponse handleGetDirectoryRequest(GetNameListRequest getNameListRequest) throws ServiceError {

        // the ObjectScope can be vmdSpecific,domainSpecific, or aaSpecific. vmdSpecific and aaSpecific are not part of
        // 61850-8-1 but are used by some IEC 61850 clients anyways. This stack will return an empty list on vmdSpecific
        // and aaSpecific requests.
        if (getNameListRequest.getObjectScope().getAaSpecific() != null
                || getNameListRequest.getObjectScope().getVmdSpecific() != null) {
            ListOfIdentifier listOfIden = new ListOfIdentifier();
            listOfIden.getIdentifier();
            GetNameListResponse getNameListResponse = new GetNameListResponse();
            getNameListResponse.setListOfIdentifier(listOfIden);
            getNameListResponse.setMoreFollows(new BerBoolean(false));
            return getNameListResponse;
        }

        String mmsDomainId = getNameListRequest.getObjectScope().getDomainSpecific().toString();

        ModelNode logicalDeviceMn = serverModel.getChild(mmsDomainId);

        if (logicalDeviceMn == null) {
            throw new ServiceError(ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
                    "Got an invalid MMS request: given Domain name in GetNameList request is not a Logical Device name");
        }

        LogicalDevice logicalDevice = (LogicalDevice) logicalDeviceMn;

        insertRef = true;

        if (getNameListRequest.getContinueAfter() != null) {
            continueAfter = getNameListRequest.getContinueAfter().toString();
            insertRef = false;
        }

        List<String> mmsReferences = new LinkedList<>();

        for (ModelNode logicalNodeMn : logicalDevice) {
            LogicalNode logicalNode = (LogicalNode) logicalNodeMn;
            mmsReferences.add(logicalNode.getName());

            for (String mmsFC : mmsFcs) {
                Fc fc = Fc.fromString(mmsFC);
                if (fc != null) {

                    List<FcDataObject> fcDataObjects = logicalNode.getChildren(fc);
                    if (fcDataObjects != null) {
                        mmsReferences.add(logicalNode.getName() + "$" + mmsFC);
                        for (FcDataObject dataObject : fcDataObjects) {
                            insertMmsRef(dataObject, mmsReferences, logicalNode.getName() + "$" + mmsFC);
                        }
                    }

                }
            }
        }

        ListOfIdentifier listOfIden = new ListOfIdentifier();
        List<Identifier> identifiers = listOfIden.getIdentifier();

        int identifierSize = 0;
        boolean moreFollows = false;
        for (String mmsReference : mmsReferences) {
            if (insertRef == true) {
                if (identifierSize > negotiatedMaxPduSize - 200) {
                    moreFollows = true;
                    logger.debug(" ->maxMMSPduSize of " + negotiatedMaxPduSize + " Bytes reached");
                    break;
                }

                Identifier identifier = null;

                identifier = new Identifier(mmsReference.getBytes());

                identifiers.add(identifier);
                identifierSize += mmsReference.length() + 2;
            }
            else {
                if (mmsReference.equals(continueAfter)) {
                    insertRef = true;
                }
            }
        }

        GetNameListResponse getNameListResponse = new GetNameListResponse();
        getNameListResponse.setListOfIdentifier(listOfIden);
        getNameListResponse.setMoreFollows(new BerBoolean(moreFollows));

        return getNameListResponse;
    }

    private static void insertMmsRef(ModelNode node, List<String> mmsRefs, String parentRef) {
        String ref = parentRef + '$' + node.getName();
        mmsRefs.add(ref);
        if (!(node instanceof Array)) {
            for (ModelNode childNode : node) {
                insertMmsRef(childNode, mmsRefs, ref);
            }
        }
    }

    /**
     * GetVariableAccessAttributes (GetDataDefinition/GetDataDirectory) can be called with different kinds of
     * references. Examples: 1. DGEN1 2. DGEN1$CF 3. DGEN1$CF$GnBlk
     *
     */
    private GetVariableAccessAttributesResponse handleGetVariableAccessAttributesRequest(
            GetVariableAccessAttributesRequest getVariableAccessAttributesRequest) throws ServiceError {
        if (getVariableAccessAttributesRequest.getName() == null) {
            throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Got an invalid MMS packet: name is not selected in GetVariableAccessAttributesRequest");
        }

        DomainSpecific domainSpecific = getVariableAccessAttributesRequest.getName().getDomainSpecific();

        if (domainSpecific == null) {
            throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Got an invalid MMS packet: Domain specific is not selected in GetVariableAccessAttributesRequest");
        }

        ModelNode modelNode = serverModel.getChild(domainSpecific.getDomainID().toString());

        if (modelNode == null) {
            throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                    "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                            + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                            + " and ItemID "
                            + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                            + " was found.");
        }

        String itemIdString = domainSpecific.getItemID().toString();

        int index1 = itemIdString.indexOf('$');

        LogicalNode logicalNode = null;

        if (index1 != -1) {
            logicalNode = (LogicalNode) modelNode.getChild(itemIdString.substring(0, index1));
            if (logicalNode == null) {
                throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                        "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                + " and ItemID "
                                + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                + " was found.");
            }
            int index2 = itemIdString.indexOf('$', index1 + 2);
            if (index2 != -1) {
                Fc fc = Fc.fromString(itemIdString.substring(index1 + 1, index2));
                if (fc == null) {
                    throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                            "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                    + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                    + " and ItemID "
                                    + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                    + " was found.");
                }
                index1 = itemIdString.indexOf('$', index2 + 2);
                ModelNode subNode;
                if (index1 == -1) {
                    subNode = logicalNode.getChild(itemIdString.substring(index2 + 1), fc);
                    if (subNode == null) {
                        throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                                "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                        + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                        + " and ItemID "
                                        + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                        + " was found.");
                    }
                }
                else {
                    subNode = logicalNode.getChild(itemIdString.substring(index2 + 1, index1), fc);
                    if (subNode == null) {
                        throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                                "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                        + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                        + " and ItemID "
                                        + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                        + " was found.");
                    }
                    index2 = itemIdString.indexOf('$', index1 + 2);
                    while (index2 != -1) {
                        subNode = subNode.getChild(itemIdString.substring(index1 + 1, index2));
                        if (subNode == null) {
                            throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                                    "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                            + getVariableAccessAttributesRequest.getName()
                                                    .getDomainSpecific()
                                                    .getDomainID()
                                            + " and ItemID " + getVariableAccessAttributesRequest.getName()
                                                    .getDomainSpecific()
                                                    .getItemID()
                                            + " was found.");
                        }
                        index1 = index2;
                        index2 = itemIdString.indexOf('$', index1 + 2);
                    }
                    subNode = subNode.getChild(itemIdString.substring(index1 + 1));
                    if (subNode == null) {
                        throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                                "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                        + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                        + " and ItemID "
                                        + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                        + " was found.");
                    }
                }

                GetVariableAccessAttributesResponse getVariableAccessAttributesResponse = new GetVariableAccessAttributesResponse();
                getVariableAccessAttributesResponse.setMmsDeletable(new BerBoolean(false));
                getVariableAccessAttributesResponse.setTypeDescription(subNode.getMmsTypeSpec());

                return getVariableAccessAttributesResponse;
            }
            else {
                Fc fc = Fc.fromString(itemIdString.substring(index1 + 1));

                if (fc == null) {
                    throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                            "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                    + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                    + " and ItemID "
                                    + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                    + " was found.");
                }

                List<FcDataObject> fcDataObjects = logicalNode.getChildren(fc);

                if (fcDataObjects == null || fcDataObjects.size() == 0) {
                    throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                            "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                                    + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                                    + " and ItemID "
                                    + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                                    + " was found.");
                }

                Components comp = new Components();
                List<TypeDescription.Structure.Components.SEQUENCE> doStructComponents = comp.getSEQUENCE();
                for (ModelNode child : fcDataObjects) {
                    TypeSpecification typeSpecification = new TypeSpecification();
                    typeSpecification.setTypeDescription(child.getMmsTypeSpec());

                    TypeDescription.Structure.Components.SEQUENCE structComponent = new TypeDescription.Structure.Components.SEQUENCE();
                    structComponent.setComponentName(new Identifier(child.getName().getBytes()));
                    structComponent.setComponentType(typeSpecification);
                    doStructComponents.add(structComponent);
                }

                Structure struct = new Structure();
                struct.setComponents(comp);

                TypeDescription typeDescription = new TypeDescription();
                typeDescription.setStructure(struct);

                GetVariableAccessAttributesResponse getVariableAccessAttributesResponse = new GetVariableAccessAttributesResponse();
                getVariableAccessAttributesResponse.setMmsDeletable(new BerBoolean(false));
                getVariableAccessAttributesResponse.setTypeDescription(typeDescription);

                return getVariableAccessAttributesResponse;
            }
        }

        logicalNode = (LogicalNode) modelNode.getChild(itemIdString);
        if (logicalNode == null) {
            throw new ServiceError(ServiceError.INSTANCE_NOT_AVAILABLE,
                    "GetVariableAccessAttributes (GetDataDefinition): no object with domainId "
                            + getVariableAccessAttributesRequest.getName().getDomainSpecific().getDomainID()
                            + " and ItemID "
                            + getVariableAccessAttributesRequest.getName().getDomainSpecific().getItemID()
                            + " was found.");
        }

        Components components = new Components();
        List<TypeDescription.Structure.Components.SEQUENCE> structComponents = components.getSEQUENCE();

        for (String mmsFc : mmsFcs) {
            Fc fc = Fc.fromString(mmsFc);
            if (fc != null) {

                Collection<FcDataObject> fcDataObjects = logicalNode.getChildren(fc);
                if (fcDataObjects == null) {
                    continue;
                }

                Components comp = new Components();
                List<TypeDescription.Structure.Components.SEQUENCE> doStructComponents = comp.getSEQUENCE();

                for (ModelNode child : fcDataObjects) {
                    TypeSpecification typeSpecification = new TypeSpecification();
                    typeSpecification.setTypeDescription(child.getMmsTypeSpec());

                    TypeDescription.Structure.Components.SEQUENCE doStructComponent = new TypeDescription.Structure.Components.SEQUENCE();
                    doStructComponent.setComponentName(new Identifier(child.getName().getBytes()));
                    doStructComponent.setComponentType(typeSpecification);

                    doStructComponents.add(doStructComponent);
                }

                Structure struct = new Structure();
                struct.setComponents(comp);

                TypeDescription fcTypeSpec = new TypeDescription();
                fcTypeSpec.setStructure(struct);

                TypeSpecification typeSpecification = new TypeSpecification();
                typeSpecification.setTypeDescription(fcTypeSpec);

                TypeDescription.Structure.Components.SEQUENCE structCom = new TypeDescription.Structure.Components.SEQUENCE();
                structCom.setComponentName(new Identifier(mmsFc.getBytes()));
                structCom.setComponentType(typeSpecification);

                structComponents.add(structCom);

            }
        }

        Structure struct = new Structure();
        struct.setComponents(components);

        TypeDescription typeSpec = new TypeDescription();
        typeSpec.setStructure(struct);

        GetVariableAccessAttributesResponse getVariableAccessAttributesResponse = new GetVariableAccessAttributesResponse();
        getVariableAccessAttributesResponse.setMmsDeletable(new BerBoolean(false));
        getVariableAccessAttributesResponse.setTypeDescription(typeSpec);

        return getVariableAccessAttributesResponse;

    }

    private ReadResponse handleGetDataValuesRequest(ReadRequest mmsReadRequest) throws ServiceError {

        VariableAccessSpecification variableAccessSpecification = mmsReadRequest.getVariableAccessSpecification();

        if (mmsReadRequest.getSpecificationWithResult() == null
                || mmsReadRequest.getSpecificationWithResult().value == false) {

            if (variableAccessSpecification.getListOfVariable() == null) {
                throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                        "handleGetDataValuesRequest: Got an invalid MMS packet");
            }

            List<VariableDefs.SEQUENCE> listOfVariable = variableAccessSpecification.getListOfVariable().getSEQUENCE();

            if (listOfVariable.size() < 1) {
                throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
                        "handleGetDataValuesRequest: less than one variableAccessSpecification is not allowed");
            }

            ListOfAccessResult listOfAccessResult = new ListOfAccessResult();
            List<AccessResult> accessResults = listOfAccessResult.getAccessResult();

            synchronized (serverModel) {
                for (VariableDefs.SEQUENCE variableDef : listOfVariable) {

                    FcModelNode modelNode = serverModel.getNodeFromVariableDef(variableDef);

                    if (modelNode == null) {
                        logger.debug("Got a GetDataValues request for a non existent model node.");
                        // 10 indicates error "object-non-existent"
                        AccessResult accessResult = new AccessResult();
                        accessResult.setFailure(new DataAccessError(10L));
                        accessResults.add(accessResult);
                    }
                    else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Got a GetDataValues request for node: " + modelNode);
                            if (!(modelNode instanceof BasicDataAttribute)) {
                                for (BasicDataAttribute bda : modelNode.getBasicDataAttributes()) {
                                    logger.debug("sub BDA is:" + bda);
                                }
                            }
                        }
                        accessResults.add(getReadResult(modelNode));
                    }
                }
            }

            ReadResponse readResponse = new ReadResponse();
            readResponse.setListOfAccessResult(listOfAccessResult);
            return readResponse;
        }
        else {
            logger.debug("Got a GetDataSetValues request.");

            String dataSetReference = convertToDataSetReference(variableAccessSpecification.getVariableListName());

            if (dataSetReference == null) {
                throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
                        "handleGetDataSetValuesRequest: DataSet name incorrect");
            }

            ListOfAccessResult listOfAccessResult = new ListOfAccessResult();
            List<AccessResult> accessResults = listOfAccessResult.getAccessResult();

            if (dataSetReference.startsWith("@")) {
                DataSet dataSet = nonPersistentDataSets.get(dataSetReference);
                if (dataSet == null) {
                    throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
                            "handleGetDataSetValuesRequest: a DataSet with the given reference does not exist");
                }

                for (FcModelNode dsMember : dataSet) {
                    accessResults.add(getReadResult(dsMember));
                }
            }
            else {
                synchronized (serverModel) {
                    DataSet dataSet = serverModel.getDataSet(dataSetReference);

                    if (dataSet == null) {
                        throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
                                "handleGetDataSetValuesRequest: a DataSet with the given reference does not exist");
                    }

                    for (FcModelNode dsMember : dataSet) {
                        accessResults.add(getReadResult(dsMember));
                    }
                }
            }
            ReadResponse readResponse = new ReadResponse();
            readResponse.setListOfAccessResult(listOfAccessResult);
            return readResponse;

        }

    }

    private AccessResult getReadResult(FcModelNode modelNode) {

        AccessResult accessResult = new AccessResult();

        if (modelNode.getFc() == Fc.CO && modelNode.getName().equals("SBO")) {
            // if (modelNode.getName().equals("SBO")) {
            FcModelNode cdcParent = (FcModelNode) modelNode.getParent();
            ModelNode ctlModelNode = serverModel.findModelNode(cdcParent.getReference(), Fc.CF).getChild("ctlModel");
            if (ctlModelNode == null || !(ctlModelNode instanceof BdaInt8)
                    || ((BdaInt8) ctlModelNode).getValue() != 2) {
                logger.warn("Selecting controle DO fails because ctlModel is not set to \"sbo-with-normal-security\"");
                // 3 indicates error "object_access_denied"
                accessResult.setFailure(new DataAccessError(3L));
                return accessResult;
            }
            if (!cdcParent.select(this, serverSap.timer)) {
                Data data = new Data();
                data.setVisibleString(new BerVisibleString(""));
                accessResult.setSuccess(data);
                return accessResult;
            }
            Data data = new Data();
            data.setVisibleString(new BerVisibleString("success"));
            accessResult.setSuccess(data);
            return accessResult;

            // }
            // else {
            // logger.warn("A client tried to read a control variable other than SBO. This is not allowed.");
            // // 3 indicates error "object_access_denied"
            // return new AccessResult(new BerInteger(3L), null);
            // }

        }

        Data data = modelNode.getMmsDataObj();

        if (data == null) {
            // 11 indicates error "object_value_invalid"
            accessResult.setFailure(new DataAccessError(11L));
            return accessResult;
        }

        accessResult.setSuccess(data);
        return accessResult;
    }

    private WriteResponse handleSetDataValuesRequest(WriteRequest mmsWriteRequest) throws ServiceError {

        VariableAccessSpecification variableAccessSpecification = mmsWriteRequest.getVariableAccessSpecification();

        List<Data> listOfData = mmsWriteRequest.getListOfData().getData();

        WriteResponse writeResponse = new WriteResponse();
        List<WriteResponse.CHOICE> mmsResponseValues = writeResponse.getCHOICE();

        if (variableAccessSpecification.getListOfVariable() != null) {
            logger.debug("Got a SetDataValues request.");

            List<VariableDefs.SEQUENCE> listOfVariable = variableAccessSpecification.getListOfVariable().getSEQUENCE();

            if (listOfVariable.size() < 1 || listOfData.size() < 1 || listOfVariable.size() != listOfData.size()) {
                throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
                        "handleSetDataValuesRequest: less than one variableAccessSpecification or data element is not allowed, or listOfData ne listOfVar");
            }

            Iterator<Data> mmsDataIterator = listOfData.iterator();

            List<BasicDataAttribute> totalBdasToBeWritten = new ArrayList<>();
            int[] numBdas = new int[listOfData.size()];

            int i = -1;
            synchronized (serverModel) {
                for (VariableDefs.SEQUENCE variableDef : listOfVariable) {
                    i++;
                    Data mmsData = mmsDataIterator.next();

                    FcModelNode modelNode = serverModel.getNodeFromVariableDef(variableDef);

                    if (modelNode == null) {
                        // 10 indicates error "object-non-existent"
                        WriteResponse.CHOICE writeResponseChoice = new WriteResponse.CHOICE();
                        writeResponseChoice.setFailure(new DataAccessError(10L));
                        mmsResponseValues.add(writeResponseChoice);
                    }
                    else {

                        getFirstWriteResults(mmsResponseValues, totalBdasToBeWritten, numBdas, i, modelNode, mmsData);
                    }
                }

                writeAndFillMissingWriteResults(mmsResponseValues, totalBdasToBeWritten, numBdas);
            }

        }
        else if (variableAccessSpecification.getVariableListName() != null) {
            logger.debug("Got a SetDataSetValues request.");

            String dataSetRef = convertToDataSetReference(variableAccessSpecification.getVariableListName());

            // TODO handle non-persisten DataSets too

            DataSet dataSet = serverModel.getDataSet(dataSetRef);

            Iterator<Data> mmsDataIterator = listOfData.iterator();

            List<BasicDataAttribute> totalBdasToBeWritten = new ArrayList<>();
            int[] numBdas = new int[listOfData.size()];

            int i = -1;
            synchronized (serverModel) {
                for (FcModelNode dataSetMember : dataSet) {
                    i++;
                    Data mmsData = mmsDataIterator.next();

                    getFirstWriteResults(mmsResponseValues, totalBdasToBeWritten, numBdas, i, dataSetMember, mmsData);
                }

                writeAndFillMissingWriteResults(mmsResponseValues, totalBdasToBeWritten, numBdas);
            }

        }
        else {
            throw new ServiceError(ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
                    "handleSetDataValuesRequest: invalid MMS request");
        }

        return writeResponse;
    }

    private void writeAndFillMissingWriteResults(List<WriteResponse.CHOICE> mmsResponseValues,
            List<BasicDataAttribute> totalBdasToBeWritten, int[] numBdas) {
        int i;
        if (totalBdasToBeWritten.size() != 0) {
            List<ServiceError> serviceErrors = serverSap.serverEventListener.write(totalBdasToBeWritten);
            ListIterator<WriteResponse.CHOICE> mmsResponseIterator = mmsResponseValues.listIterator();
            if (serviceErrors == null || serviceErrors.size() != totalBdasToBeWritten.size()) {
                while (mmsResponseIterator.hasNext()) {
                    if (mmsResponseIterator.next() == null) {
                        mmsResponseIterator.set(writeSuccess);
                    }
                }
                for (BasicDataAttribute bda : totalBdasToBeWritten) {
                    bda.mirror.setValueFrom(bda);
                }
            }
            else {
                i = -1;
                Iterator<ServiceError> serviceErrorIterator = serviceErrors.iterator();
                Iterator<BasicDataAttribute> bdaToBeWrittenIterator = totalBdasToBeWritten.iterator();
                while (mmsResponseIterator.hasNext()) {
                    i++;
                    if (mmsResponseIterator.next() == null) {
                        for (int j = 0; j < numBdas[i]; j++) {
                            ServiceError serviceError = serviceErrorIterator.next();
                            BasicDataAttribute bda = bdaToBeWrittenIterator.next();
                            if (serviceError != null) {
                                WriteResponse.CHOICE writeResponseChoice = new WriteResponse.CHOICE();
                                writeResponseChoice
                                        .setFailure(new DataAccessError(serviceErrorToMmsError(serviceError)));
                                mmsResponseIterator.set(writeResponseChoice);
                            }
                            else {
                                bda.mirror.setValueFrom(bda);
                            }
                        }
                    }
                }
            }

        }
    }

    private void getFirstWriteResults(List<WriteResponse.CHOICE> mmsResponseValues,
            List<BasicDataAttribute> totalBdasToBeWritten, int[] numBdas, int i, FcModelNode dataSetMember,
            Data mmsData) {
        WriteResponse.CHOICE writeResult = getWriteResult(dataSetMember, mmsData);
        if (writeResult == null) {
            FcModelNode fcModelNodeCopy = (FcModelNode) dataSetMember.copy();
            try {
                fcModelNodeCopy.setValueFromMmsDataObj(mmsData);
            } catch (ServiceError e) {
                logger.warn("SetDataValues failed because of data missmatch.", e);
                WriteResponse.CHOICE writeResponseChoice = new WriteResponse.CHOICE();
                writeResponseChoice.setFailure(new DataAccessError(serviceErrorToMmsError(e)));
                mmsResponseValues.add(writeResponseChoice);
                return;
            }

            if (fcModelNodeCopy.fc == Fc.CO) {
                // TODO timeactivate operate
                fcModelNodeCopy = (BasicDataAttribute) fcModelNodeCopy.getChild("ctlVal");
                // TODO write origin and ctlNum if they exist
            }
            else {

            }

            List<BasicDataAttribute> bdas = fcModelNodeCopy.getBasicDataAttributes();
            totalBdasToBeWritten.addAll(bdas);
            numBdas[i] = bdas.size();
            mmsResponseValues.add(null);

        }
        else {
            mmsResponseValues.add(writeResult);
        }
    }

    private WriteResponse.CHOICE getWriteResult(FcModelNode modelNode, Data mmsData) {

        WriteResponse.CHOICE writeResponse = new WriteResponse.CHOICE();

        Fc fc = modelNode.getFc();
        if (fc == Fc.ST || fc == Fc.MX || fc == Fc.OR || fc == Fc.EX) {
            // 3 indicates error "object_access_denied"
            writeResponse.setFailure(new DataAccessError(3L));
            return writeResponse;
        }

        if (fc == Fc.CO) {
            String nodeName = modelNode.getName();

            if (nodeName.equals("Oper")) {
                FcModelNode cdcParent = (FcModelNode) modelNode.getParent();
                ModelNode ctlModelNode = serverModel.findModelNode(cdcParent.getReference(), Fc.CF)
                        .getChild("ctlModel");
                if (ctlModelNode == null || !(ctlModelNode instanceof BdaInt8)) {
                    logger.warn("Operatring controle DO failed because ctlModel is not set.");
                    // 3 indicates error "object_access_denied"
                    writeResponse.setFailure(new DataAccessError(3L));
                    return writeResponse;
                }

                int ctlModel = ((BdaInt8) ctlModelNode).getValue();

                /* Direct control with normal security (direct-operate) */
                if (ctlModel == 1) {
                    return null;
                }
                /* SBO control with normal security (operate-once or operate-many) */
                else if (ctlModel == 2) {
                    if (cdcParent.isSelectedBy(this)) {
                        return null;
                    }
                    else {
                        // 3 indicates error "object_access_denied"
                        writeResponse.setFailure(new DataAccessError(3L));
                        return writeResponse;
                    }

                }
                else {
                    logger.warn("SetDataValues failed because of unsupported ctlModel: " + ctlModel);
                    // 9 indicates error "object_access_unsupported"
                    writeResponse.setFailure(new DataAccessError(9L));
                    return writeResponse;

                }
            }
            else {
                logger.warn("SetDataValues failed because of the operation is not allowed yet: " + modelNode.getName());
                // 9 indicates error "object_access_unsupported"
                writeResponse.setFailure(new DataAccessError(9L));
                return writeResponse;
            }
        }
        else if (fc == Fc.RP) {

            if (modelNode instanceof Rcb) {
                // 3 indicates error "object_access_denied"
                writeResponse.setFailure(new DataAccessError(3L));
                return writeResponse;
            }

            FcModelNode fcModelNodeCopy = (FcModelNode) modelNode.copy();

            try {
                fcModelNodeCopy.setValueFromMmsDataObj(mmsData);
            } catch (ServiceError e) {
                WriteResponse.CHOICE writeResponseChoice = new WriteResponse.CHOICE();
                writeResponseChoice.setFailure(new DataAccessError(serviceErrorToMmsError(e)));
                return writeResponseChoice;
            }

            Urcb urcb = (Urcb) modelNode.getParent();

            String nodeName = modelNode.getName();

            synchronized (urcb) {
                if (nodeName.equals("RptEna")) {
                    BdaBoolean rptEnaNode = (BdaBoolean) fcModelNodeCopy;
                    if (rptEnaNode.getValue()) {
                        if (urcb.dataSet == null) {
                            logger.info("client tried to enable RCB even though there is no configured data set");
                            // 3 indicates error "object_access_denied"
                            writeResponse.setFailure(new DataAccessError(3L));
                            return writeResponse;
                        }
                        if (urcb.reserved == null) {
                            urcb.reserved = this;
                            urcb.enable();
                            rsvdURCBs.add(urcb);
                            return writeSuccess;
                        }
                        else if (urcb.reserved == this) {
                            urcb.enable();
                            return writeSuccess;
                        }
                        else {
                            // 3 indicates error "object_access_denied"
                            writeResponse.setFailure(new DataAccessError(3L));
                            return writeResponse;
                        }

                    }
                    else {
                        // disable reporting
                        if (urcb.reserved == this) {
                            urcb.disable();
                            return writeSuccess;
                        }
                        else {
                            // 3 indicates error "object_access_denied"
                            writeResponse.setFailure(new DataAccessError(3L));
                            return writeResponse;
                        }
                    }
                }
                else if (nodeName.equals("Resv")) {
                    BdaBoolean rptResvNode = (BdaBoolean) fcModelNodeCopy;
                    if (rptResvNode.getValue()) {

                        if (urcb.reserved == null) {
                            urcb.reserved = this;
                            urcb.getResv().setValue(true);
                            rsvdURCBs.add(urcb);
                            return writeSuccess;
                        }
                        else if (urcb.reserved == this) {
                            return writeSuccess;
                        }
                        else {
                            // 3 indicates error "object_access_denied"
                            writeResponse.setFailure(new DataAccessError(3L));
                            return writeResponse;
                        }
                    }
                    else {
                        if (urcb.reserved == this) {
                            urcb.reserved = null;
                            urcb.getResv().setValue(false);
                            rsvdURCBs.remove(urcb);
                            return writeSuccess;
                        }
                        else {
                            // 3 indicates error "object_access_denied"
                            writeResponse.setFailure(new DataAccessError(3L));
                            return writeResponse;
                        }
                    }

                }
                else if (nodeName.equals("DatSet")) {
                    if ((urcb.reserved == null || urcb.reserved == this) && !urcb.enabled) {
                        String dataSetRef = ((BdaVisibleString) fcModelNodeCopy).getStringValue();
                        if (dataSetRef.isEmpty()) {
                            urcb.dataSet = null;
                            ((BasicDataAttribute) modelNode).setValueFrom((BasicDataAttribute) fcModelNodeCopy);
                            return writeSuccess;

                        }
                        else {
                            DataSet dataSet = serverModel.getDataSet(dataSetRef);
                            if (dataSet != null) {
                                urcb.dataSet = dataSet;
                                ((BasicDataAttribute) modelNode).setValueFrom((BasicDataAttribute) fcModelNodeCopy);
                                return writeSuccess;
                            }
                            else {
                                logger.info("Client tried to set dataSetReference of URCB to non existant data set.");
                                // 3 indicates error "object_access_denied"
                                writeResponse.setFailure(new DataAccessError(3L));
                                return writeResponse;
                            }
                        }
                    }
                    else {
                        logger.info(
                                "Client tried to write RCB parameter even though URCB is reserved by other client or already enabled.");
                        // 3 indicates error "object_access_denied"
                        writeResponse.setFailure(new DataAccessError(3L));
                        return writeResponse;
                    }
                }
                else if (nodeName.equals("OptFlds")) {
                    if ((urcb.reserved == null || urcb.reserved == this) && !urcb.enabled) {
                        if (!((BdaOptFlds) modelNode).isBufferOverflow() && !((BdaOptFlds) modelNode).isConfigRevision()
                                && !((BdaOptFlds) modelNode).isDataReference()
                                && !((BdaOptFlds) modelNode).isEntryId()) {
                            ((BasicDataAttribute) modelNode).setValueFrom((BasicDataAttribute) fcModelNodeCopy);
                            return writeSuccess;
                        }
                        else {
                            logger.info("Client tried to write OptFlds with usupported field set to true.");
                            // 3 indicates error "object_access_denied"
                            writeResponse.setFailure(new DataAccessError(3L));
                            return writeResponse;
                        }
                    }
                    else {
                        logger.info(
                                "Client tried to write RCB parameter even though URCB is reserved by other client or already enabled.");
                        // 3 indicates error "object_access_denied"
                        writeResponse.setFailure(new DataAccessError(3L));
                        return writeResponse;
                    }

                }
                else if (nodeName.equals("GI")) {

                    if ((urcb.reserved == this) && urcb.enabled
                            && ((BdaTriggerConditions) urcb.getChild("TrgOps")).isGeneralInterrogation()) {
                        urcb.generalInterrogation();
                        return writeSuccess;
                    }
                    else {
                        logger.info(
                                "Client tried to initiate a general interrogation even though URCB is not enabled by this client or general interrogation is not enabled in the trigger options.");
                        // 3 indicates error "object_access_denied"
                        writeResponse.setFailure(new DataAccessError(3L));
                        return writeResponse;
                    }

                }
                else if (nodeName.equals("RptID") || nodeName.equals("BufTm") || nodeName.equals("TrgOps")
                        || nodeName.equals("IntgPd")) {
                    if ((urcb.reserved == null || urcb.reserved == this) && !urcb.enabled) {
                        ((BasicDataAttribute) modelNode).setValueFrom((BasicDataAttribute) fcModelNodeCopy);
                        return writeSuccess;
                    }
                    else {
                        // 3 indicates error "object_access_denied"
                        writeResponse.setFailure(new DataAccessError(3L));
                        return writeResponse;
                    }

                }
                else {
                    // nodes sqnum, ConfRev, and owner may not be read
                    // 3 indicates error "object_access_denied"
                    writeResponse.setFailure(new DataAccessError(3L));
                    return writeResponse;
                }
            }

        }
        else {

            return null;
        }

    }

    // private WriteResponse.SubChoice operate(FcModelNode modelNode, Data mmsData) {
    // FcModelNode fcModelNodeCopy = (FcModelNode) modelNode.copy();
    // try {
    // fcModelNodeCopy.setValueFromMmsDataObj(mmsData);
    // } catch (ServiceError e) {
    // logger.warn("SetDataValues failed because of data missmatch.", e);
    // return new WriteResponse.SubChoice(new BerInteger(serviceErrorToMmsError(e)), null);
    // }
    //
    // // TODO timeactivate operate
    //
    // BasicDataAttribute ctlValBda = (BasicDataAttribute) fcModelNodeCopy.getChild("ctlVal");
    // List<BasicDataAttribute> bdas = new ArrayList<BasicDataAttribute>(1);
    // bdas.add(ctlValBda);
    // List<ServiceError> serviceErrors;
    // try {
    // serviceErrors = serverSap.serverEventListener.write(bdas);
    // } catch (ServiceError e) {
    // return new WriteResponse.SubChoice(new BerInteger(serviceErrorToMmsError(e)), null);
    // }
    // if (serviceErrors != null && serviceErrors.size() == bdas.size() && serviceErrors.get(1) != null) {
    // return new WriteResponse.SubChoice(new BerInteger(serviceErrorToMmsError(serviceErrors.get(1))), null);
    // }
    //
    // ctlValBda.mirror.setValueFrom(ctlValBda);
    // // TODO write origin and ctlNum if they exist
    //
    // return writeSuccess;
    // }

    private int serviceErrorToMmsError(ServiceError e) {

        switch (e.getErrorCode()) {
        case ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT:
            return 1;
        case ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT:
            return 2;
        case ServiceError.ACCESS_VIOLATION:
            return 3;
        case ServiceError.TYPE_CONFLICT:
            return 7;
        case ServiceError.INSTANCE_NOT_AVAILABLE:
            return 10;
        case ServiceError.PARAMETER_VALUE_INCONSISTENT:
            return 11;
        default:
            return 9;
        }
    }

    private GetNameListResponse handleGetDataSetNamesRequest(GetNameListRequest getNameListRequest)
            throws ServiceError {

        BerVisibleString domainSpecific = getNameListRequest.getObjectScope().getDomainSpecific();

        List<String> dsList = null;
        if (domainSpecific == null) {
            dsList = new ArrayList<>(nonPersistentDataSets.size());
            for (String dataSet : nonPersistentDataSets.keySet()) {
                dsList.add(dataSet);
            }
        }
        else {
            dsList = serverModel.getDataSetNames(domainSpecific.toString());
        }

        insertRef = true;
        if (getNameListRequest.getContinueAfter() != null) {
            continueAfter = getNameListRequest.getContinueAfter().toString();
            insertRef = false;
        }

        ListOfIdentifier listOf = new ListOfIdentifier();
        List<Identifier> identifiers = listOf.getIdentifier();

        int identifierSize = 0;
        boolean moreFollows = false;

        if (dsList != null) {
            for (String dsRef : dsList) {
                if (insertRef == true) {
                    if (identifierSize > negotiatedMaxPduSize - 200) {
                        moreFollows = true;
                        logger.info("maxMMSPduSize reached");
                        break;
                    }
                    identifiers.add(new Identifier(dsRef.getBytes()));
                    identifierSize += dsRef.length() + 2;
                }
                else {
                    if (dsRef.equals(continueAfter)) {
                        insertRef = true;
                    }
                }
            }
        }

        GetNameListResponse getNameListResponse = new GetNameListResponse();
        getNameListResponse.setListOfIdentifier(listOf);
        getNameListResponse.setMoreFollows(new BerBoolean(moreFollows));

        return getNameListResponse;
    }

    private GetNamedVariableListAttributesResponse handleGetDataSetDirectoryRequest(ObjectName mmsGetNamedVarListAttReq)
            throws ServiceError {

        String dataSetReference = convertToDataSetReference(mmsGetNamedVarListAttReq);

        DataSet dataSet;

        if (dataSetReference.startsWith("@")) {
            dataSet = nonPersistentDataSets.get(dataSetReference);
        }
        else {
            dataSet = serverModel.getDataSet(dataSetReference);
        }

        if (dataSet == null) {
            throw new ServiceError(ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
                    "DataSet with that reference is does not exist.");
        }

        VariableDefs variableDefs = new VariableDefs();

        List<VariableDefs.SEQUENCE> listOfVariable = variableDefs.getSEQUENCE();

        for (FcModelNode member : dataSet) {
            listOfVariable.add(member.getMmsVariableDef());
        }

        GetNamedVariableListAttributesResponse getNamedVariableListAttributesResponse = new GetNamedVariableListAttributesResponse();
        getNamedVariableListAttributesResponse.setListOfVariable(variableDefs);
        getNamedVariableListAttributesResponse.setMmsDeletable(new BerBoolean(dataSet.isDeletable()));

        return getNamedVariableListAttributesResponse;
    }

    private static String convertToDataSetReference(ObjectName mmsObjectName) {
        if (mmsObjectName.getDomainSpecific() != null) {
            return mmsObjectName.getDomainSpecific().getDomainID().toString() + "/"
                    + mmsObjectName.getDomainSpecific().getItemID().toString().replace('$', '.');
        }
        else if (mmsObjectName.getAaSpecific() != null) {
            // format is "@DataSetName"
            return mmsObjectName.getAaSpecific().toString();
        }
        return null;
    }

    private DefineNamedVariableListResponse handleCreateDataSetRequest(
            DefineNamedVariableListRequest mmsDefineNamedVariableListRequest) throws ServiceError {
        String dataSetReference = convertToDataSetReference(mmsDefineNamedVariableListRequest.getVariableListName());
        if (dataSetReference == null) {
            throw new ServiceError(ServiceError.PARAMETER_VALUE_INCONSISTENT,
                    "handleCreateDataSetRequest: invalid MMS request (No DataSet Name Specified)");
        }

        List<VariableDefs.SEQUENCE> nameList = mmsDefineNamedVariableListRequest.getListOfVariable().getSEQUENCE();

        List<FcModelNode> dataSetMembers = new ArrayList<>(nameList.size());

        for (VariableDefs.SEQUENCE variableDef : nameList) {
            dataSetMembers.add(serverModel.getNodeFromVariableDef(variableDef));
        }

        DataSet dataSet = new DataSet(dataSetReference, dataSetMembers, true);

        if (dataSetReference.startsWith("@")) {
            if (nonPersistentDataSets.containsKey(dataSetReference)) {
                throw new ServiceError(ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
                        "data set with that name exists already");
            }
            nonPersistentDataSets.put(dataSetReference, dataSet);
        }
        else {
            serverModel.addDataSet(dataSet);
        }

        return new DefineNamedVariableListResponse();
    }

    private DeleteNamedVariableListResponse handleDeleteDataSetRequest(
            DeleteNamedVariableListRequest mmsDelNamVarListReq) throws ServiceError {
        String dataSetReference = convertToDataSetReference(
                mmsDelNamVarListReq.getListOfVariableListName().getObjectName().get(0));

        DeleteNamedVariableListResponse deleteNamedVariableListResponse = new DeleteNamedVariableListResponse();

        if (dataSetReference.startsWith("@")) {
            if (nonPersistentDataSets.remove(dataSetReference) == null) {
                deleteNamedVariableListResponse.setNumberMatched(new Unsigned32(0));
                deleteNamedVariableListResponse.setNumberDeleted(new Unsigned32(0));
                return deleteNamedVariableListResponse;
            }
            else {
                deleteNamedVariableListResponse.setNumberMatched(new Unsigned32(1));
                deleteNamedVariableListResponse.setNumberDeleted(new Unsigned32(1));
                return deleteNamedVariableListResponse;
            }
        }
        else {
            synchronized (serverModel) {
                if (serverModel.removeDataSet(dataSetReference) == null) {
                    if (serverModel.getDataSet(dataSetReference) == null) {
                        // DataSet with the name does not exist.
                        deleteNamedVariableListResponse.setNumberMatched(new Unsigned32(0));
                        deleteNamedVariableListResponse.setNumberDeleted(new Unsigned32(0));
                        return deleteNamedVariableListResponse;
                    }
                    else {
                        // DataSet exists but is not deletable
                        deleteNamedVariableListResponse.setNumberMatched(new Unsigned32(1));
                        deleteNamedVariableListResponse.setNumberDeleted(new Unsigned32(0));
                        return deleteNamedVariableListResponse;
                    }
                }
                else {
                    deleteNamedVariableListResponse.setNumberMatched(new Unsigned32(1));
                    deleteNamedVariableListResponse.setNumberDeleted(new Unsigned32(1));
                    return deleteNamedVariableListResponse;
                }
            }
        }
    }

    void close() {
        cleanUpConnection();
        if (acseAssociation != null) {
            acseAssociation.disconnect();
        }
    }

}
