using J2N.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Lucene.Net.Facet.Taxonomy.WriterCache
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /// <summary>
    /// An an LRU cache of mapping from name to int.
    /// Used to cache Ordinals of category paths.
    /// <para/>
    /// NOTE: This was NameIntCacheLRU in Lucene
    /// <para/>
    /// @lucene.experimental
    /// </summary>
    /// <remarks>
    /// Note: Nothing in this class is synchronized. The caller is assumed to be
    /// synchronized so that no two methods of this class are called concurrently.
    /// </remarks>
    public class NameInt32CacheLru : IInternalNameInt32CacheLru // LUCENENET specific - added interface
    {
        private readonly IInternalNameInt32CacheLru cache;
        internal NameInt32CacheLru(int limit)
        {
            this.cache = new NameCacheLru<FacetLabel>(
                limit,
                (name) => name,
                (name, prefixLength) => name.Subpath(prefixLength));
        }

        /// <inheritdoc/>
        public int Count => cache.Count;

        /// <inheritdoc/>
        public int Limit => cache.Limit;

        /// <inheritdoc/>
        string IInternalNameInt32CacheLru.Stats => cache.Stats;

        /// <inheritdoc/>
        void IInternalNameInt32CacheLru.Clear() => cache.Clear();

        /// <inheritdoc/>
        bool IInternalNameInt32CacheLru.MakeRoomLRU() => cache.MakeRoomLRU();

        /// <inheritdoc/>
        bool IInternalNameInt32CacheLru.Put(FacetLabel name, int val) => cache.Put(name, val);

        /// <inheritdoc/>
        bool IInternalNameInt32CacheLru.Put(FacetLabel name, int prefixLen, int val) => cache.Put(name, prefixLen, val);

        /// <inheritdoc/>
        bool IInternalNameInt32CacheLru.TryGetValue(FacetLabel name, out int value) => cache.TryGetValue(name, out value);
    }

    /// <summary>
    /// Public members of the <see cref="NameInt32CacheLru"/> that are shared between instances.
    /// </summary>
    public interface INameInt32CacheLru
    {
        /// <summary>
        /// Number of entries currently in the cache.
        /// </summary>
        int Count { get; }

        /// <summary>
        /// Maximum number of cache entries before eviction.
        /// </summary>
        int Limit { get; }

    }

    // LUCENENET specific - defined interface so we can share public and internal members betwen cache
    // instances.
    internal interface IInternalNameInt32CacheLru : INameInt32CacheLru
    {
        string Stats { get; }

        void Clear();

        /// <summary>
        /// If cache is full remove least recently used entries from cache. Return <c>true</c>
        /// if anything was removed, <c>false</c> otherwise.
        /// <para/>
        /// See comment in <see cref="Directory.DirectoryTaxonomyWriter.AddToCache(FacetLabel, int)"/> for an
        /// explanation why we clean 2/3rds of the cache, and not just one entry.
        /// </summary>
        bool MakeRoomLRU();

        /// <summary>
        /// Add a new value to cache.
        /// Return true if cache became full and some room need to be made. 
        /// </summary>
        bool Put(FacetLabel name, int val);

        bool Put(FacetLabel name, int prefixLen, int val);

        bool TryGetValue(FacetLabel name, out int value);
    }

    /// <summary>
    /// Class for name LRU caches. Users can specify
    /// a type that can be used as the key of the cache. The
    /// key may be either a value type or a reference type.
    /// </summary>
    /// <typeparam name="TName">The type of key for the cache.</typeparam>
    // LUCENENET specific - extracted the logic out of NameInt32CacheLru to make
    // it generic so boxing/unboxing can be avoided during lookups, while also
    // keeping the generic closing type from being exposed on the public API.
    internal sealed class NameCacheLru<TName> : IInternalNameInt32CacheLru
    {
        private IDictionary<TName, int> cache;
        internal long nMisses = 0; // for debug
        internal long nHits = 0; // for debug
        private readonly int maxCacheSize;
        private readonly object syncLock = new object(); // LUCENENET specific so we don't lock this
        private readonly Func<FacetLabel, TName> getKey;
        private readonly Func<FacetLabel, int, TName> getKeyWithPrefixLength;

        internal NameCacheLru(int limit, Func<FacetLabel, TName> getKey, Func<FacetLabel, int, TName> getKeyWithPrefixLength)
        {
            this.getKey = getKey ?? throw new ArgumentNullException(nameof(getKey));
            this.getKeyWithPrefixLength = getKeyWithPrefixLength ?? throw new ArgumentNullException(nameof(getKeyWithPrefixLength));
            this.maxCacheSize = limit;
            CreateCache(limit);
        }

        /// <summary>
        /// Maximum number of cache entries before eviction.
        /// </summary>
        public int Limit => maxCacheSize;

        /// <summary>
        /// Number of entries currently in the cache.
        /// </summary>
        public int Count => cache.Count;

        private void CreateCache(int maxSize)
        {
            if (maxSize < int.MaxValue)
            {
                cache = new LurchTable<TName, int>(capacity: 1000, ordering: LurchTableOrder.Access); //for LRU
            }
            else
            {
                // LUCENENET specific - we use ConcurrentDictionary here because it supports deleting while
                // iterating through the collection, but Dictionary does not.
                cache = new ConcurrentDictionary<TName, int>(concurrencyLevel: 3, capacity: 1000); //no need for LRU
            }
        }

        bool IInternalNameInt32CacheLru.TryGetValue(FacetLabel name, out int value) // LUCENENET specific - use TryGetValue() instead of Get()
        {
            if (!cache.TryGetValue(getKey(name), out value))
            {
                nMisses++;
                return false;
            }
            else
            {
                nHits++;
                return true;
            }
        }

        // LUCENENET: No need for Key() functions, they are passed in as delegates through the constructor.

        /// <summary>
        /// Add a new value to cache.
        /// Return true if cache became full and some room need to be made. 
        /// </summary>
        bool IInternalNameInt32CacheLru.Put(FacetLabel name, int val)
        {
            cache[getKey(name)] = val;
            return IsCacheFull;
        }

        bool IInternalNameInt32CacheLru.Put(FacetLabel name, int prefixLen, int val)
        {
            cache[getKeyWithPrefixLength(name, prefixLen)] = val;
            return IsCacheFull;
        }

        private bool IsCacheFull => cache.Count > maxCacheSize;

        void IInternalNameInt32CacheLru.Clear() => cache.Clear();

        string IInternalNameInt32CacheLru.Stats => "#miss=" + nMisses + " #hit=" + nHits;

        /// <summary>
        /// If cache is full remove least recently used entries from cache. Return <c>true</c>
        /// if anything was removed, <c>false</c> otherwise.
        /// <para/>
        /// See comment in <see cref="Directory.DirectoryTaxonomyWriter.AddToCache(FacetLabel, int)"/> for an
        /// explanation why we clean 2/3rds of the cache, and not just one entry.
        /// </summary>
        bool IInternalNameInt32CacheLru.MakeRoomLRU()
        {
            if (!IsCacheFull)
            {
                return false;
            }
            int n = cache.Count - (2 * maxCacheSize) / 3;
            if (n <= 0)
            {
                return false;
            }

            lock (syncLock)
            {
                // Double-check that another thread didn't beat us to the operation
                n = cache.Count - (2 * maxCacheSize) / 3;
                if (n <= 0)
                {
                    return false;
                }

                //System.Diagnostics.Debug.WriteLine("Removing cache entries in MakeRoomLRU");
                using var it = cache.GetEnumerator();
                int i = 0;
                while (i < n && it.MoveNext())
                {
                    cache.Remove(it.Current.Key);
                    i++;
                }
            }
            return true;
        }
    }
}