using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Xml.Serialization;

namespace GFATask
{
    /// <summary>
    /// Stellt eine FrontEnd-Update Klasse dar, welche über eine Datei, Änderungen eines gewissen Typs(<T>) ausgibt
    /// </summary>
    /// <typeparam name="T">Welcher Klassentyp wird in der Datei gespeichert</typeparam>
    public class FrontEndRefresh<T>
    {
        /// <summary>
        /// Löst ein Ereignis mit dem angegebenen Ereignis-Typen (<T>) aus, welches eine Veränderung in der Datei entdeckt hat
        /// </summary>
        public event EventHandler<FrontEndRefreshEventArgs<T>> Updated;

        /// <summary>
        /// Löst ein Ereignis aus, sobald ein Fehler im Programmcode erzeugt wird
        /// </summary>
        public event EventHandler<FrontEndErrorEventArgs> Error;
        
        /// <summary>
        /// Threadsichere Ausführung des Update-Ereignisses
        /// </summary>
        private SynchronizationContext synccontext;

        /// <summary>
        /// Das FileSystemWatcher-Objekt
        /// </summary>
        private FileSystemWatcher fsw;

        /// <summary>
        /// Pfad zur Datei die überwacht werden soll
        /// </summary>
        private string path;

        /// <summary>
        /// Verhindert das mehrfache Ausführen einer geänderten Datei (Zugriff und Speichern werden immer als Änderung erkannt)
        /// </summary>
        private Dictionary<string, bool> acceptchange = new Dictionary<string, bool>();


        private bool _selfupdate = false;
        /// <summary>
        /// Bestimmt, ob das Update auch bei dem Host ausgeführt wird, der die Datei verändert hat
        /// </summary>
        public bool SelfUpdate
        {
            get => _selfupdate;
            set => _selfupdate = value;
        }


        private bool _active = true;
        /// <summary>
        /// Überprüft, ob Updates an FrontEnds gesendet werden sollen
        /// </summary>
        public bool Active
        {
            get => _active;
            set => _active = value;
        }

