using System;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using Opc;


namespace OPCConsole
{
    class Program
    {
        private static UInt64 sequence_id = 0;

        private static Dictionary<String, String> register_mapping = new Dictionary<String, String>();

        private static String xml_filename;
        private static List<String> xml_opc_register_names = new List<String>();
        private static String xml_opc_url;
        private static String xml_opc_group;
        private static String xml_opc_output_filename;

        private static Opc.URL opc_url;
        private static OpcCom.Factory opc_factory;
        private static Opc.Da.Server opc_server;
        private static Opc.Da.Subscription opc_group;
        private static Opc.Da.SubscriptionState opc_group_state;
        private static Opc.Da.Item[] opc_items;

        static void ReadXmlConfig()
        {
            Console.WriteLine("Reading GMM XML configuration file {0}...", xml_filename);

            foreach (var element in XElement.Load(xml_filename).Elements("VALUE"))
            {
                String name = element.Attribute("name").Value;
                String val = element.Attribute("val").Value;

                if (name == "opc_url")
                {
                    xml_opc_url = val;
                    Console.WriteLine("OPC url: {0}", xml_opc_url);
                }
                else if (name == "opc_output_filename")
                {
                    xml_opc_output_filename = val;
                    Console.WriteLine("OPC output filename: {0}", xml_opc_output_filename);
                    System.IO.File.Delete(xml_opc_output_filename);
                }
                else if (name == "opc_group")
                {
                    xml_opc_group = val;
                    Console.WriteLine("OPC group: {0}", xml_opc_group);
                }
                else if (name.EndsWith("_opc_kiln_register") ||
                            name.EndsWith("_opc_pkg_register") ||
                            name.EndsWith("_opc_run_register"))
                {
                    Console.WriteLine("OPC register to monitor for {0}: {1}", name, val);
                    xml_opc_register_names.Add(val);
                }
            }
        }

        static void InitializeOpc()
        {
            Console.WriteLine("Initializing OPC server connection...");

            opc_url = new Opc.URL(xml_opc_url);
            opc_factory = new OpcCom.Factory();
            opc_server = new Opc.Da.Server(opc_factory, opc_url);
            opc_server.Connect();

            Console.WriteLine("Creating OPC group...");

            opc_group_state = new Opc.Da.SubscriptionState();
            opc_group_state.Name = "GMM";
            opc_group_state.UpdateRate = 1000; // milliseconds?
            opc_group_state.Active = true;

            opc_group = (Opc.Da.Subscription)opc_server.CreateSubscription(opc_group_state);

            Console.WriteLine("Subscribing to {0} OPC items...", xml_opc_register_names.Count);

            opc_items = new Opc.Da.Item[xml_opc_register_names.Count];
            for (int idx = 0; idx < xml_opc_register_names.Count; idx++)
            {
                String name = "[" + xml_opc_group + "]" + xml_opc_register_names[idx] /*+ ",L1"*/; // L=length of word to read
                Console.WriteLine("-> #{0}: \"{1}\"", idx, name);
                opc_items[idx] = new Opc.Da.Item();
                opc_items[idx].ItemName = name;
            }
            opc_items = opc_group.AddItems(opc_items);
            Console.WriteLine("OPC group \"{0}\" now contains {1} items", opc_group.Name, opc_group.Items.Length);

            Console.WriteLine("Setting callback for OPC group \"{0}\"...", opc_group.Name);
            opc_group.DataChanged += new Opc.Da.DataChangedEventHandler(callback_group_data_changed); // set up call back

            Console.WriteLine("OPC initialization is complete!", opc_group.Name);
        }


        static void Startup()
        {
            try
            {
                ReadXmlConfig();
                InitializeOpc();
            }
            catch (Exception ex)
            {
                Console.WriteLine("OPC Console caught an exception during startup:\r\n{0}", ex.ToString());
                throw;
            }
        }


        static void LoopForever()
        {
            String old_timestamp = "";
            while (true)
            {
                String new_timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
                if (old_timestamp == new_timestamp)
                {
                    Console.Write(".");
                }
                else
                {
                    Console.WriteLine();
                    Console.Write("{0} ", new_timestamp);
                    old_timestamp = new_timestamp;
                }
                Thread.Sleep(1000);
            }
        }


        static void callback_group_data_changed(object subscriptionHandle, object requestHandle, Opc.Da.ItemValueResult[] values)
        {
            sequence_id++;

            Console.WriteLine("\r\nSequence #{0}: OPC data change callback invoked with {1} value(s)", sequence_id, values.Length);

            for (int idx = 0; idx < values.Length; idx++)
            {
                try
                {
                    if (values[idx] is null)
                    {
                        Console.WriteLine("Sequence #{0}: skipping object at index #{1} because it is NULL", sequence_id, idx);
                        continue;
                    }

                    String item_name = values[idx].ItemName;
                    String item_value = values[idx].Value.ToString();
                    Console.WriteLine("Sequence #{0} index #{1}: \"{2}\"=\"{3}\"", sequence_id, idx, item_name, item_value);
                    register_mapping[item_name] = item_value;
                }
                catch (Exception ex)
                {
                    Console.WriteLine("OPC data change callback caught an exception while processing index #{0}:\r\n{1}", idx, ex.ToString());
                }
            }

            write_output_file();
        }


        static void write_output_file()
        {
            Console.WriteLine("Sequence #{0}: saving {1} entries to OPC output file {2}", sequence_id, register_mapping.Count, xml_opc_output_filename);

            try
            {
                String output = sequence_id.ToString() + "\r\n";

                foreach (KeyValuePair<string, string> entry in register_mapping)
                {
                    output += entry.Key + "=" + entry.Value + "\r\n";
                }

                System.IO.File.WriteAllText(xml_opc_output_filename, output);
            }
            catch (Exception ex)
            {
                Console.WriteLine("OPC write_output_file() caught an exception:\r\n{0}", ex.ToString());
            }
        }


        static void Main(string[] args)
        {
            // see example OPC application:
            // https://infosys.beckhoff.com/english.php?content=../content/1033/tcopcserver/html/sample1_netapi.htm
            // https://infosys.beckhoff.com/content/1033/tcopcserver/DEMO/OpcNetApiSample1.zip

            Console.WriteLine("OPC Console for GMM started at {0}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));

            xml_filename = "C:\\ProgramData\\GormanMoistureMeter\\GormanMoistureMeter.cfg";
            if (args.Length > 0)
            {
                xml_filename = args[0];
            }

            Startup();
            LoopForever();
        }
    }
}
