using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using TwinCAT.Ads;
using TwinCAT.Ads.TypeSystem;

namespace IOB_WIN_BECKHOFF
{
    /// <summary>
    /// Client comunicazioni con PLC Beckhoff TwinCat
    /// </summary>

    public class TcAdsClient
    {
        #region Public Fields

        public System.Collections.Concurrent.ConcurrentQueue<ComandiADS> CodaComandi;

        public TwinCAT.Ads.TcAdsSymbolInfoLoader InfoLoader;

        /// <summary>
        /// Dizionario delle variabili monitorate (gestite ad evento x modifica), chiave = nome var, valore = symbol x gestione variabile
        /// </summary>
        public Dictionary<string, Symbol> MonitoredItems = new Dictionary<string, Symbol>();

        public TwinCAT.TypeSystem.ISymbolLoader SymbolLoaderInstance;

        public TcAdsSymbolInfoCollection Symbols;

        #endregion Public Fields

        #region Public Constructors

        /// <summary>
        /// Inizializza un oggetto ADS x gestione classe comunicazione con TwinCat
        /// </summary>
        /// <param name="MonitVars">
        /// Lista dei nomi delle variabili da gestire ad eventChange (es stato macchina)
        /// </param>
        /// <param name="indirizzo">indirizzo tipo AmsNetId</param>
        /// <param name="porta">
        /// Porta comunicazione: Connect to local PLC - Runtime 1 - TwinCAT2 Port=801, TwinCAT3 Port=851
        /// </param>
        public TcAdsClient(List<string> MonitVars, string indirizzo = "local", int porta = 851)
        {
            _MonitVars = MonitVars;
            MonitoredItems = new Dictionary<string, Symbol>();
            notifyStream = new AdsStream();
            newNotificationStream = new AdsStream();
            addressList = new Dictionary<int, Tuple<int, int>>();
            bool ready = false;
            while (!ready)
            {
                try
                {
                    //LETTURA DEL VETTORE DI INIZIALIZZAZIONE
                    if (adsClient == null) adsClient = new TwinCAT.Ads.TcAdsClient();
                    // Connect to local PLC - Runtime 1 - TwinCAT2 Port=801, TwinCAT3 Port=851
                    if (indirizzo == "")
                    {
                        adsClient.Connect(porta);
                    }
                    else
                    {
                        if (adsClient.IsConnected == false) adsClient.Connect(indirizzo, porta);
                    }

                    SymbolLoaderInstance = SymbolLoaderFactory.Create(adsClient, SymbolLoaderSettings.Default);
                    InfoLoader = adsClient.CreateSymbolInfoLoader();
                    Symbols = InfoLoader.GetSymbols(true);
                    ready = true;
                }
                catch (Exception ex)
                {
                    System.Threading.Thread.Sleep(100);
                    ready = false;
                    Debug.Print(ex.Message);
                }
            }

            // inizializzo dizionario delle variabili gestite
            foreach (var item in _MonitVars)
            {
                var currSymbol = (Symbol)SymbolLoaderInstance.Symbols[item];
                currSymbol.NotificationSettings = new AdsNotificationSettings(AdsTransMode.OnChange, 100, 100);
                currSymbol.ValueChanged += MonItem_ValueChanged;
                // aggiungo al dict
                MonitoredItems.Add(item, currSymbol);
            }

            StatoMacchina = (Symbol)SymbolLoaderInstance.Symbols["VarADS.StatoMacchina"];
            StatoMacchina.NotificationSettings = new AdsNotificationSettings(AdsTransMode.OnChange, 100, 100);
            StatoMacchina.ValueChanged += StatoMacchina_ValueChanged;

#if false
            notifyposition = 0;
#endif
            cts = new CancellationTokenSource();

            //adsClient.AdsNotification += new AdsNotificationEventHandler(adsClient_AdsNotification);

            CodaComandi = new System.Collections.Concurrent.ConcurrentQueue<ComandiADS>();
            cts = new CancellationTokenSource(); //Task require CancellationToken.cancel() to stop
            Action<object> Azione = commandDispatcher;
            //Definisce e Crea un Task di base a priorità favorevole
            dispatchertask = new Task(Azione, cts.Token, TaskCreationOptions.PreferFairness);
            dispatchertask.Start();
        }

