using System;
using System.Collections.Generic;

namespace Substrate.Core
{
    /// <summary>
    /// An LRU-based caching data structure for holding references to <see cref="ChunkRef"/> objects.
    /// </summary>
    /// <remarks>The <see cref="ChunkCache"/> class maintains a separate dictionary on the side for tracking
    /// dirty chunks.  References to dirty chunks will still be held even if the chunk has been evicted from
    /// the normal LRU cache.  The dirty list is reset when the world is saved, or manually cleared.</remarks>
    public class ChunkCache
    {
        private LRUCache<ChunkKey, ChunkRef> _cache;
        private Dictionary<ChunkKey, ChunkRef> _dirty;

        /// <summary>
        /// Creates a new <see cref="ChunkCache"/> with the default capacity of 256 chunks.
        /// </summary>
        public ChunkCache ()
            : this(256)
        {
        }

        /// <summary>
        /// Creates a new <see cref="ChunkCache"/> with the given chunk capacity.
        /// </summary>
        /// <param name="cacheSize">The capacity of the LRU-portion of the cache.</param>
        public ChunkCache (int cacheSize)
        {
            _cache = new LRUCache<ChunkKey, ChunkRef>(cacheSize);
            _dirty = new Dictionary<ChunkKey, ChunkRef>();

            _cache.RemoveCacheValue += EvictionHandler;
        }

        #region IChunkCache Members

        /// <summary>
        /// Inserts a new chunk into the cache.
        /// </summary>
        /// <param name="chunk">The <see cref="ChunkRef"/> to add to the cache.</param>
        /// <returns><c>True</c> if the chunk did not already exist in the cache, <c>false</c> otherwise.</returns>
        /// <remarks>If the chunk does not already exist and the list is full, then the least-recently used chunk in the cache will be evicted 
        /// to make room for the new chunk.  If the chunk is present in the dirty list, it will be removed.</remarks>
        public bool Insert (ChunkRef chunk)
        {
            ChunkKey key = new ChunkKey(chunk.X, chunk.Z);

            _dirty.Remove(key);

            ChunkRef c;
            if (!_cache.TryGetValue(key, out c)) {
                _cache[key] = chunk;
                return true;
            }

            return false;
        }

        /// <summary>
        /// Removes a chunk from the cache.
        /// </summary>
        /// <param name="key">The key identifying which <see cref="ChunkRef"/> to try and remove.</param>
        /// <returns><c>True</c> if the chunk was in the LRU-portion of the cache and removed, <c>False</c> otherwise.</returns>
        /// <remarks>The chunk will also be removed from the dirty list, if it is currently in it.</remarks>
        public bool Remove (ChunkKey key)
        {
            _dirty.Remove(key);
            return _cache.Remove(key);
        }

        /// <summary>
        /// Attempts to get a chunk from the LRU- or dirty-portions of the cache.
        /// </summary>
        /// <param name="key">The key identifying which <see cref="ChunkRef"/> to find.</param>
        /// <returns>The cached <see cref="ChunkRef"/> if it was found anywhere in the cache, or <c>null</c> if it was not found.</returns>
        /// <remarks>If the <see cref="ChunkRef"/> is found in the LRU-portion of the cache, it will be moved to the front of the
        /// LRU list, making future eviction less likely.</remarks>
        public ChunkRef Fetch (ChunkKey key)
        {
            ChunkRef c;
            if (_dirty.TryGetValue(key, out c)) {
                return c;
            }

            if (_cache.TryGetValue(key, out c)) {
                return c;
            }

            return null;
        }

        /// <summary>
        /// Gets an enumerator to iterate over all of the <see cref="ChunkRef"/> objects currently in the dirty list.
        /// </summary>
        /// <returns>An enumerator over all of the dirty <see cref="ChunkRef"/> objects.</returns>
        public IEnumerator<ChunkRef> GetDirtyEnumerator ()
        {
            return _dirty.Values.GetEnumerator();
        }

        /// <summary>
        /// Clears all chunks from the LRU list.
        /// </summary>
        /// <remarks>This method will clear all chunks from the LRU-portion of the cache, including any chunks that are
        /// dirty but have not yet been discovered and added to the dirty list.  Chunks already in the dirty list will
        /// not be affected.  To clear dirty chunks as well, see <see cref="ClearDirty"/>.</remarks>
        public void Clear ()
        {
            _cache.Clear();
        }

        /// <summary>
        /// Clears all chunks from the dirty list.
        /// </summary>
        public void ClearDirty ()
        {
            _dirty.Clear();
        }

        /// <summary>
        /// Scans the LRU list for any dirty chunks, and adds them to the dirty list.
        /// </summary>
        public void SyncDirty ()
        {
            foreach (KeyValuePair<ChunkKey, ChunkRef> e in _cache) {
                if (e.Value.IsDirty) {
                    _dirty[e.Key] = e.Value;
                }
            }
        }


        private void EvictionHandler (object sender, LRUCache<ChunkKey, ChunkRef>.CacheValueArgs e)
        {
            if (e.Value.IsDirty) {
                _dirty[e.Key] = e.Value;
            }
        }

        #endregion
    }
}