using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.Xml;

namespace Inspectron.Settings
{
    
    public class InspectronSettings
    {

        public InspectronSettings(string path=null)
        {
            Assembly assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();

            AssemblyName assemblyName = assembly.GetName();
            _applicationName = assemblyName.Name;
            Version version = assemblyName.Version;
            _versionString = version.Major + "." + version.Minor;

            string startupPath = Path.GetDirectoryName(new Uri(assemblyName.CodeBase).LocalPath);
            _defaultSettingsPath = Path.Combine(startupPath, "DefaultSettings.xml");

            
            
            if (path == null)
            {
                var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                _settingsPath = string.Format("{0}\\{1}\\{2}\\AppSettings.xml", appDataPath, _applicationName, _versionString);
            }
            else
            {
                _settingsPath = Path.Combine(path,"AppSettings.xml");
            }
            
            
            
        }
        internal Path<object> GetSettingsPath(string pathName)
        {
            string[] pathSegments = pathName.Split('/', 16);
            object[] path = new object[pathSegments.Length + 1];

            // first node is the settings tree root
            Tree<object> node = (Tree<object>)_userSettings;
            path[0] = _userSettings;

            // middle nodes are folders
            for (int i = 1; i < path.Length - 1; i++)
            {
                node = GetOrCreateFolder(pathSegments[i - 1], node);
                path[i] = node;
            }

            // leaf node is user settings object
            foreach (Tree<object> leaf in node.Children)
            {
                UserSettingsInfo info = leaf.Value as UserSettingsInfo;
                if (info != null && info.Name == pathSegments[pathSegments.Length - 1])
                {
                    path[path.Length - 1] = leaf;
                    break;
                }
            }

            return new Path<object>(path);
        }
        /// <summary>
        /// Gets or sets the current state of all properties (Memento pattern)</summary>
        public object State
        {
            get
            {
                MemoryStream stream = new MemoryStream();
                Serialize(stream);
                return stream;
            }
            set
            {
                MemoryStream stream = value as MemoryStream;
                if (stream == null)
                    throw new ArgumentException("Not a valid memento");
                stream.Position = 0;
                Deserialize(stream);
            }
        }
        
        private IEnumerable<PropertyDescriptor> UserPropertyDescriptors
        {
            get
            {
                var userSettings = UserSettings as Tree<object>;
                if (userSettings == null)
                    throw new InvalidOperationException("userSettings");
                var all = userSettings.LevelOrder.Where(x => x.Value is UserSettingsInfo);
                foreach (Tree<object> node in all)
                {
                    UserSettingsInfo info = node.Value as UserSettingsInfo;
                    if (info != null)
                    {
                        foreach (PropertyDescriptor property in info.Settings)
                            yield return property;
                    }
                }
            }
        }
        private static readonly object _unusedComponent = new object();
        public void SetDefaults()
        {
            foreach (SettingsInfo info in _settings.Values)
            {
                foreach (SettingsInfo.Setting setting in info.Settings.Values)
                {
                    if (setting.PropertyDescriptor != null &&
                        setting.PropertyDescriptor.CanResetValue(_unusedComponent))
                    {
                        setting.PropertyDescriptor.ResetValue(_unusedComponent);
                    }
                }
            }
            // Load default settings if exist.            
            if (File.Exists(_defaultSettingsPath))
            {
                using (Stream stream = File.OpenRead(_defaultSettingsPath))
                    Deserialize(stream);
            }
        }
        internal List<PropertyDescriptor> GetProperties(Tree<object> tree)
        {
            var info = tree.Value as UserSettingsInfo;
            if (info != null)
                return info.Settings;

            return null;
        }
        protected class SettingsInfo
        {
            /// <summary>
            /// Constructor with name</summary>
            /// <param name="name">Name associated with this group of settings</param>
            public SettingsInfo(string name)
            {
                Name = name;
            }

