// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Configuration.Internal;
using System.Diagnostics;
using System.IO;
using System.Xml;

namespace System.Configuration
{
    /// <summary>
    /// This class abstracts the details of config system away from the LocalFileSettingsProvider. It talks to
    /// the configuration API and the relevant Sections to read and write settings.
    /// It understands sections of type ClientSettingsSection.
    ///
    /// NOTE: This API supports reading from app.exe.config and user.config, but writing only to
    ///       user.config.
    /// </summary>
    internal sealed class ClientSettingsStore
    {
        private const string ApplicationSettingsGroupName = "applicationSettings";
        private const string UserSettingsGroupName = "userSettings";
        private const string ApplicationSettingsGroupPrefix = ApplicationSettingsGroupName + "/";
        private const string UserSettingsGroupPrefix = UserSettingsGroupName + "/";

        private static Configuration GetUserConfig(bool isRoaming)
        {
            ConfigurationUserLevel userLevel = isRoaming ? ConfigurationUserLevel.PerUserRoaming :
                                                           ConfigurationUserLevel.PerUserRoamingAndLocal;

            return ClientSettingsConfigurationHost.OpenExeConfiguration(userLevel);
        }

        private static ClientSettingsSection GetConfigSection(Configuration config, string sectionName, bool declare)
        {
            string fullSectionName = UserSettingsGroupPrefix + sectionName;
            ClientSettingsSection section = null;

            if (config != null)
            {
                section = config.GetSection(fullSectionName) as ClientSettingsSection;

                if (section == null && declare)
                {
                    // Looks like the section isn't declared - let's declare it and try again.
                    DeclareSection(config, sectionName);
                    section = config.GetSection(fullSectionName) as ClientSettingsSection;
                }
            }

            return section;
        }

        // Declares the section handler of a given section in its section group, if a declaration isn't already
        // present.
        private static void DeclareSection(Configuration config, string sectionName)
        {
            ConfigurationSectionGroup settingsGroup = config.GetSectionGroup(UserSettingsGroupName);

            if (settingsGroup == null)
            {
                //Declare settings group
                ConfigurationSectionGroup group = new UserSettingsGroup();
                config.SectionGroups.Add(UserSettingsGroupName, group);
            }

            settingsGroup = config.GetSectionGroup(UserSettingsGroupName);

            Debug.Assert(settingsGroup != null, "Failed to declare settings group");

            if (settingsGroup != null)
            {
                ConfigurationSection section = settingsGroup.Sections[sectionName];
                if (section == null)
                {
                    section = new ClientSettingsSection();
                    section.SectionInformation.AllowExeDefinition = ConfigurationAllowExeDefinition.MachineToLocalUser;
                    section.SectionInformation.RequirePermission = false;
                    settingsGroup.Sections.Add(sectionName, section);
                }
            }
        }

        internal static IDictionary ReadSettings(string sectionName, bool isUserScoped)
        {
            IDictionary settings = new Hashtable();

            if (isUserScoped && !ConfigurationManagerInternalFactory.Instance.SupportsUserConfig)
            {
                return settings;
            }

            string prefix = isUserScoped ? UserSettingsGroupPrefix : ApplicationSettingsGroupPrefix;
            ConfigurationManager.RefreshSection(prefix + sectionName);
            ClientSettingsSection section = ConfigurationManager.GetSection(prefix + sectionName) as ClientSettingsSection;

            if (section != null)
            {
                foreach (SettingElement setting in section.Settings)
                {
                    settings[setting.Name] = new StoredSetting(setting.SerializeAs, setting.Value.ValueXml);
                }
            }

            return settings;
        }

