// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;

using Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;

using TrxObjectModel = Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel;

namespace Microsoft.TestPlatform.Extensions.TrxLogger.XML;

/// <summary>
/// The xml persistence class.
/// </summary>
internal class XmlPersistence
{
    #region Types

    /// <summary>
    /// The exception type that is thrown when a duplicate key is added to a hashtable or
    /// dictionary
    /// </summary>
    public class DuplicateKeyLoadException : Exception
    {
        /// <summary>
        /// Initializes the instance
        /// </summary>
        /// <param name="key">Key that was a duplicate</param>
        /// <param name="message">The duplicate-key exception message</param>
        public DuplicateKeyLoadException(object key, string message)
            : this(key, message, null)
        {
        }

        /// <summary>
        /// Initializes the instance
        /// </summary>
        /// <param name="key">Key that was a duplicate</param>
        /// <param name="message">The duplicate-key exception message</param>
        /// <param name="innerException">The inner exception</param>
        public DuplicateKeyLoadException(object key, string message, Exception? innerException)
            : base(message, innerException)
        {
            Key = key;
        }

        /// <summary>
        /// Gets the key that was a duplicate
        /// </summary>
        public object Key
        {
            get;
            private set;
        }

    }

    #endregion

    /// <summary>This is how we persist date time except DateTime.MinValue.</summary>
    private const string DateTimePersistenceFormat = "yyyy'-'MM'-'ddTHH':'mm':'ss'.'fffffffzzz";

    /// <summary>Special case to persist DateTime.MinValue.</summary>
    private const string DateTimeUtcPersistenceFormat = "u";

    private const string DefaultNamespacePrefixEquivalent = "dpe";

    private static readonly string EmptyGuidString = Guid.Empty.ToString();

    private static readonly Type BoolType = typeof(bool);
    private static readonly Type ByteArrayType = typeof(byte[]);
    private static readonly Type DateTimeType = typeof(DateTime);

    /// <summary>
    /// this is the top level cache: Type->field information
    /// </summary>
    private static readonly Dictionary<Type, IEnumerable<FieldPersistenceInfo>> TypeToPersistenceInfoCache =
        new();

    /// <summary>
    /// Optimization: avoid re-parsing same query multiple times
    /// </summary>
    private static readonly Dictionary<string, string> QueryCache = new();

    private readonly string _prefix;
    private readonly string _namespaceUri;
    private readonly XmlNamespaceManager _xmlNamespaceManager = new(new NameTable());

    /// <summary>
    /// Initializes a new instance of the <see cref="XmlPersistence"/> class.
    /// </summary>
    public XmlPersistence()
    {
        _prefix = string.Empty;
        _namespaceUri = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010";

        if (!string.IsNullOrEmpty(_prefix))
        {
            // Register the specified prefix with the specified namespace
            _xmlNamespaceManager.AddNamespace(_prefix, _namespaceUri);
        }

        if (!string.IsNullOrEmpty(_namespaceUri))
        {
            // Register a prefix for the namespace. This is needed for XPath queries, since an element in an XPath
            // expression without a prefix is assumed to be in the empty namespace, and NOT in the provided XML namespace
            // manager's default namespace (for some reason). So, we need to register a prefix that we will prepend to
            // elements that are in this namespace (if it is not already the empty namespace) in XPath queries so that we
            // will be able to find elements, and so that callers will not need to provide the appropriate prefixes.
            //
            // See the documentation for the 'XmlNamespaceManager.AddNamespace' method, specifically the Note for the
            // 'prefix' parameter.
            _xmlNamespaceManager.AddNamespace(DefaultNamespacePrefixEquivalent, _namespaceUri);
        }
    }

    /// <summary>
    /// Create root element.
    /// </summary>
    /// <param name="name">
    /// Name of the root element
    /// </param>
    /// <returns>
    /// The <see cref="XmlElement"/>.
    /// </returns>
    public XmlElement CreateRootElement(string name)
    {
        return CreateRootElement(name, _namespaceUri);
    }

    private XmlElement CreateRootElement(string name, string namespaceUri)
    {
        namespaceUri ??= _namespaceUri;

        XmlDocument dom = new();
        dom.AppendChild(dom.CreateXmlDeclaration("1.0", "UTF-8", null));
        return (XmlElement)dom.AppendChild(dom.CreateElement(_prefix, name, namespaceUri))!;
    }

