using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace IPA.Utilities.Async
{
    /// <summary>
    /// A dictionary-like type intended for thread-safe value caches whose values are created only once ever.
    /// </summary>
    /// <typeparam name="TKey">the key type of the cache</typeparam>
    /// <typeparam name="TValue">the value type of the cache</typeparam>
    /// <remarks>
    /// This object basically wraps a <see cref="ConcurrentDictionary{TKey, TValue}"/> with some special handling
    /// to ensure that values are only created once ever, without having multiple parallel constructions.
    /// </remarks>
    public class SingleCreationValueCache<TKey, TValue>
    {
        private readonly ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)> dict;

        private static KeyValuePair<TKey, (ManualResetEventSlim, TValue)> ExpandKeyValuePair(KeyValuePair<TKey, TValue> kvp)
            => new KeyValuePair<TKey, (ManualResetEventSlim, TValue)>(kvp.Key, (null, kvp.Value));
        private static KeyValuePair<TKey, TValue> CompressKeyValuePair(KeyValuePair<TKey, (ManualResetEventSlim, TValue value)> kvp)
            => new KeyValuePair<TKey, TValue>(kvp.Key, kvp.Value.value);

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
        /// class that is empty, has the default concurrency level, has the default initial
        /// capacity, and uses the default comparer for the key type.
        /// </summary>
        public SingleCreationValueCache()
            => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>();
        /// <summary>
        /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
        /// class that contains elements copied from the specified <see cref="IEnumerable{T}"/>,
        /// has the default concurrency level, has the default initial capacity, and uses
        /// the default comparer for the key type.
        /// </summary>
        /// <param name="collection">the <see cref="IEnumerable{T}"/> whose element are to be used for the new cache</param>
        /// <exception cref="ArgumentNullException">when any arguments are null</exception>
        /// <exception cref="ArgumentException"><paramref name="collection"/> contains duplicate keys</exception>
        public SingleCreationValueCache(IEnumerable<KeyValuePair<TKey, TValue>> collection)
            => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(collection.Select(ExpandKeyValuePair));
        /// <summary>
        /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
        /// class that is empty, has the default concurrency level and capacity, and uses
        /// the specified <see cref="IEqualityComparer{T}"/>.
        /// </summary>
        /// <param name="comparer">the equality comparer to use when comparing keys</param>
        /// <exception cref="ArgumentNullException"><paramref name="comparer"/> is null</exception>
        public SingleCreationValueCache(IEqualityComparer<TKey> comparer)
            => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(comparer);
        /// <summary>
        /// Initializes a new instance of the <see cref="SingleCreationValueCache{TKey, TValue}"/>
        /// class that contains elements copied from the specified <see cref="IEnumerable{T}"/>
        /// has the default concurrency level, has the default initial capacity, and uses
        /// the specified <see cref="IEqualityComparer{T}"/>.
        /// </summary>
        /// <param name="collection">the <see cref="IEnumerable{T}"/> whose elements are to be used for the new cache</param>
        /// <param name="comparer">the equality comparer to use when comparing keys</param>
        /// <exception cref="ArgumentNullException"><paramref name="collection"/> or <paramref name="comparer"/> is null</exception>
        public SingleCreationValueCache(IEnumerable<KeyValuePair<TKey, TValue>> collection, IEqualityComparer<TKey> comparer)
            => dict = new ConcurrentDictionary<TKey, (ManualResetEventSlim wh, TValue val)>(collection.Select(ExpandKeyValuePair), comparer);
        #endregion

        /// <summary>
        /// Gets a value that indicates whether this cache is empty. 
        /// </summary>
        public bool IsEmpty => dict.IsEmpty;
        /// <summary>
        /// Gets the number of elements that this cache contains.
        /// </summary>
        public int Count => dict.Count;

        /// <summary>
        /// Clears the cache.
        /// </summary>
        public void Clear() => dict.Clear();
        /// <summary>
        /// Gets a value indicating whether or not this cache contains <paramref name="key"/>.
        /// </summary>
        /// <param name="key">the key to search for</param>
        /// <returns><see langword="true"/> if the cache contains the key, <see langword="false"/> otherwise</returns>
        public bool ContainsKey(TKey key) => dict.ContainsKey(key);
        /// <summary>
        /// Copies the key-value pairs stored by the cache to a new array, filtering all elements that are currently being
        /// created.
        /// </summary>
        /// <returns>an array containing a snapshot of the key-value pairs contained in this cache</returns>
        public KeyValuePair<TKey, TValue>[] ToArray()
            => dict.ToArray().Where(k => k.Value.wh == null).Select(CompressKeyValuePair).ToArray();

        /// <summary>
        /// Attempts to get the value associated with the specified key from the cache.
        /// </summary>
        /// <param name="key">the key to search for</param>
        /// <param name="value">the value retrieved, if any</param>
        /// <returns><see langword="true"/> if the value was found, <see langword="false"/> otherwise</returns>
        public bool TryGetValue(TKey key, out TValue value)
        {
            if (dict.TryGetValue(key, out var pair) && pair.wh == null)
            {
                value = pair.val;
                return true;
            }
            value = default;
            return false;
        }

        /// <summary>
        /// Gets the value associated with the specified key from the cache. If it does not exist, and
        /// no creators are currently running for this key, then the creator is called to create the value
        /// and the value is added to the cache. If there is a creator currently running for the key, then
        /// this waits for the creator to finish and retrieves the value.
        /// </summary>
        /// <param name="key">the key to search for</param>
        /// <param name="creator">the delegate to use to create the value if it does not exist</param>
        /// <returns>the value that was found, or the result of <paramref name="creator"/></returns>
        public TValue GetOrAdd(TKey key, Func<TKey, TValue> creator)
        {
        retry:
            if (dict.TryGetValue(key, out var value))
            {
                if (value.wh != null)
                {
                    value.wh.Wait();
                    goto retry; // this isn't really a good candidate for a loop
                    // the loop condition will never be hit, and this should only
                    //   jump back to the beginning in exceptional situations
                }
                return value.val;
            }
            else
            {
                var wh = new ManualResetEventSlim(false);
                var cmp = (wh, default(TValue));
                if (!dict.TryAdd(key, cmp))
                    goto retry; // someone else beat us to the punch, retry getting their value and wait for them
                var val = creator(key);
                while (!dict.TryUpdate(key, (null, val), cmp))
                    throw new InvalidOperationException();
                wh.Set();
                return val;
            }
        }
    }
}
