using System.Collections.Generic;

namespace ZeroLevel.Services.Cache
{
    /// <summary>
    /// A least-recently-used cache stored like a dictionary.
    /// </summary>
    /// <typeparam name="TKey">The type of the key to the cached item.</typeparam>
    /// <typeparam name="TValue">The type of the cached item.</typeparam>
    public sealed class LRUCache<TKey, TValue>
    {
        /// <summary>
        /// Default maximum number of elements to cache.
        /// </summary>
        private const int DefaultCapacity = 255;

        private readonly object _lockObj = new object();
        private readonly int _capacity;
        private readonly Dictionary<TKey, Entry> _cacheMap;
        private readonly LinkedList<TKey> _cacheList;

        /// <summary>
        /// Initializes a new instance of the <see cref="LRUCache{TKey, TValue}"/> class.
        /// </summary>
        public LRUCache()
            : this(DefaultCapacity)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="LRUCache{TKey, TValue}"/> class.
        /// </summary>
        /// <param name="capacity">Maximum number of elements to cache.</param>
        public LRUCache(int capacity)
        {
            _capacity = capacity > 0 ? capacity : DefaultCapacity;
            _cacheMap = new Dictionary<TKey, Entry>();
            _cacheList = new LinkedList<TKey>();
        }

        /// <summary>
        /// Gets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key of the value to get.</param>
        /// <param name="value">When this method returns, contains the value associated with
        /// the specified key, if the key is found; otherwise, the default value for the 
        /// type of the <paramref name="value" /> parameter.</param>
        /// <returns>true if contains an element with the specified key; otherwise, false.</returns>
        public bool TryGet(TKey key, out TValue value)
        {
            lock (_lockObj)
            {
                if (_cacheMap.TryGetValue(key, out var entry))
                {
                    Touch(entry.Node);
                    value = entry.Value;
                    return true;
                }
            }

            value = default(TValue)!;
            return false;
        }

        /// <summary>
        /// Adds the specified key and value to the cache.
        /// </summary>
        /// <param name="key">The key of the element to add.</param>
        /// <param name="value">The value of the element to add.</param>
        public void Set(TKey key, TValue value)
        {
            lock (_lockObj)
            {
                if (!_cacheMap.TryGetValue(key, out var entry))
                {
                    LinkedListNode<TKey> node;
                    if (_cacheMap.Count >= _capacity)
                    {
                        node = _cacheList.Last;
                        _cacheMap.Remove(node.Value);
                        _cacheList.RemoveLast();
                        node.Value = key;
                    }
                    else
                    {
                        node = new LinkedListNode<TKey>(key);
                    }

                    _cacheList.AddFirst(node);
                    _cacheMap.Add(key, new Entry(node, value));
                }
                else
                {
                    entry.Value = value;
                    _cacheMap[key] = entry;
                    Touch(entry.Node);
                }
            }
        }

        private void Touch(LinkedListNode<TKey> node)
        {
            if (node != _cacheList.First)
            {
                _cacheList.Remove(node);
                _cacheList.AddFirst(node);
            }
        }

        private struct Entry
        {
            public LinkedListNode<TKey> Node;
            public TValue Value;

            public Entry(LinkedListNode<TKey> node, TValue value)
            {
                this.Node = node;
                this.Value = value;
            }
        }
    }
}