        /// <summary>
        /// Stellt eine FrontEnd-Update Klasse dar, welche über eine Datei, Änderungen eines gewissen Typs(<T>) ausgibt
        /// </summary>
        /// <param name="filename">Gibt den Pfad zur überwachenden Datei an. Ist diese noch nicht vorhanden, wird sie erstellt</param>
        public FrontEndRefresh(string filename)
        {
            try
            {
                path = filename;
                if (!File.Exists(path))
                    File.Create(path).Close();
                using(FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
                {
                    fsw = new FileSystemWatcher(Path.GetDirectoryName(fs.Name), Path.GetFileName(fs.Name));
                    fsw.Changed += Fsw_Changed;
                }
            }
            catch (Exception ex)
            {
                Error?.Invoke(this, new FrontEndErrorEventArgs(ex));
            }
        }

        private void Fsw_Changed(object sender, FileSystemEventArgs e)
        {
            // Diese Methode überspringt die Änderung beim ersten Mal
            if (acceptchange.ContainsKey(path))
                acceptchange[path] = true;
            else
                acceptchange.Add(path, false);
            if (acceptchange[path])
            {
                try
                {
                    if (synccontext != null)
                    {
                        synccontext.Post(s =>
                        {
                            FrontEndObject o = this.DeserializeObject<FrontEndObject>();
                            if (_selfupdate || (!_selfupdate && o.HostName != Dns.GetHostName()))
                            {
                                acceptchange.Remove(path);
                                Updated?.Invoke(this, new FrontEndRefreshEventArgs<T>(o.Data, path, o.CurrentAssembly, o.HostName, o.IPv4, o.UpdateTime));
                            }
                        }, null);
                    }
                    else
                    {
                        FrontEndObject o = this.DeserializeObject<FrontEndObject>();
                        if (_selfupdate || (!_selfupdate && o.HostName != Dns.GetHostName()))
                        {
                            acceptchange.Remove(path);
                            Updated?.Invoke(this, new FrontEndRefreshEventArgs<T>(o.Data, path, o.CurrentAssembly, o.HostName, o.IPv4, o.UpdateTime));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }

        /// <summary>
        /// Schreibt das Objekt als XML-Model in die Datei
        /// </summary>
        /// <typeparam name="U"></typeparam>
        /// <param name="toSerialize"></param>
        private void SerializeObject<U>(U toSerialize)
        {
            synccontext = SynchronizationContext.Current;
            System.Serialization.Xml.SerializeToFile(path, toSerialize);
        }


        /// <summary>
        /// Liest das XML-Model als Objekt, aus der Datei aus
        /// </summary>
        /// <typeparam name="U"></typeparam>
        /// <returns></returns>
        private U DeserializeObject<U>()
        {
            synccontext = SynchronizationContext.Current;
            using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
            {
                //if (Verifying.IsFileLocked(new FileInfo(fs.Name)))
                //    return default(U);
                //else
                    return System.Serialization.Xml.DeserializeFromFile<U>(fs.Name);
            }
        }


        /// <summary>
        /// Führt ein Update am FrontEnd und Remote-FrontEnds aus, sofern die Klasse aktiv / gestartet ist
        /// </summary>
        /// <param name="data"></param>
        public void Update(T data)
        {
            try
            {
                if (!_active)
                    return;

                FrontEndObject o = new FrontEndObject()
                {
                    Data = data,
                    CurrentAssembly = Assembly.GetExecutingAssembly().GetName().Name,
                    HostName = Dns.GetHostName(),
                    IPv4 = Net.GetIPv4(Dns.GetHostName()).ToString(),
                    UpdateTime = DateTime.Now
                };
                this.SerializeObject(o);
                if (_selfupdate)
                {
                    if (synccontext != null)
                        synccontext.Post(s => Updated?.Invoke(this, new FrontEndRefreshEventArgs<T>(o.Data, path, o.CurrentAssembly, o.HostName, o.IPv4, o.UpdateTime)), null);
                    else
                        Updated?.Invoke(this, new FrontEndRefreshEventArgs<T>(o.Data, path, o.CurrentAssembly, o.HostName, o.IPv4, o.UpdateTime));
                }
            }
            catch (Exception ex)
            {
                Error?.Invoke(this, new FrontEndErrorEventArgs(ex));
            }
        }


        /// <summary>
        /// Startet die Überwachung
        /// </summary>
        public void Start()
        {
            fsw.EnableRaisingEvents = true;
            synccontext = SynchronizationContext.Current;
        }

        /// <summary>
        /// Stopt die Überwachung
        /// </summary>
        public void Stop()
        {
            fsw.EnableRaisingEvents = false;
        }


        public class FrontEndObject
        {
            public T Data { get; set; }
            public int Length { get; set; }
            public string HostName { get; set; }
            public string IPv4 { get; set; }
            public DateTime UpdateTime { get; set; }
            public string CurrentAssembly { get; set; }


            public FrontEndObject() { }

            new public string ToString()
            {
                return string.Join("\n", this.GetType().GetProperties().Select(prop => prop.Name + ": " + prop.GetValue(this)));
            }
        }


    }


    public class FrontEndRefreshEventArgs<T>
    {

        [JsonProperty("assembly")]
        public string Assembly { get; set; }

        [JsonProperty("host")]
        public string Host { get; set; }

        [JsonProperty("ipv4")]
        public string IPv4 { get; set; }

        [JsonProperty("updatetime")]
        public DateTime UpdateTime { get; set; } = DateTime.Now;

        [JsonProperty("path")]
        public string Path { get; set; } = string.Empty;

        [JsonProperty("data")]
        public T Data { get; set; }

        public FrontEndRefreshEventArgs(T data, string path, string assembly, string host, string ipv4, DateTime updatetime)
        {
            Data = data;
            Path = path;
            Assembly = assembly;
            Host = host;
            IPv4 = ipv4;
            UpdateTime = updatetime;
        }
    }


    public class FrontEndErrorEventArgs
    {
        public Exception Ex { get; set; }

        public FrontEndErrorEventArgs(Exception ex) =>
            Ex = ex;
    }
}
