/*
 * 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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;

import org.openmuc.openiec61850.internal.scl.AbstractDataAttribute;
import org.openmuc.openiec61850.internal.scl.Bda;
import org.openmuc.openiec61850.internal.scl.Da;
import org.openmuc.openiec61850.internal.scl.DaType;
import org.openmuc.openiec61850.internal.scl.Do;
import org.openmuc.openiec61850.internal.scl.DoType;
import org.openmuc.openiec61850.internal.scl.EnumType;
import org.openmuc.openiec61850.internal.scl.EnumVal;
import org.openmuc.openiec61850.internal.scl.LnSubDef;
import org.openmuc.openiec61850.internal.scl.LnType;
import org.openmuc.openiec61850.internal.scl.Sdo;
import org.openmuc.openiec61850.internal.scl.TypeDefinitions;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

final class SclParser {

    private TypeDefinitions typeDefinitions;
    private final Map<String, DataSet> dataSetsMap = new HashMap<>();

    private Document doc;
    private String iedName;
    private List<ServerSap> serverSaps = null;
    private boolean useResvTmsAttributes = false;

    private final List<LnSubDef> dataSetDefs = new ArrayList<>();

    public List<ServerSap> getServerSaps() {
        return serverSaps;
    }

    public void parse(String icdFile) throws SclParseException {
        try {
            parse(new FileInputStream(icdFile));
        } catch (FileNotFoundException e) {
            throw new SclParseException(e);
        }
    }

    public void parse(InputStream icdFileStream) throws SclParseException {

        typeDefinitions = new TypeDefinitions();

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setIgnoringComments(true);

        try {
            doc = factory.newDocumentBuilder().parse(icdFileStream);
        } catch (Exception e) {
            throw new SclParseException(e);
        }

        Node rootNode = doc.getDocumentElement();

        if (!"SCL".equals(rootNode.getNodeName())) {
            throw new SclParseException("Root node in SCL file is not of type \"SCL\"");
        }

        readTypeDefinitions();

        NodeList iedList = doc.getElementsByTagName("IED");
        if (iedList.getLength() == 0) {
            throw new SclParseException("No IED section found!");
        }

        Node nameAttribute = iedList.item(0).getAttributes().getNamedItem("name");

        iedName = nameAttribute.getNodeValue();
        if ((iedName == null) || (iedName.length() == 0)) {
            throw new SclParseException("IED must have a name!");
        }

        NodeList iedElements = iedList.item(0).getChildNodes();

        serverSaps = new ArrayList<>(iedElements.getLength());
        for (int i = 0; i < iedElements.getLength(); i++) {
            Node element = iedElements.item(i);
            String nodeName = element.getNodeName();
            if ("AccessPoint".equals(nodeName)) {
                serverSaps.add(createAccessPoint(element));
            }
            else if ("Services".equals(nodeName)) {
                NodeList servicesElements = element.getChildNodes();
                for (int j = 0; j < servicesElements.getLength(); j++) {
                    if ("ReportSettings".equals(servicesElements.item(j).getNodeName())) {
                        Node resvTmsAttribute = servicesElements.item(j).getAttributes().getNamedItem("resvTms");
                        if (resvTmsAttribute != null) {
                            useResvTmsAttributes = resvTmsAttribute.getNodeValue().equalsIgnoreCase("true");
                        }
                    }
                }
            }
        }
    }

    private void readTypeDefinitions() throws SclParseException {

        NodeList dttSections = doc.getElementsByTagName("DataTypeTemplates");

        if (dttSections.getLength() != 1) {
            throw new SclParseException("Only one DataTypeSection allowed");
        }

        Node dtt = dttSections.item(0);

        NodeList dataTypes = dtt.getChildNodes();

        for (int i = 0; i < dataTypes.getLength(); i++) {
            Node element = dataTypes.item(i);

            String nodeName = element.getNodeName();

            if (nodeName.equals("LNodeType")) {
                typeDefinitions.putLNodeType(new LnType(element));
            }
            else if (nodeName.equals("DOType")) {
                typeDefinitions.putDOType(new DoType(element));
            }
            else if (nodeName.equals("DAType")) {
                typeDefinitions.putDAType(new DaType(element));
            }
            else if (nodeName.equals("EnumType")) {
                typeDefinitions.putEnumType(new EnumType(element));
            }
        }
    }

    private ServerSap createAccessPoint(Node iedServer) throws SclParseException {
        ServerSap serverSap = null;

        NodeList elements = iedServer.getChildNodes();

        for (int i = 0; i < elements.getLength(); i++) {
            Node element = elements.item(i);

            if (element.getNodeName().equals("Server")) {

                ServerModel server = createServerModel(element);

                Node namedItem = iedServer.getAttributes().getNamedItem("name");
                if (namedItem == null) {
                    throw new SclParseException("AccessPoint has no name attribute!");
                }
                String name = namedItem.getNodeValue();
                serverSap = new ServerSap(102, 0, null, server, name, null);

                break;
            }
        }

        if (serverSap == null) {
            throw new SclParseException("AccessPoint has no server!");
        }

        return serverSap;
    }

    private ServerModel createServerModel(Node serverXMLNode) throws SclParseException {

        NodeList elements = serverXMLNode.getChildNodes();
        List<LogicalDevice> logicalDevices = new ArrayList<>(elements.getLength());

        for (int i = 0; i < elements.getLength(); i++) {
            Node element = elements.item(i);

            if (element.getNodeName().equals("LDevice")) {
                logicalDevices.add(createNewLDevice(element));
            }
        }

        ServerModel serverModel = new ServerModel(logicalDevices, null);

        for (LnSubDef dataSetDef : dataSetDefs) {
            DataSet dataSet = createDataSet(serverModel, dataSetDef.logicalNode, dataSetDef.defXmlNode);
            dataSetsMap.put(dataSet.getReferenceStr(), dataSet);
        }

        serverModel.addDataSets(dataSetsMap.values());

        return serverModel;

    }

    private LogicalDevice createNewLDevice(Node ldXmlNode) throws SclParseException {

        String inst = null;
        String ldName = null;

        NamedNodeMap attributes = ldXmlNode.getAttributes();

        for (int i = 0; i < attributes.getLength(); i++) {
            Node node = attributes.item(i);
            String nodeName = node.getNodeName();

            if (nodeName.equals("inst")) {
                inst = node.getNodeValue();
            }
            else if (nodeName.equals("ldName")) {
                ldName = node.getNodeValue();
            }
        }

        if (inst == null) {
            throw new SclParseException("Required attribute \"inst\" in logical device not found!");
        }

        NodeList elements = ldXmlNode.getChildNodes();
        List<LogicalNode> logicalNodes = new ArrayList<>();

        String ref;
        if ((ldName != null) && (ldName.length() != 0)) {
            ref = ldName;
        }
        else {
            ref = iedName + inst;
        }

        for (int i = 0; i < elements.getLength(); i++) {
            Node element = elements.item(i);

            if (element.getNodeName().equals("LN") || element.getNodeName().equals("LN0")) {
                logicalNodes.add(createNewLogicalNode(element, ref));
            }
        }

        LogicalDevice lDevice = new LogicalDevice(new ObjectReference(ref), logicalNodes);

        return lDevice;
    }

    private LogicalNode createNewLogicalNode(Node lnXmlNode, String parentRef) throws SclParseException {

        // attributes not needed: desc

        String inst = null;
        String lnClass = null;
        String lnType = null;
        String prefix = "";

        NamedNodeMap attributes = lnXmlNode.getAttributes();

        for (int i = 0; i < attributes.getLength(); i++) {
            Node node = attributes.item(i);
            String nodeName = node.getNodeName();

            if (nodeName.equals("inst")) {
                inst = node.getNodeValue();
            }
            else if (nodeName.equals("lnType")) {
                lnType = node.getNodeValue();
            }
            else if (nodeName.equals("lnClass")) {
                lnClass = node.getNodeValue();
            }
            else if (nodeName.equals("prefix")) {
                prefix = node.getNodeValue();
            }
        }

        if (inst == null) {
            throw new SclParseException("Required attribute \"inst\" not found!");
        }
        if (lnType == null) {
            throw new SclParseException("Required attribute \"lnType\" not found!");
        }
        if (lnClass == null) {
            throw new SclParseException("Required attribute \"lnClass\" not found!");
        }

        String ref = parentRef + '/' + prefix + lnClass + inst;

        LnType lnTypeDef = typeDefinitions.getLNodeType(lnType);

        List<FcDataObject> dataObjects = new ArrayList<>();

        if (lnTypeDef == null) {
            throw new SclParseException("LNType " + lnType + " not defined!");
        }
        for (Do dobject : lnTypeDef.dos) {

            // look for DOI node with the name of the DO
            Node doiNodeFound = null;
            for (int i = 0; i < lnXmlNode.getChildNodes().getLength(); i++) {
                Node childNode = lnXmlNode.getChildNodes().item(i);
                if ("DOI".equals(childNode.getNodeName())) {

                    NamedNodeMap doiAttributes = childNode.getAttributes();
                    Node nameAttribute = doiAttributes.getNamedItem("name");
                    if (nameAttribute != null && nameAttribute.getNodeValue().equals(dobject.getName())) {
                        doiNodeFound = childNode;
                    }
                }
            }

            dataObjects.addAll(createFcDataObjects(dobject.getName(), ref, dobject.getType(), doiNodeFound));

        }

        // look for ReportControl
        for (int i = 0; i < lnXmlNode.getChildNodes().getLength(); i++) {
            Node childNode = lnXmlNode.getChildNodes().item(i);
            if ("ReportControl".equals(childNode.getNodeName())) {
                dataObjects.addAll(createReportControlBlocks(childNode, ref));
            }
        }

        LogicalNode lNode = new LogicalNode(new ObjectReference(ref), dataObjects);

        // look for DataSet definitions
        for (int i = 0; i < lnXmlNode.getChildNodes().getLength(); i++) {
            Node childNode = lnXmlNode.getChildNodes().item(i);
            if ("DataSet".equals(childNode.getNodeName())) {
                dataSetDefs.add(new LnSubDef(childNode, lNode));
            }
        }
        return lNode;
    }

    private DataSet createDataSet(ServerModel serverModel, LogicalNode lNode, Node dsXmlNode) throws SclParseException {

        Node nameAttribute = dsXmlNode.getAttributes().getNamedItem("name");
        if (nameAttribute == null) {
            throw new SclParseException("DataSet must have a name");
        }

        String name = nameAttribute.getNodeValue();

        List<FcModelNode> dsMembers = new ArrayList<>();

        for (int i = 0; i < dsXmlNode.getChildNodes().getLength(); i++) {
            Node fcdaXmlNode = dsXmlNode.getChildNodes().item(i);
            if ("FCDA".equals(fcdaXmlNode.getNodeName())) {

                // For the definition of FCDA see Table 22 part6 ed2

                String ldInst = null;
                String prefix = "";
                String lnClass = null;
                String lnInst = "";
                String doName = "";
                String daName = "";
                Fc fc = null;

                NamedNodeMap attributes = fcdaXmlNode.getAttributes();

                for (int j = 0; j < attributes.getLength(); j++) {
                    Node node = attributes.item(j);
                    String nodeName = node.getNodeName();

                    if (nodeName.equals("ldInst")) {
                        ldInst = node.getNodeValue();
                    }
                    else if (nodeName.equals("lnInst")) {
                        lnInst = node.getNodeValue();
                    }
                    else if (nodeName.equals("lnClass")) {
                        lnClass = node.getNodeValue();
                    }
                    else if (nodeName.equals("prefix")) {
                        prefix = node.getNodeValue();
                    }
                    else if (nodeName.equals("doName")) {
                        doName = node.getNodeValue();
                    }
                    else if (nodeName.equals("daName")) {
                        if (!node.getNodeValue().isEmpty()) {
                            daName = "." + node.getNodeValue();
                        }
                    }
                    else if (nodeName.equals("fc")) {
                        fc = Fc.fromString(node.getNodeValue());
                        if (fc == null) {
                            throw new SclParseException("FCDA contains invalid FC: " + node.getNodeValue());
                        }
                    }

                }

                if (ldInst == null) {
                    throw new SclParseException(
                            "Required attribute \"ldInst\" not found in FCDA: " + nameAttribute + "!");
                }

                if (lnClass == null) {
                    throw new SclParseException("Required attribute \"lnClass\" not found in FCDA!");
                }
                if (fc == null) {
                    throw new SclParseException("Required attribute \"fc\" not found in FCDA!");
                }
                if (!doName.isEmpty()) {

                    String objectReference = iedName + ldInst + "/" + prefix + lnClass + lnInst + "." + doName + daName;

                    ModelNode fcdaNode = serverModel.findModelNode(objectReference, fc);

                    if (fcdaNode == null) {
                        throw new SclParseException("Specified FCDA: " + objectReference + " in DataSet: "
                                + nameAttribute + " not found in Model.");
                    }
                    dsMembers.add((FcModelNode) fcdaNode);
                }
                else {
                    String objectReference = iedName + ldInst + "/" + prefix + lnClass + lnInst;
                    ModelNode logicalNode = serverModel.findModelNode(objectReference, null);
                    if (logicalNode == null) {
                        throw new SclParseException("Specified FCDA: " + objectReference + " in DataSet: "
                                + nameAttribute + " not found in Model.");
                    }
                    List<FcDataObject> fcDataObjects = ((LogicalNode) logicalNode).getChildren(fc);
                    for (FcDataObject dataObj : fcDataObjects) {
                        dsMembers.add(dataObj);
                    }

                }

            }

        }

        DataSet dataSet = new DataSet(lNode.getReference().toString() + '.' + name, dsMembers, false);
        return dataSet;
    }

    private List<Rcb> createReportControlBlocks(Node xmlNode, String parentRef) throws SclParseException {

        Fc fc = Fc.RP;
        NamedNodeMap rcbNodeAttributes = xmlNode.getAttributes();
        Node attribute = rcbNodeAttributes.getNamedItem("buffered");
        if (attribute != null && "true".equalsIgnoreCase(attribute.getNodeValue())) {
            fc = Fc.BR;
        }

        Node nameAttribute = rcbNodeAttributes.getNamedItem("name");
        if (nameAttribute == null) {
            throw new SclParseException("Report Control Block has no name attribute.");
        }

        ObjectReference reportObjRef = new ObjectReference(parentRef + "." + nameAttribute.getNodeValue());

        int maxInstances = 1;

        BdaTriggerConditions trigOps = new BdaTriggerConditions(new ObjectReference(reportObjRef + ".TrgOps"));
        BdaOptFlds optFields = new BdaOptFlds(new ObjectReference(reportObjRef + ".OptFlds"));
        for (int i = 0; i < xmlNode.getChildNodes().getLength(); i++) {
            Node childNode = xmlNode.getChildNodes().item(i);
            if (childNode.getNodeName().equals("TrgOps")) {

                NamedNodeMap attributes = childNode.getAttributes();

                if (attributes != null) {
                    for (int j = 0; j < attributes.getLength(); j++) {
                        Node node = attributes.item(j);
                        String nodeName = node.getNodeName();

                        if ("dchg".equals(nodeName)) {
                            trigOps.setDataChange(node.getNodeValue().equalsIgnoreCase("true"));
                        }
                        else if ("qchg".equals(nodeName)) {
                            trigOps.setQualityChange(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        else if ("dupd".equals(nodeName)) {
                            trigOps.setDataUpdate(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        else if ("period".equals(nodeName)) {
                            trigOps.setIntegrity(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        else if ("gi".equals(nodeName)) {
                            trigOps.setGeneralInterrogation(node.getNodeValue().equalsIgnoreCase("true"));
                        }
                    }
                }
            }
            else if ("OptFields".equals(childNode.getNodeName())) {

                NamedNodeMap attributes = childNode.getAttributes();

                if (attributes != null) {
                    for (int j = 0; j < attributes.getLength(); j++) {

                        Node node = attributes.item(j);
                        String nodeName = node.getNodeName();

                        if ("seqNum".equals(nodeName)) {
                            optFields.setSequenceNumber(node.getNodeValue().equalsIgnoreCase("true"));
                        }
                        else if ("timeStamp".equals(nodeName)) {
                            optFields.setReportTimestamp(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        else if ("reasonCode".equals(nodeName)) {
                            optFields.setReasonForInclusion(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        else if ("dataSet".equals(nodeName)) {
                            optFields.setDataSetName(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        // not supported for now
                        // else if (nodeName.equals("dataRef")) {
                        // optFields.setDataReference(node.getNodeValue().equals("true"));
                        //
                        // }
                        else if (nodeName.equals("bufOvfl")) {
                            optFields.setBufferOverflow(node.getNodeValue().equalsIgnoreCase("true"));

                        }
                        else if (nodeName.equals("entryID")) {
                            optFields.setEntryId(node.getNodeValue().equalsIgnoreCase("true"));
                        }
                        // not supported for now:
                        // else if (nodeName.equals("configRef")) {
                        // optFields.setConfigRevision(node.getNodeValue().equals("true"));
                        // }
                    }
                }
            }
            else if ("RptEnabled".equals(childNode.getNodeName())) {
                Node rptEnabledMaxAttr = childNode.getAttributes().getNamedItem("max");
                if (rptEnabledMaxAttr != null) {
                    maxInstances = Integer.parseInt(rptEnabledMaxAttr.getNodeValue());
                    if (maxInstances < 1 || maxInstances > 99) {
                        throw new SclParseException(
                                "Report Control Block max instances should be between 1 and 99 but is: "
                                        + maxInstances);
                    }
                }
            }
        }

        if (fc == Fc.RP) {
            optFields.setEntryId(false);
            optFields.setBufferOverflow(false);
        }

        List<FcModelNode> children = new ArrayList<>();

        BdaVisibleString rptId = new BdaVisibleString(new ObjectReference(reportObjRef.toString() + ".RptID"), fc, "",
                129, false, false);
        attribute = rcbNodeAttributes.getNamedItem("rptID");
        if (attribute != null) {
            rptId.setValue(attribute.getNodeValue().getBytes());
        }
        else {
            rptId.setValue(reportObjRef.toString());
        }

        children.add(rptId);

        children.add(new BdaBoolean(new ObjectReference(reportObjRef.toString() + ".RptEna"), fc, "", false, false));

        if (fc == Fc.RP) {
            children.add(new BdaBoolean(new ObjectReference(reportObjRef.toString() + ".Resv"), fc, "", false, false));
        }

        BdaVisibleString datSet = new BdaVisibleString(new ObjectReference(reportObjRef.toString() + ".DatSet"), fc, "",
                129, false, false);

        attribute = xmlNode.getAttributes().getNamedItem("datSet");
        if (attribute != null) {
            String nodeValue = attribute.getNodeValue();
            String dataSetName = parentRef + "$" + nodeValue;
            datSet.setValue(dataSetName.getBytes());
        }
        children.add(datSet);

        BdaInt32U confRef = new BdaInt32U(new ObjectReference(reportObjRef.toString() + ".ConfRev"), fc, "", false,
                false);
        attribute = xmlNode.getAttributes().getNamedItem("confRev");
        if (attribute == null) {
            throw new SclParseException("Report Control Block does not contain mandatory attribute confRev");
        }
        confRef.setValue(Long.parseLong(attribute.getNodeValue()));
        children.add(confRef);

        children.add(optFields);

        BdaInt32U bufTm = new BdaInt32U(new ObjectReference(reportObjRef.toString() + ".BufTm"), fc, "", false, false);
        attribute = xmlNode.getAttributes().getNamedItem("bufTime");
        if (attribute != null) {
            bufTm.setValue(Long.parseLong(attribute.getNodeValue()));
        }
        children.add(bufTm);

        children.add(new BdaInt8U(new ObjectReference(reportObjRef.toString() + ".SqNum"), fc, "", false, false));

        children.add(trigOps);

        BdaInt32U intgPd = new BdaInt32U(new ObjectReference(reportObjRef.toString() + ".IntgPd"), fc, "", false,
                false);
        attribute = xmlNode.getAttributes().getNamedItem("intgPd");
        if (attribute != null) {
            intgPd.setValue(Long.parseLong(attribute.getNodeValue()));
        }
        children.add(intgPd);

        children.add(new BdaBoolean(new ObjectReference(reportObjRef.toString() + ".GI"), fc, "", false, false));

        Rcb rcb = null;

        if (fc == Fc.BR) {

            children.add(
                    new BdaBoolean(new ObjectReference(reportObjRef.toString() + ".PurgeBuf"), fc, "", false, false));

            children.add(new BdaOctetString(new ObjectReference(reportObjRef.toString() + ".EntryID"), fc, "", 8, false,
                    false));

            children.add(new BdaEntryTime(new ObjectReference(reportObjRef.toString() + ".TimeOfEntry"), fc, "", false,
                    false));

            if (useResvTmsAttributes) {
                children.add(
                        new BdaInt16(new ObjectReference(reportObjRef.toString() + ".ResvTms"), fc, "", false, false));
            }

            rcb = new Brcb(reportObjRef, children);

        }
        else {

            rcb = new Urcb(reportObjRef, children);

        }

        // ignoring owner because it cannot be specified in SCL file

        return rcb.getInstanceList(maxInstances);

    }

    private List<FcDataObject> createFcDataObjects(String name, String parentRef, String doTypeID, Node doiNode)
            throws SclParseException {

        DoType doType = typeDefinitions.getDOType(doTypeID);

        if (doType == null) {
            throw new SclParseException("DO type " + doTypeID + " not defined!");
        }

        String ref = parentRef + '.' + name;

        List<ModelNode> childNodes = new ArrayList<>();

        for (Da dattr : doType.das) {

            // look for DAI node with the name of the DA
            Node iNodeFound = findINode(doiNode, dattr.getName());

            if (dattr.getCount() >= 1) {
                childNodes.add(createArrayOfDataAttributes(ref + '.' + dattr.getName(), dattr, iNodeFound));
            }
            else {
                childNodes.add(createDataAttribute(ref + '.' + dattr.getName(), dattr.getFc(), dattr, iNodeFound, false,
                        false, false));
            }

        }

        for (Sdo sdo : doType.sdos) {

            // parsing Arrays of SubDataObjects is ignored for now because no SCL file was found to test against. The
            // only DO that contains an Array of SDOs is Harmonic Value (HMV). The Kalkitech SCL Manager handles the
            // array of SDOs in HMV as an array of DAs.

            Node iNodeFound = findINode(doiNode, sdo.getName());

            childNodes.addAll(createFcDataObjects(sdo.getName(), ref, sdo.getType(), iNodeFound));

        }

        Map<Fc, List<FcModelNode>> subFCDataMap = new LinkedHashMap<>();

        for (Fc fc : Fc.values()) {
            subFCDataMap.put(fc, new LinkedList<FcModelNode>());
        }

        for (ModelNode childNode : childNodes) {
            subFCDataMap.get(((FcModelNode) childNode).getFc()).add((FcModelNode) childNode);
        }

        List<FcDataObject> fcDataObjects = new LinkedList<>();
        ObjectReference objectReference = new ObjectReference(ref);

        for (Fc fc : Fc.values()) {
            if (subFCDataMap.get(fc).size() > 0) {
                fcDataObjects.add(new FcDataObject(objectReference, fc, subFCDataMap.get(fc)));
            }
        }

        return fcDataObjects;
    }

    private Node findINode(Node iNode, String dattrName) {

        if (iNode == null) {
            return null;
        }

        for (int i = 0; i < iNode.getChildNodes().getLength(); i++) {
            Node childNode = iNode.getChildNodes().item(i);
            if (childNode.getAttributes() != null) {
                Node nameAttribute = childNode.getAttributes().getNamedItem("name");
                if (nameAttribute != null && nameAttribute.getNodeValue().equals(dattrName)) {
                    return childNode;
                }
            }
        }
        return null;
    }

    private Array createArrayOfDataAttributes(String ref, Da dataAttribute, Node iXmlNode) throws SclParseException {

        Fc fc = dataAttribute.getFc();
        int size = dataAttribute.getCount();

        List<FcModelNode> arrayItems = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            // TODO go down the iXmlNode using the ix attribute?
            arrayItems.add(createDataAttribute(ref + '(' + i + ')', fc, dataAttribute, iXmlNode, dataAttribute.isDchg(),
                    dataAttribute.isDupd(), dataAttribute.isQchg()));
        }

        return new Array(new ObjectReference(ref), fc, arrayItems);
    }

    /**
     * returns a ConstructedDataAttribute or BasicDataAttribute
     */
    private FcModelNode createDataAttribute(String ref, Fc fc, AbstractDataAttribute dattr, Node iXmlNode, boolean dchg,
            boolean dupd, boolean qchg) throws SclParseException {

        if (dattr instanceof Da) {
            Da dataAttribute = (Da) dattr;
            dchg = dataAttribute.isDchg();
            dupd = dataAttribute.isDupd();
            qchg = dataAttribute.isQchg();
        }

        String bType = dattr.getbType();

        if (bType.equals("Struct")) {
            DaType datype = typeDefinitions.getDaType(dattr.getType());

            if (datype == null) {
                throw new SclParseException("DAType " + dattr.getbType() + " not declared!");
            }

            List<FcModelNode> subDataAttributes = new ArrayList<>();
            for (Bda bda : datype.bdas) {

                Node iNodeFound = findINode(iXmlNode, bda.getName());

                subDataAttributes
                        .add(createDataAttribute(ref + '.' + bda.getName(), fc, bda, iNodeFound, dchg, dupd, qchg));
            }
            return new ConstructedDataAttribute(new ObjectReference(ref), fc, subDataAttributes);
        }

        String val = null;
        String sAddr = null;
        if (iXmlNode != null) {
            NamedNodeMap attributeMap = iXmlNode.getAttributes();
            Node sAddrAttribute = attributeMap.getNamedItem("sAddr");
            if (sAddrAttribute != null) {
                sAddr = sAddrAttribute.getNodeValue();
            }

            NodeList elements = iXmlNode.getChildNodes();
            for (int i = 0; i < elements.getLength(); i++) {
                Node node = elements.item(i);
                if (node.getNodeName().equals("Val")) {
                    val = node.getTextContent();
                }
            }
            if (val == null) {
                // insert value from DA element
                val = dattr.value;
            }
        }

        if (bType.equals("BOOLEAN")) {
            BdaBoolean bda = new BdaBoolean(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                if (val.equalsIgnoreCase("true") || val.equals("1")) {
                    bda.setValue(new Boolean(true));
                }
                else if (val.equalsIgnoreCase("false") || val.equals("0")) {
                    bda.setValue(new Boolean(false));
                }
                else {
                    throw new SclParseException("invalid boolean configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT8")) {
            BdaInt8 bda = new BdaInt8(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Byte.parseByte(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT8 configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT16")) {
            BdaInt16 bda = new BdaInt16(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Short.parseShort(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT16 configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT32")) {
            BdaInt32 bda = new BdaInt32(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Integer.parseInt(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT32 configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT64")) {
            BdaInt64 bda = new BdaInt64(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Long.parseLong(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT64 configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT8U")) {
            BdaInt8U bda = new BdaInt8U(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Short.parseShort(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT8U configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT16U")) {
            BdaInt16U bda = new BdaInt16U(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Integer.parseInt(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT16U configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("INT32U")) {
            BdaInt32U bda = new BdaInt32U(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setValue(Long.parseLong(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid INT32U configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("FLOAT32")) {
            BdaFloat32 bda = new BdaFloat32(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setFloat(Float.parseFloat(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid FLOAT32 configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.equals("FLOAT64")) {
            BdaFloat64 bda = new BdaFloat64(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                try {
                    bda.setDouble(Double.parseDouble(val));
                } catch (NumberFormatException e) {
                    throw new SclParseException("invalid FLOAT64 configured value: " + val);
                }
            }
            return bda;
        }
        else if (bType.startsWith("VisString")) {
            BdaVisibleString bda = new BdaVisibleString(new ObjectReference(ref), fc, sAddr,
                    Integer.parseInt(dattr.getbType().substring(9)), dchg, dupd);
            if (val != null) {
                bda.setValue(val.getBytes());
            }
            return bda;
        }
        else if (bType.startsWith("Unicode")) {
            BdaUnicodeString bda = new BdaUnicodeString(new ObjectReference(ref), fc, sAddr,
                    Integer.parseInt(dattr.getbType().substring(7)), dchg, dupd);
            if (val != null) {
                bda.setValue(val.getBytes());
            }
            return bda;
        }
        else if (bType.startsWith("Octet")) {
            BdaOctetString bda = new BdaOctetString(new ObjectReference(ref), fc, sAddr,
                    Integer.parseInt(dattr.getbType().substring(5)), dchg, dupd);
            if (val != null) {
                // TODO
                throw new SclParseException("parsing configured value for octet string is not supported yet.");
            }
            return bda;
        }
        else if (bType.equals("Quality")) {
            return new BdaQuality(new ObjectReference(ref), fc, sAddr, qchg);
        }
        else if (bType.equals("Check")) {
            return new BdaCheck(new ObjectReference(ref));
        }
        else if (bType.equals("Dbpos")) {
            return new BdaDoubleBitPos(new ObjectReference(ref), fc, sAddr, dchg, dupd);
        }
        else if (bType.equals("Tcmd")) {
            return new BdaTapCommand(new ObjectReference(ref), fc, sAddr, dchg, dupd);
        }
        else if (bType.equals("Timestamp")) {
            BdaTimestamp bda = new BdaTimestamp(new ObjectReference(ref), fc, sAddr, dchg, dupd);
            if (val != null) {
                // TODO
                throw new SclParseException("parsing configured value for TIMESTAMP is not supported yet.");
            }
            return bda;
        }
        else if (bType.equals("Enum")) {
            String type = dattr.getType();
            if (type == null) {
                throw new SclParseException("The exact type of the enumeration is not set.");
            }
            EnumType enumType = typeDefinitions.getEnumType(type);

            if (enumType == null) {
                throw new SclParseException("Definition of enum type: " + type + " not found.");
            }

            if (enumType.max > 127 || enumType.min < -128) {
                BdaInt16 bda = new BdaInt16(new ObjectReference(ref), fc, sAddr, dchg, dupd);
                if (val != null) {
                    for (EnumVal enumVal : enumType.getValues()) {
                        if (val.equals(enumVal.getId())) {
                            bda.setValue(new Short((short) enumVal.getOrd()));
                            return bda;
                        }
                    }
                    throw new SclParseException("unknown enum value: " + val);
                }
                return bda;
            }
            else {
                BdaInt8 bda = new BdaInt8(new ObjectReference(ref), fc, sAddr, dchg, dupd);
                if (val != null) {
                    for (EnumVal enumVal : enumType.getValues()) {
                        if (val.equals(enumVal.getId())) {
                            bda.setValue(new Byte((byte) enumVal.getOrd()));
                            return bda;
                        }
                    }
                    throw new SclParseException("unknown enum value: " + val);
                }
                return bda;
            }
        }
        else if (bType.equals("ObjRef")) {
            BdaVisibleString bda = new BdaVisibleString(new ObjectReference(ref), fc, sAddr, 129, dchg, dupd);
            if (val != null) {
                bda.setValue(val.getBytes());
            }
            return bda;
        }
        else {
            throw new SclParseException("Invalid bType: " + bType);
        }

    }
}