    #region PublicSaveDataInTrx

    /// <summary>
    /// Save single fields.
    /// </summary>
    /// <param name="parentXml">
    /// The parent xml.
    /// </param>
    /// <param name="instance">
    /// The instance.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    public void SaveSingleFields(XmlElement parentXml, object instance, XmlTestStoreParameters? parameters)
    {
        SaveSingleFields(parentXml, instance, null, parameters);
    }

    /// <summary>
    /// Based on the StoreXml* attributes saves simple fields
    /// </summary>
    /// <param name="parentXml">
    /// Parent xml
    /// </param>
    /// <param name="instance">
    /// object to save
    /// </param>
    /// <param name="requestedType">
    /// The requested Type.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    public void SaveSingleFields(XmlElement parentXml, object? instance, Type? requestedType, XmlTestStoreParameters? parameters)
    {
        if (instance == null)
        {
            return; // nothing to do
        }

        Type type = requestedType ?? instance.GetType();

        foreach (FieldPersistenceInfo info in GetFieldInfos(type))
        {
            object? fieldValue = info.FieldInfo.GetValue(instance);
            if (fieldValue == null)
            {
                continue;
            }

            if (info.FieldAttribute != null)
            {
                SaveObject(fieldValue, parentXml, info.Location, parameters);
            }
            else if (info.SimpleFieldAttribute != null)
            {
                SaveSimpleField(parentXml, info.Location!, fieldValue, info.SimpleFieldAttribute.DefaultValue);
            }
        }
    }

    /// <summary>
    /// Based on the StoreXml* attributes saves simple fields
    /// </summary>
    /// <param name="objectToSave">
    /// The object to save.
    /// </param>
    /// <param name="parentXml">
    /// The parent xml.
    /// </param>
    /// <param name="location">
    /// The location.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    public void SaveObject(object? objectToSave, XmlElement parentXml, string? location, XmlTestStoreParameters? parameters)
    {
        if (objectToSave == null || location == null)
        {
            return;
        }

        string? nameSpaceUri = _namespaceUri;
        if (objectToSave is IXmlTestStoreCustom customStore)
        {
            nameSpaceUri = customStore.NamespaceUri;
        }

        XmlNode? xmlNode = EnsureLocationExists(parentXml, location, nameSpaceUri);
        TPDebug.Assert(xmlNode != null, "EnsureLocationExists should have returned a node");
        SaveObject(objectToSave, xmlNode, parameters);

        if (xmlNode is XmlElement element &&
            !element.HasAttributes &&
            !element.HasChildNodes &&
            element.InnerText.IsNullOrEmpty())
        {
            element.ParentNode!.RemoveChild(element);    // get rid of empty elements to keep the xml clean
        }
    }

    /// <summary>
    /// Save object.
    /// </summary>
    /// <param name="objectToSave">
    /// The object to save.
    /// </param>
    /// <param name="nodeToSaveAt">
    /// The node to save at.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    public static void SaveObject(object objectToSave, XmlNode nodeToSaveAt, XmlTestStoreParameters? parameters)
    {
        SaveObject(objectToSave, nodeToSaveAt, parameters, null);
    }

    /// <summary>
    /// Save the object.
    /// </summary>
    /// <param name="objectToSave">
    /// The object to save.
    /// </param>
    /// <param name="nodeToSaveAt">
    /// The node to save at.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    /// <param name="defaultValue">
    /// The default value.
    /// </param>
    public static void SaveObject(object? objectToSave, XmlNode nodeToSaveAt, XmlTestStoreParameters? parameters, object? defaultValue)
    {
        if (objectToSave == null)
        {
            return;
        }

        if (objectToSave is IXmlTestStore persistable)
        {
            persistable.Save((XmlElement)nodeToSaveAt, parameters);
        }
        else
        {
            SaveSimpleData(objectToSave, nodeToSaveAt, defaultValue);
        }
    }

    /// <summary>
    /// Save simple field.
    /// </summary>
    /// <param name="xml">
    /// The xml.
    /// </param>
    /// <param name="location">
    /// The location.
    /// </param>
    /// <param name="value">
    /// The value.
    /// </param>
    /// <param name="defaultValue">
    /// The default value.
    /// </param>
    public void SaveSimpleField(XmlElement xml, string location, object? value, object? defaultValue)
    {
        if (value == null || value.Equals(defaultValue))
        {
            return;
        }

        XmlNode? saveTarget = EnsureLocationExists(xml, location);
        TPDebug.Assert(saveTarget != null, "EnsureLocationExists should have returned a node");
        SaveSimpleData(value, saveTarget, defaultValue);
    }

