// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Xml;

namespace System.Configuration
{
    /// <summary>
    /// This is a provider used to store configuration settings locally for client applications.
    /// </summary>
    public class LocalFileSettingsProvider : SettingsProvider, IApplicationSettingsProvider
    {
        private string _appName = string.Empty;
        private ClientSettingsStore _store = null;
        private string _prevLocalConfigFileName = null;
        private string _prevRoamingConfigFileName = null;
        private XmlEscaper _escaper = null;

        /// <summary>
        /// Abstract SettingsProvider property.
        /// </summary>
        public override string ApplicationName
        {
            get
            {
                return _appName;
            }
            set
            {
                _appName = value;
            }
        }

        private XmlEscaper Escaper
        {
            get
            {
                if (_escaper == null)
                {
                    _escaper = new XmlEscaper();
                }

                return _escaper;
            }
        }

        /// <summary>
        /// We maintain a single instance of the ClientSettingsStore per instance of provider.
        /// </summary>
        private ClientSettingsStore Store
        {
            get
            {
                if (_store == null)
                {
                    _store = new ClientSettingsStore();
                }

                return _store;
            }
        }

        /// <summary>
        /// Abstract ProviderBase method.
        /// </summary>
        public override void Initialize(string name, NameValueCollection values)
        {
            if (string.IsNullOrEmpty(name))
            {
                name = "LocalFileSettingsProvider";
            }

            base.Initialize(name, values);
        }

        /// <summary>
        /// Abstract SettingsProvider method
        /// </summary>
        public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection properties)
        {
            SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
            string sectionName = GetSectionName(context);

            // Look for this section in both applicationSettingsGroup and userSettingsGroup
            IDictionary appSettings = Store.ReadSettings(sectionName, false);
            IDictionary userSettings = Store.ReadSettings(sectionName, true);
            ConnectionStringSettingsCollection connStrings = Store.ReadConnectionStrings();

            // Now map each SettingProperty to the right StoredSetting and deserialize the value if found.
            foreach (SettingsProperty setting in properties)
            {
                string settingName = setting.Name;
                SettingsPropertyValue value = new SettingsPropertyValue(setting);

                // First look for and handle "special" settings
                SpecialSettingAttribute attr = setting.Attributes[typeof(SpecialSettingAttribute)] as SpecialSettingAttribute;
                bool isConnString = (attr != null) ? (attr.SpecialSetting == SpecialSetting.ConnectionString) : false;

                if (isConnString)
                {
                    string connStringName = sectionName + "." + settingName;
                    if (connStrings != null && connStrings[connStringName] != null)
                    {
                        value.PropertyValue = connStrings[connStringName].ConnectionString;
                    }
                    else if (setting.DefaultValue != null && setting.DefaultValue is string)
                    {
                        value.PropertyValue = setting.DefaultValue;
                    }
                    else
                    {
                        //No value found and no default specified 
                        value.PropertyValue = string.Empty;
                    }

                    value.IsDirty = false; //reset IsDirty so that it is correct when SetPropertyValues is called 
                    values.Add(value);
                    continue;
                }

                // Not a "special" setting
                bool isUserSetting = IsUserSetting(setting);

                if (isUserSetting && !ConfigurationManagerInternalFactory.Instance.SupportsUserConfig)
                {
                    // We encountered a user setting, but the current configuration system does not support
                    // user settings.
                    throw new ConfigurationErrorsException(SR.UserSettingsNotSupported);
                }

                IDictionary settings = isUserSetting ? userSettings : appSettings;

                if (settings.Contains(settingName))
                {
                    StoredSetting ss = (StoredSetting)settings[settingName];
                    string valueString = ss.Value.InnerXml;

                    // We need to un-escape string serialized values
                    if (ss.SerializeAs == SettingsSerializeAs.String)
                    {
                        valueString = Escaper.Unescape(valueString);
                    }

                    value.SerializedValue = valueString;
                }
                else if (setting.DefaultValue != null)
                {
                    value.SerializedValue = setting.DefaultValue;
                }
                else
                {
                    //No value found and no default specified 
                    value.PropertyValue = null;
                }

                value.IsDirty = false; //reset IsDirty so that it is correct when SetPropertyValues is called 
                values.Add(value);
            }

            return values;
        }

