// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

using System;
using System.Diagnostics;
using System.Threading;

#if DEBUG
using System.Runtime.CompilerServices;
using System.IO;
#endif

namespace Microsoft.CodeAnalysis.PooledObjects
{
    /// <summary>
    /// Generic implementation of object pooling pattern with predefined pool size limit. The main
    /// purpose is that limited number of frequently used objects can be kept in the pool for
    /// further recycling.
    /// 
    /// Notes: 
    /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
    ///    is no space in the pool, extra returned objects will be dropped.
    /// 
    /// 2) it is implied that if object was obtained from a pool, the caller will return it back in
    ///    a relatively short time. Keeping checked out objects for long durations is ok, but 
    ///    reduces usefulness of pooling. Just new up your own.
    /// 
    /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. 
    /// Rationale: 
    ///    If there is no intent for reusing the object, do not use pool - just use "new". 
    /// </summary>
    internal class ObjectPool<T> where T : class
    {
        [DebuggerDisplay("{Value,nq}")]
        private struct Element
        {
            internal T? Value;
        }

        /// <remarks>
        /// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
        /// which does not have that type (since it compiles against .NET 2.0).
        /// </remarks>
        internal delegate T Factory();

        // Storage for the pool objects. The first item is stored in a dedicated field because we
        // expect to be able to satisfy most requests from it.
        private T? _firstItem;
        private readonly Element[] _items;

        // factory is stored for the lifetime of the pool. We will call this only when pool needs to
        // expand. compared to "new T()", Func gives more flexibility to implementers and faster
        // than "new T()".
        private readonly Factory _factory;

        public readonly bool TrimOnFree;

#if DEBUG
        /// <summary>
        /// When false, this pool's objects are not tracked for leak detection.
        /// Used for pools where cross-thread usage causes false positive leak reports.
        /// </summary>
        private readonly bool _trackLeaks;

        /// <summary>
        /// Identifies this pool in leak reports.
        /// </summary>
        private readonly string _poolName;
#endif

        internal ObjectPool(Factory factory, bool trimOnFree = true, bool trackLeaks = true
#if DEBUG
            , [CallerFilePath] string filePath = ""
            , [CallerLineNumber] int lineNumber = 0
#endif
            )
            : this(factory, Environment.ProcessorCount * 2, trimOnFree, trackLeaks
#if DEBUG
                , filePath, lineNumber
#endif
                )
        {
        }

        internal ObjectPool(Factory factory, int size, bool trimOnFree = true, bool trackLeaks = true
#if DEBUG
            , [CallerFilePath] string filePath = ""
            , [CallerLineNumber] int lineNumber = 0
#endif
            )
        {
            Debug.Assert(size >= 1);
            _factory = factory;
            _items = new Element[size - 1];
            TrimOnFree = trimOnFree;
#if DEBUG
            _trackLeaks = trackLeaks;
            _poolName = Path.GetFileName(filePath) + ":" + lineNumber;
#endif
        }

        internal ObjectPool(Func<ObjectPool<T>, T> factory, int size
#if DEBUG
            , [CallerFilePath] string filePath = ""
            , [CallerLineNumber] int lineNumber = 0
#endif
            )
        {
            Debug.Assert(size >= 1);
            _factory = () => factory(this);
            _items = new Element[size - 1];
#if DEBUG
            _trackLeaks = true;
            _poolName = Path.GetFileName(filePath) + ":" + lineNumber;
#endif
        }

        private T CreateInstance()
        {
            var inst = _factory();
            return inst;
        }

        /// <summary>
        /// Produces an instance.
        /// </summary>
        /// <remarks>
        /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
        /// Note that Free will try to store recycled objects close to the start thus statistically 
        /// reducing how far we will typically search.
        /// </remarks>
        internal T Allocate(
#if DEBUG
            [CallerFilePath] string filePath = "",
            [CallerLineNumber] int lineNumber = 0
#endif
            )
        {
            // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
            // Note that the initial read is optimistically not synchronized. That is intentional. 
            // We will interlock only when we have a candidate. in a worst case we may miss some
            // recently returned objects. Not a big deal.
            var inst = _firstItem;
            if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst))
            {
                inst = AllocateSlow();
            }

#if DEBUG
            if (_trackLeaks)
                PoolTracker.OnAllocate(inst, _poolName, filePath, lineNumber);
#endif
            return inst;
        }

        private T AllocateSlow()
        {
            var items = _items;

            for (var i = 0; i < items.Length; i++)
            {
                // Note that the initial read is optimistically not synchronized. That is intentional. 
                // We will interlock only when we have a candidate. in a worst case we may miss some
                // recently returned objects. Not a big deal.
                var inst = items[i].Value;
                if (inst != null)
                {
                    if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst))
                    {
                        return inst;
                    }
                }
            }

            return CreateInstance();
        }

        /// <summary>
        /// Returns objects to the pool.
        /// </summary>
        /// <remarks>
        /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
        /// Note that Free will try to store recycled objects close to the start thus statistically 
        /// reducing how far we will typically search in Allocate.
        /// </remarks>
        internal void Free(T obj)
        {
            Validate(obj);
            ForgetTrackedObject(obj);

            if (_firstItem == null)
            {
                // Intentionally not using interlocked here. 
                // In a worst case scenario two objects may be stored into same slot.
                // It is very unlikely to happen and will only mean that one of the objects will get collected.
                _firstItem = obj;
            }
            else
            {
                FreeSlow(obj);
            }
        }

        private void FreeSlow(T obj)
        {
            var items = _items;
            for (var i = 0; i < items.Length; i++)
            {
                if (items[i].Value == null)
                {
                    // Intentionally not using interlocked here. 
                    // In a worst case scenario two objects may be stored into same slot.
                    // It is very unlikely to happen and will only mean that one of the objects will get collected.
                    items[i].Value = obj;
                    break;
                }
            }
        }

        /// <summary>
        /// Removes an object from leak tracking.  
        /// 
        /// This is called when an object is returned to the pool.  It may also be explicitly 
        /// called if an object allocated from the pool is intentionally not being returned
        /// to the pool.  This can be of use with pooled arrays if the consumer wants to 
        /// return a larger array to the pool than was originally allocated.
        /// </summary>
        [Conditional("DEBUG")]
        internal void ForgetTrackedObject(T old, T? replacement = null)
        {
#if DEBUG
            if (!_trackLeaks)
                return;

            PoolTracker.OnFree(old);

            if (replacement != null)
            {
                PoolTracker.OnAllocate(replacement, _poolName);
            }
#endif
        }

        [Conditional("DEBUG")]
        private void Validate(object obj)
        {
            Debug.Assert(obj != null, "freeing null?");

            Debug.Assert(_firstItem != obj, "freeing twice?");

            var items = _items;
            for (var i = 0; i < items.Length; i++)
            {
                var value = items[i].Value;
                if (value == null)
                {
                    return;
                }

                Debug.Assert(value != obj, "freeing twice?");
            }
        }
    }
}
