// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.Common.Utils
{
    using System.Collections.Generic;
    using System.Data.Entity.Utilities;
    using System.Diagnostics.CodeAnalysis;
    using System.Threading;

    // <summary>
    // Remembers the result of evaluating an expensive function so that subsequent
    // evaluations are faster. Thread-safe.
    // </summary>
    // <typeparam name="TArg"> Type of the argument to the function. </typeparam>
    // <typeparam name="TResult"> Type of the function result. </typeparam>
    [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
    internal sealed class Memoizer<TArg, TResult>
    {
        private readonly Func<TArg, TResult> _function;
        private readonly Dictionary<TArg, Result> _resultCache;
        private readonly ReaderWriterLockSlim _lock;

        // <summary>
        // Constructs
        // </summary>
        // <param name="function"> Required. Function whose values are being cached. </param>
        // <param name="argComparer"> Optional. Comparer used to determine if two functions arguments are the same. </param>
        internal Memoizer(Func<TArg, TResult> function, IEqualityComparer<TArg> argComparer)
        {
            DebugCheck.NotNull(function);

            _function = function;
            _resultCache = new Dictionary<TArg, Result>(argComparer);
            _lock = new ReaderWriterLockSlim();
        }

        // <summary>
        // Evaluates the wrapped function for the given argument. If the function has already
        // been evaluated for the given argument, returns cached value. Otherwise, the value
        // is computed and returned.
        // </summary>
        // <param name="arg"> Function argument. </param>
        // <returns> Function result. </returns>
        internal TResult Evaluate(TArg arg)
        {
            Result result;

            // Check to see if a result has already been computed
            if (!TryGetResult(arg, out result))
            {
                // compute the new value
                _lock.EnterWriteLock();
                try
                {
                    // see if the value has been computed in the interim
                    if (!_resultCache.TryGetValue(arg, out result))
                    {
                        result = new Result(() => _function(arg));
                        _resultCache.Add(arg, result);
                    }
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }

            // note: you need to release the global cache lock before (potentially) acquiring
            // a result lock in result.GetValue()
            return result.GetValue();
        }

        internal bool TryGetValue(TArg arg, out TResult value)
        {
            Result result;
            if (TryGetResult(arg, out result))
            {
                value = result.GetValue();
                return true;
            }
            else
            {
                value = default(TResult);
                return false;
            }
        }

        private bool TryGetResult(TArg arg, out Result result)
        {
            _lock.EnterReadLock();
            try
            {
                return _resultCache.TryGetValue(arg, out result);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        // <summary>
        // Encapsulates a 'deferred' result. The result is constructed with a delegate (must not
        // be null) and when the user requests a value the delegate is invoked and stored.
        // </summary>
        private class Result
        {
            private TResult _value;
            private Func<TResult> _delegate;

            internal Result(Func<TResult> createValueDelegate)
            {
                DebugCheck.NotNull(createValueDelegate);
                _delegate = createValueDelegate;
            }

            internal TResult GetValue()
            {
                if (null == _delegate)
                {
                    // if the delegate has been cleared, it means we have already computed the value
                    return _value;
                }

                // lock the entry while computing the value so that two threads
                // don't simultaneously do the work
                lock (this)
                {
                    if (null == _delegate)
                    {
                        // between our initial check and our acquisition of the lock, some other
                        // thread may have computed the value
                        return _value;
                    }
                    _value = _delegate();

                    // ensure _delegate (and its closure) is garbage collected, and set to null
                    // to indicate that the value has been computed
                    _delegate = null;
                    return _value;
                }
            }
        }
    }
}