            /// <summary>
            /// Add a setting to group of settings as a name and value, replacing the previous value if present</summary>
            /// <param name="name">Setting name</param>
            /// <param name="valueString">Setting value</param>
            public void Add(string name, string valueString)
            {
                Setting setting;
                if (Settings.TryGetValue(name, out setting))
                {
                    setting.Set(name, valueString);
                }
                else
                {
                    Settings.Add(name, new Setting(name, valueString));
                }
            }

            /// <summary>
            /// Add a setting to group of settings as a PropertyDescriptor, replacing the previous value if present</summary>
            /// <param name="descriptor">PropertyDescriptor describing setting</param>
            public void Add(PropertyDescriptor descriptor)
            {
                Setting setting;
                if (Settings.TryGetValue(descriptor.Name, out setting))
                {
                    setting.Set(descriptor);
                }
                else
                {
                    Settings.Add(descriptor.Name, new Setting(descriptor));
                }
            }

            /// <summary>
            /// Name associated with this group of settings</summary>
            public readonly string Name;
            /// <summary>
            /// Dictionary for names and values of settings in group</summary>
            public readonly SortedDictionary<string, Setting> Settings = new SortedDictionary<string, Setting>();

            /// <summary>
            /// Class for handling individual setting information</summary>
            public class Setting
            {
                /// <summary>
                /// Constructor with PropertyDescriptor</summary>
                /// <param name="descriptor">PropertyDescriptor describing setting</param>
                public Setting(PropertyDescriptor descriptor)
                {
                    PropertyDescriptor = descriptor;
                }

                /// <summary>
                /// Constructor with name and value of setting</summary>
                /// <param name="name">Setting name</param>
                /// <param name="valueString">Setting value</param>
                public Setting(string name, string valueString)
                {
                    Name = name;
                    ValueString = valueString;
                }

                /// <summary>
                /// Set a setting's value with name and value of setting</summary>
                /// <param name="name">Setting name</param>
                /// <param name="valueString">Setting value</param>
                public void Set(string name, string valueString)
                {
                    Name = name;
                    ValueString = valueString;
                    if (PropertyDescriptor != null)
                    {
                        SetValue();
                    }
                }

                /// <summary>
                /// Set a setting's value with PropertyDescriptor</summary>
                /// <param name="descriptor">PropertyDescriptor describing setting</param>
                public void Set(PropertyDescriptor descriptor)
                {
                    PropertyDescriptor = descriptor;
                    if (Name != null && ValueString != null)
                    {
                        SetValue();
                    }
                }

                private void SetValue()
                {
                    if (!CanMakeChanges)
                        return;

                    object value = GetValue(PropertyDescriptor.PropertyType, ValueString);
                    PropertyDescriptor.SetValue(null, value);
                }

                /// <summary>
                /// Setting name</summary>
                public string Name;
                /// <summary>
                /// Setting value</summary>
                public string ValueString;
                /// <summary>
                /// Setting PropertyDescriptor</summary>
                public PropertyDescriptor PropertyDescriptor;
            }

            /// <summary>
            /// Set whether property descriptors are allowed to make changes</summary>
            /// <remarks>Used when persisted settings are being loaded from disk
            /// to prevent property descriptors from being set multiple times as
            /// in the case where DefaultSettings.xml and AppSettings.xml both exist</remarks>
            public static bool CanMakeChanges { private get; set; }
        }
        private static object GetValue(Type type, string valueString)
        {
            object value = null;
            try
            {
                TypeConverter converter = TypeDescriptor.GetConverter(type);
                if (CanConvertToAndFromString(converter))
                {
                    value = converter.ConvertFromInvariantString(valueString);
                }
                else if (TypeConverterRegistry.CanConvert(type))
                {
                    value = TypeConverterRegistry.GetConverter(type).ConvertFrom(valueString);
                }
                else
                {
                    // deserialize
                    byte[] data = Convert.FromBase64String(valueString);
                    using (MemoryStream stream = new MemoryStream(data))
                    {
                        BinaryFormatter formatter = new BinaryFormatter();
                        value = formatter.Deserialize(stream);
                    }
                }
            }
            catch
            {
                value = null;
            }
            return value;
        }
        private static bool CanConvertToAndFromString(TypeConverter converter)
        {
            return (converter.CanConvertFrom(typeof(string)) &&
                   converter.CanConvertTo(typeof(string)));
        }
        /// <summary>
        /// Class for group of user settings</summary>
        public class UserSettingsInfo
        {
            /// <summary>
            /// Constructor with name and PropertyDescriptors</summary>
            /// <param name="name">Name of group of user settings</param>
            /// <param name="settings">PropertyDescriptors with settings</param>
            public UserSettingsInfo(string name, PropertyDescriptor[] settings)
            {
                Name = name;
                Settings = new List<PropertyDescriptor>(settings);
            }

