using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using System.Threading;
namespace Jace.Util
{
    /// 
    /// An in-memory based cache to store objects. The implementation is thread safe and supports
    /// the multiple platforms supported by Jace (.NET, WinRT, WP7 and WP8).
    /// 
    /// The type of the keys.
    /// The type of the values.
    public class MemoryCache
    {
        private readonly int maximumSize;
        private readonly int reductionSize;
        private long counter; // We cannot use DateTime.Now, because the precission is not high enough.
        private readonly ConcurrentDictionary dictionary;
        /// 
        /// Create a new instance of the .
        /// 
        /// The maximum allowed number of items in the cache.
        /// The number of items to be deleted per cleanup of the cache.
        public MemoryCache(int maximumSize, int reductionSize)
        {
            if (maximumSize < 1)
                throw new ArgumentOutOfRangeException("maximumSize",
                    "The maximum allowed number of items in the cache must be at least one.");
            if (reductionSize < 1)
                throw new ArgumentOutOfRangeException("reductionSize",
                    "The cache reduction size must be at least one.");
            this.maximumSize = maximumSize;
            this.reductionSize = reductionSize;
            this.dictionary = new ConcurrentDictionary();
        }
        /// 
        /// Get the value in the cache for the given key.
        /// 
        /// The key to lookup in the cache.
        /// The value for the given key.
        public TValue this[TKey key]
        {
            get
            {
                CacheItem cacheItem = dictionary[key];
                cacheItem.Accessed();
                return cacheItem.Value;
            }
        }
        /// 
        /// Gets the number of items in the cache.
        /// 
        public int Count
        {
            get
            {
                return dictionary.Count;
            }
        }
        /// 
        /// Returns true if an item with the given key is present in the cache.
        /// 
        /// The key to lookup in the cache.
        /// True if an item is present in the cache for the given key.
        public bool ContainsKey(TKey key)
        {
            return dictionary.ContainsKey(key);
        }
        public bool TryGetValue (TKey key, out TValue result)
        {
            if (dictionary.TryGetValue(key, out var cachedItem))
            {
                cachedItem.Accessed();
                result = cachedItem.Value;
                return true;
            }
            else
            {
                result = default(TValue);
                return false;
            }
        }
        /// 
        /// If for a given key an item is present in the cache, this method will return
        /// the value for the given key. If no item is present in the cache for the given
        /// key, the valueFactory is executed to produce the value. This value is stored in
        /// the cache and returned to the caller.
        /// 
        /// The key to lookup in the cache.
        /// The factory to produce the value matching with the key.
        /// The value for the given key.
        public TValue GetOrAdd(TKey key, Func valueFactory)
        {
            if (valueFactory == null)
                throw new ArgumentNullException("valueFactory");
            CacheItem cacheItem = dictionary.GetOrAdd(key, k => 
                {
                    EnsureCacheStorageAvailable();
                    TValue value = valueFactory(k);
                    return new CacheItem(this, valueFactory(k));
                });
            return cacheItem.Value;
        }
        /// 
        /// Ensure that the cache has room for an additional item.
        /// If there is not enough room anymore, force a removal of oldest
        /// accessed items in the cache.
        /// 
        private void EnsureCacheStorageAvailable()
        {
            if (dictionary.Count >= maximumSize) // >= because we want to add an item after this method
            {
                IList keysToDelete = (from p in dictionary.ToArray()
                                            where p.Key != null && p.Value != null
                                            orderby p.Value.LastAccessed ascending
                                            select p.Key).Take(reductionSize).ToList();
                foreach (TKey key in keysToDelete)
                {
                    CacheItem cacheItem;
                    dictionary.TryRemove(key, out cacheItem);
                }
            }
        }
        private class CacheItem
        {
            private MemoryCache cache;
            public CacheItem(MemoryCache cache, TValue value)
            {
                this.cache = cache;
                this.Value = value;
                Accessed();
            }
            public TValue Value { get; private set; }
            public long LastAccessed { get; private set; }
            public void Accessed()
            {
                this.LastAccessed = Interlocked.Increment(ref cache.counter);
            }
        }
    }
}