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

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;

using Microsoft.Build.Shared;

#nullable disable

namespace Microsoft.Build.Collections
{
    /// <summary>
    /// A dictionary of unordered property or metadata name/value pairs, with copy-on-write semantics.
    ///
    /// The copy-on-write semantics are only possible if the contained type is immutable, which currently
    /// means it can only be used for ProjectMetadataInstance's.
    /// USE THIS DICTIONARY ONLY FOR IMMUTABLE TYPES. OTHERWISE USE PROPERTYDICTIONARY.
    ///
    /// </summary>
    /// <remarks>
    /// The value that this adds over IDictionary&lt;string, T&gt; is:
    ///     - supports copy on write
    ///     - enforces that key = T.Name
    ///     - default enumerator is over values
    ///     - (marginal) enforces the correct key comparer
    ///
    /// Really a Dictionary&lt;string, T&gt; where the key (the name) is obtained from IKeyed.Key.
    /// Is not observable, so if clients wish to observe modifications they must mediate them themselves and
    /// either not expose this collection or expose it through a readonly wrapper.
    ///
    /// This collection is safe for concurrent readers and a single writer.
    /// </remarks>
    /// <typeparam name="T">Property or Metadata class type to store</typeparam>
    [DebuggerDisplay("#Entries={Count}")]
    internal sealed class CopyOnWritePropertyDictionary<T> : ICopyOnWritePropertyDictionary<T>, IEquatable<CopyOnWritePropertyDictionary<T>>
        where T : class, IKeyed, IValued, IEquatable<T>, IImmutable
    {
        private static readonly ImmutableDictionary<string, T> NameComparerDictionaryPrototype = ImmutableDictionary.Create<string, T>(MSBuildNameIgnoreCaseComparer.Default);

        /// <summary>
        /// Backing dictionary.
        /// </summary>
        private ImmutableDictionary<string, T> _backing;

        /// <summary>
        /// Creates empty dictionary.
        /// </summary>
        public CopyOnWritePropertyDictionary()
        {
            _backing = NameComparerDictionaryPrototype;
        }

        /// <summary>
        /// Cloning constructor, with deferred cloning semantics.
        /// </summary>
        private CopyOnWritePropertyDictionary(CopyOnWritePropertyDictionary<T> that)
        {
            _backing = that._backing;
        }

        /// <summary>
        /// Accessor for the list of property names.
        /// </summary>
        ICollection<string> IDictionary<string, T>.Keys => ((IDictionary<string, T>)_backing).Keys;

        /// <summary>
        /// Accessor for the list of properties.
        /// </summary>
        ICollection<T> IDictionary<string, T>.Values => ((IDictionary<string, T>)_backing).Values;

        /// <summary>
        /// Whether the collection is read-only.
        /// </summary>
        bool ICollection<KeyValuePair<string, T>>.IsReadOnly => false;

        /// <summary>
        /// Returns the number of properties in the collection.
        /// </summary>
        public int Count => _backing.Count;

        /// <summary>
        /// Get the property with the specified name, or null if none exists.
        /// Sets the property with the specified name, overwriting it if already exists.
        /// </summary>
        /// <remarks>
        /// Unlike Dictionary&lt;K,V&gt;[K], the getter returns null instead of throwing if the key does not exist.
        /// This better matches the semantics of property, which are considered to have a blank value if they
        /// are not defined.
        /// </remarks>
        public T this[string name]
        {
            get
            {
                // We don't want to check for a zero length name here, since that is a valid name
                // and should return a null instance which will be interpreted as blank
                _backing.TryGetValue(name, out T projectProperty);
                return projectProperty;
            }

            set
            {
                ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value");
                ErrorUtilities.VerifyThrow(String.Equals(name, value.Key, StringComparison.OrdinalIgnoreCase), "Key must match value's key");
                Set(value);
            }
        }

        /// <summary>
        /// Returns true if a property with the specified name is present in the collection,
        /// otherwise false.
        /// </summary>
        public bool Contains(string name) => _backing.ContainsKey(name);

        public string GetEscapedValue(string name)
        {
            if (_backing.TryGetValue(name, out T value))
            {
                return value?.EscapedValue;
            }

            return null;
        }

        /// <summary>
        /// Empties the collection
        /// </summary>
        public void Clear()
        {
            _backing = _backing.Clear();
        }

        /// <summary>
        /// Gets an enumerator over all the properties in the collection
        /// Enumeration is in undefined order. This overload exposes the struct enumerator
        /// directly to avoid an allocation due to boxing.
        /// </summary>
        public ImmutableDictionary<string, T>.Enumerator GetEnumerator() => _backing.GetEnumerator();

        /// <summary>
        /// Gets an enumerator over all the properties in the collection
        /// Enumeration is in undefined order
        /// </summary>
        IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);