    /// <summary>
    /// Save GUID.
    /// </summary>
    /// <param name="xml">
    /// The xml.
    /// </param>
    /// <param name="location">
    /// The location.
    /// </param>
    /// <param name="guid">
    /// The GUID.
    /// </param>
    public void SaveGuid(XmlElement xml, string location, Guid guid)
    {
        SaveSimpleField(xml, location, guid.ToString(), EmptyGuidString);
    }

    public void SaveHashtable(Hashtable ht, XmlElement element, string location, string keyLocation, string? valueLocation, string itemElementName, XmlTestStoreParameters? parameters)
    {
        if (ht == null || ht.Count <= 0)
        {
            return;
        }

        XmlElement? dictionaryElement = (XmlElement?)EnsureLocationExists(element, location);
        TPDebug.Assert(dictionaryElement != null, "EnsureLocationExists should have returned a node");
        foreach (DictionaryEntry de in ht)
        {
            XmlElement itemXml = CreateElement(dictionaryElement, itemElementName);

            SaveObject(de.Key, itemXml, keyLocation, parameters);
            SaveObject(de.Value, itemXml, valueLocation, parameters);
        }
    }

    public void SaveStringDictionary(StringDictionary dict, XmlElement element, string location, string keyLocation, string valueLocation, string itemElementName, XmlTestStoreParameters parameters)
    {
        if (dict == null || dict.Count <= 0)
        {
            return;
        }

        XmlElement? dictionaryElement = (XmlElement?)EnsureLocationExists(element, location);
        TPDebug.Assert(dictionaryElement != null, "EnsureLocationExists should have returned a node");
        foreach (DictionaryEntry de in dict)
        {
            XmlElement itemXml = CreateElement(dictionaryElement, itemElementName);

            SaveObject(de.Key, itemXml, keyLocation, parameters);
            SaveObject(de.Value, itemXml, valueLocation, parameters);
        }
    }

    #region Lists
    /// <summary>
    /// Save list of object .
    /// </summary>
    /// <param name="list">
    /// The list.
    /// </param>
    /// <param name="element">
    /// The parent element.
    /// </param>
    /// <param name="listXmlElement">
    /// The list xml element.
    /// </param>
    /// <param name="itemLocation">
    /// The item location.
    /// </param>
    /// <param name="itemElementName">
    /// The item element name.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    public void SaveIEnumerable(IEnumerable? list, XmlElement element, string listXmlElement, string itemLocation, string? itemElementName, XmlTestStoreParameters? parameters)
    {
        if (list == null || !list.GetEnumerator().MoveNext())
        {
            return;
        }

        XmlElement? listElement = (XmlElement?)EnsureLocationExists(element, listXmlElement);
        TPDebug.Assert(listElement != null, "EnsureLocationExists should have returned a node");
        foreach (object item in list)
        {
            XmlElement itemXml = CreateElement(listElement, itemElementName, item);
            SaveObject(item, itemXml, itemLocation, parameters);
        }
    }

    /// <summary>
    /// Save list.
    /// </summary>
    /// <param name="list">
    /// The list.
    /// </param>
    /// <param name="element">
    /// The element.
    /// </param>
    /// <param name="listXmlElement">
    /// The list xml element.
    /// </param>
    /// <param name="itemLocation">
    /// The item location.
    /// </param>
    /// <param name="itemElementName">
    /// The item element name.
    /// </param>
    /// <param name="parameters">
    /// The parameters.
    /// </param>
    /// <typeparam name="V"> Generic parameter
    /// </typeparam>
    public void SaveList<V>(IList<V> list, XmlElement element, string listXmlElement, string itemLocation, string itemElementName, XmlTestStoreParameters parameters)
        where V : notnull
    {
        if (list == null || list.Count <= 0)
        {
            return;
        }

        XmlElement? listElement = (XmlElement?)EnsureLocationExists(element, listXmlElement);
        TPDebug.Assert(listElement != null, "EnsureLocationExists should have returned a node");
        foreach (V item in list)
        {
            XmlElement itemXml = CreateElement(listElement, itemElementName, item);
            SaveObject(item, itemXml, itemLocation, parameters);
        }
    }