        /// <summary>
        ///     Abstract SettingsProvider method
        /// </summary>
        public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection values)
        {
            string sectionName = GetSectionName(context);
            IDictionary roamingUserSettings = new Hashtable();
            IDictionary localUserSettings = new Hashtable();

            foreach (SettingsPropertyValue value in values)
            {
                SettingsProperty setting = value.Property;
                bool isUserSetting = IsUserSetting(setting);

                if (value.IsDirty)
                {
                    if (isUserSetting)
                    {
                        bool isRoaming = IsRoamingSetting(setting);
                        StoredSetting ss = new StoredSetting(setting.SerializeAs, SerializeToXmlElement(setting, value));

                        if (isRoaming)
                        {
                            roamingUserSettings[setting.Name] = ss;
                        }
                        else
                        {
                            localUserSettings[setting.Name] = ss;
                        }

                        value.IsDirty = false; //reset IsDirty
                    }
                    else
                    {
                        // This is an app-scoped or connection string setting that has been written to. 
                        // We don't support saving these.
                    }
                }
            }

            // Semi-hack: If there are roamable settings, let's write them before local settings so if a handler 
            // declaration is necessary, it goes in the roaming config file in preference to the local config file.
            if (roamingUserSettings.Count > 0)
            {
                Store.WriteSettings(sectionName, true, roamingUserSettings);
            }

            if (localUserSettings.Count > 0)
            {
                Store.WriteSettings(sectionName, false, localUserSettings);
            }
        }

        /// <summary>
        ///     Implementation of IClientSettingsProvider.Reset. Resets user scoped settings to the values 
        ///     in app.exe.config, does nothing for app scoped settings.
        /// </summary>
        public void Reset(SettingsContext context)
        {
            string sectionName = GetSectionName(context);

            // First revert roaming, then local
            Store.RevertToParent(sectionName, true);
            Store.RevertToParent(sectionName, false);
        }

        /// <summary>
        ///    Implementation of IClientSettingsProvider.Upgrade.
        ///    Tries to locate a previous version of the user.config file. If found, it migrates matching settings.
        ///    If not, it does nothing.
        /// </summary>
        public void Upgrade(SettingsContext context, SettingsPropertyCollection properties)
        {
            // Separate the local and roaming settings and upgrade them separately.

            SettingsPropertyCollection local = new SettingsPropertyCollection();
            SettingsPropertyCollection roaming = new SettingsPropertyCollection();

            foreach (SettingsProperty sp in properties)
            {
                bool isRoaming = IsRoamingSetting(sp);

                if (isRoaming)
                {
                    roaming.Add(sp);
                }
                else
                {
                    local.Add(sp);
                }
            }

            if (roaming.Count > 0)
            {
                Upgrade(context, roaming, true);
            }

            if (local.Count > 0)
            {
                Upgrade(context, local, false);
            }
        }

        /// <summary>
        /// Implementation of IClientSettingsProvider.GetPreviousVersion.
        /// </summary>
        public SettingsPropertyValue GetPreviousVersion(SettingsContext context, SettingsProperty property)
        {
            bool isRoaming = IsRoamingSetting(property);
            string prevConfig = GetPreviousConfigFileName(isRoaming);

            if (!string.IsNullOrEmpty(prevConfig))
            {
                SettingsPropertyCollection properties = new SettingsPropertyCollection();
                properties.Add(property);
                SettingsPropertyValueCollection values = GetSettingValuesFromFile(prevConfig, GetSectionName(context), true, properties);
                return values[property.Name];
            }
            else
            {
                SettingsPropertyValue value = new SettingsPropertyValue(property);
                value.PropertyValue = null;
                return value;
            }
        }

        /// <summary>
        /// Locates the previous version of user.config, if present. The previous version is determined
        /// by walking up one directory level in the *UserConfigPath and searching for the highest version
        /// number less than the current version.
        /// </summary>
        private string GetPreviousConfigFileName(bool isRoaming)
        {
            if (!ConfigurationManagerInternalFactory.Instance.SupportsUserConfig)
            {
                throw new ConfigurationErrorsException(SR.UserSettingsNotSupported);
            }

            string prevConfigFile = isRoaming ? _prevRoamingConfigFileName : _prevLocalConfigFileName;

            if (string.IsNullOrEmpty(prevConfigFile))
            {
                string userConfigPath = isRoaming
                    ? ConfigurationManagerInternalFactory.Instance.ExeRoamingConfigDirectory
                    : ConfigurationManagerInternalFactory.Instance.ExeLocalConfigDirectory;

                Version currentVersion;
                if (!Version.TryParse(ConfigurationManagerInternalFactory.Instance.ExeProductVersion, out currentVersion))
                {
                    return null;
                }

                Version previousVersion = null;
                DirectoryInfo previousDirectory = null;
                string file = null;

                DirectoryInfo parentDirectory = Directory.GetParent(userConfigPath);

                if (parentDirectory.Exists)
                {
                    foreach (DirectoryInfo directory in parentDirectory.GetDirectories())
                    {
                        Version tempVersion;

                        if (Version.TryParse(directory.Name, out tempVersion) && tempVersion < currentVersion)
                        {
                            if (previousVersion == null)
                            {
                                previousVersion = tempVersion;
                                previousDirectory = directory;
                            }
                            else if (tempVersion > previousVersion)
                            {
                                previousVersion = tempVersion;
                                previousDirectory = directory;
                            }
                        }
                    }

                    if (previousDirectory != null)
                    {
                        file = Path.Combine(previousDirectory.FullName, ConfigurationManagerInternalFactory.Instance.UserConfigFilename);
                    }

                    if (File.Exists(file))
                    {
                        prevConfigFile = file;
                    }
                }

                // Cache for future use.
                if (isRoaming)
                {
                    _prevRoamingConfigFileName = prevConfigFile;
                }
                else
                {
                    _prevLocalConfigFileName = prevConfigFile;
                }
            }

            return prevConfigFile;
        }

        /// <summary>
        /// Gleans information from the SettingsContext and determines the name of the config section.
        /// </summary>
        private string GetSectionName(SettingsContext context)
        {
            string groupName = (string)context["GroupName"];
            string key = (string)context["SettingsKey"];

            Debug.Assert(groupName != null, "SettingsContext did not have a GroupName!");

            string sectionName = groupName;

            if (!string.IsNullOrEmpty(key))
            {
                sectionName = sectionName + "." + key;
            }

            return XmlConvert.EncodeLocalName(sectionName);
        }

        /// <summary>
        /// Retrieves the values of settings from the given config file (as opposed to using 
        /// the configuration for the current context)
        /// </summary>
        private SettingsPropertyValueCollection GetSettingValuesFromFile(string configFileName, string sectionName, bool userScoped, SettingsPropertyCollection properties)
        {
            SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
            IDictionary settings = ClientSettingsStore.ReadSettingsFromFile(configFileName, sectionName, userScoped);

            // Map each SettingProperty to the right StoredSetting and deserialize the value if found.
            foreach (SettingsProperty setting in properties)
            {
                string settingName = setting.Name;
                SettingsPropertyValue value = new SettingsPropertyValue(setting);

                if (settings.Contains(settingName))
                {
                    StoredSetting ss = (StoredSetting)settings[settingName];
                    string valueString = ss.Value.InnerXml;

                    // We need to un-escape string serialized values
                    if (ss.SerializeAs == SettingsSerializeAs.String)
                    {
                        valueString = Escaper.Unescape(valueString);
                    }

                    value.SerializedValue = valueString;
                    value.IsDirty = true;
                    values.Add(value);
                }
            }

            return values;
        }

        /// <summary>
        /// Indicates whether a setting is roaming or not.
        /// </summary>
        private static bool IsRoamingSetting(SettingsProperty setting)
        {
            SettingsManageabilityAttribute manageAttr = setting.Attributes[typeof(SettingsManageabilityAttribute)] as SettingsManageabilityAttribute;
            return manageAttr != null && ((manageAttr.Manageability & SettingsManageability.Roaming) == SettingsManageability.Roaming);
        }

        /// <summary>
        /// This provider needs settings to be marked with either the UserScopedSettingAttribute or the
        /// ApplicationScopedSettingAttribute. This method determines whether this setting is user-scoped
        /// or not. It will throw if none or both of the attributes are present.
        /// </summary>
        private bool IsUserSetting(SettingsProperty setting)
        {
            bool isUser = setting.Attributes[typeof(UserScopedSettingAttribute)] is UserScopedSettingAttribute;
            bool isApp = setting.Attributes[typeof(ApplicationScopedSettingAttribute)] is ApplicationScopedSettingAttribute;

            if (isUser && isApp)
            {
                throw new ConfigurationErrorsException(SR.Format(SR.BothScopeAttributes, setting.Name));
            }
            else if (!(isUser || isApp))
            {
                throw new ConfigurationErrorsException(SR.Format(SR.NoScopeAttributes, setting.Name));
            }

            return isUser;
        }

        private XmlNode SerializeToXmlElement(SettingsProperty setting, SettingsPropertyValue value)
        {
            XmlDocument doc = new XmlDocument();
            XmlElement valueXml = doc.CreateElement(nameof(value));

            string serializedValue = value.SerializedValue as string;

            if (serializedValue == null && setting.SerializeAs == SettingsSerializeAs.Binary)
            {
                // SettingsPropertyValue returns a byte[] in the binary serialization case. We need to
                // encode this - we use base64 since SettingsPropertyValue understands it and we won't have
                // to special case while deserializing.

                byte[] buffer = value.SerializedValue as byte[];
                if (buffer != null)
                {
                    serializedValue = Convert.ToBase64String(buffer);
                }
            }

            if (serializedValue == null)
            {
                serializedValue = string.Empty;
            }

            // We need to escape string serialized values
            if (setting.SerializeAs == SettingsSerializeAs.String)
            {
                serializedValue = Escaper.Escape(serializedValue);
            }

            valueXml.InnerXml = serializedValue;

            // Hack to remove the XmlDeclaration that the XmlSerializer adds.
            XmlNode unwanted = null;
            foreach (XmlNode child in valueXml.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.XmlDeclaration)
                {
                    unwanted = child;
                    break;
                }
            }
            if (unwanted != null)
            {
                valueXml.RemoveChild(unwanted);
            }

            return valueXml;
        }

        /// <summary>
        /// Private version of upgrade that uses isRoaming to determine which config file to use.
        /// </summary> 
        private void Upgrade(SettingsContext context, SettingsPropertyCollection properties, bool isRoaming)
        {
            string prevConfig = GetPreviousConfigFileName(isRoaming);

            if (!string.IsNullOrEmpty(prevConfig))
            {
                //Filter the settings properties to exclude those that have a NoSettingsVersionUpgradeAttribute on them.
                SettingsPropertyCollection upgradeProperties = new SettingsPropertyCollection();
                foreach (SettingsProperty sp in properties)
                {
                    if (!(sp.Attributes[typeof(NoSettingsVersionUpgradeAttribute)] is NoSettingsVersionUpgradeAttribute))
                    {
                        upgradeProperties.Add(sp);
                    }
                }

                SettingsPropertyValueCollection values = GetSettingValuesFromFile(prevConfig, GetSectionName(context), true, upgradeProperties);
                SetPropertyValues(context, values);
            }
        }

        private class XmlEscaper
        {
            private XmlDocument document;
            private XmlElement tempElement;

            internal XmlEscaper()
            {
                document = new XmlDocument();
                tempElement = document.CreateElement("temp");
            }

            internal string Escape(string xmlString)
            {
                if (string.IsNullOrEmpty(xmlString))
                {
                    return xmlString;
                }

                tempElement.InnerText = xmlString;
                return tempElement.InnerXml;
            }

            internal string Unescape(string escapedString)
            {
                if (string.IsNullOrEmpty(escapedString))
                {
                    return escapedString;
                }

                tempElement.InnerXml = escapedString;
                return tempElement.InnerText;
            }
        }
    }
}