        internal static IDictionary ReadSettingsFromFile(string configFileName, string sectionName, bool isUserScoped)
        {
            IDictionary settings = new Hashtable();

            if (isUserScoped && !ConfigurationManagerInternalFactory.Instance.SupportsUserConfig)
            {
                return settings;
            }

            string prefix = isUserScoped ? UserSettingsGroupPrefix : ApplicationSettingsGroupPrefix;
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();

            // NOTE: When isUserScoped is true, we don't care if configFileName represents a roaming file or
            //       a local one. All we want is three levels of configuration. So, we use the PerUserRoaming level.
            ConfigurationUserLevel userLevel = isUserScoped ? ConfigurationUserLevel.PerUserRoaming : ConfigurationUserLevel.None;

            if (isUserScoped)
            {
                fileMap.ExeConfigFilename = ConfigurationManagerInternalFactory.Instance.ApplicationConfigUri;
                fileMap.RoamingUserConfigFilename = configFileName;
            }
            else
            {
                fileMap.ExeConfigFilename = configFileName;
            }

            Configuration config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, userLevel);
            ClientSettingsSection section = config.GetSection(prefix + sectionName) as ClientSettingsSection;

            if (section != null)
            {
                foreach (SettingElement setting in section.Settings)
                {
                    settings[setting.Name] = new StoredSetting(setting.SerializeAs, setting.Value.ValueXml);
                }
            }

            return settings;
        }

        internal static ConnectionStringSettingsCollection ReadConnectionStrings()
        {
            return PrivilegedConfigurationManager.ConnectionStrings;
        }

        internal static void RevertToParent(string sectionName, bool isRoaming)
        {
            if (!ConfigurationManagerInternalFactory.Instance.SupportsUserConfig)
            {
                throw new ConfigurationErrorsException(SR.UserSettingsNotSupported);
            }

            Configuration config = GetUserConfig(isRoaming);
            ClientSettingsSection section = GetConfigSection(config, sectionName, false);

            // If the section is null, there is nothing to revert.
            if (section != null)
            {
                section.SectionInformation.RevertToParent();
                config.Save();
            }
        }

        internal static void WriteSettings(string sectionName, bool isRoaming, IDictionary newSettings)
        {
            if (!ConfigurationManagerInternalFactory.Instance.SupportsUserConfig)
            {
                throw new ConfigurationErrorsException(SR.UserSettingsNotSupported);
            }

            Configuration config = GetUserConfig(isRoaming);
            ClientSettingsSection section = GetConfigSection(config, sectionName, true);

            if (section != null)
            {
                SettingElementCollection sec = section.Settings;
                foreach (DictionaryEntry entry in newSettings)
                {
                    SettingElement se = sec.Get((string)entry.Key);

                    if (se == null)
                    {
                        se = new SettingElement();
                        se.Name = (string)entry.Key;
                        sec.Add(se);
                    }

                    StoredSetting ss = (StoredSetting)entry.Value;
                    se.SerializeAs = ss.SerializeAs;
                    se.Value.ValueXml = ss.Value;
                }

                try
                {
                    config.Save();
                }
                catch (ConfigurationErrorsException ex)
                {
                    // We wrap this in an exception with our error message and throw again.
                    throw new ConfigurationErrorsException(SR.Format(SR.SettingsSaveFailed, ex.Message), ex);
                }
            }
            else
            {
                throw new ConfigurationErrorsException(SR.SettingsSaveFailedNoSection);
            }
        }

        /// <summary>
        /// A private configuration host that we use to write settings to config. We need this so we
        /// can enforce a quota on the size of stuff written out.
        /// </summary>
        private sealed class ClientSettingsConfigurationHost : DelegatingConfigHost
        {
            private const string ClientConfigurationHostTypeName = "System.Configuration.ClientConfigurationHost, " + TypeUtil.ConfigurationManagerAssemblyName;
            private const string InternalConfigConfigurationFactoryTypeName = "System.Configuration.Internal.InternalConfigConfigurationFactory, " + TypeUtil.ConfigurationManagerAssemblyName;
            private static volatile IInternalConfigConfigurationFactory s_configFactory;

            /// <summary>
            /// ClientConfigurationHost implements this - a way of getting some info from it without
            /// depending too much on its internals.
            /// </summary>
            private IInternalConfigClientHost ClientHost
            {
                get
                {
                    return (IInternalConfigClientHost)Host;
                }
            }

            internal static IInternalConfigConfigurationFactory ConfigFactory =>
                s_configFactory ??= TypeUtil.CreateInstance<IInternalConfigConfigurationFactory>(InternalConfigConfigurationFactoryTypeName);

            private ClientSettingsConfigurationHost() { }