        #endregion Public Constructors

        #region Public Delegates

        public delegate void CountChangedEventHandler(TcAdsClient sender, int newCount);

        public delegate void StatusChangedEventHandler(TcAdsClient sender, int newStatus);

        public delegate void ValueChangedEventHandler(TcAdsClient sender, string key, string value);

        #endregion Public Delegates

        #region Public Events

        public event CountChangedEventHandler CountChanged;

        public event StatusChangedEventHandler StatusChanged;

        public event ValueChangedEventHandler ValueChanged;

        #endregion Public Events

        #region Public Properties

        public TwinCAT.Ads.TcAdsClient Client
        {
            get { return adsClient; }
        }

        public bool Connected
        {
            get
            {
                bool answ = false;
                if (adsClient != null)
                {
                    answ = adsClient.IsConnected;
                }
                return answ;
            }
        }

        public bool EnableEvents { get; set; }

        public int Status
        {
            get
            {
                var stato = ReadVariabile("VarADS.StatoMacchina");
                if (stato != null) _status = (int)stato;
                else
                {
                    throw new Exception("Errore lettura stato");
                }
                return _status;
            }
        }

        #endregion Public Properties

        #region Public Methods

        public void dispose()
        {
            adsClient.Dispose();
        }

        public TcAdsSymbolInfo GetSymbolInfo(string nome)
        {
            try
            {
                var symbol = InfoLoader.FindSymbol(nome);
                return symbol;
            }
            catch (Exception)
            {
                throw;
            }
        }

        public object ReadVariabile(ref TcAdsSymbolInfo variabile)
        {
            var comando = new ComandiADS { ComandoScrittua = false, Symbol = variabile };
            return ReadVariabile(comando);
        }

        public object ReadVariabile(string symbolName, Type type = null)
        {
            var comando = new ComandiADS { ComandoScrittua = false, SymbolName = symbolName };
            return ReadVariabile(comando);
        }

        public bool WriteVariabile(string symbolName, object value, bool syncronous = false)
        {
            var comando = new ComandiADS { Value = value, ComandoScrittua = true, SymbolName = symbolName };
            return WriteVariabile(comando, syncronous);
        }

        public bool WriteVariabile(TcAdsSymbolInfo symbol, object value, bool syncronous = false)
        {
            var comando = new ComandiADS { Value = value, ComandoScrittua = true, Symbol = symbol };
            return WriteVariabile(comando, syncronous);
        }

        #endregion Public Methods

        #region Public Classes

        public class ComandiADS
        {
            #region Public Fields

            public bool ComandoScrittua;
            public bool Error;
            public TcAdsSymbolInfo Symbol;
            public string SymbolName;
            public ManualResetEventSlim Updating;
            public object Value;

            #endregion Public Fields

            #region Public Constructors

            public ComandiADS()
            {
                Updating = new ManualResetEventSlim(false);
            }

            public ComandiADS(string name)
            {
                SymbolName = name;
                Updating = new ManualResetEventSlim(false);
            }

            public ComandiADS(TcAdsSymbolInfo info)
            {
                Symbol = info;
                Updating = new ManualResetEventSlim(false);
            }

            #endregion Public Constructors
        }

        #endregion Public Classes

        #region Private Fields

        /// <summary>
        /// elenco delle variabili monitorate x change event
        /// </summary>
        private List<string> _MonitVars = new List<string>();

        private int _status;

        private List<int> addedSignalationList = new List<int>();

        /// <summary>
        /// Dizionario di conversione da indice a index group e index offset
        /// </summary>
        private Dictionary<int, Tuple<int, int>> addressList;

        private TwinCAT.Ads.TcAdsClient adsClient;

        private CancellationTokenSource cts;

        private System.Threading.Tasks.Task dispatchertask;

#if false
        //private Action<object> dispatcher;
        private int eventHandle;

        private int SegnalazioniADSEventHandle, StatusChangedEventHandle, MessageQueuedEventHandle;