            /// <summary>
            /// Name of group of user settings</summary>
            public readonly string Name;
            /// <summary>
            /// PropertyDescriptors with settings</summary>
            public readonly List<PropertyDescriptor> Settings;
        }
        private readonly SortedDictionary<string, SettingsInfo> _settings = new SortedDictionary<string, SettingsInfo>();
        private ITreeView _userSettings = new TreeView(string.Empty);

        public void RegisterSettings(string uid, params PropertyDescriptor[] settings)
        {
            SettingsInfo settingsInfo;
            if (!_settings.TryGetValue(uid, out settingsInfo))
            {
                settingsInfo = new SettingsInfo(uid);
                _settings.Add(uid, settingsInfo);
            }

            foreach (PropertyDescriptor descriptor in settings)
                settingsInfo.Add(descriptor);
        }
        public ITreeView UserSettings
        {
            get { return _userSettings; }
        }

        public void RegisterSimple(object owner,Expression<Func<object>> expression,string pathName,string name,string category="General",string description="")
        {
            var descriptor = new BoundPropertyDescriptor(owner,expression, name, category,
                description);
            
            RegisterSettings(pathName,descriptor);
            
            RegisterUserSettings(pathName,descriptor);
        }

        public void RegisterUserSettings(string pathName, params PropertyDescriptor[] settings)
        {
            if (string.IsNullOrEmpty(pathName))
                throw new ArgumentException("pathName");

            string[] path = pathName.Split('/', 16);

            // get root folder
            Tree<object> folder = UserSettings as Tree<object>;
            if (folder == null)
                throw new InvalidOperationException("userSettings");

            // for each subsequent segment of the path, get folder
            for (int i = 0; i < path.Length - 1; i++)
                folder = GetOrCreateFolder(path[i], folder);

            // get the node that should hold the settings, if it already exists
            string name = path[path.Length - 1];
            UserSettingsInfo existing = null;
            int index = 0;
            foreach (Tree<object> node in folder.Children)
            {
                UserSettingsInfo info = node.Value as UserSettingsInfo;
                if (info != null)
                {
                    if (info.Name == name)
                    {
                        existing = info;
                        break;
                    }
                    if (info.Name.CompareTo(name) < 0)
                        index++;
                }
            }

            // add the settings, either by merging with the existing node or creating a new one
            if (existing != null)
            {
                foreach (PropertyDescriptor pd in settings)
                    existing.Settings.Add(pd);
            }
            else
            {
                Tree<object> node = new Tree<object>(new UserSettingsInfo(name, settings));
                folder.Children.Insert(index, node);
            }
        }
        private static string GetMutexName(string pathName)
        {
            string safeName = pathName;

            //255 characters will break IpcChannel constructor. 250 works.
            if (safeName.Length > 250)
                safeName = safeName.Substring(safeName.Length - 250);

            // The Mutex constructor will crash if given '\', unless it's part of a valid path.
            // NextInstanceMonitor.ActivateApplication() will crash if there are '/' characters.
            safeName = safeName.Replace('/', '-');
            safeName = safeName.Replace('\\', '-');

            return safeName;
        }
        protected void Serialize(Stream stream)
        {
            Saving.Raise(this, EventArgs.Empty);

            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.AppendChild(xmlDoc.CreateXmlDeclaration("1.0", "utf-8", "yes"));
            XmlElement root = xmlDoc.CreateElement("settings");
            xmlDoc.AppendChild(root);

            // add application name and version to the root element.
            root.SetAttribute("appName", _applicationName);
            root.SetAttribute("appVersion", _versionString);

            foreach (SettingsInfo info in _settings.Values)
            {
                XmlElement block = xmlDoc.CreateElement("block");
                block.SetAttribute("id", info.Name);

                foreach (SettingsInfo.Setting setting in info.Settings.Values)
                {
                    PropertyDescriptor descriptor = setting.PropertyDescriptor;
                    if (descriptor != null)
                    {
                        object value = descriptor.GetValue(null);
                        if (CanWriteValue(value))
                            WriteValue(descriptor.Name, value, block);
                    }
                    else
                    {
                        WriteValue(setting.Name, setting.ValueString, block);
                    }
                }
                // skip empty block
                if (block.ChildNodes.Count > 0)
                    root.AppendChild(block);
            }

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.CloseOutput = false;
            settings.Indent = true;

            using (XmlWriter writer = XmlWriter.Create(stream, settings))
            {
                xmlDoc.WriteTo(writer);
            }
        }
        private bool CanWriteValue(object value)
        {
            if (value == null)
                return false;

            TypeConverter converter = TypeDescriptor.GetConverter(value.GetType());
            return CanConvertToAndFromString(converter) || value.GetType().IsSerializable;
        }
        private void WriteValue(string name, object value, XmlElement block)
        {
            if (value == null)
                return;

            // skip persisting if any exception occurs
            string valueString = null;
            Type type = value.GetType();
            TypeConverter converter = TypeDescriptor.GetConverter(type);
            if (CanConvertToAndFromString(converter))
            {
                valueString = converter.ConvertToInvariantString(value);
            }
            else if (TypeConverterRegistry.TryGetConverter(type, out var typeConverter))
            {
                valueString = typeConverter.ConvertTo(value, typeof(string)) as string;
            }
            else if (type.IsSerializable)
            {
                // serialize
                BinaryFormatter formatter = new BinaryFormatter();
                using (MemoryStream stream = new MemoryStream())
                {
                    formatter.Serialize(stream, value);
                    valueString = Convert.ToBase64String(stream.GetBuffer());
                }
            }

            if (string.IsNullOrEmpty(valueString))
                return;

            XmlDocument xmlDoc = block.OwnerDocument;
            XmlElement elmValue = xmlDoc.CreateElement("value");
            elmValue.SetAttribute("name", name);
            elmValue.SetAttribute("type", type.Name);

            // if the valueString is xmlDoc then
            XmlDocument temp = StringToXmlDoc(valueString);
            if (temp != null)
            {
                // remove xml declaration if exists
                XmlDeclaration decl = temp.FirstChild as XmlDeclaration;
                if (decl != null)
                    temp.RemoveChild(decl);
                elmValue.InnerXml = temp.DocumentElement.OuterXml;
            }
            else
            {
                elmValue.InnerText = valueString;
            }

            block.AppendChild(elmValue);
        }
        private XmlDocument StringToXmlDoc(string strXml)
        {
            XmlDocument xmlDoc = null;
            try
            {
                int len = (strXml.Length > 20) ? 20 : strXml.Length;
                string test = RemoveAllWhiteSpace(strXml.Substring(0, len)).ToLower();
                if (test.Contains("<?xmlversion="))
                {
                    xmlDoc = new XmlDocument();
                    xmlDoc.LoadXml(strXml);
                }
            }
            catch
            {
                xmlDoc = null;
            }
            return xmlDoc;

        }
        public static string RemoveAllWhiteSpace(string s)
        {
            if (string.IsNullOrEmpty(s))
                return s;
            StringBuilder result = new StringBuilder(s.Length);
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                if (!char.IsWhiteSpace(c))
                    result.Append(c);
            }
            return result.ToString();
        }
        public void SaveSettings()
        {
            string tempNew = string.Empty;

            string mutexName = GetMutexName(_settingsPath);
            using (Mutex saveMutex = new Mutex(false, mutexName))
            {
                try
                {
                    saveMutex.WaitOne();

                    // Create zero-size file.
                    tempNew = Path.GetTempFileName();

                    using (Stream stream = File.Create(tempNew))
                        Serialize(stream);

                    // Make sure the settings directory exists. Do nothing if it already exists.
                    string settingsDir = Path.GetDirectoryName(_settingsPath);
                    Directory.CreateDirectory(settingsDir);

                    // Erase old backup (if any) and move current settings file (if any).
                    string tempBackup = Path.Combine(settingsDir, "~Settings.xml");
                    if (File.Exists(tempBackup)) // seems unnecessary, but Dan put this check in the WPF version --Ron
                        File.Delete(tempBackup);
                    if (File.Exists(_settingsPath))
                        File.Move(_settingsPath, tempBackup);

                    // Move temporary file to be the new settings file, then delete backup.
                    File.Move(tempNew, _settingsPath);
                    File.Delete(tempBackup);
                }
                catch (TargetInvocationException)
                {
                    // Catch and ignore TargetInvocationException happening if Windows
                    // is shut down with the application still running.
                    // TO DO: Find a way to successfully save settings and exit on shutdown.
                }
                catch (Exception ex)
                {
                    
                }
                finally
                {
                    // Attempt clean-up. No exception is thrown if file doesn't exist.
                    File.Delete(tempNew);
                    saveMutex.ReleaseMutex();
                }
            }
        }
        private Tree<object> GetOrCreateFolder(string name, Tree<object> tree)
        {
            // search for folder
            Tree<object> result = null;
            int index = 0;
            foreach (Tree<object> child in tree.Children)
            {
                string folderName = child.Value as string;
                if (folderName != null)
                {
                    if (folderName == name)
                    {
                        result = child;
                        break;
                    }

                    if (folderName.CompareTo(name) < 0)
                        index++;
                }
                else // child is UserSettingsInfo
                {
                    index++; // folders should follow settings nodes
                }

            }

            // if not found, create it
            if (result == null)
            {
                result = new Tree<object>(name);
                tree.Children.Insert(index, result);
            }

            return result;
        }
        
        
        public event EventHandler Saving;
        public event EventHandler Loading;
        public event EventHandler Reloaded;