            public override void Init(IInternalConfigRoot configRoot, params object[] hostInitParams)
            {
                Debug.Fail("Did not expect to get called here");
            }

            /// <summary>
            /// We delegate this to the ClientConfigurationHost. The only thing we need to do here is to
            /// build a configPath from the ConfigurationUserLevel we get passed in.
            /// </summary>
            public override void InitForConfiguration(ref string locationSubPath, out string configPath, out string locationConfigPath,
                    IInternalConfigRoot configRoot, params object[] hostInitConfigurationParams)
            {

                ConfigurationUserLevel userLevel = (ConfigurationUserLevel)hostInitConfigurationParams[0];
                Host = TypeUtil.CreateInstance<IInternalConfigHost>(ClientConfigurationHostTypeName);

                string desiredConfigPath = userLevel switch
                {
                    ConfigurationUserLevel.None => ClientHost.GetExeConfigPath(),
                    ConfigurationUserLevel.PerUserRoaming => ClientHost.GetRoamingUserConfigPath(),
                    ConfigurationUserLevel.PerUserRoamingAndLocal => ClientHost.GetLocalUserConfigPath(),
                    _ => throw new ArgumentException(SR.UnknownUserLevel),
                };

                Host.InitForConfiguration(ref locationSubPath, out configPath, out locationConfigPath, configRoot, null, null, desiredConfigPath);
            }

            private static bool IsKnownConfigFile(string filename)
            {
                return
                  string.Equals(filename, ConfigurationManagerInternalFactory.Instance.MachineConfigPath, StringComparison.OrdinalIgnoreCase) ||
                  string.Equals(filename, ConfigurationManagerInternalFactory.Instance.ApplicationConfigUri, StringComparison.OrdinalIgnoreCase) ||
                  string.Equals(filename, ConfigurationManagerInternalFactory.Instance.ExeLocalConfigPath, StringComparison.OrdinalIgnoreCase) ||
                  string.Equals(filename, ConfigurationManagerInternalFactory.Instance.ExeRoamingConfigPath, StringComparison.OrdinalIgnoreCase);

            }

            internal static Configuration OpenExeConfiguration(ConfigurationUserLevel userLevel)
            {
                return ConfigFactory.Create(typeof(ClientSettingsConfigurationHost), userLevel);
            }

            /// <summary>
            /// If the stream we are asked for represents a config file that we know about, we ask
            /// the host to assert appropriate permissions.
            /// </summary>
            public override Stream OpenStreamForRead(string streamName)
            {
                if (IsKnownConfigFile(streamName))
                {
                    return Host.OpenStreamForRead(streamName, true);
                }
                else
                {
                    return Host.OpenStreamForRead(streamName);
                }
            }

            public override Stream OpenStreamForWrite(string streamName, string templateStreamName, ref object writeContext)
            {
                // On .NET Framework we do a bunch of work here around ensuring permissions and quotas
                return Host.OpenStreamForWrite(streamName, templateStreamName, ref writeContext);
            }

            /// <summary>
            /// If this is a stream that represents a user.config file that we know about, we ask
            /// the host to assert appropriate permissions.
            /// </summary>
            public override void WriteCompleted(string streamName, bool success, object writeContext)
            {
                if (string.Equals(streamName, ConfigurationManagerInternalFactory.Instance.ExeLocalConfigPath, StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(streamName, ConfigurationManagerInternalFactory.Instance.ExeRoamingConfigPath, StringComparison.OrdinalIgnoreCase))
                {

                    Host.WriteCompleted(streamName, success, writeContext, true);
                }
                else
                {
                    Host.WriteCompleted(streamName, success, writeContext);
                }
            }
        }
    }

    /// <summary>
    /// The ClientSettingsStore talks to the LocalFileSettingsProvider through a dictionary which maps from
    /// setting names to StoredSetting structs. This struct contains the relevant information.
    /// </summary>
    internal readonly struct StoredSetting
    {
        internal StoredSetting(SettingsSerializeAs serializeAs, XmlNode value)
        {
            SerializeAs = serializeAs;
            Value = value;
        }
        internal readonly SettingsSerializeAs SerializeAs;
        internal readonly XmlNode Value;
    }
}