        private int notifyposition;
#endif
        private object lockobj = new object();
        private AdsStream newNotificationStream;
        private AdsStream notifyStream;
        private Symbol StatoMacchina;

        #endregion Private Fields

        #region Private Methods

        private void commandDispatcher(object tk)
        {
            ComandiADS comando;
            Thread.CurrentThread.Name = "ADS Command Dispatcher";
            CancellationToken chiudi = (CancellationToken)tk;
            while (!chiudi.IsCancellationRequested)
            {
                Thread.Sleep(1);
                if (CodaComandi.Count <= 0)
                {
                    Thread.Sleep(1);
                    continue;
                }
                if (CodaComandi.Count > 100) Debug.Print("CODA COMANDI! " + CodaComandi.Count.ToString());
                if (!CodaComandi.TryDequeue(out comando)) continue;

                if (CodaComandi.Count > 1000) continue;

                if (comando.ComandoScrittua) //gestione scrittura
                {
                    try
                    {
                        if (comando.SymbolName != "")
                        {
                            if (comando.Symbol == null) comando.Symbol = GetSymbolInfo(comando.SymbolName);
                        }
                        else
                        {
                            comando.SymbolName = comando.Symbol.Name;
                        }
                        if (comando.Value is int && comando.Symbol.Category == TwinCAT.TypeSystem.DataTypeCategory.Array)
                        {
                            var newvalue = new int[comando.Symbol.ArrayInfos[0].Elements];
                            newvalue[0] = (int)comando.Value;
                            comando.Value = newvalue;
                        }
                        if (comando.Value is double && comando.Symbol.Category == TwinCAT.TypeSystem.DataTypeCategory.Array)
                        {
                            var newvalue = new double[comando.Symbol.ArrayInfos[0].Elements];
                            newvalue[0] = (double)comando.Value;
                            comando.Value = newvalue;
                        }
                        adsClient.WriteSymbol(comando.Symbol, comando.Value);
                    }
                    catch (Exception err)
                    {
                        comando.Error = true;
                        Debug.Print(comando.SymbolName + " Scrittura " + err.Message);
                    }
                    comando.Updating.Set();
                }
                else  // gestione lettura
                {
                    try
                    {
                        if (comando.SymbolName != "")
                        {
                            if (comando.Symbol == null) comando.Symbol = GetSymbolInfo(comando.SymbolName);
                        }
                        else
                        {
                            comando.SymbolName = comando.Symbol.Name;
                        }

                        comando.Value = adsClient.ReadSymbol(comando.Symbol);
                    }
                    catch (Exception errore)
                    {
                        Debug.Print(errore.Message);
                        comando.Error = true;
                        Debug.Print("Error reading from ADS: VarName: " + comando.SymbolName);
                    }
                    comando.Updating.Set();
                }
            }
        }

        private void MonItem_ValueChanged(object sender, TwinCAT.TypeSystem.ValueChangedArgs e)
        {
            string newStatus = $"{e.Value}";
            if (ValueChanged != null)
            {
                ValueChanged(this, $"{sender}", newStatus);
            }
        }

        private object ReadVariabile(ComandiADS comando)
        {
            CodaComandi.Enqueue(comando);
            bool test = comando.Updating.Wait(3000);
            if (!test) Debug.Print("Errore attesa lettura: " + comando.SymbolName);
            if (comando.Value == null) Debug.Print("ADS Variabile non trovata: " + comando.SymbolName);
            return comando.Value;
        }

        private void StatoMacchina_ValueChanged(object sender, TwinCAT.TypeSystem.ValueChangedArgs e)
        {
            int newStatus = (int)e.Value;
            if (StatusChanged != null) StatusChanged(this, newStatus);
        }

        private bool WriteVariabile(ComandiADS comando, bool syncronous)
        {
            bool test = true;
            CodaComandi.Enqueue(comando);
            if (syncronous) test = comando.Updating.Wait(3000);
            if (!test) Debug.Print("Errore attesa lettura: " + comando.SymbolName);
            if (comando.Error) Debug.Print("Errore ADS durante la scrittura della variabile: " + comando.SymbolName);
            return !comando.Error;
        }

        #endregion Private Methods
    }
}