    #region Counters

    public void SaveCounters(XmlElement xml, string location, int[] counters)
    {
        xml = (XmlElement)LocationToXmlNode(xml, location)!;

        for (int i = 0; i < counters.Length; i++)
        {
            TrxObjectModel.TestOutcome outcome = (TrxObjectModel.TestOutcome)i;
            string attributeName = outcome.ToString();
            attributeName = attributeName.Substring(0, 1).ToLowerInvariant() + attributeName.Substring(1);

            xml.SetAttribute(attributeName, counters[i].ToString(CultureInfo.InvariantCulture));
        }
    }

    #endregion Counters

    #endregion List

    internal static void SaveUsingReflection(XmlElement element, object instance, Type? requestedType, XmlTestStoreParameters? parameters)
    {
        XmlPersistence helper = new();
        helper.SaveSingleFields(element, instance, requestedType, parameters);
    }

    #endregion PublicSaveDataInTrx

    #region Utilities

    #region Optimization: Reflection caching

    /// <summary>
    /// Updates the cache if needed and gets the field info collection
    /// </summary>
    /// <param name="type">
    /// The type.
    /// </param>
    /// <returns>
    /// The <see cref="IEnumerable"/>.
    /// </returns>
    private static IEnumerable<FieldPersistenceInfo> GetFieldInfos(Type type)
    {
        if (!TypeToPersistenceInfoCache.TryGetValue(type, out var toReturn))
        {
            toReturn = ReflectFields(type);
            lock (TypeToPersistenceInfoCache)
            {
                if (!TypeToPersistenceInfoCache.TryGetValue(type, out var checkCache))
                {
                    TypeToPersistenceInfoCache.Add(type, toReturn);
                }
            }
        }

        return toReturn;
    }

