// ******************************************************************************//
//  RAYTHEON PROPRIETARY: THIS DOCUMENT CONTAINS DATA OR INFORMATION
//  PROPRIETARY TO RAYTHEON COMPANY AND IS RESTRICTED TO USE ONLY BY PERSONS
//  AUTHORIZED BY RAYTHEON COMPANY IN WRITING TO USE IT. DISCLOSURE TO
//  UNAUTHORIZED PERSONS WOULD LIKELY CAUSE SUBSTANTIAL COMPETITIVE HARM TO
//  RAYTHEON COMPANY''S BUSINESS POSITION. NEITHER SAID DOCUMENT NOR ITS
//  CONTENTS SHALL BE FURNISHED OR DISCLOSED TO OR COPIED OR USED BY PERSONS
//  OUTSIDE RAYTHEON COMPANY WITHOUT THE EXPRESS WRITTEN APPROVAL OF RAYTHEON
//  COMPANY.
//
//  THIS PROPRIETARY NOTICE IS NOT APPLICABLE IF DELIVERED TO THE U.S. GOVERNMENT
//
//  UNPUBLISHED WORK - COPYRIGHT RAYTHEON COMPANY.
//
//  WARNING: THIS DOCUMENT CONTAINS TECHNICAL DATA AND / OR TECHNOLOGY WHOSE
//  EXPORT OR DISCLOSURE TO NON-U.S. PERSONS, WHEREVER LOCATED, IS RESTRICTED
//  BY THE INTERNATIONAL TRAFFIC IN ARMS REGULATIONS (ITAR) (22 C.F.R. SECTION
//  120-130) OR THE EXPORT ADMINISTRATION REGULATIONS (EAR) (15 C.F.R. SECTION
//  730-774). THIS DOCUMENT CANNOT BE EXPORTED (E.G., PROVIDED TO A SUPPLIER
//  OUTSIDE OF THE UNITED STATES) OR DISCLOSED TO A NON-U.S. PERSON, WHEREVER
//  LOCATED, UNTIL A FINAL JURISDICTION AND CLASSIFICATION DETERMINATION HAS
//  BEEN COMPLETED AND APPROVED BY RAYTHEON, AND ANY REQUIRED U.S. GOVERNMENT
//  APPROVALS HAVE BEEN OBTAINED. VIOLATIONS ARE SUBJECT TO SEVERE CRIMINAL
//  PENALTIES.
//
//  DOD 5220.22-M, INDUSTRIAL SECURITY MANUAL, CHAPTER 5, SECTION 1 THROUGH 9 :
//  FOR CLASSIFIED DOCUMENTS FOLLOW THE PROCEDURES IN OR DOD 5200.1-R,
//  INFORMATION SECURITY PROGRAM, CHAPTER 6. FOR UNCLASSIFIED, LIMITED DOCUMENTS
//  DESTROY BY ANY  METHOD THAT WILL PREVENT DISCLOSURE OF CONTENTS OR
//  RECONSTRUCTION OF THE DOCUMENT.
// ****************************************************************************//
// Ignore Spelling: Deserialize

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Xml.XPath;

