using NLog;
using Opc.Ua;
using Opc.Ua.Client;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IOB_WIN_OPC_UA
{
    /// <summary>
    /// Evento per incapsulare dati x refresh pagina
    /// </summary>
    public class opcUaMonitItemChange : EventArgs
    {
        #region Public Constructors

        /// <summary>
        /// salvataggio obj
        /// </summary>
        /// <param name="newObject"></param>
        public opcUaMonitItemChange(MonitoredItem monitoredItem, MonitoredItemNotification notification)
        {
            _monitoredItem = monitoredItem;
            _notification = notification;
        }

        #endregion Public Constructors

        #region Public Properties

        /// <summary>
        /// Proprietà lettura del MonitoredItem
        /// </summary>
        public MonitoredItem CurrMonitoredItem
        {
            get { return _monitoredItem; }
        }

        /// <summary>
        /// Proprietà lettura della notifica
        /// </summary>
        public MonitoredItemNotification CurrNotify
        {
            get { return _notification; }
        }

        #endregion Public Properties

        #region Private Fields

        /// <summary>
        /// Monitored Item da notificare
        /// </summary>
        private readonly MonitoredItem _monitoredItem;

        /// <summary>
        /// Valore notifica
        /// </summary>
        private readonly MonitoredItemNotification _notification;

        #endregion Private Fields
    }

    /// <summary>
    /// OPC UA Client with examples of basic functionality.
    /// </summary>
    public class UAClient
    {
        #region Public Constructors

        /// <summary>
        /// Initializes a new instance of the UAClient class.
        /// </summary>
        /// <param name="configuration"></param>
        /// <param name="codIOB"></param>
        /// <param name="user"></param>
        /// <param name="pwd"></param>
        /// <param name="verboseLog"></param>
        /// <param name="validateResponse"></param>
        /// <param name="maxNullRead">Numero massimo di risposte nulle in browsing prima di disconnettersi</param>
        public UAClient(ApplicationConfiguration configuration, string codIOB, string user, string pwd, bool verboseLog, Action<IList, IList> validateResponse, int maxNullRead)
        {
            currNullReceiv = 0;
            maxNullAllowed = maxNullRead;
            m_validateResponse = validateResponse;
            currIob = codIOB;
            lg = LogManager.GetCurrentClassLogger();
            if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(pwd))
            {
                CurrUserIdentity = new UserIdentity(user, pwd);
            }
            else
            {
                CurrUserIdentity = new UserIdentity();
            }
            isLogVerbose = verboseLog;
            m_configuration = configuration;
            m_configuration.CertificateValidator.CertificateValidation += CertificateValidation;
        }

        #endregion Public Constructors

        #region Public Events

        /// <summary>
        /// Evento notifica variazione MonitoredItem
        /// </summary>
        public event EventHandler<opcUaMonitItemChange> eh_MonItChange;

        /// <summary>
        /// Evento notifica superamento numero letture null
        /// </summary>
        public event EventHandler<int> eh_nullExceed;

        #endregion Public Events

        #region Public Properties

        /// <summary>
        /// The user identity to use when creating the session.
        /// </summary>
        public IUserIdentity CurrUserIdentity { get; set; } = new UserIdentity();

        /// <summary>
        /// Gets or sets the server URL.
        /// </summary>
        public string ServerUrl { get; set; } = "opc.tcp://localhost:4840";

        /// <summary>
        /// Gets the client session.
        /// </summary>
        public Session Session => m_session;

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// Browse Server nodes
        /// </summary>
        public bool Browse(ushort startNodeNS, uint startNodeVal, List<string> vetoBrowse, ref Dictionary<string, string> nodeIdNameList)
        {
            bool fatto = false;
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return false;
            }

            try
            {
                // Create a Browser object
                Browser browser = new Browser(m_session);

                // Set browse parameters
                browser.BrowseDirection = BrowseDirection.Forward;
                browser.NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable;
                browser.ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences;

                //NodeId nodeToBrowse = ObjectIds.Server;
                //NodeId nodeToBrowse = new NodeId("ns=4,i=5001");
                NodeId nodeToBrowse = new NodeId(startNodeVal, startNodeNS);

                // Call Browse service
                lgTrace($"Browsing {nodeToBrowse} node...");
                ReferenceDescriptionCollection browseResults = browser.Browse(nodeToBrowse);

                // Display the results
                lgTrace($"Browse returned {browseResults.Count} results:");

                foreach (ReferenceDescription result in browseResults)
                {
                    lgTrace($"     NodeId = {result.NodeId}, TypeId = {result.TypeId}, DisplayName = {result.DisplayName.Text}, NodeClass = {result.NodeClass}, Others:  {result.BinaryEncodingId} | {result.BrowseName}");

                    // se NON fa parte dell'elenco dei VETO di filterItems...
                    if (!vetoBrowse.Contains($"{result.NodeId}"))
                    {
                        // se mancasse aggiungo...
                        if (!nodeIdNameList.ContainsKey($"{result.NodeId}"))
                        {
                            nodeIdNameList.Add(result.NodeId.ToString(), result.DisplayName.Text);
                        }
                    }
                }
                fatto = true;
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Browse Error : {ex.Message}");
            }

            return fatto;
        }

        /// <summary>
        /// Browse Server nodes
        /// </summary>
        public bool Browse(string browsePath, List<string> vetoBrowse, ref Dictionary<string, string> nodeIdNameList)
        {
            bool fatto = false;
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return false;
            }

            try
            {
                // Create a Browser object
                Browser browser = new Browser(m_session);

                // Set browse parameters
                browser.BrowseDirection = BrowseDirection.Forward;
                browser.NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable;
                browser.ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences;

                //NodeId nodeToBrowse = ObjectIds.Server;
                //NodeId nodeToBrowse = new NodeId("ns=4;i=5001");
                NodeId nodeToBrowse = new NodeId(browsePath);
                //nodeToBrowse = ObjectIds.Server;
                //nodeToBrowse = new NodeId("Calibratrice_L1", 4);
                //nodeToBrowse = new NodeId("Dati_Mes", 4);
                //nodeToBrowse = new NodeId(5001, 2);
                //nodeToBrowse = new NodeId("ns=4;s=NxController");
                //nodeToBrowse = new NodeId("ns=4;s=Dati_Mes");
                //nodeToBrowse = new NodeId("NxController.GlobalVars", 4);
                //nodeToBrowse = new NodeId("Dati_Mes", 4);

                // Call Browse service
                lgTrace($"Browsing {nodeToBrowse} node...");
                ReferenceDescriptionCollection browseResults = browser.Browse(nodeToBrowse);

                // Display the results
                lgTrace($"Browse returned {browseResults.Count} results:");

                foreach (ReferenceDescription result in browseResults)
                {
                    // se veto --> loggo veto
                    if (vetoBrowse.Contains($"{result.NodeId}"))
                    {
                        lgDebug($"FILTER -->  NodeId = {result.NodeId}, DisplayName = {result.DisplayName.Text}, NodeClass = {result.NodeClass}, Others:  {result.BinaryEncodingId} | {result.BrowseName}");
                    }
                    // se NON fa parte dell'elenco dei VETO di filterItems...
                    else
                    {
                        lgDebug($"     NodeId = {result.NodeId}, DisplayName = {result.DisplayName.Text}, NodeClass = {result.NodeClass}, Others:  {result.BinaryEncodingId} | {result.BrowseName}");
                        // se mancasse aggiungo...
                        if (!nodeIdNameList.ContainsKey($"{result.NodeId}"))
                        {
                            nodeIdNameList.Add($"{result.NodeId}", result.DisplayName.Text);
                            // se è un nodo object --> faccio sub browse!
                            if (result.NodeClass != NodeClass.Variable)
                            {
                                this.Browse($"{result.NodeId}", vetoBrowse, ref nodeIdNameList);
                            }
                        }
                    }
                }
                fatto = true;
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Browse Error : {ex.Message}");
            }

            return fatto;
        }

        /// <summary>
        /// Call UA method
        /// </summary>
        public void CallMethod()
        {
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return;
            }

            try
            {
                // Define the UA Method to call Parent node - Objects\CTT\Methods Method node - Objects\CTT\Methods\Add
                NodeId objectId = new NodeId("ns=2;s=Methods");
                NodeId methodId = new NodeId("ns=2;s=Methods_Add");

                // Define the method parameters Input argument requires a Float and an UInt32 value
                object[] inputArguments = new object[] { (float)10.5, (uint)10 };
                IList<object> outputArguments = null;

                // Invoke Call service
                lgDebug($"Calling UAMethod for node {methodId} ...");
                outputArguments = m_session.Call(objectId, methodId, inputArguments);

                // Display results
                lgDebug($"Method call returned {outputArguments.Count} output argument(s):");

                foreach (var outputArgument in outputArguments)
                {
                    lgDebug($"     OutputValue = {outputArgument}");
                }
            }
            catch (Exception ex)
            {
                lgError($"Method call error: {ex.Message}");
            }
        }

        /// <summary>
        /// Creates a session with the UA server
        /// </summary>
        public async Task<bool> ConnectAsync()
        {
            try
            {
                if (m_session != null && m_session.Connected == true)
                {
                    lgInfo("Session already connected!");
                }
                else
                {
                    lgInfo("Connecting...");

                    // Get the endpoint by connecting to server's discovery endpoint. Try to find
                    // the first endopint without security.
                    EndpointDescription endpointDescription = CoreClientUtils.SelectEndpoint(ServerUrl, false);

                    EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
                    ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);

                    // Create the session
                    Session session = await Session.Create(
                        m_configuration,
                        endpoint,
                        true,
                        false,
                        m_configuration.ApplicationName,
                        60 * 60 * 1000,
                        CurrUserIdentity,
                        null
                    );

#if false
                    Session session = await Session.Create(
                                    m_configuration,
                                    endpoint,
                                    false,
                                    false,
                                    m_configuration.ApplicationName,
                                    60 * 60 * 1000,
                                    CurrUserIdentity,
                                    null
                                );
#endif

                    // Assign the created session
                    if (session != null && session.Connected)
                    {
                        m_session = session;
                    }

                    // Session created successfully.
                    lgInfo($"New Session Created with SessionName = {m_session.SessionName}");
                }

                return true;
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Create Session Error : {ex.Message}");
                return false;
            }
        }

        /// <summary>
        /// Disconnects the session.
        /// </summary>
        public void Disconnect()
        {
            try
            {
                if (m_session != null)
                {
                    lgInfo("Disconnecting...");

                    m_session.Close();
                    m_session.Dispose();
                    m_session = null;

                    // Log Session Disconnected event
                    lgInfo("Session Disconnected.");
                }
                else
                {
                    lgError("Session not created!");
                }
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Disconnect Error : {ex.Message}");
            }
        }

        /// <summary>
        /// Reads a node by node Id
        /// </summary>
        /// <param name="nodeIdString">The node Id as string</param>
        /// <returns>The read node</returns>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        public Node ReadNode(String nodeIdString)
        {
            //Create a nodeId using the identifier string
            NodeId nodeId = new NodeId(nodeIdString);
            //Create a node
            Node node = new Node();
            //Read the dataValue
            node = m_session.ReadNode(nodeId);
            return node;
        }

        /// <summary>Reads a node by node Id</summary>
        /// <param name="nodeIdString">The node Id as string</param>
        /// <returns>A dictionary containing the attribute values of the node using the attribute uint identifier as key;
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        public Dictionary<uint, DataValue> ReadNodeAttributes(String nodeIdString)
        {
            //Create a nodeId using the identifier string
            NodeId nodeId = new NodeId(nodeIdString);
            return ReadNodeAttributes(nodeId);
        }

        /// <summary>Reads a node by node Id</summary>
        /// <param name="nodeId">The node Id</param>
        /// <returns>A dictionary containing the attribute values of the node using the attribute uint identifier as key;
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        public Dictionary<uint, DataValue> ReadNodeAttributes(NodeId nodeId)
        {
            Dictionary<int, uint> attributeDictionary = new Dictionary<int, uint>()
            {
                {1, Attributes.NodeId },
                {2, Attributes.NodeClass },
                {3, Attributes.BrowseName },
                {4, Attributes.DisplayName },
                {5, Attributes.Value },
                {6, Attributes.DataType},
                {7, Attributes.ValueRank},
                {8, Attributes.ArrayDimensions},
                {9, Attributes.AccessLevel }
            };

            //Create a read value id collection to store the nodes to read
            ReadValueIdCollection nodesToRead = new ReadValueIdCollection();

            //Create a StatusCodeCollection
            DataValueCollection dataValueCollection = new DataValueCollection();

            //Create a DiagnosticInfoCollection
            DiagnosticInfoCollection diag = new DiagnosticInfoCollection();

            //Go through the attribute dictionary and create the nodes accordingly
            for (int i = 0; i < attributeDictionary.Count; i++)
            {
                ///Create a read value id with the nessessary attribute
                ReadValueId nodeToRead = new ReadValueId();
                nodeToRead.NodeId = nodeId;
                nodeToRead.AttributeId = attributeDictionary[i + 1];
                nodesToRead.Add(nodeToRead);
            }
            //Read the read value id collection
            m_session.Read(null, 0, TimestampsToReturn.Neither, nodesToRead, out dataValueCollection, out diag);
            //Create a dictionary to store the attribute values
            Dictionary<uint, DataValue> attributeValues = new Dictionary<uint, DataValue>();
            //Go through the data value collection to store every data value according to its attribute identifier
            int counter = 1;
            foreach (DataValue dataValue in dataValueCollection)
            {
                attributeValues.Add(attributeDictionary[counter], dataValue);
                counter += 1;
            }
            return attributeValues;
        }

        /// <summary>
        /// Read a SINGLE of nodes value (RAW) from Server
        /// </summary>
        /// <param name="reqNodeId"></param>
        /// <returns></returns>
        public object ReadNodeRaw(NodeId reqNodeId)
        {
            object answ = null;
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                numErrors++;
                return answ;
            }

            try
            {
                #region Read the Value attribute of a node by calling the Session.ReadValue method

                try
                {
                    DataValue resp = m_session.ReadValue(reqNodeId);
                    answ = resp;
                }
                catch (Exception exc)
                {
                    // Log Error
                    lgError($"ReadNodeRaw Error 01: {Environment.NewLine}{exc}");
                    numErrors++;

                    // provare disconnect?!?
                    if (numErrors >= maxErrors)
                    {
                        numErrors = 0;
                        Disconnect();
                    }
                }

                #endregion Read the Value attribute of a node by calling the Session.ReadValue method
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"ReadNodeRaw Error 02: {ex.Message}.");
            }
            return answ;
        }

        /// <summary>
        /// Read a list of nodes from Server
        /// </summary>
        public void ReadNodes()
        {
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return;
            }

            try
            {
                #region Read a node by calling the Read Service

                // build a list of nodes to be read
                ReadValueIdCollection nodesToRead = new ReadValueIdCollection()
                {
                    // Value of ServerStatus
                    new ReadValueId() { NodeId = Variables.Server_ServerStatus, AttributeId = Attributes.Value },
                    // BrowseName of ServerStatus_StartTime
                    new ReadValueId() { NodeId = Variables.Server_ServerStatus_StartTime, AttributeId = Attributes.BrowseName },
                    // Value of ServerStatus_StartTime
                    new ReadValueId() { NodeId = Variables.Server_ServerStatus_StartTime, AttributeId = Attributes.Value }
                };

                // Read the node attributes
                lgInfo("Reading nodes...");

                // Call Read Service
                m_session.Read(
                    null,
                    0,
                    TimestampsToReturn.Both,
                    nodesToRead,
                    out DataValueCollection resultsValues,
                    out DiagnosticInfoCollection diagnosticInfos);

                // Validate the results
                m_validateResponse(resultsValues, nodesToRead);

                // Display the results.
                foreach (DataValue result in resultsValues)
                {
                    lgTrace($"Read Value = {result.Value} , StatusCode = {result.StatusCode}");
                }

                #endregion Read a node by calling the Read Service

                #region Read the Value attribute of a node by calling the Session.ReadValue method

                // Read Server NamespaceArray
                lgTrace("Reading Value of NamespaceArray node...");
                DataValue namespaceArray = m_session.ReadValue(Variables.Server_NamespaceArray);
                // Display the result
                lgTrace($"NamespaceArray Value = {namespaceArray}");

                #endregion Read the Value attribute of a node by calling the Session.ReadValue method
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Read Nodes Error : {ex.Message}.");
            }
        }

        /// <summary>
        /// Read a SINGLE of nodes value from Server
        /// </summary>
        /// <param name="reqNodeId"></param>
        /// <returns>Value as String</returns>
        public string ReadNodeString(NodeId reqNodeId)
        {
            string answ = "";
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return answ;
            }

            try
            {
                try
                {
                    DataValue resp = m_session.ReadValue(reqNodeId);
                    answ = $"{resp.Value}";
                    // decremento errore...
                    if (numErrors > 0)
                    {
                        numErrors--;
                    }
                }
                catch (Exception exc)
                {
                    // Log Error
                    numErrors++;
                    lgTrace($"ReadValue Error | reqNodeId: {reqNodeId} | numErrors: {numErrors}{Environment.NewLine}{exc}");
                }
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Read Nodes Error : {ex.Message}.");
                numErrors++;
            }
            // se supero maxErrors --> invio eccezione e resetto...
            if (numErrors >= maxErrors)
            {
                // reset + exception
                numErrors = 0;
                throw new Exception($"Errore lettura OPC-UA: {maxErrors} errori");
            }
            return answ;
        }

        /// <summary>Reads a struct or UDT by node Id</summary>
        /// <param name="nodeIdString">The node Id as strings</param>
        /// <returns>The read struct/UDT elements as a list of string[3]; string[0] = tag name, string[1] = value, string[2] = opc data type</returns>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        public List<string[]> ReadStructUdt(String nodeIdString)
        {
            //Define result list to return var name and var value
            List<string[]> resultStringList = new List<string[]>();

            //Read the node attributes
            Dictionary<uint, DataValue> nodeAttributes = ReadNodeAttributes(nodeIdString);
            //Create a data value storing the value
            DataValue data = nodeAttributes[Attributes.Value];

            //Check if the entered node id is of type array and accordingly get the byte array of the data values
            ExtensionObject dataValue = new ExtensionObject();
            List<byte[]> byteArrays = new List<byte[]>();
            if (data.Value is ExtensionObject[])
            {
                foreach (ExtensionObject temp in (ExtensionObject[])data.Value)
                {
                    byteArrays.Add((byte[])temp.Body);
                }
            }
            else if (data.Value is ExtensionObject)
            {
                dataValue = (ExtensionObject)data.Value;
                byteArrays.Add((byte[])dataValue.Body);
            }
            else
            {
                throw new Exception("The entered node id is not of tpye struct/udt.");
            }

            //Get the type dictionary of the struct as a list of string[3]; string[0] = tag name, string[1] = data type, string [2] = array dimensions
            List<string[]> structureDictionary = new List<string[]>();
            DataTypeNode dataTypeNode = (DataTypeNode)ReadNode(nodeAttributes[Attributes.DataType].ToString());
            GetTypeDictionary(dataTypeNode, m_session, structureDictionary);

            //Get the deserialized byte string as a list of string[4]; string[0]=array index; string[1]=tag name; string[2]=tag value; string[3]=tag data type
            resultStringList = ParseDataToTagsFromDictionary(structureDictionary, byteArrays);

            //return result as List<string[]> string[0]=array index; string[1]=tag name; string[2]=tag value; string[3]=tag data type
            return resultStringList;
        }

        /// <summary>
        /// Create Subscription and MonitoredItems for DataChanges
        /// </summary>
        public List<MonitoredItem> SubscribeToDataChanges(Dictionary<string, string> DataList)
        {
            List<MonitoredItem> monItList = new List<MonitoredItem>();
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return monItList;
            }

            try
            {
                // Create a subscription for receiving data change notifications

                // Define Subscription parameters
                Subscription subscription = new Subscription(m_session.DefaultSubscription);

                subscription.DisplayName = "Steamware IOB-WIN Subscription";
                subscription.PublishingEnabled = true;
                subscription.PublishingInterval = 1000;

                m_session.AddSubscription(subscription);

                // Create the subscription on Server side
                subscription.Create();
                lgInfo($"New Subscription created with SubscriptionId = {subscription.Id}");

                // Create MonitoredItems for data changes
                foreach (var item in DataList)
                {
                    MonitoredItem currMonIt = new MonitoredItem(subscription.DefaultItem);
                    // Int32 Node - Objects\CTT\Scalar\Simulation\Int32
                    currMonIt.StartNodeId = new NodeId(item.Key);
                    currMonIt.AttributeId = Attributes.Value;
                    currMonIt.DisplayName = item.Value;
                    currMonIt.SamplingInterval = 1000;
                    currMonIt.Notification += OnMonitoredItemNotification;
                    subscription.AddItem(currMonIt);
                    monItList.Add(currMonIt);
                }

                // Create the monitored items on Server side
                subscription.ApplyChanges();
                lgInfo($"MonitoredItems created for SubscriptionId = {subscription.Id}");
            }
            catch (Exception ex)
            {
                lgError($"Subscribe error: {ex.Message}");
            }
            return monItList;
        }

        /// <summary>
        /// Write a list of nodes to the Server
        /// </summary>
        /// <param name="node2Write"></param>
        public void WriteNodes(List<WriteValue> node2Write)
        {
            if (node2Write != null)
            {
                if (m_session == null || m_session.Connected == false)
                {
                    lgError("Session not connected!");
                    return;
                }

                try
                {
                    // Write the configured nodes
                    WriteValueCollection nodesToWrite = new WriteValueCollection();
                    nodesToWrite.AddRange(node2Write);

                    // Write the node attributes
                    StatusCodeCollection results = null;
                    DiagnosticInfoCollection diagnosticInfos;
                    lgDebug("Writing nodes...");

                    // Call Write Service
                    m_session.Write(null,
                                    nodesToWrite,
                                    out results,
                                    out diagnosticInfos);

                    // Validate the response
                    m_validateResponse(results, nodesToWrite);

                    // Display the results.
                    lgDebug("Write Results :");

                    int idx = 0;
                    foreach (StatusCode writeResult in results)
                    {
                        lgDebug($"     {writeResult}");
                        if (!StatusCode.IsGood(writeResult))
                        {
                            lgError($"Error | {nodesToWrite[idx].NodeId} | {nodesToWrite[idx].Value} | {writeResult}");
                        }
                        idx++;
                    }
                }
                catch (Exception ex)
                {
                    // Log Error
                    lgError($"Write Nodes Error : {ex.Message}");
                    numErrors++;

                    // provare disconnect?!?
                    if (numErrors >= maxErrors)
                    {
                        numErrors = 0;
                        Disconnect();
                    }
                }
            }
        }

        /// <summary>
        /// Metodo scrittura alternativa in formato UDT dei dati...
        /// </summary>
        /// <param name="node2Write"></param>
        public void WriteNodesUdt(string nodeIdString, Dictionary<string, string> data2write)
        {
            if (!string.IsNullOrEmpty(nodeIdString) && data2write != null && data2write.Count > 0)
            {
                if (m_session == null || m_session.Connected == false)
                {
                    lgError("Session not connected!");
                    return;
                }

                try
                {
                    var listUdtRows = new List<string[]>();

                    listUdtRows = new List<string[]>();
                    // leggo elenco dati in primis
                    listUdtRows = ReadStructUdt(nodeIdString);
                    // va scartata ultima riga!!!!
                    listUdtRows.RemoveAt(listUdtRows.Count - 1);

                    // sostituzione dei dati: per ogni valore corrente cerco nei valori data2write
                    foreach (var item in listUdtRows)
                    {
                        // se trovo valore da sostituire
                        if (data2write.ContainsKey(item[1]))
                        {
                            item[2] = data2write[item[1]];
                        }
                    }

                    // scrivo
                    WriteStructUdt(nodeIdString, listUdtRows);
                }
                catch (Exception ex)
                {
                    // Log Error
                    lgError($"Error WriteNodesUdt for {nodeIdString}: {Environment.NewLine}{ex.Message}");
                    numErrors++;

                    // provare disconnect?!?
                    if (numErrors >= maxErrors)
                    {
                        numErrors = 0;
                        Disconnect();
                    }
                }
            }
        }

        /// <summary>
        /// Writes values to node Ids
        /// </summary>
        /// <param name="valuesByNodeId">Mulitple NodeIds with there values (multiple possible if not a scalar).</param>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        public void WriteValues(Dictionary<NodeId, IEnumerable<string>> valuesByNodeId)
        {
            //Create a collection of values to write
            WriteValueCollection valuesToWrite = new WriteValueCollection();

            foreach (var keyValuePair in valuesByNodeId)
            {
                NodeId nodeId = keyValuePair.Key;
                IEnumerable<string> values = keyValuePair.Value;

                //Get the OPC UA data type and the Rank
                Node node = m_session.ReadNode(nodeId);
                VariableNode variableNode = (VariableNode)node.DataLock;
                NodeId datatypeId = variableNode.DataType;
                BuiltInType targetype = TypeInfo.GetBuiltInType(datatypeId);

                //Ensure that target type is not null by reading the node of the data type id
                if (targetype == BuiltInType.Null)
                {
                    try
                    {
                        //Get the node id of the parent base data type
                        NodeId tempNodeId = GetParentDataType(datatypeId, m_session);
                        targetype = TypeInfo.GetBuiltInType(tempNodeId);
                        if (targetype == BuiltInType.Null)//Ensure that the entered node id is not of type struct
                        {
                            throw new Exception("The entered node id may be of type struct. Please use the methods for read/write structs.");
                        }
                    }
                    catch
                    {
                        throw new Exception("The node id data type is not castable. Please check the data type.");
                    }

                }
                int valueRank = variableNode.ValueRank;
                DataValue dataValue = null;

                //Check if there is a value entered
                if (!values.Any() || nodeId == null) //no value in the textbox
                {
                    throw new Exception("There is no NodeId or value entered.");
                }

                //Ensure that boolean entries have lower case initial and float, double and long double have the rigth format
                List<string> tempValues = new List<string>();
                if (targetype == BuiltInType.Boolean)
                {
                    foreach (string tempValue in values)
                    {
                        tempValues.Add(tempValue.ToLower());
                    }
                    values = tempValues;
                }
                if (targetype == BuiltInType.Float || targetype == BuiltInType.Double)
                {
                    foreach (string tempValue in values)
                    {
                        tempValues.Add(tempValue.Replace(',', '.'));
                    }
                    values = tempValues;
                }

                //Check if the inputed value has the type matrix or array
                if (valueRank >= ValueRanks.OneDimension)
                {
                    //Cast values in the target type
                    Array castValues = TypeInfo.CastArray(values.ToArray(), BuiltInType.Null, targetype, (object source, BuiltInType srcType, BuiltInType dstType) => TypeInfo.Cast(source, dstType));

                    if (valueRank == ValueRanks.OneDimension) //Check if inputed value is array
                    {
                        dataValue = new DataValue(new Variant(castValues, new TypeInfo(targetype, valueRank)));
                    }
                    else //Inputed value has more then one dimension
                    {
                        //Get matrix dimensions
                        int[] dimensions = new int[valueRank];
                        for (int i = 0; i < valueRank; i++)
                        {
                            dimensions[i] = (int)variableNode.ArrayDimensions[i];
                        }
                        Matrix matrix = new Matrix(castValues, targetype, dimensions);
                        dataValue = new DataValue(new Variant(matrix));
                    }
                }
                else
                {
                    try //OPC UA data type with type scalar
                    {
                        dataValue = new DataValue(new Variant(TypeInfo.Cast(values.First(), targetype)));
                    }
                    catch //no OPC UA data type
                    {
                        throw new FormatException($"The value inputed [{values.First()}] is not convertable to type {targetype}.");
                    }
                }
                //Create a WriteValue using the NodeId, dataValue and attributeType
                WriteValue valueToWrite = new WriteValue()
                {
                    Value = dataValue,
                    NodeId = nodeId,
                    AttributeId = Attributes.Value
                };

                //Add the dataValues to the collection
                valuesToWrite.Add(valueToWrite);
            }

            StatusCodeCollection result;
            //Write the collection to the server
            m_session.Write(null, valuesToWrite, out result, out _);

            // Validate the response
            m_validateResponse(result, valuesToWrite);

            // Display the results.
            lgDebug("OPC-UA: Write Results :");
            int idx = 0;
            foreach (StatusCode writeResult in result)
            {
                if (!StatusCode.IsGood(writeResult))
                {
                    lgError($"Error | {valuesToWrite[idx].NodeId} | {valuesToWrite[idx].Value} | {writeResult}");
                }
                else
                {
                    lgDebug($"{valuesToWrite[idx].NodeId} | {valuesToWrite[idx].Value} | {writeResult}");
                }
                idx++;
            }
        }

        /// <summary>
        /// Write a list of nodes to the Server
        /// </summary>
        public void WriteSingleNode(WriteValue node2Write)
        {
            if (node2Write != null)
            {
                if (m_session == null || m_session.Connected == false)
                {
                    lgError("Session not connected!");
                    return;
                }

                try
                {
                    // Write the configured nodes
                    WriteValueCollection nodesToWrite = new WriteValueCollection();
                    nodesToWrite.Add(node2Write);

                    // Write the node attributes
                    StatusCodeCollection results = null;
                    DiagnosticInfoCollection diagnosticInfos;
                    lgDebug("Writing nodes...");

                    // Call Write Service
                    m_session.Write(null,
                                    nodesToWrite,
                                    out results,
                                    out diagnosticInfos);

                    // Validate the response
                    m_validateResponse(results, nodesToWrite);

                    // Display the results.
                    lgDebug("Write Results :");

                    foreach (StatusCode writeResult in results)
                    {
                        lgDebug($"     {writeResult}");
                    }
                }
                catch (Exception ex)
                {
                    // Log Error
                    lgError($"Write Nodes Error : {ex.Message}");
                }
            }
        }

        /// <summary>Writes data to a struct or UDT by node Id</summary>
        /// <param name="nodeIdString">The node Id as strings</param>
        /// <param name="dataToWrite">The data to write as string[3]; string[0] = tag name, string[1] = value, string[2] = opc data type</param>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        public void WriteStructUdt(String nodeIdString, List<string[]> dataToWrite)
        {
            //Create a NodeId from the NodeIdString
            NodeId nodeId = new NodeId(nodeIdString);

            //Creat a WriteValueColelction
            WriteValueCollection valuesToWrite = new WriteValueCollection();

            //Create a WriteValue
            WriteValue writevalue = new WriteValue();

            //Read the node attributes
            Dictionary<uint, DataValue> nodeAttributes = ReadNodeAttributes(nodeIdString);
            //Create a data value storing the value
            DataValue data = nodeAttributes[Attributes.Value];

            //Check if the entered node id is of type array and accordingly get the byte array(s) of the data values
            ExtensionObject extensionObject = new ExtensionObject();
            List<byte[]> byteArrays = new List<byte[]>();
            if (data.Value is ExtensionObject[])
            {
                foreach (ExtensionObject temp in (ExtensionObject[])data.Value)
                {
                    byteArrays.Add((byte[])temp.Body);
                }
            }
            else
            {
                extensionObject = (ExtensionObject)data.Value;
                byteArrays.Add((byte[])extensionObject.Body);
            }

            //Get the type dictionary of the struct as a list of string[3]; string[0] = tag name, string[1] = data type, string [2] = array dimensions
            List<string[]> structureDictionary = new List<string[]>();
            DataTypeNode dataTypeNode = (DataTypeNode)ReadNode(nodeAttributes[Attributes.DataType].ToString());
            GetTypeDictionary(dataTypeNode, m_session, structureDictionary);

            //Delete empty list elements in the structure dictionary and in the list of the gui
            for (int j = 0; j < structureDictionary.Count; j++)
            {
                if (structureDictionary[j][1] == "")//Check if the data type entry is empty as this is an indicator for a struct/udt in a struct/udt
                {
                    structureDictionary.RemoveAt(j);
                    //dataToWrite.RemoveAt(j);
                }
            }

            //Create a byte array
            List<byte[]> bytesToWrite;

            //Parse dataToWrite to the byte array
            bytesToWrite = ParseDataToByteArray(dataToWrite, structureDictionary);

            //Create a StatusCodeCollection
            StatusCodeCollection results = new StatusCodeCollection();

            //Create a DiagnosticInfoCollection
            DiagnosticInfoCollection diag = new DiagnosticInfoCollection();

            //Get the structure definition
            StructureDefinition structureDefinition = (StructureDefinition)dataTypeNode.DataTypeDefinition.Body;

            //Create an ExtensionObject from the Structure given to this function
            ExtensionObject writeExtObj = new ExtensionObject();

            //Copy the encoding node id to the extension object type id
            writeExtObj.TypeId = structureDefinition.DefaultEncodingId;

            //Copy data to extension object body
            DataValue dataValue = null;

            if (bytesToWrite.Count == 1)
            {
                //Copy data to extension object body
                writeExtObj.Body = bytesToWrite[0];
                //Turn the created ExtensionObject into a DataValue
                dataValue = new DataValue(writeExtObj);
            }
            else
            {
                ExtensionObject[] writeExteObjArr = new ExtensionObject[bytesToWrite.Count];
                for (int i = 0; i < bytesToWrite.Count; i++)
                {
                    //Create an ExtensionObject from the Structure given to this function
                    writeExtObj = new ExtensionObject();
                    //Copy data to extension object body
                    writeExtObj.TypeId = structureDefinition.DefaultEncodingId;
                    writeExtObj.Body = bytesToWrite[i];
                    //Copy extension object to extension object array
                    writeExteObjArr[i] = writeExtObj;
                }
                //Turn the created ExtensionObject[] into a DataValue
                dataValue = new DataValue(writeExteObjArr);
            }
            //Setup for the WriteValue
            writevalue.NodeId = nodeId;
            writevalue.Value = dataValue;
            writevalue.AttributeId = Attributes.Value;
            //Add the created value to the collection
            valuesToWrite.Add(writevalue);

            try
            {
                m_session.Write(null, valuesToWrite, out results, out diag);
            }
            catch (Exception e)
            {
                //Handle Exception here
                throw e;
            }

            //Check result codes
            foreach (StatusCode result in results)
            {
                if (result.ToString() != "Good")
                {
                    Exception e = new Exception(result.ToString());
                    throw e;
                }
            }
        }

        /// <summary>
        /// Write a list of nodes to the Server
        /// </summary>
        public void WriteTestNodes()
        {
            if (m_session == null || m_session.Connected == false)
            {
                lgError("Session not connected!");
                return;
            }

            try
            {
                // scrivo vaslori a caso.. hhmm odierni
                int hhmm = 9876;
                int.TryParse(DateTime.Now.ToString("HHmm"), out hhmm);
                // Write the configured nodes
                WriteValueCollection nodesToWrite = new WriteValueCollection();

                // Int32 Node - Objects\CTT\Scalar\Scalar_Static\Int32
                WriteValue commWriteVal = new WriteValue();
                commWriteVal.NodeId = new NodeId("ns=4;s=IO_151");
                commWriteVal.AttributeId = Attributes.Value;
                commWriteVal.Value = new DataValue();
                commWriteVal.Value.Value = (int)hhmm - 10;
                nodesToWrite.Add(commWriteVal);

                WriteValue artWriteVal = new WriteValue();
                artWriteVal.NodeId = new NodeId("ns=4;s=IO_151");
                artWriteVal.AttributeId = Attributes.Value;
                artWriteVal.Value = new DataValue();
                artWriteVal.Value.Value = (int)hhmm;
                nodesToWrite.Add(artWriteVal);

                WriteValue qtyWriteVal = new WriteValue();
                qtyWriteVal.NodeId = new NodeId("ns=4;s=IO_153");
                qtyWriteVal.AttributeId = Attributes.Value;
                qtyWriteVal.Value = new DataValue();
                qtyWriteVal.Value.Value = (int)hhmm + 10;
                nodesToWrite.Add(qtyWriteVal);

                // Write the node attributes
                StatusCodeCollection results = null;
                DiagnosticInfoCollection diagnosticInfos;
                lgDebug("Writing nodes...");

                // Call Write Service
                m_session.Write(null,
                                nodesToWrite,
                                out results,
                                out diagnosticInfos);

                // Validate the response
                m_validateResponse(results, nodesToWrite);

                // Display the results.
                lgDebug("Write Results :");

                foreach (StatusCode writeResult in results)
                {
                    lgDebug($"     {writeResult}");
                }
            }
            catch (Exception ex)
            {
                // Log Error
                lgError($"Write Nodes Error : {ex.Message}.");
            }
        }

        #endregion Public Methods

        #region Protected Fields

        protected static bool isLogVerbose = false;

        protected static Logger lg;

        protected int currNullReceiv = 0;

        protected int maxErrors = 20;

        protected int maxNullAllowed = 100;

        protected int numErrors = 0;

        #endregion Protected Fields

        #region Protected Methods

        /// <summary>
        /// Effettua logging DEBUG corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="message"></param>
        protected void lgDebug(string message)
        {
            lg.Factory.Configuration.Variables["codIOB"] = currIob;
            lg.Debug(message);
        }

        /// <summary>
        /// Effettua logging ERROR corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="message"></param>
        protected void lgError(string message)
        {
            lg.Factory.Configuration.Variables["codIOB"] = currIob;
            lg.Error(message);
        }

        /// <summary>
        /// Effettua logging INFO corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="message"></param>
        protected void lgInfo(string message)
        {
            lg.Factory.Configuration.Variables["codIOB"] = currIob;
            lg.Info(message);
        }

        /// <summary>
        /// Effettua logging TRACE corretto impostanto anche la variabile IOB prima di scrivere...
        /// </summary>
        /// <param name="message"></param>
        protected void lgTrace(string message)
        {
            lg.Factory.Configuration.Variables["codIOB"] = currIob;
            lg.Trace(message);
        }

        #endregion Protected Methods

        #region Private Fields

        private readonly string currIob;

        private readonly Action<IList, IList> m_validateResponse;

        private ApplicationConfiguration m_configuration;

        private Session m_session;

        #endregion Private Fields

        #region Private Methods

        /// <summary>
        /// Browses for the node id of the parent data type of type base data type
        /// </summary>
        /// <param name="nodeId">The data to write as string[4]; string[0] = index, string[1] = tag name, string[2] = value, string[3] = opc data type</param>
        /// <returns>The node id of the parent data type</returns>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        private static NodeId GetParentDataType(NodeId nodeId, Session theSessionToBrowseIn)
        {
            ReferenceDescriptionCollection referenceDescriptionCollection;
            byte[] continuationPoint;
            theSessionToBrowseIn.Browse(null, null, nodeId, 1, BrowseDirection.Inverse, ReferenceTypeIds.HasSubtype, true, 0, out continuationPoint, out referenceDescriptionCollection);
            NodeId nodeIdParentDataType = (NodeId)referenceDescriptionCollection[0].NodeId;

            if (nodeIdParentDataType.NamespaceIndex != 0)
            {
                nodeIdParentDataType = GetParentDataType(nodeIdParentDataType, theSessionToBrowseIn);
            }

            return nodeIdParentDataType;
        }

        /// <summary>
        /// Browses for the desired type dictonary to parse for containing data types
        /// </summary>
        /// <param name="dataTypeNode">The data type node</param>
        /// <param name="theSessionToBrowseIn">The current session to browse in</param>
        /// <param name="structureDictionary">The list of string arrays containing the structure information tag name, data type id and array dimension</param>
        /// <returns>The dictionary as List of string arrays</returns>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        private static List<string[]> GetTypeDictionary(DataTypeNode dataTypeNode, Session theSessionToBrowseIn, List<string[]> structureDictionary)
        {
            //Create a temp list of string containing the structure dictionary
            List<string[]> tempStructureDictionary = new List<string[]>();
            tempStructureDictionary = structureDictionary;
            Session tempSession = theSessionToBrowseIn;
            //Get Structure Definition
            StructureFieldCollection structureFieldCollection = null;
            try
            {
                StructureDefinition structureDefinition = (StructureDefinition)dataTypeNode.DataTypeDefinition.Body;
                structureFieldCollection = (StructureFieldCollection)structureDefinition.Fields;
            }
            catch
            {
                structureDictionary[structureDictionary.Count - 1][1] = UAClient.GetParentDataType(dataTypeNode.NodeId, tempSession).ToString();
                return structureDictionary;
            }

            //Check every structure field in the collection
            foreach (StructureField structureField in structureFieldCollection)
            {
                if (structureField.DataType.NamespaceIndex != 0) //Check for structures in the structure
                {
                    //Create a reference description collection
                    ReferenceDescriptionCollection referenceDescriptionCollection;
                    //Create a continuationPoint
                    byte[] continuationPoint;
                    //Browse for the references of the data type of the structure field
                    theSessionToBrowseIn.Browse(
                                null,
                                null,
                                structureField.DataType,
                                0u,
                                BrowseDirection.Both,
                                ReferenceTypeIds.References,
                                true,
                                0,
                                out continuationPoint,
                                out referenceDescriptionCollection);
                    if (((NodeId)referenceDescriptionCollection[0].NodeId).NamespaceIndex == 0)//Data type is sub type of a base data type, i.e. BYTE is a sub tpye of Byte
                    {
                        //Check if the structure field is an array and parse the dimension to string
                        string arrayDimensions = "";
                        if (structureField.ValueRank == ValueRanks.OneDimension)
                        {
                            arrayDimensions = structureField.ArrayDimensions[0].ToString();
                        }
                        //Add the structure field name, its data type node id and array dimension to the dictionary list
                        tempStructureDictionary.Add(new string[] { structureField.Name, ((NodeId)referenceDescriptionCollection[0].NodeId).ToString(), arrayDimensions });
                    }
                    else//The structure/udt has at least one additional structure/udt inside
                    {
                        //Read the data type node id of the structure/udt in the parent structure/udt
                        DataTypeNode tempDataTypeNode = (DataTypeNode)theSessionToBrowseIn.ReadNode(structureField.DataType.ToString());
                        //Enter an empty Line to seperate the inside structure from the parent
                        tempStructureDictionary.Add(new string[] { structureField.Name + " - " + "struct/udt of type: " + tempDataTypeNode.DisplayName.ToString(), "", "" });
                        //Copy the temp dictionary to the real dictionary
                        structureDictionary = tempStructureDictionary;
                        //Recursively get the structure of the structure/udt in the parent structure/udt
                        GetTypeDictionary(tempDataTypeNode, tempSession, structureDictionary);
                    }
                }
                else //The structure field is a base data type
                {
                    //Check if the structure field is an array and parse the dimension to string
                    string arrayDimensions = "";
                    if (structureField.ValueRank == ValueRanks.OneDimension)
                    {
                        arrayDimensions = structureField.ArrayDimensions[0].ToString();
                    }
                    //Add the structure field name, its data type node id and array dimension to the dictionary list
                    tempStructureDictionary.Add(new string[] { structureField.Name, structureField.DataType.ToString(), arrayDimensions });
                }
                //Copy the temp dictionary to the real dictionary
                structureDictionary = tempStructureDictionary;
            }
            //Add a row for marking the end of the struct/udt

            structureDictionary.Add(new string[] { "/" + dataTypeNode.DisplayName.ToString(), "", "" });
            return structureDictionary;
        }

        /// <summary>
        /// Parses data to write to a list of byte arrays
        /// </summary>
        /// <param name="dataToWrite">The data to write as string[4]; string[0] = index, string[1] = tag name, string[2] = value, string[3] = opc data type</param>
        /// <param name="structureDictionary">The structure of the udt/struct</param>
        /// <returns>The list of the parsed byte arrays</returns>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        private static List<byte[]> ParseDataToByteArray(List<string[]> dataToWrite, List<string[]> structureDictionary)
        {
            //Define result byte list to return a byte array
            List<byte[]> resultByteList = new List<byte[]>();

            //Dictionary to get the right byte length depending on the data type
            Dictionary<BuiltInType, int> byteLength = new Dictionary<BuiltInType, int>()
                {
                    {BuiltInType.Boolean, 1},
                    {BuiltInType.Int16, 2},
                    {BuiltInType.Int32, 4},
                    {BuiltInType.Int64, 8},
                    {BuiltInType.UInt16, 2},
                    {BuiltInType.UInt32, 4},
                    {BuiltInType.UInt64, 8},
                    {BuiltInType.Float, 4},
                    {BuiltInType.Double, 8},
                    {BuiltInType.Byte, 1},
                    {BuiltInType.String, 4}
                };

            //Counter for the structure dictionary browsing
            int tempCounter = 0;

            //Create byte array to store the data of one array
            byte[] byteArray = new byte[0];

            //Start decoding for opc data types
            foreach (string[] element in dataToWrite)
            {
                //Reset the counter and the byteArray each time the counter reaches the size of the target Structure/UDT and the data to write include the target Structure/UDT several times
                if (tempCounter == structureDictionary.Count)
                {
                    //Add the byte array to the result byte array list
                    resultByteList.Add(byteArray);

                    //Reset the counter and the array
                    byteArray = new byte[0];
                    tempCounter = 0;
                }

                //Create byte array to store the bytes of one value depending on its data type, hence it is reseted to 0 for each element
                byte[] dataByteArray = new byte[0];

                //Get the data type id and the target type by using the structure dictionary
                NodeId datatypeId = new NodeId(structureDictionary[tempCounter][1]);
                BuiltInType targetType = TypeInfo.GetBuiltInType(datatypeId);

                //Get the system type for getting the rigth parse method
                Type systemType = TypeInfo.GetSystemType(datatypeId, null);

                //Parse array dimension string of the structure dictionary in int32
                Int32 arraySize = 0;
                if (structureDictionary[tempCounter][2] != "")
                {
                    arraySize = Int32.Parse(structureDictionary[tempCounter][2]);
                }

                //Serialize the byte string depending on the target data type and the array size - special if queries are needed for byte and string
                if (targetType == BuiltInType.String && !(arraySize > 0))
                {
                    //Resize the data byte array to its correct size depending on the byte length of target type by using the dictionary
                    dataByteArray = new byte[byteLength[targetType]];
                    //Get the bytes of the string length and copy them to the beginning of the data byte array
                    Array.Copy(BitConverter.GetBytes(element[2].Length), 0, dataByteArray, 0, byteLength[targetType]);
                    //Convert the string to byte and concat them to the data byte array by converting every char of the string
                    foreach (Char c in element[2])
                    {
                        byte[] tempArray = new byte[1];
                        tempArray[0] = Convert.ToByte(c);
                        dataByteArray = dataByteArray.Concat(tempArray).ToArray();
                    }
                }
                else if (targetType == BuiltInType.String && arraySize > 0)
                {
                    //Create a temp string array with the array size
                    string[] tempStringArr = new string[(arraySize - 1)];
                    //Resize the data byte array to its correct size depending on the byte length of target type by using the dictionary
                    dataByteArray = new byte[byteLength[BuiltInType.UInt32]];
                    //Get the bytes of the string length and copy them to the beginning of the data byte array
                    Array.Copy(BitConverter.GetBytes(arraySize), 0, dataByteArray, 0, byteLength[targetType]);

                    //Get the single array elements by splitting the string
                    tempStringArr = element[2].Split(';');
                    //Go through every string element in the temp string array
                    for (int ii = 0; ii < arraySize; ii++)
                    {
                        //Copy the string length as byte to the data byte array
                        byte[] tempArray = new byte[byteLength[targetType]];
                        Array.Copy(BitConverter.GetBytes(tempStringArr[ii].Length), 0, tempArray, 0, byteLength[targetType]);
                        dataByteArray = dataByteArray.Concat(tempArray).ToArray();
                        //Convert the string to byte and concat them to the data byte array by converting every char of the string
                        foreach (Char c in tempStringArr[ii])
                        {
                            byte[] tempArraySingle = new byte[1];
                            tempArraySingle[0] = Convert.ToByte(c);
                            dataByteArray = dataByteArray.Concat(tempArraySingle).ToArray();
                        }
                    }
                }
                else if (targetType == BuiltInType.Byte && !(arraySize > 0))
                {
                    //Convert the string of byte to byte
                    dataByteArray = new byte[byteLength[targetType]];
                    dataByteArray[0] = Convert.ToByte(element[2]);
                }
                else if (targetType == BuiltInType.Byte && arraySize > 0)
                {
                    //Create a temp string variable to store the array elements which are divided by ;
                    String tempString = "";
                    //Resize the data byte array to its correct size depending on the byte length of target type by using the dictionary
                    dataByteArray = new byte[byteLength[BuiltInType.UInt32]];
                    //Get the bytes of the string length and copy them to the beginning of the data byte array
                    Array.Copy(BitConverter.GetBytes(arraySize), 0, dataByteArray, 0, byteLength[targetType]);

                    //Check every char in the string value of the element
                    foreach (Char c in element[2])
                    {
                        if (c != ';') //Concat every char to a string value in elements except the ;
                        {
                            tempString = String.Concat(tempString, c);
                        }
                        else
                        {
                            //Create a temp byte array with its correct size depending on the byte length of the target data type
                            byte[] tempArray = new byte[byteLength[targetType]];
                            tempArray[0] = Convert.ToByte(tempString);
                            //Copy the temp array to the data byte array
                            dataByteArray = dataByteArray.Concat(tempArray).ToArray();
                            //Reset the temp string
                            tempString = "";
                        }
                    }
                }
                else if (!(arraySize > 0))
                {
                    //Create the byte array with its correct size depending on the byte length of the target data type
                    dataByteArray = new byte[byteLength[targetType]];
                    //Get the converter method depending on the system type
                    System.Reflection.MethodInfo methodInfo = typeof(Convert).GetMethod("To" + systemType.Name, new Type[] { typeof(string) });
                    //Call the converter method
                    dynamic convertDataType = methodInfo.Invoke(null, new Object[] { element[2] });
                    Array.Copy(BitConverter.GetBytes(convertDataType), 0, dataByteArray, 0, byteLength[targetType]);
                }
                else if (arraySize > 0)
                {
                    //Create a temp string variable to store the array elements which are divided by ;
                    String tempString = "";
                    //Create the byte array with the size of a UInt32, which is the data type for the array dimension
                    dataByteArray = new byte[byteLength[BuiltInType.UInt32]];
                    //Get the bytes of the array dimension and copy them to the data byte array as they always are at the beginning of a byte array of an array
                    Array.Copy(BitConverter.GetBytes(arraySize), 0, dataByteArray, 0, byteLength[targetType]);

                    //Check every char in the string value of the element
                    foreach (Char c in element[2])
                    {
                        if (c != ';') //Concat every char to a string value in elements except the ;
                        {
                            tempString = String.Concat(tempString, c);
                        }
                        else
                        {
                            //Create a temp byte array with its correct size depending on the byte length of the target data type
                            byte[] tempArray = new byte[byteLength[targetType]];
                            //Get the converter method depending on the system type
                            System.Reflection.MethodInfo methodInfo = typeof(Convert).GetMethod("To" + systemType.Name, new Type[] { typeof(string) });
                            //Call the converter method
                            dynamic convertDataType = methodInfo.Invoke(null, new Object[] { element[2] });
                            Array.Copy(BitConverter.GetBytes(convertDataType), 0, tempArray, 0, byteLength[targetType]);
                            //Concat the temp array with the data array
                            dataByteArray = dataByteArray.Concat(tempArray).ToArray();
                            //Reset the temp string
                            tempString = "";
                        }
                    }
                }
                else
                {
                    Exception e = new Exception("Data type is too complex to be parsed." + System.Environment.NewLine);
                    throw e;
                }

                //Add the data byte array to the byte array containing all data and count the counter
                byteArray = byteArray.Concat(dataByteArray).ToArray();
                tempCounter++;
            }
            resultByteList.Add(byteArray);
            return resultByteList;
        }

        /// <summary>
        /// Parses a byte array to objects containing tag names and tag data types
        /// </summary>
        /// <param name="varList">List of object containing tag names and tag data types</param>
        /// <param name="byteResult">A byte array to parse</param>
        /// <returns>A list of string[4]; string[0] = tag name, string[1] = tag name, string[2] = value, string[3] = opc data type</returns>
        /// <exception cref="Exception">Throws and forwards any exception with short error description.</exception>
        private static List<string[]> ParseDataToTagsFromDictionary(List<string[]> varList, List<byte[]> byteArrays)
        {
            //Define result list to return var name, var value and var data type
            List<string[]> resultStringList = new List<string[]>();

            //Dictionary for index counting
            Dictionary<BuiltInType, int> byteLength = new Dictionary<BuiltInType, int>()
                {
                    {BuiltInType.Boolean, 1},
                    {BuiltInType.Int16, 2},
                    {BuiltInType.Int32, 4},
                    {BuiltInType.Int64, 8},
                    {BuiltInType.UInt16, 2},
                    {BuiltInType.UInt32, 4},
                    {BuiltInType.UInt64, 8},
                    {BuiltInType.Float, 4},
                    {BuiltInType.Double, 8},
                    {BuiltInType.Byte, 1},
                    {BuiltInType.String, 4}
                };

            //Array index
            int arrayIndex = 0;
            foreach (byte[] byteResult in byteArrays)
            {
                //Byte decoding index
                int index = 0;

                //Start decoding for opc data types
                foreach (string[] val in varList)
                {
                    string[] dataReferenceStringArray = new string[4];

                    //Check if the type dictionary includes a struct/udt in a struct/udt and enter an empty row
                    if (val[1] == "")
                    {
                        dataReferenceStringArray[0] = "[" + arrayIndex.ToString() + "]";
                        dataReferenceStringArray[1] = val[0];
                        dataReferenceStringArray[2] = "";
                        dataReferenceStringArray[3] = "";
                        //Add the data Reference string array to the result string list
                        resultStringList.Add(dataReferenceStringArray);
                        continue;
                    }
                    //Add array index
                    dataReferenceStringArray[0] = "[" + arrayIndex.ToString() + "]";
                    //Copy tag name
                    dataReferenceStringArray[1] = val[0];
                    //Get the target data type via the data type id
                    NodeId datatypeId = new NodeId(val[1]);
                    BuiltInType targetType = TypeInfo.GetBuiltInType(datatypeId);
                    //Get the system type
                    Type systemType = TypeInfo.GetSystemType(datatypeId, null);
                    //Parse array dimensions string in int32
                    Int32 arrayDimensions = 0;
                    if (val[2] != "")
                    {
                        arrayDimensions = Int32.Parse(val[2]);
                    }

                    //Deserialize the byte array depending on the target data type and the array dimension
                    if (targetType == BuiltInType.String && !(arrayDimensions > 0))
                    {
                        //Get the string length
                        Int32 stringlength = BitConverter.ToInt32(byteResult, index);
                        index += byteLength[targetType];
                        if (stringlength > 0) //Decode the bytes to its string value
                        {
                            dataReferenceStringArray[2] = Encoding.UTF8.GetString(byteResult, index, stringlength);
                            index += stringlength;
                        }
                        else
                        {
                            dataReferenceStringArray[2] = "";
                        }
                    }
                    else if (targetType == BuiltInType.String && arrayDimensions > 0)
                    {
                        //Skip array information (UInt32) regarding its size
                        index += byteLength[BuiltInType.UInt32];
                        //Check every element of the array
                        for (int i = 0; i < arrayDimensions; i++)
                        {
                            //Get the string length
                            Int32 stringlength = BitConverter.ToInt32(byteResult, index);
                            index += byteLength[targetType];
                            if (stringlength > 0) //Decode the bytes to its string value
                            {
                                dataReferenceStringArray[2] = String.Concat(dataReferenceStringArray[2], Encoding.UTF8.GetString(byteResult, index, stringlength));
                                //Add a ; to the string value to seperate the value from the other array values
                                dataReferenceStringArray[2] = String.Concat(dataReferenceStringArray[2], ";");
                                index += stringlength;
                            }
                            else
                            {
                                dataReferenceStringArray[2] = dataReferenceStringArray[2] + ";";
                            }
                        }
                    }
                    else if (targetType == BuiltInType.Byte && !(arrayDimensions > 0))
                    {
                        //Copy the byte value as string to the data reference array
                        dataReferenceStringArray[2] = byteResult[index].ToString();
                        index += byteLength[targetType];
                    }
                    else if (targetType == BuiltInType.Byte && arrayDimensions > 0)
                    {
                        //Skip array information (UInt32) regarding its size
                        index += byteLength[BuiltInType.UInt32];
                        Int32[] tempArray = new Int32[arrayDimensions];
                        //Check every element of the array
                        for (int i = 0; i < arrayDimensions; i++)
                        {
                            tempArray[i] = byteResult[index];
                            index += byteLength[targetType];
                        }
                        //Add the values as one value seperated by ; to the data reference array
                        dataReferenceStringArray[2] = String.Join(";", tempArray);
                        dataReferenceStringArray[2] = String.Concat(dataReferenceStringArray[2], ";");
                    }
                    else if (!(arrayDimensions > 0))
                    {
                        //Get the converter method depending on the system type
                        System.Reflection.MethodInfo methodInfo = typeof(BitConverter).GetMethod("To" + systemType.Name);
                        //Call the method
                        dataReferenceStringArray[2] = methodInfo.Invoke(null, new Object[] { byteResult, index }).ToString();
                        index += byteLength[targetType];
                    }
                    else if (arrayDimensions > 0)
                    {
                        //Skip array information (UInt32) regarding its size
                        index += byteLength[BuiltInType.UInt32];
                        //Get the converter method depending on the system type
                        System.Reflection.MethodInfo methodInfo = typeof(BitConverter).GetMethod("To" + systemType.Name);
                        dynamic[] tempArray = new dynamic[arrayDimensions];
                        //Check every element of the array
                        for (int i = 0; i < arrayDimensions; i++)
                        {
                            tempArray[i] = methodInfo.Invoke(null, new Object[] { byteResult, index }).ToString();
                            index += byteLength[targetType];
                        }
                        //Add the values as one value seperated by ; to the data reference array
                        dataReferenceStringArray[2] = String.Join(";", tempArray);
                        dataReferenceStringArray[2] = String.Concat(dataReferenceStringArray[1], ";");
                    }
                    else
                    {
                        Exception e = new Exception("Data type is too complex to be parsed." + System.Environment.NewLine);
                        throw e;
                    }
                    //Copy the target type as string to the data reference array
                    dataReferenceStringArray[3] = targetType.ToString();
                    //Add the data Reference string array to the result string list
                    resultStringList.Add(dataReferenceStringArray);
                }
                //Count the array index
                arrayIndex += 1;
            }
            return resultStringList;
        }

        /// <summary>
        /// Handles the certificate validation event. This event is triggered every time an
        /// untrusted certificate is received from the server.
        /// </summary>
        private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e)
        {
            bool certificateAccepted = true;

            // **** Implement a custom logic to decide if the certificate should be accepted or not
            // and set certificateAccepted flag accordingly. The certificate can be retrieved from
            // the e.Certificate field ***

            ServiceResult error = e.Error;
            while (error != null)
            {
                lgError($"{error.StatusCode} | {error.Code} | {error.LocalizedText}");
                error = error.InnerResult;
            }

            if (certificateAccepted)
            {
                lgInfo($"Untrusted Certificate accepted. SubjectName = {e.Certificate.SubjectName}");
            }

            e.AcceptAll = certificateAccepted;
        }

        /// <summary>
        /// Handle DataChange notifications from Server
        /// </summary>
        private void OnMonitoredItemNotification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
        {
            try
            {
                // Log MonitoredItem Notification event
                MonitoredItemNotification notification = e.NotificationValue as MonitoredItemNotification;

                // verifico se ricevo null, superata soglia genero eccezione
                if ($"{notification.Value}".Contains("null"))
                {
                    currNullReceiv++;
                    lgTrace($"NUll data read | Variable: {monitoredItem.StartNodeId.Identifier} | Value: {notification.Value}");
                }
                else
                {
                    currNullReceiv--;
                    currNullReceiv = currNullReceiv > 0 ? currNullReceiv : 0;
                    // sollevo evento notifica vaziazione MonitoredItem
                    if (eh_MonItChange != null)
                    {
                        eh_MonItChange(this, new opcUaMonitItemChange(monitoredItem, notification));
                    }
                }
                lgTrace($"Notification Received | Variable: {monitoredItem.StartNodeId.Identifier} | Value: {notification.Value}");
                // se ho ricevuto troppi nul --> disconnetto
                if (currNullReceiv > maxNullAllowed)
                {
                    if (eh_nullExceed != null)
                    {
                        eh_nullExceed(this, currNullReceiv);
                    }
                    // reset
                    currNullReceiv = 0;
                }
            }
            catch (Exception ex)
            {
                lgError($"OnMonitoredItemNotification error: {ex.Message}");
            }
        }

        #endregion Private Methods
    }
}