    /// <summary>
    /// EXPENSIVE! Uses reflection to build a cache item of information about the type and its fields
    /// </summary>
    /// <param name="type">the type to reflect on</param>
    /// <returns>collection of field information</returns>
    private static IEnumerable<FieldPersistenceInfo> ReflectFields(Type type)
    {
        List<FieldPersistenceInfo> toReturn = new();

        foreach (
            FieldInfo reflectedFieldInfo in
            type.GetFields(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
        {
            FieldPersistenceInfo info = new(reflectedFieldInfo);

            // only fields with known location need to be persisted
            if (!string.IsNullOrEmpty(info.Location))
            {
                toReturn.Add(info);
            }
        }

        return toReturn;
    }

    #endregion Optimization: Reflection caching

    /// <summary>
    /// Convert dateTime to string.
    /// </summary>
    /// <param name="dateTime">
    /// The date time.
    /// </param>
    /// <returns>
    /// The <see cref="string"/>.
    /// </returns>
    private static string DateTimeToString(DateTime dateTime)
    {
        if (dateTime == DateTime.MinValue)
        {
            // DateTime.MinValue has Kind.Unspecified and thus it does not make any sense to convert it to local/universal.
            // Also note that we use format w/o time zones to persist DateTime.MinValue.
            return dateTime.ToString(DateTimeUtcPersistenceFormat, CultureInfo.InvariantCulture.DateTimeFormat);
        }
        else
        {
            // Ensure that the datetime value is in local time..
            // This is needed as the persistence format we use needs the datetime to be in local time..
            DateTime localDateTime = dateTime.ToLocalTime();
            return localDateTime.ToString(DateTimePersistenceFormat, CultureInfo.InvariantCulture.DateTimeFormat);
        }
    }

    private static string? GetFieldLocation(FieldInfo fieldInfo)
    {
        string? location = null;

        StoreXmlAttribute? locationAttribute = GetAttribute<StoreXmlAttribute>(fieldInfo);
        if (locationAttribute != null)
        {
            location = locationAttribute.Location ?? GetDefaultFieldLocation(fieldInfo);
        }

        return location;
    }

    private static string GetDefaultFieldLocation(FieldInfo fieldInfo)
    {
        string fieldName = fieldInfo.Name;
        string defaultFieldLocation = fieldName.StartsWith("m_", StringComparison.Ordinal) ? fieldName.Substring(2, fieldName.Length - 2) : fieldName;

        if (!ImplementsIXmlTestStore(fieldInfo.FieldType))
        {
            defaultFieldLocation = '@' + defaultFieldLocation;
        }

        return defaultFieldLocation;
    }

    private static bool ImplementsIXmlTestStore(Type type)
    {
        return type.GetInterface(typeof(IXmlTestStore).Name) != null;
    }

    private static T? GetAttribute<T>(FieldInfo fieldInfo) where T : Attribute
    {
        var attributes = fieldInfo.GetCustomAttributes(typeof(T), false).ToArray();

        return attributes.Length > 0 ? (T)attributes[0] : default;
    }

    private static void SaveSimpleData(object? value, XmlNode nodeToSaveAt, object? defaultValue)
    {
        if (value == null || value.Equals(defaultValue))
        {
            return;
        }

        Type valueType = value.GetType();

        string? valueToSave;
        if (valueType == BoolType)
        {
            valueToSave = value.ToString()!.ToLowerInvariant();
        }
        else if (valueType == ByteArrayType)
        {
            // Use only for Arrays, Collections and Lists. E.g. string is also IEnumerable.
            valueToSave = Convert.ToBase64String((byte[])value);
        }
        else if (valueType == DateTimeType)
        {
            // always save as a sortable date time with fractional seconds.
            valueToSave = DateTimeToString((DateTime)value);
        }
        else
        {
            // use the appropriate type converter to get a culture invariant string.
            try
            {
                TypeConverter convert = TypeDescriptor.GetConverter(valueType);
                valueToSave = convert.ConvertToInvariantString(value);
            }
            catch (NotSupportedException nosupportEx)
            {
                EqtTrace.Info("TypeConverter not supported for {0} : NotSupportedException: {1}", value.ToString(), nosupportEx);
                valueToSave = value.ToString();
            }
        }

        // Remove invalid char if any
        valueToSave = RemoveInvalidXmlChar(valueToSave);
        if (nodeToSaveAt is XmlElement elementToSaveAt)
        {
            TPDebug.Assert(valueToSave is not null, "valueToSave is null");
            elementToSaveAt.InnerText = valueToSave;
        }
        else
        {
            nodeToSaveAt.Value = valueToSave;
        }
    }

    public XmlNode? EnsureLocationExists(XmlElement xml, string location)
    {
        return EnsureLocationExists(xml, location, _namespaceUri);
    }

    private static string? RemoveInvalidXmlChar(string? str)
    {
        if (str == null)
        {
            return null;
        }

        // From xml spec (http://www.w3.org/TR/xml/#charsets) valid chars:
        // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]

        // we are handling only #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
        // because C# support unicode character in range \u0000 to \uFFFF
        MatchEvaluator evaluator = new(ReplaceInvalidCharacterWithUniCodeEscapeSequence);
        string invalidChar = @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]";
        return Regex.Replace(str, invalidChar, evaluator);
    }

    private static string ReplaceInvalidCharacterWithUniCodeEscapeSequence(Match match)
    {
        char x = match.Value[0];
        return $@"\u{(ushort)x:x4}";
    }

    private XmlNode? EnsureLocationExists(XmlElement xml, string location, string? nameSpaceUri)
    {
        XmlNode? node = LocationToXmlNode(xml, location);
        if (node != null)
        {
            return node;
        }

        if (location.StartsWith("@", StringComparison.Ordinal))
        {
            string attributeName = location.Substring(1, location.Length - 1);
            return xml.HasAttribute(attributeName)
                ? xml.GetAttributeNode(attributeName)
                : (XmlNode)xml.Attributes.Append(xml.OwnerDocument.CreateAttribute(attributeName));
        }
        else
        {
            string[] parts = location.Split(['/'], 2);
            string firstPart = parts[0];

            XmlNode? firstChild = LocationToXmlNode(xml, firstPart);
            firstChild ??= CreateElement(xml, firstPart, GetNamespaceUriOrDefault(nameSpaceUri));

            return parts.Length > 1 ? EnsureLocationExists((XmlElement)firstChild, parts[1]) : firstChild;
        }
    }

    private string GetNamespaceUriOrDefault(string? nameSpaceUri)
    {
        return nameSpaceUri ?? _namespaceUri;
    }

    /// <summary>
    /// Creates a new element with the given name in the current (of this instance of XmlPersistence namespace)
    /// </summary>
    /// <param name="xml">parent xml</param>
    /// <param name="name">element name</param>
    /// <returns>a new XmlElement attached to the parent</returns>
    private XmlElement CreateElement(XmlElement xml, string name)
    {
        return CreateElement(xml, name, _namespaceUri);
    }

    private XmlElement CreateElement(XmlElement xml, string name, string? elementNamespaceUri)
    {
        return (XmlElement)xml.AppendChild(xml.OwnerDocument.CreateElement(_prefix, name, elementNamespaceUri))!;
    }

    /// <summary>
    /// Accepts name == null
    /// </summary>
    /// <param name="parent"> parent xml</param>
    /// <param name="name">The local name of the new element. </param>
    /// <param name="instance">the object for which element has to create</param>
    /// <returns>a new XmlElement attached to the parent</returns>
    private XmlElement CreateElement(XmlElement parent, string? name, object instance)
    {
        if (name != null)
        {
            return CreateElement(parent, name);
        }
        else
        {
            NewElementCreateData createData = GetElementCreateData(instance);
            TPDebug.Assert(createData.ElementName is not null, "createData.ElementName is null");
            return CreateElement(parent, createData.ElementName, createData.NamespaceUri);
        }
    }

    private NewElementCreateData GetElementCreateData(object persistee)
    {
        TPDebug.Assert(persistee != null, "persistee is null");

        NewElementCreateData toReturn = new();
        if (persistee is IXmlTestStoreCustom custom)
        {
            toReturn.ElementName = custom.ElementName;
            toReturn.NamespaceUri = custom.NamespaceUri;
        }

        toReturn.ElementName ??= persistee.GetType().Name;

        toReturn.NamespaceUri ??= _namespaceUri;

        return toReturn;
    }

    private XmlNode? LocationToXmlNode(XmlElement element, string location)
    {
        location = ProcessXPathQuery(location);

        try
        {
            return element.SelectSingleNode(location, _xmlNamespaceManager);
        }
        catch (System.Xml.XPath.XPathException e)
        {
            throw new Exception($"The persistence location is invalid. Element: '{element.Name}', location: '{location}'", e);
        }
    }

    private string ProcessXPathQuery(string queryIn)
    {
        // If we are working with the empty namespace, there is no need to decorate elements in the XPath expression, since
        // elements in the XPath expression that don't have a prefix will be searched for in the empty namespace.
        if (string.IsNullOrEmpty(_namespaceUri))
        {
            return queryIn;
        }

        if (QueryCache.ContainsKey(queryIn))
        {
            return QueryCache[queryIn];
        }

        // fix the empty namespaces to a temp prefix, so xpath query can understand them
        string[] parts = queryIn.Split(['/'], StringSplitOptions.None);

        StringBuilder query = new();

        foreach (string part in parts)
        {
            if (query.Length > 0 || queryIn.StartsWith("/", StringComparison.Ordinal))
            {
                query.Append('/');
            }

            if (part != "." && part != ".." && !part.Contains(":") && (part.Length > 0) && (!part.StartsWith("@", StringComparison.Ordinal)))
            {
                query.Append(DefaultNamespacePrefixEquivalent + ":");
            }

            query.Append(part);
        }

        string queryString = query.ToString();
        QueryCache[queryIn] = queryString;

        return queryString;
    }

    #endregion Utilities

    #region Types

    private class NewElementCreateData
    {
        public string? NamespaceUri { get; set; }

        public string? ElementName { get; set; }
    }

    /// <summary>
    /// caches information about a field
    /// </summary>
    private class FieldPersistenceInfo
    {
        internal readonly FieldInfo FieldInfo;

        internal readonly string? Location;

        internal readonly StoreXmlAttribute? Attribute;

        internal readonly StoreXmlSimpleFieldAttribute? SimpleFieldAttribute;

        internal readonly StoreXmlFieldAttribute? FieldAttribute;

        internal FieldPersistenceInfo(FieldInfo fieldInfo)
        {
            FieldInfo = fieldInfo;
            Location = GetFieldLocation(fieldInfo);

            Attribute = GetAttribute<StoreXmlAttribute>(fieldInfo);
            SimpleFieldAttribute = Attribute as StoreXmlSimpleFieldAttribute;
            FieldAttribute = Attribute as StoreXmlFieldAttribute;
        }
    }

    #endregion
}
