using IOB_UT_NEXT.Config;
using MapoSDK;
using Newtonsoft.Json;
using Opc.Ua;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading;

namespace IOB_WIN_OPC_UA.IobOpc
{
    public class OpcUaSiemens : OpcUa
    {
        #region Public Constructors

        /// <summary>
        /// Estende l'init della classe base, impiegando il pacchetto Nuget OPC-UA foundation con la
        /// gestione specifica per Siemens OPC-UA (es OPC x Cereria Finassi e Unitech x Rama)
        /// https://github.com/OPCFoundation/UA-.NETStandard
        /// </summary>
        /// <param name="caller">Form chiamante</param>
        /// <param name="IOBConf">Configurazione (legacy)</param>
        /// <param name="IobConfFull">Configurazione (v 4.x)</param>
        public OpcUaSiemens(AdapterFormNext caller, IobConfTree IobConfFull) : base(caller, IobConfFull)
        {
            lgInfo("Init OpcUa Siemens");
            sendKeyRichiesta = true;
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// Processo i task richiesti e li elimino dalla coda 1:1
        /// </summary>
        /// <param name="task2exe"></param>
        public override Dictionary<string, string> executeTasks(Dictionary<string, string> task2exe, string codTav)
        {
            lgInfo($"OpcUaSiemens: call executeTasks | {task2exe.Count} task");
            // se ci fosse il task di gestione attrezzaggio --> aggiunge un SYNC DB...
            Dictionary<string, string> task2add = new Dictionary<string, string>();
            // cerco nei task
            if (task2exe != null)
            {
                bool forceDbSync = false;
                bool addResetPzCount = false;
                // controllo se memMap != null...
                if (memMap != null)
                {
                    // cerco task specifici: se ho task da startSetup --> preparo un task di syncDb in coda...
                    foreach (var item in task2exe)
                    {
                        // converto richiesta in enum...
                        taskType tName = taskType.nihil;
                        Enum.TryParse(item.Key, out tName);
                        // controllo sulla KEY...
                        switch (tName)
                        {
                            case taskType.setArt:
                            case taskType.setComm:
                            case taskType.setPzComm:
                                forceDbSync = true;
                                break;

                            case taskType.endProd:
                                forceDbSync = true;
                                // reset contapezzi inizio setup
                                if (IOBConfFull.Counters.ResetOnProdEnd)
                                {
                                    addResetPzCount = true;
                                }
                                lgInfo($"Chiamato {tName} | addResetPzCount: {addResetPzCount}");
                                break;

                            case taskType.startSetup:
                                forceDbSync = true;
                                // reset contapezzi inizio setup
                                if (IOBConfFull.Counters.ResetOnSetupStart)
                                {
                                    addResetPzCount = true;
                                }
                                lgInfo($"Chiamato {tName} | addResetPzCount: {addResetPzCount}");
                                break;

                            case taskType.stopSetup:
                                forceDbSync = true;
                                // reset contapezzi inizio setup
                                if (IOBConfFull.Counters.ResetOnSetupStop)
                                {
                                    addResetPzCount = true;
                                }
                                lgInfo($"Chiamato {tName} | addResetPzCount: {addResetPzCount}");
                                break;
                        }
                    }
                }
                // se NON c'è già sync DB
                if (!task2exe.ContainsKey("syncDbData"))
                {
                    if (forceDbSync)
                    {
                        task2exe.Add("syncDbData", "syncDbData");
                    }
                }
                // se NON c'è già sync DB
                if (addResetPzCount && !task2exe.ContainsKey("forceResetPzCount"))
                {
                    task2exe.Add("forceSetPzCount", "0");
                    task2exe.Add("forceResetPzCount", "0");
                }

                if (addResetPzCount)
                {
                    // imposto reset gestione pzcount da parametri
                    vetoQueuePzCount = DateTime.Now.AddMinutes(IOBConfFull.General.DelayReadPzCountSetup);
                    // se è una doppia tavola aggiungo anche tag "processOtherInfo"...
                    if (isMulti)
                    {
                        bool resetTav = false;
                        // se contiene chiave reset x tavola...
                        foreach (var item in IOBConfFull.Device.MultiIobList)
                        {
                            string keyName = $"forceResetPzCount#{item}";
                            resetTav = resetTav || memMap.mMapWrite.ContainsKey(keyName);
                            if (resetTav)
                            {
                                ProcessOtherInfo(keyName, "True");
                            }
                        }
                        if (resetTav)
                        {
                            // attendo
                            Thread.Sleep(500);
                            // mando chiusura...
                            foreach (var item in IOBConfFull.Device.MultiIobList)
                            {
                                ProcessOtherInfo($"forceResetPzCount#{item}", "False");
                            }
                        }
                    }
                    lgInfo("Aggiunti task reset contapezzi (ex EMCO) tramite forceSetPzCount e forceResetPzCount");
                }
            }

            // uso metodo base ora (con eventuale aggiunta)
            return base.executeTasks(task2exe, codTav);
        }

        /// <summary>
        /// Effettua reset del contapezzi
        /// </summary>
        /// <returns></returns>
        public override bool resetContapezziPLC(string codTav)
        {
            lgInfo("OpcUaSiemens.resetContapezziPLC 01");
            bool answ = false;
            // ...SE abilitato da conf IOB
            if (IOBConfFull.Counters.ResetOnSetupStart || IOBConfFull.Counters.ResetOnSetupStop || IOBConfFull.Counters.EnableSetPzCount || IOBConfFull.Counters.ResetOnProdEnd)
            {
                lgInfo("OpcUaSiemens.resetContapezziPLC 02");
                // imposto un veto in lettura cmq x evitare problemi
                dtVetoReadPzCount = DateTime.Now.AddMinutes(delayMinReadPzCount);
                if (opcUaParams.actResetCounter != null && opcUaParams.actResetCounter.Count > 0)
                {
                    lgInfo("OpcUaSiemens.resetContapezziPLC 03");
                    try
                    {
                        List<WriteValue> nodes2Write = new List<WriteValue>();
                        foreach (var item in opcUaParams.actResetCounter)
                        {
                            WriteValue commWriteVal = new WriteValue();
                            commWriteVal.NodeId = new NodeId(item.Key);
                            commWriteVal.AttributeId = Attributes.Value;
                            commWriteVal.Value = new DataValue();
                            commWriteVal.Value.Value = item.Value;

                            nodes2Write.Add(commWriteVal);
                            lgInfo($"Accodo nodo per scrittura OPC-UA:{Environment.NewLine}{JsonConvert.SerializeObject(commWriteVal)}");
                        }
                        // vera scrittura
                        UA_ref.WriteNodes(nodes2Write);
                        answ = true;
                        lgInfo($"Eseguito scrittura OPC-UA su nodo actResetCounter");
                    }
                    catch
                    { }
                }
                else
                {
                    lgInfo("OpcUaSiemens.resetContapezziPLC 04");
                    // imposto a zero (SE gestito)
                    setcontapezziPLC(0, codTav);
                }
            }
            else
            {
                lgError("Impossibile effettuare RESET contapezzi, mancanza parametri IOBConfFull.Counters.*");
            }
            return answ;
        }

        /// <summary>
        /// Effettua IMPOSTAZIONE FORZATA del contapezzi
        /// </summary>
        /// <returns></returns>
        public override bool setcontapezziPLC(int newPzCount, string codTav)
        {
            lgInfo($"OpcUaSiemens.setcontapezziPLC | {newPzCount}");
            bool answ = false;
            if (memMap != null && memMap.mMapWrite.ContainsKey("forceSetPzCount"))
            {
                // modalità invio secondo richiesta
                lgDebug($"OpcUaSiemens.setcontapezziPLC | {IOBConfFull.Counters.SendPzCountMode}");
                if (IOBConfFull.Counters.SendPzCountMode == "TASK")
                {
                    Dictionary<string, string> task2add = new Dictionary<string, string>();
                    task2add.Add("forceSetPzCount", $"{newPzCount}");
                    executeTasks(task2add, codTav);
                }
                else
                {
                    // provare x Donati EMCO e Siemens Rama Tenditalia
                    var ListTempParams = new List<objItem>();
                    // aggiungo reset caricamento
                    objItem writeReq = new objItem()
                    {
                        uid = "forceSetPzCount",
                        reqValue = $"{newPzCount}",
                        name = "forceSetPzCount"
                    };
                    ListTempParams.Add(writeReq);
                    // effettua chiamata scrittura verso impianto x il solo reset......
                    plcWriteParams(ref ListTempParams);
                }
                answ = true;
                lgInfo($"Eseguito scrittura OPC-UA su nodo setcontapezziPLC | sendMode: {IOBConfFull.Counters.SendPzCountMode}");
            }
            else
            {
                lgError("Impossibile effettuare SET contapezzi, mancanza conf per mMapWrite.forceSetPzCount");
            }
            return answ;
        }

        #endregion Public Methods

        #region Protected Fields

        protected bool testDone = false;

        #endregion Protected Fields

        #region Protected Methods

        /// <summary>
        /// Effettua decodifica aree memoria alla bitmap usata x MAPO
        /// </summary>
        protected override void decodeToBaseBitmap()
        {
            DateTime adesso = DateTime.Now;
            CheckLiveData(adesso);
            // init a zero...
            B_input = 0;

            /* -----------------------------------------------------
             *  STATE MACHINE 60 STD / SIMULA
             *------------------------------------------------------
             * bitmap MAPO
             * B0: POWER_ON
             * B1: RUN
             * B2: pzCount
             * B3: allarme
             * B4: manuale
             * B5: SlowTC (NON gestito qui)
             * B6: warm-up / cool-down / setup
             * B7: emergenza ARMATA (1=ok, 0 = premuta)
             ---------------------------------------------------- */

            // se valido il check ping lo eseguo... altrimenti lo do x buono
            bool checkPing = !opcUaParams.pingAsPowerOn;
            string currRun = "N.A.";
#if false
            if (!checkPing)
            {
                checkPing = (testPingMachine == IPStatus.Success);
            }
#endif
            // test ping come base + volte prima di dire che non risponde...
            int currTryPing = maxTryPing;
            while (currTryPing > 0 && !checkPing)
            {
                if (!checkPing)
                {
                    checkPing = (testPingMachine == IPStatus.Success);
                }
                currTryPing--;
                // se ping KO --> aspetto prima di ripetere test
                if (!checkPing)
                {
                    Random rand = new Random();
                    Thread.Sleep(rand.Next(pingServerMsTimeout * 2));
                }
            }
            // bit 0 (poweron) imposto a 1 SE pingo + PowerOn=="ON"...
            bool powerOnOk = checkPing && hasPowerOn;
            // procedo SOLO SE mi da ping OK...
            if (checkPing)
            {
                B_input = powerOnOk ? 1 : 0;

                procRunMode(ref currRun);

                // salvo running come = working...
                isRunning = isWorking;
                // check lavora x contapezzi...
                if (!isWorking && delayMinReadPzCount > 0)
                {
                    // metto un veto al contapezzi...
                    dtVetoReadPzCount = DateTime.Now.AddMinutes(delayMinReadPzCount);
                }

                // se ho emergenza premuta --> emergenza!
                if (hasEStopArmed)
                {
                    B_input += (1 << 7);
                }
                // se ho emergenza premuta --> emergenza!
                if (isWarmUpCoolDown)
                {
                    B_input += (1 << 6);
                }
                if (isWorking)
                {
                    // RUN = LAVORA!
                    B_input += (1 << 1);
                    if (dtVetoReadPzCount >= adesso)
                    {
                        // tolgo eventuale veto al contapezzi...
                        dtVetoReadPzCount = DateTime.Now.AddSeconds(-1);
                    }
                }
                // se ho almeno 1 allarme E NON SONO IN AUTO --> ALARM!
                else if (hasError)
                {
                    B_input += (1 << 3);
                }
                else if (powerOnOk && (!isReady || isManual))
                {
                    // se NON ready --> manual
                    B_input += (1 << 4);
                }
                lastCurrent = DateTime.Now;
            }
            else
            {
                lgError($"Errore: test ping non superato | B_input: {B_input}");
            }
            // se abilitato watchdog...
            if (opcUaParams.WatchDog.IsEnabled)
            {
                if (adesso.Subtract(lastWatchDogPLC).TotalSeconds > 2)
                {
                    lastWatchDogPLC = adesso;
                    WatchDog++;
                    WatchDog = WatchDog > opcUaParams.WatchDog.MaxVal ? 0 : WatchDog;

                    lgTrace($"WatchDog val: {WatchDog}");
                    try
                    {
                        WriteValue commWriteVal = new WriteValue();
                        commWriteVal.NodeId = new NodeId(opcUaParams.WatchDog.MemConfWrite);
                        commWriteVal.AttributeId = Attributes.Value;
                        commWriteVal.Value = new DataValue();
                        commWriteVal.Value.Value = WatchDog;

                        List<WriteValue> nodes2Write = new List<WriteValue>();
                        nodes2Write.Add(commWriteVal);

                        UA_ref.WriteNodes(nodes2Write);
                    }
                    catch (Exception exc)
                    {
                        lgError($"Eccezione in gestione WatchDog, valore attuale {WatchDog}{Environment.NewLine}{exc}");
                    }
                }
            }
            else
            {
                lgTrace("WatchDog disabilitato");
            }

            // controllo se sono poweroff e se non ho dati buoni da > 2 minuti --> disconnetto
            if (!powerOnOk && adesso.Subtract(lastCurrent).TotalMinutes > 2)
            {
                lgTrace("decodeToBaseBitmap --> tryDisconnect");
                tryDisconnect();
            }

            // solo se non ho veto check
            int vFactor = 2;
            if (vetoCheckStatus < adesso)
            {
                lgDebug($"Stato variabili checkPing: {testPingMachine}");
                // imposto veto per vetoSeconds...
                vetoCheckStatus = adesso.AddSeconds(vetoSeconds * vFactor);
            }

            // log opzionale!
            if (verboseLog)
            {
                lgDebug($"Trasformazione checkPing: {checkPing} | hasPowerOn: {hasPowerOn} | B_input: {B_input} | currRun = {currRun}");
            }
        }

        /// <summary>
        /// Effettua vera scrittura parametri
        /// </summary>
        /// <param name="updatedPar"></param>
        protected override void plcWriteParams(ref List<objItem> updatedPar)
        {
            // solo se connesso!
            if (connectionOk)
            {
                dataConf currMem = null;
                int byteSize = 0;
                string memAddrWrite = "";
                string serObj = "";
                if (updatedPar != null)
                {
                    // valori da scrivere in memoria
                    Dictionary<string, WriteValue> nodes2Write = new Dictionary<string, WriteValue>();
                    Dictionary<string, Dictionary<string, string>> nodesUdt2Write = new Dictionary<string, Dictionary<string, string>>();
                    Dictionary<string, string> currUdsDict = new Dictionary<string, string>();
                    // controllo i parametri... ne gestisco 4...
                    foreach (var item in updatedPar)
                    {
                        if (connectionOk)
                        {
                            Opc.Ua.DataValue currNodeVal = new DataValue();
                            try
                            {
                                memAddrWrite = "";
                                int valInt = 0;
                                double valReal = 0;
                                // cerco in area memMapWrite...
                                if (memMap.mMapWrite.ContainsKey(item.uid))
                                {
                                    // recupero!
                                    currMem = memMap.mMapWrite[item.uid];
                                    byteSize = currMem.size;
                                    memAddrWrite = currMem.memAddr;

                                    // init obj scrittura
                                    WriteValue commWriteVal = new WriteValue();
                                    commWriteVal.Value = new DataValue();
                                    commWriteVal.AttributeId = Attributes.Value;

                                    serObj = JsonConvert.SerializeObject(item);
                                    lgInfo($"Inizio processing plcWriteParams per {currMem.name} | valore richiesto {currMem.value}");
                                    lgInfo($"{Environment.NewLine}---------------UPDATED PARAM:{Environment.NewLine}{serObj}{Environment.NewLine}---------------");
                                    serObj = JsonConvert.SerializeObject(currMem);
                                    lgInfo($"{Environment.NewLine}---------------MEMORY CONTENT:{Environment.NewLine}{serObj}{Environment.NewLine}---------------");

                                    // verifico se devo processare byte[]...
                                    bool isUds = memAddrWrite.Contains(opcUaParams.kvDelim);

                                    if (isUds)
                                    {
                                        // ciclo e calcolo valore
                                        int iPos = memAddrWrite.IndexOf(opcUaParams.kvDelim);
                                        string nodeId = memAddrWrite.Substring(0, iPos);
                                        string varName = memAddrWrite.Substring(iPos + 1);

                                        // cerco se ci sia già un record Uds...
                                        if (nodesUdt2Write.ContainsKey(nodeId))
                                        {
                                            currUdsDict = nodesUdt2Write[nodeId];
                                            // controllo valore...
                                            if (currUdsDict.ContainsKey(varName))
                                            {
                                                currUdsDict[varName] = item.reqValue;
                                            }
                                            else
                                            {
                                                currUdsDict.Add(varName, item.reqValue);
                                            }
                                            nodesUdt2Write[nodeId] = currUdsDict;
                                        }
                                        else
                                        {
                                            currUdsDict = new Dictionary<string, string>();
                                            currUdsDict.Add(varName, item.reqValue);
                                            nodesUdt2Write.Add(nodeId, currUdsDict);
                                        }
                                    }
                                    else
                                    {
                                        // init generico x scrittura nell'oggetto OPC-UA direttamente
                                        commWriteVal.NodeId = new NodeId(memAddrWrite);
                                        commWriteVal.Value.Value = item.reqValue;

                                        // check tipo specifico
                                        switch (currMem.tipoMem)
                                        {
                                            case plcDataType.Boolean:
                                                break;

                                            case plcDataType.Int:
                                            case plcDataType.DInt:
                                            case plcDataType.Word:
                                            case plcDataType.DWord:
                                                int.TryParse(item.reqValue, out valInt);
                                                commWriteVal.Value.Value = valInt;
                                                break;

                                            case plcDataType.Real:
                                                double.TryParse(item.reqValue, out valReal);
                                                commWriteVal.Value.Value = valReal;
                                                break;

                                            case plcDataType.String:
                                                commWriteVal.Value.Value = item.reqValue;
                                                break;

                                            default:
                                                break;
                                        }
                                    }
                                    lgInfo($"---------------{Environment.NewLine}OPC-UA data:{Environment.NewLine}NodeId: {commWriteVal.NodeId}{Environment.NewLine}Value: {commWriteVal.Value.Value}{Environment.NewLine}---------------");

                                    // se supero soglia errori lettura --> disconnetto e resetto
                                    if (numErroriCheck >= maxErroriCheck)
                                    {
                                        lgInfo($"numErroriCheck: {numErroriCheck} --> richiesta disconnessione adapter con tryDisconnect");

                                        numErroriCheck = 0;
                                        tryDisconnect();
                                    }
                                    else
                                    {
                                        if (!string.IsNullOrEmpty(memAddrWrite) && !string.IsNullOrEmpty($"{commWriteVal.NodeId}"))
                                        {
                                            // cerco se ci fosse già --> aggiorno
                                            if (nodes2Write.ContainsKey($"{commWriteVal.NodeId}"))
                                            {
                                                nodes2Write[$"{commWriteVal.NodeId}"] = commWriteVal;
                                            }
                                            else
                                            {
                                                nodes2Write.Add($"{commWriteVal.NodeId}", commWriteVal);
                                            }
                                            // 2023.09.19 resetto richiesta
                                            item.reqValue = "";
                                        }
                                        else
                                        {
                                            lgInfo($"Errore: memAddrWrite vuoto!");
                                        }
                                    }
                                }
                                else
                                {
                                    lgInfo($"Errore uid non trovato in area write memory: {item.uid}, ci sono {memMap.mMapWrite.Count} in area write");
                                }
                            }
                            catch (Exception exc)
                            {
                                lgError($"Eccezione in fase di plcWriteParams per item {item.uid} con valore {item.value}{Environment.NewLine}{exc}");
                            }
                        }
                    }

                    if (connectionOk && (nodes2Write.Count + nodesUdt2Write.Count) > 0)
                    {
                        List<WriteValue> listWriteReq = nodes2Write.Select(x => x.Value).ToList();
                        // valori "diretti"
                        if (listWriteReq.Count > 0)
                        {
                            var toWrite = new Dictionary<NodeId, IEnumerable<string>>();
                            foreach (var item in listWriteReq)
                            {
                                toWrite.Add(item.NodeId, new List<string>() { $"{item.Value.Value}" });
                            }
                            // scrittura modalità siemens
                            UA_ref.WriteValues(toWrite);
                        }
                        // se valori Udt con modalità alternativa
                        if (nodesUdt2Write.Count > 0)
                        {
                            foreach (var item in nodesUdt2Write)
                            {
                                UA_ref.WriteNodesUdt(item.Key, item.Value);
                            }
                        }
                    }
                }
            }
        }

        #endregion Protected Methods
    }
}