        private class TreeView : Tree<object>, ITreeView
        {
            public TreeView(object root)
                : base(root)
            {
            }

            #region ITreeView Members

            public object Root
            {
                get { return this; }
            }

            public IEnumerable<object> GetChildren(object parent)
            {
                foreach (object child in ((Tree<object>)parent).Children)
                    yield return child;
            }

            #endregion

            #region IItemView Members

            /// <summary>
            /// Gets item's display information</summary>
            /// <param name="item">Item being displayed</param>
            /// <param name="info">Item info, to fill out</param>
            public void GetInfo(object item, ItemInfo info)
            {
                object value = ((Tree<object>)item).Value;
                if (value is string)
                {
                    info.Label = (string)value;
                    //info.ImageIndex = info.GetImageList().Images.IndexOfKey(Resources.FolderImage);
                    info.AllowSelect = false;
                }
                else
                {
                    UserSettingsInfo settingsInfo = value as UserSettingsInfo;
                    info.Label = settingsInfo.Name;
                    info.AllowLabelEdit = false;
                    //info.ImageIndex = info.GetImageList().Images.IndexOfKey(Resources.PreferencesImage);
                    info.IsLeaf = true;
                }
            }

            #endregion
        }
        

        
        private string _settingsPath;
        private string _defaultSettingsPath;
        private string _applicationName;
        private string _versionString;
        private string _propertyViewState;
        