namespace Raytheon.Common
{
    /// <summary>
    /// for serializing and deserializing class types
    /// </summary>
    internal static class XmlSerializerExtensions
    {
        /// <summary>
        /// deserializes XML type into an object
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static T Deserialize<T>(this string value)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(T));
            using(StringReader reader = new StringReader(value))
            {
                return (T)serializer.Deserialize(reader);
            }
        }

        /// <summary>
        /// extension method to serialize XML type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string Serialize<T>(this T value)
        {
            if(value == null)
            {
                return string.Empty;
            }

            XmlSerializer xmlSerializer = new(typeof(T));

            using(StringWriter stringWriter = new())
            {
                using(XmlWriter xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { Indent = true }))
                {
                    xmlSerializer.Serialize(xmlWriter, value);
                    return stringWriter.ToString();
                }
            }
        }

        /// <summary>
        /// extension method to serialize XML type without a namespace
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static string SerializeNoNamespace<T>(this T value)
        {
            if(value == null)
            {
                return string.Empty;
            }

            XmlSerializerNamespaces emptyNamespaces = new(new[] { XmlQualifiedName.Empty });
            XmlSerializer serializer = new(value.GetType());
            XmlWriterSettings settings = new()
            {
                Indent = true,
                OmitXmlDeclaration = true
            };

            using(StringWriter stream = new())
            {
                using(XmlWriter writer = XmlWriter.Create(stream, settings))
                {
                    serializer.Serialize(writer, value, emptyNamespaces);
                    return stream.ToString();
                }
            }
        }
    }

    /// <summary>
    /// type conversion utility with a special case for enums
    /// </summary>
    public static class TypeConverter
    {
        /// <summary>
        /// special rule for enumeration when converting a type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static T ChangeType<T>(object value)
        {
            return typeof(T).IsEnum ? (T)Enum.Parse(typeof(T), value.ToString()) : (T)ChangeType(typeof(T), value);
        }

        /// <summary>
        /// convert type with TypeDescriptor
        /// </summary>
        /// <param name="t"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public static object ChangeType(Type t, object value)
        {
            System.ComponentModel.TypeConverter tc = TypeDescriptor.GetConverter(t);
            return tc.ConvertFrom(value);
        }
        /// <summary>
        /// register type with the type descriptor for later conversion
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="TC"></typeparam>
        public static void RegisterTypeConverter<T, TC>() where TC : System.ComponentModel.TypeConverter
        {
            TypeDescriptor.AddAttributes(typeof(T), new TypeConverterAttribute(typeof(TC)));
        }
    }

    /// <summary>
    /// Helper class contains extension functions for reading types other than strings from configuration,
    /// as well as reading lists of values
    /// </summary>
    public static class ConfigurationHelper
    {
        /// <summary>
        /// template function for reading different types from configuration
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="configuration"></param>
        /// <param name="section"></param>
        /// <param name="key"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static T GetConfigurationValue<T>(this IConfiguration configuration, string section, string key, T defaultValue)
        {
            Type defaultType = typeof(T);
            if(!defaultType.IsValueType && defaultType != typeof(string))
            {
                string tmpResult = configuration.GetConfigurationValue(section, key, string.Empty);

                if(!string.IsNullOrEmpty(tmpResult))
                {
                    return tmpResult.Deserialize<T>();
                }
                else
                {
                    configuration.SetConfigurationValue(section, key, defaultValue);
                    return defaultValue;
                }
            }
            else
            {
                string tmpResult = configuration.GetConfigurationValue(section, key, defaultValue.ToString());
                return !string.IsNullOrEmpty(tmpResult) ? TypeConverter.ChangeType<T>(tmpResult) : default;
            }
        }

        /// <summary>
        /// Sets configuration value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="configuration"></param>
        /// <param name="section"></param>
        /// <param name="key"></param>
        /// <param name="valueToWrite"></param>
        public static void SetConfigurationValue<T>(this IConfiguration configuration, string section, string key, T valueToWrite)
        {
            Type defaultType = typeof(T);
            if(!defaultType.IsValueType && defaultType != typeof(string))
            {
                configuration.SetConfigurationValue(section, key, valueToWrite.SerializeNoNamespace());
            }
            else
            {
                configuration.SetConfigurationValue(section, key, valueToWrite.ToString());
            }
        }

        /// <summary>
        /// returns multi-value result (list of T) from configuration
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="configuration"></param>
        /// <param name="section"></param>
        /// <param name="key"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static List<T> GetConfigurationListValue<T>(this IConfiguration configuration, string section, string key, IList<T> defaultValue)
        {
            string tmpResult = configuration.GetXmlConfiguration(section);
            if(string.IsNullOrEmpty(tmpResult) || !ContainsSection(tmpResult, key))
            {
                SetConfigurationListValue(configuration, section, key, defaultValue);
                return new List<T>(defaultValue);
            }
            else
            {
                var stringRes = BuildElementListFromXml(tmpResult, key);

                XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));

                List<T> result = new List<T>();

                foreach(string item in stringRes)
                {
                    if(string.IsNullOrEmpty(item))
                    { 
                        continue; 
                    }

                    if(item.TrimStart().StartsWith("<"))
                    {
                        result.Add(item.Deserialize<T>());
                    }
                    else
                    {
                        result.Add(TypeConverter.ChangeType<T>(item));
                    }
                }

                return result;
            }
        }

        /// <summary>
        /// Writes configuration list of values.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="configuration"></param>
        /// <param name="section"></param>
        /// <param name="key"></param>
        /// <param name="listToWrite"></param>
        public static void SetConfigurationListValue<T>(this IConfiguration configuration, string section, string key, IList<T> listToWrite)
        {
            StringBuilder xmlStr = new();
            string data = configuration.GetXmlConfiguration(section);
            if(ContainsSection(data, $"{key}s"))
            {
                xmlStr.Append($"<{key}s>");
                foreach(var item in listToWrite)
                {
                    xmlStr.Append($"<{key}>");
                    xmlStr.Append(item.ToString());
                    xmlStr.Append($"</{key}>");
                }
                xmlStr.Append($"</{key}s>");
                configuration.SetXmlConfiguration(section, xmlStr.ToString());
            }
            else
            {
                xmlStr.Append($"<{key}>");
                foreach(var item in listToWrite)
                {
                    xmlStr.Append(item.SerializeNoNamespace());
                }
                xmlStr.Append($"</{key}>");

                XElement xElement = new XElement(key, XElement.Parse(xmlStr.ToString()));

                if(string.IsNullOrEmpty(data))
                {
                    data = $"<XmlConfiguration name=\"{section}\"></XmlConfiguration>";
                }

                XDocument doc = XDocument.Parse(data);
                XElement existing = doc.Descendants(key).FirstOrDefault();

                if(existing != null)
                {
                    existing.ReplaceWith(xElement);

                    var reader = doc.CreateReader();
                    reader.MoveToContent();
                    string innerXml = reader.ReadInnerXml();

                    configuration.SetXmlConfiguration(section, innerXml);

                }
                else
                {
                    doc.Root.Add(xElement.FirstNode);
                    var reader = doc.CreateReader();
                    reader.MoveToContent();
                    string outerXml = reader.ReadOuterXml();

                    configuration.SetXmlConfiguration(section, outerXml);
                }


            }
        }

        /// <summary>
        /// returns values from XML section converted to string list
        /// </summary>
        /// <param name="data"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        private static List<string> BuildElementListFromXml(string data, string key)
        {
            XElement doc = XElement.Parse(data);
            IEnumerable<XElement> xmlMessages;

            if(ContainsSection(data, $"{key}s"))
            {
                xmlMessages = from m in doc.Elements($"{key}s").Elements(key)
                              select m;

                var messages = xmlMessages.Select(x => x.Value);
                return messages?.ToList();

            }
            else
            {
                xmlMessages = from m in doc.Elements($"{key}").Elements()
                              select m;

                List<string> result = new();

                foreach(var item in xmlMessages)
                {
                    var reader = item.CreateReader();
                    reader.MoveToContent();
                    result.Add(reader.ReadOuterXml());
                }
                return result;
            }
        }

        private static bool ContainsSection(string xmlString, string sectionName)
        {
            if(string.IsNullOrEmpty(xmlString))
            {
                return false;
            }

            try
            {
                XDocument doc = XDocument.Parse(xmlString);
                return doc.Descendants(sectionName).Any();
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// From XML configuration file reads all sections under IniConfiguration root
        /// </summary>
        /// <returns></returns>
        public static List<string> GetAvailableConfigSections(string fileName)
        {
            List<string> sections = new();

            FileInfo fileInfo = new(fileName);
            if(!fileInfo.Exists)
            {
                return null;
            }

            XDocument xdocument = XDocument.Load(fileInfo.FullName);
            if(xdocument != null)
            {
                var configurationSection = xdocument.Root.Element("IniConfiguration");
                if(configurationSection != null && configurationSection.HasElements)
                {
                    var sectionElements = configurationSection.Elements();
                    sections.AddRange(from item in sectionElements
                                      let name = item?.FirstAttribute?.Value
                                      where !string.IsNullOrEmpty(name)
                                      select name);
                }

                configurationSection = xdocument.Root.Element("XmlConfigurations");
                if(configurationSection != null && configurationSection.HasElements)
                {
                    var sectionElements = configurationSection.Elements();
                    sections.AddRange(from item in sectionElements
                                      let name = item?.FirstAttribute?.Value
                                      where !string.IsNullOrEmpty(name)
                                      select name);
                }
            }

            return sections;
        }

        /// <summary>
        /// From XML configuration file reads all keys from config sections under IniConfiguration root
        /// </summary>
        /// <returns></returns>
        public static List<string> GetAvailableConfigSectionKeys(string fileName, string sectionName)
        {
            List<string> sections = new();

            FileInfo fileInfo = new(fileName);
            if(!fileInfo.Exists)
            {
                return null;
            }

            XDocument xdocument = XDocument.Load(fileInfo.FullName);
            if(xdocument != null)
            {
                var configurationSection = xdocument.XPathSelectElement($"//IniConfiguration//section[@name='{sectionName}']");
                if(configurationSection != null && configurationSection.HasElements)
                {
                    var sectionElements = configurationSection.Elements();
                    sections.AddRange(from item in sectionElements
                                      let name = item?.FirstAttribute?.Value
                                      where !string.IsNullOrEmpty(name)
                                      select name);
                }

                configurationSection = xdocument.XPathSelectElement($"//XmlConfigurations//XmlConfiguration[@name='{sectionName}']");
                if(configurationSection != null && configurationSection.HasElements)
                {
                    var sectionElements = configurationSection.Elements();
                    sections.AddRange(from item in sectionElements
                                      let name = item?.Name
                                      where name != null
                                      select name.ToString());
                }
            }
            return sections;
        }
    }
}