        /// <summary>
        /// Get an enumerator over entries
        /// </summary>
        IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);

        #region IEquatable<CopyOnWritePropertyDictionary<T>> Members

        /// <summary>
        /// Compares two property dictionaries for equivalence.  They are equal if each contains the same properties with the
        /// same values as the other, unequal otherwise.
        /// </summary>
        /// <param name="other">The dictionary to which this should be compared</param>
        /// <returns>True if they are equivalent, false otherwise.</returns>
        public bool Equals(CopyOnWritePropertyDictionary<T> other)
        {
            if (other == null)
            {
                return false;
            }

            // Copy both backing collections to locals
            ImmutableDictionary<string, T> thisBacking = _backing;
            ImmutableDictionary<string, T> thatBacking = other._backing;

            // If the backing collections are the same, we are equal.
            // Note that with this check, we intentionally avoid the common reference
            // comparison between 'this' and 'other'.
            if (ReferenceEquals(thisBacking, thatBacking))
            {
                return true;
            }

            if (thisBacking.Count != thatBacking.Count)
            {
                return false;
            }

            foreach (T thisProp in thisBacking.Values)
            {
                if (!thatBacking.TryGetValue(thisProp.Key, out T thatProp) ||
                    !EqualityComparer<T>.Default.Equals(thisProp, thatProp))
                {
                    return false;
                }
            }

            return true;
        }

        #endregion

        #region IEquatable<CopyOnWritePropertyDictionary<T>> Members

        /// <summary>
        /// Compares two property dictionaries for equivalence.  They are equal if each contains the same properties with the
        /// same values as the other, unequal otherwise.
        /// </summary>
        /// <param name="other">The dictionary to which this should be compared</param>
        /// <returns>True if they are equivalent, false otherwise.</returns>
        public bool Equals(ICopyOnWritePropertyDictionary<T> other)
        {
            if (other == null)
            {
                return false;
            }

            ImmutableDictionary<string, T> thisBacking = _backing;
            IDictionary<string, T> otherDict = other;

            if (other is CopyOnWritePropertyDictionary<T> otherCopyOnWritePropertyDictionary)
            {
                // If the backing collections are the same, we are equal.
                // Note that with this check, we intentionally avoid the common reference
                // comparison between 'this' and 'other'.
                if (ReferenceEquals(thisBacking, otherCopyOnWritePropertyDictionary._backing))
                {
                    return true;
                }

                otherDict = otherCopyOnWritePropertyDictionary._backing;
            }

            if (thisBacking.Count != otherDict.Count)
            {
                return false;
            }

            foreach (T thisProp in thisBacking.Values)
            {
                if (!otherDict.TryGetValue(thisProp.Key, out T thatProp) ||
                    !EqualityComparer<T>.Default.Equals(thisProp, thatProp))
                {
                    return false;
                }
            }

            return true;
        }

        #endregion

        #region IDictionary<string,T> Members

        /// <summary>
        /// Adds a property
        /// </summary>
        void IDictionary<string, T>.Add(string key, T value)
        {
            ErrorUtilities.VerifyThrowInternalNull(value, "Properties can't have null value");
            ErrorUtilities.VerifyThrow(key == value.Key, "Key must match value's key");
            Set(value);
        }

        /// <summary>
        /// Returns true if the dictionary contains the key
        /// </summary>
        bool IDictionary<string, T>.ContainsKey(string key) => _backing.ContainsKey(key);

        /// <summary>
        /// Attempts to retrieve the a property.
        /// </summary>
        bool IDictionary<string, T>.TryGetValue(string key, out T value) => _backing.TryGetValue(key, out value);

        #endregion

        #region ICollection<KeyValuePair<string,T>> Members

        /// <summary>
        /// Adds a property
        /// </summary>
        void ICollection<KeyValuePair<string, T>>.Add(KeyValuePair<string, T> item)
        {
            ((IDictionary<string, T>)this).Add(item.Key, item.Value);
        }

        /// <summary>
        /// Checks for a property in the collection
        /// </summary>
        bool ICollection<KeyValuePair<string, T>>.Contains(KeyValuePair<string, T> item)
        {
            if (_backing.TryGetValue(item.Key, out T value))
            {
                return EqualityComparer<T>.Default.Equals(value, item.Value);
            }

            return false;
        }

        /// <summary>
        /// Not implemented
        /// </summary>
        void ICollection<KeyValuePair<string, T>>.CopyTo(KeyValuePair<string, T>[] array, int arrayIndex)
        {
            ErrorUtilities.ThrowInternalError("CopyTo is not supported on PropertyDictionary.");
        }

        /// <summary>
        /// Removes a property from the collection
        /// </summary>
        bool ICollection<KeyValuePair<string, T>>.Remove(KeyValuePair<string, T> item)
        {
            ErrorUtilities.VerifyThrow(item.Key == item.Value.Key, "Key must match value's key");
            return Remove(item.Key);
        }

        #endregion

        #region IEnumerable<KeyValuePair<string,T>> Members

        /// <summary>
        /// Get an enumerator over the entries.
        /// </summary>
        IEnumerator<KeyValuePair<string, T>> IEnumerable<KeyValuePair<string, T>>.GetEnumerator()
        {
            return _backing.GetEnumerator();
        }

        #endregion

        /// <summary>
        /// Removes any property with the specified name.
        /// Returns true if the property was in the collection, otherwise false.
        /// </summary>
        public bool Remove(string name)
        {
            ErrorUtilities.VerifyThrowArgumentLength(name);

            return ImmutableInterlocked.TryRemove(ref _backing, name, out _);
        }

        /// <summary>
        /// Add the specified property to the collection.
        /// Overwrites any property with the same name already in the collection.
        /// To remove a property, use Remove(...) instead.
        /// </summary>
        public void Set(T projectProperty)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectProperty);

            _backing = _backing.SetItem(projectProperty.Key, projectProperty);
        }

        /// <summary>
        /// Adds the specified properties to this dictionary.
        /// </summary>
        /// <param name="other">An enumerator over the properties to add.</param>
        public void ImportProperties(IEnumerable<T> other)
        {
            ImmutableDictionary<string, T>.Builder builder = null;

            if (other is CopyOnWritePropertyDictionary<T> copyOnWriteDictionary)
            {
                foreach (KeyValuePair<string, T> kvp in copyOnWriteDictionary)
                {
                    builder ??= _backing.ToBuilder();
                    builder[kvp.Value.Key] = kvp.Value;
                }
            }
            else
            {
                foreach (T property in other)
                {
                    builder ??= _backing.ToBuilder();
                    builder[property.Key] = property;
                }
            }

            if (builder is not null)
            {
                _backing = builder.ToImmutable();
            }
        }

        /// <summary>
        /// Clone. As we're copy on write, this
        /// should be cheap.
        /// </summary>
        public ICopyOnWritePropertyDictionary<T> DeepClone()
        {
            return new CopyOnWritePropertyDictionary<T>(this);
        }

        /// <summary>
        /// Struct based enumerator to expose the values of the backing collection.
        /// This avoids the allocation when accessing the Values property directly.
        /// </summary>
        public struct Enumerator : IEnumerator<T>
        {
            private ImmutableDictionary<string, T>.Enumerator _dictionaryEnumerator;
            public Enumerator(CopyOnWritePropertyDictionary<T> dictionary)
            {
                _dictionaryEnumerator = dictionary._backing.GetEnumerator();
            }

            public T Current { get; private set; }

            readonly object IEnumerator.Current => Current;

            public void Dispose() => _dictionaryEnumerator.Dispose();

            public bool MoveNext()
            {
                if (_dictionaryEnumerator.MoveNext())
                {
                    Current = _dictionaryEnumerator.Current.Value;

                    return true;
                }

                return false;
            }

            public void Reset() => _dictionaryEnumerator.Reset();
        }
    }
}
