using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using EasyDevCore.Common;
using System.Text.Json;
using System.Xml;
using EasyDevCore.Common.Security;
using System.Collections;
using System.Runtime.CompilerServices;

namespace EasyDevCore.Configuration
{
    /// <summary>
    /// Easy Configuration file 
    /// </summary>
    public class EasyConfiguration : INotifyPropertyChanged, IEnumerable<EasyConfigItem>, IDisposable
    {

        System.Timers.Timer _AutoSaveTimer;
        private bool _DataChanged = false;

        /// <summary>
        /// Gets or sets a value indicating whether this instance is loading file.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is loading file; otherwise, <c>false</c>.
        /// </value>
        protected bool IsLoadingFile { get; set; } = false;

        private void OptionChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case nameof(Options.AutoSaveDelay):
                    if (_AutoSaveTimer != null)
                    {
                        _AutoSaveTimer.Interval = Options.AutoSaveDelay;
                    }
                    break;
                case nameof(Options.AutoSave):
                    if (_AutoSaveTimer == null)
                    {
                        _AutoSaveTimer = new System.Timers.Timer(Options.AutoSaveDelay * 1000);
                        _AutoSaveTimer.Elapsed += AutoSaveElapsed;
                        _AutoSaveTimer.Start();
                    }
                    break;
            }
        }

        /// <summary>
        /// Gets the options.
        /// </summary>
        /// <value>
        /// The options.
        /// </value>
        public EasyConfigOptions Options { get; private set; }

        /// <summary>
        /// The items
        /// </summary>
        protected Dictionary<string, EasyConfigItem> Items { get; private set; } = new Dictionary<string, EasyConfigItem>(StringComparer.InvariantCultureIgnoreCase);

        /// <summary>
        /// Prevents a default instance of the <see cref="EasyConfiguration"/> class from being created.
        /// </summary>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="System.NotImplementedException"></exception>
        /// <exception cref="System.NotSupportedException"></exception>
        private EasyConfiguration()
        {
            LoadDefaultConfig();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EasyConfiguration" /> class.
        /// </summary>
        /// <param name="options">The options.</param>
        public EasyConfiguration(EasyConfigOptions options) : this()
        {
            Options = options;
            options.PropertyChanged += OptionChanged;
            string configFile = Options.FileName;
            var ext = Path.GetExtension(Options.FileName).ToLower();
            if (Options.ConfigType == EasyConfigFileType.Auto)
            {
                if (!ext.In(".xml", ".json"))
                {
                    throw new ConfigurationErrorsException("Extension of file must be .xml/.json");
                }
                Options.ConfigType = ext.Switch(new[] { ".xml", ".json" }, new[] { EasyConfigFileType.Xml, EasyConfigFileType.Json });
            }
            else
            {
                if (string.IsNullOrWhiteSpace(ext))
                {
                    ext = Options.ConfigType.Switch(new[] { EasyConfigFileType.Xml, EasyConfigFileType.Json }, new[] { ".xml", ".json" });
                    configFile += ext;
                }
            }
            Options.FileName = configFile;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EasyConfiguration" /> class.
        /// </summary>
        /// <param name="configFile">The configuration file.</param>
        /// <param name="password">The password.</param>
        public EasyConfiguration(string configFile, string password = "") : this(new() { FileName = configFile, EncryptPassword = password })
        {
        }

        /// <summary>
        /// Loads the default config.
        /// </summary>
        private void LoadDefaultConfig()
        {
            PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(p => p.CanWrite && p.CanRead).ToArray();
            foreach (PropertyInfo property in properties)
            {
                EasyConfigItem item = new();
                string itemName = property.Name;
                Items[itemName] = item;
                item.PropertyType = property.PropertyType;
                Attribute[] attrs = (Attribute[])property.GetCustomAttributes(typeof(EasyConfigPropertyAttribute), false);
                if (attrs.Length > 0)
                {
                    EasyConfigPropertyAttribute configItem = (EasyConfigPropertyAttribute)attrs[0];
                    item.Property = configItem;
                    item.Value = configItem.DefaultValue.ChangeType(property.PropertyType);
                }
                else
                {
                    item.Property = new();
                }
            }
            DataChanged = false;
        }

        /// <summary>
        /// Clears this instance.
        /// </summary>
        public void Clear()
        {
            Items.Clear();
            LoadDefaultConfig();
        }

        /// <summary>
        /// Gets or sets the encryption handler.
        /// </summary>
        /// <value>
        /// The encryption handler (string: value, bool: true/false - Encrypted/Decrypted).
        /// </value>
        public Func<string, bool, string> EncryptionHandler { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether [data changed].
        /// </summary>
        /// <value>
        ///   <c>true</c> if [data changed]; otherwise, <c>false</c>.
        /// </value>
        public bool DataChanged
        {
            get
            {
                return _DataChanged;
            }
            protected set
            {
                _DataChanged = value;
                if (_AutoSaveTimer != null)
                {
                    if (DataChanged)
                    {
                        _AutoSaveTimer.Stop();
                        _AutoSaveTimer.Start();
                    }
                    else
                    {
                        _AutoSaveTimer.Stop();
                    }
                }
            }
        }

        /// <summary>
        /// Automatics the save elapsed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Timers.ElapsedEventArgs"/> instance containing the event data.</param>
        private void AutoSaveElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            _AutoSaveTimer.Stop();
            lock (this)
            {
                this.Save();
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="System.Object" /> with the specified name.
        /// </summary>
        /// <value>
        /// The <see cref="System.Object" />.
        /// </value>
        /// <param name="name">The name (case insensitive).</param>
        /// <returns></returns>
        /// <exception cref="ConfigurationErrorsException"></exception>
        /// <exception cref="System.NotSupportedException"></exception>
        public object this[string name]
        {
            get
            {
                return Items[name].Value;
            }
            set
            {
                var item = Items[name];
                if (item.Value != value)
                {
                    if ((item.Property.MinValue != null && value.CompareValue(item.Property.MinValue) == 1)
                        || (item.Property.MaxValue != null && value.CompareValue(item.Property.MaxValue) == 2))
                    {
                        var s = string.Format("[{0}] must be {1}{2}{3}", name, (item.Property.MinValue != null ? $">= {item.Property.MinValue}" : ""),
                            (item.Property.MinValue != null && item.Property.MaxValue != null ? " and " : ""), (item.Property.MaxValue != null ? $"<= {item.Property.MaxValue}" : ""));
                        throw new ConfigurationErrorsException(s);
                    }
                    Items[name].Value = value;
                    if(!IsLoadingFile)
                    {
                        NotifyPropertyChanged(name);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the specified name.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        protected T Get<T>([CallerMemberName] string name = null)
        {
            return (T)this[name];
        }

        /// <summary>
        /// Sets the specified value.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="name">The name.</param>
        protected void Set(object value, [CallerMemberName] string name = null)
        {
            this[name] = value;
        }

        /// <summary>
        /// Existses the specified name.
        /// </summary>
        /// <param name="name">The name (case insensitive).</param>
        /// <returns></returns>
        public bool Exists(string name) => Items.ContainsKey(name);

        /// <summary>
        /// Gets the content of the file.
        /// </summary>
        /// <returns></returns>
        /// hjnui
        private string GetFileContent()
        {
            return System.IO.File.ReadAllText(Options.FileName);
        }

        /// <summary>
        /// Saves the content of the file.
        /// </summary>
        private void SaveFileContent(string content)
        {
            System.IO.File.WriteAllText(Options.FileName, content);
        }

        /// <summary>
        /// Loads the item value.
        /// </summary>
        /// <param name="itemName">Name of the item.</param>
        /// <param name="rawValue">The raw value.</param>
        private void LoadItemValue(string itemName, string rawValue)
        {
            object value = rawValue;
            if (Items.ContainsKey(itemName))
            {
                var item = Items[itemName];
                if (item.Property.IsEncrypted)
                {
                    if (EncryptionHandler != null)
                    {
                        value = EncryptionHandler(rawValue, false);
                    }
                    else
                    {
                        value = EncryptionHelper.DecryptString(Options.EncryptPassword, rawValue);
                    }
                    if ((string)value == rawValue) //Value not encrypted
                    {
                        DataChanged = true;
                    }
                }
                if (item.Property.IsSerialization)
                {
                    value = JsonSerializer.Deserialize(((string)value).FromBase64(), item.PropertyType);
                }
                else
                {
                    value = value.ChangeType(item.PropertyType);
                }
                this[itemName] = value;
            }
            else
            {
                Items.Add(itemName, new() { PropertyType = typeof(string), Value = value, Property = new()});
            }
        }

        /// <summary>
        /// Loads the json configuration.
        /// </summary>
        private void LoadJsonConfig()
        {
            JsonSerializer.Deserialize<Dictionary<string, object>>(GetFileContent()).ForEach(kv => LoadItemValue(kv.Key, kv.Value == null ? null : kv.Value.ToString()));
        }

        /// <summary>
        /// Loads the XML configuration.
        /// </summary>
        private void LoadXmlConfig()
        {
            using (System.Xml.XmlReader reader = XmlReader.Create(new StringReader(GetFileContent())))
            {
                while (reader.Read() && !(reader.NodeType == XmlNodeType.Element && reader.LocalName.Equals("configuration", StringComparison.InvariantCultureIgnoreCase)))
                {
                }
                while (reader.Read())
                {
                    reader.MoveToContent();
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        string itemName = reader.Name;
                        string itemValue = reader.ReadInnerXml();
                        LoadItemValue(itemName, itemValue);
                        //data[itemName] = itemValue;
                    }
                }
            }
        }

        /// <summary>
        /// Loads this settings
        /// </summary>
        /// <param name="isAutoCreate">if set to <c>true</c> [is automatic create].</param>
        public void Load(bool isAutoCreate = true)
        {
            if (isAutoCreate)
            {
                if (!File.Exists(Options.FileName))
                {
                    Save();
                }       
            }
            DataChanged = false;
            try
            {
                IsLoadingFile = true;
                if (Options.ConfigType == EasyConfigFileType.Json)
                {
                    LoadJsonConfig();
                }
                if (Options.ConfigType == EasyConfigFileType.Xml)
                {
                    LoadXmlConfig();
                }
                if (DataChanged) // Has encrypted value
                {
                    Save();
                }
            }
            finally
            {
                IsLoadingFile = false;
            }
        }

        /// <summary>
        /// Cammels the case.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <returns></returns>
        private string CammelCase(string key) => key.Substitude(0, key.Substring(0, 1).ToLower());

        /// <summary>
        /// Gets the save items.
        /// </summary>
        /// <returns></returns>
        private Dictionary<string, string> GetSaveItems()
        {
            Dictionary<string, string> data = new();
            JsonSerializerOptions options = new() { WriteIndented = true };
            foreach (var item in Items.Where(it => !it.Value.Property.IsInternal))
            {
                var itemName = item.Value.Property.Alias ?? CammelCase(item.Key);
                var property = item.Value.Property;
                var value = string.Empty;
                if (property.IsSerialization)
                {
                    value = JsonSerializer.Serialize(item.Value.Value, item.Value.PropertyType, options: options);
                }
                else
                {
                    value = item.Value.Value.StringFrom();
                }
                if (property.IsEncrypted)
                {
                    if (EncryptionHandler != null)
                    {
                        value = EncryptionHandler(value, true);
                    }
                    else
                    {
                        value = EncryptionHelper.EncryptString(Options.EncryptPassword, value);
                    }
                }
                data[itemName] = value;
            }
            return data;
        }

        /// <summary>
        /// Saves the json configuration.
        /// </summary>
        private void SaveJsonConfig()
        {
            var data = GetSaveItems();
            SaveFileContent(JsonSerializer.Serialize(data, data.GetType(), options: new() { WriteIndented = true }));
        }

        /// <summary>
        /// Saves the XML configuration.
        /// </summary>
        private void SaveXmlConfig()
        {
            var data = GetSaveItems();
            XmlDocument doc = XmlHelper.CreateDocument("configuration");
            XmlNode configNode = XmlHelper.SelectSingleNode(doc, "configuration");
            foreach(var kp in data)
            {
                XmlHelper.AddSingleChildNode(configNode, kp.Key, kp.Value);
            }
            SaveFileContent(XmlHelper.DocToString(doc));
        }

        /// <summary>
        /// Saves this settings
        /// </summary>
        public virtual void Save()
        {
            if (Options.ConfigType == EasyConfigFileType.Json)
            {
                SaveJsonConfig();
            }
            if (Options.ConfigType == EasyConfigFileType.Xml)
            {
                SaveXmlConfig();
            }
            DataChanged = false;
        }

        #region IEnumerable<EasyConfigurationItem> Members
        IEnumerator<EasyConfigItem> IEnumerable<EasyConfigItem>.GetEnumerator()
        {
            return Items.Values.GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return Items.Values.GetEnumerator();
        }
        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName)
        {
            DataChanged = true;
            OnPropertyChanged(propertyName);
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Called when [property changed].
        /// </summary>
        /// <param name="name">The name.</param>
        protected virtual void OnPropertyChanged(string name)
        {
        }
        #endregion
        #region IDisposable Members
        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Items.Clear();
            if (_AutoSaveTimer != null)
            {
                if (_AutoSaveTimer.Enabled)
                {
                    AutoSaveElapsed(null, null);
                }
                _AutoSaveTimer.Dispose();
            }
        }
        #endregion
    }

}