        public void LoadSettings()
        {
            Loading.Raise(this, EventArgs.Empty);

            try
            {
                bool defaultSettingsExists = File.Exists(_defaultSettingsPath);
                bool appSettingsExists = File.Exists(_settingsPath);

                // only update property descriptors during the DefaultSettings.xml pass
                // if DefaultSettings.xml exists and AppSettings.xml does not exist
                SettingsInfo.CanMakeChanges = defaultSettingsExists && !appSettingsExists;

                // first, load default settings, if they exist:
                if (defaultSettingsExists)
                {
                    using (Stream stream = File.OpenRead(_defaultSettingsPath))
                        Deserialize(stream);
                }

                // restore for AppSettings.xml pass
                SettingsInfo.CanMakeChanges = true;

                // now load user settings, overriding defaults:
                if (appSettingsExists)
                {
                    using (Stream stream = File.OpenRead(_settingsPath))
                        Deserialize(stream);
                }
            }
            catch (Exception ex)
            {
               
            }
            finally
            {
                // restore value
                SettingsInfo.CanMakeChanges = true;
            }
        }

        protected void ApplyStoredSettings()
        {
            foreach (SettingsInfo info in _settings.Values)
            {
                foreach (SettingsInfo.Setting setting in info.Settings.Values)
                {
                    if (setting.PropertyDescriptor is BoundPropertyDescriptor pd)
                    {
                        if (setting.ValueString == null)
                        {
                            continue;
                        }
                        object v;
                        if (setting.PropertyDescriptor.PropertyType.IsEnum)
                        {
                            v = Enum.Parse(setting.PropertyDescriptor.PropertyType, setting.ValueString);
                        }
                        // if is list
                        else if (setting.PropertyDescriptor.PropertyType.IsGenericType &&
                                 setting.PropertyDescriptor.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
                        {
                            Type itemType = setting.PropertyDescriptor.PropertyType.GetGenericArguments()[0];
                            TypeConverter converter = TypeDescriptor.GetConverter(itemType);
                            string[] items = setting.ValueString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                            IList list = (IList)Activator.CreateInstance(setting.PropertyDescriptor.PropertyType);
                            foreach (string item in items)
                            {
                                list.Add(converter.ConvertFromInvariantString(item.Trim()));
                            }
                            v = list;
                        }
                        else
                        {
                            v = Convert.ChangeType(setting.ValueString, setting.PropertyDescriptor.PropertyType);
                        }
                        pd.SetValue(pd.Owner, v);
                    }
                }
            }
        }
        

        protected bool Deserialize(Stream stream)
        {
            // create XML DOM from stream
            // if failed, display message box and return
            try
            {
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.Load(stream);

                XmlElement root = xmlDoc.DocumentElement;

                // get all the blocks
                XmlNodeList blocks = root.SelectNodes("block");
                if (blocks == null || blocks.Count == 0)
                    throw new Exception("The setting file is empty");

                foreach (XmlElement block in blocks)
                {
                    try
                    {
                        string id = block.GetAttribute("id");

                        SettingsInfo info;
                        if (!_settings.TryGetValue(id, out info))
                        {
                            info = new SettingsInfo(id);
                            _settings.Add(id, info);
                        }

                        // get a list of value element in each block
                        XmlNodeList valueNodes = block.SelectNodes("value");

                        // skip over empty block
                        if (valueNodes == null || valueNodes.Count == 0)
                            continue;

                        foreach (XmlElement xmlElement in valueNodes)
                        {
                            string name = xmlElement.GetAttribute("name");
                            string valueString = GetElementValueString(xmlElement);

                            info.Add(name, valueString);
                        }
                    }
                    catch (Exception ex)
                    {
                        
                    }
                }
                //ApplyStoredSettings();
                Reloaded.Raise(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                

                return false;
            }

            return true;
        }
        private static string GetElementValueString(XmlElement element)
        {
            string valueString = element.InnerText;
            if (string.IsNullOrEmpty(valueString))
                valueString = element.InnerXml;

            return valueString;
        }
        

      



    }
}